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 layer_map: Option<Vec<usize>>,
147 pub mhc: Option<Box<crate::sensenova::Mhc>>,
149 mhc_r: Vec<f32>,
151 pub mrope_sections: Option<[usize; 3]>,
155 pub prompt_mrope: Option<Vec<[i64; 3]>>,
156 pub vocab_size: usize,
157 pub rms_eps: f64,
158 pub rope_base: f32,
159 pub norm_style: NormStyle,
160 pub rotary_dim: usize,
162 pub attention_heads_per_layer: Option<Vec<usize>>,
164 pub kv_heads_per_layer: Option<Vec<usize>>,
169 pub v_head_dim: Option<usize>,
174 pub layer_dump: Option<std::path::PathBuf>,
187 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
190 pub(crate) mimo_moe: crate::mimo_moe::Slot,
193 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
198 pub vmf_cfg: Option<VmfPhaseCfg>,
200 pub gdn_cfg: Option<GdnCfg>,
202 pub logit_multiplier: Option<f32>,
204 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
209 graph_failed: std::sync::atomic::AtomicBool,
214 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
217 pub kv_history: Vec<u32>,
222 pub kv_history_device: bool,
225 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
227 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
230 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
233 pub dsv4: Option<
237 Box<(
238 crate::dsv4::Dsv4Globals,
239 Vec<crate::dsv4::Dsv4Layer>,
240 crate::dsv4::Dsv4Cfg,
241 crate::dsv4::Dsv4State,
242 )>,
243 >,
244 pub dsv41: Option<
248 Box<(
249 crate::dsv41::Dsv41Globals,
250 Vec<crate::dsv41::Dsv41Layer>,
251 crate::dsv41::Dsv41Cfg,
252 crate::dsv41::Dsv41State,
253 )>,
254 >,
255 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
257 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
259 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
264 clover_stats: clover::CloverStats,
265 clover_pen: clover::Penalties,
267 pub qwen4_exp: Option<
268 Box<(
269 crate::qwen4_exp::Globals,
270 Vec<crate::qwen4_exp::Layer>,
271 crate::qwen4_exp::Cfg,
272 crate::qwen4_exp::State,
273 )>,
274 >,
275 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
279 pub dspark: Option<crate::dsv4::DsparkState>,
281 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
284 pub dspark_hist: Vec<usize>,
286 pub dspark_real: Vec<u32>,
290 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
294 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
296 pub dspark_draft_ns: u128,
300 m3_winners: Vec<Vec<u32>>,
302 m3_counts: Vec<u32>,
304 m3_n_exp: usize,
305 m3_tokens: u64,
306 m3_prewarmed: bool,
307 pub short_conv_cfg: Option<ShortConvCfg>,
310 pub mtp: Option<MtpModule>,
312 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
316 verify_exact_moe: bool,
319 pub speculative: bool,
321 pub ignore_eos: bool,
327 pub draft_full_streak: u32,
333 pub spec_k_adapt: Option<usize>,
341 pub spec_acc_ewma: f32,
343 rng: SplitMix64,
344 sampler_scratch: SamplerScratch,
345 spec_forced: Option<u32>,
351 spec_q: Vec<Vec<f32>>,
352 spec_p: Vec<f32>,
353 spec_res: Vec<f32>,
354 spec_qs: Vec<sampler::Sparse>,
356 spec_ps: sampler::Sparse,
357 spec_ress: sampler::Sparse,
358 mtp_graph_mode: Option<bool>,
365 #[cfg(target_os = "macos")]
368 metal_verify: Option<MetalVerifyPending>,
369 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
373 ws: ForwardScratch,
377 pool: Option<std::sync::Arc<Pool>>,
379 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
383 pub(crate) dyn_force_f32: bool,
385 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
390 pub(crate) dyn_active: Option<usize>,
396 pub(crate) dyn_blend_loaded: bool,
400 pub(crate) dyn_phi_layer: Option<usize>,
403 dyn_phi_ema: Vec<f32>,
405 dyn_phi_seen: usize,
406 pub dyn_router: Option<crate::swarm::DynRouter>,
409 o1_cfg: Option<crate::nystrom::O1Cfg>,
412 o1_epoch: u64,
415 o1_flags: Vec<bool>,
417 trace: bool,
420 calib_temp: f32,
423 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
425 graph_kv_id: u64,
426 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
429 graph_want_logits: bool,
430 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
434 graph_head_required: bool,
435 graph_logits: Option<Vec<f32>>,
438 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
441 graph_refused: std::sync::atomic::AtomicBool,
447 pub embed_multiplier: f32,
449 pub attn_scale: f32,
452 pub swa: Option<(usize, usize)>,
455 pub swa_trim: Option<(usize, usize)>,
462 swa_trim_anchor: usize,
466 pub chunk_min_rows: usize,
470 pub sliding_layers: Option<Vec<bool>>,
473 pub no_rope_layers: Option<Vec<bool>>,
478 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
484 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
487 pub kv_prefix: KvPrefix,
491 pub last_prefill_tokens: usize,
494 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
497 pub rotary_dim_local: Option<usize>,
498 pub rope_scale: f32,
499 pub rope_scale_local: f32,
500 pub global_attn: Option<(usize, usize)>,
503 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
506 pub attn_v_norm: bool,
508 pub qk_norm_after_rope: bool,
510 pub proj_gate_sigmoid: bool,
513 pub final_softcap: Option<f32>,
515 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
519 pub attn_softcap: f32,
521 confidence_on: bool,
525 #[cfg(test)]
528 nll_test_fail_at: Option<usize>,
529 #[cfg(test)]
532 nll_test_force_serial: bool,
533}
534
535#[cfg(target_os = "macos")]
536impl Drop for Pipeline {
537 fn drop(&mut self) {
538 let _ = crate::gpu_metal::wait_replay();
540 crate::gpu::kv_mirror_drop(self.graph_kv_id);
541 }
542}
543
544#[cfg(not(target_os = "macos"))]
545impl Drop for Pipeline {
546 fn drop(&mut self) {
547 crate::gpu::graph_kv_reset(self.graph_kv_id);
551 }
552}
553
554pub struct PipelineWeights {
559 pub embed_tokens: QTensor,
561 pub layers: Vec<LayerWeights>,
563 pub lm_head: QTensor,
565 pub final_norm: Vec<f32>,
567}
568
569pub struct LayerWeights {
571 pub input_norm: Vec<f32>,
572 pub post_norm: Vec<f32>,
575 pub attn_out_norm: Option<Vec<f32>>,
578 pub layer_scale: Option<f32>,
580 pub ffn_out_norm: Option<Vec<f32>>,
583 pub ffn: FfnKind,
584 pub attn: AttnKind,
585}
586
587#[derive(Clone, Copy, PartialEq, Debug, Default)]
591pub enum Act {
592 #[default]
593 Silu,
594 GeluTanh,
595 Gelu,
597 Situ {
600 beta: f32,
601 linear_beta: f32,
602 },
603 SwiGluOai {
608 alpha: f32,
609 limit: f32,
610 },
611}
612
613impl Act {
614 pub fn from_arch(name: &str) -> Self {
615 if name == "gelu_tanh" {
616 Self::GeluTanh
617 } else if name == "gelu" {
618 Self::Gelu
619 } else {
620 Self::Silu
621 }
622 }
623
624 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
626 match arch.hidden_act.as_str() {
627 "situ" => Self::Situ {
628 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
629 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
630 },
631 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
636 alpha: 1.702,
637 limit: 7.0,
638 },
639 other => Self::from_arch(other),
640 }
641 }
642
643 #[inline]
648 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
649 Self::SwiGluOai { alpha, limit }
650 }
651
652 #[inline]
653 pub fn apply(self, x: f32) -> f32 {
654 match self {
655 Self::Silu => inference::silu(x),
656 Self::GeluTanh => inference::gelu_tanh(x),
657 Self::Gelu => inference::gelu_erf(x),
658 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
659 Self::SwiGluOai { alpha, limit } => {
660 let gate = x.min(limit);
661 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
662 }
663 }
664 }
665
666 #[inline]
669 pub fn combine(self, g: f32, u: f32) -> f32 {
670 match self {
671 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
672 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
673 }
674 Self::SwiGluOai { alpha, limit } => {
675 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
676 }
677 _ => self.apply(g) * u,
678 }
679 }
680
681 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
685 match self {
686 Self::Silu => Some(crate::gpu::GraphAct::Silu),
687 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
688 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
689 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
690 }
691 }
692
693 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
699 self.moe_job_graph_contract().map(|(act, _)| act)
700 }
701
702 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
708 match self {
709 Self::SwiGluOai { alpha, limit }
710 if alpha.to_bits() == 1.702f32.to_bits()
711 && limit.to_bits() == 7.0f32.to_bits() =>
712 {
713 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
714 }
715 _ => self.graph_act().map(|act| (act, 0.0)),
716 }
717 }
718}
719
720pub struct DenseFfn {
722 pub gate_proj: QTensor,
723 pub up_proj: QTensor,
724 pub down_proj: QTensor,
725 pub act: Act,
727 pub down_t: Option<QTensor>,
733 pub segs: Vec<FfnSeg>,
740}
741
742pub struct FfnSeg {
747 pub gate: QTensor,
748 pub up: QTensor,
749 pub down: QTensor,
750 pub start: usize,
751 pub width: usize,
752}
753
754pub enum FfnKind {
757 Dense(DenseFfn),
758 Moe(MoeFfn),
762 DenseMoe(Box<DenseMoeFfn>),
769}
770
771pub struct DenseMoeFfn {
773 pub dense: DenseFfn,
774 pub moe: MoeFfn,
775 pub post_norm_1: Vec<f32>,
777 pub pre_norm_2: Vec<f32>,
780 pub post_norm_2: Vec<f32>,
782}
783
784pub struct MoeFfn {
785 pub router: QTensor,
787 pub experts: Vec<DenseFfn>,
788 pub top_k: usize,
789 pub norm_topk_prob: bool,
790 pub router_sigmoid: bool,
793 pub sigmoid_denom_eps: f32,
799 pub expert_bias: Option<Vec<f32>>,
803 pub routed_scaling: f32,
806 pub route_tau: Option<f32>,
812 pub shared: Option<(DenseFfn, Option<QTensor>)>,
815 pub stats: std::cell::RefCell<Vec<u64>>,
819 pub act_sq: std::cell::RefCell<Vec<f64>>,
826 pub act_rows: std::cell::RefCell<Vec<f32>>,
832 pub mask: Option<Vec<bool>>,
837 pub per_expert_scale: Option<Vec<f32>>,
840 pub router_input_norm: bool,
844 pub resonance: Option<Resonance>,
848 pub grown: Vec<GrownExpert>,
854}
855
856#[derive(Debug, Clone, PartialEq, Eq)]
858pub struct GrownExpert {
859 pub record: String,
861 pub record_index: usize,
863 pub layer: usize,
864 pub expert: usize,
868}
869
870pub struct Resonance {
872 pub mu: Vec<f32>,
874 pub u: Vec<f32>,
876 pub k: usize,
877 pub bias: Vec<f32>,
879 pub shell: Vec<f32>,
885}
886
887static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
890
891pub fn growth_shell_enabled() -> bool {
896 use std::sync::atomic::Ordering;
897 match GROWTH_SHELL.load(Ordering::Relaxed) {
898 1 => true,
899 2 => false,
900 _ => {
901 let off = std::env::var("CMF_GROWTH_SHELL")
902 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
903 .unwrap_or(false);
904 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
905 !off
906 }
907 }
908}
909
910pub fn set_growth_shell(on: Option<bool>) {
915 GROWTH_SHELL.store(
916 match on {
917 Some(true) => 1,
918 Some(false) => 2,
919 None => 0,
920 },
921 std::sync::atomic::Ordering::Relaxed,
922 );
923}
924
925impl Resonance {
926 pub fn has_shell(&self) -> bool {
928 self.shell.iter().any(|s| s.is_finite())
929 }
930
931 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
934 let mut out = vec![f32::INFINITY; ne];
935 if growth_shell_enabled() {
936 for (o, s) in out.iter_mut().zip(&self.shell) {
937 *o = *s;
938 }
939 }
940 out
941 }
942
943 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
948 let h = x.len();
949 let ne = out.len();
950 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
951 for e in 0..ne {
952 let mu = &self.mu[e * h..(e + 1) * h];
953 let mut d2 = 0.0f32;
954 for j in 0..h {
955 let d = x[j] - mu[j];
956 d2 += d * d;
957 }
958 let mut proj = 0.0f32;
959 for i in 0..self.k {
960 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
961 let mut p = 0.0f32;
962 for j in 0..h {
963 p += (x[j] - mu[j]) * u[j];
964 }
965 proj += p * p;
966 }
967 let err = d2 - proj;
968 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
969 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
970 out[e] = f32::NEG_INFINITY;
971 }
972 }
973 }
974}
975
976pub enum AttnKind {
979 Full {
981 wq: QTensor,
982 wk: QTensor,
983 wv: QTensor,
984 wo: QTensor,
985 q_norm: Option<Vec<f32>>,
986 k_norm: Option<Vec<f32>>,
987 output_gate: bool,
988 softplus_gate: Option<(QTensor, bool)>,
992 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
994 },
995 Linear(VmfPhaseWeights),
997 LinearGdn(GdnWeights),
999 ShortConv(ShortConvWeights),
1002 Mla(Box<MlaWeights>),
1010 Kda(Box<crate::linear_core::KdaWeights>),
1014 Bounded(Box<crate::bounded::BoundedWeights>),
1019}
1020
1021pub struct MlaWeights {
1023 pub q_proj: QTensor,
1027 pub q_a: Option<QTensor>,
1030 pub q_a_norm: Option<Vec<f32>>,
1031 pub kv_a: QTensor,
1033 pub kv_a_norm: Vec<f32>,
1035 pub kv_b: QTensor,
1037 pub o_proj: QTensor,
1039 pub nh: usize,
1040 pub qk_rope: usize,
1041 pub qk_nope: usize,
1042 pub v_dim: usize,
1043 pub lora: usize,
1044 pub scale: f32,
1046 pub nope: bool,
1048}
1049
1050pub struct MtpModule {
1055 pub enorm: Vec<f32>,
1056 pub hnorm: Vec<f32>,
1057 pub eh_proj: QTensor,
1059 pub layer: LayerWeights,
1060 pub final_norm: Vec<f32>,
1061 pub kv: crate::kv_cache::LayerKvCache,
1062}
1063
1064const MTP_PROBE_FFN_ROWS: usize = 128;
1073
1074#[derive(Clone, Debug, PartialEq)]
1079pub struct MtpFfnProbe {
1080 pub mass: Vec<f64>,
1081 pub squared_mass: Vec<f64>,
1082 pub observed_positions: usize,
1083}
1084
1085#[cfg(target_os = "macos")]
1092enum MetalRowsItem<'a> {
1093 Gdn {
1094 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1095 first: usize,
1096 },
1097 Attn {
1098 l: crate::gpu_metal::AttnGpuLayer<'a>,
1099 li: usize,
1100 q_norm: Option<&'a [f32]>,
1101 k_norm: Option<&'a [f32]>,
1102 output_gate: bool,
1103 },
1104}
1105
1106#[cfg(target_os = "macos")]
1107struct MetalVerifyPending {
1108 graph: crate::gpu_metal::VerifyGraph,
1109 gdn_layers: Vec<usize>,
1110 attn_layers: Vec<(usize, usize)>,
1111}
1112
1113#[cfg(target_os = "macos")]
1117struct MetalWarmPending {
1118 graph: crate::gpu_metal::VerifyGraph,
1119 cpu_stored: usize,
1120 b: usize,
1121}
1122
1123#[cfg(target_os = "macos")]
1124enum MetalRowsRun {
1125 Declined,
1127 Failed,
1130 Completed(MetalVerifyPending),
1131}
1132
1133#[cfg(target_os = "macos")]
1134enum MetalPrefillOutcome {
1135 Declined,
1136 Failed,
1137 Completed(Vec<f32>),
1138}
1139
1140#[cfg(target_os = "macos")]
1141enum MetalBatchNllOutcome {
1142 Declined,
1143 Failed(String),
1144 Completed(f64, usize),
1145}
1146
1147#[derive(Clone, Copy)]
1153enum SpecTrial {
1154 Spec {
1155 t0: std::time::Instant,
1156 gen0: usize,
1157 rounds: usize,
1158 },
1159 Plain {
1160 t0: std::time::Instant,
1161 gen0: usize,
1162 },
1163 Decided {
1164 spec: bool,
1165 recheck_at: usize,
1166 },
1167}
1168
1169pub(crate) fn spec_time_level() -> u8 {
1173 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1174 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1175 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1176 Err(_) => 0,
1177 })
1178}
1179
1180fn graph_moe_bank_on() -> bool {
1183 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1184 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1185}
1186
1187fn graph_spec_wanted_by_policy(
1195 spec_env: Option<&str>,
1196 spec_default_ok: bool,
1197 penalized: bool,
1198 metal_wgpu: bool,
1199 native_metal: bool,
1200) -> bool {
1201 match spec_env {
1202 Some("0") => false,
1203 Some("1") => true,
1208 Some(_) => !native_metal,
1209 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1212 }
1213}
1214
1215struct SpecStampLog {
1221 t_last: std::time::Instant,
1222 items: Vec<(&'static str, f32)>,
1223}
1224
1225static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1226
1227pub(crate) fn spec_stamp(name: &'static str) {
1228 if spec_time_level() == 0 {
1229 return;
1230 }
1231 if let Ok(mut g) = SPEC_STAMPS.lock() {
1232 if let Some(log) = g.as_mut() {
1233 let now = std::time::Instant::now();
1234 log.items
1235 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1236 log.t_last = now;
1237 }
1238 }
1239}
1240
1241fn spec_stamps_begin() {
1242 if spec_time_level() == 0 {
1243 return;
1244 }
1245 if let Ok(mut g) = SPEC_STAMPS.lock() {
1246 *g = Some(SpecStampLog {
1247 t_last: std::time::Instant::now(),
1248 items: Vec::with_capacity(64),
1249 });
1250 }
1251}
1252
1253fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1254 SPEC_STAMPS
1255 .lock()
1256 .ok()
1257 .and_then(|mut g| g.take())
1258 .map(|l| l.items)
1259 .unwrap_or_default()
1260}
1261
1262fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1265 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1266 for &(n, ms) in items {
1267 match agg.iter_mut().find(|e| e.0 == n) {
1268 Some(e) => {
1269 e.1 += ms;
1270 e.2 += 1;
1271 }
1272 None => agg.push((n, ms, 1)),
1273 }
1274 }
1275 let mut s = String::with_capacity(agg.len() * 16);
1276 for (n, ms, k) in agg {
1277 if k > 1 {
1278 s.push_str(&format!("{n} {ms:.1}/{k} "));
1279 } else {
1280 s.push_str(&format!("{n} {ms:.1} "));
1281 }
1282 }
1283 s
1284}
1285
1286#[derive(Default, Clone, Copy)]
1308struct SpecMon {
1309 round_ms: f64,
1310 tokens: f64,
1311 plain_ms: f64,
1312 n: u32,
1313 fails: u32,
1314 zero_accept_rounds: u8,
1319 metal: bool,
1320}
1321
1322const SPEC_PROXY_TOKENS: f64 = 3.5;
1325const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1328
1329impl SpecMon {
1330 fn round(&mut self, dt_ms: f64, produced: usize) {
1331 if produced <= 1 {
1332 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1333 } else {
1334 self.zero_accept_rounds = 0;
1335 }
1336 self.n += 1;
1337 if self.n == 1 {
1338 return; }
1340 let a = if self.n == 2 { 1.0 } else { 0.3 };
1341 self.round_ms += a * (dt_ms - self.round_ms);
1342 self.tokens += a * (produced as f64 - self.tokens);
1343 }
1344 fn pays(&self) -> bool {
1345 if self.plain_ms > 0.0 {
1346 self.tokens * self.plain_ms > self.round_ms * 1.03
1347 } else {
1348 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1349 }
1350 }
1351 fn repeated_zero_acceptance(&self) -> bool {
1352 self.zero_accept_rounds >= 2
1353 }
1354 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1356 let n = generated.saturating_sub(gen0);
1357 if n >= 8 {
1358 return true;
1359 }
1360 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1361 }
1362}
1363
1364pub const KV_PREFIX_TAIL: usize = 128;
1367
1368#[derive(Debug, Clone, Default)]
1374pub struct KvPrefix {
1375 len: usize,
1376 hash: u64,
1377 tail: Vec<u32>,
1378 device: bool,
1383}
1384
1385impl KvPrefix {
1386 #[inline]
1387 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1388 for &id in ids {
1389 h ^= id as u64;
1390 h = h.wrapping_mul(0x100000001b3);
1391 h ^= h >> 29;
1392 }
1393 h
1394 }
1395
1396 pub fn clear(&mut self) {
1397 self.len = 0;
1398 self.hash = 0xcbf29ce484222325;
1399 self.tail.clear();
1400 self.device = false;
1401 }
1402
1403 pub fn on_device(&self) -> bool {
1405 self.device
1406 }
1407
1408 pub fn set_on_device(&mut self, device: bool) {
1410 self.device = device;
1411 }
1412
1413 pub fn len(&self) -> usize {
1415 self.len
1416 }
1417
1418 pub fn is_empty(&self) -> bool {
1419 self.len == 0
1420 }
1421
1422 pub fn tail_len(&self) -> usize {
1424 self.tail.len()
1425 }
1426
1427 pub fn set(&mut self, ids: &[u32]) {
1429 self.clear();
1430 self.extend(ids);
1431 }
1432
1433 pub fn extend(&mut self, more: &[u32]) {
1435 if self.len == 0 && self.hash == 0 {
1436 self.hash = 0xcbf29ce484222325;
1437 }
1438 self.hash = Self::fold(self.hash, more);
1439 self.len += more.len();
1440 if more.len() >= KV_PREFIX_TAIL {
1441 self.tail.clear();
1442 self.tail
1443 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1444 } else {
1445 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1446 self.tail.drain(..drop);
1447 self.tail.extend_from_slice(more);
1448 }
1449 }
1450
1451 pub fn extension(&self, ids: &[u32]) -> usize {
1455 if self.len == 0 || ids.len() <= self.len {
1456 return 0;
1457 }
1458 let t = self.tail.len();
1459 if ids[self.len - t..self.len] != self.tail[..] {
1460 return 0;
1461 }
1462 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1463 return 0;
1464 }
1465 self.len
1466 }
1467}
1468
1469pub struct GenerateResult {
1471 pub text: String,
1472 pub token_ids: Vec<u32>,
1473 pub prompt_tokens: usize,
1474 pub tokens_generated: usize,
1475 pub finish_reason: String,
1476 pub mtp_drafted: usize,
1478 pub mtp_accepted: usize,
1479 pub token_confidence: Vec<f32>,
1484 pub traces: Vec<TokenTrace>,
1487}
1488
1489#[derive(Clone, Debug)]
1494pub struct TokenTrace {
1495 pub t: usize,
1497 pub token_id: u32,
1499 pub confidence: f32,
1501 pub active_skill: Option<String>,
1503 pub recon: Option<f32>,
1507 pub switched: bool,
1510}
1511
1512#[cfg_attr(not(test), allow(dead_code))]
1517fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1518 let t = if temp > 1e-3 { temp } else { 1.0 };
1519 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1520 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1521 if sum > 0.0 {
1522 (((logits[id as usize] - max) / t).exp()) / sum
1523 } else {
1524 0.0
1525 }
1526}
1527
1528fn prefill_batched() -> bool {
1531 std::env::var("CMF_PREFILL")
1532 .map(|v| v != "seq")
1533 .unwrap_or(true)
1534}
1535
1536#[inline]
1540fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1542 if a.len() != b.len() {
1543 return f64::INFINITY;
1544 }
1545 let (mut d, mut r) = (0f64, 0f64);
1546 for (&x, &y) in a.iter().zip(b) {
1547 let e = (x - y) as f64;
1548 d += e * e;
1549 r += y as f64 * y as f64;
1550 }
1551 (d / r.max(1e-30)).sqrt()
1552}
1553
1554fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1555 let graph_quality = unmasked && prefer_graph;
1556 let fused_head_quality = graph_quality && native_metal;
1557 (graph_quality, fused_head_quality)
1558}
1559
1560#[derive(Clone, Copy)]
1564enum PrefillIn<'a> {
1565 Ids(&'a [u32]),
1566 Hidden(&'a [f32]),
1567}
1568
1569impl Pipeline {
1576 pub(crate) fn can_prefill_batched(&self) -> bool {
1577 #[cfg(test)]
1578 let force_serial = self.nll_test_force_serial;
1579 #[cfg(not(test))]
1580 let force_serial = false;
1581 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1582 }
1583
1584 fn automatic_gpu_prefix(&self) -> Option<usize> {
1587 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1588 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1589 }
1590
1591 pub fn prefill_chunk(&self) -> usize {
1595 let env = env_prefill_chunk();
1596 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1597 return prefill_chunk_rule(env, ChunkHost::here(), false);
1598 }
1599 prefill_chunk_rule(
1600 None,
1601 ChunkHost::Other,
1602 self.chunk_stack_facts().dense_on_discrete(),
1603 )
1604 }
1605
1606 fn per_token_stack(&self) -> bool {
1610 self.g3n.is_some() || self.g4e.is_some()
1611 }
1612
1613 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1614 let plain_dense = !self.weights.layers.is_empty()
1615 && !self.per_token_stack()
1616 && self.dsv4.is_none()
1617 && self.dsv41.is_none()
1618 && self.qwen4_exp.is_none()
1619 && self.weights.layers.iter().all(|lw| {
1620 matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1625 && matches!(lw.ffn, FfnKind::Dense(_))
1626 });
1627 let gpu_on = crate::gpu::enabled();
1628 ChunkStackFacts {
1629 plain_dense,
1630 discrete: gpu_on && crate::gpu::discrete(),
1631 gpu_on,
1632 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1635 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1636 multi_gpu: self.gpu_plan.is_some(),
1637 o1: self.o1_active(),
1638 }
1639 }
1640}
1641
1642pub fn prefill_chunk() -> usize {
1651 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1652}
1653
1654fn env_prefill_chunk() -> Option<usize> {
1655 std::env::var("CMF_PREFILL_CHUNK")
1656 .ok()
1657 .and_then(|v| v.parse::<usize>().ok())
1658}
1659
1660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1662enum ChunkHost {
1663 Macos,
1664 Aarch64,
1666 Other,
1668}
1669
1670impl ChunkHost {
1671 fn here() -> Self {
1672 if cfg!(target_os = "macos") {
1673 ChunkHost::Macos
1674 } else if cfg!(target_arch = "aarch64") {
1675 ChunkHost::Aarch64
1676 } else {
1677 ChunkHost::Other
1678 }
1679 }
1680}
1681
1682const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1689
1690fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1696 if let Some(n) = env {
1697 return n.max(1);
1698 }
1699 match host {
1700 ChunkHost::Macos => 512,
1701 ChunkHost::Aarch64 => 256,
1704 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1705 ChunkHost::Other => 48,
1706 }
1707}
1708
1709#[derive(Clone, Copy, Debug, Default)]
1711struct ChunkStackFacts {
1712 plain_dense: bool,
1716 discrete: bool,
1718 gpu_on: bool,
1720 capacity_split: bool,
1722 multi_gpu: bool,
1724 o1: bool,
1726}
1727
1728impl ChunkStackFacts {
1729 fn dense_on_discrete(self) -> bool {
1730 self.plain_dense
1731 && self.discrete
1732 && self.gpu_on
1733 && !self.capacity_split
1734 && !self.multi_gpu
1735 && !self.o1
1736 }
1737}
1738
1739#[inline]
1745fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1746 if end <= start || start >= input_len {
1747 return 0;
1748 }
1749 let rows = (end.min(input_len) - start).min(input_len - start);
1750 if end < input_len {
1751 rows
1752 } else {
1753 rows.saturating_sub(1)
1754 }
1755}
1756
1757pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1759
1760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1762pub(crate) struct ReuseLayer {
1763 pub full: bool,
1766 pub host_rows: usize,
1769 pub device_rows: Option<usize>,
1771 pub device_state: bool,
1773}
1774
1775#[derive(Debug, Clone, PartialEq, Eq)]
1777pub(crate) enum ReusePlan {
1778 Ready,
1780 Pull(Vec<(usize, usize, usize)>),
1783 Fresh,
1785}
1786
1787pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1797 let mut pulls = Vec::new();
1798 for (li, l) in layers.iter().enumerate() {
1799 if !l.full {
1800 if l.device_state {
1801 return ReusePlan::Fresh;
1802 }
1803 continue;
1804 }
1805 if l.host_rows == reuse_from {
1806 continue;
1807 }
1808 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1809 pulls.push((li, l.host_rows, reuse_from));
1810 continue;
1811 }
1812 return ReusePlan::Fresh;
1813 }
1814 if pulls.is_empty() {
1815 ReusePlan::Ready
1816 } else {
1817 ReusePlan::Pull(pulls)
1818 }
1819}
1820
1821impl Pipeline {
1822 fn reset_fresh_mtp_draft_state(&mut self) {
1825 self.draft_full_streak = 0;
1826 }
1827
1828 fn clear_sequence_state(&mut self) {
1836 #[cfg(target_os = "macos")]
1839 let _ = crate::gpu_metal::wait_replay();
1840 self.kv_cache.clear();
1841 self.clear_history();
1844 self.graph_logits = None;
1845 if let Some(b) = &mut self.dsv41 {
1846 b.3.clear();
1847 }
1848 crate::gpu::graph_kv_reset(self.graph_kv_id);
1849 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1854 }
1855
1856 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1863 if self.graph_prefill_preferred() {
1864 return true;
1865 }
1866 let kv_id = self.graph_kv_id;
1867 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1868 .map(|li| {
1869 let full = matches!(
1870 self.weights.layers[self.phys_layer(li)].attn,
1871 AttnKind::Full { .. }
1872 );
1873 ReuseLayer {
1874 full,
1875 host_rows: self.kv_cache.layers[li].pos_len(),
1878 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1879 device_state: crate::gpu::graph_state_resident(kv_id, li),
1880 }
1881 })
1882 .collect();
1883 if layers
1887 .iter()
1888 .all(|l| l.device_rows.is_none() && !l.device_state)
1889 {
1890 return true;
1891 }
1892 let plan = kv_reuse_plan(reuse_from, &layers);
1893 let (what, rows, n) = match &plan {
1894 ReusePlan::Ready => ("host ready", 0, 0),
1895 ReusePlan::Fresh => ("fresh", 0, 0),
1896 ReusePlan::Pull(p) => (
1897 "pull",
1898 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1899 p.len(),
1900 ),
1901 };
1902 let t0 = std::time::Instant::now();
1903 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1904 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1905 eprintln!(
1906 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1907 if ok { "" } else { " (failed → fresh)" },
1908 t0.elapsed().as_secs_f64() * 1e3
1909 );
1910 }
1911 ok
1912 }
1913
1914 fn apply_kv_reuse_plan(
1915 &mut self,
1916 reuse_from: usize,
1917 plan: ReusePlan,
1918 layers: &[ReuseLayer],
1919 ) -> bool {
1920 let kv_id = self.graph_kv_id;
1921 match plan {
1922 ReusePlan::Fresh => return false,
1923 ReusePlan::Ready => {}
1924 ReusePlan::Pull(pulls) => {
1925 let (nkv, hd) = {
1930 let c = &self.kv_cache.layers[pulls[0].0];
1931 (c.num_kv_heads, c.head_dim)
1932 };
1933 let uniform = pulls.iter().all(|&(li, _, _)| {
1934 let c = &self.kv_cache.layers[li];
1935 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1936 });
1937 let batched = if uniform {
1938 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1939 } else {
1940 None
1941 };
1942 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1943 Some(rows) => rows,
1944 None => {
1945 let mut rows = Vec::with_capacity(pulls.len());
1946 for &(li, from, to) in &pulls {
1947 let (lnkv, lhd) = {
1948 let c = &self.kv_cache.layers[li];
1949 (c.num_kv_heads, c.head_dim)
1950 };
1951 let Some((k, v, first_valid)) =
1952 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1953 else {
1954 return false;
1955 };
1956 let need_from = match self.layer_window(li) {
1960 Some(w) => from.max((to + 1).saturating_sub(w)),
1961 None => from,
1962 };
1963 if first_valid > need_from {
1964 return false;
1965 }
1966 rows.push((k, v));
1967 }
1968 rows
1969 }
1970 };
1971 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1972 let cache = &mut self.kv_cache.layers[li];
1973 let row = cache.num_kv_heads * cache.head_dim;
1974 for p in 0..to - from {
1975 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1976 }
1977 if cache.pos_len() != to {
1978 return false;
1979 }
1980 }
1981 }
1982 }
1983 for (li, l) in layers.iter().enumerate() {
1987 if l.full
1988 && l.device_rows.is_some_and(|d| d > reuse_from)
1989 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1990 {
1991 return false;
1992 }
1993 }
1994 true
1995 }
1996
1997 fn finish_generation(
2003 &mut self,
2004 mtp: &mut Option<MtpModule>,
2005 router: &mut Option<crate::swarm::DynRouter>,
2006 clear_sequence: bool,
2007 ) {
2008 if router.is_some() {
2012 let _ = self.set_active_skill(None);
2013 }
2014 #[cfg(target_os = "macos")]
2021 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2022 if clear_sequence {
2023 self.clear_sequence_state();
2024 if let Some(m) = mtp.as_mut() {
2025 m.kv.clear();
2031 }
2032 if let Some(m) = self.mtp.as_mut() {
2033 m.kv.clear();
2037 }
2038 }
2039 self.graph_want_logits = false;
2040 self.graph_head_required = false;
2041 self.graph_logits = None;
2042 self.graph_failed
2043 .store(false, std::sync::atomic::Ordering::Relaxed);
2044 self.cancel
2045 .store(false, std::sync::atomic::Ordering::Relaxed);
2046 self.dyn_router = router.take().or(self.dyn_router.take());
2047 self.mtp = mtp.take().or(self.mtp.take());
2048 self.mtp_graph_mode = None;
2049 self.spec_forced = None;
2050 }
2051
2052 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2056 if self
2057 .graph_failed
2058 .swap(false, std::sync::atomic::Ordering::Relaxed)
2059 {
2060 self.cancel
2061 .store(false, std::sync::atomic::Ordering::Relaxed);
2062 self.clear_sequence_state();
2063 self.graph_logits = None;
2064 self.graph_want_logits = false;
2065 self.graph_head_required = false;
2066 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2067 }
2068 Ok(())
2069 }
2070
2071 #[cfg(target_os = "macos")]
2072 fn fail_metal_graph(&mut self, reason: &str) {
2073 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2074 self.clear_sequence_state();
2075 self.graph_logits = None;
2076 self.graph_failed
2077 .store(true, std::sync::atomic::Ordering::Relaxed);
2078 self.cancel
2079 .store(true, std::sync::atomic::Ordering::Relaxed);
2080 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2081 }
2082
2083 fn nll_begin(&mut self) -> Result<(), String> {
2088 if self
2089 .graph_failed
2090 .swap(false, std::sync::atomic::Ordering::Relaxed)
2091 {
2092 self.cancel
2093 .store(false, std::sync::atomic::Ordering::Relaxed);
2094 self.clear_sequence_state();
2095 self.graph_logits = None;
2096 self.graph_want_logits = false;
2097 self.graph_head_required = false;
2098 return Err("GPU graph failed before NLL scoring".to_string());
2099 }
2100 self.clear_sequence_state();
2101 self.graph_logits = None;
2102 self.graph_want_logits = false;
2103 self.graph_head_required = false;
2104 Ok(())
2105 }
2106
2107 fn nll_end(&mut self) {
2111 self.clear_sequence_state();
2112 self.graph_logits = None;
2113 self.graph_want_logits = false;
2114 self.graph_head_required = false;
2115 self.graph_failed
2116 .store(false, std::sync::atomic::Ordering::Relaxed);
2117 }
2118
2119 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2122 #[cfg(test)]
2123 if self.nll_test_fail_at == Some(pos) {
2124 self.nll_test_fail_at = None;
2125 self.graph_failed
2126 .store(true, std::sync::atomic::Ordering::Relaxed);
2127 self.cancel
2128 .store(true, std::sync::atomic::Ordering::Relaxed);
2129 }
2130 if self
2131 .graph_failed
2132 .swap(false, std::sync::atomic::Ordering::Relaxed)
2133 {
2134 self.cancel
2135 .store(false, std::sync::atomic::Ordering::Relaxed);
2136 self.clear_sequence_state();
2137 self.graph_logits = None;
2138 self.graph_want_logits = false;
2139 return Err(format!(
2140 "GPU graph failed during NLL {phase} at position {pos}"
2141 ));
2142 }
2143 Ok(())
2144 }
2145
2146 #[inline]
2150 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2151 match &self.layer_map {
2152 Some(m) => m[virtual_idx],
2153 None => virtual_idx % self.physical_layers,
2154 }
2155 }
2156
2157 fn onevision2_prefill(&self) -> bool {
2160 self.model.as_ref().is_some_and(|m| m.header.arch.arch_name == "llava_onevision2")
2161 && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2162 }
2163
2164 #[inline]
2167 pub fn mhc_layer(&self, li: usize) -> bool {
2168 self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2169 }
2170
2171 fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2174 let p = self.phys_layer(li);
2175 let m = self.mhc.as_deref().expect("mhc layer");
2176 let n = m.s.streams * m.dim;
2177 if self.mhc_r.len() != n {
2178 self.mhc_r.resize(n, 0.0);
2179 }
2180 if !ffn && m.enters(p) {
2181 m.enter(&mut self.mhc_r, h);
2182 }
2183 m.route(p, ffn, &self.mhc_r, h)
2184 }
2185
2186 fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2189 let p = self.phys_layer(li);
2190 let m = self.mhc.as_deref().expect("mhc layer");
2191 m.merge(&mut self.mhc_r, y, c);
2192 if ffn && m.leaves(p) {
2193 m.leave(&self.mhc_r, h);
2194 }
2195 }
2196
2197 #[inline]
2200 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2201 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2202 }
2203
2204 #[allow(clippy::too_many_arguments)]
2206
2207 #[cfg(target_os = "macos")]
2226 fn graph_prefill_preferred(&self) -> bool {
2227 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2228 if !crate::gpu::enabled_here()
2229 || !graph_force
2230 || std::env::var("CMF_GPU_BLOCK")
2231 .map(|v| v == "0")
2232 .unwrap_or(false)
2233 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2236 {
2237 return false;
2238 }
2239 self.weights
2240 .layers
2241 .iter()
2242 .any(|lw| {
2243 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2244 })
2245 }
2246
2247 #[cfg(not(target_os = "macos"))]
2254 fn batch_prefix_prefill(&self) -> bool {
2255 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2256 Ok("0") => return false,
2257 Ok("1") => true,
2258 _ => false,
2259 };
2260 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2261 && crate::gpu::enabled_here()
2262 && !self.graph_refused()
2263 && (forced || self.graph_attn_decline_reason().is_some())
2264 && self.wgpu_graph_attn_decline().is_none()
2265 && self.attn_softcap == 0.0
2266 && self
2267 .weights
2268 .layers
2269 .iter()
2270 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2271 && self.automatic_gpu_prefix().is_some()
2272 }
2273
2274 fn g4_batch_prefill(&self) -> bool {
2280 #[cfg(target_os = "macos")]
2281 {
2282 false
2283 }
2284 #[cfg(not(target_os = "macos"))]
2285 {
2286 !self.weights.layers.is_empty()
2290 && !self.per_token_stack()
2291 && (self.global_attn.is_some() || self.attn_v_norm)
2292 && crate::gpu::g4_batch_on()
2293 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2294 && crate::gpu::enabled_here()
2295 && crate::gpu::discrete()
2296 && !self.graph_refused()
2297 && !self.o1_active()
2298 && self.attn_softcap == 0.0
2299 && self.wgpu_graph_attn_decline().is_none()
2300 && self.weights.layers.iter().all(|lw| {
2301 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2302 })
2303 && self.automatic_gpu_prefix().is_none()
2304 }
2305 }
2306
2307 #[cfg(not(target_os = "macos"))]
2314 fn g4_batch_ingest(
2315 &mut self,
2316 ids: &[u32],
2317 pos0: usize,
2318 keep_all: bool,
2319 ) -> Result<(usize, Vec<f32>), String> {
2320 let hs = self.hidden_size;
2321 let chunk = self.generation_batch_k().max(1);
2322 let end_all = pos0 + ids.len();
2323 let mut pos = pos0;
2324 let mut out = Vec::new();
2325 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2326 let end = (pos + chunk).min(end_all);
2327 let bk = end - pos;
2328 let mut hiddens = vec![0f32; bk * hs];
2329 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2330 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2331 }
2332 let positions: Vec<usize> = (pos..end).collect();
2333 let t_chunk = std::time::Instant::now();
2334 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2335 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2336 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2337 eprintln!(
2338 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2339 bk as f64 / (ms / 1000.0)
2340 );
2341 }
2342 match outcome {
2343 crate::gpu::BatchGraphOutcome::Completed => {
2344 if keep_all {
2345 out.extend_from_slice(&hiddens);
2346 } else {
2347 out = hiddens[(bk - 1) * hs..].to_vec();
2348 }
2349 pos = end;
2350 }
2351 crate::gpu::BatchGraphOutcome::Failed => {
2352 return Err("Gemma-4 batched prefill failed after admission".into());
2353 }
2354 crate::gpu::BatchGraphOutcome::Declined => {
2355 #[cfg(feature = "gpu")]
2356 if pos > pos0 {
2357 self.pull_lagging_host_kv(0, self.num_layers, pos);
2358 }
2359 break;
2360 }
2361 }
2362 }
2363 Ok((pos - pos0, out))
2364 }
2365
2366 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2371 #[cfg(not(target_os = "macos"))]
2372 {
2373 if ids.is_empty() || !self.g4_batch_prefill() {
2374 return None;
2375 }
2376 self.clear_sequence_state();
2377 match self.g4_batch_ingest(ids, 0, true) {
2378 Ok((n, rows)) if n == ids.len() => Some(rows),
2379 _ => {
2380 self.clear_sequence_state();
2381 None
2382 }
2383 }
2384 }
2385 #[cfg(target_os = "macos")]
2386 {
2387 let _ = ids;
2388 None
2389 }
2390 }
2391
2392 #[cfg(not(target_os = "macos"))]
2393 fn graph_prefill_preferred(&self) -> bool {
2394 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2402 if !graph_on || !crate::gpu::enabled_here() {
2403 return false;
2404 }
2405 if self.embryo_resident_eligible() {
2409 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2414 }
2415 if self.o1_active() {
2429 return false;
2430 }
2431 if self.wgpu_graph_attn_decline().is_some() {
2435 return false;
2436 }
2437 if self
2438 .weights
2439 .layers
2440 .iter()
2441 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2442 || self.onevision2_prefill()
2446 {
2447 return true;
2448 }
2449 self.weights
2460 .layers
2461 .iter()
2462 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2463 && self.automatic_gpu_prefix().is_none()
2464 }
2465
2466 #[cfg(target_os = "macos")]
2467 fn q1_graph_gpu(
2468 &mut self,
2469 start: usize,
2470 upto: Option<usize>,
2471 position: usize,
2472 h: &mut [f32],
2473 ) -> usize {
2474 let _mt0 = std::time::Instant::now(); use crate::gpu::{
2476 AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2477 };
2478 const MOE_WORKING_SET_REASON: &str =
2479 "model weights exceed Metal working set for native MoE graph";
2480 if self
2484 .graph_declines
2485 .borrow()
2486 .iter()
2487 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2488 {
2489 return start;
2490 }
2491 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2492 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2494 || !crate::gpu::enabled_here()
2495 || !graph_force
2496 || std::env::var("CMF_GPU_BLOCK")
2497 .map(|v| v == "0")
2498 .unwrap_or(false)
2499 {
2500 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2501 eprintln!(
2502 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2503 self.attn_softcap > 0.0,
2504 crate::gpu::enabled_here(),
2505 graph_force,
2506 );
2507 }
2508 if self.graph_head_required {
2509 self.fail_metal_graph("native graph front gate refused");
2510 }
2511 return start;
2512 }
2513 let gemma4 = self.metal_gemma4();
2523 let swa_graph = self.metal_graph_swa() || gemma4;
2524 let graph_attn_reason = self.graph_attn_decline_reason();
2531 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2532 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2533 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2537 if (self.swa.is_some() && !swa_graph)
2538 || (self.global_attn.is_some() && !gemma4)
2539 || self.attention_heads_per_layer.is_some()
2540 || (self.attn_v_norm && !gemma4)
2541 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2544 || self.weights.layers.iter().any(|lw| {
2545 ((lw.layer_scale.is_some()
2546 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2547 && !gemma4)
2548 || matches!(&lw.ffn, FfnKind::Dense(d)
2549 if !(matches!(d.act, Act::Silu | Act::Gelu)
2550 || (gemma4 && d.act == Act::GeluTanh)))
2551 })
2552 {
2553 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2556 self.note_graph_decline("metal block graph", reason);
2557 }
2558 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2559 eprintln!(
2560 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2561 self.swa.is_some(),
2562 self.global_attn.is_some(),
2563 self.attention_heads_per_layer.is_some(),
2564 self.attn_v_norm,
2565 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2566 );
2567 }
2568 if self.graph_head_required {
2569 self.fail_metal_graph("native graph architecture gate refused");
2570 }
2571 return start;
2572 }
2573 let limit = upto
2576 .map(|u| u + 1)
2577 .unwrap_or(self.num_layers)
2578 .min(self.num_layers);
2579
2580 enum Item<'a> {
2581 Gdn {
2582 run: Vec<GdnGpuLayer<'a>>,
2583 first: usize,
2584 },
2585 Sconv {
2587 run: Vec<SconvGpuLayer<'a>>,
2588 first: usize,
2589 },
2590 Attn {
2591 l: AttnGpuLayer<'a>,
2592 li: usize,
2593 q_norm: Option<&'a [f32]>,
2594 k_norm: Option<&'a [f32]>,
2595 output_gate: bool,
2596 proj_gate: Option<(&'a QTensor, bool)>,
2600 head_gate_w: Option<&'a [f32]>,
2603 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2604 full_gpu: bool,
2607 },
2608 }
2609
2610 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2617 let attend_contract = attend_mode != "0"
2618 && attend_mode != "off"
2619 && self.head_dim % 4 == 0
2620 && self.head_dim <= 256
2621 && (self.rotary_dim == 0
2625 || (self.rotary_dim >= 2
2626 && self.rotary_dim <= self.head_dim
2627 && (self.rotary_dim / 2) % 32 == 0))
2628 && self.num_kv_heads > 0
2629 && self.num_heads % self.num_kv_heads == 0;
2630
2631 let mut plan: Vec<Item> = Vec::new();
2632 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2633 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2635 let mut scan = start;
2636 while scan < limit {
2637 if self.prompt_mrope.is_some()
2641 || (self.mhc_layer(scan)
2642 && (!crate::gpu_metal::mhc::available()
2643 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
2644 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
2645 || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
2646 {
2647 break;
2648 }
2649 let lw = &self.weights.layers[self.phys_layer(scan)];
2650 let ffn = match &lw.ffn {
2651 FfnKind::Dense(d) if d.segs.is_empty() => {
2652 let (Some(g), Some(u), Some(dn)) = (
2653 d.gate_proj.metal_graph_parts(),
2654 d.up_proj.metal_graph_parts(),
2655 d.down_proj.metal_graph_parts(),
2656 ) else {
2657 if block_diag {
2658 eprintln!(
2659 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2660 );
2661 }
2662 break;
2663 };
2664 MetalFfn::Dense {
2665 gate: g,
2666 up: u,
2667 down: dn,
2668 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2671 }
2672 }
2673 FfnKind::Moe(m) => {
2674 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2680 {
2681 MetalFfn::FlintMoe(flint)
2682 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2683 MetalFfn::Moe(moe)
2684 } else {
2685 if block_diag {
2686 eprintln!(
2687 "block-graph: L{scan} MoE outside the graph contract — run ends"
2688 );
2689 }
2690 break;
2691 };
2692 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2693 model_ref.get_or_insert_with(|| model.clone());
2694 }
2695 ffn
2696 }
2697 _ => {
2698 if block_diag {
2699 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2700 }
2701 break;
2702 }
2703 };
2704 match &lw.attn {
2705 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2706 let parts = (
2707 w.in_proj_qkv.metal_graph_parts(),
2708 w.in_proj_z.metal_graph_parts(),
2709 w.in_proj_a.f32_parts(),
2710 w.in_proj_b.f32_parts(),
2711 w.out_proj.metal_graph_parts(),
2712 );
2713 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2714 if block_diag {
2715 eprintln!(
2716 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2717 w.in_proj_qkv.metal_graph_parts().is_some(),
2718 w.in_proj_z.metal_graph_parts().is_some(),
2719 w.in_proj_a.f32_parts().is_some(),
2720 w.in_proj_b.f32_parts().is_some(),
2721 w.out_proj.metal_graph_parts().is_some(),
2722 );
2723 }
2724 break;
2725 };
2726 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2727 model_ref.get_or_insert_with(|| model.clone());
2728 }
2729 let gl = GdnGpuLayer {
2730 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2731 attn_norm: &lw.input_norm,
2732 post_norm: &lw.post_norm,
2733 qkv,
2734 z,
2735 a,
2736 b,
2737 out,
2738 ffn,
2739 conv1d: &w.conv1d,
2740 a_log: &w.a_log,
2741 dt_bias: &w.dt_bias,
2742 gnorm: &w.norm,
2743 };
2744 match plan.last_mut() {
2745 Some(Item::Gdn { run, .. }) => run.push(gl),
2746 _ => plan.push(Item::Gdn {
2747 run: vec![gl],
2748 first: scan,
2749 }),
2750 }
2751 }
2752 AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2753 let (Some(inp), Some(out)) =
2754 (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2755 else {
2756 if block_diag {
2757 eprintln!("block-graph: L{scan} short-conv parts refused");
2758 }
2759 break;
2760 };
2761 if let QTensor::Mapped { model, .. } = &w.in_proj {
2762 model_ref.get_or_insert_with(|| model.clone());
2763 }
2764 let sl = SconvGpuLayer {
2765 attn_norm: &lw.input_norm,
2766 post_norm: &lw.post_norm,
2767 in_proj: inp,
2768 taps: &w.conv,
2769 out,
2770 attn_out_norm: lw.attn_out_norm.as_deref(),
2771 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2772 ffn,
2773 };
2774 match plan.last_mut() {
2775 Some(Item::Sconv { run, .. }) => run.push(sl),
2776 _ => plan.push(Item::Sconv {
2777 run: vec![sl],
2778 first: scan,
2779 }),
2780 }
2781 }
2782 AttnKind::Full {
2783 wq,
2784 wk,
2785 wv,
2786 wo,
2787 q_norm,
2788 k_norm,
2789 output_gate,
2790 softplus_gate,
2791 bias,
2792 } if (!self.kv_cache.layers[scan].o1_sealed()
2793 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2798 && softplus_gate
2802 .as_ref()
2803 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2804 {
2805 let parts = (
2806 wq.metal_graph_parts(),
2807 wk.metal_graph_parts(),
2808 wv.metal_graph_parts(),
2809 wo.metal_graph_parts(),
2810 );
2811 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2812 break;
2813 };
2814 if let QTensor::Mapped { model, .. } = wq {
2815 model_ref.get_or_insert_with(|| model.clone());
2816 }
2817 let cache = &self.kv_cache.layers[scan];
2818 let o1_metal = cache.o1.is_some()
2822 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2823 && cache.o1_views().is_some();
2824 let head_gate_w = softplus_gate
2828 .as_ref()
2829 .and_then(|(g, _)| g.f32_parts())
2830 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2831 .map(|(d, _, _)| d);
2832 let (nkv_g, hd_g) = if gemma4 {
2835 let (a, b, _) = self.layer_geom(scan);
2836 (a, b)
2837 } else {
2838 (self.num_kv_heads, self.head_dim)
2839 };
2840 let full_gpu = attend_contract
2841 && softplus_gate.is_none() == head_gate_w.is_none()
2842 && cache.mode == crate::kv_cache::KvMode::F32
2843 && (cache.o1.is_none() || o1_metal)
2844 && bias.is_none()
2845 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2846 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2847 && pk.1 == nkv_g * hd_g
2848 && pv.1 == nkv_g * hd_g
2849 && po.2 == self.num_heads * hd_g;
2850 if !full_gpu && self.mhc_layer(scan) {
2853 break;
2854 }
2855 plan.push(Item::Attn {
2856 l: AttnGpuLayer {
2857 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2858 attn_norm: &lw.input_norm,
2859 post_norm: &lw.post_norm,
2860 wq: pq,
2861 wk: pk,
2862 wv: pv,
2863 wo: po,
2864 ffn,
2865 attn_out_norm: lw.attn_out_norm.as_deref(),
2866 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2867 layer_scale: lw.layer_scale,
2868 },
2869 li: scan,
2870 q_norm: q_norm.as_deref(),
2871 k_norm: k_norm.as_deref(),
2872 output_gate: *output_gate,
2873 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2874 head_gate_w,
2875 bias: bias
2876 .as_ref()
2877 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2878 full_gpu,
2879 });
2880 }
2881 _ => break,
2882 }
2883 scan += 1;
2884 }
2885 if let Some(m) = self.mhc.as_deref() {
2889 let inside = |li: usize| m.on(self.phys_layer(li));
2890 let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
2891 let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
2892 if starts_mid || stops_mid {
2893 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2894 eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
2895 }
2896 return start;
2897 }
2898 }
2899 let Some(model) = model_ref else {
2900 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2901 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2902 }
2903 if self.graph_head_required {
2904 self.fail_metal_graph("native graph has no mapped model reference");
2905 }
2906 return start;
2907 };
2908 if plan.is_empty() {
2909 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2910 eprintln!("q1-graph: empty plan at layer {start}");
2911 }
2912 if self.graph_head_required {
2913 self.fail_metal_graph("native graph plan is empty");
2914 }
2915 return start;
2916 }
2917 let has_moe = plan.iter().any(|it| match it {
2918 Item::Gdn { run, .. } => run
2919 .iter()
2920 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2921 Item::Sconv { run, .. } => run
2922 .iter()
2923 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2924 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2925 });
2926 let has_flint_moe = plan.iter().any(|it| match it {
2927 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2928 Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2929 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2930 });
2931 let metal_working_set = crate::gpu_metal::working_set_bytes();
2932 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2933 let model_bytes = model.primary_bytes().len() as u64;
2934 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2940 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2941 let pager_cap = if !has_moe {
2950 None
2951 } else if has_flint_moe {
2952 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2956 } else {
2957 crate::gpu_metal::moe_pager_cap_bytes()
2958 };
2959 let pager_eligible =
2960 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2961 let mut pager_span: Option<(usize, usize)> = None;
2962 if pager_eligible {
2963 let cap = pager_cap.expect("checked above");
2964 let tail_span = (self.graph_want_logits
2968 && upto.is_none()
2969 && scan == self.num_layers
2970 && std::env::var("CMF_GPU_LMHEAD")
2971 .map(|v| v != "0")
2972 .unwrap_or(true))
2973 .then(|| self.weights.lm_head.metal_graph_parts())
2974 .flatten()
2975 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2976 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2977 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2978 Some(match span {
2979 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2980 None => (lo, hi),
2981 })
2982 };
2983 let page_span = |span: (usize, usize), include_tail: bool| {
2984 let (mut lo, mut hi) = span;
2985 if include_tail {
2986 if let Some((tail_lo, tail_hi)) = tail_span {
2987 lo = lo.min(tail_lo);
2988 hi = hi.max(tail_hi);
2989 }
2990 }
2991 crate::gpu_metal::page_round_span(lo, hi)
2992 };
2993 let mut selected = 0usize;
2994 let mut selected_span: Option<(usize, usize)> = None;
2995 let mut gdn_prefix: Option<(usize, usize)> = None;
3003 'items: for (i, item) in plan.iter().enumerate() {
3004 match item {
3005 Item::Sconv { .. } => break,
3008 Item::Attn { li, .. } => {
3009 let Some(span) = add_layer_span(selected_span, *li) else {
3010 break;
3011 };
3012 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3013 break;
3014 };
3015 if page_hi - page_lo > cap {
3016 break;
3017 }
3018 selected = i + 1;
3019 selected_span = Some((page_lo, page_hi));
3020 }
3021 Item::Gdn { first, run } => {
3022 let mut whole = None;
3029 for take in 1..=run.len() {
3030 whole = add_layer_span(whole, *first + take - 1);
3031 if whole.is_none() {
3032 break;
3033 }
3034 }
3035 let whole_fits_empty_page = whole
3036 .and_then(|span| page_span(span, false))
3037 .is_some_and(|(lo, hi)| hi - lo <= cap);
3038 if whole_fits_empty_page {
3039 let mut span = selected_span;
3040 for take in 1..=run.len() {
3041 let Some(next) = add_layer_span(span, *first + take - 1) else {
3042 break 'items;
3043 };
3044 span = Some(next);
3045 }
3046 let Some(span) = span else {
3047 break;
3048 };
3049 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3050 else {
3051 break;
3052 };
3053 if page_hi - page_lo > cap {
3054 break;
3055 }
3056 selected = i + 1;
3057 selected_span = Some((page_lo, page_hi));
3058 continue;
3059 }
3060 if selected != 0 {
3061 break;
3062 }
3063 let mut span = None;
3064 let mut best: Option<(usize, (usize, usize))> = None;
3065 for take in 1..=run.len() {
3066 let Some(next) = add_layer_span(span, *first + take - 1) else {
3067 break;
3068 };
3069 let Some((page_lo, page_hi)) = page_span(next, false) else {
3072 break;
3073 };
3074 if page_hi - page_lo > cap {
3075 break;
3076 }
3077 span = Some((page_lo, page_hi));
3078 best = Some((take, (page_lo, page_hi)));
3079 }
3080 let Some((take, page)) = best else {
3081 break;
3082 };
3083 selected = i + 1;
3084 selected_span = Some(page);
3085 if take != run.len() {
3086 gdn_prefix = Some((i, take));
3087 break 'items;
3088 }
3089 }
3090 }
3091 }
3092 if selected != 0 {
3093 if let Some((item, take)) = gdn_prefix {
3094 match &mut plan[item] {
3095 Item::Gdn { run, .. } => run.truncate(take),
3096 Item::Sconv { .. } | Item::Attn { .. } => {
3097 unreachable!("only a GDN run is splittable")
3098 }
3099 }
3100 }
3101 let (page_lo, page_hi) = selected_span.expect("selected page span");
3102 tracing::info!(
3103 page_start = page_lo,
3104 page_end = page_hi,
3105 page_mb = (page_hi - page_lo) / (1024 * 1024),
3106 cap_mb = cap / (1024 * 1024),
3107 graph_items = selected,
3108 "native Metal MoE graph uses transient bounded weight page"
3109 );
3110 plan.truncate(selected);
3111 pager_span = Some((page_lo, page_hi));
3112 } else {
3113 tracing::warn!(
3114 cap_mb = cap / (1024 * 1024),
3115 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3116 );
3117 }
3118 }
3119 if has_moe && needs_bounded_pager && pager_span.is_none() {
3128 tracing::info!(
3129 model_bytes,
3130 metal_working_set,
3131 "native Metal MoE graph declined before allocation"
3132 );
3133 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3134 if self.graph_head_required {
3135 self.fail_metal_graph(MOE_WORKING_SET_REASON);
3136 }
3137 return start;
3138 }
3139 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3140 let dev_attend = attend_contract
3141 && (self.head_dim <= 128
3142 || has_moe
3143 || (self.head_dim <= 256 && has_gdn)
3149 || (self.head_dim <= 256 && swa_graph)
3153 || attend_mode == "force"
3154 || attend_mode == "256");
3155 if !dev_attend {
3156 for it in &mut plan {
3157 if let Item::Attn { li, full_gpu, .. } = it {
3158 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3161 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3162 if !keep_o1 {
3163 *full_gpu = false;
3164 }
3165 }
3166 }
3167 }
3168 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3169 use std::sync::atomic::{AtomicBool, Ordering};
3170 static SAID: AtomicBool = AtomicBool::new(false);
3171 if !SAID.swap(true, Ordering::Relaxed) {
3172 let fg = plan
3173 .iter()
3174 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3175 .count();
3176 let att = plan
3177 .iter()
3178 .filter(|it| matches!(it, Item::Attn { .. }))
3179 .count();
3180 eprintln!(
3181 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3182 plan.len(),
3183 self.head_dim,
3184 self.rotary_dim,
3185 self.num_kv_heads,
3186 self.num_heads,
3187 );
3188 }
3189 }
3190 let dims = GraphDims {
3191 hidden: self.hidden_size,
3192 eps: self.rms_eps as f32,
3193 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3194 };
3195 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3199 crate::gpu_metal::MoePagerPageScope::new(
3200 _mt0,
3201 position,
3202 start,
3203 page_lo,
3204 page_hi,
3205 pager_cap.unwrap_or(0),
3209 )
3210 });
3211 let graph_new = match pager_span {
3212 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3213 None => TokenGraph::new(&model, dims, h),
3214 };
3215 let Some(mut graph) = graph_new else {
3216 if self.graph_head_required {
3217 self.fail_metal_graph("native TokenGraph allocation refused");
3218 }
3219 return start;
3220 };
3221 if swa_graph && self.head_dim > 128 {
3222 graph.set_attend_blk(1, 32);
3232 }
3233 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3234 nv: cfg.num_v_heads,
3235 nk: cfg.num_k_heads,
3236 dk: cfg.key_head_dim,
3237 dv: cfg.value_head_dim,
3238 kk: cfg.conv_kernel,
3239 hidden: self.hidden_size,
3240 inter: self.intermediate_size,
3241 c_dim: cfg.conv_dim(),
3242 eps: cfg.rms_eps as f32,
3243 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3244 });
3245 let mut valid = 0usize;
3249 let mut end = start;
3250 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3252 static ONCE: std::sync::Once = std::sync::Once::new();
3253 ONCE.call_once(|| {
3254 for it in &plan {
3255 match it {
3256 Item::Gdn { first, run } => {
3257 eprintln!("plan: Gdn first={first} len={}", run.len())
3258 }
3259 Item::Sconv { first, run } => {
3260 eprintln!("plan: Sconv first={first} len={}", run.len())
3261 }
3262 Item::Attn { li, full_gpu, .. } => {
3263 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3264 }
3265 }
3266 }
3267 });
3268 }
3269 for item in &plan {
3270 let ok = match item {
3271 Item::Gdn { run, .. } => gcfg
3272 .as_ref()
3273 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3274 .unwrap_or(false),
3275 Item::Sconv { run, .. } => self
3276 .short_conv_cfg
3277 .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3278 Item::Attn { l, .. } => graph.attn_ok(l),
3279 };
3280 if !ok {
3281 if block_diag {
3282 eprintln!(
3283 "block-graph: plan item {} ({}) failed graph preflight",
3284 valid,
3285 match item {
3286 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3287 Item::Sconv { run, first } => {
3288 format!("short-conv run L{first}+{}", run.len())
3289 }
3290 Item::Attn { li, .. } => format!("Attn L{li}"),
3291 }
3292 );
3293 }
3294 break;
3295 }
3296 valid += 1;
3297 end += match item {
3298 Item::Gdn { run, .. } => run.len(),
3299 Item::Sconv { run, .. } => run.len(),
3300 Item::Attn { .. } => 1,
3301 };
3302 }
3303 plan.truncate(valid);
3304 if plan.is_empty() {
3305 if self.graph_head_required {
3306 self.fail_metal_graph("native graph preflight produced no valid items");
3307 }
3308 return start;
3309 }
3310
3311 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3312 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3313 return start;
3314 }
3315
3316 let one_pass = |t: (usize, usize, usize)| {
3325 use cortiq_core::TensorDtype as D;
3326 matches!(
3327 model.tensors[t.0].dtype,
3328 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3329 )
3330 };
3331 let dense_fast = plan.iter().all(|it| match it {
3332 Item::Attn {
3333 l, li, full_gpu, ..
3334 } => {
3335 *full_gpu
3336 && self.kv_cache.layers[*li].o1.is_none()
3337 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3338 && match l.ffn {
3339 MetalFfn::Dense { gate, up, down, .. } => {
3340 one_pass(gate) && one_pass(up) && one_pass(down)
3341 }
3342 _ => false,
3343 }
3344 }
3345 Item::Sconv { run, .. } => run.iter().all(|l| {
3348 one_pass(l.in_proj)
3349 && one_pass(l.out)
3350 && match l.ffn {
3351 MetalFfn::Dense { gate, up, down, .. } => {
3352 one_pass(gate) && one_pass(up) && one_pass(down)
3353 }
3354 _ => false,
3355 }
3356 }),
3357 Item::Gdn { .. } => false,
3358 });
3359 let moe_plan = !dense_fast
3363 && plan.iter().all(|it| match it {
3364 Item::Attn {
3365 l, li, full_gpu, ..
3366 } => {
3367 *full_gpu
3368 && self.kv_cache.layers[*li].o1.is_none()
3369 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3370 }
3371 Item::Gdn { .. } | Item::Sconv { .. } => false,
3372 });
3373 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3374 if block_diag {
3375 use std::sync::atomic::{AtomicBool, Ordering};
3376 static SAID_AB: AtomicBool = AtomicBool::new(false);
3377 if !SAID_AB.swap(true, Ordering::Relaxed) {
3378 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3379 }
3380 }
3381 let _mv_fast = match ab {
3382 Some((bits, _)) => {
3383 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3384 crate::gpu_metal::MvFastGuard::set_raw(bits)
3385 }
3386 None if moe_plan => {
3387 graph.set_dense_concurrent(
3388 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3389 );
3390 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3391 }
3392 None => {
3393 graph.set_dense_concurrent(dense_fast);
3394 let q8r4 = if swa_graph {
3397 crate::gpu_metal::DENSE_Q8R4
3398 } else {
3399 0
3400 };
3401 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3402 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3403 } else {
3404 0
3405 })
3406 }
3407 };
3408
3409 let pool = self.pool.clone();
3412 let (nh, nkv, hd, hs, eps) = (
3413 self.num_heads,
3414 self.num_kv_heads,
3415 self.head_dim,
3416 self.hidden_size,
3417 self.rms_eps,
3418 );
3419 let norm_style = self.norm_style;
3420 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3421 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3422 let kv_id = self.graph_kv_id;
3423 let mut pending: Vec<(usize, usize)> = Vec::new();
3426 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3429 for item in &plan {
3430 let _xt0 = std::time::Instant::now();
3431 let _xkind: u32 = match item {
3432 Item::Gdn { .. } | Item::Sconv { .. } => 2,
3433 Item::Attn { .. } => 3,
3434 };
3435 if self.loop_final_norm {
3437 let item_start = match item {
3438 Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3439 Item::Attn { li, .. } => *li,
3440 };
3441 if item_start > start && self.is_loop_end(item_start - 1) {
3442 graph.encode_loop_norm(&self.weights.final_norm);
3443 }
3444 }
3445 match item {
3446 Item::Gdn { run, first } => {
3447 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3448 if l.linear_state.len() != want {
3449 l.linear_state = vec![0f32; want];
3450 }
3451 }
3452 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3453 .iter()
3454 .map(|l| l.linear_state.as_slice())
3455 .collect();
3456 let _ig = std::time::Instant::now();
3457 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3458 tracing::error!("q1 graph: GDN run refused after validation");
3460 return start;
3461 }
3462 graph.commit_kind = 2;
3465 graph.commit();
3466 crate::gpu::stageprof(0, _ig.elapsed());
3467 pending.push((*first, run.len()));
3468 }
3469 Item::Sconv { run, first } => {
3470 let sc = self.short_conv_cfg.expect("validated short-conv run");
3471 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3472 if l.linear_state.len() != sc.state_len() {
3473 l.linear_state = vec![0f32; sc.state_len()];
3474 }
3475 }
3476 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3477 .iter()
3478 .map(|l| l.linear_state.as_slice())
3479 .collect();
3480 if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3481 tracing::error!("q1 graph: short-conv run refused after validation");
3482 return start;
3483 }
3484 graph.commit_kind = 2;
3485 graph.commit();
3486 pending.push((*first, run.len()));
3487 }
3488 Item::Attn {
3489 l,
3490 li,
3491 q_norm,
3492 k_norm,
3493 output_gate,
3494 proj_gate,
3495 head_gate_w,
3496 bias,
3497 full_gpu,
3498 } => {
3499 let _ia = std::time::Instant::now();
3500 let inv_freq_l = self.layer_inv_freq(*li);
3506 let rd_l = self.layer_geom(*li).2;
3507 let window_l = self.layer_window(*li);
3508 let head_gate_w = *head_gate_w;
3509 let (nkv, hd) = if gemma4 {
3512 let (a, b, _) = self.layer_geom(*li);
3513 (a, b)
3514 } else {
3515 (nkv, hd)
3516 };
3517 if *full_gpu {
3519 let cache = &self.kv_cache.layers[*li];
3520 let o1p = if cache.o1.is_some() {
3521 match cache.o1_views() {
3522 Some(views) => Some(crate::gpu::O1AttnParams {
3523 views,
3524 epoch: self.o1_epoch,
3525 }),
3526 None => None,
3528 }
3529 } else {
3530 None
3531 };
3532 let o1_layer = cache.o1.is_some();
3533 if o1_layer && o1p.is_none() {
3534 }
3536 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3537 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3538 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3539 debug_assert!(
3542 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3543 );
3544 let p = crate::gpu::AttnDeviceParams {
3545 kv_id,
3546 layer: *li,
3547 nh,
3548 nkv,
3549 hd,
3550 rd: rd_l,
3551 position,
3552 scale: self.attn_scale,
3553 eps: eps as f32,
3554 gemma,
3555 late_qk_norm: self.qk_norm_after_rope,
3556 output_gate: *output_gate,
3557 q_norm: *q_norm,
3558 k_norm: *k_norm,
3559 inv_freq: &inv_freq_l,
3560 rope_scale: self.layer_rope_scale(*li),
3561 cpu_k,
3562 cpu_v,
3563 cpu_stored,
3564 cpu_gen: cache.generation(),
3565 o1: o1p,
3566 window: window_l,
3567 softmax_sink: cache.sinks.as_deref(),
3568 head_gate: head_gate_w,
3569 v_norm: self.attn_v_norm,
3570 };
3571 let o1_bad = o1_layer && p.o1.is_none();
3572 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3573 {
3574 if p.o1.is_none() {
3576 dev_attn.push((*li, nkv, hd));
3577 }
3578 graph.commit_kind = 3;
3579 graph.commit();
3580 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3584 continue;
3585 }
3586 }
3588 graph.encode_attn_prefix(l);
3589 if let Err(err) = graph.sync_checked() {
3590 self.fail_metal_graph(&err);
3591 return start;
3592 }
3593 if !pending.is_empty() {
3594 let idxs: Vec<usize> =
3595 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3596 let mut outs: Vec<&mut [f32]> = self
3597 .kv_cache
3598 .layers
3599 .iter_mut()
3600 .enumerate()
3601 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3602 .map(|(_, s)| s.linear_state.as_mut_slice())
3603 .collect();
3604 graph.read_states(&mut outs);
3605 }
3606 let mut q_raw = attention::take_buf(l.wq.1);
3607 let mut k = attention::take_buf(l.wk.1);
3608 let mut v = attention::take_buf(l.wv.1);
3609 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3610 let mut gate_raw = proj_gate.map(|(gp, _)| {
3613 let mut normed = attention::take_buf(hs);
3614 graph.read_normed(&mut normed);
3615 let mut raw = attention::take_buf(gp.rows());
3616 gp.matvec(&normed, &mut raw, pool.as_deref());
3617 attention::recycle_buf(&mut normed);
3618 raw
3619 });
3620 let cfg = QwenAttnCfg {
3621 mrope: None,
3622 num_heads: nh,
3623 num_kv_heads: nkv,
3624 head_dim: hd,
3625 hidden_size: hs,
3626 position,
3627 inv_freq: &inv_freq_l,
3628 rotary_dim: rd_l,
3629 scale: self.attn_scale,
3630 softcap: self.attn_softcap,
3631 window: window_l,
3632 v_norm: self.attn_v_norm,
3635 qk_norm_after_rope: self.qk_norm_after_rope,
3636 gate_sigmoid: self.proj_gate_sigmoid,
3637 q_norm: *q_norm,
3638 k_norm: *k_norm,
3639 output_gate: *output_gate,
3640 softplus_gate: None,
3641 rope_scale: self.layer_rope_scale(*li),
3642 bias: *bias,
3643 rms_eps: eps,
3644 norm_style,
3645 pool: pool.as_deref(),
3646 v_head_dim: hd,
3647 };
3648 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3651 || std::env::var("CMF_ATTN_DUMP").is_ok();
3652 let _ = full_gpu;
3653 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3654 let mut ao = attention::qwen_attention_core(
3655 q_raw,
3656 k,
3657 v,
3658 &mut self.kv_cache.layers[*li],
3659 &cfg,
3660 );
3661 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3665 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3666 let (cq, _cg, _ck, _cv) =
3667 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3668 let cache = &self.kv_cache.layers[*li];
3669 let n = cache.head_keys(0).len() / hd;
3670 let mut bytes: Vec<u8> = Vec::new();
3671 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3672 bytes.extend_from_slice(&v.to_le_bytes());
3673 }
3674 for v in &cq {
3675 bytes.extend_from_slice(&v.to_le_bytes());
3676 }
3677 for g in 0..nkv {
3678 for v in cache.head_keys(g) {
3679 bytes.extend_from_slice(&v.to_le_bytes());
3680 }
3681 }
3682 for g in 0..nkv {
3683 for v in cache.head_values(g) {
3684 bytes.extend_from_slice(&v.to_le_bytes());
3685 }
3686 }
3687 let _ =
3688 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3689 }
3690 }
3691 if let Some((qr0, k0, v0)) =
3692 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3693 {
3694 let (cq, _cg, ck, cv) =
3695 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3696 let mut h_now = vec![0f32; hs];
3697 graph.read_h(&mut h_now);
3698 let cache = &self.kv_cache.layers[*li];
3699 let n_after = cache.head_keys(0).len() / hd;
3700 let stored = n_after.saturating_sub(1);
3704 let cpu_k: Vec<&[f32]> = (0..nkv)
3705 .map(|g| &cache.head_keys(g)[..stored * hd])
3706 .collect();
3707 let cpu_v: Vec<&[f32]> = (0..nkv)
3708 .map(|g| &cache.head_values(g)[..stored * hd])
3709 .collect();
3710 let p = crate::gpu::AttnDeviceParams {
3711 kv_id,
3712 layer: *li,
3713 nh,
3714 nkv,
3715 hd,
3716 rd: rd_l,
3720 position,
3721 scale: self.attn_scale,
3722 eps: eps as f32,
3723 gemma,
3724 late_qk_norm: self.qk_norm_after_rope,
3725 output_gate: *output_gate,
3726 q_norm: *q_norm,
3727 k_norm: *k_norm,
3728 inv_freq: &inv_freq_l,
3729 rope_scale: self.layer_rope_scale(*li),
3730 cpu_k,
3731 cpu_v,
3732 cpu_stored: stored,
3733 cpu_gen: cache.generation(),
3734 o1: None,
3735 window: window_l,
3736 softmax_sink: cache.sinks.as_deref(),
3737 head_gate: None,
3738 v_norm: self.attn_v_norm,
3739 };
3740 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3741 let md = |a: &[f32], b: &[f32]| {
3742 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3743 };
3744 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3745 eprintln!(
3746 "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}",
3747 nn(&cq),
3748 md(&cq, &dq),
3749 nn(&ck),
3750 md(&ck, &dk),
3751 nn(&cv),
3752 md(&cv, &dv),
3753 nn(&ao),
3754 md(&ao, &dao)
3755 );
3756 } else {
3757 eprintln!("attn-oracle L{li}: device probe declined");
3758 }
3759 }
3760 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3761 attention::apply_projected_gate(
3764 &mut ao,
3765 raw,
3766 per_head,
3767 hd,
3768 self.proj_gate_sigmoid,
3769 );
3770 }
3771 if let Some(mut raw) = gate_raw.take() {
3772 attention::recycle_buf(&mut raw);
3773 }
3774 graph.encode_attn_suffix(l, &ao);
3775 graph.commit();
3778 attention::recycle_buf(&mut ao);
3779 }
3780 }
3781
3782 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3783 }
3784 let mut lm_rows = None;
3789 if self.graph_want_logits
3790 && upto.is_none()
3791 && end == self.num_layers
3792 && std::env::var("CMF_GPU_LMHEAD")
3793 .map(|v| v != "0")
3794 .unwrap_or(true)
3795 {
3796 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3797 if graph.lm_head_ok(lm) {
3798 graph.encode_lm_head(&self.weights.final_norm, lm);
3799 lm_rows = Some(lm.1);
3800 }
3801 }
3802 }
3803 if self.graph_head_required && lm_rows.is_none() {
3804 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3805 self.fail_metal_graph("fused graph head was requested but not encodable");
3806 return start;
3807 }
3808 let _sy0 = std::time::Instant::now();
3809 if let Err(err) = graph.sync_checked() {
3810 if let Some(profile) = pager_profile_page.as_mut() {
3811 profile.finish(start, graph.pager_page_stats(), "sync-error");
3812 }
3813 self.fail_metal_graph(&err);
3814 return start;
3815 }
3816 let _rs0 = std::time::Instant::now();
3817 if !pending.is_empty() {
3818 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3819 let mut outs: Vec<&mut [f32]> = self
3820 .kv_cache
3821 .layers
3822 .iter_mut()
3823 .enumerate()
3824 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3825 .map(|(_, s)| s.linear_state.as_mut_slice())
3826 .collect();
3827 graph.read_states(&mut outs);
3828 }
3829 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3830 use std::sync::atomic::{AtomicU64, Ordering};
3831 static SY: AtomicU64 = AtomicU64::new(0);
3832 static RS: AtomicU64 = AtomicU64::new(0);
3833 static N: AtomicU64 = AtomicU64::new(0);
3834 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3835 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3836 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3837 if n % 100 == 0 {
3838 eprintln!(
3839 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3840 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3841 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3842 );
3843 }
3844 }
3845 if let Some(rows) = lm_rows {
3846 crate::gpu::hostprof_encode_done(_mt0);
3847 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3848 graph.read_logits(&mut lg);
3849 crate::gpu::hostprof_total(_mt0);
3850 lg.resize(self.vocab_size, 0.0);
3851 if let Some(c) = self.final_softcap {
3852 softcap_in_place(&mut lg, c, self.pool.as_deref());
3853 }
3854 self.graph_logits = Some(lg);
3855 }
3856 graph.read_h(h);
3857 if self.graph_head_required && self.graph_logits.is_none() {
3858 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3859 self.fail_metal_graph("fused graph head completed without logits readback");
3860 return start;
3861 }
3862 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3863 METAL_GRAPH_LAYERS.fetch_add(
3864 end.saturating_sub(start) as u64,
3865 std::sync::atomic::Ordering::Relaxed,
3866 );
3867 if self.graph_head_required {
3868 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3869 }
3870 for (li, nkv, hd) in dev_attn {
3874 let mut krow = attention::take_buf(nkv * hd);
3875 let mut vrow = attention::take_buf(nkv * hd);
3876 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3877 let cache = &mut self.kv_cache.layers[li];
3878 cache.append(&krow, &vrow, &[]);
3879 let n = cache.seq_len;
3880 let mut imp = attention::take_buf(n);
3881 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3882 cache.accumulate_imp(&imp);
3883 attention::recycle_buf(&mut imp);
3884 }
3885 attention::recycle_buf(&mut krow);
3886 attention::recycle_buf(&mut vrow);
3887 }
3888 if let Some(profile) = pager_profile_page.as_mut() {
3889 profile.finish(end, graph.pager_page_stats(), "page-complete");
3890 }
3891 if let Some((_, arm)) = ab {
3892 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3893 }
3894 end
3895 }
3896
3897 pub fn new(
3898 tokenizer: Tokenizer,
3899 weights: PipelineWeights,
3900 hidden_size: usize,
3901 intermediate_size: usize,
3902 num_heads: usize,
3903 num_kv_heads: usize,
3904 head_dim: usize,
3905 num_layers: usize,
3906 physical_layers: usize,
3907 loop_final_norm: bool,
3908 vocab_size: usize,
3909 rms_eps: f64,
3910 rope_base: f32,
3911 norm_style: NormStyle,
3912 max_seq_len: usize,
3913 sampler_config: SamplerConfig,
3914 ) -> Self {
3915 let rng = match sampler_config.seed {
3916 Some(s) => SplitMix64::new(s),
3917 None => SplitMix64::from_entropy(),
3918 };
3919 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3920 let pool = Pool::from_env();
3921 if let Some(p) = &pool {
3922 tracing::info!("worker pool: {} threads", p.n_workers());
3923 if let Some(model) = weights
3925 .lm_head
3926 .model_arc()
3927 .or_else(|| weights.embed_tokens.model_arc())
3928 {
3929 let regions: Vec<&[u8]> =
3930 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3931 p.bind_numa(®ions);
3932 }
3933 }
3934 Self {
3935 gpu_plan: None,
3936 tokenizer: std::sync::Arc::new(tokenizer),
3937 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3938 sampler_config,
3939 weights,
3940 hidden_size,
3941 intermediate_size,
3942 num_heads,
3943 num_kv_heads,
3944 head_dim,
3945 num_layers,
3946 physical_layers,
3947 loop_final_norm,
3948 layer_map: None,
3949 mhc: None,
3950 mhc_r: Vec::new(),
3951 mrope_sections: None,
3952 prompt_mrope: None,
3953 vocab_size,
3954 rms_eps,
3955 rope_base,
3956 norm_style,
3957 rotary_dim: head_dim,
3958 attention_heads_per_layer: None,
3959 kv_heads_per_layer: None,
3960 v_head_dim: None,
3961 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3962 .filter(|v| !v.is_empty())
3963 .map(std::path::PathBuf::from),
3964 graph_declines: std::cell::RefCell::new(Vec::new()),
3965 mimo_moe: Default::default(),
3966 minicpm_tail: Default::default(),
3967 vmf_cfg: None,
3968 gdn_cfg: None,
3969 kda_cfg: None,
3970 g3n: None,
3971 g4e: None,
3972 dsv4: None,
3973 dsv41: None,
3974 dsv41_vision: None,
3975 dsv41_prefill: None,
3976 qwen4_exp: None,
3977 clover: None,
3978 clover_stats: clover::CloverStats::default(),
3979 clover_pen: Default::default(),
3980 dsv4_mtp: Vec::new(),
3981 dspark: None,
3982 dspark_pending: Vec::new(),
3983 dspark_hist: Vec::new(),
3984 dspark_real: Vec::new(),
3985 dspark_trunk_picks: Vec::new(),
3986 dspark_exp: Vec::new(),
3987 dspark_draft_ns: 0,
3988 m3_winners: Vec::new(),
3989 m3_counts: Vec::new(),
3990 m3_n_exp: 0,
3991 m3_tokens: 0,
3992 m3_prewarmed: false,
3993 logit_multiplier: None,
3994 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3995 graph_failed: std::sync::atomic::AtomicBool::new(false),
3996 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3997 kv_history: Vec::new(),
3998 kv_history_device: false,
3999 short_conv_cfg: None,
4000 mtp: None,
4001 mimo_mtp: None,
4002 verify_exact_moe: false,
4003 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4004 ignore_eos: false,
4005 draft_full_streak: 0,
4006 spec_k_adapt: None,
4007 spec_acc_ewma: 0.7,
4008 rng,
4009 sampler_scratch: SamplerScratch::default(),
4010 spec_forced: None,
4011 spec_q: Vec::new(),
4012 spec_p: Vec::new(),
4013 spec_res: Vec::new(),
4014 spec_qs: Vec::new(),
4015 spec_ps: Vec::new(),
4016 spec_ress: Vec::new(),
4017 mtp_graph_mode: None,
4018 #[cfg(target_os = "macos")]
4019 metal_verify: None,
4020 inv_freq,
4021 ws: ForwardScratch::new(hidden_size),
4022 pool,
4023 model: None,
4024 dyn_force_f32: false,
4025 dyn_skill_layers: Vec::new(),
4026 dyn_active: None,
4027 dyn_blend_loaded: false,
4028 dyn_phi_layer: None,
4029 dyn_phi_ema: Vec::new(),
4030 dyn_phi_seen: 0,
4031 dyn_router: None,
4032 o1_cfg: None,
4033 o1_epoch: 0,
4034 o1_flags: Vec::new(),
4035 trace: false,
4036 calib_temp: 1.0,
4037 confidence_on: true,
4038 embed_multiplier: 1.0,
4039 attn_scale: 1.0 / (head_dim as f32).sqrt(),
4040 swa: None,
4041 swa_trim: None,
4042 swa_trim_anchor: usize::MAX,
4043 chunk_min_rows: 32,
4044 sliding_layers: None,
4045 no_rope_layers: None,
4046 anchor_core: None,
4047 bounded_rope: None,
4048 kv_prefix: KvPrefix::default(),
4049 last_prefill_tokens: 0,
4050 inv_freq_local: None,
4051 rotary_dim_local: None,
4052 rope_scale: 1.0,
4053 rope_scale_local: 1.0,
4054 global_attn: None,
4055 inv_freq_global: None,
4056 attn_v_norm: false,
4057 qk_norm_after_rope: false,
4058 proj_gate_sigmoid: false,
4059 final_softcap: None,
4060 head_clusters: None,
4061 attn_softcap: 0.0,
4062 graph_want_logits: false,
4063 graph_head_required: false,
4064 graph_logits: None,
4065 embryo_graph: None,
4066 graph_refused: std::sync::atomic::AtomicBool::new(false),
4067 graph_kv_id: {
4068 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4069 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4070 },
4071 #[cfg(test)]
4072 nll_test_fail_at: None,
4073 #[cfg(test)]
4074 nll_test_force_serial: false,
4075 }
4076 }
4077
4078 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4086 if let Err(e) = self.try_set_o1(cfg) {
4087 tracing::error!("{e}");
4088 }
4089 }
4090
4091 pub fn bounded_native(&self) -> bool {
4095 self.anchor_core.is_some()
4096 }
4097
4098 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4101 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4102 }
4103
4104 pub fn o1_refusal(&self) -> Option<String> {
4106 self.anchor_core.as_ref().map(|ac| {
4107 format!(
4108 "--o1 / CMF_O1 refused: the anchor is native bounded \
4109 (anchor_core kind={} window={} sink={}); the file's operator \
4110 is executed as-is and no post-hoc Nyström overlay applies",
4111 ac.kind, ac.window, ac.sink
4112 )
4113 })
4114 }
4115
4116 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4118 if let Some(c) = &cfg {
4119 if let Some(why) = self.o1_refusal() {
4120 self.o1_flags = Vec::new();
4121 self.o1_cfg = None;
4122 return Err(why);
4123 }
4124 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4125 self.o1_flags.clear();
4126 self.o1_cfg = None;
4127 return Err(format!(
4128 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4129 c.w, c.sink
4130 ));
4131 }
4132 }
4133 self.o1_flags = match &cfg {
4134 Some(c) => {
4135 let mut flags = c.layer_flags(self.num_layers);
4136 for (li, f) in flags.iter_mut().enumerate() {
4137 if *f
4143 && (!matches!(
4144 self.weights.layers[self.phys_layer(li)].attn,
4145 AttnKind::Full { .. }
4146 ) || self.layer_window(li).is_some()
4147 || self.kv_cache.layers[li].sinks.is_some()
4148 || self.layer_v_dim(li) != self.layer_geom(li).1)
4149 {
4150 *f = false;
4151 }
4152 }
4153 flags
4154 }
4155 None => Vec::new(),
4156 };
4157 if let Some(c) = &cfg {
4158 let n = self.o1_flags.iter().filter(|&&f| f).count();
4159 tracing::info!(
4160 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4161 self.num_layers,
4162 c.m,
4163 c.w,
4164 c.sink,
4165 c.rect
4166 );
4167 }
4168 self.o1_cfg = cfg;
4169 Ok(())
4170 }
4171
4172 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4178 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4179 return Err(format!(
4180 "anchor_core kind '{}' is not executable by this runtime",
4181 cfg.kind
4182 ));
4183 }
4184 if cfg.window == 0 {
4185 return Err("anchor_core.window must be >= 1".into());
4186 }
4187 let mut n = 0usize;
4188 for li in 0..self.num_layers {
4189 let pl = self.phys_layer(li);
4190 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4191 if w.window != cfg.window
4192 || w.sink != cfg.sink
4193 || w.group_windows != cfg.group_windows
4194 {
4195 return Err(format!(
4196 "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
4197 with anchor_core (window {} sink {} groups {:?})",
4198 w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
4199 ));
4200 }
4201 self.kv_cache.layers[li].install_bounded(cfg.window);
4202 n += 1;
4203 }
4204 }
4205 if n == 0 {
4206 return Err("anchor_core is present but no layer executes it".into());
4207 }
4208 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4209 self.bounded_rope = Some(std::sync::Arc::new(rope));
4210 self.anchor_core = Some(cfg.clone());
4211 self.embryo_graph = None;
4212 tracing::info!(
4213 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4214 cfg.kind,
4215 cfg.window,
4216 cfg.sink,
4217 self.kv_cache
4218 .layers
4219 .iter()
4220 .map(|l| l.bounded_state_bytes())
4221 .max()
4222 .unwrap_or(0)
4223 );
4224 Ok(())
4225 }
4226
4227 pub fn install_wire_identity(&mut self, identity: u64) {
4231 for li in 0..self.kv_cache.layers.len() {
4232 let pl = self.phys_layer(li);
4233 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4234 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4235 Some(AttnKind::Linear(_))
4236 | Some(AttnKind::LinearGdn(_))
4237 | Some(AttnKind::ShortConv(_))
4238 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4239 _ => crate::kv_cache::WireKind::Full,
4240 };
4241 let l = &mut self.kv_cache.layers[li];
4242 l.wire_kind = kind;
4243 l.wire_identity = identity;
4244 l.wire_layer = li as u32;
4247 }
4248 }
4249
4250 pub fn clear_history(&mut self) {
4252 self.kv_history.clear();
4253 self.kv_history_device = false;
4254 self.kv_prefix.clear();
4255 }
4256
4257 pub fn graph_refused(&self) -> bool {
4261 self.graph_refused
4262 .load(std::sync::atomic::Ordering::Relaxed)
4263 }
4264
4265 pub fn mark_graph_refused(&self) {
4267 if !self
4268 .graph_refused
4269 .swap(true, std::sync::atomic::Ordering::Relaxed)
4270 {
4271 tracing::info!(
4272 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4273 self.graph_kv_id
4274 );
4275 }
4276 }
4277
4278 pub fn device_sequence_position(&self) -> Option<usize> {
4282 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4283 }
4284
4285 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4290 let dev = self.device_sequence_position();
4291 if recorded_on_device {
4292 dev == Some(n) && self.embryo_resident_wanted()
4293 } else {
4294 dev.is_none()
4295 }
4296 }
4297
4298 pub(crate) fn invalidate_for_weight_change(&mut self) {
4306 self.clear_sequence_state();
4307 self.embryo_graph = None;
4308 }
4309
4310 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4316 let (n, on_device) = if self.bounded_native() {
4317 (
4318 self.kv_prefix.extension(input_ids),
4319 self.kv_prefix.on_device(),
4320 )
4321 } else {
4322 let h = &self.kv_history;
4323 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4324 (h.len(), self.kv_history_device)
4325 } else if !h.is_empty() && !self.kv_history_device {
4326 (self.rollback_prefix_len(input_ids), false)
4327 } else {
4328 (0, false)
4329 }
4330 };
4331 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4334 tracing::warn!(
4335 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4336 the device now holds {:?} — re-prefilling from zero",
4337 if on_device { "resident device" } else { "host" },
4338 self.device_sequence_position()
4339 );
4340 return 0;
4341 }
4342 n
4343 }
4344
4345 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4356 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4357 || self.device_sequence_position().is_some()
4358 {
4359 return 0;
4360 }
4361 let h = &self.kv_history;
4362 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4363 let n = lcp.min(input_ids.len().saturating_sub(1));
4364 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4365 return 0;
4366 }
4367 let kv_id = self.graph_kv_id;
4368 for li in 0..self.num_layers {
4369 let l = &self.kv_cache.layers[li];
4370 let tail_ok = l.base() == 0
4371 || self
4372 .layer_window(li)
4373 .is_some_and(|w| l.base() + w <= n + 1);
4374 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4375 || l.o1.is_some()
4376 || l.bounded.is_some()
4377 || !l.linear_state.is_empty()
4378 || l.mode != crate::kv_cache::KvMode::F32
4379 || l.pos_len() != h.len()
4380 || !tail_ok
4381 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4382 || crate::gpu::graph_state_resident(kv_id, li)
4383 {
4384 return 0;
4385 }
4386 }
4387 n
4388 }
4389
4390 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4393 self.cached_prefix_len(input_ids)
4394 }
4395
4396 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4402 let on_device = self.device_sequence_position().is_some();
4403 if self.bounded_native() {
4404 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4405 let prev_device = self.kv_prefix.on_device();
4406 self.kv_history.clear();
4407 self.kv_history_device = false;
4408 if keep && prev_device == on_device {
4409 self.kv_prefix.extend(&consumed[reused..]);
4410 } else {
4411 self.kv_prefix.set(consumed);
4412 }
4413 self.kv_prefix.set_on_device(on_device);
4414 } else {
4415 self.kv_history = consumed.to_vec();
4416 self.kv_history_device = on_device;
4417 }
4418 }
4419
4420 pub fn o1_active(&self) -> bool {
4422 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4423 }
4424
4425 pub fn generation_batch_k(&self) -> usize {
4443 if let Some(k) = std::env::var("CMF_BATCH_K")
4444 .ok()
4445 .and_then(|v| v.parse::<usize>().ok())
4446 {
4447 return k;
4448 }
4449 if self.g4_batch_prefill() {
4452 return 128;
4453 }
4454 #[cfg(not(target_os = "macos"))]
4459 if self.o1_active()
4460 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4461 && crate::gpu::enabled_here()
4462 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
4463 {
4464 return 128;
4465 }
4466 #[cfg(not(target_os = "macos"))]
4467 if self.graph_prefill_preferred() && !self.o1_active() {
4468 let moe = self
4472 .weights
4473 .layers
4474 .iter()
4475 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4476 let q82 = crate::gpu::moe_q82_graph_on()
4482 && self.weights.layers.iter().any(|lw| {
4483 matches!(&lw.ffn, FfnKind::Moe(m)
4484 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4485 });
4486 let q4tp_moe = self.weights.layers.iter().any(|lw| {
4493 matches!(&lw.ffn, FfnKind::Moe(m)
4494 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4495 });
4496 return if q82 {
4497 128
4498 } else if q4tp_moe || self.onevision2_prefill() {
4499 256
4500 } else if moe {
4501 32
4502 } else {
4503 128
4504 };
4505 }
4506 0
4507 }
4508
4509 pub fn generation_graph_prefill(&self) -> bool {
4510 let graph = self.graph_prefill_preferred();
4511 #[cfg(not(target_os = "macos"))]
4522 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4523 return false;
4524 }
4525 graph
4526 }
4527
4528 pub fn o1_device_stats(&self) -> (usize, u64) {
4533 crate::gpu::o1_device_stats(self.graph_kv_id)
4534 }
4535
4536 pub fn o1_begin(&mut self) {
4541 self.o1_begin_with_prefix(None);
4542 }
4543
4544 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4548 if let Some(c) = &self.o1_cfg {
4549 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4550 let boundary = requested_prefix.map(|p| {
4551 p.max(
4552 crate::nystrom::o1_deferred_boundary(w, sink)
4553 .expect("o1 config boundary validated in set_o1"),
4554 )
4555 });
4556 for (li, &f) in self.o1_flags.iter().enumerate() {
4557 if f {
4558 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4559 }
4560 }
4561 }
4562 }
4563
4564 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4566 self.o1_cfg.as_ref().and_then(|c| {
4567 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4568 .map(|floor| requested_prefix.max(floor))
4569 })
4570 }
4571
4572 fn o1_note_transition(&mut self) {
4573 let mut transitioned = false;
4577 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4578 if flagged {
4579 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4580 }
4581 }
4582 if transitioned {
4583 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4584 }
4585 }
4586
4587 fn o1_pending(&self) -> bool {
4588 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4589 f && self.kv_cache.layers[li].seq_len > 0
4590 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4591 })
4592 }
4593
4594 fn o1_fail(&mut self, err: String) {
4595 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4596 self.clear_sequence_state();
4597 self.graph_failed
4598 .store(true, std::sync::atomic::Ordering::Relaxed);
4599 self.cancel
4600 .store(true, std::sync::atomic::Ordering::Relaxed);
4601 }
4602
4603 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4608 if self.o1_cfg.is_none() {
4609 return Ok(false);
4610 }
4611 let mut participating = false;
4612 for li in 0..self.num_layers {
4613 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4614 continue;
4615 }
4616 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4617 return Err(err);
4618 }
4619 if self.kv_cache.layers[li].seq_len == 0 {
4620 continue;
4621 }
4622 participating = true;
4623 let num_heads = self.layer_num_heads(li);
4624 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4625 }
4626 self.o1_note_transition();
4627 for li in 0..self.num_layers {
4628 if self.o1_flags.get(li).copied().unwrap_or(false) {
4629 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4630 return Err(err);
4631 }
4632 }
4633 }
4634 Ok(participating
4635 && (0..self.num_layers).all(|li| {
4636 !self.o1_flags.get(li).copied().unwrap_or(false)
4637 || self.kv_cache.layers[li].seq_len == 0
4638 || self.kv_cache.layers[li].o1_sealed()
4639 }))
4640 }
4641
4642 fn o1_progress(&mut self) {
4645 if !self.o1_active() {
4646 return;
4647 }
4648 for li in 0..self.num_layers {
4649 if self.o1_flags.get(li).copied().unwrap_or(false) {
4650 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4651 self.o1_fail(err);
4652 return;
4653 }
4654 }
4655 }
4656 self.o1_note_transition();
4660 if !self.o1_pending() {
4661 return;
4662 }
4663 if let Err(err) = self.o1_seal_checked() {
4664 self.o1_fail(err);
4665 }
4666 }
4667
4668 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4673 if self
4674 .graph_failed
4675 .swap(false, std::sync::atomic::Ordering::Relaxed)
4676 {
4677 self.cancel
4678 .store(false, std::sync::atomic::Ordering::Relaxed);
4679 self.clear_sequence_state();
4680 return Err(format!("{phase}: deferred O(1) transition failed"));
4681 }
4682 Ok(())
4683 }
4684
4685 pub fn o1_seal(&mut self) {
4689 if let Err(err) = self.o1_seal_checked() {
4690 self.o1_fail(err);
4691 }
4692 }
4693
4694 fn o1_prefix_on_graph(
4703 &mut self,
4704 ids: &[u32],
4705 chunk: usize,
4706 ) -> Result<Option<Vec<f32>>, String> {
4707 let o1_layers: Vec<usize> = (0..self.num_layers)
4708 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
4709 .collect();
4710 let Some(&first) = o1_layers.first() else {
4711 return Ok(None);
4712 };
4713 let (nkv, hd) = {
4714 let c = &self.kv_cache.layers[first];
4715 (c.num_kv_heads, c.head_dim)
4716 };
4717 let nh = self.num_heads;
4718 if chunk == 0
4719 || o1_layers.iter().any(|&li| {
4720 let c = &self.kv_cache.layers[li];
4721 self.layer_num_heads(li) != nh
4722 || (c.num_kv_heads, c.head_dim) != (nkv, hd)
4723 || c.seq_len != 0
4724 || c.o1_pending_boundary().is_none()
4725 })
4726 || !crate::gpu::o1_qcap_begin(&o1_layers)
4727 {
4728 return Ok(None);
4729 }
4730 let hs = self.hidden_size;
4731 let n = ids.len();
4732 let mut last = vec![0f32; hs];
4733 let mut pos = 0;
4734 while pos < n {
4735 let end = (pos + chunk).min(n);
4736 let bk = end - pos;
4737 let mut hiddens = vec![0f32; bk * hs];
4738 for (j, &id) in ids[pos..end].iter().enumerate() {
4739 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4740 }
4741 let positions: Vec<usize> = (pos..end).collect();
4742 match self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None) {
4743 crate::gpu::BatchGraphOutcome::Completed => {
4744 last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4745 pos = end;
4746 }
4747 crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
4748 let _ = crate::gpu::o1_qcap_take();
4749 return Ok(None);
4750 }
4751 _ => {
4752 let _ = crate::gpu::o1_qcap_take();
4753 return Err("O(1) prefix batch graph stopped after admission".into());
4754 }
4755 }
4756 }
4757 let q = crate::gpu::o1_qcap_take().ok_or("O(1) prefix: query readback failed")?;
4758 let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
4759 let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
4760 .ok_or("O(1) prefix: K/V readback failed")?;
4761 let (row, qrow) = (nkv * hd, nh * hd);
4762 for ((li, qs), (k, v)) in q.into_iter().zip(kv) {
4763 if k.len() != n * row || v.len() != n * row || qs.len() != n * qrow {
4764 return Err(format!(
4765 "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
4766 k.len(),
4767 v.len(),
4768 qs.len(),
4769 n * row,
4770 n * row,
4771 n * qrow
4772 ));
4773 }
4774 let cache = &mut self.kv_cache.layers[li];
4775 for p in 0..n {
4776 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
4777 cache.o1_push_q(&qs[p * qrow..(p + 1) * qrow]);
4778 }
4779 }
4780 Ok(Some(last))
4781 }
4782
4783 pub fn set_trace(&mut self, on: bool) {
4785 self.trace = on;
4786 }
4787
4788 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4791 self.rng = match config.seed {
4792 Some(seed) => SplitMix64::new(seed),
4793 None => SplitMix64::from_entropy(),
4794 };
4795 self.sampler_config = config;
4796 }
4797
4798 pub fn set_confidence(&mut self, on: bool) {
4803 self.confidence_on = on;
4804 }
4805
4806 pub fn set_calib_temp(&mut self, t: f32) {
4809 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4810 }
4811
4812 pub fn calib_temp(&self) -> f32 {
4814 self.calib_temp
4815 }
4816
4817 pub fn rope_inv_freq(&self) -> &[f32] {
4821 self.inv_freq.as_slice()
4822 }
4823
4824 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4827 self.rotary_dim = rotary_dim.min(self.head_dim);
4828 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4829 self.embryo_graph = None;
4833 }
4834
4835 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4836 QwenAttnCfg {
4837 mrope: None,
4838 num_heads: self.num_heads,
4839 num_kv_heads: self.num_kv_heads,
4840 head_dim: self.head_dim,
4841 hidden_size: self.hidden_size,
4842 position,
4843 inv_freq: &self.inv_freq,
4844 rotary_dim: self.rotary_dim,
4845 scale: self.attn_scale,
4846 softcap: self.attn_softcap,
4847 window: None,
4848 v_norm: false,
4849 qk_norm_after_rope: self.qk_norm_after_rope,
4850 gate_sigmoid: self.proj_gate_sigmoid,
4851 q_norm: None,
4852 k_norm: None,
4853 output_gate: false,
4854 softplus_gate: None,
4855 rope_scale: self.rope_scale,
4856 bias: None,
4857 rms_eps: self.rms_eps,
4858 norm_style: self.norm_style,
4859 pool: self.pool.as_deref(),
4860 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4861 }
4862 }
4863
4864 pub fn generate(
4866 &mut self,
4867 prompt: &str,
4868 max_tokens: usize,
4869 task_mask: Option<&TaskMask>,
4870 on_token: Option<TokenCallback>,
4871 ) -> Result<GenerateResult, String> {
4872 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4873 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4874 }
4875
4876 pub fn generate_from_vl(
4879 &mut self,
4880 input: &crate::dsv41_vision::PreparedVlInputs,
4881 max_tokens: usize,
4882 task_mask: Option<&TaskMask>,
4883 on_token: Option<TokenCallback>,
4884 ) -> Result<GenerateResult, String> {
4885 let Some(dsv41) = &self.dsv41 else {
4886 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4887 };
4888 if input.token_ids.is_empty() {
4889 return Err("empty V4.1 multimodal prompt".into());
4890 }
4891 if input.token_types.len() != input.token_ids.len() {
4892 return Err(format!(
4893 "V4.1 token type count {} != token count {}",
4894 input.token_types.len(),
4895 input.token_ids.len()
4896 ));
4897 }
4898 let dim = dsv41.2.dim;
4899 let mut embeddings = vec![None; input.token_ids.len()];
4900 let mut participates = vec![true; input.token_ids.len()];
4901 if !input.images.is_empty() {
4902 let vision = self
4903 .dsv41_vision
4904 .as_ref()
4905 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4906 for image in &input.images {
4907 let end = image.start.saturating_add(image.types.len());
4908 if end > input.token_ids.len() {
4909 return Err(format!(
4910 "V4.1 image span {}..{} exceeds prompt length {}",
4911 image.start,
4912 end,
4913 input.token_ids.len()
4914 ));
4915 }
4916 let mut span = vec![0.0f32; image.types.len() * dim];
4917 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4918 for (offset, &kind) in image.types.iter().enumerate() {
4919 let pos = image.start + offset;
4920 if input.token_types[pos] != kind {
4921 return Err(format!(
4922 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4923 input.token_types[pos]
4924 ));
4925 }
4926 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4927 participates[pos] = false;
4928 }
4929 }
4930 }
4931 for (pos, &kind) in input.token_types.iter().enumerate() {
4932 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4933 return Err(format!("V4.1 text position {pos} has an image embedding"));
4934 }
4935 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4936 return Err(format!("V4.1 image position {pos} has no image embedding"));
4937 }
4938 }
4939 self.dsv41_prefill = Some((embeddings, participates));
4940 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4941 self.dsv41_prefill = None;
4942 result
4943 }
4944
4945 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4947 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4948 }
4949
4950 pub fn generate_from_ids(
4958 &mut self,
4959 input_ids: &[u32],
4960 max_tokens: usize,
4961 task_mask: Option<&TaskMask>,
4962 on_token: Option<TokenCallback>,
4963 ) -> Result<GenerateResult, String> {
4964 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4965 }
4966
4967 pub fn generate_from_embeds(
4973 &mut self,
4974 input_ids: &[u32],
4975 prompt_rows: &[f32],
4976 max_tokens: usize,
4977 task_mask: Option<&TaskMask>,
4978 on_token: Option<TokenCallback>,
4979 ) -> Result<GenerateResult, String> {
4980 if input_ids.is_empty()
4981 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4982 {
4983 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4984 }
4985 if prompt_rows.iter().any(|x| !x.is_finite()) {
4986 return Err("embedded prompt contains non-finite values".into());
4987 }
4988 if !self.can_prefill_batched()
4989 || self.dyn_router.is_some()
4990 || self.o1_active()
4991 || self.mtp.is_some()
4992 || self.gpu_plan.is_some()
4993 {
4994 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4995 }
4996 self.generate_with_prompt_rows(
4997 input_ids,
4998 Some(prompt_rows),
4999 max_tokens,
5000 task_mask,
5001 on_token,
5002 )
5003 }
5004
5005 fn generate_with_prompt_rows(
5006 &mut self,
5007 input_ids: &[u32],
5008 prompt_rows: Option<&[f32]>,
5009 max_tokens: usize,
5010 task_mask: Option<&TaskMask>,
5011 mut on_token: Option<TokenCallback>,
5012 ) -> Result<GenerateResult, String> {
5013 #[cfg(target_os = "macos")]
5014 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5015 if std::env::var("CMF_TRACE_H").is_ok() {
5016 eprintln!("input_ids: {input_ids:?}");
5017 }
5018 if input_ids.is_empty() {
5019 return Err("empty prompt: nothing to generate from".to_string());
5020 }
5021 self.graph_failed
5025 .store(false, std::sync::atomic::Ordering::Relaxed);
5026 let task_mask = self.drop_open_mask(task_mask);
5031 if self.clover.is_some() && !self.clover_causal_request() {
5034 if prompt_rows.is_some() || task_mask.is_some() {
5035 return Err("block-diffusion generation takes token prompts without task masks".into());
5036 }
5037 return self.clover_generate(input_ids, max_tokens, on_token);
5038 }
5039
5040 let mut reuse_from = {
5048 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
5049 if on
5050 && prompt_rows.is_none()
5051 && task_mask.is_none()
5052 && self.mtp.is_none()
5053 && !(self.mimo_mtp.is_some() && self.speculative)
5054 && self.o1_cfg.is_none()
5055 && self.dsv41.is_none()
5056 {
5057 self.cached_prefix_len(input_ids)
5058 } else {
5059 0
5060 }
5061 };
5062 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
5065 reuse_from = 0;
5066 }
5067 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
5070 for l in &mut self.kv_cache.layers {
5071 let d = l.pos_len() - reuse_from;
5072 l.truncate_last(d);
5073 }
5074 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5075 eprintln!(
5076 "kv-reuse: rolled back {} answer position(s) to the common prefix",
5077 self.kv_history.len() - reuse_from
5078 );
5079 }
5080 self.kv_history.truncate(reuse_from);
5081 self.graph_logits = None;
5082 }
5083 self.swa_trim_anchor = input_ids.len();
5084 self.last_prefill_tokens = input_ids.len() - reuse_from;
5085 let bounded_native = self.bounded_native();
5086 if reuse_from == 0 {
5087 self.reset_fresh_mtp_draft_state();
5092 self.clear_sequence_state();
5093 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
5094 eprintln!(
5095 "kv-reuse: {} of {} prompt positions already cached",
5096 reuse_from,
5097 input_ids.len()
5098 );
5099 }
5100 crate::gpu::graph_race_begin_generation();
5101 let o1_prefill = if self.o1_active() && task_mask.is_none() {
5105 std::env::var("CMF_O1_PREFILL")
5106 .ok()
5107 .and_then(|v| v.parse::<usize>().ok())
5108 .filter(|&p| p > 0)
5109 } else {
5110 None
5111 };
5112 if task_mask.is_none() {
5113 self.o1_begin_with_prefix(o1_prefill);
5114 }
5115
5116 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5122 #[cfg(target_os = "macos")]
5160 let metal_graph = crate::gpu::q1_force()
5161 && crate::gpu::enabled_here()
5162 && std::env::var("CMF_GPU_BLOCK")
5163 .map(|v| v != "0")
5164 .unwrap_or(true);
5165 #[cfg(target_os = "macos")]
5171 let native_metal_backend = crate::gpu::q1_force();
5172 #[cfg(not(target_os = "macos"))]
5173 let metal_graph = false;
5174 #[cfg(not(target_os = "macos"))]
5175 let native_metal_backend = false;
5176 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
5177 let spec_cheap_round =
5181 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
5182 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
5183 || match spec_sample_env.as_deref() {
5184 Some("1") => true,
5185 Some(_) => false,
5186 None => metal_graph && spec_cheap_round,
5187 };
5188 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
5204 for lw in &self.weights.layers {
5205 if let FfnKind::Dense(d) = &lw.ffn {
5206 dense_n += 1;
5207 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5208 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5209 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5210 {
5211 dense_q4tp += 1;
5212 }
5213 }
5214 }
5215 #[cfg(feature = "gpu")]
5224 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5225 #[cfg(not(feature = "gpu"))]
5226 let mtp_on_graph = true;
5227 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5228 let penalized = !metal_graph
5240 && (self.sampler_config.repetition_penalty != 1.0
5241 || self.sampler_config.presence_penalty != 0.0
5242 || !self.sampler_config.suppress_tokens.is_empty());
5243 #[cfg(feature = "gpu")]
5248 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5249 #[cfg(not(feature = "gpu"))]
5250 let metal_wgpu = false;
5251 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5252 let spec_wanted = graph_spec_wanted_by_policy(
5253 spec_env.as_deref(),
5254 spec_default_ok,
5255 penalized,
5256 metal_wgpu,
5257 native_metal_backend,
5258 );
5259 if spec_wanted && metal_wgpu {
5260 tracing::warn!(
5261 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5262 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5263 );
5264 }
5265 let graph_spec = self.speculative
5269 && (graph_on || metal_graph)
5270 && self.mtp.is_some()
5271 && task_mask.is_none()
5272 && !self.o1_active()
5273 && spec_sampling_ok
5274 && spec_wanted;
5275 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5288 let pair_pays = host_pair_opt_in
5289 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5290 #[cfg(target_os = "macos")]
5294 if metal_graph {
5295 static SAID: std::sync::Once = std::sync::Once::new();
5296 SAID.call_once(|| {
5297 let spec = if graph_spec {
5298 let k = std::env::var("CMF_GRAPH_SPEC_K")
5299 .ok()
5300 .and_then(|v| v.parse::<usize>().ok())
5301 .filter(|&v| (1..=8).contains(&v))
5302 .unwrap_or(7);
5303 let arm = if self.sampler_config.temperature < 1e-6 {
5304 "greedy"
5305 } else {
5306 "sampling"
5307 };
5308 format!(
5309 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5310 Self::draft_vocab_rows(usize::MAX)
5311 )
5312 } else if !self.speculative {
5313 "spec off (CMF_MTP=0)".to_string()
5314 } else if self.mtp.is_none() {
5315 "spec off (no MTP head)".to_string()
5316 } else if !spec_sampling_ok {
5317 if spec_cheap_round {
5318 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5319 } else {
5320 "spec off (sampling without a top-k: the dense chain \
5321 costs more than it saves)"
5322 .to_string()
5323 }
5324 } else if !spec_wanted {
5325 if spec_env.is_none() {
5326 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5327 } else {
5328 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5329 }
5330 } else if task_mask.is_some() {
5331 "spec off (task mask)".to_string()
5332 } else if !graph_spec && !pair_pays {
5333 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5334 .to_string()
5335 } else {
5336 "spec off (O(1) attention)".to_string()
5337 };
5338 let on = |var: &str| {
5339 if std::env::var(var).as_deref() == Ok("0") {
5340 "off"
5341 } else {
5342 "on"
5343 }
5344 };
5345 tracing::info!(
5346 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5347 MTP graph {}, attend {}, probe {}",
5348 if crate::gpu_metal::state4_on() {
5349 "on"
5350 } else {
5351 "off"
5352 },
5353 if crate::gpu_metal::async_replay_on() {
5354 "on"
5355 } else {
5356 "off"
5357 },
5358 on("CMF_METAL_PREFILL"),
5359 on("CMF_MTP_GRAPH"),
5360 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5361 if crate::gpu::probe_enabled() {
5362 "bypassed (q1 force)"
5363 } else {
5364 "off"
5365 },
5366 );
5367 });
5368 }
5369 let spec_active = self.speculative
5370 && self.mtp.is_some()
5371 && task_mask.is_none()
5372 && !self.o1_active()
5373 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5374 let mut mtp = if spec_active { self.mtp.take() } else { None };
5377 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5378 eprintln!(
5379 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5380 mtp.is_some(),
5381 self.speculative,
5382 self.sampler_config.temperature < 1e-6,
5383 );
5384 }
5385 if let Some(m) = &mut mtp {
5386 m.kv.clear();
5387 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5389 self.mtp_graph_mode = None;
5390 }
5391 let mimo_spec = self.speculative
5395 && self.mimo_mtp.is_some()
5396 && task_mask.is_none()
5397 && !self.o1_active()
5398 && self.dyn_router.is_none()
5399 && self.sampler_config.temperature < 1e-6
5400 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5401 if let Some(st) = self.mimo_mtp.as_mut() {
5402 st.reset();
5403 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5404 Self::mimo_mtp_hist_cap(st, input_ids.len());
5405 }
5406 }
5407 let mut router = if mtp.is_none() {
5411 self.dyn_router.take()
5412 } else {
5413 None
5414 };
5415 let mut reuse_from = reuse_from;
5416 if let Some(r) = &mut router {
5417 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5420 let _ = self.set_active_skill(None);
5423 reuse_from = 0;
5424 self.last_prefill_tokens = input_ids.len();
5425 }
5426 }
5427
5428 let mut all_ids = input_ids.to_vec();
5429 let penalty_from = if self.clover.is_some() { input_ids.len() } else { 0 };
5432 let mut generated = 0usize;
5433 let mut finish_reason = "max_tokens".to_string();
5434 let mut drafted = 0usize;
5435 let mut accepted = 0usize;
5436 let mut dsv4_spec_bad = 0usize;
5443 let mut dsv4_spec_retry_at = 0usize;
5444 let mut confidence: Vec<f32> = Vec::new();
5445 let trace_on = self.trace;
5446 let calib_temp = self.calib_temp;
5447 let mut traces: Vec<TokenTrace> = Vec::new();
5448
5449 let mut hidden = vec![0.0f32; self.hidden_size];
5455 let mut pos = reuse_from;
5456 let fuse_lm = mtp.is_none()
5465 && router.is_none()
5466 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5467 self.graph_logits = None;
5468 self.graph_want_logits = false;
5469 let _tpf = std::time::Instant::now();
5470 let batch_k = self.generation_batch_k();
5471 if let Some(rows) = prompt_rows {
5472 let hs = self.hidden_size;
5473 let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
5475 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5476 let end = (pos + chunk).min(input_ids.len());
5477 let hb = match self.prefill_input_rows(
5478 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5479 pos,
5480 task_mask,
5481 ) {
5482 Ok(hb) => hb,
5483 Err(err) => {
5484 self.prompt_mrope = None;
5485 self.finish_generation(&mut mtp, &mut router, true);
5486 return Err(err);
5487 }
5488 };
5489 if mimo_spec {
5490 self.mimo_note_rows(&hb, pos);
5491 }
5492 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5493 pos = end;
5494 }
5495 }
5496 self.prompt_mrope = None;
5499 while self.qwen4_exp.is_some()
5510 && mtp.is_none()
5511 && pos < input_ids.len()
5512 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5513 {
5514 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5517 let want_logits = end == input_ids.len();
5518 let mut lg = Vec::new();
5519 if let Some(b) = &mut self.qwen4_exp {
5520 crate::qwen4_exp::forward_tokens(
5521 &b.0,
5522 &b.1,
5523 &b.2,
5524 &mut b.3,
5525 &input_ids[pos..end],
5526 pos,
5527 &self.inv_freq,
5528 self.pool.as_deref(),
5529 &mut lg,
5530 want_logits,
5531 );
5532 }
5533 if want_logits {
5534 self.graph_logits = Some(lg);
5535 }
5536 pos = end;
5537 hidden.fill(0.0);
5538 }
5539 while self.dsv4.is_some()
5540 && mtp.is_none()
5541 && pos < input_ids.len()
5542 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5543 {
5544 let end = (pos + prefill_chunk()).min(input_ids.len());
5545 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5546 let mut lg = Vec::new();
5547 if let Some(b) = &mut self.dsv4 {
5548 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5549 crate::dsv4::forward_chunk(
5550 g,
5551 layers,
5552 &cfg,
5553 st,
5554 &ids,
5555 pos,
5556 &self.inv_freq,
5557 self.pool.as_deref(),
5558 &mut lg,
5559 end == input_ids.len(),
5560 );
5561 }
5562 if end == input_ids.len() {
5563 self.graph_logits = Some(lg);
5564 }
5565 pos = end;
5566 hidden = vec![0.0; self.hidden_size];
5567 }
5568 let dsv41_prefill = self.dsv41_prefill.take();
5569 while self.dsv41.is_some()
5570 && mtp.is_none()
5571 && pos < input_ids.len()
5572 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5573 {
5574 let end = (pos + prefill_chunk()).min(input_ids.len());
5575 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5576 let mut lg = Vec::new();
5577 if let Some(b) = &mut self.dsv41 {
5578 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5579 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5580 crate::dsv41::forward_chunk_masked_with_embeddings(
5581 g,
5582 layers,
5583 cfg,
5584 st,
5585 &ids,
5586 pos,
5587 &embeddings[pos..end],
5588 &participates[pos..end],
5589 self.pool.as_deref(),
5590 &mut lg,
5591 );
5592 } else {
5593 crate::dsv41::forward_chunk(
5594 g,
5595 layers,
5596 cfg,
5597 st,
5598 &ids,
5599 pos,
5600 self.pool.as_deref(),
5601 &mut lg,
5602 );
5603 }
5604 }
5605 if end == input_ids.len() {
5606 self.graph_logits = Some(lg);
5607 }
5608 pos = end;
5609 hidden = vec![0.0; self.hidden_size];
5610 }
5611 let dyn_prefill = router.is_some();
5616 let o1_prefill_limit = o1_prefill
5624 .and_then(|requested| self.o1_effective_boundary(requested))
5625 .map(|boundary| boundary.min(input_ids.len()));
5626 let mut o1_sealed = false;
5627 if let Some(limit) = o1_prefill_limit {
5628 if pos == 0
5635 && limit < input_ids.len()
5636 && batch_k > 0
5637 && task_mask.is_none()
5638 && mtp.is_none()
5639 && !dyn_prefill
5640 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5641 && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
5642 && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
5643 {
5644 let t_px = std::time::Instant::now();
5645 match self.o1_prefix_on_graph(&input_ids[..limit], batch_k) {
5646 Ok(Some(last)) => {
5647 hidden.copy_from_slice(&last);
5648 pos = limit;
5649 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5650 eprintln!(
5651 "o1 prefix on the batch graph: {limit} rows in {:.1} ms",
5652 t_px.elapsed().as_secs_f64() * 1e3
5653 );
5654 }
5655 }
5656 Ok(None) => {}
5657 Err(err) => {
5658 self.finish_generation(&mut mtp, &mut router, true);
5659 return Err(err);
5660 }
5661 }
5662 }
5663 if pos < limit && self.can_prefill_batched() && limit > 2 {
5666 let chunk = self.prefill_chunk();
5667 let hs = self.hidden_size;
5668 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5669 let end = (pos + chunk).min(limit);
5670 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5671 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5672 pos = end;
5673 }
5674 } else {
5675 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5676 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5677 pos += 1;
5678 }
5679 }
5680 if pos >= limit {
5681 o1_sealed = match self.o1_seal_checked() {
5682 Ok(sealed) => sealed,
5683 Err(err) => {
5684 self.finish_generation(&mut mtp, &mut router, true);
5685 return Err(err);
5686 }
5687 };
5688 tracing::info!(
5689 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5690 o1_prefill.unwrap_or(0),
5691 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5692 .unwrap_or(limit),
5693 limit,
5694 input_ids.len()
5695 );
5696 }
5697 }
5698 let graph_prefill = self.graph_prefill_preferred();
5704 #[cfg(target_os = "macos")]
5712 if task_mask.is_none()
5713 && !dyn_prefill
5714 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5715 && crate::gpu::enabled_here()
5716 && self.gdn_cfg.is_some()
5717 && !self.per_token_stack()
5718 && input_ids.len() > 8
5719 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5720 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5721 {
5722 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5723 .ok()
5724 .and_then(|v| v.parse().ok())
5725 .filter(|&v| (16..=512).contains(&v))
5726 .unwrap_or(256);
5727 let hs = self.hidden_size;
5728 let _tp = std::time::Instant::now();
5729 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5730 let end = (pos + chunk).min(input_ids.len());
5731 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5732 MetalPrefillOutcome::Completed(hb) => hb,
5733 MetalPrefillOutcome::Declined => break,
5734 MetalPrefillOutcome::Failed => {
5735 self.finish_generation(&mut mtp, &mut router, true);
5736 return Err("ordinary Metal prefill failed after admission".into());
5737 }
5738 };
5739 if let Some(m) = &mut mtp {
5740 let n_pairs = if end < input_ids.len() {
5741 end - pos
5742 } else {
5743 end - pos - 1
5744 };
5745 if n_pairs > 0 {
5746 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5747 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5748 .collect();
5749 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5750 for (j, (h, t)) in pairs.iter().enumerate() {
5751 let h = h.to_vec();
5752 let _ = self.mtp_step(m, &h, *t, pos + j);
5753 }
5754 }
5755 }
5756 }
5757 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5758 pos = end;
5759 }
5760 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5761 eprintln!(
5762 "metal-prefill: {} of {} tokens in {:.1} ms",
5763 pos,
5764 input_ids.len(),
5765 _tp.elapsed().as_secs_f64() * 1e3
5766 );
5767 }
5768 }
5769 #[cfg(not(target_os = "macos"))]
5774 if task_mask.is_none()
5775 && !dyn_prefill
5776 && mtp.is_none()
5777 && o1_prefill.is_none()
5778 && batch_k > 0
5779 && pos + 1 < input_ids.len()
5780 && self.g4_batch_prefill()
5781 {
5782 let t_g4 = std::time::Instant::now();
5783 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5784 Ok((n, last)) => {
5785 if n > 0 {
5786 hidden.copy_from_slice(&last);
5787 }
5788 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5789 eprintln!(
5790 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5791 input_ids.len() - pos,
5792 t_g4.elapsed().as_secs_f64() * 1e3
5793 );
5794 }
5795 pos += n;
5796 }
5797 Err(e) => {
5798 self.finish_generation(&mut mtp, &mut router, true);
5799 return Err(e);
5800 }
5801 }
5802 }
5803 self.mimo_moe_prepare();
5804 #[cfg(not(target_os = "macos"))]
5810 if task_mask.is_none()
5811 && !dyn_prefill
5812 && !graph_prefill
5813 && mtp.is_none()
5814 && o1_prefill.is_none()
5815 && !self.o1_active()
5816 && input_ids.len() > 2
5817 && self.batch_prefix_prefill()
5818 {
5819 let chunk = self.prefill_chunk().max(1);
5820 let hs = self.hidden_size;
5821 let t_bp = std::time::Instant::now();
5822 let pos0 = pos;
5823 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5824 let end = (pos + chunk).min(input_ids.len());
5825 let bk = end - pos;
5826 let mut hiddens = vec![0f32; bk * hs];
5827 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5828 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5829 }
5830 let positions: Vec<usize> = (pos..end).collect();
5831 let mut run = 0usize;
5832 let outcome = self.try_batch_graph_wgpu_prefix(
5833 &mut hiddens,
5834 &positions,
5835 bk,
5836 None,
5837 Some(&mut run),
5838 );
5839 match outcome {
5840 crate::gpu::BatchGraphOutcome::Completed => {
5841 let hb = if run < self.num_layers {
5842 self.prefill_batch_span(
5843 PrefillIn::Hidden(&hiddens),
5844 pos,
5845 None,
5846 run,
5847 self.num_layers,
5848 )
5849 } else {
5850 hiddens
5851 };
5852 if mimo_spec {
5853 self.mimo_note_rows(&hb, pos);
5854 }
5855 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5856 pos = end;
5857 }
5858 crate::gpu::BatchGraphOutcome::Failed => {
5859 self.finish_generation(&mut mtp, &mut router, true);
5860 return Err("batched prefix prefill failed after admission".into());
5861 }
5862 crate::gpu::BatchGraphOutcome::Declined => {
5863 #[cfg(feature = "gpu")]
5866 if pos > pos0 {
5867 self.pull_lagging_host_kv(0, self.num_layers, pos);
5868 }
5869 break;
5870 }
5871 }
5872 }
5873 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5874 eprintln!(
5875 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5876 pos - pos0,
5877 input_ids.len(),
5878 t_bp.elapsed().as_secs_f64() * 1e3
5879 );
5880 }
5881 }
5882 if task_mask.is_none()
5883 && !dyn_prefill
5884 && !graph_prefill
5885 && self.can_prefill_batched()
5886 && !self.per_token_stack()
5887 && o1_prefill.is_none()
5888 && input_ids.len() > 2
5889 {
5890 let chunk = self.prefill_chunk();
5896 let hs = self.hidden_size;
5897 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5898 let end = (pos + chunk).min(input_ids.len());
5899 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5900 if mimo_spec {
5901 self.mimo_note_rows(&hb, pos);
5902 }
5903 if let Some(m) = &mut mtp {
5904 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5905 .ok()
5906 .and_then(|v| v.parse().ok())
5907 .unwrap_or(0);
5908 for p in pos..end {
5909 if p + 1 < input_ids.len() {
5910 if probe >= 1 && p + 2 < input_ids.len() {
5911 let (d1, mut hx) = self.mtp_step_h(
5915 m,
5916 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5917 input_ids[p + 1],
5918 p,
5919 );
5920 let mut ok = d1 == input_ids[p + 2];
5921 Self::chain_probe_note(0, ok);
5922 let mut d_prev = d1;
5923 let mut extra = 0usize;
5924 for j in 1..probe {
5925 if p + 2 + j >= input_ids.len() {
5926 break;
5927 }
5928 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5929 extra += 1;
5930 ok = ok && dj == input_ids[p + 2 + j];
5931 Self::chain_probe_note(j, ok);
5932 d_prev = dj;
5933 hx = hj;
5934 }
5935 m.kv.truncate_last(extra);
5936 } else {
5937 let _ = self.mtp_step(
5938 m,
5939 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5940 input_ids[p + 1],
5941 p,
5942 );
5943 }
5944 }
5945 }
5946 }
5947 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5948 pos = end;
5949 }
5950 }
5951 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5952 if task_mask.is_none()
5953 && !dyn_prefill
5954 && !graph_prefill
5955 && !pair_off
5956 && self.pair_supported()
5957 && o1_prefill.is_none()
5958 {
5959 while pos + 1 < input_ids.len()
5960 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5961 {
5962 let e1 = self.embed_single(input_ids[pos]);
5963 let e2 = self.embed_single(input_ids[pos + 1]);
5964 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5965 if mimo_spec {
5966 self.mimo_note_rows(&h1, pos);
5967 self.mimo_note_rows(&h2, pos + 1);
5968 }
5969 self.commit_linear_scratch();
5971 if let Some(m) = &mut mtp {
5972 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5973 if pos + 2 < input_ids.len() {
5974 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5975 .ok()
5976 .and_then(|v| v.parse().ok())
5977 .unwrap_or(0);
5978 if probe >= 1 && pos + 3 < input_ids.len() {
5979 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5983 let mut ok = d1 == input_ids[pos + 3];
5984 Self::chain_probe_note(0, ok);
5985 let mut d_prev = d1;
5986 let mut extra = 0usize;
5987 for j in 1..probe {
5988 if pos + 3 + j >= input_ids.len() {
5989 break;
5990 }
5991 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5992 extra += 1;
5993 ok = ok && dj == input_ids[pos + 3 + j];
5994 Self::chain_probe_note(j, ok);
5995 d_prev = dj;
5996 hx = hj;
5997 }
5998 m.kv.truncate_last(extra);
5999 } else {
6000 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
6001 }
6002 }
6003 }
6004 hidden = h2;
6005 pos += 2;
6006 }
6007 }
6008 let o1_batch_ready = o1_sealed
6021 && o1_prefill.is_some()
6022 && mtp.is_none()
6023 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6024 && (0..self.num_layers).all(|li| {
6025 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6026 cache.o1.is_none() || cache.o1_views().is_some()
6027 });
6028 let mtp_batch_prefill = mtp.is_some()
6033 && graph_prefill
6034 && task_mask.is_none()
6035 && !dyn_prefill
6036 && !self.o1_active()
6037 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
6038 if batch_k > 0
6039 && (graph_prefill || o1_batch_ready)
6040 && task_mask.is_none()
6041 && (!self.o1_active() || o1_batch_ready)
6042 && (mtp.is_none() || mtp_batch_prefill)
6043 && !dyn_prefill
6044 && pos + 1 < input_ids.len()
6045 {
6046 let hs = self.hidden_size;
6047 let chunk = batch_k;
6048 while pos < input_ids.len() {
6049 let end = (pos + chunk).min(input_ids.len());
6050 let bk = end - pos;
6051 let mut hiddens = vec![0f32; bk * hs];
6052 for (j, &id) in input_ids[pos..end].iter().enumerate() {
6053 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6054 }
6055 let positions: Vec<usize> = (pos..end).collect();
6056 let t_chunk = std::time::Instant::now();
6057 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6058 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
6059 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6060 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
6061 eprintln!(
6062 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6063 if o1_batch_ready {
6064 "o1"
6065 } else if mtp_batch_prefill {
6066 "ordinary_mtp"
6067 } else {
6068 "ordinary"
6069 },
6070 bk as f64 / (ms / 1000.0)
6071 );
6072 }
6073 {
6074 use std::sync::atomic::{AtomicBool, Ordering};
6075 static SAID: AtomicBool = AtomicBool::new(false);
6076 if !SAID.swap(true, Ordering::Relaxed) {
6077 if ok_b {
6078 tracing::info!(
6079 "batched prefill: ACTIVE mode={} (k={bk})",
6080 if o1_batch_ready {
6081 "o1"
6082 } else if mtp_batch_prefill {
6083 "ordinary_mtp"
6084 } else {
6085 "ordinary"
6086 }
6087 );
6088 } else {
6089 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
6090 }
6091 }
6092 }
6093 if ok_b {
6094 if mimo_spec {
6095 self.mimo_note_rows(&hiddens, pos);
6096 }
6097 if mtp_batch_prefill {
6098 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
6099 if n_pairs > 0 {
6100 let rows: Vec<Vec<f32>> = (0..n_pairs)
6106 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
6107 .collect();
6108 let pairs: Vec<(&[f32], u32)> = rows
6109 .iter()
6110 .enumerate()
6111 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
6112 .collect();
6113 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6114 eprintln!(
6115 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
6116 pos,
6117 n_pairs,
6118 pos + n_pairs - 1,
6119 );
6120 }
6121 let warm_error = if let Some(m) = mtp.as_mut() {
6122 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
6123 } else {
6124 None
6125 };
6126 if let Some(err) = warm_error {
6127 self.finish_generation(&mut mtp, &mut router, true);
6132 return Err(err.to_string());
6133 }
6134 }
6135 }
6136 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6137 pos = end;
6138 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
6139 self.finish_generation(&mut mtp, &mut router, true);
6144 return Err(if o1_batch_ready {
6145 "sealed O(1) batch graph failed after admission".to_string()
6146 } else {
6147 "ordinary recurrent batch graph failed after admission".to_string()
6148 });
6149 } else {
6150 break; }
6152 }
6153 }
6154 if graph_prefill
6158 && task_mask.is_none()
6159 && mtp.is_none()
6160 && !dyn_prefill
6161 && pos == 0
6162 && input_ids.len() > 1
6163 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
6164 {
6165 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
6166 self.graph_logits = Some(lg);
6167 hidden = vec![0.0; self.hidden_size];
6168 pos = input_ids.len();
6169 }
6170 }
6171 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
6172 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
6173 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
6174 if mimo_spec {
6175 self.mimo_note_rows(&hidden, pos);
6176 }
6177 if let Some(m) = &mut mtp {
6178 if pos + 1 < input_ids.len() {
6179 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
6185 .ok()
6186 .and_then(|v| v.parse().ok())
6187 .unwrap_or(0);
6188 if probe >= 1 && pos + 2 < input_ids.len() {
6189 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
6190 let mut ok = d1 == input_ids[pos + 2];
6191 Self::chain_probe_note(0, ok);
6192 let mut d_prev = d1;
6193 let mut extra = 0usize;
6194 for j in 1..probe {
6195 if pos + 2 + j >= input_ids.len() {
6196 break;
6197 }
6198 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
6199 extra += 1;
6200 ok = ok && dj == input_ids[pos + 2 + j];
6201 Self::chain_probe_note(j, ok);
6202 d_prev = dj;
6203 hx = hj;
6204 }
6205 m.kv.truncate_last(extra);
6208 } else {
6209 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
6210 }
6211 }
6212 }
6213 pos += 1;
6214 }
6215 if std::env::var("CMF_PREFILL_PROF").is_ok() {
6216 eprintln!(
6217 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
6218 input_ids.len(),
6219 _tpf.elapsed().as_secs_f64() * 1000.0
6220 );
6221 }
6222 if self
6223 .graph_failed
6224 .swap(false, std::sync::atomic::Ordering::Relaxed)
6225 {
6226 self.finish_generation(&mut mtp, &mut router, true);
6231 return Err("GPU token graph failed during prefill".to_string());
6232 }
6233 if self
6236 .cancel
6237 .swap(false, std::sync::atomic::Ordering::Relaxed)
6238 {
6239 self.finish_generation(&mut mtp, &mut router, true);
6243 return Ok(GenerateResult {
6244 text: String::new(),
6245 token_ids: Vec::new(),
6246 prompt_tokens: input_ids.len(),
6247 tokens_generated: 0,
6248 finish_reason: "cancelled".to_string(),
6249 mtp_drafted: 0,
6250 mtp_accepted: 0,
6251 token_confidence: Vec::new(),
6252 traces: Vec::new(),
6253 });
6254 }
6255
6256 if !o1_sealed {
6259 match self.o1_seal_checked() {
6260 Ok(_) => {}
6261 Err(err) => {
6262 self.finish_generation(&mut mtp, &mut router, true);
6263 return Err(err);
6264 }
6265 }
6266 }
6267
6268 macro_rules! commit {
6270 ($id:expr) => {{
6271 all_ids.push($id);
6272 generated += 1;
6273 self.note_draft_id($id);
6274 if self.tokenizer.is_eos($id) && !self.ignore_eos {
6275 finish_reason = "stop".to_string();
6276 false
6277 } else {
6278 let token_text = self.tokenizer.decode_token($id);
6279 let mut go = true;
6280 if let Some(ref mut cb) = on_token {
6281 if !cb(&token_text) {
6282 finish_reason = "cancelled".to_string();
6283 go = false;
6284 }
6285 }
6286 go
6287 }
6288 }};
6289 }
6290
6291 let mut spec_trial = SpecTrial::Spec {
6302 t0: std::time::Instant::now(),
6303 gen0: generated,
6304 rounds: 0,
6305 };
6306 let mut spec_mon = SpecMon {
6312 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6313 ..SpecMon::default()
6314 };
6315 let mut spec_watchdog_off = false;
6316 let mut spec_walls: Vec<f32> = Vec::new();
6319 let mut spec_round_end: Option<std::time::Instant> = None;
6322 if mimo_spec {
6323 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6324 if let Some(mut st) = self.mimo_mtp.take() {
6325 self.mimo_mtp_probe(&mut st, input_ids, &path);
6326 self.mimo_mtp = Some(st);
6327 }
6328 }
6329 }
6330 let mut next_pos = input_ids.len();
6332 'decode: while generated < max_tokens {
6333 if self
6334 .graph_failed
6335 .swap(false, std::sync::atomic::Ordering::Relaxed)
6336 {
6337 self.finish_generation(&mut mtp, &mut router, true);
6342 return Err("GPU token graph failed during decode".to_string());
6343 }
6344 if self
6345 .cancel
6346 .swap(false, std::sync::atomic::Ordering::Relaxed)
6347 {
6348 finish_reason = "cancelled".to_string();
6349 break 'decode;
6350 }
6351 if mimo_spec && next_pos > 0 {
6356 self.mimo_note_rows(&hidden, next_pos - 1);
6359 }
6360 let forced = self.spec_forced.take();
6361 let mut logits = match (forced, self.graph_logits.take()) {
6362 (Some(_), _) => Vec::new(),
6363 (None, Some(lg)) => lg,
6364 (None, None) => {
6365 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6366 #[cfg(feature = "gpu")]
6371 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6372 lg
6373 } else {
6374 inference::rms_norm_into(
6375 &hidden,
6376 &self.weights.final_norm,
6377 self.rms_eps,
6378 self.norm_style,
6379 &mut self.ws.n1,
6380 );
6381 self.lm_head_forward(&self.ws.n1)
6382 };
6383 #[cfg(not(feature = "gpu"))]
6384 let logits_out = {
6385 inference::rms_norm_into(
6386 &hidden,
6387 &self.weights.final_norm,
6388 self.rms_eps,
6389 self.norm_style,
6390 &mut self.ws.n1,
6391 );
6392 self.lm_head_forward(&self.ws.n1)
6393 };
6394 logits_out
6395 }
6396 };
6397 if generated
6400 == std::env::var("CMF_LOGIT_DUMP_STEP")
6401 .ok()
6402 .and_then(|v| v.parse().ok())
6403 .unwrap_or(0)
6404 {
6405 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6406 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6407 for v in hidden.iter().chain(logits.iter()) {
6408 bytes.extend_from_slice(&v.to_le_bytes());
6409 }
6410 if let Err(e) = std::fs::write(&path, &bytes) {
6411 eprintln!("logit dump: failed to write {path}: {e}");
6412 self.finish_generation(&mut mtp, &mut router, true);
6413 return Err(format!("logit dump write failed: {e}"));
6414 }
6415 }
6416 }
6417 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6421 if !logits.is_empty() {
6422 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6423 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6424 if let Err(e) =
6425 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6426 {
6427 eprintln!("logit dump: failed to write {}: {e}", path.display());
6428 }
6429 }
6430 }
6431 self.clover_causal_logits(&mut logits, generated, &all_ids[penalty_from..]);
6432 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6433 Some(c) => c,
6434 None => {
6435 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6436 sampler::sample_with_scratch_pool(
6437 &logits,
6438 &self.sampler_config,
6439 self.sampler_config.penalty_past(&all_ids[penalty_from..], bounded_native),
6440 &mut self.rng,
6441 &mut self.sampler_scratch,
6442 self.pool.as_deref(),
6443 )
6444 }
6445 };
6446 if self.confidence_on {
6447 confidence.push(if logits.is_empty() {
6448 0.0
6449 } else {
6450 sampler::top1_prob_pool(
6451 self.pool.as_deref(),
6452 &mut self.sampler_scratch,
6453 &logits,
6454 t_next,
6455 calib_temp,
6456 )
6457 });
6458 }
6459 if !logits.is_empty() {
6460 attention::recycle_buf(&mut logits);
6461 }
6462 if trace_on {
6463 let skill = router.as_ref().and_then(|r| r.active_id());
6467 traces.push(TokenTrace {
6468 t: generated,
6469 token_id: t_next,
6470 confidence: confidence.last().copied().unwrap_or(0.0),
6471 active_skill: skill,
6472 recon: None,
6473 switched: false,
6474 });
6475 }
6476 if !commit!(t_next) {
6477 break 'decode;
6478 }
6479 if generated >= max_tokens {
6480 break 'decode;
6481 }
6482
6483 self.swa_trim_tails();
6486 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6487 static SAID: std::sync::Once = std::sync::Once::new();
6493 SAID.call_once(|| {
6494 tracing::warn!(
6495 "KV cache full at {} positions — evicting half; quality \
6496 will degrade. Raise CMF_MAX_SEQ.",
6497 self.kv_cache.max_seq_len,
6498 );
6499 });
6500 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6501 self.kv_cache.evict(keep);
6502 }
6503
6504 if graph_spec {
6507 match spec_trial {
6508 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6509 spec_mon.plain_ms =
6510 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6511 let keep = spec_mon.pays();
6512 tracing::info!(
6513 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6514 spec_mon.tokens,
6515 spec_mon.round_ms,
6516 spec_mon.plain_ms,
6517 if keep { "speculating" } else { "plain" }
6518 );
6519 spec_mon.fails = 0;
6520 spec_trial = SpecTrial::Decided {
6521 spec: keep,
6522 recheck_at: if keep { usize::MAX } else { generated + 128 },
6523 };
6524 }
6525 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6526 spec_mon.n = 0;
6527 spec_mon.zero_accept_rounds = 0;
6528 spec_trial = SpecTrial::Spec {
6529 t0: std::time::Instant::now(),
6530 gen0: generated,
6531 rounds: 0,
6532 };
6533 }
6534 _ => {}
6535 }
6536 spec_watchdog_off = matches!(
6537 spec_trial,
6538 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6539 );
6540 }
6541 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6543 let budget = max_tokens - generated - 1;
6544 if let Some(mut st) = self.mimo_mtp.take() {
6545 let k = st.depth.min(budget);
6546 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6547 self.mimo_mtp = Some(st);
6548 let r = match r {
6549 Ok(r) => r,
6550 Err(err) => {
6551 self.finish_generation(&mut mtp, &mut router, true);
6552 return Err(err);
6553 }
6554 };
6555 if let Some(r) = r {
6556 drafted += r.drafted;
6557 accepted += r.accepted.len();
6558 let mut stopped = false;
6559 for &id in &r.accepted {
6560 if self.confidence_on {
6561 confidence.push(0.0);
6562 }
6563 if !commit!(id) {
6564 stopped = true;
6565 break;
6566 }
6567 }
6568 if stopped {
6569 break 'decode;
6570 }
6571 next_pos += r.accepted.len() + 1;
6572 hidden = r.hidden;
6573 self.graph_logits = Some(r.logits);
6576 continue 'decode;
6577 }
6578 }
6579 }
6580 #[cfg(feature = "gpu")]
6583 if self.speculative
6584 && self.qwen4_exp.is_some()
6585 && task_mask.is_none()
6586 && self.sampler_config.temperature < 1e-6
6587 && generated + 1 < max_tokens
6588 && next_pos > 0
6589 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6590 {
6591 let r = match &mut self.qwen4_exp {
6592 Some(b) => crate::qwen4_exp::spec_round(
6593 &b.0,
6594 &b.1,
6595 &b.2,
6596 &mut b.3,
6597 next_pos,
6598 &all_ids,
6599 &self.inv_freq,
6600 self.pool.as_deref(),
6601 ),
6602 None => None,
6603 };
6604 if let Some(r) = r {
6605 drafted += r.drafted;
6606 accepted += r.accepted.len();
6607 let mut stopped = false;
6608 for &id in &r.accepted {
6609 if self.confidence_on {
6610 confidence.push(0.0);
6611 }
6612 if !commit!(id) {
6613 stopped = true;
6614 break;
6615 }
6616 }
6617 if stopped {
6618 break 'decode;
6619 }
6620 next_pos += r.accepted.len() + 1;
6621 hidden.fill(0.0);
6622 self.graph_logits = Some(r.logits);
6623 continue 'decode;
6624 }
6625 }
6626 match &mut mtp {
6627 #[cfg(feature = "gpu")]
6629 Some(m)
6630 if graph_spec
6631 && !spec_watchdog_off
6632 && generated + 1 < max_tokens
6633 && next_pos > 0 =>
6634 {
6635 let t_round = std::time::Instant::now();
6636 if spec_time_level() >= 2 {
6637 if let Some(t) = spec_round_end.take() {
6638 eprintln!(
6639 "spec-gap {:.2} ms (host between rounds)",
6640 t.elapsed().as_secs_f64() * 1e3
6641 );
6642 }
6643 }
6644 spec_stamps_begin();
6645 #[cfg(target_os = "macos")]
6650 let allocs0 =
6651 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6652 #[cfg(not(target_os = "macos"))]
6653 let allocs0 = 0u64;
6654 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6655 m,
6656 &hidden,
6657 t_next,
6658 next_pos,
6659 &mut drafted,
6660 &mut accepted,
6661 &mut all_ids,
6662 max_tokens - generated,
6663 ) {
6664 next_pos = n_pos;
6665 hidden = new_h;
6666 let level = spec_time_level();
6667 if level > 0 {
6668 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6669 let stamps = spec_stamps_take();
6670 let median = if spec_walls.len() >= 3 {
6673 let mut s = spec_walls.clone();
6674 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6675 Some(s[s.len() / 2])
6676 } else {
6677 None
6678 };
6679 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6680 #[cfg(target_os = "macos")]
6681 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6682 .load(std::sync::atomic::Ordering::Relaxed)
6683 - allocs0;
6684 #[cfg(not(target_os = "macos"))]
6685 let allocs = allocs0;
6686 eprintln!(
6687 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6688 extra.len() + 1,
6689 if allocs > 0 {
6690 format!(" [{allocs} new device buffers]")
6691 } else {
6692 String::new()
6693 },
6694 match (outlier, median) {
6695 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6696 _ => String::new(),
6697 }
6698 );
6699 if level >= 2 || outlier {
6700 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6701 eprintln!(
6702 "spec-stamps: {}| untracked {:.1}",
6703 spec_stamps_format(&stamps),
6704 wall - sum
6705 );
6706 }
6707 if spec_mon.n >= 1 {
6708 spec_walls.push(wall);
6709 }
6710 }
6711 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6715 spec_trial = Self::spec_trial_round(
6718 spec_trial,
6719 &mut spec_mon,
6720 generated + extra.len() + 1,
6721 );
6722 let mut stopped = false;
6723 for &id in &extra {
6724 if self.confidence_on {
6725 confidence.push(0.0);
6726 }
6727 if trace_on {
6740 traces.push(TokenTrace {
6741 t: generated,
6742 token_id: id,
6743 confidence: confidence.last().copied().unwrap_or(0.0),
6744 active_skill: None,
6745 recon: None,
6746 switched: false,
6747 });
6748 }
6749 if !commit!(id) {
6750 stopped = true;
6751 break;
6752 }
6753 }
6754 if stopped {
6755 break 'decode;
6756 }
6757 if spec_time_level() >= 2 {
6758 spec_round_end = Some(std::time::Instant::now());
6759 }
6760 continue 'decode;
6761 }
6762 if self
6763 .graph_failed
6764 .swap(false, std::sync::atomic::Ordering::Relaxed)
6765 {
6766 self.finish_generation(&mut mtp, &mut router, true);
6772 return Err("GPU MTP graph failed during speculative decode".to_string());
6773 }
6774 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6785 spec_mon.tokens = 0.0;
6786 spec_mon.fails = 3;
6787 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6788 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6789 next_pos += 1;
6790 continue 'decode;
6791 }
6792 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6794 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6795 drafted += 1;
6796 let emb1 = self.embed_single(t_next);
6797 let emb2 = self.embed_single(draft);
6798 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6799
6800 inference::rms_norm_into(
6801 &h1,
6802 &self.weights.final_norm,
6803 self.rms_eps,
6804 self.norm_style,
6805 &mut self.ws.n1,
6806 );
6807 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6808 let t_after = sampler::sample_with_scratch_pool(
6809 &logits1,
6810 &self.sampler_config,
6811 self.sampler_config.penalty_past(&all_ids, bounded_native),
6812 &mut self.rng,
6813 &mut self.sampler_scratch,
6814 self.pool.as_deref(),
6815 );
6816 if self.confidence_on {
6817 confidence.push(sampler::top1_prob_pool(
6818 self.pool.as_deref(),
6819 &mut self.sampler_scratch,
6820 &logits1,
6821 t_after,
6822 calib_temp,
6823 ));
6824 }
6825 attention::recycle_buf(&mut logits1);
6826 if trace_on {
6827 traces.push(TokenTrace {
6830 t: generated,
6831 token_id: t_after,
6832 confidence: confidence.last().copied().unwrap_or(0.0),
6833 active_skill: None,
6834 recon: None,
6835 switched: false,
6836 });
6837 }
6838 let stop = !commit!(t_after);
6839
6840 if t_after == draft {
6841 accepted += 1;
6842 self.commit_linear_scratch();
6843 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6844 hidden = h2;
6845 next_pos += 2;
6846 } else {
6847 for layer in &mut self.kv_cache.layers {
6849 layer.truncate_last(1);
6850 }
6851 if !stop {
6852 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6853 hidden = self.forward_layers(
6854 &self.embed_single(t_after),
6855 next_pos + 1,
6856 None,
6857 );
6858 }
6859 next_pos += 2;
6860 }
6861 if stop {
6862 break 'decode;
6863 }
6864 }
6865 _ => {
6867 #[cfg(feature = "gpu")]
6872 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6873 static SAID: std::sync::Once = std::sync::Once::new();
6874 SAID.call_once(|| {
6875 eprintln!(
6876 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6877 !self.dsv4_mtp.is_empty(),
6878 task_mask.is_none(),
6879 router.is_none(),
6880 !trace_on,
6881 self.sampler_config.temperature < 1e-6,
6882 self.sampler_config.repetition_penalty == 1.0,
6883 );
6884 });
6885 }
6886 #[cfg(feature = "gpu")]
6887 if Self::dsv4_spec_on()
6888 && self.dsv4.is_some()
6889 && !self.dsv4_mtp.is_empty()
6890 && task_mask.is_none()
6891 && router.is_none()
6892 && !trace_on
6893 && self.sampler_config.temperature < 1e-6
6894 && self.sampler_config.repetition_penalty == 1.0
6895 && generated + 1 < max_tokens
6896 && all_ids.len() >= 2
6897 && generated >= dsv4_spec_retry_at
6898 {
6899 let tip_token = all_ids[all_ids.len() - 2];
6900 let drafted0 = drafted;
6901 let round = self.dsv4_spec_step(
6902 tip_token,
6903 t_next,
6904 next_pos,
6905 max_tokens.saturating_sub(generated),
6906 &mut drafted,
6907 &mut accepted,
6908 );
6909 if drafted > drafted0 {
6910 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6911 if useful {
6912 dsv4_spec_bad = 0;
6913 } else {
6914 dsv4_spec_bad += 1;
6915 if dsv4_spec_bad >= 2 {
6916 dsv4_spec_bad = 0;
6917 dsv4_spec_retry_at = generated.saturating_add(32);
6918 tracing::info!(
6919 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6920 );
6921 }
6922 }
6923 }
6924 if let Some((extra, n_pos)) = round {
6925 next_pos = n_pos;
6926 let mut stopped = false;
6927 for &id in &extra {
6928 if self.confidence_on {
6929 confidence.push(0.0);
6930 }
6931 if !commit!(id) {
6932 stopped = true;
6933 break;
6934 }
6935 }
6936 if stopped {
6937 break 'decode;
6938 }
6939 continue 'decode;
6940 }
6941 }
6942 self.graph_want_logits = fuse_lm;
6943 let mut t_fwd = t_next;
6949 let pure_greedy = self.sampler_config.temperature < 1e-6
6950 && self.sampler_config.repetition_penalty == 1.0
6951 && self.sampler_config.presence_penalty == 0.0
6952 && self.sampler_config.suppress_tokens.is_empty();
6953 let burst_k = std::env::var("CMF_MULTISTEP")
6958 .ok()
6959 .and_then(|v| v.parse::<usize>().ok())
6960 .unwrap_or(0);
6961 if pure_greedy
6962 && burst_k >= 1
6963 && fuse_lm
6964 && task_mask.is_none()
6965 && router.is_none()
6966 && !trace_on
6967 && !self.confidence_on
6968 && std::env::var_os("CMF_FORCE_IDS").is_none()
6973 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6974 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6975 {
6976 let mut stopped = false;
6977 loop {
6978 if self
6983 .cancel
6984 .swap(false, std::sync::atomic::Ordering::Relaxed)
6985 {
6986 finish_reason = "cancelled".to_string();
6987 stopped = true;
6988 break;
6989 }
6990 let room = max_tokens.saturating_sub(generated);
6991 if room <= 2 {
6992 break;
6993 }
6994 let k = burst_k.min(room - 1);
6995 if k < 1 {
6996 break;
6997 }
6998 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6999 if self
7000 .graph_failed
7001 .swap(false, std::sync::atomic::Ordering::Relaxed)
7002 {
7003 self.finish_generation(&mut mtp, &mut router, true);
7004 return Err(
7005 "GPU token graph failed during greedy burst".to_string()
7006 );
7007 }
7008 break;
7009 };
7010 next_pos += k;
7011 for &id in &ids {
7012 if !commit!(id) {
7013 stopped = true;
7014 break;
7015 }
7016 }
7017 if stopped {
7018 break;
7019 }
7020 t_fwd = *ids.last().unwrap();
7021 }
7022 if stopped {
7023 break 'decode;
7024 }
7025 }
7026 #[cfg(target_os = "macos")]
7036 if graph_spec
7037 && spec_watchdog_off
7038 && next_pos > 0
7039 && self.mtp_graph_mode == Some(true)
7040 && crate::gpu::q1_force()
7041 {
7042 if let Some(m) = mtp.as_mut() {
7043 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
7044 }
7045 }
7046 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
7047 next_pos += 1;
7048 if let Some(r) = &mut router {
7051 let phi = self.dyn_phi_ema.clone();
7052 let decision = r.step(&phi, generated);
7053 if let Some(new_active) = decision {
7054 let _ = self.set_active_skill(new_active);
7055 }
7056 if trace_on {
7059 if let Some(last) = traces.last_mut() {
7060 let e = r.last_best_e();
7061 last.recon = e.is_finite().then_some(e);
7062 last.switched = decision.is_some();
7063 }
7064 }
7065 }
7066 }
7067 }
7068 }
7069
7070 let cancelled = finish_reason == "cancelled";
7071 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
7075 if mimo_spec {
7076 if let Some(st) = self.mimo_mtp.as_ref() {
7077 let line = st.stats.line();
7078 tracing::info!("{line}");
7079 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
7080 eprintln!("{line}");
7081 }
7082 }
7083 }
7084 self.finish_generation(&mut mtp, &mut router, cancelled);
7085
7086 let output_ids = &all_ids[input_ids.len()..];
7087 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
7091 let consumed = std::mem::take(&mut all_ids);
7096 if dyn_switched {
7097 self.clear_sequence_state();
7098 } else if cancelled || mimo_spec || prompt_rows.is_some() {
7099 self.clear_history();
7100 } else {
7101 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
7102 }
7103 all_ids = consumed;
7104 let output_ids = &all_ids[input_ids.len()..];
7105 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
7107 Ok(GenerateResult {
7108 text: self.tokenizer.decode(output_ids),
7109 token_ids: output_ids.to_vec(),
7110 prompt_tokens: input_ids.len(),
7111 tokens_generated: generated,
7112 finish_reason,
7113 mtp_drafted: drafted,
7114 mtp_accepted: accepted,
7115 token_confidence: confidence,
7116 traces,
7117 })
7118 }
7119
7120 fn mtp_step(
7124 &mut self,
7125 m: &mut MtpModule,
7126 hidden: &[f32],
7127 next_token: u32,
7128 position: usize,
7129 ) -> u32 {
7130 self.mtp_step_h(m, hidden, next_token, position).0
7131 }
7132
7133 fn chain_probe_note(depth: usize, prefix_ok: bool) {
7137 use std::sync::Mutex;
7138 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
7139 let mut t = T.lock().unwrap();
7140 if t.len() <= depth {
7141 t.resize(depth + 1, (0, 0));
7142 }
7143 t[depth].0 += 1;
7144 t[depth].1 += prefix_ok as u64;
7145 if depth == 0 && t[0].0 % 128 == 0 {
7146 let line: Vec<String> = t
7147 .iter()
7148 .enumerate()
7149 .map(|(d, (n, k))| {
7150 format!(
7151 "d{}={:.0}%({n})",
7152 d + 1,
7153 100.0 * *k as f64 / (*n).max(1) as f64
7154 )
7155 })
7156 .collect();
7157 eprintln!("mtp-chain: {}", line.join(" "));
7158 }
7159 }
7160
7161 fn mtp_step_hl(
7169 &mut self,
7170 m: &mut MtpModule,
7171 hidden: &[f32],
7172 next_token: u32,
7173 position: usize,
7174 ) -> (Vec<f32>, Vec<f32>) {
7175 #[cfg(target_os = "macos")]
7180 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
7181 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
7182 self.mtp_graph_mode = Some(true);
7183 return r;
7184 }
7185 if self.mtp_graph_mode == Some(true) {
7186 tracing::error!("mtp Metal graph failed after admission");
7187 self.clear_sequence_state();
7188 self.graph_failed
7189 .store(true, std::sync::atomic::Ordering::Relaxed);
7190 self.cancel
7191 .store(true, std::sync::atomic::Ordering::Relaxed);
7192 return (Vec::new(), Vec::new());
7193 }
7194 self.mtp_graph_mode = Some(false);
7195 }
7196 #[cfg(feature = "gpu")]
7197 if self.mtp_graph_mode != Some(false) {
7198 if !self.mtp_graph_ok(m) {
7199 if self.mtp_graph_mode == Some(true) {
7200 tracing::error!("mtp graph became unavailable after admission");
7205 self.clear_sequence_state();
7206 self.graph_failed
7207 .store(true, std::sync::atomic::Ordering::Relaxed);
7208 self.cancel
7209 .store(true, std::sync::atomic::Ordering::Relaxed);
7210 return (Vec::new(), Vec::new());
7211 }
7212 self.mtp_graph_mode = Some(false);
7213 } else {
7214 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
7215 self.mtp_graph_mode = Some(true);
7216 return r;
7217 }
7218 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7219 return (Vec::new(), Vec::new());
7226 }
7227 tracing::error!("mtp graph failed or declined after admission");
7231 self.clear_sequence_state();
7232 self.graph_failed
7233 .store(true, std::sync::atomic::Ordering::Relaxed);
7234 self.cancel
7235 .store(true, std::sync::atomic::Ordering::Relaxed);
7236 return (Vec::new(), Vec::new());
7237 }
7238 }
7239 let e = self.embed_single(next_token);
7243 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7244 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7245 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7246 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7247 let mut x = vec![0.0f32; self.hidden_size];
7248 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7249
7250 let lw = &m.layer;
7252 inference::rms_norm_into(
7253 &x,
7254 &lw.input_norm,
7255 self.rms_eps,
7256 self.norm_style,
7257 &mut self.ws.n1,
7258 );
7259 let attn = match &lw.attn {
7260 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7262 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7263 AttnKind::Full {
7264 wq,
7265 wk,
7266 wv,
7267 wo,
7268 q_norm,
7269 k_norm,
7270 output_gate,
7271 softplus_gate,
7272 bias,
7273 } => {
7274 let mut cfg = self.attn_cfg(position);
7275 cfg.q_norm = q_norm.as_deref();
7276 cfg.k_norm = k_norm.as_deref();
7277 cfg.output_gate = *output_gate;
7278 cfg.softplus_gate = softplus_gate
7279 .as_ref()
7280 .map(|(gate, per_head)| (gate, *per_head));
7281 cfg.bias = bias
7282 .as_ref()
7283 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7284 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7285 }
7286 AttnKind::Linear(_)
7287 | AttnKind::LinearGdn(_)
7288 | AttnKind::ShortConv(_)
7289 | AttnKind::Bounded(_) => {
7290 unreachable!("MTP block is full attention")
7291 }
7292 };
7293 for (i, &a) in attn.iter().enumerate() {
7294 x[i] += a;
7295 }
7296 inference::rms_norm_into(
7297 &x,
7298 &lw.post_norm,
7299 self.rms_eps,
7300 self.norm_style,
7301 &mut self.ws.p1,
7302 );
7303 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7304 for (i, &f) in ffn.iter().enumerate() {
7305 x[i] += f;
7306 }
7307
7308 inference::rms_norm_into(
7309 &x,
7310 &m.final_norm,
7311 self.rms_eps,
7312 self.norm_style,
7313 &mut self.ws.n1,
7314 );
7315 let lg = self.lm_head_forward(&self.ws.n1);
7316 (lg, x)
7317 }
7318
7319 fn mtp_step_h(
7321 &mut self,
7322 m: &mut MtpModule,
7323 hidden: &[f32],
7324 next_token: u32,
7325 position: usize,
7326 ) -> (u32, Vec<f32>) {
7327 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7328 let draft = sampler::argmax(&lg);
7329 attention::recycle_buf(&mut lg);
7330 (draft, x)
7331 }
7332
7333 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7339 match trial {
7340 SpecTrial::Spec { t0, gen0, rounds } => {
7341 let rounds = rounds + 1;
7342 if mon.repeated_zero_acceptance() {
7350 tracing::info!(
7351 "speculation trial: no accepted drafts in two rounds — timing plain"
7352 );
7353 return SpecTrial::Plain {
7354 t0: std::time::Instant::now(),
7355 gen0: generated,
7356 };
7357 }
7358 if rounds >= 5 {
7359 if mon.plain_ms > 0.0 {
7360 let keep = mon.pays();
7361 mon.fails = 0;
7362 tracing::info!(
7363 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7364 mon.tokens,
7365 mon.round_ms,
7366 mon.plain_ms,
7367 if keep { "speculating" } else { "plain" }
7368 );
7369 SpecTrial::Decided {
7370 spec: keep,
7371 recheck_at: if keep { usize::MAX } else { generated + 128 },
7372 }
7373 } else if mon.pays() {
7374 mon.fails = 0;
7379 tracing::info!(
7380 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7381 mon.tokens,
7382 mon.round_ms,
7383 );
7384 SpecTrial::Decided {
7385 spec: true,
7386 recheck_at: usize::MAX,
7387 }
7388 } else {
7389 SpecTrial::Plain {
7390 t0: std::time::Instant::now(),
7391 gen0: generated,
7392 }
7393 }
7394 } else {
7395 SpecTrial::Spec { t0, gen0, rounds }
7396 }
7397 }
7398 SpecTrial::Decided { spec: true, .. } => {
7399 if mon.pays() {
7400 mon.fails = 0;
7401 trial
7402 } else {
7403 mon.fails += 1;
7404 if mon.fails >= 4 {
7405 if mon.plain_ms <= 0.0 {
7406 tracing::info!(
7410 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7411 mon.tokens,
7412 mon.round_ms,
7413 );
7414 return SpecTrial::Plain {
7415 t0: std::time::Instant::now(),
7416 gen0: generated,
7417 };
7418 }
7419 tracing::info!(
7420 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7421 mon.tokens,
7422 mon.round_ms,
7423 mon.plain_ms
7424 );
7425 SpecTrial::Decided {
7426 spec: false,
7427 recheck_at: generated + 128,
7428 }
7429 } else {
7430 trial
7431 }
7432 }
7433 }
7434 other => other,
7435 }
7436 }
7437
7438 fn mtp_kv_id(&self) -> u64 {
7441 self.graph_kv_id | (1u64 << 40)
7442 }
7443
7444 const MTP_LAYER_BASE: usize = 0;
7449
7450 #[cfg(feature = "gpu")]
7457 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7458 self.mtp_graph_mode != Some(true)
7459 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7460 }
7461
7462 #[cfg(feature = "gpu")]
7468 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7469 let mut ok = true;
7470 let mut expected = false;
7471 for li in 0..self.num_layers {
7472 if matches!(
7473 self.weights.layers[self.phys_layer(li)].attn,
7474 AttnKind::Full { .. }
7475 ) {
7476 expected = true;
7477 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7478 }
7479 }
7480 !expected || ok
7481 }
7482
7483 fn graph_gdn_layer_count(&self) -> usize {
7487 (0..self.num_layers)
7488 .filter(|&li| {
7489 matches!(
7490 &self.weights.layers[self.phys_layer(li)].attn,
7491 AttnKind::LinearGdn(_)
7492 )
7493 })
7494 .count()
7495 }
7496
7497 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7500 let e = self.embed_single(next_token);
7501 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7502 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7503 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7504 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7505 let mut x = vec![0.0f32; self.hidden_size];
7506 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7507 x
7508 }
7509
7510 #[cfg(feature = "gpu")]
7513 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7514 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7515 return false;
7516 }
7517 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7518 || !crate::gpu::enabled_here()
7519 || self.attn_softcap > 0.0
7520 || self.attention_heads_per_layer.is_some()
7521 || self.v_head_dim.is_some()
7524 {
7525 return false;
7526 }
7527 matches!(
7528 &m.layer.attn,
7529 AttnKind::Full {
7530 softplus_gate: None,
7531 ..
7532 }
7533 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7534 }
7535
7536 #[cfg(feature = "gpu")]
7540 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7541 if !self.mtp_block_graph_ok(m) {
7542 return false;
7543 }
7544 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7545 return false;
7546 };
7547 let FfnKind::Dense(d) = &m.layer.ffn else {
7548 return false;
7549 };
7550 d.segs.is_empty()
7551 && wq.graph_weight().is_some()
7552 && wk.graph_weight().is_some()
7553 && wv.graph_weight().is_some()
7554 && wo.graph_weight().is_some()
7555 && d.gate_proj.graph_weight().is_some()
7556 && d.up_proj.graph_weight().is_some()
7557 && d.down_proj.graph_weight().is_some()
7558 && self.weights.lm_head.graph_weight().is_some()
7559 }
7560
7561 #[cfg(feature = "gpu")]
7567 fn mtp_step_graph(
7568 &mut self,
7569 m: &mut MtpModule,
7570 hidden: &[f32],
7571 next_token: u32,
7572 position: usize,
7573 ) -> Option<(Vec<f32>, Vec<f32>)> {
7574 if !self.mtp_graph_ok(m) {
7575 return None;
7576 }
7577 let lw = &m.layer;
7578 let AttnKind::Full {
7579 wq,
7580 wk,
7581 wv,
7582 wo,
7583 q_norm,
7584 k_norm,
7585 output_gate,
7586 softplus_gate,
7587 bias,
7588 } = &lw.attn
7589 else {
7590 return None;
7591 };
7592 if softplus_gate.is_some() {
7593 return None;
7594 }
7595 let FfnKind::Dense(d) = &lw.ffn else {
7596 return None;
7597 };
7598 if !d.segs.is_empty() {
7599 return None; }
7601 let mut x = self.mtp_block_input(m, hidden, next_token);
7604 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7605 let (_, i, kind, rs) = t.graph_weight()?;
7606 Some(crate::gpu::GraphW {
7607 idx: i,
7608 kind,
7609 row_scale: rs,
7610 data: &[],
7611 prism: crate::gpu::GraphPrismOp::None,
7612 affine: false,
7613 })
7614 }
7615 let (model, _, _, _) = wq.graph_weight()?;
7616 let model = model.clone();
7617 let (lm_gw, lm_rows) = {
7618 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7619 let rows = if kind == 6 {
7623 self.draft_head_rows(self.weights.lm_head.rows())
7624 } else {
7625 self.weights.lm_head.rows()
7626 };
7627 (
7628 crate::gpu::GraphW {
7629 idx: i,
7630 kind,
7631 row_scale: rs,
7632 data: &[],
7633 prism: crate::gpu::GraphPrismOp::None,
7634 affine: false,
7635 },
7636 rows,
7637 )
7638 };
7639 let layer = crate::gpu::GraphLayer {
7640 mhc: None,
7641 input_norm: &lw.input_norm,
7642 attn: crate::gpu::GraphAttn::Full {
7643 wq: gw(wq)?,
7644 wk: gw(wk)?,
7645 wv: gw(wv)?,
7646 wo: gw(wo)?,
7647 q_norm: q_norm.as_deref(),
7648 k_norm: k_norm.as_deref(),
7649 late_qk_norm: self.qk_norm_after_rope,
7650 bias: bias
7651 .as_ref()
7652 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7653 output_gate: *output_gate,
7654 cpu_k: m.kv.k_heads(),
7655 cpu_v: m.kv.v_heads(),
7656 cpu_base: m.kv.base(),
7657 geom: None,
7658 head_gate: None,
7659 },
7660 post_norm: &lw.post_norm,
7661 attn_out_norm: None,
7662 ffn_out_norm: None,
7663 layer_scale: None,
7664 ffn: crate::gpu::GraphFfn::Dense {
7665 gate: gw(&d.gate_proj)?,
7666 up: gw(&d.up_proj)?,
7667 down: gw(&d.down_proj)?,
7668 act: d.act.graph_act()?,
7669 },
7670 };
7671 let nh = self.num_heads;
7672 let (nkv, hd, rd) = self.layer_geom(0);
7673 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7674 let mut logits = Vec::new();
7675 let ok = crate::gpu::forward_token_graph(
7676 &model,
7677 self.mtp_kv_id(),
7678 std::slice::from_ref(&layer),
7679 &[None],
7680 self.o1_epoch,
7681 &self.inv_freq,
7682 &mut x,
7683 nh,
7684 nkv,
7685 hd,
7686 self.attn_scale,
7687 rd,
7688 self.hidden_size,
7689 d.gate_proj.rows(),
7693 position,
7694 self.kv_cache.max_seq_len,
7695 gemma,
7696 self.rms_eps as f32,
7697 Some((&lm_gw, lm_rows)),
7698 &m.final_norm,
7699 &mut logits,
7700 &[],
7701 1,
7702 None,
7703 None,
7704 None,
7705 Self::MTP_LAYER_BASE,
7706 true,
7707 );
7708 match ok {
7709 crate::gpu::TokenGraphOutcome::Completed => {}
7710 crate::gpu::TokenGraphOutcome::Declined => return None,
7711 crate::gpu::TokenGraphOutcome::Failed => {
7712 self.clear_sequence_state();
7716 self.graph_failed
7717 .store(true, std::sync::atomic::Ordering::Relaxed);
7718 self.cancel
7719 .store(true, std::sync::atomic::Ordering::Relaxed);
7720 return None;
7721 }
7722 }
7723 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7727 Some((logits, x))
7728 }
7729
7730 #[cfg(feature = "gpu")]
7738 fn mtp_warm_graph(
7739 &mut self,
7740 m: &mut MtpModule,
7741 pairs: &[(&[f32], u32)],
7742 first_pos: usize,
7743 ) -> crate::gpu::BatchGraphOutcome {
7744 if pairs.is_empty() {
7745 return crate::gpu::BatchGraphOutcome::Completed;
7746 }
7747 if !self.mtp_block_graph_ok(m) {
7748 return crate::gpu::BatchGraphOutcome::Declined;
7749 }
7750 let hs = self.hidden_size;
7751 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7754 for (h, t) in pairs {
7755 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7756 }
7757 let lw = &m.layer;
7758 let AttnKind::Full {
7759 wq,
7760 wk,
7761 wv,
7762 wo,
7763 q_norm,
7764 k_norm,
7765 output_gate,
7766 bias,
7767 ..
7768 } = &lw.attn
7769 else {
7770 return crate::gpu::BatchGraphOutcome::Declined;
7771 };
7772 let FfnKind::Dense(d) = &lw.ffn else {
7773 return crate::gpu::BatchGraphOutcome::Declined;
7774 };
7775 if !d.segs.is_empty() {
7776 return crate::gpu::BatchGraphOutcome::Declined; }
7778 let (
7781 AttnKind::Full {
7782 softplus_gate: None,
7783 ..
7784 },
7785 Some(gact),
7786 ) = (&lw.attn, d.act.graph_act())
7787 else {
7788 return crate::gpu::BatchGraphOutcome::Declined;
7789 };
7790 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7791 let (_, i, kind, rs) = t.graph_weight()?;
7792 Some(crate::gpu::GraphW {
7793 idx: i,
7794 kind,
7795 row_scale: rs,
7796 data: &[],
7797 prism: crate::gpu::GraphPrismOp::None,
7798 affine: false,
7799 })
7800 }
7801 let Some((model, _, _, _)) = wq.graph_weight() else {
7802 return crate::gpu::BatchGraphOutcome::Declined;
7803 };
7804 let model = model.clone();
7805 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7806 gw(wq),
7807 gw(wk),
7808 gw(wv),
7809 gw(wo),
7810 gw(&d.gate_proj),
7811 gw(&d.up_proj),
7812 gw(&d.down_proj),
7813 ) else {
7814 return crate::gpu::BatchGraphOutcome::Declined;
7815 };
7816 let layer = crate::gpu::GraphLayer {
7817 mhc: None,
7818 input_norm: &lw.input_norm,
7819 attn: crate::gpu::GraphAttn::Full {
7820 wq: gwq,
7821 wk: gwk,
7822 wv: gwv,
7823 wo: gwo,
7824 q_norm: q_norm.as_deref(),
7825 k_norm: k_norm.as_deref(),
7826 late_qk_norm: self.qk_norm_after_rope,
7827 bias: bias
7828 .as_ref()
7829 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7830 output_gate: *output_gate,
7831 cpu_k: m.kv.k_heads(),
7832 cpu_v: m.kv.v_heads(),
7833 cpu_base: m.kv.base(),
7834 geom: None,
7835 head_gate: None,
7836 },
7837 post_norm: &lw.post_norm,
7838 attn_out_norm: None,
7839 ffn_out_norm: None,
7840 layer_scale: None,
7841 ffn: crate::gpu::GraphFfn::Dense {
7842 gate: gg,
7843 up: gu,
7844 down: gd,
7845 act: gact,
7846 },
7847 };
7848 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7849 let nh = self.num_heads;
7850 let (nkv, hd, rd) = self.layer_geom(0);
7851 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7852 crate::gpu::forward_batch_graph(
7853 &model,
7854 self.mtp_kv_id(),
7855 std::slice::from_ref(&layer),
7856 &self.inv_freq,
7857 &mut hiddens,
7858 nh,
7859 nkv,
7860 hd,
7861 rd,
7862 hs,
7863 d.gate_proj.rows(),
7866 &positions,
7867 self.kv_cache.max_seq_len,
7868 gemma,
7869 self.rms_eps as f32,
7870 self.attn_scale,
7871 pairs.len(),
7872 &[],
7873 0,
7874 None,
7875 None,
7876 )
7877 }
7878
7879 #[cfg(feature = "gpu")]
7886 fn mtp_warm_graph_fallback(
7887 &mut self,
7888 m: &mut MtpModule,
7889 pairs: &[(&[f32], u32)],
7890 first_pos: usize,
7891 ) -> bool {
7892 if pairs.is_empty() {
7893 return true;
7894 }
7895 let graphable = self.mtp_block_graph_ok(m);
7896 if !graphable {
7897 if self.mtp_graph_mode == Some(true) {
7901 return false;
7902 }
7903 self.mtp_graph_mode = Some(false);
7904 for (j, (h, t)) in pairs.iter().enumerate() {
7905 self.mtp_warm(m, h, *t, first_pos + j);
7906 }
7907 return true;
7908 }
7909
7910 for (j, (h, t)) in pairs.iter().enumerate() {
7915 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7916 return false;
7917 }
7918 }
7919 self.mtp_graph_mode = Some(true);
7920 true
7921 }
7922
7923 #[cfg(feature = "gpu")]
7928 fn mtp_warm_prefill_pairs(
7929 &mut self,
7930 m: &mut MtpModule,
7931 pairs: &[(&[f32], u32)],
7932 first_pos: usize,
7933 ) -> Result<(), &'static str> {
7934 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7939 if self.mtp_graph_mode == Some(true) {
7940 return Err("MTP token graph became unavailable after admission");
7941 }
7942 self.mtp_graph_mode = Some(false);
7943 for (j, (h, t)) in pairs.iter().enumerate() {
7944 self.mtp_warm(m, h, *t, first_pos + j);
7945 }
7946 return Ok(());
7947 }
7948 match self.mtp_warm_graph(m, pairs, first_pos) {
7949 crate::gpu::BatchGraphOutcome::Completed => {
7950 if !pairs.is_empty() {
7951 self.mtp_graph_mode = Some(true);
7952 }
7953 Ok(())
7954 }
7955 crate::gpu::BatchGraphOutcome::Declined => {
7956 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7957 Ok(())
7958 } else {
7959 Err("MTP warm-up fallback failed after device admission")
7960 }
7961 }
7962 crate::gpu::BatchGraphOutcome::Failed => {
7963 Err("MTP warm batch graph failed after admission")
7964 }
7965 }
7966 }
7967
7968 #[cfg(not(feature = "gpu"))]
7969 fn mtp_warm_prefill_pairs(
7970 &mut self,
7971 m: &mut MtpModule,
7972 pairs: &[(&[f32], u32)],
7973 first_pos: usize,
7974 ) -> Result<(), &'static str> {
7975 for (j, (h, t)) in pairs.iter().enumerate() {
7976 self.mtp_warm(m, h, *t, first_pos + j);
7977 }
7978 Ok(())
7979 }
7980
7981 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7985 let e = self.embed_single(next_token);
7986 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7987 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7988 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7989 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7990 let mut x = vec![0.0f32; self.hidden_size];
7991 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7992 inference::rms_norm_into(
7993 &x,
7994 &m.layer.input_norm,
7995 self.rms_eps,
7996 self.norm_style,
7997 &mut self.ws.n1,
7998 );
7999 let attn = match &m.layer.attn {
8000 AttnKind::Full {
8001 wq,
8002 wk,
8003 wv,
8004 wo,
8005 q_norm,
8006 k_norm,
8007 output_gate,
8008 softplus_gate,
8009 bias,
8010 } => {
8011 let mut cfg = self.attn_cfg(position);
8012 cfg.q_norm = q_norm.as_deref();
8013 cfg.k_norm = k_norm.as_deref();
8014 cfg.output_gate = *output_gate;
8015 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
8016 cfg.bias = bias
8017 .as_ref()
8018 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
8019 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
8020 }
8021 _ => return,
8022 };
8023 let _ = attn;
8024 }
8025
8026 #[cfg(feature = "gpu")]
8033 #[allow(clippy::too_many_arguments)]
8034 fn graph_spec_step(
8035 &mut self,
8036 m: &mut MtpModule,
8037 hidden: &[f32],
8038 t_next: u32,
8039 next_pos: usize,
8040 drafted: &mut usize,
8041 accepted: &mut usize,
8042 all_ids: &mut Vec<u32>,
8046 room: usize,
8051 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
8052 #[cfg(target_os = "macos")]
8063 let metal_native = crate::gpu::q1_force();
8064 #[cfg(not(target_os = "macos"))]
8065 let metal_native = false;
8066 #[cfg(feature = "gpu")]
8067 let k_default = if metal_native {
8068 7
8071 } else if crate::gpu_wgpu::verify_i8_on() {
8072 5
8073 } else {
8074 4
8075 };
8076 #[cfg(not(feature = "gpu"))]
8077 let k_default = 4;
8078 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
8079 .ok()
8080 .and_then(|v| v.parse().ok())
8081 .filter(|&v| (1..=8).contains(&v));
8082 let (k_start, k_max) = if metal_native {
8087 (7, 7)
8088 } else {
8089 (3, k_default.max(5))
8090 };
8091 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
8092 let k_spec = k_full.min(room).max(1);
8093 let k_capped = k_spec < k_full;
8096 if next_pos == 0 {
8097 return None;
8098 }
8099 let t_round = std::time::Instant::now();
8100 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
8116 let sub0 = subs();
8117 let cfg = self.sampler_config.clone();
8122 let penalized = !(cfg.repetition_penalty == 1.0
8123 && cfg.presence_penalty == 0.0
8124 && cfg.suppress_tokens.is_empty());
8125 let greedy_pen = cfg.temperature < 1e-6 && penalized;
8130 let sampling = cfg.temperature >= 1e-6;
8131 let sparse = sampling && sampler::sparse_ok(&cfg);
8137 let base_len = all_ids.len();
8138 if sampling && !sparse && self.spec_q.len() < k_spec {
8139 self.spec_q.resize_with(k_spec, Vec::new);
8140 }
8141 if sparse && self.spec_qs.len() < k_spec {
8142 self.spec_qs.resize_with(k_spec, Vec::new);
8143 }
8144 let mut drafts = Vec::with_capacity(k_spec);
8149 let mut hx = hidden.to_vec();
8150 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
8153 spec_stamp("pro");
8154 #[cfg(target_os = "macos")]
8160 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
8161 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
8162 Ok(ids) => {
8163 self.mtp_graph_mode = Some(true);
8164 drafts = ids;
8165 }
8166 Err(true) => {
8167 tracing::error!("mtp Metal draft chain failed after commit");
8168 self.clear_sequence_state();
8169 self.graph_failed
8170 .store(true, std::sync::atomic::Ordering::Relaxed);
8171 self.cancel
8172 .store(true, std::sync::atomic::Ordering::Relaxed);
8173 return None;
8174 }
8175 Err(false) => {}
8176 }
8177 }
8178 for j in drafts.len()..k_spec {
8179 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
8180 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
8181 if spec_dbg {
8182 let saved = self.mtp_graph_mode;
8183 self.mtp_graph_mode = Some(false);
8184 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8185 self.mtp_graph_mode = saved;
8186 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8187 return None;
8188 }
8189 m.kv.truncate_last(1);
8190 dbg_ref = Some(r);
8191 }
8192 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8193 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8194 return None;
8195 }
8196 if let Some((lg_cpu, h_cpu)) = dbg_ref {
8197 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
8198 let dl = lg
8199 .iter()
8200 .zip(&lg_cpu)
8201 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8202 let dh = hj
8203 .iter()
8204 .zip(&h_cpu)
8205 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8206 eprintln!(
8207 "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 {}",
8208 next_pos - 1 + j,
8209 sampler::argmax(&lg_cpu),
8210 sampler::argmax(&lg),
8211 n(&h_cpu),
8212 n(&hj),
8213 m.kv.seq_len
8214 );
8215 }
8216 let dj = if sparse {
8217 let mut q = std::mem::take(&mut self.spec_qs[j]);
8218 let ok = sampler::sparse_distribution_into(
8219 &lg,
8220 &cfg,
8221 all_ids,
8222 &mut self.sampler_scratch,
8223 self.pool.as_deref(),
8224 &mut q,
8225 );
8226 let d = if ok {
8227 sampler::draw_sparse(&q, &mut self.rng)
8228 } else {
8229 let t = sampler::argmax(&lg);
8231 q.clear();
8232 q.push((t, 1.0));
8233 t
8234 };
8235 self.spec_qs[j] = q;
8236 all_ids.push(d);
8237 d
8238 } else if sampling {
8239 let mut q = std::mem::take(&mut self.spec_q[j]);
8240 sampler::distribution_into(
8241 &lg,
8242 &cfg,
8243 all_ids,
8244 &mut self.sampler_scratch,
8245 self.pool.as_deref(),
8246 &mut q,
8247 );
8248 let d = sampler::draw(&q, &mut self.rng);
8249 self.spec_q[j] = q;
8250 all_ids.push(d); d
8252 } else if greedy_pen {
8253 let d = sampler::argmax_penalized(
8254 &lg,
8255 &cfg,
8256 all_ids,
8257 &mut self.sampler_scratch,
8258 self.pool.as_deref(),
8259 );
8260 all_ids.push(d);
8261 d
8262 } else {
8263 sampler::argmax(&lg)
8264 };
8265 attention::recycle_buf(&mut lg);
8266 drafts.push(dj);
8267 hx = hj;
8268 spec_stamp("d.pick");
8269 }
8270 all_ids.truncate(base_len);
8271 *drafted += k_spec;
8272 let t_draft = t_round.elapsed();
8273 let sub_draft = subs();
8274 let b = k_spec + 1;
8277 let mut hiddens = vec![0.0f32; b * self.hidden_size];
8278 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8279 let e = self.embed_single(t);
8280 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8281 }
8282 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8283 spec_stamp("v.emb");
8284 let (lm_gw, lm_rows) = {
8285 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8286 (
8287 crate::gpu::GraphW {
8288 idx: i,
8289 kind,
8290 row_scale: rs,
8291 data: &[],
8292 prism: crate::gpu::GraphPrismOp::None,
8293 affine: false,
8294 },
8295 self.weights.lm_head.rows(),
8296 )
8297 };
8298 let mut logits = Vec::new();
8299 let final_norm = self.weights.final_norm.clone();
8300 #[cfg(target_os = "macos")]
8309 let greedy_dev = metal_native
8310 && !sampling
8311 && !greedy_pen
8312 && !self.confidence_on
8313 && self.final_softcap.is_none()
8314 && self.vocab_size == lm_rows
8320 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8321 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8322 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8323 #[cfg(not(target_os = "macos"))]
8324 let greedy_dev = false;
8325 let mut dev_ids: Vec<u32> = Vec::new();
8326 #[cfg(target_os = "macos")]
8327 let verify_outcome = if metal_native {
8328 let lm = self.weights.lm_head.q1_parts()?;
8329 let n_score = self.vocab_size.min(lm_rows);
8330 self.try_batch_graph_metal(
8331 &mut hiddens,
8332 &positions,
8333 b,
8334 Some((lm, &final_norm, &mut logits)),
8335 if greedy_dev {
8336 Some((n_score, &mut dev_ids))
8337 } else {
8338 None
8339 },
8340 )
8341 } else {
8342 self.try_batch_graph_wgpu(
8343 &mut hiddens,
8344 &positions,
8345 b,
8346 Some(crate::gpu::SpecTail {
8347 lm: lm_gw,
8348 lm_rows,
8349 final_norm: &final_norm,
8350 logits_out: &mut logits,
8351 pick: None,
8352 }),
8353 )
8354 };
8355 #[cfg(not(target_os = "macos"))]
8356 let verify_outcome = self.try_batch_graph_wgpu(
8357 &mut hiddens,
8358 &positions,
8359 b,
8360 Some(crate::gpu::SpecTail {
8361 lm: lm_gw,
8362 lm_rows,
8363 final_norm: &final_norm,
8364 logits_out: &mut logits,
8365 pick: None,
8366 }),
8367 );
8368 match verify_outcome {
8369 crate::gpu::BatchGraphOutcome::Completed => {}
8370 crate::gpu::BatchGraphOutcome::Declined => {
8371 m.kv.truncate_last(k_spec);
8375 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8376 self.clear_sequence_state();
8377 self.graph_failed
8378 .store(true, std::sync::atomic::Ordering::Relaxed);
8379 self.cancel
8380 .store(true, std::sync::atomic::Ordering::Relaxed);
8381 tracing::error!("MTP graph mirror rewind failed after verify decline");
8382 }
8383 return None;
8384 }
8385 crate::gpu::BatchGraphOutcome::Failed => {
8386 self.clear_sequence_state();
8390 self.graph_failed
8391 .store(true, std::sync::atomic::Ordering::Relaxed);
8392 self.cancel
8393 .store(true, std::sync::atomic::Ordering::Relaxed);
8394 tracing::error!("MTP verify batch graph failed after admission");
8395 return None;
8396 }
8397 }
8398 #[cfg(target_os = "macos")]
8404 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8405 let snap: Vec<Vec<f32>> = self
8406 .kv_cache
8407 .layers
8408 .iter()
8409 .map(|l| l.linear_state.clone())
8410 .collect();
8411 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8412 let toks: Vec<u32> = std::iter::once(t_next)
8413 .chain(drafts.iter().copied())
8414 .collect();
8415 let want_save = self.graph_want_logits;
8416 self.graph_want_logits = false;
8417 for (i, &t) in toks.iter().enumerate() {
8418 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8419 let _ = self.graph_logits.take();
8420 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8424 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8425 }
8426 let ref_lg = self.logits_from_hidden(&hi);
8427 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8428 let ra = sampler::argmax(&ref_lg);
8429 let va = sampler::argmax(row);
8430 let mut md = 0f32;
8431 let mut rms = 0f64;
8432 for j in 0..lm_rows.min(ref_lg.len()) {
8433 let d = (ref_lg[j] - row[j]).abs();
8434 md = md.max(d);
8435 rms += (d as f64) * (d as f64);
8436 }
8437 let mut hd = 0f32;
8438 for j in 0..self.hidden_size {
8439 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8440 }
8441 eprintln!(
8442 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8443 next_pos + i,
8444 if ra == va { "OK" } else { "MISMATCH" },
8445 (rms / lm_rows as f64).sqrt()
8446 );
8447 }
8448 self.graph_want_logits = want_save;
8449 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8452 if l.linear_state.len() == st.len() {
8453 l.linear_state.copy_from_slice(&st);
8454 } else {
8455 l.linear_state = st;
8456 }
8457 }
8458 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8459 let extra = l.seq_len.saturating_sub(n0);
8460 if extra > 0 {
8461 l.truncate_last(extra);
8462 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8463 }
8464 }
8465 }
8466 let t_verify = t_round.elapsed();
8467 let sub_verify = subs();
8468 let mut a = 0usize;
8473 let mut forced: Option<u32> = None;
8474 let ids: Vec<u32> = if sparse {
8475 let mut p = std::mem::take(&mut self.spec_ps);
8476 let mut res = std::mem::take(&mut self.spec_ress);
8477 while a < k_spec {
8478 let ok = sampler::sparse_distribution_into(
8479 &logits[a * lm_rows..(a + 1) * lm_rows],
8480 &cfg,
8481 all_ids,
8482 &mut self.sampler_scratch,
8483 self.pool.as_deref(),
8484 &mut p,
8485 );
8486 if !ok {
8487 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8488 p.clear();
8489 p.push((t, 1.0));
8490 }
8491 match sampler::spec_accept_or_correct_sparse(
8492 &p,
8493 &self.spec_qs[a],
8494 drafts[a],
8495 &mut self.rng,
8496 &mut res,
8497 ) {
8498 None => {
8499 all_ids.push(drafts[a]);
8500 a += 1;
8501 }
8502 Some(c) => {
8503 forced = Some(c);
8504 break;
8505 }
8506 }
8507 }
8508 all_ids.truncate(base_len);
8509 self.spec_ps = p;
8510 self.spec_ress = res;
8511 drafts.clone()
8512 } else if sampling {
8513 let mut p = std::mem::take(&mut self.spec_p);
8514 let mut res = std::mem::take(&mut self.spec_res);
8515 while a < k_spec {
8516 sampler::distribution_into(
8517 &logits[a * lm_rows..(a + 1) * lm_rows],
8518 &cfg,
8519 all_ids,
8520 &mut self.sampler_scratch,
8521 self.pool.as_deref(),
8522 &mut p,
8523 );
8524 match sampler::spec_accept_or_correct(
8525 &p,
8526 &self.spec_q[a],
8527 drafts[a],
8528 &mut self.rng,
8529 &mut res,
8530 self.pool.as_deref(),
8531 ) {
8532 None => {
8533 all_ids.push(drafts[a]);
8534 a += 1;
8535 }
8536 Some(c) => {
8537 forced = Some(c);
8538 break;
8539 }
8540 }
8541 }
8542 all_ids.truncate(base_len);
8543 self.spec_p = p;
8544 self.spec_res = res;
8545 drafts.clone()
8547 } else if greedy_pen {
8548 let mut ids: Vec<u32> = Vec::with_capacity(b);
8552 for i in 0..b {
8553 let t = sampler::argmax_penalized(
8554 &logits[i * lm_rows..(i + 1) * lm_rows],
8555 &cfg,
8556 all_ids,
8557 &mut self.sampler_scratch,
8558 self.pool.as_deref(),
8559 );
8560 ids.push(t);
8561 if i < k_spec && t == drafts[i] {
8562 all_ids.push(t);
8563 } else {
8564 break;
8565 }
8566 }
8567 all_ids.truncate(base_len);
8568 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8569 a += 1;
8570 }
8571 ids
8574 } else if greedy_dev && dev_ids.len() == b {
8575 let ids = std::mem::take(&mut dev_ids);
8576 while a < k_spec && ids[a] == drafts[a] {
8577 a += 1;
8578 }
8579 ids
8580 } else {
8581 if logits.len() < b * lm_rows {
8582 self.clear_sequence_state();
8585 self.graph_failed
8586 .store(true, std::sync::atomic::Ordering::Relaxed);
8587 self.cancel
8588 .store(true, std::sync::atomic::Ordering::Relaxed);
8589 tracing::error!("Metal verify returned neither logits nor argmax ids");
8590 return None;
8591 }
8592 let ids: Vec<u32> = (0..b)
8593 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8594 .collect();
8595 while a < k_spec && ids[a] == drafts[a] {
8596 a += 1;
8597 }
8598 ids
8599 };
8600 spec_stamp("acc");
8601 if spec_dbg {
8602 eprintln!(
8603 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8604 drafts, ids
8605 );
8606 }
8607 #[cfg(target_os = "macos")]
8611 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8612 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8613 {
8614 let snap: Vec<Vec<f32>> = self
8615 .kv_cache
8616 .layers
8617 .iter()
8618 .map(|l| l.linear_state.clone())
8619 .collect();
8620 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8621 let toks: Vec<u32> = std::iter::once(t_next)
8622 .chain(drafts.iter().copied())
8623 .collect();
8624 let want_save = self.graph_want_logits;
8625 self.graph_want_logits = false;
8626 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8627 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8628 let _ = self.graph_logits.take();
8629 }
8630 self.graph_want_logits = want_save;
8631 let plain_states: Vec<Vec<f32>> = self
8632 .kv_cache
8633 .layers
8634 .iter()
8635 .map(|l| l.linear_state.clone())
8636 .collect();
8637 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8638 let mut rows = Vec::new();
8639 for (li, (l, n0)) in self
8640 .kv_cache
8641 .layers
8642 .iter_mut()
8643 .zip(attn_lens.iter())
8644 .enumerate()
8645 {
8646 let extra = l.seq_len.saturating_sub(*n0);
8647 if extra > 0 {
8648 let mut kk = Vec::new();
8649 let mut vv = Vec::new();
8650 for g in 0..nkv {
8651 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8652 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8653 }
8654 rows.push((li, kk, vv));
8655 l.truncate_last(extra);
8656 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8657 }
8658 }
8659 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8660 if l.linear_state.len() == st.len() {
8661 l.linear_state.copy_from_slice(&st);
8662 } else {
8663 l.linear_state = st;
8664 }
8665 }
8666 Some((plain_states, rows))
8667 } else {
8668 None
8669 };
8670 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8671 #[cfg(target_os = "macos")]
8680 let mut warm_pending: Option<MetalWarmPending> = None;
8681 #[cfg(target_os = "macos")]
8682 if metal_native {
8683 m.kv.truncate_last(k_spec.saturating_sub(1));
8684 if self.mtp_graph_mode == Some(true) {
8685 crate::gpu_metal::kv_mirror_set_stored(
8688 self.mtp_kv_id(),
8689 Self::MTP_LAYER_BASE,
8690 m.kv.seq_len,
8691 );
8692 if !warm_off && a > 0 {
8693 let pairs: Vec<(&[f32], u32)> = (0..a)
8694 .map(|j| {
8695 (
8696 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8697 ids[j],
8698 )
8699 })
8700 .collect();
8701 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8702 }
8703 }
8704 spec_stamp("c.wsub");
8705 }
8706 #[cfg(target_os = "macos")]
8708 if metal_native {
8709 if !self.metal_verify_commit(a) {
8712 self.clear_sequence_state();
8713 self.graph_failed
8714 .store(true, std::sync::atomic::Ordering::Relaxed);
8715 self.cancel
8716 .store(true, std::sync::atomic::Ordering::Relaxed);
8717 tracing::error!("Metal verify state/KV handoff failed after admission");
8718 return None;
8719 }
8720 if let Some((plain_states, rows)) = commit_ref {
8721 crate::gpu_metal::queue_fence();
8722 let _ = crate::gpu_metal::wait_replay();
8725 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8726 let mut worst_s = 0f32;
8727 let mut worst_li = 0usize;
8728 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8729 if l.linear_state.len() != ps.len() || ps.is_empty() {
8730 continue;
8731 }
8732 let d = l
8733 .linear_state
8734 .iter()
8735 .zip(ps)
8736 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8737 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8738 let rel = d / n.max(1e-6);
8739 if rel > worst_s {
8740 worst_s = rel;
8741 worst_li = li;
8742 }
8743 }
8744 let mut worst_k = 0f32;
8745 for (li, kk, vv) in &rows {
8746 let l = &self.kv_cache.layers[*li];
8747 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8748 let mut ck = Vec::new();
8749 let mut cv = Vec::new();
8750 for g in 0..nkv {
8751 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8752 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8753 }
8754 if ck.len() == kk.len() {
8755 let dk = ck
8756 .iter()
8757 .zip(kk)
8758 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8759 let dv = cv
8760 .iter()
8761 .zip(vv)
8762 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8763 worst_k = worst_k.max(dk).max(dv);
8764 } else {
8765 eprintln!(
8766 "commit-check L{li}: kv row count mismatch {} vs {}",
8767 ck.len(),
8768 kk.len()
8769 );
8770 }
8771 }
8772 eprintln!(
8773 "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}"
8774 );
8775 }
8776 }
8777 if !metal_native && a + 1 < b {
8778 let expected_gdn_layers = self.graph_gdn_layer_count();
8779 if expected_gdn_layers > 0
8780 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8781 {
8782 self.clear_sequence_state();
8783 self.graph_failed
8784 .store(true, std::sync::atomic::Ordering::Relaxed);
8785 self.cancel
8786 .store(true, std::sync::atomic::Ordering::Relaxed);
8787 tracing::error!("GDN speculative restore failed after verify");
8788 return None;
8789 }
8790 }
8791 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8792 self.clear_sequence_state();
8797 self.graph_failed
8798 .store(true, std::sync::atomic::Ordering::Relaxed);
8799 self.cancel
8800 .store(true, std::sync::atomic::Ordering::Relaxed);
8801 tracing::error!("trunk graph KV rewind failed after speculative verify");
8802 return None;
8803 }
8804 *accepted += a;
8805 if !metal_native {
8816 m.kv.truncate_last(k_spec.saturating_sub(1));
8818 }
8819 spec_stamp("c.trunc");
8820 if !metal_native
8821 && self.mtp_graph_mode == Some(true)
8822 && !self.rewind_mtp_graph_mirror(next_pos)
8823 {
8824 self.clear_sequence_state();
8828 self.graph_failed
8829 .store(true, std::sync::atomic::Ordering::Relaxed);
8830 self.cancel
8831 .store(true, std::sync::atomic::Ordering::Relaxed);
8832 tracing::error!("MTP graph mirror rewind failed after verify commit");
8833 return None;
8834 }
8835 if !warm_off && a > 0 {
8836 let mut warmed = false;
8839 #[cfg(target_os = "macos")]
8840 if metal_native && self.mtp_graph_mode == Some(true) {
8841 warmed = match warm_pending.take() {
8845 Some(p) => self.mtp_warm_batch_finish(m, p),
8846 None => false,
8847 };
8848 if !warmed {
8849 warmed = true;
8850 for j in 0..a {
8851 let row =
8852 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8853 if self
8854 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8855 .is_none()
8856 {
8857 warmed = false;
8858 break;
8859 }
8860 }
8861 }
8862 }
8863 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8864 let rows: Vec<Vec<f32>> = (0..a)
8865 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8866 .collect();
8867 let pairs: Vec<(&[f32], u32)> = rows
8868 .iter()
8869 .zip(ids.iter())
8870 .map(|(r, &t)| (r.as_slice(), t))
8871 .collect();
8872 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8873 Ok(()) => warmed = true,
8874 Err(err) => {
8875 tracing::error!("{err}");
8881 self.clear_sequence_state();
8882 self.graph_failed
8883 .store(true, std::sync::atomic::Ordering::Relaxed);
8884 self.cancel
8885 .store(true, std::sync::atomic::Ordering::Relaxed);
8886 return None;
8887 }
8888 }
8889 }
8890 if !warmed {
8891 for j in 0..a {
8892 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8893 let row = row.to_vec();
8894 self.mtp_warm(m, &row, ids[j], next_pos + j);
8895 }
8896 }
8897 }
8898 spec_stamp("c.warm");
8902 if let Some(c) = forced {
8903 self.spec_forced = Some(c);
8904 self.graph_logits = None;
8905 } else if greedy_dev && logits.is_empty() {
8906 self.spec_forced = Some(ids[a]);
8909 self.graph_logits = None;
8910 } else {
8911 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8912 row.resize(self.vocab_size, 0.0);
8913 if let Some(c) = self.final_softcap {
8914 for l in row.iter_mut() {
8915 *l = c * (*l / c).tanh();
8916 }
8917 }
8918 self.graph_logits = Some(row);
8919 }
8920 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8921 spec_stamp("c.row");
8922 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8928 let end = subs();
8929 eprintln!(
8930 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8931 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8932 t_draft.as_secs_f64() * 1e3,
8933 sub_draft - sub0,
8934 (t_verify - t_draft).as_secs_f64() * 1e3,
8935 sub_verify - sub_draft,
8936 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8937 end - sub_verify,
8938 self.draft_full_streak,
8939 );
8940 }
8941 if k_env.is_none() && !metal_native && !k_capped {
8946 let f = a as f32 / k_spec.max(1) as f32;
8950 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8951 let mut k_next = k_spec;
8952 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8953 k_next = k_spec + 1;
8954 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8955 k_next = k_spec - 1;
8956 }
8957 if k_next != k_spec {
8958 self.spec_acc_ewma = 0.6;
8959 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8960 eprintln!("spec-k: {k_spec} → {k_next}");
8961 }
8962 }
8963 self.spec_k_adapt = Some(k_next);
8964 }
8965 spec_stamp("end");
8966 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8967 }
8968
8969 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8978 if !self.pair_supported() {
8979 return (0.0, 0.0);
8980 }
8981 if self.gdn_cfg.is_some()
8989 && self.graph_prefill_preferred()
8990 && self.automatic_gpu_prefix().is_some()
8991 {
8992 tracing::info!(
8993 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8994 );
8995 return (0.0, 0.0);
8996 }
8997 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
9004 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
9005 let emb1 = self.embed_single(1);
9006 let emb2 = self.embed_single(2);
9007 let pos = self.kv_cache.seq_len();
9008
9009 let t0 = std::time::Instant::now();
9010 for _ in 0..iters {
9011 let _ = self.forward_layers(&emb1, pos, None);
9012 let _ = self.forward_layers(&emb2, pos + 1, None);
9013 for l in &mut self.kv_cache.layers {
9014 l.truncate_last(2);
9015 }
9016 }
9017 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9018
9019 let t1 = std::time::Instant::now();
9020 for _ in 0..iters {
9021 let _ = self.forward_pair(&emb1, &emb2, pos);
9022 for l in &mut self.kv_cache.layers {
9023 l.truncate_last(2);
9024 }
9025 }
9026 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9027 match graph_env {
9028 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
9029 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
9030 }
9031 (singles_ms, pair_ms)
9032 }
9033
9034 fn pair_supported(&self) -> bool {
9042 !self.weights.layers.is_empty()
9049 && !self.per_token_stack()
9050 && !self
9051 .weights
9052 .layers
9053 .iter()
9054 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
9055 }
9056
9057 fn forward_pair(
9058 &mut self,
9059 emb1: &[f32],
9060 emb2: &[f32],
9061 position: usize,
9062 ) -> (Vec<f32>, Vec<f32>) {
9063 self.mimo_moe_prepare();
9066 let mut h1 = emb1.to_vec();
9067 let mut h2 = emb2.to_vec();
9068 let (_nkv, _hd, hs, _rd, eps) = (
9069 self.num_kv_heads,
9070 self.head_dim,
9071 self.hidden_size,
9072 self.rotary_dim,
9073 self.rms_eps,
9074 );
9075 let pool = self.pool.clone();
9076 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9083
9084 for li in 0..self.num_layers {
9085 let _capacity_tail = automatic_gpu_prefix
9086 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
9087 .map(|_| crate::gpu::enter_cpu_scope());
9088 crate::gpu::set_layer(li as i64);
9091 let lw = &self.weights.layers[self.phys_layer(li)];
9092 inference::rms_norm_into(
9095 &h1,
9096 &lw.input_norm,
9097 self.rms_eps,
9098 self.norm_style,
9099 &mut self.ws.n1,
9100 );
9101 inference::rms_norm_into(
9102 &h2,
9103 &lw.input_norm,
9104 self.rms_eps,
9105 self.norm_style,
9106 &mut self.ws.n2,
9107 );
9108
9109 let (a1, a2) = match &lw.attn {
9110 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9111 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9112 AttnKind::Bounded(w) => {
9113 let rope = self
9116 .bounded_rope
9117 .clone()
9118 .expect("bounded layer without an installed rotation table");
9119 let cfg = crate::bounded::BoundedAttnCfg {
9120 num_heads: self.num_heads,
9121 num_kv_heads: self.num_kv_heads,
9122 head_dim: self.head_dim,
9123 hidden_size: hs,
9124 scale: self.attn_scale,
9125 rope: &rope,
9126 pool: pool.as_deref(),
9127 };
9128 let a1 = crate::bounded::bounded_attention(
9129 &self.ws.n1,
9130 w,
9131 &mut self.kv_cache.layers[li],
9132 &cfg,
9133 );
9134 let a2 = crate::bounded::bounded_attention(
9135 &self.ws.n2,
9136 w,
9137 &mut self.kv_cache.layers[li],
9138 &cfg,
9139 );
9140 (a1, a2)
9141 }
9142 AttnKind::Linear(w) => {
9143 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9144 let layer = &mut self.kv_cache.layers[li];
9145 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9146 vmf_phase_pair(
9147 &self.ws.n1,
9148 &self.ws.n2,
9149 w,
9150 &cfg,
9151 state,
9152 scratch,
9153 self.pool.as_deref(),
9154 )
9155 }
9156 AttnKind::LinearGdn(w) => {
9157 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9158 let layer = &mut self.kv_cache.layers[li];
9159 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9160 gdn_pair(
9161 &self.ws.n1,
9162 &self.ws.n2,
9163 w,
9164 &cfg,
9165 state,
9166 scratch,
9167 self.pool.as_deref(),
9168 )
9169 }
9170 AttnKind::ShortConv(w) => {
9171 let cfg = self
9172 .short_conv_cfg
9173 .expect("short-conv layer without short_conv_cfg");
9174 let layer = &mut self.kv_cache.layers[li];
9175 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9176 short_conv_pair(
9177 &self.ws.n1,
9178 &self.ws.n2,
9179 w,
9180 &cfg,
9181 state,
9182 scratch,
9183 self.pool.as_deref(),
9184 )
9185 }
9186 AttnKind::Full {
9187 wq,
9188 wk,
9189 wv,
9190 wo,
9191 q_norm,
9192 k_norm,
9193 output_gate,
9194 softplus_gate,
9195 bias,
9196 } => {
9197 let inv_freq_l = self.layer_inv_freq(li);
9198 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9199 let cfg = QwenAttnCfg {
9200 mrope: None,
9201 num_heads: self.layer_num_heads(li),
9202 num_kv_heads: nkv_l,
9203 head_dim: hd_l,
9204 hidden_size: hs,
9205 position,
9206 inv_freq: &inv_freq_l,
9207 rotary_dim: rd_l,
9208 scale: self.attn_scale,
9209 softcap: self.attn_softcap,
9210 window: self.layer_window(li),
9211 v_norm: self.attn_v_norm,
9212 qk_norm_after_rope: self.qk_norm_after_rope,
9213 gate_sigmoid: self.proj_gate_sigmoid,
9214 q_norm: q_norm.as_deref(),
9215 k_norm: k_norm.as_deref(),
9216 output_gate: *output_gate,
9217 softplus_gate: softplus_gate
9218 .as_ref()
9219 .map(|(gate, per_head)| (gate, *per_head)),
9220 rope_scale: self.layer_rope_scale(li),
9221 bias: bias
9222 .as_ref()
9223 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9224 rms_eps: eps,
9225 norm_style: self.norm_style,
9226 pool: pool.as_deref(),
9227 v_head_dim: self.layer_v_dim(li),
9228 };
9229 attention::qwen_attention_pair(
9230 &self.ws.n1,
9231 &self.ws.n2,
9232 wq,
9233 wk,
9234 wv,
9235 wo,
9236 &mut self.kv_cache.layers[li],
9237 &cfg,
9238 )
9239 }
9240 };
9241 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
9242 Some(w) => (
9243 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
9244 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
9245 ),
9246 None => (a1, a2),
9247 };
9248 for i in 0..self.hidden_size {
9249 h1[i] += a1[i];
9250 h2[i] += a2[i];
9251 }
9252 let (mut a1, mut a2) = (a1, a2);
9253 attention::recycle_buf(&mut a1);
9254 attention::recycle_buf(&mut a2);
9255
9256 let lw = &self.weights.layers[self.phys_layer(li)];
9257 inference::rms_norm_into(
9258 &h1,
9259 &lw.post_norm,
9260 self.rms_eps,
9261 self.norm_style,
9262 &mut self.ws.p1,
9263 );
9264 inference::rms_norm_into(
9265 &h2,
9266 &lw.post_norm,
9267 self.rms_eps,
9268 self.norm_style,
9269 &mut self.ws.p2,
9270 );
9271 let (f1, f2) = match &lw.ffn {
9272 FfnKind::DenseMoe(dm) => (
9275 dense_moe_ffn(
9276 dm,
9277 &self.ws.p1,
9278 &h1,
9279 self.rms_eps,
9280 self.norm_style,
9281 self.pool.as_deref(),
9282 ),
9283 dense_moe_ffn(
9284 dm,
9285 &self.ws.p2,
9286 &h2,
9287 self.rms_eps,
9288 self.norm_style,
9289 self.pool.as_deref(),
9290 ),
9291 ),
9292 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9293 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9294 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9295 ),
9296 _ => ffn_forward_pair(
9297 &lw.ffn,
9298 &self.ws.p1,
9299 &self.ws.p2,
9300 self.pool.as_deref(),
9301 None,
9302 ),
9303 };
9304 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9305 Some(w) => (
9306 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9307 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9308 ),
9309 None => (f1, f2),
9310 };
9311 for i in 0..self.hidden_size {
9312 h1[i] += f1[i];
9313 h2[i] += f2[i];
9314 }
9315 let (mut f1, mut f2) = (f1, f2);
9316 attention::recycle_buf(&mut f1);
9317 attention::recycle_buf(&mut f2);
9318 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9319 for i in 0..self.hidden_size {
9320 h1[i] *= sc;
9321 h2[i] *= sc;
9322 }
9323 }
9324 if self.is_loop_end(li) && li + 1 < self.num_layers {
9326 h1 = inference::rms_norm(
9327 &h1,
9328 &self.weights.final_norm,
9329 self.rms_eps,
9330 self.norm_style,
9331 );
9332 h2 = inference::rms_norm(
9333 &h2,
9334 &self.weights.final_norm,
9335 self.rms_eps,
9336 self.norm_style,
9337 );
9338 }
9339 }
9340 crate::gpu::set_layer(-1); if self.o1_active() {
9347 self.commit_linear_scratch();
9348 }
9349 self.o1_progress();
9350 self.swa_trim_tails();
9351 (h1, h2)
9352 }
9353
9354 fn commit_linear_scratch(&mut self) {
9356 for layer in &mut self.kv_cache.layers {
9357 if !layer.linear_scratch.is_empty() {
9358 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9359 layer.linear_scratch.clear();
9360 }
9361 }
9362 }
9363
9364 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9372 #[cfg(target_os = "macos")]
9373 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9374 if ids.is_empty() {
9375 return Err("empty id sequence".to_string());
9376 }
9377
9378 self.nll_begin()?;
9379 let result = (|| {
9380 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9381 self.nll_check_graph("encoder prefill", 0)?;
9382 let expected = ids.len() * self.hidden_size;
9383 if hidden.len() != expected {
9384 return Err(format!(
9385 "encoder prefill returned {} values for {} rows of hidden size {}",
9386 hidden.len(),
9387 ids.len(),
9388 self.hidden_size
9389 ));
9390 }
9391
9392 let mut out = vec![0.0f32; expected];
9393 for (src, dst) in hidden
9394 .chunks_exact(self.hidden_size)
9395 .zip(out.chunks_exact_mut(self.hidden_size))
9396 {
9397 inference::rms_norm_into(
9398 src,
9399 &self.weights.final_norm,
9400 self.rms_eps,
9401 self.norm_style,
9402 dst,
9403 );
9404 }
9405 Ok(out)
9406 })();
9407 self.nll_end();
9408 result
9409 }
9410
9411 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9416 #[cfg(target_os = "macos")]
9417 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9418 if ids.is_empty() {
9419 return Err("empty id sequence".to_string());
9420 }
9421 self.nll_begin()?;
9422 let result = (|| {
9423 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9424 self.nll_check_graph("hidden prefill", 0)?;
9425 if hidden.len() != ids.len() * self.hidden_size {
9426 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9427 }
9428 Ok(hidden)
9429 })();
9430 self.nll_end();
9431 result
9432 }
9433
9434 pub fn forward_ids(
9437 &mut self,
9438 ids: &[u32],
9439 task_mask: Option<&TaskMask>,
9440 ) -> Result<Vec<f32>, String> {
9441 #[cfg(target_os = "macos")]
9442 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9443 if ids.is_empty() {
9444 return Err("empty id sequence".to_string());
9445 }
9446 self.clear_sequence_state();
9447 self.check_forward_graph("forward_ids setup", 0)?;
9448 if task_mask.is_none() {
9449 self.o1_begin();
9450 }
9451 let mut hidden = vec![0.0f32; self.hidden_size];
9452 let mut pos = 0usize;
9453 if let Some(b) = &mut self.dsv41 {
9454 let pool = self.pool.clone();
9455 let mut logits = Vec::new();
9456 crate::dsv41::forward_chunk(
9457 &b.0,
9458 &b.1,
9459 &b.2,
9460 &mut b.3,
9461 ids,
9462 0,
9463 pool.as_deref(),
9464 &mut logits,
9465 );
9466 if let Err(err) = self.o1_seal_checked() {
9467 self.clear_sequence_state();
9468 return Err(err);
9469 }
9470 return Ok(logits);
9471 }
9472 #[cfg(not(target_os = "macos"))]
9483 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9484 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9485 Ok(r) => r,
9486 Err(e) => {
9487 self.clear_sequence_state();
9488 return Err(e);
9489 }
9490 };
9491 if n > 0 {
9492 hidden.copy_from_slice(&last);
9493 pos = n;
9494 }
9495 }
9496 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9497 let chunk = self.prefill_chunk();
9501 let hs = self.hidden_size;
9502 while pos < ids.len() {
9503 let end = (pos + chunk).min(ids.len());
9504 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9505 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9506 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9507 pos = end;
9508 }
9509 }
9510 if task_mask.is_none()
9519 && !self.graph_prefill_preferred()
9520 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9521 && self.pair_supported()
9522 {
9523 while pos + 1 < ids.len() {
9524 let e1 = self.embed_single(ids[pos]);
9525 let e2 = self.embed_single(ids[pos + 1]);
9526 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9527 self.check_forward_graph("forward_ids pair", pos + 1)?;
9528 self.commit_linear_scratch();
9529 hidden = h2;
9530 pos += 2;
9531 }
9532 }
9533 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9536 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9537 self.graph_logits = Some(lg);
9538 hidden = vec![0.0; self.hidden_size];
9539 pos = ids.len();
9540 }
9541 }
9542 while pos < ids.len() {
9543 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9544 self.check_forward_graph("forward_ids", pos)?;
9545 pos += 1;
9546 }
9547 if let Some(logits) = self.graph_logits.take() {
9548 if let Err(err) = self.o1_seal_checked() {
9552 self.clear_sequence_state();
9553 return Err(err);
9554 }
9555 return Ok(logits);
9556 }
9557 if let Err(err) = self.o1_seal_checked() {
9561 self.clear_sequence_state();
9562 return Err(err);
9563 }
9564 let normed = inference::rms_norm(
9565 &hidden,
9566 &self.weights.final_norm,
9567 self.rms_eps,
9568 self.norm_style,
9569 );
9570 Ok(self.lm_head_forward(&normed))
9571 }
9572
9573 #[doc(hidden)]
9577 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9578 #[cfg(target_os = "macos")]
9579 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9580 if ids.is_empty() {
9581 return Err("empty id sequence".to_string());
9582 }
9583 self.clear_sequence_state();
9584 self.dsv41
9585 .as_ref()
9586 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9587 self.o1_begin();
9588 let rows = {
9589 let pool = self.pool.clone();
9590 let b = self
9591 .dsv41
9592 .as_mut()
9593 .expect("dsv41 checked above; state cannot change during forward");
9594 let mut rows = Vec::with_capacity(ids.len());
9595 for (position, &id) in ids.iter().enumerate() {
9596 let mut logits = Vec::new();
9597 crate::dsv41::forward_token(
9598 &b.0,
9599 &b.1,
9600 &b.2,
9601 &mut b.3,
9602 id,
9603 position,
9604 pool.as_deref(),
9605 &mut logits,
9606 );
9607 rows.push(logits);
9608 }
9609 rows
9610 };
9611 self.o1_seal();
9612 Ok(rows)
9613 }
9614
9615 pub fn dsv41_moe_capture(
9619 &mut self,
9620 ids: &[u32],
9621 lo: usize,
9622 hi: usize,
9623 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9624 #[cfg(target_os = "macos")]
9625 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9626 if ids.is_empty() {
9627 return Err("empty id sequence".to_string());
9628 }
9629 self.dsv41
9630 .as_ref()
9631 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9632 self.clear_sequence_state();
9633 let res = {
9634 let pool = self.pool.clone();
9635 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9636 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9637 };
9638 self.clear_sequence_state();
9639 res
9640 }
9641
9642 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9649 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9650 Ok((nll / cnt.max(1) as f64).exp())
9651 }
9652
9653 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9660 if !self.can_prefill_batched() {
9661 return self.nll_ids_from(ids, 0).map(|_| ());
9662 }
9663 self.nll_begin()?;
9664 let result: Result<(), String> = (|| {
9665 let chunk = chunk.max(1);
9666 let mut pos = 0usize;
9667 while pos < ids.len() {
9668 let end = (pos + chunk).min(ids.len());
9669 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9670 self.nll_check_graph("calibration prefill", pos)?;
9671 pos = end;
9672 }
9673 Ok(())
9674 })();
9675 self.nll_end();
9676 result
9677 }
9678
9679 pub fn calib_window_span(
9688 &mut self,
9689 ids: &[u32],
9690 input: Option<&[f32]>,
9691 from: usize,
9692 mid: usize,
9693 upto: usize,
9694 ) -> Result<Vec<f32>, String> {
9695 if !self.can_prefill_batched() {
9696 return Err("calib_window_span: this architecture has no batched prefill".into());
9697 }
9698 self.nll_begin()?;
9699 let result: Result<Vec<f32>, String> = (|| {
9700 let h = if mid > from {
9701 let inp = match input {
9702 Some(h) => PrefillIn::Hidden(h),
9703 None => PrefillIn::Ids(ids),
9704 };
9705 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9706 self.nll_check_graph("calibration span", 0)?;
9707 h
9708 } else {
9709 input.map(<[f32]>::to_vec).unwrap_or_default()
9710 };
9711 if upto > mid {
9712 let inp = if mid > from || input.is_some() {
9713 PrefillIn::Hidden(&h)
9714 } else {
9715 PrefillIn::Ids(ids)
9716 };
9717 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9718 self.nll_check_graph("calibration span", 0)?;
9719 }
9720 Ok(h)
9721 })();
9722 self.nll_end();
9723 result
9724 }
9725
9726 #[cfg(feature = "gpu")]
9730 pub fn qwen4_capture(
9731 &mut self,
9732 ids: &[u32],
9733 lo: usize,
9734 hi: usize,
9735 ) -> Result<Vec<Vec<f32>>, String> {
9736 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9737 crate::qwen4_exp::capture_window(
9738 &b.0,
9739 &b.1,
9740 &b.2,
9741 &mut b.3,
9742 ids,
9743 lo,
9744 hi,
9745 &self.inv_freq,
9746 self.pool.as_deref(),
9747 )
9748 }
9749
9750 #[cfg(feature = "gpu")]
9756 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9757 use crate::gpu_wgpu::clover as cap;
9758 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9759 let ne = (lo..hi)
9760 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9761 FfnKind::Moe(m) => Some(m.experts.len()),
9762 _ => None,
9763 })
9764 .ok_or("no MoE layer in the capture range")?;
9765 let chunk = self.prefill_chunk().max(1);
9766 self.clear_sequence_state();
9767 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9768 return Err("capture unavailable (no wgpu device)".into());
9769 }
9770 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9771 let res = (|| {
9772 for (ci, part) in ids.chunks(chunk).enumerate() {
9773 let mut hiddens = Vec::with_capacity(part.len() * hs);
9774 for &id in part {
9775 hiddens.extend_from_slice(&self.embed_single(id));
9776 }
9777 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9778 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9779 crate::gpu::BatchGraphOutcome::Completed => {}
9780 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9781 }
9782 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9783 if written < hi - lo {
9784 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9785 }
9786 for (o, r) in out.iter_mut().zip(rows) {
9787 o.extend_from_slice(&r);
9788 }
9789 }
9790 Ok(())
9791 })();
9792 cap::moe_capture_end();
9793 self.clear_sequence_state();
9794 res.map(|_| out)
9795 }
9796
9797 pub fn moe_expert_dirs(
9800 &self,
9801 li: usize,
9802 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9803 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9804 return None;
9805 };
9806 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9807 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9808 let shared = match &m.shared {
9809 Some((se, _)) => Some(dirs(se)?),
9810 None => None,
9811 };
9812 Some((routed, shared))
9813 }
9814
9815 pub fn qwen4_expert_dirs(
9817 &self,
9818 li: usize,
9819 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9820 let b = self.qwen4_exp.as_ref()?;
9821 crate::qwen4_exp::expert_dirs(&b.1, li)
9822 }
9823
9824 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9839 self.reset_mtp_probe_state();
9840 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9846 let result = if ids.len() < 2 {
9847 Err("MTP FFN probe needs at least two token ids".into())
9848 } else if ids
9849 .iter()
9850 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9851 {
9852 Err("MTP FFN probe received a token id outside the embedding table".into())
9853 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9854 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9857 } else if self.mimo_mtp.is_some() {
9858 let mut st = self
9859 .mimo_mtp
9860 .take()
9861 .expect("checked MiMo MTP before detaching it");
9862 let result = match self.probe_trunk_rows(ids) {
9863 Ok(trunk) => crate::gpu::cpu_scope(|| {
9864 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9865 }),
9866 Err(e) => Err(e),
9867 };
9868 st.reset();
9871 self.mimo_mtp = Some(st);
9872 result
9873 } else if self.mtp.is_some() {
9874 let mut mtp = self
9875 .mtp
9876 .take()
9877 .expect("checked canonical MTP before detaching it");
9878 let result = match self.probe_trunk_rows(ids) {
9879 Ok(trunk) => crate::gpu::cpu_scope(|| {
9880 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9881 }),
9882 Err(e) => Err(e),
9883 };
9884 mtp.kv.clear();
9885 self.mtp = Some(mtp);
9886 result
9887 } else {
9888 Err("MTP FFN probe: this pipeline has no draft stack".into())
9889 };
9890 self.reset_mtp_probe_state();
9893 result
9894 }
9895
9896 fn reset_mtp_probe_state(&mut self) {
9898 self.clear_sequence_state();
9899 if let Some(mtp) = self.mtp.as_mut() {
9900 mtp.kv.clear();
9901 }
9902 if let Some(mimo) = self.mimo_mtp.as_mut() {
9903 mimo.reset();
9904 }
9905 self.mtp_graph_mode = None;
9906 }
9907
9908 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9914 if ids.len() < 2 {
9915 return Err("MTP FFN probe needs at least two token ids".into());
9916 }
9917 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9918 self.clear_sequence_state();
9919 Ok(rows)
9920 }
9921
9922 fn mtp_ffn_probe_teacher_forced(
9928 &mut self,
9929 mtp: &mut MtpModule,
9930 ids: &[u32],
9931 trunk: &[f32],
9932 ) -> Result<Vec<MtpFfnProbe>, String> {
9933 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9934 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9935 };
9936 let inter = dense.gate_proj.rows();
9937 let (hs, n) = (self.hidden_size, ids.len() - 1);
9938 if trunk.len() != n * hs {
9939 return Err(format!(
9940 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9941 trunk.len()
9942 ));
9943 }
9944 let mut mass = vec![0.0f64; inter];
9945 let mut squared_mass = vec![0.0f64; inter];
9946 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9947 for position in 0..n {
9948 self.mtp_probe_ffn_input(
9949 mtp,
9950 &trunk[position * hs..(position + 1) * hs],
9951 ids[position + 1],
9952 position,
9953 )?;
9954 inputs.extend_from_slice(&self.ws.p1[..hs]);
9955 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9956 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9957 unreachable!("checked dense above");
9958 };
9959 accumulate_dense_ffn_activation_mass(
9960 dense,
9961 &inputs,
9962 inputs.len() / hs,
9963 self.pool.as_deref(),
9964 &mut mass,
9965 &mut squared_mass,
9966 )?;
9967 inputs.clear();
9968 }
9969 }
9970 Ok(vec![MtpFfnProbe {
9971 mass,
9972 squared_mass,
9973 observed_positions: n,
9974 }])
9975 }
9976
9977 fn mtp_probe_ffn_input(
9983 &mut self,
9984 mtp: &mut MtpModule,
9985 hidden: &[f32],
9986 next_token: u32,
9987 position: usize,
9988 ) -> Result<(), String> {
9989 let e = self.embed_single(next_token);
9990 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9991 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9992 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9993 inference::rms_norm_into(
9994 hidden,
9995 &mtp.hnorm,
9996 self.rms_eps,
9997 self.norm_style,
9998 cat_h,
9999 );
10000 let mut x = vec![0.0f32; self.hidden_size];
10001 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
10002
10003 let lw = &mtp.layer;
10004 inference::rms_norm_into(
10005 &x,
10006 &lw.input_norm,
10007 self.rms_eps,
10008 self.norm_style,
10009 &mut self.ws.n1,
10010 );
10011 let attn = match &lw.attn {
10012 AttnKind::Full {
10013 wq,
10014 wk,
10015 wv,
10016 wo,
10017 q_norm,
10018 k_norm,
10019 output_gate,
10020 softplus_gate,
10021 bias,
10022 } => {
10023 let mut cfg = self.attn_cfg(position);
10024 cfg.q_norm = q_norm.as_deref();
10025 cfg.k_norm = k_norm.as_deref();
10026 cfg.output_gate = *output_gate;
10027 cfg.softplus_gate = softplus_gate
10028 .as_ref()
10029 .map(|(gate, per_head)| (gate, *per_head));
10030 cfg.bias = bias
10031 .as_ref()
10032 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10033 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
10034 }
10035 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
10036 };
10037 for (dst, src) in x.iter_mut().zip(attn) {
10038 *dst += src;
10039 }
10040 inference::rms_norm_into(
10041 &x,
10042 &lw.post_norm,
10043 self.rms_eps,
10044 self.norm_style,
10045 &mut self.ws.p1,
10046 );
10047 Ok(())
10048 }
10049
10050 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
10055 self.clear_sequence_state();
10056 FFN_PROBE.with(|p| {
10057 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10058 });
10059 crate::gpu::cpu_scope(|| {
10060 for (pos, &id) in ids.iter().enumerate() {
10061 let emb = self.embed_single(id);
10062 let _ = self.forward_layers(&emb, pos, None);
10063 }
10064 });
10065 self.clear_sequence_state();
10066 FFN_PROBE
10067 .with(|p| p.borrow_mut().take())
10068 .unwrap_or_default()
10069 }
10070
10071 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
10075 if let Err(err) = self.nll_begin() {
10076 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
10080 self.nll_end();
10081 return Err(err);
10082 }
10083 FFN_PROBE.with(|p| {
10084 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10085 });
10086 let result: Result<(), String> = (|| {
10087 for chunk in ids.chunks(256) {
10088 if chunk.len() < 2 {
10089 continue;
10090 }
10091 self.nll_ids_masked(chunk, 0, None)?;
10092 }
10093 Ok(())
10094 })();
10095 self.nll_end();
10096 let probe = FFN_PROBE
10097 .with(|p| p.borrow_mut().take())
10098 .unwrap_or_default();
10099 match result {
10100 Ok(()) => Ok(probe),
10101 Err(err) => {
10102 drop(probe);
10103 Err(err)
10104 }
10105 }
10106 }
10107
10108 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
10112 self.nll_begin()?;
10113 let result: Result<f64, String> = (|| {
10114 let mut nll = 0f64;
10115 let mut cnt = 0usize;
10116 let mut hidden = vec![0f32; self.hidden_size];
10117 for (pos, &id) in ids.iter().enumerate() {
10118 if pos > 0 {
10119 inference::rms_norm_into(
10120 &hidden,
10121 &self.weights.final_norm,
10122 self.rms_eps,
10123 self.norm_style,
10124 &mut self.ws.n1,
10125 );
10126 let mut logits = self.lm_head_forward(&self.ws.n1);
10127 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10128 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
10129 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
10130 nll -= p.max(1e-300).ln();
10131 cnt += 1;
10132 attention::recycle_buf(&mut logits);
10133 }
10134 let emb = self.embed_single(id);
10135 hidden = self.forward_layers(&emb, pos, Some(mask));
10136 self.nll_check_graph("masked serial forward", pos)?;
10137 let _ = self.graph_logits.take();
10141 }
10142 Ok((nll / cnt.max(1) as f64).exp())
10143 })();
10144 self.nll_end();
10145 result
10146 }
10147
10148 pub fn nll_ids_masked(
10167 &mut self,
10168 ids: &[u32],
10169 start: usize,
10170 task_mask: Option<&TaskMask>,
10171 ) -> Result<(f64, usize), String> {
10172 let task_mask = self.drop_open_mask(task_mask);
10173 self.nll_ids_inner(ids, start, task_mask)
10174 }
10175
10176 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
10177 self.nll_ids_inner(ids, start, None)
10178 }
10179
10180 pub fn prefill_parity_probe(
10189 &mut self,
10190 ids: &[u32],
10191 split: usize,
10192 bk: usize,
10193 ) -> Result<String, String> {
10194 use std::fmt::Write as _;
10195 let (n, hs) = (ids.len(), self.hidden_size);
10196 if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
10197 return Err(format!(
10198 "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
10199 ));
10200 }
10201 let kinds: Vec<u8> = (0..self.num_layers)
10203 .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
10204 AttnKind::LinearGdn(_) => 1,
10205 AttnKind::Full { .. } => 2,
10206 _ => 0,
10207 })
10208 .collect();
10209 let kv_id = self.graph_kv_id;
10210 let host_chunk = self.prefill_chunk().max(1);
10211 let host_states = |p: &Self| -> Vec<Vec<f32>> {
10212 (0..p.num_layers)
10213 .map(|li| {
10214 if kinds[li] == 1 {
10215 p.kv_cache.layers[li].linear_state.clone()
10216 } else {
10217 Vec::new()
10218 }
10219 })
10220 .collect()
10221 };
10222 let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
10223 (0..kinds.len())
10224 .map(|li| {
10225 if kinds[li] == 1 {
10226 crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
10227 } else {
10228 Vec::new()
10229 }
10230 })
10231 .collect()
10232 };
10233 let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10235 (0..p.num_layers)
10236 .map(|li| {
10237 let c = &p.kv_cache.layers[li];
10238 if kinds[li] != 2
10239 || c.mode != crate::kv_cache::KvMode::F32
10240 || c.base() != 0
10241 || c.seq_len != n
10242 {
10243 return (Vec::new(), Vec::new());
10244 }
10245 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10246 let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
10247 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10248 for pos in 0..n {
10249 let d = (pos * nkv + h) * hd;
10250 k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
10251 v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
10252 }
10253 }
10254 (k, v)
10255 })
10256 .collect()
10257 };
10258 let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10259 (0..p.num_layers)
10260 .map(|li| {
10261 let c = &p.kv_cache.layers[li];
10262 if kinds[li] != 2 {
10263 return (Vec::new(), Vec::new());
10264 }
10265 crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10266 .and_then(|mut v| v.pop())
10267 .unwrap_or_default()
10268 })
10269 .collect()
10270 };
10271 let host_walk = |p: &mut Self,
10272 from: usize,
10273 to: usize,
10274 rows: &mut [f32],
10275 at_split: &mut Option<Vec<Vec<f32>>>| {
10276 let mut pos = from;
10277 while pos < to {
10278 let mut end = (pos + host_chunk).min(to);
10279 if pos < split && end > split {
10280 end = split;
10281 }
10282 let hb = p.prefill_batch(&ids[pos..end], pos);
10283 rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10284 pos = end;
10285 if pos == split {
10286 *at_split = Some(host_states(p));
10287 }
10288 }
10289 };
10290 let graph_walk = |p: &mut Self,
10291 from: usize,
10292 to: usize,
10293 rows: &mut [f32],
10294 at_split: &mut Option<Vec<Vec<f32>>>|
10295 -> Result<(), String> {
10296 let mut pos = from;
10297 while pos < to {
10298 let mut end = (pos + bk).min(to);
10299 if pos < split && end > split {
10300 end = split;
10301 }
10302 let k = end - pos;
10303 let mut hid = vec![0f32; k * hs];
10304 for (j, &id) in ids[pos..end].iter().enumerate() {
10305 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10306 }
10307 let positions: Vec<usize> = (pos..end).collect();
10308 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10309 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10310 return Err(format!(
10311 "prefill parity: batch graph {outcome:?} at position {pos}"
10312 ));
10313 }
10314 rows[pos * hs..end * hs].copy_from_slice(&hid);
10315 pos = end;
10316 if pos == split {
10317 *at_split = Some(graph_states(p));
10318 }
10319 }
10320 Ok(())
10321 };
10322 let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10323
10324 let t = std::time::Instant::now();
10325 self.clear_sequence_state();
10326 let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10327 host_walk(self, 0, n, &mut hh, &mut h_split);
10328 let (h_end, h_kv) = (host_states(self), host_kv(self));
10329 let t_h = t.elapsed().as_secs_f64();
10330
10331 let t = std::time::Instant::now();
10332 self.clear_sequence_state();
10333 let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10334 let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10335 let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10336 let t_g = t.elapsed().as_secs_f64();
10337
10338 let t = std::time::Instant::now();
10339 self.clear_sequence_state();
10340 let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10341 host_walk(self, 0, split, &mut hm, &mut m_split);
10342 let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10343 let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10344 let t_m = t.elapsed().as_secs_f64();
10345 self.clear_sequence_state();
10346 g_run?;
10347 m_run?;
10348
10349 let (sh, sg, sm) = (
10350 score(self, &hh, 0),
10351 score(self, &hg, 0),
10352 score(self, &hm, split),
10353 );
10354 let mut r = String::new();
10355 let _ = writeln!(
10356 r,
10357 "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10358 (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10359 );
10360 let _ = writeln!(
10361 r,
10362 " bin | NLL host graph h+g | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10363 );
10364 let bin = 256usize;
10365 let mut b0 = 0;
10366 while b0 + 1 < n {
10367 let b1 = (b0 + bin).min(n - 1);
10368 let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10369 let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10370 (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10371 };
10372 let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10373 let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10374 if ps.is_empty() {
10375 return None;
10376 }
10377 let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10378 Some(100.0 * same as f64 / ps.len() as f64)
10379 };
10380 let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10381 (b0.max(from)..b1)
10382 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10383 .reduce(f64::max)
10384 };
10385 let f =
10386 |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10387 let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10388 let _ = writeln!(
10389 r,
10390 " {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10391 f(mean(&sh, 0), 3),
10392 f(mean(&sg, 0), 3),
10393 f(mean(&sm, split), 3),
10394 f(agree(&sh, 0, &sg, 0), 1),
10395 f(agree(&sm, split, &sg, 0), 1),
10396 e(hid_rel(&hh, 0)),
10397 e(hid_rel(&hm, split)),
10398 );
10399 b0 = b1;
10400 }
10401 let ring_len = self
10403 .gdn_cfg
10404 .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10405 .unwrap_or(0);
10406 let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10407 let _ = write!(r, " GDN {label} (ring/S rel):");
10408 let mut first = None;
10409 for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10410 let (x, y) = (&a[li], &b[li]);
10411 if x.len() != y.len() || x.len() <= ring_len {
10412 let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10413 continue;
10414 }
10415 let (rr, rs) = (
10416 rel_l2(&x[..ring_len], &y[..ring_len]),
10417 rel_l2(&x[ring_len..], &y[ring_len..]),
10418 );
10419 if first.is_none() && rr.max(rs) > 1e-2 {
10420 first = Some(li);
10421 }
10422 let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10423 }
10424 let _ = writeln!(
10425 r,
10426 "\n first GDN layer past 1e-2: {}",
10427 first.map_or("none".to_string(), |l| l.to_string())
10428 );
10429 };
10430 match (&h_split, &g_split) {
10431 (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10432 _ => {
10433 let _ = writeln!(r, " GDN at split: not captured");
10434 }
10435 }
10436 if let (Some(a), Some(b)) = (&m_split, &h_split) {
10437 state_line(&mut r, "h+g host part vs host at split", a, b);
10438 }
10439 state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10440 state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10441 let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10444 let _ = writeln!(r, " K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10445 for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10446 let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10447 if ak.len() != gk.len() || ak.is_empty() {
10448 let _ = writeln!(r, " L{li}: rows {}/{}", ak.len(), gk.len());
10449 continue;
10450 }
10451 let row = ak.len() / n;
10452 let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10453 let mut b0 = from;
10454 while b0 < n {
10455 let b1 = (b0 + bin).min(n);
10456 let (s0, s1) = (b0 * row, b1 * row);
10457 let (rk, rv) = (
10458 rel_l2(&ak[s0..s1], &gk[s0..s1]),
10459 rel_l2(&av[s0..s1], &gv[s0..s1]),
10460 );
10461 (wk, wv) = (wk.max(rk), wv.max(rv));
10462 if first.is_none() && rk.max(rv) > 1e-2 {
10463 first = Some(b0);
10464 }
10465 b0 = b1;
10466 }
10467 let _ = writeln!(
10468 r,
10469 " L{li}: {wk:.1e}/{wv:.1e} first {}",
10470 first.map_or("none".to_string(), |p| p.to_string())
10471 );
10472 }
10473 };
10474 kv_line(&mut r, "host vs graph", &h_kv, 0);
10475 kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10476 Ok(r)
10477 }
10478
10479 fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10483 let (n, hs) = (ids.len(), self.hidden_size);
10484 let vrows = self.weights.lm_head.rows();
10485 let vs = self.vocab_size.min(vrows);
10486 let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10487 let mut pos = from;
10488 while pos + 1 < n {
10489 let end = (pos + 32).min(n - 1);
10490 let sb = end - pos;
10491 let mut normed = vec![0f32; sb * hs];
10492 for j in 0..sb {
10493 let r = inference::rms_norm(
10494 &rows[(pos + j) * hs..(pos + j + 1) * hs],
10495 &self.weights.final_norm,
10496 self.rms_eps,
10497 self.norm_style,
10498 );
10499 normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10500 }
10501 let mut logits = vec![0f32; sb * vrows];
10502 self.weights
10503 .lm_head
10504 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10505 for j in 0..sb {
10506 let lg = &mut logits[j * vrows..j * vrows + vs];
10507 if let Some(mu) = self.logit_multiplier {
10508 lg.iter_mut().for_each(|v| *v *= mu);
10509 }
10510 if let Some(c) = self.final_softcap {
10511 softcap_in_place(lg, c, self.pool.as_deref());
10512 }
10513 let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10514 for (i, &v) in lg.iter().enumerate() {
10515 if v > max {
10516 (max, top) = (v, i);
10517 }
10518 }
10519 let lse = lg
10520 .iter()
10521 .map(|&v| ((v - max) as f64).exp())
10522 .sum::<f64>()
10523 .ln()
10524 + max as f64;
10525 out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10526 }
10527 pos = end;
10528 }
10529 out
10530 }
10531
10532 pub fn o1_seal_parity_probe(
10546 &mut self,
10547 ids: &[u32],
10548 cfg: crate::nystrom::O1Cfg,
10549 requested: usize,
10550 bk: usize,
10551 ) -> Result<String, String> {
10552 use std::fmt::Write as _;
10553 let (n, hs) = (ids.len(), self.hidden_size);
10554 self.set_o1(Some(cfg));
10555 let split = self
10556 .o1_effective_boundary(requested)
10557 .ok_or("o1 parity: no O(1) configuration")?;
10558 if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10559 return Err(format!(
10560 "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10561 ));
10562 }
10563 let o1_layers: Vec<usize> = (0..self.num_layers)
10564 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10565 .collect();
10566 let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10567 let mut pos = from;
10568 while pos < to {
10569 let end = (pos + bk).min(to);
10570 let k = end - pos;
10571 let mut hid = vec![0f32; k * hs];
10572 for (j, &id) in ids[pos..end].iter().enumerate() {
10573 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10574 }
10575 let positions: Vec<usize> = (pos..end).collect();
10576 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10577 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10578 return Err(format!(
10579 "o1 parity: batch graph {outcome:?} at position {pos}"
10580 ));
10581 }
10582 rows[pos * hs..end * hs].copy_from_slice(&hid);
10583 pos = end;
10584 }
10585 Ok(())
10586 };
10587 let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10590 for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10591 let c = &mut p.kv_cache.layers[li];
10592 let (row, qrow) = (k.len() / split, q.len() / split);
10593 for pos in 0..split {
10594 c.append(
10595 &k[pos * row..(pos + 1) * row],
10596 &v[pos * row..(pos + 1) * row],
10597 &[],
10598 );
10599 c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10600 }
10601 }
10602 p.o1_seal_checked()
10603 };
10604 let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10607 .ok()
10608 .and_then(|v| v.parse().ok())
10609 .unwrap_or(40);
10610 let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10613 let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10614 let argmax = |lg: &[f32], vs: usize| {
10615 (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10616 };
10617 let normed = inference::rms_norm(
10618 &rows[(n - 1) * hs..n * hs],
10619 &p.weights.final_norm,
10620 p.rms_eps,
10621 p.norm_style,
10622 );
10623 let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10624 for s in 1..steps {
10625 let (tok, at) = (out[s - 1], n + s - 1);
10626 let lg = if token_decode {
10627 p.decode_step_logits(tok, at)
10628 } else {
10629 let mut hid = p.embed_single(tok);
10630 let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10631 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10632 return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10633 }
10634 let normed =
10635 inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10636 p.lm_head_forward(&normed)
10637 };
10638 out.push(argmax(&lg, p.vocab_size));
10639 }
10640 Ok(out)
10641 };
10642
10643 self.clear_sequence_state();
10649 self.o1_begin_with_prefix(Some(requested));
10650 let walk_chunk = self.prefill_chunk().max(1);
10651 let walk_from = (split - 1) / walk_chunk * walk_chunk;
10652 let mut ow = vec![0f32; n * hs];
10653 let mut pos = 0;
10654 while pos < split {
10655 let end = (pos + walk_chunk).min(split);
10656 let hb = self.prefill_batch(&ids[pos..end], pos);
10657 ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10658 pos = end;
10659 }
10660 self.o1_seal_checked()?;
10661 let ow_run = graph_rows(self, split, n, &mut ow);
10662 let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10663
10664 let t = std::time::Instant::now();
10668 self.clear_sequence_state();
10669 self.o1_begin_with_prefix(Some(split + 1));
10670 let mut oh = vec![0f32; n * hs];
10671 let mut pos = 0;
10672 while pos < split {
10673 let end = (pos + 128).min(split);
10674 let hb = self.prefill_batch(&ids[pos..end], pos);
10675 oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10676 pos = end;
10677 }
10678 let mut host = Vec::with_capacity(o1_layers.len());
10679 for &li in &o1_layers {
10680 let c = &self.kv_cache.layers[li];
10681 let q = c
10682 .o1_q_trace()
10683 .ok_or("o1 parity: host walk sealed early")?
10684 .to_vec();
10685 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10686 let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10687 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10688 for p in 0..split {
10689 let d = (p * nkv + h) * hd;
10690 k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10691 v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10692 }
10693 }
10694 host.push((q, k, v));
10695 }
10696 for &li in &o1_layers {
10697 self.kv_cache.layers[li].o1_set_boundary(split);
10698 }
10699 self.o1_seal_checked()?;
10700 let oh_run = graph_rows(self, split, n, &mut oh);
10701 let t_oh = t.elapsed().as_secs_f64();
10702 let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10703
10704 self.clear_sequence_state();
10708 self.o1_begin_with_prefix(Some(requested));
10709 let mut og = vec![0f32; n * hs];
10710 graph_rows(self, 0, split, &mut og)?;
10711 let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10712 .ok()
10713 .and_then(|v| v.parse().ok())
10714 .unwrap_or(0.0);
10715 if perturb > 0.0 {
10716 let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10717 let noisy: Vec<_> = host
10718 .iter()
10719 .map(|(q, k, v)| {
10720 let mut jitter = |x: &f32| {
10721 seed ^= seed << 13;
10722 seed ^= seed >> 7;
10723 seed ^= seed << 17;
10724 x * if seed & 1 == 0 {
10725 1.0 + perturb
10726 } else {
10727 1.0 - perturb
10728 }
10729 };
10730 let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10731 let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10732 (q.clone(), k, v)
10733 })
10734 .collect();
10735 seal_from(self, &noisy)?;
10736 } else {
10737 seal_from(self, &host)?;
10738 }
10739 let og_run = graph_rows(self, split, n, &mut og);
10740 let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10741
10742 self.clear_sequence_state();
10744 self.o1_begin_with_prefix(Some(requested));
10745 let mut od = vec![0f32; n * hs];
10746 graph_rows(self, 0, split, &mut od)?;
10747 let mut dev = Vec::with_capacity(o1_layers.len());
10748 let mut kv_rel = Vec::with_capacity(o1_layers.len());
10749 for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10750 let c = &self.kv_cache.layers[li];
10751 let (k, v) = crate::gpu::graph_kv_read_rows(
10752 self.graph_kv_id,
10753 &[(li, 0, split)],
10754 c.num_kv_heads,
10755 c.head_dim,
10756 )
10757 .and_then(|mut rows| rows.pop())
10758 .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10759 kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10760 dev.push((q.clone(), k, v));
10761 }
10762 seal_from(self, &dev)?;
10763 let od_run = graph_rows(self, split, n, &mut od);
10764 let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10765
10766 self.clear_sequence_state();
10768 self.set_o1(None);
10769 let mut xx = vec![0f32; n * hs];
10770 let x_run = graph_rows(self, 0, n, &mut xx);
10771 let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10772 self.clear_sequence_state();
10773 oh_run?;
10774 ow_run?;
10775 og_run?;
10776 od_run?;
10777 x_run?;
10778
10779 let sx = self.parity_score(ids, &xx, split);
10780 let arms = [
10781 ("OH", self.parity_score(ids, &oh, split)),
10782 ("OG", self.parity_score(ids, &og, split)),
10783 ("OD", self.parity_score(ids, &od, split)),
10784 ("OW", self.parity_score(ids, &ow, split)),
10785 ];
10786 let mut r = String::new();
10787 let _ = writeln!(
10788 r,
10789 "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10790 OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10791 );
10792 let _ = write!(r, " seal K/V rel host vs graph:");
10793 for (li, rk, rv) in &kv_rel {
10794 let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10795 }
10796 let worst = |a: &[f32], p0: usize, p1: usize| {
10799 (p0..p1)
10800 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10801 .fold(0f64, f64::max)
10802 };
10803 let _ = writeln!(
10804 r,
10805 "\n prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10806 OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10807 worst(&oh, 0, walk_from),
10808 worst(&oh, walk_from, split),
10809 worst(&ow, 0, walk_from),
10810 worst(&ow, walk_from, split),
10811 );
10812 let _ = writeln!(
10813 r,
10814 " bin | NLL exact OH OG OD OW | top1=exact OH OG OD OW"
10815 );
10816 let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10817 (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10818 };
10819 let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10820 let same = (b0..b1)
10821 .filter(|&p| s[p - split].1 == sx[p - split].1)
10822 .count();
10823 100.0 * same as f64 / (b1 - b0).max(1) as f64
10824 };
10825 let mut b0 = split;
10826 let mut total = [0f64; 5];
10827 while b0 + 1 < n {
10828 let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10829 let _ = writeln!(
10830 r,
10831 " {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10832 mean(&sx, b0, b1),
10833 mean(&arms[0].1, b0, b1),
10834 mean(&arms[1].1, b0, b1),
10835 mean(&arms[2].1, b0, b1),
10836 mean(&arms[3].1, b0, b1),
10837 agree(&arms[0].1, b0, b1),
10838 agree(&arms[1].1, b0, b1),
10839 agree(&arms[2].1, b0, b1),
10840 agree(&arms[3].1, b0, b1),
10841 );
10842 b0 = b1;
10843 }
10844 for (i, s) in std::iter::once(&sx)
10845 .chain(arms.iter().map(|a| &a.1))
10846 .enumerate()
10847 {
10848 total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10849 }
10850 let _ = writeln!(
10851 r,
10852 " all | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10853 total[0],
10854 total[1],
10855 total[2],
10856 total[3],
10857 total[4],
10858 (total[1] - total[0]).exp(),
10859 (total[2] - total[0]).exp(),
10860 (total[3] - total[0]).exp(),
10861 (total[4] - total[0]).exp(),
10862 );
10863 for (name, text) in [
10864 ("X ", x_text),
10865 ("OH", oh_text),
10866 ("OG", og_text),
10867 ("OD", od_text),
10868 ("OW", ow_text),
10869 ] {
10870 let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10871 let _ = writeln!(r, " greedy {name}: {:?}", s);
10872 }
10873 Ok(r)
10874 }
10875
10876 fn nll_ids_inner(
10877 &mut self,
10878 ids: &[u32],
10879 start: usize,
10880 task_mask: Option<&TaskMask>,
10881 ) -> Result<(f64, usize), String> {
10882 self.nll_begin()?;
10883 #[cfg(target_os = "macos")]
10885 if task_mask.is_none() && self.is_block_diffusion() {
10886 if let Some(r) = self.clover_metal_nll(ids, start) {
10887 self.nll_end();
10888 return r;
10889 }
10890 }
10891 let result: Result<(f64, usize), String> = (|| {
10892 let mut nll = 0f64;
10893 let mut cnt = 0usize;
10894 let (graph_quality, fused_head_quality) = nll_graph_policy(
10907 task_mask.is_none(),
10908 self.graph_prefill_preferred(),
10909 crate::gpu::q1_force(),
10910 );
10911 self.graph_head_required = fused_head_quality;
10912 self.graph_want_logits = fused_head_quality;
10913 #[cfg(target_os = "macos")]
10914 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10915 match self.nll_batch_metal(ids, start) {
10916 MetalBatchNllOutcome::Completed(nll, count) => {
10917 return Ok((nll, count));
10918 }
10919 MetalBatchNllOutcome::Declined => {}
10920 MetalBatchNllOutcome::Failed(err) => return Err(err),
10921 }
10922 }
10923 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10928 if self.can_prefill_batched() && !graph_quality && !force_serial {
10929 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10938 .ok()
10939 .and_then(|v| v.parse().ok())
10940 .filter(|&c: &usize| c > 0)
10941 .unwrap_or(128);
10942 const LM_SUB: usize = 32;
10943 let n = ids.len().saturating_sub(1);
10944 let hs = self.hidden_size;
10945 let rows = self.weights.lm_head.rows();
10946 let mut pos = 0usize;
10947 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10948 while pos < n {
10949 let end = (pos + chunk_len).min(n);
10950 let bsz = end - pos;
10951 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10952 self.nll_check_graph("batched prefill", pos)?;
10953 if state_trace && end % 256 == 0 {
10954 self.trace_recurrent_state(end);
10955 }
10956 let mut k0 = 0usize;
10957 while k0 < bsz {
10958 let k1 = (k0 + LM_SUB).min(bsz);
10959 let sb = k1 - k0;
10960 if pos + k1 <= start {
10963 k0 = k1;
10964 continue;
10965 }
10966 let mut normed = vec![0.0f32; sb * hs];
10967 for k in 0..sb {
10968 let r = inference::rms_norm(
10969 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10970 &self.weights.final_norm,
10971 self.rms_eps,
10972 self.norm_style,
10973 );
10974 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10975 }
10976 let mut logits = vec![0.0f32; sb * rows];
10977 self.weights
10978 .lm_head
10979 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10980 for k in 0..sb {
10981 if pos + k0 + k < start {
10982 continue;
10983 }
10984 self.nll_check_graph("batched score row", pos + k0 + k)?;
10985 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10986 if let Some(mu) = self.logit_multiplier {
10987 for v in lg.iter_mut() {
10988 *v *= mu;
10989 }
10990 }
10991 if let Some(c) = self.final_softcap {
10995 softcap_in_place(lg, c, self.pool.as_deref());
10996 }
10997 if let Some(cm) = self.head_clusters.clone() {
11000 self.hierarchical_head_logprobs(
11001 &normed[k * hs..(k + 1) * hs],
11002 &cm,
11003 lg,
11004 );
11005 }
11006 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
11007 let target = ids[pos + k0 + k + 1] as usize;
11008 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11009 let lse: f64 = lg
11010 .iter()
11011 .map(|&v| ((v - max) as f64).exp())
11012 .sum::<f64>()
11013 .ln()
11014 + max as f64;
11015 nll += lse - lg[target] as f64;
11016 cnt += 1;
11017 if std::env::var("CMF_PPL_TRACE").is_ok() {
11018 let top = lg
11019 .iter()
11020 .enumerate()
11021 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11022 .map(|(i, _)| i)
11023 .unwrap_or(0);
11024 eprintln!(
11025 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
11026 pos + k0 + k,
11027 target,
11028 lse - lg[target] as f64,
11029 top,
11030 lg[target],
11031 lg[top]
11032 );
11033 }
11034 }
11035 k0 = k1;
11036 }
11037 pos = end;
11038 }
11039 return Ok((nll, cnt));
11040 }
11041 for pos in 0..ids.len().saturating_sub(1) {
11042 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
11043 self.nll_check_graph("serial forward", pos)?;
11044 let out_of_band = self.graph_logits.take();
11052 if self.graph_head_required && out_of_band.is_none() {
11053 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11054 return Err(format!(
11055 "fused Metal graph head did not complete at NLL position {pos}"
11056 ));
11057 }
11058 if pos < start {
11059 continue;
11060 }
11061 let logits = match out_of_band {
11062 Some(lg) => lg,
11063 None => {
11064 let normed = inference::rms_norm(
11065 &hidden,
11066 &self.weights.final_norm,
11067 self.rms_eps,
11068 self.norm_style,
11069 );
11070 self.lm_head_forward(&normed)
11074 }
11075 };
11076 let target = ids[pos + 1] as usize;
11077 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11078 let lse: f64 = logits
11079 .iter()
11080 .map(|&v| ((v - max) as f64).exp())
11081 .sum::<f64>()
11082 .ln()
11083 + max as f64;
11084 let tok_nll = lse - logits[target] as f64;
11085 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11086 let top = logits
11087 .iter()
11088 .enumerate()
11089 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11090 .map(|(i, _)| i)
11091 .unwrap_or(0);
11092 eprintln!(
11093 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11094 logits[target], logits[top]
11095 );
11096 }
11097 nll += tok_nll;
11098 cnt += 1;
11099 }
11100 Ok((nll, cnt))
11101 })();
11102 self.nll_end();
11103 result
11104 }
11105
11106 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
11111 let normed = inference::rms_norm(
11112 hidden,
11113 &self.weights.final_norm,
11114 self.rms_eps,
11115 self.norm_style,
11116 );
11117 let mut logits = self.lm_head_forward(&normed);
11120 let target = target as usize;
11121 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11122 let lse: f64 = logits
11123 .iter()
11124 .map(|&v| ((v - max) as f64).exp())
11125 .sum::<f64>()
11126 .ln()
11127 + max as f64;
11128 let tok_nll = lse - logits[target] as f64;
11129 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11130 let top = logits
11131 .iter()
11132 .enumerate()
11133 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11134 .map(|(i, _)| i)
11135 .unwrap_or(0);
11136 eprintln!(
11137 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11138 logits[target], logits[top]
11139 );
11140 }
11141 attention::recycle_buf(&mut logits);
11142 tok_nll
11143 }
11144
11145 fn trace_recurrent_state(&self, pos: usize) {
11153 let stats = |v: &[f32]| -> (f64, f64) {
11154 if v.is_empty() {
11155 return (0.0, 0.0);
11156 }
11157 let (mut ss, mut mx) = (0f64, 0f64);
11158 for &x in v {
11159 ss += (x as f64) * (x as f64);
11160 mx = mx.max((x as f64).abs());
11161 }
11162 ((ss / v.len() as f64).sqrt(), mx)
11163 };
11164 for (li, l) in self.kv_cache.layers.iter().enumerate() {
11165 let lw = &self.weights.layers[self.phys_layer(li)];
11166 let (kind, s_len) = match &lw.attn {
11167 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
11168 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
11169 AttnKind::Bounded(_) => ("bounded", 0),
11170 AttnKind::Full { .. } => ("full", 0),
11171 _ => ("other", 0),
11172 };
11173 let (rms, max) = stats(&l.linear_state);
11174 let s_part = if kind == "vmf" {
11175 &l.linear_state[..s_len.min(l.linear_state.len())]
11176 } else if kind == "gdn" {
11177 let ring = l
11178 .linear_state
11179 .len()
11180 .saturating_sub(s_len)
11181 .min(l.linear_state.len());
11182 &l.linear_state[ring..]
11183 } else {
11184 &l.linear_state[..0]
11185 };
11186 let (s_rms, s_max) = stats(s_part);
11187 let (ring_rms, ring_len) = match &l.bounded {
11188 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
11189 None => (0.0, 0),
11190 };
11191 eprintln!(
11192 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
11193 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
11194 l.linear_state.len(),
11195 l.seq_len
11196 );
11197 }
11198 }
11199
11200 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
11218 self.nll_begin()?;
11223 let requested_prefix = (prefill > 0).then_some(prefill);
11224 self.o1_begin_with_prefix(requested_prefix);
11225 let n = ids.len().saturating_sub(1);
11226 let requested_start = prefill.min(n);
11227 let exact_end = if self.o1_active() {
11232 match requested_prefix {
11233 Some(requested) => self.o1_effective_boundary(requested),
11234 None => self
11235 .o1_cfg
11236 .as_ref()
11237 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
11238 }
11239 .unwrap_or(requested_start)
11240 .min(n)
11241 } else {
11242 requested_start
11243 };
11244 let mut nll = 0f64;
11245 let mut cnt = 0usize;
11246
11247 let mut pos = 0usize;
11251 if self.can_prefill_batched() {
11252 const CHUNK: usize = 128;
11253 while pos < exact_end {
11254 let end = (pos + CHUNK).min(exact_end);
11255 let hiddens = self.prefill_batch(&ids[pos..end], pos);
11256 if self
11257 .graph_failed
11258 .swap(false, std::sync::atomic::Ordering::Relaxed)
11259 {
11260 self.cancel
11261 .store(false, std::sync::atomic::Ordering::Relaxed);
11262 self.nll_end();
11263 return Err("GPU graph failed during O(1) NLL prefix".into());
11264 }
11265 for row in 0..end - pos {
11266 let score_pos = pos + row;
11267 if score_pos >= requested_start && score_pos < n {
11268 nll += self.nll_from_hidden(
11269 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11270 ids[score_pos + 1],
11271 score_pos,
11272 );
11273 cnt += 1;
11274 }
11275 }
11276 pos = end;
11277 }
11278 } else {
11279 while pos < exact_end {
11280 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11281 if self
11282 .graph_failed
11283 .swap(false, std::sync::atomic::Ordering::Relaxed)
11284 {
11285 self.cancel
11286 .store(false, std::sync::atomic::Ordering::Relaxed);
11287 self.nll_end();
11288 return Err("GPU graph failed during O(1) NLL prefix".into());
11289 }
11290 if pos >= requested_start && pos < n {
11291 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11292 cnt += 1;
11293 }
11294 pos += 1;
11295 }
11296 }
11297 self.o1_seal_checked().map_err(|err| {
11298 self.nll_end();
11299 err
11300 })?;
11301
11302 let batch_k = std::env::var("CMF_BATCH_K")
11311 .ok()
11312 .and_then(|v| v.parse::<usize>().ok())
11313 .unwrap_or(0);
11314 let batch_admitted = batch_k > 0
11315 && self.can_prefill_batched()
11316 && self.o1_active()
11317 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11318 && (0..self.num_layers).all(|li| {
11319 let cache = &self.kv_cache.layers[self.phys_layer(li)];
11320 cache.o1.is_none() || cache.o1_views().is_some()
11321 });
11322 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11323 eprintln!(
11324 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11325 batch_admitted,
11326 batch_k,
11327 n.saturating_sub(exact_end),
11328 );
11329 }
11330 let mut batch_completed = false;
11331 if batch_admitted && exact_end < n {
11332 let hs = self.hidden_size;
11333 let mut batch_pos = exact_end;
11334 while batch_pos < n {
11335 let end = (batch_pos + batch_k).min(n);
11336 let bk = end - batch_pos;
11337 let mut hiddens = vec![0.0f32; bk * hs];
11338 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11339 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11340 }
11341 let positions: Vec<usize> = (batch_pos..end).collect();
11342 let t_batch = std::time::Instant::now();
11343 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11344 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11345 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11346 eprintln!(
11347 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11348 batch_pos,
11349 end.saturating_sub(1),
11350 bk as f64 / (ms / 1000.0),
11351 );
11352 }
11353 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11354 self.nll_end();
11355 return Err(err);
11356 }
11357 match outcome {
11358 crate::gpu::BatchGraphOutcome::Completed => {
11359 batch_completed = true;
11360 for row in 0..bk {
11361 nll += self.nll_from_hidden(
11362 &hiddens[row * hs..(row + 1) * hs],
11363 ids[batch_pos + row + 1],
11364 batch_pos + row,
11365 );
11366 cnt += 1;
11367 }
11368 batch_pos = end;
11369 }
11370 crate::gpu::BatchGraphOutcome::Declined => {
11371 if batch_completed {
11372 self.nll_end();
11373 return Err(format!(
11374 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11375 ));
11376 }
11377 break;
11378 }
11379 crate::gpu::BatchGraphOutcome::Failed => {
11380 self.nll_end();
11381 return Err(format!(
11382 "O(1) NLL batch graph failed after admission at position {batch_pos}"
11383 ));
11384 }
11385 }
11386 }
11387 if batch_completed && cnt == n.saturating_sub(requested_start) {
11388 self.nll_end();
11389 return Ok((nll, cnt));
11390 }
11391 }
11392
11393 for pos in exact_end..n {
11398 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11399 if self
11400 .graph_failed
11401 .swap(false, std::sync::atomic::Ordering::Relaxed)
11402 {
11403 self.cancel
11404 .store(false, std::sync::atomic::Ordering::Relaxed);
11405 self.nll_end();
11406 return Err(format!(
11407 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11408 ));
11409 }
11410 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11411 cnt += 1;
11412 }
11413 self.nll_end();
11414 Ok((nll, cnt))
11415 }
11416
11417 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11425 self.clear_sequence_state();
11426 let n = ids.len().saturating_sub(1);
11427 let mut correct = Vec::with_capacity(n);
11428 let mut pmax = Vec::with_capacity(n);
11429 for pos in 0..n {
11430 let emb = self.embed_single(ids[pos]);
11431 let hidden = self.forward_layers(&emb, pos, None);
11432 let logits = if let Some(logits) = self.graph_logits.take() {
11433 logits
11434 } else {
11435 let normed = inference::rms_norm(
11436 &hidden,
11437 &self.weights.final_norm,
11438 self.rms_eps,
11439 self.norm_style,
11440 );
11441 self.lm_head_forward(&normed)
11445 };
11446 let target = ids[pos + 1] as usize;
11447 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11448 for (i, &v) in logits.iter().enumerate() {
11449 if v > mval {
11450 mval = v;
11451 amax = i;
11452 }
11453 }
11454 correct.push(amax == target);
11455 let row: Vec<f32> = temps
11456 .iter()
11457 .map(|&t| {
11458 let tt = t.max(1e-3);
11459 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11460 1.0 / s.max(1e-12) })
11462 .collect();
11463 pmax.push(row);
11464 }
11465 self.clear_sequence_state();
11466 (correct, pmax)
11467 }
11468
11469 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11476 if self.dyn_router.is_none() {
11477 return Ok((self.ppl_ids(ids)?, 0));
11478 }
11479 self.nll_begin()?;
11480 let saved_active = self.dyn_active;
11481 let mut router = self
11482 .dyn_router
11483 .take()
11484 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11485 router.reset();
11486 self.dyn_phi_seen = 0;
11487 let _ = self.set_active_skill(None);
11488
11489 let result: Result<(f64, usize), String> = (|| {
11490 let mut nll = 0f64;
11491 let mut cnt = 0usize;
11492 for pos in 0..ids.len().saturating_sub(1) {
11493 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11494 self.nll_check_graph("dynamic serial forward", pos)?;
11495 let out_of_band = self.graph_logits.take();
11496 let mut logits = match out_of_band {
11497 Some(lg) => lg,
11498 None => {
11499 let normed = inference::rms_norm(
11500 &hidden,
11501 &self.weights.final_norm,
11502 self.rms_eps,
11503 self.norm_style,
11504 );
11505 self.lm_head_forward(&normed)
11509 }
11510 };
11511 let target = ids[pos + 1] as usize;
11512 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11513 let lse: f64 = logits
11514 .iter()
11515 .map(|&v| ((v - max) as f64).exp())
11516 .sum::<f64>()
11517 .ln()
11518 + max as f64;
11519 let tok_nll = lse - logits[target] as f64;
11520 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11521 let top = logits
11522 .iter()
11523 .enumerate()
11524 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11525 .map(|(i, _)| i)
11526 .unwrap_or(0);
11527 eprintln!(
11528 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11529 logits[target], logits[top]
11530 );
11531 }
11532 nll += tok_nll;
11533 cnt += 1;
11534 attention::recycle_buf(&mut logits);
11535 let phi = self.dyn_phi_ema.clone();
11537 if let Some(new_active) = router.step(&phi, pos) {
11538 let _ = self.set_active_skill(new_active);
11539 }
11540 }
11541 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11542 })();
11543
11544 let _ = self.set_active_skill(saved_active);
11547 self.dyn_router = Some(router);
11548 self.nll_end();
11549 result
11550 }
11551
11552 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11554 self.clear_sequence_state();
11555 let mut acc = vec![0f32; self.hidden_size];
11556 for (pos, &id) in ids.iter().enumerate() {
11557 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11558 for (a, v) in acc.iter_mut().zip(&h) {
11559 *a += v;
11560 }
11561 }
11562 let n = ids.len().max(1) as f32;
11563 for a in acc.iter_mut() {
11564 *a /= n;
11565 }
11566 self.clear_sequence_state();
11567 acc
11568 }
11569
11570 pub fn probe_phi_span(
11584 &mut self,
11585 ids: &[u32],
11586 layer: usize,
11587 span: std::ops::Range<usize>,
11588 ) -> Vec<f32> {
11589 #[cfg(target_os = "macos")]
11590 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11591 let end = span.end.min(ids.len());
11592 let start = span.start.min(end);
11593 let reset = |p: &mut Self| p.clear_sequence_state();
11594 reset(self);
11595 let mut acc = vec![0f32; self.hidden_size];
11596 for (pos, &id) in ids[..end].iter().enumerate() {
11597 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11598 if pos >= start {
11599 for (a, v) in acc.iter_mut().zip(&h) {
11600 *a += v;
11601 }
11602 }
11603 }
11604 let n = end - start;
11605 if n > 0 {
11606 let n = n as f32;
11607 for a in acc.iter_mut() {
11608 *a /= n;
11609 }
11610 }
11611 reset(self);
11612 acc
11613 }
11614
11615 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11622 #[cfg(target_os = "macos")]
11623 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11624 self.graph_logits = None;
11625 let hidden = self.forward_layers(&self.embed_single(token), position, None);
11626 if let Some(logits) = self.graph_logits.take() {
11627 return logits;
11628 }
11629 inference::rms_norm_into(
11630 &hidden,
11631 &self.weights.final_norm,
11632 self.rms_eps,
11633 self.norm_style,
11634 &mut self.ws.n1,
11635 );
11636 self.lm_head_forward(&self.ws.n1)
11637 }
11638
11639 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11645 self.prefill_batch_masked(ids, start_pos, None)
11646 }
11647
11648 fn prefill_batch_masked(
11654 &mut self,
11655 ids: &[u32],
11656 start_pos: usize,
11657 task_mask: Option<&TaskMask>,
11658 ) -> Vec<f32> {
11659 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11660 }
11661
11662 fn prefill_rows(
11670 &mut self,
11671 ids: &[u32],
11672 pos: usize,
11673 task_mask: Option<&TaskMask>,
11674 ) -> Result<Vec<f32>, String> {
11675 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11676 }
11677
11678 fn prefill_input_rows(
11679 &mut self,
11680 input: PrefillIn<'_>,
11681 pos: usize,
11682 task_mask: Option<&TaskMask>,
11683 ) -> Result<Vec<f32>, String> {
11684 self.mimo_moe_prepare();
11685 let hs = self.hidden_size;
11686 let bk = match input {
11687 PrefillIn::Ids(ids) => ids.len(),
11688 PrefillIn::Hidden(rows) => rows.len() / hs,
11689 };
11690 #[cfg(not(target_os = "macos"))]
11691 if task_mask.is_none()
11692 && !self.o1_active()
11693 && bk > 1
11694 && (self.batch_prefix_prefill()
11695 || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
11698 || (self.verify_exact_moe
11699 && crate::gpu::enabled_here()
11700 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11701 {
11702 let mut hiddens = match input {
11703 PrefillIn::Hidden(rows) => rows.to_vec(),
11704 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11705 };
11706 let positions: Vec<usize> = (pos..pos + bk).collect();
11707 let mut run = 0usize;
11708 match self.try_batch_graph_wgpu_prefix(
11709 &mut hiddens,
11710 &positions,
11711 bk,
11712 None,
11713 Some(&mut run),
11714 ) {
11715 crate::gpu::BatchGraphOutcome::Completed => {
11716 let out = if run < self.num_layers {
11717 self.prefill_batch_span(
11718 PrefillIn::Hidden(&hiddens),
11719 pos,
11720 None,
11721 run,
11722 self.num_layers,
11723 )
11724 } else {
11725 hiddens
11726 };
11727 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11728 Err("MiMo attention graph failed after admission".into())
11729 } else {
11730 Ok(out)
11731 };
11732 }
11733 crate::gpu::BatchGraphOutcome::Failed => {
11734 return Err("batched prefix prefill failed after admission".into());
11735 }
11736 crate::gpu::BatchGraphOutcome::Declined => {
11737 #[cfg(feature = "gpu")]
11739 self.pull_lagging_host_kv(0, self.num_layers, pos);
11740 }
11741 }
11742 }
11743 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11744 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11745 Err("batch tail graph failed after admission".into())
11746 } else {
11747 Ok(out)
11748 }
11749 }
11750
11751 fn prefill_batch_span(
11757 &mut self,
11758 input: PrefillIn<'_>,
11759 start_pos: usize,
11760 task_mask: Option<&TaskMask>,
11761 from: usize,
11762 upto_excl: usize,
11763 ) -> Vec<f32> {
11764 let hs = self.hidden_size;
11765 let b = match input {
11766 PrefillIn::Ids(ids) => ids.len(),
11767 PrefillIn::Hidden(hb) => hb.len() / hs,
11768 };
11769 let upto_excl = upto_excl.min(self.num_layers);
11770 let mut h: Vec<f32>;
11774 let mut h_ready;
11775 match input {
11776 PrefillIn::Ids(_) => {
11777 h = vec![0.0; b * hs];
11778 h_ready = false;
11779 }
11780 PrefillIn::Hidden(hb) => {
11781 h = hb.to_vec();
11782 h_ready = true;
11783 }
11784 }
11785 let fill_h = |h: &mut Vec<f32>, me: &Self| {
11786 if let PrefillIn::Ids(ids) = input {
11787 for (bi, &id) in ids.iter().enumerate() {
11788 let e = me.embed_single(id);
11789 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11790 }
11791 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11792 if let Ok(t) = tp.parse::<usize>() {
11793 if t >= start_pos && t < start_pos + ids.len() {
11794 let bi = t - start_pos;
11795 let row = &h[bi * hs..(bi + 1) * hs];
11796 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11797 eprintln!(
11798 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11799 ids[bi],
11800 row[0],
11801 row[1],
11802 ids.len(),
11803 &ids[..ids.len().min(8)]
11804 );
11805 }
11806 }
11807 }
11808 }
11809 };
11810 let (_nkv, _hd, _rd, eps) = (
11811 self.num_kv_heads,
11812 self.head_dim,
11813 self.rotary_dim,
11814 self.rms_eps,
11815 );
11816 let pool = self.pool.clone();
11817 let norm_style = self.norm_style;
11818 self.mimo_moe_prepare();
11819 let automatic_gpu_prefix = self.automatic_gpu_prefix();
11820
11821 #[cfg(target_os = "macos")]
11822 let mut chunk_skip_until = 0usize;
11823 let mut mhc_rs: Vec<f32> = Vec::new();
11825 for li in from..upto_excl {
11826 let _capacity_tail = automatic_gpu_prefix
11827 .filter(|&prefix| {
11828 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11829 })
11830 .map(|_| crate::gpu::enter_cpu_scope());
11831 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
11836 let _fast_gemm = self
11837 .proj_gate_sigmoid
11838 .then(crate::gpu::enter_prefill_fast_gemm);
11839 #[cfg(target_os = "macos")]
11845 if task_mask.is_none() {
11846 if li < chunk_skip_until {
11847 continue;
11848 }
11849 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11855 fill_h(&mut h, self);
11856 h_ready = true;
11857 }
11858 let ids_for_embed = match input {
11859 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11860 PrefillIn::Hidden(_) => None,
11861 };
11862 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11863 if end > li {
11864 h_ready = true;
11865 chunk_skip_until = end;
11866 if self.is_loop_end(end - 1) && end < self.num_layers {
11869 for bi in 0..b {
11870 let normed = inference::rms_norm(
11871 &h[bi * hs..(bi + 1) * hs],
11872 &self.weights.final_norm,
11873 eps,
11874 norm_style,
11875 );
11876 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11877 }
11878 }
11879 continue;
11880 }
11881 }
11882 if !h_ready {
11883 fill_h(&mut h, self);
11884 h_ready = true;
11885 }
11886 if task_mask.is_none() && self.verify_exact_moe {
11887 let positions: Vec<_> = (start_pos..start_pos + b).collect();
11888 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11889 crate::gpu::BatchGraphOutcome::Completed => continue,
11890 crate::gpu::BatchGraphOutcome::Failed => return h,
11891 crate::gpu::BatchGraphOutcome::Declined => {}
11892 }
11893 }
11894 #[cfg(feature = "gpu")]
11895 self.pull_lagging_host_kv(li, li + 1, start_pos);
11896 let mhc_attn = self.mhc_layer(li).then(|| {
11900 let m = self.mhc.as_deref().expect("mhc layer");
11901 let p = self.phys_layer(li);
11902 let n = m.s.streams * hs;
11903 mhc_rs.resize(b * n, 0.0);
11904 let coefs: Vec<_> = (0..b)
11905 .map(|bi| {
11906 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
11907 if m.enters(p) {
11908 m.enter(r, &h[bi * hs..(bi + 1) * hs]);
11909 }
11910 m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
11911 })
11912 .collect();
11913 (h.clone(), coefs)
11914 });
11915 let lw = &self.weights.layers[self.phys_layer(li)];
11916 let t_attn = std::time::Instant::now();
11917 match &lw.attn {
11919 AttnKind::Kda(w) => {
11920 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11922 let mut normed = vec![0.0f32; b * hs];
11923 for bi in 0..b {
11924 inference::rms_norm_into(
11925 &h[bi * hs..(bi + 1) * hs],
11926 &lw.input_norm,
11927 eps,
11928 norm_style,
11929 &mut normed[bi * hs..(bi + 1) * hs],
11930 );
11931 }
11932 let attn = crate::linear_core::kda_forward_batch(
11933 &normed,
11934 b,
11935 w,
11936 &cfg,
11937 &mut self.kv_cache.layers[li].linear_state,
11938 pool.as_deref(),
11939 );
11940 for (dst, &a) in h.iter_mut().zip(&attn) {
11941 *dst += a;
11942 }
11943 }
11944 AttnKind::LinearGdn(w) => {
11945 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11947 let mut normed = vec![0.0f32; b * hs];
11948 for bi in 0..b {
11949 let r = inference::rms_norm(
11950 &h[bi * hs..(bi + 1) * hs],
11951 &lw.input_norm,
11952 eps,
11953 norm_style,
11954 );
11955 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11956 }
11957 let attn = crate::linear_core::gdn_forward_batch(
11958 &normed,
11959 b,
11960 w,
11961 &cfg,
11962 &mut self.kv_cache.layers[li].linear_state,
11963 pool.as_deref(),
11964 );
11965 for (dst, &a) in h.iter_mut().zip(&attn) {
11966 *dst += a;
11967 }
11968 }
11969 AttnKind::ShortConv(w) => {
11970 let cfg = self
11973 .short_conv_cfg
11974 .expect("short-conv layer without short_conv_cfg");
11975 let mut normed = vec![0.0f32; b * hs];
11976 for bi in 0..b {
11977 inference::rms_norm_into(
11978 &h[bi * hs..(bi + 1) * hs],
11979 &lw.input_norm,
11980 eps,
11981 norm_style,
11982 &mut normed[bi * hs..(bi + 1) * hs],
11983 );
11984 }
11985 let mut attn = short_conv_forward_batch(
11986 &normed,
11987 b,
11988 w,
11989 &cfg,
11990 &mut self.kv_cache.layers[li].linear_state,
11991 pool.as_deref(),
11992 );
11993 if let Some(wn) = &lw.attn_out_norm {
11996 for bi in 0..b {
11997 inference::rms_norm_into(
11998 &attn[bi * hs..(bi + 1) * hs],
11999 wn,
12000 eps,
12001 norm_style,
12002 &mut normed[bi * hs..(bi + 1) * hs],
12003 );
12004 }
12005 attn.copy_from_slice(&normed);
12006 }
12007 for (dst, &a) in h.iter_mut().zip(&attn) {
12008 *dst += a;
12009 }
12010 }
12011 AttnKind::Mla(w) => {
12012 let inv_freq_l = self.layer_inv_freq(li);
12015 let rs = self.layer_rope_scale(li);
12016 let mut normed = vec![0.0f32; hs];
12017 for bi in 0..b {
12018 inference::rms_norm_into(
12019 &h[bi * hs..(bi + 1) * hs],
12020 &lw.input_norm,
12021 eps,
12022 norm_style,
12023 &mut normed,
12024 );
12025 let ao = mla_attention(
12026 w,
12027 &normed,
12028 &mut self.kv_cache.layers[li],
12029 start_pos + bi,
12030 &inv_freq_l,
12031 rs,
12032 eps,
12033 pool.as_deref(),
12034 );
12035 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
12036 *dst += a;
12037 }
12038 }
12039 }
12040 AttnKind::Full {
12041 wq,
12042 wk,
12043 wv,
12044 wo,
12045 q_norm,
12046 k_norm,
12047 output_gate,
12048 softplus_gate,
12049 bias,
12050 } => {
12051 let mut normed = vec![0.0f32; b * hs];
12055 for bi in 0..b {
12056 inference::rms_norm_into(
12057 &h[bi * hs..(bi + 1) * hs],
12058 &lw.input_norm,
12059 eps,
12060 norm_style,
12061 &mut normed[bi * hs..(bi + 1) * hs],
12062 );
12063 }
12064 let inv_freq_l = self.layer_inv_freq(li);
12065 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12066 let cfg = QwenAttnCfg {
12067 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
12068 num_heads: self.layer_num_heads(li),
12069 num_kv_heads: nkv_l,
12070 head_dim: hd_l,
12071 hidden_size: hs,
12072 position: start_pos,
12073 inv_freq: &inv_freq_l,
12074 rotary_dim: rd_l,
12075 scale: self.attn_scale,
12076 softcap: self.attn_softcap,
12077 window: self.layer_window(li),
12078 v_norm: self.attn_v_norm,
12079 qk_norm_after_rope: self.qk_norm_after_rope,
12080 gate_sigmoid: self.proj_gate_sigmoid,
12081 q_norm: q_norm.as_deref(),
12082 k_norm: k_norm.as_deref(),
12083 output_gate: *output_gate,
12084 softplus_gate: softplus_gate
12085 .as_ref()
12086 .map(|(gate, per_head)| (gate, *per_head)),
12087 rope_scale: self.layer_rope_scale(li),
12088 bias: bias
12089 .as_ref()
12090 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12091 rms_eps: eps,
12092 norm_style,
12093 pool: pool.as_deref(),
12094 v_head_dim: self.layer_v_dim(li),
12095 };
12096 #[cfg(feature = "gpu")]
12097 let _mirror = self
12098 .prefill_mirror_target(li)
12099 .map(crate::gpu::enter_prefill_mirror);
12100 let mut attn = attention::qwen_attention_batch(
12101 &normed,
12102 b,
12103 wq,
12104 wk,
12105 wv,
12106 wo,
12107 &mut self.kv_cache.layers[li],
12108 &cfg,
12109 );
12110 if let Some(w) = &lw.attn_out_norm {
12111 for bi in 0..b {
12112 inference::rms_norm_into(
12113 &attn[bi * hs..(bi + 1) * hs],
12114 w,
12115 eps,
12116 norm_style,
12117 &mut normed[bi * hs..(bi + 1) * hs],
12118 );
12119 }
12120 attn.copy_from_slice(&normed);
12121 }
12122 for (dst, &a) in h.iter_mut().zip(&attn) {
12123 *dst += a;
12124 }
12125 }
12126 AttnKind::Bounded(w) => {
12127 let mut normed = vec![0.0f32; b * hs];
12130 for bi in 0..b {
12131 inference::rms_norm_into(
12132 &h[bi * hs..(bi + 1) * hs],
12133 &lw.input_norm,
12134 eps,
12135 norm_style,
12136 &mut normed[bi * hs..(bi + 1) * hs],
12137 );
12138 }
12139 let rope = self
12140 .bounded_rope
12141 .clone()
12142 .expect("bounded layer without an installed rotation table");
12143 let cfg = crate::bounded::BoundedAttnCfg {
12144 num_heads: self.num_heads,
12145 num_kv_heads: self.num_kv_heads,
12146 head_dim: self.head_dim,
12147 hidden_size: hs,
12148 scale: self.attn_scale,
12149 rope: &rope,
12150 pool: pool.as_deref(),
12151 };
12152 let mut attn = crate::bounded::bounded_attention_batch(
12153 &normed,
12154 b,
12155 w,
12156 &mut self.kv_cache.layers[li],
12157 &cfg,
12158 );
12159 if let Some(wn) = &lw.attn_out_norm {
12160 for bi in 0..b {
12161 inference::rms_norm_into(
12162 &attn[bi * hs..(bi + 1) * hs],
12163 wn,
12164 eps,
12165 norm_style,
12166 &mut normed[bi * hs..(bi + 1) * hs],
12167 );
12168 }
12169 attn.copy_from_slice(&normed);
12170 }
12171 for (dst, &a) in h.iter_mut().zip(&attn) {
12172 *dst += a;
12173 }
12174 attention::recycle_buf(&mut attn);
12175 }
12176 AttnKind::Linear(w) => {
12177 for bi in 0..b {
12178 let normed = inference::rms_norm(
12179 &h[bi * hs..(bi + 1) * hs],
12180 &lw.input_norm,
12181 eps,
12182 norm_style,
12183 );
12184 vmf_phase_forward(
12185 &normed,
12186 w,
12187 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
12188 &mut self.kv_cache.layers[li].linear_state,
12189 pool.as_deref(),
12190 )
12191 .iter()
12192 .enumerate()
12193 .for_each(|(i, &a)| h[bi * hs + i] += a);
12194 }
12195 }
12196 }
12197
12198 let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
12201 let m = self.mhc.as_deref().expect("mhc layer");
12202 let p = self.phys_layer(li);
12203 let n = m.s.streams * hs;
12204 let mut y = vec![0f32; hs];
12205 (0..b)
12206 .map(|bi| {
12207 for (e, v) in y.iter_mut().enumerate() {
12208 *v = h[bi * hs + e] - branch[bi * hs + e];
12209 }
12210 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12211 m.merge(r, &y, &coefs[bi]);
12212 m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
12213 })
12214 .collect::<Vec<_>>()
12215 });
12216 let lw = &self.weights.layers[self.phys_layer(li)];
12218 let mut post = vec![0.0f32; b * hs];
12219 for bi in 0..b {
12220 let r =
12221 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
12222 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12223 }
12224 let mask_row = task_mask
12227 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
12228 .and_then(|m| m.ffn_masks.get(li))
12229 .map(|v| v.as_slice());
12230 let attn_ns = t_attn.elapsed().as_nanos() as u64;
12231 let t_ffn = std::time::Instant::now();
12232 let mut ffn = match &lw.ffn {
12233 FfnKind::Dense(d) if !d.segs.is_empty() => {
12234 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
12235 }
12236 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
12237 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
12238 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
12239 }
12240 FfnKind::Moe(m) if self.verify_exact_moe => {
12241 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
12242 }
12243 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
12246 let before = m.stats.borrow().clone();
12247 let out = crate::gpu::cpu_scope(|| {
12248 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
12249 });
12250 self.mimo_moe.prime(li, m, &before);
12251 out
12252 }
12253 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
12254 FfnKind::DenseMoe(dm) => {
12257 let mut out = vec![0.0f32; b * hs];
12258 for bi in 0..b {
12259 let r = dense_moe_ffn(
12260 dm,
12261 &post[bi * hs..(bi + 1) * hs],
12262 &h[bi * hs..(bi + 1) * hs],
12263 eps,
12264 norm_style,
12265 pool.as_deref(),
12266 );
12267 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12268 }
12269 out
12270 }
12271 };
12272 if prefill_prof_on() {
12273 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
12274 PREFILL_SPLIT[1].fetch_add(
12275 t_ffn.elapsed().as_nanos() as u64,
12276 std::sync::atomic::Ordering::Relaxed,
12277 );
12278 }
12279 if let Some(w) = &lw.ffn_out_norm {
12280 for bi in 0..b {
12281 inference::rms_norm_into(
12282 &ffn[bi * hs..(bi + 1) * hs],
12283 w,
12284 eps,
12285 norm_style,
12286 &mut post[bi * hs..(bi + 1) * hs],
12287 );
12288 }
12289 ffn.copy_from_slice(&post);
12290 }
12291 if let Some(coefs) = mhc_ffn {
12292 let m = self.mhc.as_deref().expect("mhc layer");
12295 let p = self.phys_layer(li);
12296 let n = m.s.streams * hs;
12297 for bi in 0..b {
12298 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12299 m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
12300 if m.leaves(p) {
12301 m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
12302 }
12303 }
12304 } else {
12305 for (dst, &f) in h.iter_mut().zip(&ffn) {
12306 *dst += f;
12307 }
12308 }
12309 if let Some(sc) = lw.layer_scale {
12310 for v in h.iter_mut() {
12311 *v *= sc;
12312 }
12313 }
12314 if self.layer_dump.is_some() {
12316 for bi in 0..b {
12317 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12318 }
12319 }
12320 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12321 if let Ok(t) = tp.parse::<usize>() {
12322 if t >= start_pos && t < start_pos + b {
12323 let bi = t - start_pos;
12324 let row = &h[bi * hs..(bi + 1) * hs];
12325 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12326 eprintln!(
12327 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12328 row[0], row[1]
12329 );
12330 }
12331 }
12332 }
12333 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12337 let row = &h[(b - 1) * hs..b * hs];
12338 let rms =
12339 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12340 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12341 eprintln!(
12342 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12343 match &self.weights.layers[self.phys_layer(li)].attn {
12344 AttnKind::LinearGdn(_) => "gdn",
12345 AttnKind::Linear(_) => "vmf",
12346 AttnKind::ShortConv(_) => "conv",
12347 _ => "attn",
12348 },
12349 match &lw.ffn {
12350 FfnKind::Moe(_) => "moe",
12351 FfnKind::Dense(_) => "dense",
12352 FfnKind::DenseMoe(_) => "dense+moe",
12353 },
12354 );
12355 }
12356 if self.is_loop_end(li) && li + 1 < self.num_layers {
12358 for bi in 0..b {
12359 let normed = inference::rms_norm(
12360 &h[bi * hs..(bi + 1) * hs],
12361 &self.weights.final_norm,
12362 eps,
12363 norm_style,
12364 );
12365 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12366 }
12367 }
12368 if std::env::var("CMF_TRACE_H").is_ok() {
12369 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12370 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12371 eprintln!(
12372 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12373 lw.layer_scale
12374 );
12375 }
12376 }
12377 crate::gpu::set_layer(-1); if prefill_prof_on() {
12379 let ms = |a: &std::sync::atomic::AtomicU64| {
12380 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12381 };
12382 let sp = &attention::ATTN_SPLIT;
12383 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12384 eprintln!(
12385 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12386 host gate {:.1}], host loop {:.1}, attend {:.1} \
12387 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12388 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12389 ms(&PREFILL_SPLIT[0]),
12390 ms(&sp[0]),
12391 qkv_enq,
12392 qkv_wait,
12393 ms(&sp[6]),
12394 ms(&sp[1]),
12395 ms(&sp[2]),
12396 ms(&sp[4]),
12397 ms(&sp[5]),
12398 ca_wait,
12399 ms(&sp[3]),
12400 ms(&PREFILL_SPLIT[1]),
12401 );
12402 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12403 eprintln!(
12404 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12405 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12406 fallbacks (cumulative)",
12407 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12408 );
12409 }
12410 self.o1_progress();
12415 self.swa_trim_tails();
12417 h
12418 }
12419
12420 fn embed_single(&self, id: u32) -> Vec<f32> {
12422 let mut out = vec![0.0f32; self.hidden_size];
12423 if (id as usize) < self.weights.embed_tokens.rows() {
12424 self.weights.embed_tokens.row_f32(id as usize, &mut out);
12425 }
12426 if self.embed_multiplier != 1.0 {
12427 for v in out.iter_mut() {
12428 *v *= self.embed_multiplier;
12429 }
12430 }
12431 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12435 let mut v = vec![0.0f32; self.hidden_size.max(1)];
12436 v[0] = id as f32;
12437 return v;
12438 }
12439 if let Some(b) = &self.g4e {
12443 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12444 }
12445 if let Some(b) = &self.g3n {
12446 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12447 }
12448 out
12449 }
12450
12451 #[cfg(target_os = "macos")]
12457 fn chunk_run_gpu(
12458 &mut self,
12459 li0: usize,
12460 h: &mut [f32],
12461 b: usize,
12462 pos0: usize,
12463 embed_ids: Option<&[u32]>,
12464 cap: usize,
12465 ) -> usize {
12466 let gemma4 =
12473 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12474 if !crate::gpu::enabled_here()
12475 || std::env::var("CMF_GPU_CHUNK")
12476 .map(|v| v == "0")
12477 .unwrap_or(false)
12478 || b < self.chunk_min_rows
12479 || self.prompt_mrope.is_some()
12481 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12484 || (self.global_attn.is_some() && !gemma4)
12485 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12489 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12492 || self.o1_active()
12495 || (self.attn_v_norm && !gemma4)
12496 || !self.metal_rope_scale_ok()
12497 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12498 {
12499 return li0;
12500 }
12501 let Some(model) = self.model.clone() else {
12502 return li0;
12503 };
12504 let (nh, nkv, hd, hs) = (
12505 self.num_heads,
12506 self.num_kv_heads,
12507 self.head_dim,
12508 self.hidden_size,
12509 );
12510 let loop_end = if self.loop_final_norm {
12514 ((li0 / self.physical_layers) + 1) * self.physical_layers
12515 } else {
12516 self.num_layers
12517 };
12518 let run_end = self.num_layers.min(loop_end).min(cap);
12519 let tables: Vec<std::sync::Arc<Vec<f32>>> =
12524 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12525 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12526 let mut stored_at: Vec<usize> = Vec::new();
12527 for li in li0..run_end {
12528 if self.mhc_layer(li) {
12529 break;
12530 }
12531 let lw = &self.weights.layers[self.phys_layer(li)];
12532 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12533 && !gemma4
12534 {
12535 break;
12536 }
12537 let AttnKind::Full {
12538 wq,
12539 wk,
12540 wv,
12541 wo,
12542 q_norm,
12543 k_norm,
12544 output_gate: false,
12545 softplus_gate,
12546 bias,
12547 } = &lw.attn
12548 else {
12549 break;
12550 };
12551 let head_gate = match softplus_gate {
12553 None => None,
12554 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12555 Some((d, r, c)) if r == nh && c == hs => Some(d),
12556 _ => break,
12557 },
12558 Some(_) => break,
12559 };
12560 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12564 FfnKind::Dense(d)
12565 if (matches!(d.act, Act::Silu | Act::Gelu)
12566 || (gemma4 && d.act == Act::GeluTanh))
12567 && d.segs.is_empty() =>
12568 {
12569 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12570 }
12571 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12572 Some((router, experts, q8)) => {
12573 let e0 = &m.experts[0];
12574 let route = Box::new(move |lg: &[f32]| {
12575 let (idx, p, wsum) = moe_route(lg, m, None);
12577 let mut st = m.stats.borrow_mut();
12578 if st.len() < m.experts.len() {
12579 st.resize(m.experts.len(), 0);
12580 }
12581 for &e in &idx {
12582 st[e] += 1;
12583 }
12584 let w = idx.iter().map(|&e| p[e] / wsum).collect();
12585 (idx, w)
12586 });
12587 (
12588 &e0.gate_proj,
12589 &e0.up_proj,
12590 &e0.down_proj,
12591 Act::Silu,
12592 Some(crate::gpu_metal::ChunkMoe {
12593 router,
12594 experts,
12595 q8,
12596 route,
12597 }),
12598 )
12599 }
12600 None => break,
12601 },
12602 _ => break,
12603 };
12604 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12610 t.q8_row_parts()
12611 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12612 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12613 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12614 }
12615 fn cf(t: &QTensor) -> &[f32] {
12616 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12617 }
12618 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12619 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12620 if moe.is_none()
12630 && !gemma4
12631 && ffn_act != Act::Gelu
12632 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12633 {
12634 break;
12635 }
12636 let parts = (
12637 cw(wq),
12638 cw(wk),
12639 cw(wv),
12640 cw(wo),
12641 cw(gate_t),
12642 cw(up_t),
12643 cw(down_t),
12644 );
12645 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12646 else {
12647 break;
12648 };
12649 let layer = &self.kv_cache.layers[li];
12650 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12651 break;
12652 }
12653 debug_assert!(
12655 layer.base() == 0
12656 || self
12657 .layer_window(li)
12658 .is_some_and(|w| layer.head_len(0) + 1 >= w)
12659 );
12660 stored_at.push(layer.head_len(0));
12661 let (nkv, hd) = if gemma4 {
12664 let (a, bb, _) = self.layer_geom(li);
12665 (a, bb)
12666 } else {
12667 (nkv, hd)
12668 };
12669 layers.push(crate::gpu_metal::ChunkLayer {
12670 model: &model,
12671 kv_id: self.graph_kv_id,
12672 layer: li,
12673 wq: pq,
12674 wk: pk,
12675 wv: pv,
12676 wo: po,
12677 gate: pg,
12678 up: pu,
12679 down: pd,
12680 input_norm: &lw.input_norm,
12681 post_norm: &lw.post_norm,
12682 bias: bias
12683 .as_ref()
12684 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12685 q_norm: q_norm.as_deref(),
12686 k_norm: k_norm.as_deref(),
12687 inv_freq: &tables[li - li0],
12688 rope_scale: self.layer_rope_scale(li),
12689 rd: self.layer_geom(li).2,
12690 nh,
12691 nkv,
12692 hd,
12693 hs,
12694 inter: gate_t.rows(),
12695 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12696 late_qk_norm: self.qk_norm_after_rope,
12697 eps: self.rms_eps as f32,
12698 window: self.layer_window(li),
12699 head_gate,
12700 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12701 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12702 v_norm: self.attn_v_norm,
12703 scale: self.attn_scale,
12704 attn_out_norm: lw.attn_out_norm.as_deref(),
12705 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12706 layer_scale: lw.layer_scale,
12707 }),
12708 col,
12709 ffn_col: if moe.is_none() && !gemma4 {
12712 ffn_col
12713 } else {
12714 [&[][..]; 3]
12715 },
12716 moe,
12717 });
12718 }
12719 if layers.is_empty() {
12720 return li0;
12721 }
12722 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12724 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12725 .iter()
12726 .zip(&rows_kv)
12727 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12728 .collect();
12729 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12730 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12731 let li = layers[i].layer;
12732 let layer = &self.kv_cache.layers[li];
12733 let nkv = rows_kv[i].0;
12734 io.push(crate::gpu_metal::ChunkIo {
12735 cpu_stored: stored_at[i],
12736 cpu_gen: layer.generation(),
12737 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12738 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12739 out_k: ok,
12740 out_v: ov,
12741 imp: oi,
12742 });
12743 }
12744 let n_run = layers.len();
12745 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12746 let ep = embed_ids.and_then(|ids| {
12749 self.weights
12750 .embed_tokens
12751 .q8_row_parts()
12752 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12753 idx,
12754 rows,
12755 row_scale: rs,
12756 ids,
12757 mult: self.embed_multiplier,
12758 })
12759 });
12760 if embed_ids.is_some() && ep.is_none() {
12761 return li0;
12762 }
12763 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12764 return li0;
12765 }
12766 drop(io);
12767 drop(layers);
12768 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12771 let li = li0 + i;
12772 let row = rows_kv[i].1;
12773 let layer = &mut self.kv_cache.layers[li];
12774 for bi in 0..b {
12775 layer.append(
12776 &ok[bi * row..(bi + 1) * row],
12777 &ov[bi * row..(bi + 1) * row],
12778 &[],
12779 );
12780 }
12781 layer.accumulate_imp(oi);
12782 }
12783 last
12784 }
12785
12786 fn layer_is_local(&self, li: usize) -> bool {
12789 if let Some(layers) = &self.sliding_layers {
12790 return layers.get(li).copied().unwrap_or(false);
12791 }
12792 match self.swa {
12793 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12794 None => false,
12795 }
12796 }
12797
12798 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12801 if self.layer_is_local(li) {
12802 if let Some(f) = &self.inv_freq_local {
12803 return f.clone();
12804 }
12805 } else if let Some(f) = &self.inv_freq_global {
12806 return f.clone();
12807 }
12808 self.inv_freq.clone()
12809 }
12810
12811 fn layer_window(&self, li: usize) -> Option<usize> {
12813 self.swa
12814 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12815 }
12816
12817 fn swa_trim_tails(&mut self) {
12826 let Some((slack, align)) = self.swa_trim else {
12827 return;
12828 };
12829 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12830 return;
12831 }
12832 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12833 if let Some(w) = self.layer_window(li) {
12834 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12835 }
12836 }
12837 }
12838
12839 fn layer_num_heads(&self, li: usize) -> usize {
12840 self.attention_heads_per_layer
12841 .as_ref()
12842 .and_then(|v| v.get(li).copied())
12843 .unwrap_or(self.num_heads)
12844 }
12845
12846 fn layer_rope_scale(&self, li: usize) -> f32 {
12847 if self.layer_is_local(li) {
12848 self.rope_scale_local
12849 } else {
12850 self.rope_scale
12851 }
12852 }
12853
12854 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12857 if !self.layer_is_local(li) {
12858 if let Some((ghd, gkv)) = self.global_attn {
12859 return (gkv, ghd, ghd);
12860 }
12861 }
12862 let rd = if self.layer_is_local(li) {
12863 self.rotary_dim_local.unwrap_or(self.rotary_dim)
12864 } else {
12865 self.rotary_dim
12866 };
12867 let rd = if self
12869 .no_rope_layers
12870 .as_ref()
12871 .and_then(|v| v.get(li))
12872 .copied()
12873 == Some(true)
12874 {
12875 0
12876 } else {
12877 rd
12878 };
12879 (self.layer_num_kv_heads(li), self.head_dim, rd)
12880 }
12881
12882 fn layer_num_kv_heads(&self, li: usize) -> usize {
12885 self.kv_heads_per_layer
12886 .as_ref()
12887 .and_then(|v| v.get(self.phys_layer(li)).copied())
12888 .unwrap_or(self.num_kv_heads)
12889 }
12890
12891 fn layer_v_dim(&self, li: usize) -> usize {
12893 let (_, hd, _) = self.layer_geom(li);
12894 self.v_head_dim.unwrap_or(hd).min(hd)
12895 }
12896
12897 pub fn set_attn_geometry(
12906 &mut self,
12907 kv_heads_per_layer: Option<Vec<usize>>,
12908 v_head_dim: Option<usize>,
12909 ) -> Result<(), String> {
12910 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12911 if self.global_attn.is_some() {
12912 return Err(
12913 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12914 attention geometry"
12915 .into(),
12916 );
12917 }
12918 if self
12919 .weights
12920 .layers
12921 .iter()
12922 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12923 {
12924 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12925 }
12926 }
12927 if let Some(vd) = v_head_dim {
12928 if vd == 0 || vd > self.head_dim {
12929 return Err(format!(
12930 "v_head_dim {vd} must be in 1..={} (head_dim)",
12931 self.head_dim
12932 ));
12933 }
12934 }
12935 if let Some(v) = &kv_heads_per_layer {
12936 if v.len() != self.physical_layers {
12937 return Err(format!(
12938 "kv_heads_per_layer has {} entries, expected {} layers",
12939 v.len(),
12940 self.physical_layers
12941 ));
12942 }
12943 for (li, &nkv) in v.iter().enumerate() {
12944 let is_attn = matches!(
12945 self.weights.layers.get(li).map(|lw| &lw.attn),
12946 Some(AttnKind::Full { .. }) | None
12947 );
12948 if !is_attn {
12949 continue;
12950 }
12951 let nh = self
12952 .attention_heads_per_layer
12953 .as_ref()
12954 .and_then(|h| h.get(li).copied())
12955 .unwrap_or(self.num_heads);
12956 if nkv == 0 || nh % nkv != 0 {
12957 return Err(format!(
12958 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12959 ));
12960 }
12961 }
12962 }
12963 self.kv_heads_per_layer = kv_heads_per_layer;
12964 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12965 if self.kv_heads_per_layer.is_some() {
12966 for li in 0..self.kv_cache.layers.len() {
12967 let full = matches!(
12968 self.weights
12969 .layers
12970 .get(self.phys_layer(li))
12971 .map(|lw| &lw.attn),
12972 Some(AttnKind::Full { .. })
12973 );
12974 let nkv = self.layer_num_kv_heads(li);
12975 let cache = &self.kv_cache.layers[li];
12976 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12977 let sinks = cache.sinks.clone();
12978 self.kv_cache.layers[li] =
12979 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12980 self.kv_cache.layers[li].sinks = sinks;
12981 }
12982 }
12983 }
12984 Ok(())
12985 }
12986
12987 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12991 let Some(lw) = self.weights.layers.get(phys) else {
12992 return Err(format!("sinks for layer {phys}: no such layer"));
12993 };
12994 if !matches!(lw.attn, AttnKind::Full { .. }) {
12995 return Err(format!(
12996 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12997 ));
12998 }
12999 let nh = self
13000 .attention_heads_per_layer
13001 .as_ref()
13002 .and_then(|h| h.get(phys).copied())
13003 .unwrap_or(self.num_heads);
13004 if sinks.len() != nh {
13005 return Err(format!(
13006 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
13007 sinks.len()
13008 ));
13009 }
13010 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
13011 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
13012 }
13013 for li in 0..self.kv_cache.layers.len() {
13014 if self.phys_layer(li) == phys {
13015 self.kv_cache.layers[li].sinks = Some(sinks.clone());
13016 }
13017 }
13018 Ok(())
13019 }
13020
13021 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
13031 if self.kv_heads_per_layer.is_some() {
13032 return Some("per-layer KV head counts");
13033 }
13034 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
13035 return Some("V heads narrower than Q/K heads");
13036 }
13037 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
13038 return Some("learned attention sinks");
13039 }
13040 if self.swa.is_some() || self.sliding_layers.is_some() {
13041 return Some("sliding-window layers");
13042 }
13043 None
13044 }
13045
13046 #[cfg(target_os = "macos")]
13054 fn metal_rope_scale_ok(&self) -> bool {
13055 !(self.qk_norm_after_rope
13056 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
13057 }
13058
13059 #[cfg(target_os = "macos")]
13076 fn metal_graph_swa(&self) -> bool {
13077 (self.swa.is_some() || self.sliding_layers.is_some())
13078 && self.attention_heads_per_layer.is_none()
13079 && !self.attn_v_norm
13080 && self.attn_softcap == 0.0
13081 && self.kv_heads_per_layer.is_none()
13082 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
13083 && self.global_attn.is_none()
13084 && self.inv_freq_global.is_none()
13085 && self.metal_rope_scale_ok()
13086 && !(0..self.num_layers).any(|li| {
13087 self.layer_is_local(li)
13088 && self.inv_freq_local.is_none()
13089 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13090 })
13091 }
13092
13093 #[cfg(target_os = "macos")]
13101 fn metal_gemma4(&self) -> bool {
13102 crate::gpu_metal::gemma4::enabled()
13103 && (self.global_attn.is_some() || self.attn_v_norm)
13104 && self.attention_heads_per_layer.is_none()
13105 && self.kv_heads_per_layer.is_none()
13106 && self.v_head_dim.is_none()
13107 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
13108 && self.attn_softcap == 0.0
13109 && !self.qk_norm_after_rope
13110 && self.metal_rope_scale_ok()
13111 && self
13112 .weights
13113 .layers
13114 .iter()
13115 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
13116 && !(0..self.num_layers).any(|li| {
13117 self.layer_is_local(li)
13118 && self.inv_freq_local.is_none()
13119 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13120 })
13121 }
13122
13123 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
13130 let geometry_reason = self.graph_attn_decline_reason();
13134 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
13135 if has_scaled_rope && geometry_reason.is_none() {
13136 return Some("scaled RoPE without per-layer geometry");
13137 }
13138 if has_scaled_rope && self.qk_norm_after_rope {
13143 return Some("scaled RoPE with post-RoPE Q/K norm");
13144 }
13145 geometry_reason?;
13146 if self.attention_heads_per_layer.is_some() {
13152 return Some("per-layer Q head counts with per-layer geometry");
13153 }
13154 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
13159 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
13160 if !gemma4
13161 && self.weights.layers.iter().any(|lw| {
13162 lw.layer_scale.is_some()
13163 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
13164 })
13165 {
13166 return Some("sandwich norms / layer scale with per-layer geometry");
13167 }
13168 if self.weights.layers.iter().any(|lw| {
13172 matches!(
13173 &lw.attn,
13174 AttnKind::Full {
13175 output_gate: true,
13176 ..
13177 }
13178 )
13179 }) && self.v_head_dim.is_some()
13180 {
13181 return Some("gated attention with V narrower than K");
13182 }
13183 if (0..self.num_layers).any(|li| {
13184 self.layer_is_local(li)
13185 && self.inv_freq_local.is_none()
13186 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13187 }) {
13188 return Some("local rotary width without a local RoPE table");
13189 }
13190 None
13191 }
13192
13193 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
13197 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
13200 return None;
13201 }
13202 if self.global_attn.is_some() {
13203 return Some("per-layer head width (Gemma-4 global layers)");
13204 }
13205 if self.attn_v_norm {
13206 return Some("V norm");
13207 }
13208 if self.weights.layers.iter().any(|lw| {
13209 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
13210 }) {
13211 return Some("sandwich norms / layer scale");
13212 }
13213 None
13214 }
13215
13216 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
13221 self.graph_attn_decline_reason()?;
13222 let (nkv, hd, rd) = self.layer_geom(li);
13223 let invf: &[f32] = if self.layer_is_local(li) {
13224 match &self.inv_freq_local {
13225 Some(f) => f.as_slice(),
13226 None => self.inv_freq.as_slice(),
13227 }
13228 } else {
13229 match &self.inv_freq_global {
13230 Some(f) => f.as_slice(),
13231 None => self.inv_freq.as_slice(),
13232 }
13233 };
13234 Some(crate::gpu::GraphAttnGeom {
13235 nkv,
13236 hd,
13237 dv: self.layer_v_dim(li),
13238 rd,
13239 invf,
13240 rope_scale: self.layer_rope_scale(li),
13241 window: self.layer_window(li),
13242 sink: self.kv_cache.layers[li].sinks.as_deref(),
13243 v_norm: self.attn_v_norm,
13244 })
13245 }
13246
13247 #[cfg(feature = "gpu")]
13259 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
13260 if !self.proj_gate_sigmoid
13261 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
13262 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
13263 || self.graph_refused()
13264 || self.o1_active()
13265 || self.attn_softcap > 0.0
13266 || self.wgpu_graph_attn_decline().is_some()
13267 {
13268 return None;
13269 }
13270 let g = self.graph_attn_geom(li)?;
13271 let (nkv, hd, _) = self.layer_geom(li);
13272 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
13274 return None;
13275 }
13276 Some(crate::gpu::PrefillMirror {
13277 kv_id: self.graph_kv_id,
13278 layer: li,
13279 limit: self.kv_cache.max_seq_len,
13280 })
13281 }
13282
13283 #[cfg(feature = "gpu")]
13292 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
13293 let kv_id = self.graph_kv_id;
13294 for li in from..upto.min(self.num_layers) {
13295 if !matches!(
13296 self.weights.layers[self.phys_layer(li)].attn,
13297 AttnKind::Full { .. }
13298 ) {
13299 continue;
13300 }
13301 let host = self.kv_cache.layers[li].pos_len();
13303 if host >= position {
13304 continue;
13305 }
13306 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
13307 continue;
13308 };
13309 let to = dev.min(position);
13310 if to <= host {
13311 continue;
13312 }
13313 let (nkv, hd) = {
13314 let c = &self.kv_cache.layers[li];
13315 (c.num_kv_heads, c.head_dim)
13316 };
13317 let Some((k, v, first_valid)) =
13318 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13319 else {
13320 continue;
13321 };
13322 let need_from = match self.layer_window(li) {
13325 Some(w) => host.max((position + 1).saturating_sub(w)),
13326 None => host,
13327 };
13328 if first_valid > need_from {
13329 tracing::warn!(
13330 "layer {li}: device KV rows {host}..{to} no longer resident \
13331 (from {first_valid}); host attention will miss them"
13332 );
13333 }
13334 let row = nkv * hd;
13335 let cache = &mut self.kv_cache.layers[li];
13336 for p in 0..to - host {
13337 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13338 }
13339 }
13340 }
13341
13342 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13345 let mut seen = self.graph_declines.borrow_mut();
13346 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13347 tracing::warn!("{site} declined: {reason} (CPU attention path)");
13348 seen.push((site, reason));
13349 }
13350 }
13351
13352 pub fn graph_declines(&self) -> Vec<String> {
13355 self.graph_declines
13356 .borrow()
13357 .iter()
13358 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13359 .collect()
13360 }
13361
13362 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13371 let Some(dir) = &self.layer_dump else {
13372 return;
13373 };
13374 let mut bytes = Vec::with_capacity(row.len() * 4);
13375 for v in row {
13376 bytes.extend_from_slice(&v.to_le_bytes());
13377 }
13378 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13379 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13380 use std::sync::atomic::{AtomicBool, Ordering};
13381 static SAID: AtomicBool = AtomicBool::new(false);
13382 if !SAID.swap(true, Ordering::Relaxed) {
13383 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13384 }
13385 }
13386 }
13387
13388 fn mimo_moe_prepare(&mut self) {
13391 if !self.mimo_moe.is_undecided() {
13392 return;
13393 }
13394 let slot = {
13395 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13396 .filter_map(
13397 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13398 FfnKind::Moe(m) => Some((li, m)),
13399 _ => None,
13400 },
13401 )
13402 .collect();
13403 if layers.is_empty()
13406 || self.physical_layers != self.num_layers
13407 || self.gpu_plan.is_some()
13408 {
13409 crate::mimo_moe::Slot::Off
13410 } else {
13411 let graph_prefix =
13415 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13416 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13417 }
13418 };
13419 self.mimo_moe = slot;
13420 }
13421
13422 #[cfg(test)]
13423 pub(crate) fn test_graph_kv_id(&self) -> u64 {
13424 self.graph_kv_id
13425 }
13426
13427 pub(crate) fn mimo_graph_layer_rows(
13431 &mut self,
13432 li: usize,
13433 h: &mut [f32],
13434 positions: &[usize],
13435 ) -> crate::gpu::BatchGraphOutcome {
13436 use crate::gpu::BatchGraphOutcome as Out;
13437 let b = positions.len();
13438 if !(1..=4).contains(&b)
13439 || h.len() != b * self.hidden_size
13440 || !self.mimo_moe.is_dynamic(li, true)
13441 || !crate::gpu::enabled_here()
13442 || !crate::gpu::wgpu_active()
13443 || self.o1_active()
13444 || self.physical_layers != self.num_layers
13445 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13449 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13450 || self.wgpu_graph_attn_decline().is_some()
13451 {
13452 return Out::Declined;
13453 }
13454 let attn_started = std::time::Instant::now();
13455 let outcome = {
13456 let lw = &self.weights.layers[li];
13457 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13458 return Out::Declined;
13459 }
13460 let FfnKind::Moe(m) = &lw.ffn else {
13461 return Out::Declined;
13462 };
13463 let AttnKind::Full {
13464 wq,
13465 wk,
13466 wv,
13467 wo,
13468 q_norm,
13469 k_norm,
13470 output_gate,
13471 softplus_gate,
13472 bias,
13473 } = &lw.attn
13474 else {
13475 return Out::Declined;
13476 };
13477 if *output_gate || softplus_gate.is_some() {
13478 return Out::Declined;
13479 }
13480 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13481 m.experts
13482 .first()?
13483 .gate_proj
13484 .mapped_q4tp()
13485 .map(|(m, _)| m.clone())
13486 }) else {
13487 return Out::Declined;
13488 };
13489 fn gw<'a>(
13490 t: &'a QTensor,
13491 owner: &std::sync::Arc<cortiq_core::CmfModel>,
13492 ) -> Option<crate::gpu::GraphW<'a>> {
13493 if let Some((m, idx, kind, rs)) = t.graph_weight() {
13494 if m.uid() != owner.uid() || t.has_prism_contract() {
13495 return None;
13496 }
13497 return Some(crate::gpu::GraphW {
13498 idx,
13499 kind,
13500 row_scale: rs,
13501 data: &[],
13502 prism: crate::gpu::GraphPrismOp::None,
13503 affine: false,
13504 });
13505 }
13506 t.as_f32().map(|data| crate::gpu::GraphW {
13507 idx: 0,
13508 kind: 4,
13509 row_scale: &[],
13510 data,
13511 prism: crate::gpu::GraphPrismOp::None,
13512 affine: false,
13513 })
13514 }
13515 let (Some(q), Some(k), Some(v), Some(o)) = (
13516 gw(wq, &model),
13517 gw(wk, &model),
13518 gw(wv, &model),
13519 gw(wo, &model),
13520 ) else {
13521 return Out::Declined;
13522 };
13523 let layer = crate::gpu::GraphLayer {
13524 mhc: None,
13525 input_norm: &lw.input_norm,
13526 post_norm: &lw.post_norm,
13527 attn_out_norm: None,
13528 ffn_out_norm: None,
13529 layer_scale: None,
13530 ffn: crate::gpu::GraphFfn::AttentionOnly,
13531 attn: crate::gpu::GraphAttn::Full {
13532 wq: q,
13533 wk: k,
13534 wv: v,
13535 wo: o,
13536 q_norm: q_norm.as_deref(),
13537 k_norm: k_norm.as_deref(),
13538 late_qk_norm: self.qk_norm_after_rope,
13539 bias: bias
13540 .as_ref()
13541 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13542 output_gate: false,
13543 cpu_k: self.kv_cache.layers[li].k_heads(),
13544 cpu_v: self.kv_cache.layers[li].v_heads(),
13545 cpu_base: self.kv_cache.layers[li].base(),
13546 geom: self.graph_attn_geom(li),
13547 head_gate: None,
13548 },
13549 };
13550 let (nkv, hd, rd) = self.layer_geom(li);
13551 crate::gpu::forward_batch_graph_at(
13552 &model,
13553 self.graph_kv_id,
13554 li,
13555 &[layer],
13556 &self.inv_freq,
13557 h,
13558 self.layer_num_heads(li),
13559 nkv,
13560 hd,
13561 rd,
13562 self.hidden_size,
13563 1,
13564 positions,
13565 self.kv_cache.max_seq_len,
13566 self.norm_style == cortiq_core::NormStyle::Gemma,
13567 self.rms_eps as f32,
13568 self.attn_scale,
13569 b,
13570 &[],
13571 self.o1_epoch,
13572 None,
13573 None,
13574 )
13575 };
13576 match outcome {
13577 Out::Completed => {}
13578 Out::Declined => return Out::Declined,
13579 Out::Failed => {
13580 self.graph_failed
13581 .store(true, std::sync::atomic::Ordering::Relaxed);
13582 return Out::Failed;
13583 }
13584 }
13585 let attn_ns = attn_started.elapsed().as_nanos() as u64;
13586 let hs = self.hidden_size;
13587 let lw = &self.weights.layers[li];
13588 let FfnKind::Moe(m) = &lw.ffn else {
13589 unreachable!()
13590 };
13591 let mut post = vec![0.0; h.len()];
13592 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13593 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13594 }
13595 let mut ffn = if b == 1 {
13596 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13597 } else {
13598 moe_ffn_banked_rows(
13599 &mut self.mimo_moe,
13600 li,
13601 m,
13602 &post,
13603 b,
13604 hs,
13605 self.pool.as_deref(),
13606 )
13607 };
13608 for (x, &f) in h.iter_mut().zip(&ffn) {
13609 *x += f;
13610 }
13611 attention::recycle_buf(&mut ffn);
13612 if self.layer_dump.is_some() {
13613 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13614 self.dump_layer_row(pos, li, row);
13615 }
13616 }
13617 crate::mimo_moe::note_attention_graph(b, attn_ns);
13618 Out::Completed
13619 }
13620
13621 fn layer_attn_plain(&self, li: usize) -> bool {
13622 self.kv_heads_per_layer.is_none()
13623 && self.v_head_dim.is_none()
13624 && self.global_attn.is_none()
13625 && self.layer_window(li).is_none()
13626 && self.kv_cache.layers[li].sinks.is_none()
13627 }
13628
13629 fn forward_layers(
13631 &mut self,
13632 hidden: &[f32],
13633 position: usize,
13634 task_mask: Option<&TaskMask>,
13635 ) -> Vec<f32> {
13636 let out = self.forward_layers_upto(hidden, position, task_mask, None);
13637 self.o1_progress();
13638 self.swa_trim_tails();
13639 out
13640 }
13641
13642 pub fn embed_id(&self, id: u32) -> Vec<f32> {
13650 self.embed_single(id)
13651 }
13652
13653 pub fn split_supported(&self) -> Result<(), String> {
13657 if self.dsv4.is_some() {
13658 return Err(
13659 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13660 );
13661 }
13662 if self.dsv41.is_some() {
13663 return Err(
13664 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13665 .into(),
13666 );
13667 }
13668 if self.qwen4_exp.is_some() {
13669 return Err(
13670 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13671 );
13672 }
13673 if self.per_token_stack() {
13674 return Err(
13675 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13676 );
13677 }
13678 Ok(())
13679 }
13680
13681 pub fn forward_span(
13686 &mut self,
13687 hidden: &[f32],
13688 position: usize,
13689 from: usize,
13690 upto: usize,
13691 task_mask: Option<&TaskMask>,
13692 ) -> Result<Vec<f32>, String> {
13693 self.split_supported()?;
13694 if from > upto || upto >= self.num_layers {
13695 return Err(format!(
13696 "forward_span: layer range {from}..={upto} outside 0..{}",
13697 self.num_layers
13698 ));
13699 }
13700 if hidden.len() != self.hidden_size {
13701 return Err(format!(
13702 "forward_span: hidden len {} ≠ hidden_size {}",
13703 hidden.len(),
13704 self.hidden_size
13705 ));
13706 }
13707 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13708 self.o1_progress();
13709 self.swa_trim_tails();
13710 if self
13711 .graph_failed
13712 .swap(false, std::sync::atomic::Ordering::Relaxed)
13713 {
13714 self.cancel
13715 .store(false, std::sync::atomic::Ordering::Relaxed);
13716 self.clear_sequence_state();
13717 return Err("forward_span: deferred O(1) transition failed".into());
13718 }
13719 Ok(out)
13720 }
13721
13722 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13725 let normed = inference::rms_norm(
13726 hidden,
13727 &self.weights.final_norm,
13728 self.rms_eps,
13729 self.norm_style,
13730 );
13731 self.lm_head_forward(&normed)
13732 }
13733
13734 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13736 sampler::sample_with_scratch(
13737 logits,
13738 &self.sampler_config,
13739 past_tokens,
13740 &mut self.rng,
13741 &mut self.sampler_scratch,
13742 )
13743 }
13744
13745 pub fn reset_session(&mut self) {
13747 self.clear_sequence_state();
13748 }
13749
13750 pub fn prefill_span_ids(
13756 &mut self,
13757 ids: &[u32],
13758 start_pos: usize,
13759 upto: usize,
13760 task_mask: Option<&TaskMask>,
13761 ) -> Result<Vec<f32>, String> {
13762 self.split_supported()?;
13763 if upto >= self.num_layers {
13764 return Err(format!(
13765 "prefill_span_ids: upto {upto} outside 0..{}",
13766 self.num_layers
13767 ));
13768 }
13769 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13773 let out =
13774 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13775 self.check_o1_progress_failure("prefill_span_ids")?;
13776 Ok(out)
13777 } else {
13778 let hs = self.hidden_size;
13779 let mut out = Vec::with_capacity(ids.len() * hs);
13780 for (i, &id) in ids.iter().enumerate() {
13781 let emb = self.embed_id(id);
13782 out.extend_from_slice(&self.forward_span(
13783 &emb,
13784 start_pos + i,
13785 0,
13786 upto,
13787 task_mask,
13788 )?);
13789 }
13790 Ok(out)
13791 }
13792 }
13793
13794 pub fn prefill_span_hidden(
13797 &mut self,
13798 hidden: &[f32],
13799 start_pos: usize,
13800 from: usize,
13801 upto: usize,
13802 task_mask: Option<&TaskMask>,
13803 ) -> Result<Vec<f32>, String> {
13804 self.split_supported()?;
13805 let hs = self.hidden_size;
13806 if hidden.is_empty() || hidden.len() % hs != 0 {
13807 return Err(format!(
13808 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13809 hidden.len()
13810 ));
13811 }
13812 if from > upto || upto >= self.num_layers {
13813 return Err(format!(
13814 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13815 self.num_layers
13816 ));
13817 }
13818 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13819 let out = self.prefill_batch_span(
13820 PrefillIn::Hidden(hidden),
13821 start_pos,
13822 task_mask,
13823 from,
13824 upto + 1,
13825 );
13826 self.check_o1_progress_failure("prefill_span_hidden")?;
13827 Ok(out)
13828 } else {
13829 let b = hidden.len() / hs;
13830 let mut out = Vec::with_capacity(hidden.len());
13831 for i in 0..b {
13832 let h = self.forward_span(
13833 &hidden[i * hs..(i + 1) * hs],
13834 start_pos + i,
13835 from,
13836 upto,
13837 task_mask,
13838 )?;
13839 out.extend_from_slice(&h);
13840 }
13841 Ok(out)
13842 }
13843 }
13844
13845 fn try_token_graph_wgpu(
13849 &self,
13850 hidden: &[f32],
13851 position: usize,
13852 logits_out: &mut Vec<f32>,
13853 layers_run: &mut usize,
13854 ) -> Option<Result<Vec<f32>, ()>> {
13855 self.try_token_graph_wgpu_steps(
13856 hidden,
13857 position,
13858 logits_out,
13859 1,
13860 None,
13861 Some(layers_run),
13862 0,
13863 self.num_layers,
13864 )
13865 }
13866
13867 fn try_token_graph_wgpu_span(
13871 &self,
13872 hidden: &[f32],
13873 position: usize,
13874 logits_out: &mut Vec<f32>,
13875 from: usize,
13876 upto_excl: usize,
13877 layers_run: &mut usize,
13878 ) -> Option<Result<Vec<f32>, ()>> {
13879 self.try_token_graph_wgpu_steps(
13880 hidden,
13881 position,
13882 logits_out,
13883 1,
13884 None,
13885 Some(layers_run),
13886 from,
13887 upto_excl,
13888 )
13889 }
13890
13891 #[cfg(feature = "gpu")]
13895 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13896 if !crate::minicpm_tail::minimax_pool_on()
13897 || !crate::gpu::enabled_here()
13898 || self.head_clusters.is_some()
13899 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13900 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13901 {
13902 return None;
13903 }
13904 let (model, i, kind, rs) = self
13905 .weights
13906 .lm_head
13907 .graph_weight()
13908 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13909 let name = &model.tensors[i].name;
13910 let prism = if crate::prism::is_inverse_embedding(&model, name) {
13911 crate::gpu::GraphPrismOp::InverseEmbedding
13912 } else if crate::prism::is_forward_weight(&model, name) {
13913 crate::gpu::GraphPrismOp::Forward
13914 } else {
13915 crate::gpu::GraphPrismOp::None
13916 };
13917 let gw = crate::gpu::GraphW {
13918 idx: i,
13919 kind,
13920 row_scale: rs,
13921 data: &[],
13922 prism,
13923 affine: crate::prism::is_affine_target(&model, name),
13924 };
13925 let rows = self.weights.lm_head.rows();
13926 let mut h = hidden.to_vec();
13927 let mut lg = Vec::new();
13928 let mut layers_run = 0usize;
13929 let outcome = crate::gpu::forward_token_graph(
13930 &model,
13931 self.graph_kv_id,
13932 &[],
13933 &[],
13934 self.o1_epoch,
13935 &self.inv_freq,
13936 &mut h,
13937 self.num_heads,
13938 self.num_kv_heads,
13939 self.head_dim,
13940 self.attn_scale,
13941 self.head_dim,
13942 self.hidden_size,
13943 self.intermediate_size,
13944 0,
13945 self.kv_cache.max_seq_len,
13946 self.norm_style == cortiq_core::NormStyle::Gemma,
13947 self.rms_eps as f32,
13948 Some((&gw, rows)),
13949 &self.weights.final_norm,
13950 &mut lg,
13951 &[],
13952 1,
13953 None,
13954 None,
13955 Some(&mut layers_run),
13956 0,
13957 false,
13958 );
13959 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13960 return None;
13961 }
13962 lg.resize(self.vocab_size, 0.0);
13965 if let Some(m) = self.logit_multiplier {
13966 for l in lg.iter_mut() {
13967 *l *= m;
13968 }
13969 }
13970 if let Some(c) = self.final_softcap {
13971 softcap_in_place(&mut lg, c, self.pool.as_deref());
13972 }
13973 Some(lg)
13974 }
13975
13976 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13979 if winners.is_empty() {
13980 return;
13981 }
13982 if self.m3_n_exp == 0 {
13983 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13984 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13985 }
13986 for &e in winners {
13987 let e = e as usize;
13988 if e < self.m3_n_exp && li < self.num_layers {
13989 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13990 }
13991 }
13992 if li + 1 == self.num_layers {
13993 self.m3_tokens += 1;
13994 if self.m3_tokens % 16 == 0 {
13995 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13996 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13997 let _ = std::fs::write(&path, bytes);
13998 }
13999 }
14000 }
14001 }
14002
14003 fn m3_prewarm(&mut self) {
14008 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
14009 return;
14010 };
14011 let Ok(bytes) = std::fs::read(&path) else {
14012 tracing::warn!("m3 prewarm: cannot read profile {path}");
14013 return;
14014 };
14015 let counts: Vec<u32> = bytes
14016 .chunks_exact(4)
14017 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
14018 .collect();
14019 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
14020 if counts.len() != self.num_layers * n_exp {
14021 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
14022 return;
14023 }
14024 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
14025 .ok()
14026 .and_then(|v| v.parse().ok())
14027 .unwrap_or(300);
14028 let mut ranked: Vec<(u32, usize, usize)> = counts
14029 .iter()
14030 .enumerate()
14031 .filter(|(_, c)| **c > 0)
14032 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
14033 .collect();
14034 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
14035 ranked.truncate(limit);
14036 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
14037 for (_, li, e) in ranked {
14038 per_layer[li].push(e);
14039 }
14040 let mut admitted = 0usize;
14041 for (li, ids) in per_layer.iter().enumerate() {
14042 if ids.is_empty() {
14043 continue;
14044 }
14045 let lw = &self.weights.layers[self.phys_layer(li)];
14046 let FfnKind::Moe(m) = &lw.ffn else {
14047 continue;
14048 };
14049 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
14051 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
14052 admitted += ids.len();
14053 }
14054 }
14055 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
14056 }
14057
14058 #[cfg(feature = "gpu")]
14069 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
14070 if self.o1_active() || self.attn_softcap > 0.0 {
14071 return Ok(false);
14072 }
14073 if !self.m3_prewarmed {
14074 self.m3_prewarmed = true;
14075 self.m3_prewarm();
14076 }
14077 let frame = {
14078 let lw = &self.weights.layers[self.phys_layer(li)];
14079 let FfnKind::Moe(m) = &lw.ffn else {
14080 return Ok(false);
14081 };
14082 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
14083 return Ok(false);
14084 }
14085 let t_arm = std::time::Instant::now();
14086 if self.m3_winners.len() != self.num_layers {
14087 self.m3_winners = vec![Vec::new(); self.num_layers];
14088 }
14089 let pred = std::mem::take(&mut self.m3_winners[li]);
14090 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
14091 if armed.is_none() {
14092 self.m3_winners[li] = pred; }
14094 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14095 match armed {
14096 Some(frame) => frame,
14097 None => return Ok(false),
14100 }
14101 };
14102 crate::gpu_wgpu::pooled_frame_enter(frame);
14103 let want_logits = self.graph_want_logits;
14107 self.graph_want_logits = false;
14108 let mut lg = Vec::new();
14109 let mut gl = 0usize;
14110 let t_span = std::time::Instant::now();
14111 let span = crate::gpu::gpu_scope(|| {
14112 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
14113 });
14114 self.graph_want_logits = want_logits;
14115 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14116 let hh = match span {
14117 Some(Ok(hh)) => hh,
14118 Some(Err(())) => return Err(()),
14119 None => {
14120 return Ok(false);
14123 }
14124 };
14125 if gl != 1 {
14126 return if gl == 0 { Ok(false) } else { Err(()) };
14129 }
14130 *h = hh;
14131 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
14132 self.m3_note_route(li, &winners);
14133 if self.m3_winners.len() != self.num_layers {
14134 self.m3_winners = vec![Vec::new(); self.num_layers];
14135 }
14136 self.m3_winners[li] = winners;
14137 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
14138 cold.retain(|&(_, w)| {
14141 if w.is_finite() {
14142 true
14143 } else {
14144 tracing::error!("pooled tail: non-finite cold weight from expert route");
14145 false
14146 }
14147 });
14148 if cold.is_empty() {
14149 return Ok(true);
14150 }
14151 let x = crate::gpu_wgpu::pooled_frame_take_input();
14152 if x.len() != self.hidden_size {
14153 return Err(());
14154 }
14155 let lw = &self.weights.layers[self.phys_layer(li)];
14161 let FfnKind::Moe(m) = &lw.ffn else {
14162 return Err(());
14163 };
14164 let t_cold = std::time::Instant::now();
14165 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
14166 let mut out = crate::attention::take_buf(self.hidden_size);
14167 let on_device = !force_host
14168 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
14169 if !on_device {
14170 crate::attention::recycle_buf(&mut out);
14171 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
14172 .iter()
14173 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
14174 .collect::<Result<_, ()>>()?;
14175 out = crate::gpu::cpu_scope(|| {
14176 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
14177 });
14178 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
14179 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
14180 }
14181 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14182 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14183 if out.len() != h.len() {
14184 crate::attention::recycle_buf(&mut out);
14185 return Err(());
14186 }
14187 for (dst, src) in h.iter_mut().zip(out.iter()) {
14188 *dst += src;
14189 }
14190 crate::attention::recycle_buf(&mut out);
14191 if on_device {
14192 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14193 }
14194 Ok(true)
14195 }
14196
14197 #[cfg(not(feature = "gpu"))]
14198 fn m3_pooled_layer(
14199 &mut self,
14200 _li: usize,
14201 _position: usize,
14202 _h: &mut Vec<f32>,
14203 ) -> Result<bool, ()> {
14204 Ok(false)
14205 }
14206
14207 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
14211 if self.o1_active() || self.attn_softcap > 0.0 {
14212 return None;
14213 }
14214 if let Some(reason) = self.wgpu_graph_attn_decline() {
14217 self.note_graph_decline("wgpu multi-burst", reason);
14218 return None;
14219 }
14220 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
14221 if !graph_on || self.graph_refused() {
14222 return None;
14229 }
14230 let emb = self.embed_single(t_next);
14231 let mut lg = Vec::new();
14232 let mut ids = Vec::new();
14233 match self.try_token_graph_wgpu_steps(
14234 &emb,
14235 position,
14236 &mut lg,
14237 k,
14238 Some(&mut ids),
14239 None,
14240 0,
14241 self.num_layers,
14242 ) {
14243 Some(Ok(_)) => {}
14244 Some(Err(())) => {
14245 self.graph_failed
14250 .store(true, std::sync::atomic::Ordering::Relaxed);
14251 return None;
14252 }
14253 None => return None,
14254 }
14255 (ids.len() == k).then_some(ids)
14256 }
14257
14258 fn try_token_graph_wgpu_steps(
14262 &self,
14263 hidden: &[f32],
14264 position: usize,
14265 logits_out: &mut Vec<f32>,
14266 steps: usize,
14267 ids_out: Option<&mut Vec<u32>>,
14268 layers_run: Option<&mut usize>,
14269 from: usize,
14270 upto_excl: usize,
14271 ) -> Option<Result<Vec<f32>, ()>> {
14272 if self.prompt_mrope.is_some()
14276 || self.mhc.as_deref().is_some_and(|m| {
14277 from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
14278 })
14279 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
14280 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
14281 && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
14282 {
14283 return None;
14284 }
14285 let upto_excl = match self.mimo_moe.graph_prefix_end() {
14288 Some(end) if end < upto_excl => {
14289 if steps != 1 || layers_run.is_none() || from >= end {
14290 return None;
14291 }
14292 end
14293 }
14294 _ => upto_excl,
14295 };
14296 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
14299 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
14300 return None;
14304 }
14305 if let Some(reason) = self.wgpu_graph_attn_decline() {
14312 self.note_graph_decline("wgpu token graph", reason);
14313 return None;
14314 }
14315 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
14320 .map(|li| {
14321 if !o1_gpu {
14322 return None;
14323 }
14324 self.kv_cache.layers[self.phys_layer(li)].o1_views()
14325 })
14326 .collect();
14327 if self.o1_active() && o1_gpu {
14328 let want: usize = (from..upto_excl)
14331 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14332 .count();
14333 let have = o1_views.iter().filter(|v| v.is_some()).count();
14334 if want == 0 || have != want {
14335 use std::sync::atomic::{AtomicUsize, Ordering};
14345 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14346 let code = have * 1000 + want;
14347 if LAST.swap(code, Ordering::Relaxed) != code {
14348 tracing::warn!(
14349 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14350 );
14351 }
14352 return None;
14353 }
14354 }
14355 let nh = self.num_heads;
14356 let (nkv, hd, rd) = self.layer_geom(0);
14357 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14358 let mut layers = Vec::with_capacity(upto_excl - from);
14359 let mut model = None;
14360 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14361 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14362 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14364 let name = &m.tensors[i].name;
14365 let prism = if crate::prism::is_inverse_embedding(m, name) {
14366 crate::gpu::GraphPrismOp::InverseEmbedding
14367 } else if crate::prism::is_forward_weight(m, name) {
14368 crate::gpu::GraphPrismOp::Forward
14369 } else {
14370 crate::gpu::GraphPrismOp::None
14371 };
14372 return Some(crate::gpu::GraphW {
14373 idx: i,
14374 kind,
14375 row_scale: rs,
14376 data: &[],
14377 prism,
14378 affine: crate::prism::is_affine_target(m, name),
14379 });
14380 }
14381 match t.as_f32() {
14383 Some(d) => Some(crate::gpu::GraphW {
14384 idx: 0,
14385 kind: 4,
14386 row_scale: &[],
14387 data: d,
14388 prism: crate::gpu::GraphPrismOp::None,
14389 affine: false,
14390 }),
14391 None => {
14392 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14393 eprintln!("batch graph: weight has no graph/f32 representation");
14394 }
14395 None
14396 }
14397 }
14398 }
14399 for li in from..upto_excl {
14400 let lw = &self.weights.layers[self.phys_layer(li)];
14401 if dbg {
14402 let ak = match &lw.attn {
14403 AttnKind::Mla(_) => "Mla".into(),
14404 AttnKind::Full {
14405 output_gate, bias, ..
14406 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14407 AttnKind::LinearGdn(_) => "LinearGdn".into(),
14408 AttnKind::Kda(_) => "Kda".into(),
14409 AttnKind::Linear(_) => "Linear".into(),
14410 AttnKind::ShortConv(_) => "ShortConv".into(),
14411 AttnKind::Bounded(_) => "Bounded".into(),
14412 };
14413 let fk = match &lw.ffn {
14414 FfnKind::Dense(_) => "Dense",
14415 FfnKind::Moe(_) => "Moe",
14416 FfnKind::DenseMoe(_) => "DenseMoe",
14417 };
14418 eprintln!("graph L{li}: attn={ak} ffn={fk}");
14419 }
14420 let gffn = match &lw.ffn {
14421 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14425 FfnKind::Dense(d) => {
14426 let Some(act) = d.act.wgpu_graph_act() else {
14430 self.note_graph_decline(
14431 "wgpu token graph",
14432 "dense FFN activation without a graph kernel",
14433 );
14434 return None;
14435 };
14436 crate::gpu::GraphFfn::Dense {
14437 gate: gw(&d.gate_proj)?,
14438 up: gw(&d.up_proj)?,
14439 down: gw(&d.down_proj)?,
14440 act,
14441 }
14442 }
14443 FfnKind::Moe(m) => {
14444 if m.route_tau.is_some() || m.mask.is_some() {
14452 return None;
14453 }
14454 let shared = m.shared.as_ref();
14455 let inter = m.experts.first()?.gate_proj.rows();
14461 let detached_shared = match shared {
14469 Some((se, None))
14470 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14471 {
14472 if !matches!(se.act, Act::Silu)
14473 || se.gate_proj.mapped_q4tp().is_none()
14474 || se.up_proj.mapped_q4tp().is_none()
14475 || se.down_proj.mapped_q4tp().is_none()
14476 {
14477 return None;
14478 }
14479 Some(crate::gpu::GraphMoeDetachedShared {
14480 gate: gw(&se.gate_proj)?,
14481 up: gw(&se.up_proj)?,
14482 down: gw(&se.down_proj)?,
14483 width: se.gate_proj.rows(),
14484 })
14485 }
14486 Some((se, Some(_)))
14490 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14491 {
14492 return None;
14493 }
14494 _ => None,
14495 };
14496 let has_shared = shared.is_some() && detached_shared.is_none();
14497 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14498 let sgate = match shared {
14499 Some((_, Some(sg))) if has_shared => gw(sg)?,
14500 _ => gw(&m.router)?,
14504 };
14505 let router = gw(&m.router)?;
14506 if router.prism != crate::gpu::GraphPrismOp::None
14512 || sgate.prism != crate::gpu::GraphPrismOp::None
14513 || router.affine
14514 || sgate.affine
14515 {
14516 tracing::warn!(
14517 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14518 );
14519 return None;
14520 }
14521 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14522 let edge = |t: &QTensor| {
14523 t.graph_weight()
14524 .or_else(|| t.graph_weight_descriptor())
14525 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14526 };
14527 let bank_key: Option<GraphMoeKey> = match (
14528 m.experts.first().and_then(|e| edge(&e.gate_proj)),
14529 m.experts.last().and_then(|e| edge(&e.down_proj)),
14530 ) {
14531 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14532 self.phys_layer(li),
14533 m.experts.as_ptr() as usize,
14534 m.experts.len(),
14535 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14536 fm,
14537 fi,
14538 di,
14539 )),
14540 _ => None,
14541 };
14542 let cached =
14543 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14544 let bank = if let Some(b) = cached {
14545 b
14546 } else {
14547 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14548 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14549 let mut q4tp: Option<bool> = None;
14552 let mut gu_q2: Option<bool> = None;
14555 let mut q82: Option<bool> = None;
14557 let mut moe_act = None;
14561 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14562 let Some(e_act) = e.act.wgpu_graph_act() else {
14563 return None;
14564 };
14565 match moe_act {
14566 Some(existing) if existing != e_act => return None,
14567 None => moe_act = Some(e_act),
14568 _ => {}
14569 }
14570 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14571 return None;
14572 }
14573 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14578 let Some((em, ei, _, _)) = expert_weight
14579 .graph_weight()
14580 .or_else(|| expert_weight.graph_weight_descriptor())
14581 else {
14582 return None;
14583 };
14584 let name = &em.tensors[ei].name;
14585 if crate::prism::is_forward_weight(em, name)
14586 || crate::prism::is_inverse_embedding(em, name)
14587 || crate::prism::is_affine_target(em, name)
14588 {
14589 tracing::warn!(
14590 "resident MoE declined: expert Prism/affine transform is not implemented"
14591 );
14592 return None;
14593 }
14594 }
14595 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14596 Some((mm, gi)) => (
14597 mm,
14598 gi,
14599 e.up_proj.mapped_q4t()?.1,
14600 e.down_proj.mapped_q4t()?.1,
14601 false,
14602 false,
14603 false,
14604 ),
14605 None => match e.gate_proj.mapped_q2tp() {
14606 Some((mm, gi)) => (
14607 mm,
14608 gi,
14609 e.up_proj.mapped_q2tp()?.1,
14610 e.down_proj.mapped_q4tp()?.1,
14611 true,
14612 true,
14613 false,
14614 ),
14615 None => match e.gate_proj.mapped_q4tp() {
14616 Some((mm, gi)) => (
14617 mm,
14618 gi,
14619 e.up_proj.mapped_q4tp()?.1,
14620 e.down_proj.mapped_q4tp()?.1,
14621 true,
14622 false,
14623 false,
14624 ),
14625 None => {
14628 if !crate::gpu::moe_q82_graph_on() {
14629 return None;
14630 }
14631 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14632 (
14633 mm,
14634 gi,
14635 e.up_proj.mapped_q8_2f()?.1,
14636 e.down_proj.mapped_q8_2f()?.1,
14637 false,
14638 false,
14639 true,
14640 )
14641 }
14642 },
14643 },
14644 };
14645 if *q4tp.get_or_insert(is_p) != is_p
14646 || *gu_q2.get_or_insert(is_q2) != is_q2
14647 || *q82.get_or_insert(is_8) != is_8
14648 {
14649 tracing::warn!(
14655 "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."
14656 );
14657 return None;
14658 }
14659 bank_model.get_or_insert_with(|| mm.clone());
14660 experts.push((gi, ui, di));
14661 }
14662 let b = GraphMoeBank {
14663 experts,
14664 q4tp,
14665 gu_q2,
14666 q82,
14667 act: moe_act,
14668 model: bank_model,
14669 };
14670 if let Some(k) = bank_key {
14671 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14672 }
14673 b
14674 };
14675 if let Some(mm) = &bank.model {
14676 model.get_or_insert_with(|| mm.clone());
14677 }
14678 crate::gpu::GraphFfn::Moe {
14679 router,
14680 shared_gate: sgate,
14681 experts: bank.experts,
14682 n_exp: m.experts.len(),
14683 top_k: std::env::var("CMF_TOPK_PROBE")
14689 .ok()
14690 .and_then(|v| v.parse::<usize>().ok())
14691 .filter(|k| *k > 0 && *k <= m.top_k)
14692 .unwrap_or(m.top_k),
14693 inter,
14694 act: bank.act?,
14695 norm_topk: m.norm_topk_prob,
14696 q4tp: bank.q4tp?,
14697 gu_q2: bank.gu_q2.unwrap_or(false),
14698 q82: bank.q82.unwrap_or(false),
14699 sigmoid: m.router_sigmoid,
14700 sigmoid_denom_eps: m.sigmoid_denom_eps,
14701 bias: m.expert_bias.as_deref(),
14702 has_shared,
14703 detached_shared,
14704 shared_gated,
14705 route_scale: m.routed_scaling,
14706 }
14707 }
14708 };
14709 let attn = match &lw.attn {
14710 AttnKind::Full {
14711 wq,
14712 wk,
14713 wv,
14714 wo,
14715 q_norm,
14716 k_norm,
14717 output_gate,
14718 softplus_gate,
14719 bias,
14720 } => {
14721 if self.attention_heads_per_layer.is_some() {
14722 return None;
14723 }
14724 let head_gate = match softplus_gate {
14728 None => None,
14729 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14730 Some(_) => {
14731 self.note_graph_decline(
14732 "wgpu token graph",
14733 "projected softplus / per-element output gate",
14734 );
14735 return None;
14736 }
14737 };
14738 let (m, _, _, _) = wq.graph_weight_or_blr()?;
14739 model = Some(m.clone());
14740 crate::gpu::GraphAttn::Full {
14741 wq: gw(wq)?,
14742 wk: gw(wk)?,
14743 wv: gw(wv)?,
14744 wo: gw(wo)?,
14745 q_norm: q_norm.as_deref(),
14746 k_norm: k_norm.as_deref(),
14747 late_qk_norm: self.qk_norm_after_rope,
14748 bias: bias
14749 .as_ref()
14750 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14751 output_gate: *output_gate,
14752 cpu_k: self.kv_cache.layers[li].k_heads(),
14753 cpu_v: self.kv_cache.layers[li].v_heads(),
14754 cpu_base: self.kv_cache.layers[li].base(),
14755 geom: self.graph_attn_geom(li),
14756 head_gate,
14757 }
14758 }
14759 AttnKind::LinearGdn(w) => {
14760 let cfg = self.gdn_cfg?;
14761 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14762 model = Some(m.clone());
14763 crate::gpu::GraphAttn::Gdn {
14764 qkv: gw(&w.in_proj_qkv)?,
14765 z: gw(&w.in_proj_z)?,
14766 a: gw(&w.in_proj_a)?,
14767 b: gw(&w.in_proj_b)?,
14768 out: gw(&w.out_proj)?,
14769 conv1d: &w.conv1d,
14770 a_log: &w.a_log,
14771 dt_bias: &w.dt_bias,
14772 norm: &w.norm,
14773 nv: cfg.num_v_heads,
14774 nk: cfg.num_k_heads,
14775 dk: cfg.key_head_dim,
14776 dv: cfg.value_head_dim,
14777 kk: cfg.conv_kernel,
14778 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14779 preview: None,
14780 }
14781 }
14782 AttnKind::ShortConv(w) => {
14783 let cfg = self.short_conv_cfg?;
14784 let (m, _, _, _) = w
14785 .in_proj
14786 .graph_weight()
14787 .or_else(|| w.in_proj.graph_weight_descriptor())?;
14788 model = Some(m.clone());
14789 crate::gpu::GraphAttn::ShortConv {
14790 inp: gw(&w.in_proj)?,
14791 out: gw(&w.out_proj)?,
14792 taps: &w.conv,
14793 kernel: cfg.kernel,
14794 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14795 }
14796 }
14797 _ => return None,
14798 };
14799 layers.push(crate::gpu::GraphLayer {
14800 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
14801 input_norm: &lw.input_norm,
14802 attn,
14803 post_norm: &lw.post_norm,
14804 attn_out_norm: lw.attn_out_norm.as_deref(),
14805 ffn_out_norm: lw.ffn_out_norm.as_deref(),
14806 layer_scale: lw.layer_scale,
14807 ffn: gffn,
14808 });
14809 }
14810 let model = model?;
14811 let lm_gw = if upto_excl == self.num_layers
14817 && self.graph_want_logits
14818 && std::env::var("CMF_GPU_LMHEAD")
14819 .map(|v| v != "0")
14820 .unwrap_or(true)
14821 {
14822 self.weights
14823 .lm_head
14824 .graph_weight()
14825 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14826 .map(|(m, i, kind, rs)| {
14827 let name = &m.tensors[i].name;
14828 let prism = if crate::prism::is_inverse_embedding(m, name) {
14829 crate::gpu::GraphPrismOp::InverseEmbedding
14830 } else if crate::prism::is_forward_weight(m, name) {
14831 crate::gpu::GraphPrismOp::Forward
14832 } else {
14833 crate::gpu::GraphPrismOp::None
14834 };
14835 (
14836 crate::gpu::GraphW {
14837 idx: i,
14838 kind,
14839 row_scale: rs,
14840 data: &[],
14841 prism,
14842 affine: crate::prism::is_affine_target(m, name),
14843 },
14844 self.weights.lm_head.rows(),
14845 )
14846 })
14847 } else {
14848 None
14849 };
14850 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14851 let emb_gw = if steps > 1 {
14853 self.weights
14854 .embed_tokens
14855 .graph_weight()
14856 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14857 .map(|(m, i, kind, rs)| {
14858 let name = &m.tensors[i].name;
14859 let prism = if crate::prism::is_inverse_embedding(m, name) {
14860 crate::gpu::GraphPrismOp::InverseEmbedding
14861 } else if crate::prism::is_forward_weight(m, name) {
14862 crate::gpu::GraphPrismOp::Forward
14863 } else {
14864 crate::gpu::GraphPrismOp::None
14865 };
14866 (
14867 crate::gpu::GraphW {
14868 idx: i,
14869 kind,
14870 row_scale: rs,
14871 data: &[],
14872 prism,
14873 affine: crate::prism::is_affine_target(m, name),
14874 },
14875 self.weights.embed_tokens.rows(),
14876 self.embed_multiplier,
14877 )
14878 })
14879 } else {
14880 None
14881 };
14882
14883 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14889 (from..upto_excl.min(self.num_layers - 1))
14890 .filter(|&li| (li + 1) % self.physical_layers == 0)
14891 .map(|li| li - from)
14892 .collect()
14893 } else {
14894 Vec::new()
14895 };
14896 let mut h = hidden.to_vec();
14897 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14903 let outcome = crate::gpu::forward_token_graph(
14904 &model,
14905 self.graph_kv_id,
14906 &layers,
14907 &o1_views,
14908 self.o1_epoch,
14909 &self.inv_freq,
14910 &mut h,
14911 nh,
14912 nkv,
14913 hd,
14914 self.attn_scale,
14915 rd,
14916 self.hidden_size,
14917 self.intermediate_size,
14918 position,
14919 self.kv_cache.max_seq_len,
14920 gemma,
14921 self.rms_eps as f32,
14922 lm,
14923 &self.weights.final_norm,
14924 logits_out,
14925 &loop_norm_at,
14926 steps,
14927 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14928 ids_out,
14929 layers_run,
14930 from,
14931 dump_hidden,
14932 );
14933 match outcome {
14934 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14935 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14936 crate::gpu::TokenGraphOutcome::Declined => None,
14937 }
14938 }
14939
14940 #[cfg(target_os = "macos")]
14949 #[allow(clippy::type_complexity)]
14950 fn metal_rows_plan(
14951 &self,
14952 ) -> Option<(
14953 Vec<MetalRowsItem<'_>>,
14954 std::sync::Arc<cortiq_core::CmfModel>,
14955 Option<crate::gpu_metal::GdnGpuCfg>,
14956 )> {
14957 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14958 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14959 if !graph_force
14960 || !crate::gpu::enabled_here()
14961 || std::env::var("CMF_GPU_BLOCK")
14962 .map(|v| v == "0")
14963 .unwrap_or(false)
14964 || self.attn_softcap > 0.0
14965 || self.o1_active()
14966 || self.swa.is_some()
14967 || self.global_attn.is_some()
14968 || self.attention_heads_per_layer.is_some()
14969 || self.graph_attn_decline_reason().is_some()
14971 || self.attn_v_norm
14972 || self.loop_final_norm
14973 || !self.metal_rope_scale_ok()
14974 {
14975 return None;
14976 }
14977 let attend_contract = self.head_dim % 4 == 0
14978 && self.head_dim <= 256
14979 && self.rotary_dim >= 2
14980 && self.rotary_dim <= self.head_dim
14981 && (self.rotary_dim / 2) % 32 == 0
14982 && self.num_kv_heads > 0
14983 && self.num_heads % self.num_kv_heads == 0;
14984 if !attend_contract {
14985 return None;
14986 }
14987 let mut plan: Vec<MetalRowsItem> = Vec::new();
14988 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14989 for li in 0..self.num_layers {
14990 let lw = &self.weights.layers[self.phys_layer(li)];
14991 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14992 return None;
14993 }
14994 let ffn = match &lw.ffn {
14995 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14996 let (Some(g), Some(u), Some(dn)) = (
14997 d.gate_proj.metal_graph_parts(),
14998 d.up_proj.metal_graph_parts(),
14999 d.down_proj.metal_graph_parts(),
15000 ) else {
15001 return None;
15002 };
15003 MetalFfn::Dense {
15004 gate: g,
15005 up: u,
15006 down: dn,
15007 act: 0, }
15009 }
15010 _ => return None,
15011 };
15012 match &lw.attn {
15013 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
15014 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
15015 w.in_proj_qkv.metal_graph_parts(),
15016 w.in_proj_z.metal_graph_parts(),
15017 w.in_proj_a.f32_parts(),
15018 w.in_proj_b.f32_parts(),
15019 w.out_proj.metal_graph_parts(),
15020 ) else {
15021 return None;
15022 };
15023 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
15024 model_ref.get_or_insert_with(|| model.clone());
15025 }
15026 let gl = GdnGpuLayer {
15027 mhc: None,
15028 attn_norm: &lw.input_norm,
15029 post_norm: &lw.post_norm,
15030 qkv,
15031 z,
15032 a,
15033 b: bb,
15034 out,
15035 ffn,
15036 conv1d: &w.conv1d,
15037 a_log: &w.a_log,
15038 dt_bias: &w.dt_bias,
15039 gnorm: &w.norm,
15040 };
15041 match plan.last_mut() {
15042 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
15043 _ => plan.push(MetalRowsItem::Gdn {
15044 run: vec![gl],
15045 first: li,
15046 }),
15047 }
15048 }
15049 AttnKind::Full {
15050 wq,
15051 wk,
15052 wv,
15053 wo,
15054 q_norm,
15055 k_norm,
15056 output_gate,
15057 softplus_gate: None,
15058 bias: None,
15059 } => {
15060 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
15061 wq.metal_graph_parts(),
15062 wk.metal_graph_parts(),
15063 wv.metal_graph_parts(),
15064 wo.metal_graph_parts(),
15065 ) else {
15066 return None;
15067 };
15068 if let QTensor::Mapped { model, .. } = wq {
15069 model_ref.get_or_insert_with(|| model.clone());
15070 }
15071 let cache = &self.kv_cache.layers[li];
15072 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
15073 return None;
15074 }
15075 plan.push(MetalRowsItem::Attn {
15076 l: AttnGpuLayer {
15077 mhc: None,
15078 attn_norm: &lw.input_norm,
15079 post_norm: &lw.post_norm,
15080 wq: pq,
15081 wk: pk,
15082 wv: pv,
15083 wo: po,
15084 ffn,
15085 attn_out_norm: None,
15086 ffn_out_norm: None,
15087 layer_scale: None,
15088 },
15089 li,
15090 q_norm: q_norm.as_deref(),
15091 k_norm: k_norm.as_deref(),
15092 output_gate: *output_gate,
15093 });
15094 }
15095 _ => return None,
15096 }
15097 }
15098 let model = model_ref?;
15099 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
15100 nv: cfg.num_v_heads,
15101 nk: cfg.num_k_heads,
15102 dk: cfg.key_head_dim,
15103 dv: cfg.value_head_dim,
15104 kk: cfg.conv_kernel,
15105 hidden: self.hidden_size,
15106 inter: self.intermediate_size,
15107 c_dim: cfg.conv_dim(),
15108 eps: cfg.rms_eps as f32,
15109 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15110 });
15111 Some((plan, model, gcfg))
15112 }
15113
15114 #[cfg(target_os = "macos")]
15116 #[allow(clippy::too_many_arguments)]
15117 fn metal_attn_params<'a>(
15118 li: usize,
15119 cache: &'a crate::kv_cache::LayerKvCache,
15120 q_norm: Option<&'a [f32]>,
15121 k_norm: Option<&'a [f32]>,
15122 output_gate: bool,
15123 inv_freq: &'a [f32],
15124 rope_scale: f32,
15125 geom: (usize, usize, usize, usize),
15126 pos0: usize,
15127 kv_id: u64,
15128 scale: f32,
15129 eps: f32,
15130 gemma: bool,
15131 late_qk_norm: bool,
15132 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
15133 let (nh, nkv, hd, rd) = geom;
15134 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15135 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15136 let cpu_stored = cpu_k[0].len() / hd;
15137 (
15138 crate::gpu_metal::AttnDeviceParams {
15139 kv_id,
15140 layer: li,
15141 nh,
15142 nkv,
15143 hd,
15144 rd,
15145 position: pos0,
15146 scale,
15147 eps,
15148 gemma,
15149 late_qk_norm,
15150 output_gate,
15151 q_norm,
15152 k_norm,
15153 inv_freq,
15154 rope_scale,
15155 cpu_k,
15156 cpu_v,
15157 cpu_stored,
15158 cpu_gen: cache.generation(),
15159 o1: None,
15160 window: None,
15161 softmax_sink: cache.sinks.as_deref(),
15162 head_gate: None,
15163 v_norm: false,
15164 },
15165 cpu_stored,
15166 )
15167 }
15168
15169 #[cfg(target_os = "macos")]
15174 #[allow(clippy::type_complexity)]
15175 fn metal_rows_run(
15176 &mut self,
15177 hiddens: &mut [f32],
15178 pos0: usize,
15179 b: usize,
15180 prefill: bool,
15181 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15182 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
15186 ) -> MetalRowsRun {
15187 use crate::gpu_metal::{GraphDims, VerifyGraph};
15188 if !crate::gpu_metal::wait_replay() {
15194 tracing::error!("Metal rows graph: the pending async replay failed");
15195 return MetalRowsRun::Failed;
15196 }
15197 spec_stamp("v.wait");
15198 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
15204 if want > 0 {
15205 let phys = self.physical_layers.max(1);
15206 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
15207 let is_gdn = self
15208 .weights
15209 .layers
15210 .get(li % phys)
15211 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
15212 if is_gdn && l.linear_state.len() != want {
15213 l.linear_state = vec![0f32; want];
15214 }
15215 }
15216 }
15217 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
15218 return MetalRowsRun::Declined;
15219 };
15220 spec_stamp("v.plan");
15221 let dims = GraphDims {
15222 hidden: self.hidden_size,
15223 eps: self.rms_eps as f32,
15224 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15225 };
15226 let Some(mut graph) = (if prefill {
15227 VerifyGraph::new_prefill(&model, dims, hiddens, b)
15228 } else {
15229 VerifyGraph::new(&model, dims, hiddens, b)
15230 }) else {
15231 return MetalRowsRun::Declined;
15232 };
15233 let geom = (
15234 self.num_heads,
15235 self.num_kv_heads,
15236 self.head_dim,
15237 self.rotary_dim,
15238 );
15239 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15240 let eps = self.rms_eps as f32;
15241 let kv_id = self.graph_kv_id;
15242 let inv_freq = self.inv_freq.clone();
15243 for item in &plan {
15244 let ok = match item {
15245 MetalRowsItem::Gdn { run, .. } => gcfg
15246 .as_ref()
15247 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
15248 .unwrap_or(false),
15249 MetalRowsItem::Attn {
15250 l,
15251 li,
15252 q_norm,
15253 k_norm,
15254 output_gate,
15255 } => {
15256 let (p, _) = Self::metal_attn_params(
15257 *li,
15258 &self.kv_cache.layers[*li],
15259 *q_norm,
15260 *k_norm,
15261 *output_gate,
15262 &inv_freq,
15263 self.layer_rope_scale(*li),
15264 geom,
15265 pos0,
15266 kv_id,
15267 self.attn_scale,
15268 eps,
15269 gemma,
15270 self.qk_norm_after_rope,
15271 );
15272 graph.attn_ok(l, &p)
15273 }
15274 };
15275 if !ok {
15276 use std::sync::atomic::{AtomicBool, Ordering};
15277 static SAID: AtomicBool = AtomicBool::new(false);
15278 if !SAID.swap(true, Ordering::Relaxed) {
15279 tracing::warn!("metal rows graph: a layer failed preflight — declining");
15280 }
15281 return MetalRowsRun::Declined;
15282 }
15283 }
15284 let lm = match &spec {
15285 Some((lm, _, _)) => {
15286 if !graph.lm_head_ok(*lm) {
15287 return MetalRowsRun::Declined;
15288 }
15289 Some(*lm)
15290 }
15291 None => None,
15292 };
15293 let mut gdn_layers = Vec::new();
15294 let mut attn_layers = Vec::new();
15295 for item in &plan {
15296 match item {
15297 MetalRowsItem::Gdn { run, first } => {
15298 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
15299 .iter()
15300 .map(|l| l.linear_state.as_slice())
15301 .collect();
15302 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
15303 return MetalRowsRun::Declined;
15304 }
15305 gdn_layers.extend(*first..*first + run.len());
15306 }
15307 MetalRowsItem::Attn {
15308 l,
15309 li,
15310 q_norm,
15311 k_norm,
15312 output_gate,
15313 } => {
15314 let (p, cpu_stored) = Self::metal_attn_params(
15315 *li,
15316 &self.kv_cache.layers[*li],
15317 *q_norm,
15318 *k_norm,
15319 *output_gate,
15320 &inv_freq,
15321 self.layer_rope_scale(*li),
15322 geom,
15323 pos0,
15324 kv_id,
15325 self.attn_scale,
15326 eps,
15327 gemma,
15328 self.qk_norm_after_rope,
15329 );
15330 if !graph.encode_attn_b(l, &p) {
15331 return MetalRowsRun::Declined;
15332 }
15333 attn_layers.push((*li, cpu_stored));
15334 }
15335 }
15336 }
15337 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15338 if !graph.encode_lm_head_b(final_norm, lm) {
15339 return MetalRowsRun::Declined;
15340 }
15341 if let Some((n, _)) = argmax_out.as_ref() {
15346 if !graph.encode_argmax_b(*n) {
15347 argmax_out = None;
15348 }
15349 }
15350 }
15351 spec_stamp("v.enc");
15352 if !graph.sync() {
15353 return MetalRowsRun::Failed;
15354 }
15355 spec_stamp("v.gpu");
15356 match (spec, argmax_out) {
15357 (Some(_), Some((_, ids))) => {
15358 ids.resize(b, 0);
15359 if !graph.read_argmax(ids) {
15360 return MetalRowsRun::Failed;
15361 }
15362 spec_stamp("v.am");
15363 }
15364 (Some((lm, _, logits)), None) => {
15365 logits.resize(b * lm.1, 0.0);
15366 if !graph.read_logits(logits) {
15367 return MetalRowsRun::Failed;
15368 }
15369 spec_stamp("v.lg");
15370 }
15371 (None, _) => {}
15372 }
15373 if !graph.read_hidden(hiddens) {
15374 return MetalRowsRun::Failed;
15375 }
15376 spec_stamp("v.hid");
15377 MetalRowsRun::Completed(MetalVerifyPending {
15378 graph,
15379 gdn_layers,
15380 attn_layers,
15381 })
15382 }
15383
15384 #[cfg(target_os = "macos")]
15390 fn try_batch_graph_metal(
15391 &mut self,
15392 hiddens: &mut [f32],
15393 positions: &[usize],
15394 b: usize,
15395 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15396 argmax_out: Option<(usize, &mut Vec<u32>)>,
15397 ) -> crate::gpu::BatchGraphOutcome {
15398 let _t0 = std::time::Instant::now();
15399 if positions.len() != b
15400 || self.mhc.is_some()
15401 || self.prompt_mrope.is_some()
15402 || positions.windows(2).any(|w| w[1] != w[0] + 1)
15403 || hiddens.len() != b * self.hidden_size
15404 {
15405 return crate::gpu::BatchGraphOutcome::Declined;
15406 }
15407 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15408 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15409 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15410 MetalRowsRun::Completed(pending) => pending,
15411 };
15412 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15413 eprintln!(
15414 "metal-verify: {:.1} ms | b={b}",
15415 _t0.elapsed().as_secs_f64() * 1e3
15416 );
15417 }
15418 self.metal_verify = Some(pending);
15419 crate::gpu::BatchGraphOutcome::Completed
15420 }
15421
15422 #[cfg(target_os = "macos")]
15427 fn prefill_rows_metal(
15428 &mut self,
15429 ids: &[u32],
15430 start_pos: usize,
15431 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15432 ) -> MetalPrefillOutcome {
15433 let b = ids.len();
15434 if b == 0 || b > 512 {
15435 return MetalPrefillOutcome::Declined;
15436 }
15437 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15438 let with_head = spec.is_some();
15439 let hs = self.hidden_size;
15440 let mut hiddens = vec![0f32; b * hs];
15441 for (j, &id) in ids.iter().enumerate() {
15442 let e = self.embed_single(id);
15443 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15444 }
15445 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15446 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15447 MetalRowsRun::Failed => {
15448 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15449 return MetalPrefillOutcome::Failed;
15450 }
15451 MetalRowsRun::Completed(pending) => pending,
15452 };
15453 let idxs = pending.gdn_layers.clone();
15455 let mut outs: Vec<&mut [f32]> = self
15456 .kv_cache
15457 .layers
15458 .iter_mut()
15459 .enumerate()
15460 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15461 .map(|(_, l)| l.linear_state.as_mut_slice())
15462 .collect();
15463 if !pending.graph.finish_states(&mut outs) {
15464 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15465 return MetalPrefillOutcome::Failed;
15466 }
15467 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15468 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15472 for (li, cpu_stored) in &pending.attn_layers {
15473 let mut kbuf = vec![0f32; b * nkv * hd];
15474 let mut vbuf = vec![0f32; b * nkv * hd];
15475 if !crate::gpu_metal::kv_mirror_read_rows(
15476 self.graph_kv_id,
15477 *li,
15478 nkv,
15479 hd,
15480 *cpu_stored,
15481 b,
15482 &mut kbuf,
15483 &mut vbuf,
15484 ) {
15485 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15486 return MetalPrefillOutcome::Failed;
15487 }
15488 rows.push((*li, *cpu_stored, kbuf, vbuf));
15489 }
15490 for (li, cpu_stored, kbuf, vbuf) in rows {
15491 let cache = &mut self.kv_cache.layers[li];
15492 for r in 0..b {
15493 cache.append(
15494 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15495 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15496 &[],
15497 );
15498 }
15499 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15500 }
15501 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15502 if with_head {
15503 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15504 }
15505 MetalPrefillOutcome::Completed(hiddens)
15506 }
15507
15508 #[cfg(target_os = "macos")]
15509 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15510 self.prefill_rows_metal(ids, start_pos, None)
15511 }
15512
15513 #[cfg(target_os = "macos")]
15518 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15519 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15520 return MetalBatchNllOutcome::Declined;
15521 }
15522 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15523 return MetalBatchNllOutcome::Declined;
15524 };
15525 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15526 .ok()
15527 .and_then(|v| v.parse::<usize>().ok())
15528 .filter(|&v| (1..=512).contains(&v))
15529 .unwrap_or(32);
15530 let final_norm = self.weights.final_norm.clone();
15531 let mut nll = 0.0f64;
15532 let mut count = 0usize;
15533 let mut pos = 0usize;
15534 let mut completed = 0usize;
15535 while pos < ids.len() {
15536 let end = (pos + chunk).min(ids.len());
15537 let mut logits = Vec::new();
15538 let outcome =
15539 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15540 match outcome {
15541 MetalPrefillOutcome::Declined => {
15542 return if completed == 0 {
15543 MetalBatchNllOutcome::Declined
15544 } else {
15545 MetalBatchNllOutcome::Failed(format!(
15546 "ordinary Metal NLL batch declined after {completed} chunks"
15547 ))
15548 };
15549 }
15550 MetalPrefillOutcome::Failed => {
15551 return MetalBatchNllOutcome::Failed(
15552 "ordinary Metal NLL batch failed after admission".to_string(),
15553 );
15554 }
15555 MetalPrefillOutcome::Completed(_) => {}
15556 }
15557 completed += 1;
15558 let vocab = self.vocab_size.min(lm.1);
15559 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15560 return MetalBatchNllOutcome::Failed(
15561 "ordinary Metal NLL head returned an invalid shape".to_string(),
15562 );
15563 }
15564 for row in 0..(end - pos) {
15565 let absolute = pos + row;
15566 if absolute < start || absolute + 1 >= ids.len() {
15567 continue;
15568 }
15569 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15570 if let Some(mu) = self.logit_multiplier {
15571 for v in lg.iter_mut() {
15572 *v *= mu;
15573 }
15574 }
15575 if let Some(c) = self.final_softcap {
15576 softcap_in_place(lg, c, self.pool.as_deref());
15577 }
15578 let target = ids[absolute + 1] as usize;
15579 if target >= vocab {
15580 return MetalBatchNllOutcome::Failed(format!(
15581 "target token {target} exceeds Metal head rows {vocab}"
15582 ));
15583 }
15584 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15585 let lse: f64 = lg
15586 .iter()
15587 .map(|&v| ((v - max) as f64).exp())
15588 .sum::<f64>()
15589 .ln()
15590 + max as f64;
15591 nll += lse - lg[target] as f64;
15592 count += 1;
15593 }
15594 pos = end;
15595 }
15596 MetalBatchNllOutcome::Completed(nll, count)
15597 }
15598
15599 #[cfg(target_os = "macos")]
15603 fn metal_verify_commit(&mut self, a: usize) -> bool {
15604 let Some(mut pending) = self.metal_verify.take() else {
15605 return false;
15606 };
15607 let n = a + 1;
15608 let idxs = pending.gdn_layers.clone();
15610 let mut outs: Vec<&mut [f32]> = self
15611 .kv_cache
15612 .layers
15613 .iter_mut()
15614 .enumerate()
15615 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15616 .map(|(_, l)| l.linear_state.as_mut_slice())
15617 .collect();
15618 if !pending.graph.commit(n, &mut outs) {
15619 return false;
15620 }
15621 spec_stamp("c.replay");
15622 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15623 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15627 for (li, cpu_stored) in &pending.attn_layers {
15628 let mut kbuf = vec![0f32; n * nkv * hd];
15629 let mut vbuf = vec![0f32; n * nkv * hd];
15630 if !crate::gpu_metal::kv_mirror_read_rows(
15631 self.graph_kv_id,
15632 *li,
15633 nkv,
15634 hd,
15635 *cpu_stored,
15636 n,
15637 &mut kbuf,
15638 &mut vbuf,
15639 ) {
15640 return false;
15641 }
15642 rows.push((*li, *cpu_stored, kbuf, vbuf));
15643 }
15644 for (li, cpu_stored, kbuf, vbuf) in rows {
15645 let cache = &mut self.kv_cache.layers[li];
15646 for r in 0..n {
15647 cache.append(
15648 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15649 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15650 &[],
15651 );
15652 }
15653 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15654 }
15655 spec_stamp("c.kv");
15656 true
15657 }
15658
15659 #[cfg(target_os = "macos")]
15666 fn mtp_warm_batch_submit(
15667 &mut self,
15668 m: &mut MtpModule,
15669 pairs: &[(&[f32], u32)],
15670 first_pos: usize,
15671 ) -> Option<MetalWarmPending> {
15672 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15673 let b = pairs.len();
15674 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15675 return None;
15676 }
15677 let AttnKind::Full {
15678 wq,
15679 wk,
15680 wv,
15681 wo,
15682 q_norm,
15683 k_norm,
15684 output_gate,
15685 softplus_gate: None,
15686 bias: None,
15687 } = &m.layer.attn
15688 else {
15689 return None;
15690 };
15691 let FfnKind::Dense(d) = &m.layer.ffn else {
15692 return None;
15693 };
15694 if !d.segs.is_empty() {
15695 return None;
15696 }
15697 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15698 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15699 else {
15700 return None;
15701 };
15702 let (Some(g), Some(u), Some(dn)) = (
15703 d.gate_proj.q1_parts(),
15704 d.up_proj.q1_parts(),
15705 d.down_proj.q1_parts(),
15706 ) else {
15707 return None;
15708 };
15709 let Some(eh) = m.eh_proj.q1_parts() else {
15710 return None;
15711 };
15712 let QTensor::Mapped { model, .. } = wq else {
15713 return None;
15714 };
15715 let model = model.clone();
15716 let hs = self.hidden_size;
15717 let mut cat = vec![0f32; b * 2 * hs];
15719 for (j, (h, tok)) in pairs.iter().enumerate() {
15720 let e = self.embed_single(*tok);
15721 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15722 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15723 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15724 }
15725 let dims = GraphDims {
15726 hidden: hs,
15727 eps: self.rms_eps as f32,
15728 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15729 };
15730 spec_stamp("w.cat");
15731 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15732 return None;
15733 };
15734 spec_stamp("w.new");
15735 let l = AttnGpuLayer {
15736 mhc: None,
15737 attn_norm: &m.layer.input_norm,
15738 post_norm: &m.layer.post_norm,
15739 wq: pq,
15740 wk: pk,
15741 wv: pv,
15742 wo: po,
15743 ffn: MetalFfn::Dense {
15744 gate: g,
15745 up: u,
15746 down: dn,
15747 act: (d.act == Act::Gelu) as u8,
15748 },
15749 attn_out_norm: None,
15750 ffn_out_norm: None,
15751 layer_scale: None,
15752 };
15753 let (nh, nkv, hd, rd) = (
15754 self.num_heads,
15755 self.num_kv_heads,
15756 self.head_dim,
15757 self.rotary_dim,
15758 );
15759 let inv_freq = self.inv_freq.clone();
15760 let cpu_stored;
15761 {
15762 let cache = &m.kv;
15763 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15764 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15765 cpu_stored = cpu_k[0].len() / hd;
15766 if cpu_stored > first_pos {
15771 spec_stamp("w.decl");
15772 return None;
15773 }
15774 let p = AttnDeviceParams {
15775 kv_id: self.mtp_kv_id(),
15776 layer: Self::MTP_LAYER_BASE,
15777 nh,
15778 nkv,
15779 hd,
15780 rd,
15781 position: first_pos,
15782 scale: self.attn_scale,
15783 eps: self.rms_eps as f32,
15784 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15785 late_qk_norm: self.qk_norm_after_rope,
15786 output_gate: *output_gate,
15787 q_norm: q_norm.as_deref(),
15788 k_norm: k_norm.as_deref(),
15789 inv_freq: &inv_freq,
15790 rope_scale: self.rope_scale,
15791 cpu_k,
15792 cpu_v,
15793 cpu_stored,
15794 cpu_gen: cache.generation(),
15795 o1: None,
15796 window: None,
15797 softmax_sink: None,
15798 head_gate: None,
15799 v_norm: false,
15800 };
15801 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15802 return None;
15803 }
15804 }
15805 spec_stamp("w.enc");
15806 if !graph.submit() {
15807 return None;
15808 }
15809 spec_stamp("w.sub");
15810 Some(MetalWarmPending {
15811 graph,
15812 cpu_stored,
15813 b,
15814 })
15815 }
15816
15817 #[cfg(target_os = "macos")]
15820 fn mtp_warm_batch_metal(
15821 &mut self,
15822 m: &mut MtpModule,
15823 pairs: &[(&[f32], u32)],
15824 first_pos: usize,
15825 ) -> bool {
15826 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15827 Some(p) => self.mtp_warm_batch_finish(m, p),
15828 None => false,
15829 }
15830 }
15831
15832 #[cfg(target_os = "macos")]
15837 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15838 let MetalWarmPending {
15839 mut graph,
15840 cpu_stored,
15841 b,
15842 } = pending;
15843 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15844 if !graph.sync() {
15845 return false;
15846 }
15847 spec_stamp("w.gpu");
15848 let mut kbuf = vec![0f32; b * nkv * hd];
15849 let mut vbuf = vec![0f32; b * nkv * hd];
15850 if !crate::gpu_metal::kv_mirror_read_rows(
15851 self.mtp_kv_id(),
15852 Self::MTP_LAYER_BASE,
15853 nkv,
15854 hd,
15855 cpu_stored,
15856 b,
15857 &mut kbuf,
15858 &mut vbuf,
15859 ) {
15860 return false;
15861 }
15862 for r in 0..b {
15863 m.kv.append(
15864 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15865 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15866 &[],
15867 );
15868 }
15869 crate::gpu_metal::kv_mirror_set_stored(
15870 self.mtp_kv_id(),
15871 Self::MTP_LAYER_BASE,
15872 cpu_stored + b,
15873 );
15874 spec_stamp("w.kv");
15875 true
15876 }
15877
15878 pub(crate) fn note_draft_id(&mut self, id: u32) {
15885 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15886 if (id as usize) >= cut {
15887 self.draft_full_streak = 16;
15888 } else {
15889 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15890 }
15891 }
15892
15893 fn draft_head_rows(&self, head_rows: usize) -> usize {
15896 if self.draft_full_streak > 0 {
15897 head_rows
15898 } else {
15899 Self::draft_vocab_rows(head_rows)
15900 }
15901 }
15902
15903 fn draft_vocab_rows(head_rows: usize) -> usize {
15906 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15907 let n = *N.get_or_init(|| {
15908 std::env::var("CMF_DRAFT_VOCAB")
15909 .ok()
15910 .and_then(|v| v.parse().ok())
15911 .unwrap_or(65536)
15912 });
15913 if n == 0 { head_rows } else { n.min(head_rows) }
15914 }
15915
15916 #[cfg(target_os = "macos")]
15921 fn mtp_step_metal(
15922 &mut self,
15923 m: &mut MtpModule,
15924 hidden: &[f32],
15925 next_token: u32,
15926 position: usize,
15927 want_logits: bool,
15928 ) -> Option<(Vec<f32>, Vec<f32>)> {
15929 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15930 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15931 || !crate::gpu::q1_force()
15932 || !crate::gpu::enabled_here()
15933 || self.attn_softcap > 0.0
15934 || self.attention_heads_per_layer.is_some()
15935 || m.kv.mode != crate::kv_cache::KvMode::F32
15936 || m.kv.o1.is_some()
15937 {
15938 return None;
15939 }
15940 let AttnKind::Full {
15941 wq,
15942 wk,
15943 wv,
15944 wo,
15945 q_norm,
15946 k_norm,
15947 output_gate,
15948 softplus_gate: None,
15949 bias: None,
15950 } = &m.layer.attn
15951 else {
15952 return None;
15953 };
15954 let FfnKind::Dense(d) = &m.layer.ffn else {
15955 return None;
15956 };
15957 if d.act != Act::Silu || !d.segs.is_empty() {
15958 return None;
15959 }
15960 let (pq, pk, pv, po) = (
15961 wq.q1_parts()?,
15962 wk.q1_parts()?,
15963 wv.q1_parts()?,
15964 wo.q1_parts()?,
15965 );
15966 let (g, u, dn) = (
15967 d.gate_proj.q1_parts()?,
15968 d.up_proj.q1_parts()?,
15969 d.down_proj.q1_parts()?,
15970 );
15971 let QTensor::Mapped { model, .. } = wq else {
15972 return None;
15973 };
15974 let model = model.clone();
15975 let lm = if want_logits {
15976 Some(self.weights.lm_head.q1_parts()?)
15977 } else {
15978 None
15979 };
15980 let dims = GraphDims {
15981 hidden: self.hidden_size,
15982 eps: self.rms_eps as f32,
15983 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15984 };
15985 let hs = self.hidden_size;
15988 let mut x = vec![0f32; hs];
15989 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15996 crate::gpu_metal::DENSE_MV,
15997 );
15998 let mut graph = TokenGraph::new(&model, dims, &x)?;
15999 let mut folded = false;
16000 if let Some(eh) = m.eh_proj.q1_parts() {
16001 let e = self.embed_single(next_token);
16002 let mut cat = vec![0.0f32; 2 * hs];
16003 let (cat_e, cat_h) = cat.split_at_mut(hs);
16004 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
16005 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
16006 folded = graph.encode_input_proj(eh, &cat);
16007 }
16008 if !folded {
16009 x = self.mtp_block_input(m, hidden, next_token);
16010 graph = TokenGraph::new(&model, dims, &x)?;
16011 }
16012 spec_stamp("d.in");
16013 let l = AttnGpuLayer {
16014 mhc: None,
16015 attn_norm: &m.layer.input_norm,
16016 post_norm: &m.layer.post_norm,
16017 wq: pq,
16018 wk: pk,
16019 wv: pv,
16020 wo: po,
16021 ffn: MetalFfn::Dense {
16022 gate: g,
16023 up: u,
16024 down: dn,
16025 act: (d.act == Act::Gelu) as u8,
16026 },
16027 attn_out_norm: None,
16028 ffn_out_norm: None,
16029 layer_scale: None,
16030 };
16031 let (nh, nkv, hd, rd) = (
16032 self.num_heads,
16033 self.num_kv_heads,
16034 self.head_dim,
16035 self.rotary_dim,
16036 );
16037 let inv_freq = self.inv_freq.clone();
16038 {
16039 let cache = &m.kv;
16040 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16041 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16042 let cpu_stored = cpu_k[0].len() / hd;
16043 let p = AttnDeviceParams {
16044 kv_id: self.mtp_kv_id(),
16045 layer: Self::MTP_LAYER_BASE,
16046 nh,
16047 nkv,
16048 hd,
16049 rd,
16050 position,
16051 scale: self.attn_scale,
16052 eps: self.rms_eps as f32,
16053 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16054 late_qk_norm: self.qk_norm_after_rope,
16055 output_gate: *output_gate,
16056 q_norm: q_norm.as_deref(),
16057 k_norm: k_norm.as_deref(),
16058 inv_freq: &inv_freq,
16059 rope_scale: self.rope_scale,
16060 cpu_k,
16061 cpu_v,
16062 cpu_stored,
16063 cpu_gen: cache.generation(),
16064 o1: None,
16065 window: None,
16066 softmax_sink: None,
16067 head_gate: None,
16068 v_norm: false,
16069 };
16070 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16071 return None;
16072 }
16073 }
16074 let draft_rows = if let Some(lm) = lm {
16080 self.draft_head_rows(lm.1)
16081 } else {
16082 0
16083 };
16084 if let Some(lm) = lm {
16085 if !graph.lm_head_ok(lm) {
16086 return None;
16087 }
16088 if draft_rows < lm.1 {
16089 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16090 return None;
16091 }
16092 } else {
16093 graph.encode_lm_head(&m.final_norm, lm);
16094 }
16095 }
16096 spec_stamp("d.enc");
16097 if graph.sync_checked().is_err() {
16098 return None;
16099 }
16100 spec_stamp("d.gpu");
16101 let mut logits = Vec::new();
16102 if let Some(lm) = lm {
16103 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
16104 logits = attention::take_buf(n_read);
16105 graph.read_logits(&mut logits);
16106 pad_mtp_draft_logits(&mut logits, self.vocab_size);
16108 }
16109 graph.finish(&mut x);
16110 let mut krow = attention::take_buf(nkv * hd);
16111 let mut vrow = attention::take_buf(nkv * hd);
16112 if crate::gpu_metal::kv_mirror_read_last(
16113 self.mtp_kv_id(),
16114 Self::MTP_LAYER_BASE,
16115 nkv,
16116 hd,
16117 &mut krow,
16118 &mut vrow,
16119 ) {
16120 m.kv.append(&krow, &vrow, &[]);
16121 }
16122 attention::recycle_buf(&mut krow);
16123 attention::recycle_buf(&mut vrow);
16124 spec_stamp("d.rd");
16125 Some((logits, x))
16126 }
16127
16128 fn mtp_chain_on() -> bool {
16141 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16142 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
16143 }
16144
16145 #[cfg(target_os = "macos")]
16157 fn mtp_draft_chain_metal(
16158 &mut self,
16159 m: &mut MtpModule,
16160 hidden: &[f32],
16161 t_next: u32,
16162 position: usize,
16163 k: usize,
16164 ) -> Result<Vec<u32>, bool> {
16165 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
16166 if k == 0
16167 || k > 64
16168 || !Self::mtp_chain_on()
16169 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
16170 || !crate::gpu::q1_force()
16171 || !crate::gpu::enabled_here()
16172 || self.attn_softcap > 0.0
16173 || self.attention_heads_per_layer.is_some()
16174 || m.kv.mode != crate::kv_cache::KvMode::F32
16175 || m.kv.o1.is_some()
16176 || self.dsv4.is_some()
16178 || self.dsv41.is_some()
16179 || self.qwen4_exp.is_some()
16180 || self.per_token_stack()
16181 {
16182 return Err(false);
16183 }
16184 let AttnKind::Full {
16185 wq,
16186 wk,
16187 wv,
16188 wo,
16189 q_norm,
16190 k_norm,
16191 output_gate,
16192 softplus_gate: None,
16193 bias: None,
16194 } = &m.layer.attn
16195 else {
16196 return Err(false);
16197 };
16198 let FfnKind::Dense(d) = &m.layer.ffn else {
16199 return Err(false);
16200 };
16201 if d.act != Act::Silu || !d.segs.is_empty() {
16202 return Err(false);
16203 }
16204 let (Some(pq), Some(pk), Some(pv), Some(po)) =
16205 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
16206 else {
16207 return Err(false);
16208 };
16209 let (Some(g), Some(u), Some(dn)) = (
16210 d.gate_proj.q1_parts(),
16211 d.up_proj.q1_parts(),
16212 d.down_proj.q1_parts(),
16213 ) else {
16214 return Err(false);
16215 };
16216 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
16217 return Err(false);
16218 };
16219 let QTensor::Mapped { model, .. } = wq else {
16220 return Err(false);
16221 };
16222 let model = model.clone();
16223 let QTensor::Mapped {
16226 model: em,
16227 idx: eidx,
16228 dtype: cortiq_core::TensorDtype::Q4TiledP,
16229 ..
16230 } = &self.weights.embed_tokens
16231 else {
16232 return Err(false);
16233 };
16234 if !std::sync::Arc::ptr_eq(em, &model)
16235 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
16236 {
16237 return Err(false);
16238 }
16239 let embed = (
16240 *eidx,
16241 self.weights.embed_tokens.rows(),
16242 self.weights.embed_tokens.cols(),
16243 );
16244 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
16245 return Err(false);
16246 }
16247 let dims = GraphDims {
16248 hidden: self.hidden_size,
16249 eps: self.rms_eps as f32,
16250 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16251 };
16252 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
16256 crate::gpu_metal::DENSE_MV,
16257 );
16258 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
16259 return Err(false);
16260 };
16261 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
16262 return Err(false);
16263 }
16264 let l = AttnGpuLayer {
16265 mhc: None,
16266 attn_norm: &m.layer.input_norm,
16267 post_norm: &m.layer.post_norm,
16268 wq: pq,
16269 wk: pk,
16270 wv: pv,
16271 wo: po,
16272 ffn: MetalFfn::Dense {
16273 gate: g,
16274 up: u,
16275 down: dn,
16276 act: (d.act == Act::Gelu) as u8,
16277 },
16278 attn_out_norm: None,
16279 ffn_out_norm: None,
16280 layer_scale: None,
16281 };
16282 let (nh, nkv, hd, rd) = (
16283 self.num_heads,
16284 self.num_kv_heads,
16285 self.head_dim,
16286 self.rotary_dim,
16287 );
16288 let inv_freq = self.inv_freq.clone();
16289 let draft_rows = self.draft_head_rows(lm.1);
16290 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
16291 if n_arg == 0 {
16292 return Err(false);
16293 }
16294 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
16301 let t_chain = std::time::Instant::now();
16302 graph.chain_ids_init(t_next, k);
16303 let cpu_stored;
16304 {
16305 let cache = &m.kv;
16306 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16307 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16308 cpu_stored = cpu_k[0].len() / hd;
16309 for j in 0..k {
16310 if !graph.encode_chain_input(
16311 embed,
16312 j as u32,
16313 &m.enorm,
16314 &m.hnorm,
16315 self.embed_multiplier,
16316 eh,
16317 ) {
16318 return Err(false);
16319 }
16320 let p = AttnDeviceParams {
16324 kv_id: self.mtp_kv_id(),
16325 layer: Self::MTP_LAYER_BASE,
16326 nh,
16327 nkv,
16328 hd,
16329 rd,
16330 position: position + j,
16331 scale: self.attn_scale,
16332 eps: self.rms_eps as f32,
16333 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16334 late_qk_norm: self.qk_norm_after_rope,
16335 output_gate: *output_gate,
16336 q_norm: q_norm.as_deref(),
16337 k_norm: k_norm.as_deref(),
16338 inv_freq: &inv_freq,
16339 rope_scale: self.rope_scale,
16340 cpu_k: cpu_k.clone(),
16341 cpu_v: cpu_v.clone(),
16342 cpu_stored: cpu_stored + j,
16343 cpu_gen: cache.generation(),
16344 o1: None,
16345 window: None,
16346 softmax_sink: None,
16347 head_gate: None,
16348 v_norm: false,
16349 };
16350 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16351 return Err(false);
16352 }
16353 if draft_rows < lm.1 {
16354 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16355 return Err(false);
16356 }
16357 } else {
16358 graph.encode_lm_head(&m.final_norm, lm);
16359 }
16360 if !graph.encode_argmax(n_arg, j as u32 + 1) {
16361 return Err(false);
16362 }
16363 if split {
16364 graph.commit();
16367 }
16368 }
16369 }
16370 let t_enc = t_chain.elapsed();
16371 if graph.sync_checked().is_err() {
16372 return Err(true);
16373 }
16374 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16375 eprintln!(
16376 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16377 t_enc.as_secs_f64() * 1e3,
16378 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16379 if split { ", split" } else { "" }
16380 );
16381 }
16382 let mut ids = vec![0u32; k];
16383 if !graph.chain_ids_read(&mut ids) {
16384 return Err(true);
16385 }
16386 let mut kbuf = vec![0f32; k * nkv * hd];
16387 let mut vbuf = vec![0f32; k * nkv * hd];
16388 if !crate::gpu_metal::kv_mirror_read_rows(
16389 self.mtp_kv_id(),
16390 Self::MTP_LAYER_BASE,
16391 nkv,
16392 hd,
16393 cpu_stored,
16394 k,
16395 &mut kbuf,
16396 &mut vbuf,
16397 ) {
16398 return Err(true);
16399 }
16400 for r in 0..k {
16401 m.kv.append(
16402 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16403 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16404 &[],
16405 );
16406 }
16407 Ok(ids)
16408 }
16409
16410 fn try_batch_graph_wgpu(
16411 &self,
16412 hiddens: &mut [f32],
16413 positions: &[usize],
16414 k: usize,
16415 spec: Option<crate::gpu::SpecTail<'_>>,
16416 ) -> crate::gpu::BatchGraphOutcome {
16417 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16418 }
16419
16420 fn try_batch_graph_wgpu_prefix(
16425 &self,
16426 hiddens: &mut [f32],
16427 positions: &[usize],
16428 k: usize,
16429 spec: Option<crate::gpu::SpecTail<'_>>,
16430 layers_run: Option<&mut usize>,
16431 ) -> crate::gpu::BatchGraphOutcome {
16432 if self.mhc.as_ref().is_some_and(|m| {
16436 m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
16437 })
16438 {
16439 return crate::gpu::BatchGraphOutcome::Declined;
16440 }
16441 let graph_end = match self.mimo_moe.graph_prefix_end() {
16442 Some(end) if end < self.num_layers => {
16443 if layers_run.is_none() || spec.is_some() || end == 0 {
16444 return crate::gpu::BatchGraphOutcome::Declined;
16445 }
16446 end
16447 }
16448 _ => self.num_layers,
16449 };
16450 let _tb = std::time::Instant::now();
16451 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16452 if self.attn_softcap > 0.0 {
16453 return crate::gpu::BatchGraphOutcome::Declined; }
16455 if let Some(reason) = self
16458 .wgpu_graph_attn_decline()
16459 .or_else(|| self.batch_graph_gemma_decline())
16460 {
16461 self.note_graph_decline("wgpu batch graph", reason);
16462 return crate::gpu::BatchGraphOutcome::Declined;
16463 }
16464 let nh = self.num_heads;
16465 let (nkv, hd, rd) = self.layer_geom(0);
16466 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16467 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16468 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16469 {
16470 let name = &m.tensors[i].name;
16471 let prism = if crate::prism::is_inverse_embedding(m, name) {
16472 crate::gpu::GraphPrismOp::InverseEmbedding
16473 } else if crate::prism::is_forward_weight(m, name) {
16474 crate::gpu::GraphPrismOp::Forward
16475 } else {
16476 crate::gpu::GraphPrismOp::None
16477 };
16478 return Some(crate::gpu::GraphW {
16479 idx: i,
16480 kind,
16481 row_scale: rs,
16482 data: &[],
16483 prism,
16484 affine: crate::prism::is_affine_target(m, name),
16485 });
16486 }
16487 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16488 eprintln!(
16489 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16490 t.rows(),
16491 t.cols()
16492 );
16493 }
16494 t.as_f32().map(|d| crate::gpu::GraphW {
16495 idx: 0,
16496 kind: 4,
16497 row_scale: &[],
16498 data: d,
16499 prism: crate::gpu::GraphPrismOp::None,
16500 affine: false,
16501 })
16502 }
16503 let built: Option<(
16504 Vec<crate::gpu::GraphLayer<'_>>,
16505 std::sync::Arc<cortiq_core::CmfModel>,
16506 )> = (|| {
16507 let mut layers = Vec::with_capacity(graph_end);
16508 let mut model = None;
16509 for li in 0..graph_end {
16510 let lw = &self.weights.layers[self.phys_layer(li)];
16511 let gffn = match &lw.ffn {
16518 FfnKind::Dense(d) if !d.segs.is_empty() => {
16519 if batch_debug {
16520 eprintln!("batch graph: dense segmented FFN at layer {li}");
16521 }
16522 return None;
16523 }
16524 FfnKind::Dense(d) => {
16525 let Some(act) = d.act.wgpu_graph_act() else {
16526 if batch_debug {
16527 eprintln!(
16528 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16529 d.act
16530 );
16531 }
16532 return None;
16533 };
16534 crate::gpu::GraphFfn::Dense {
16535 gate: gw(&d.gate_proj)?,
16536 up: gw(&d.up_proj)?,
16537 down: gw(&d.down_proj)?,
16538 act,
16539 }
16540 }
16541 FfnKind::Moe(m) => {
16542 if m.route_tau.is_some() || m.mask.is_some() {
16549 return None;
16550 }
16551 let shared = m.shared.as_ref();
16555 let has_shared = shared.is_some();
16556 let shared_gated = matches!(shared, Some((_, Some(_))));
16557 let sgate = match shared {
16558 Some((_, Some(sg))) => gw(sg)?,
16559 _ => gw(&m.router)?,
16563 };
16564 let router = gw(&m.router)?;
16565 if router.prism != crate::gpu::GraphPrismOp::None
16571 || router.affine
16572 || sgate.prism != crate::gpu::GraphPrismOp::None
16573 || sgate.affine
16574 {
16575 return None;
16576 }
16577 let inter = m.experts.first()?.gate_proj.rows();
16578 let mut experts = Vec::with_capacity(m.experts.len() + 1);
16579 let mut q4tp: Option<bool> = None;
16580 let mut gu_q2: Option<bool> = None;
16581 let mut q82: Option<bool> = None;
16582 let mut moe_act = None;
16586 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16587 let Some(e_act) = e.act.wgpu_graph_act() else {
16588 return None;
16589 };
16590 match moe_act {
16591 Some(existing) if existing != e_act => return None,
16592 None => moe_act = Some(e_act),
16593 _ => {}
16594 }
16595 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16596 return None;
16597 }
16598 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16602 {
16603 Some((mm, gi)) => (
16604 mm,
16605 gi,
16606 e.up_proj.mapped_q4t()?.1,
16607 e.down_proj.mapped_q4t()?.1,
16608 false,
16609 false,
16610 false,
16611 ),
16612 None => match e.gate_proj.mapped_q2tp() {
16613 Some((mm, gi)) => (
16614 mm,
16615 gi,
16616 e.up_proj.mapped_q2tp()?.1,
16617 e.down_proj.mapped_q4tp()?.1,
16618 true,
16619 true,
16620 false,
16621 ),
16622 None => match e.gate_proj.mapped_q4tp() {
16623 Some((mm, gi)) => (
16624 mm,
16625 gi,
16626 e.up_proj.mapped_q4tp()?.1,
16627 e.down_proj.mapped_q4tp()?.1,
16628 true,
16629 false,
16630 false,
16631 ),
16632 None => {
16633 if !crate::gpu::moe_q82_graph_on() {
16634 return None;
16635 }
16636 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16637 (
16638 mm,
16639 gi,
16640 e.up_proj.mapped_q8_2f()?.1,
16641 e.down_proj.mapped_q8_2f()?.1,
16642 false,
16643 false,
16644 true,
16645 )
16646 }
16647 },
16648 },
16649 };
16650 if *q4tp.get_or_insert(is_p) != is_p
16651 || *gu_q2.get_or_insert(is_q2) != is_q2
16652 || *q82.get_or_insert(is_8) != is_8
16653 {
16654 return None;
16655 }
16656 if [gi, ui, di].into_iter().any(|idx| {
16657 mm.tensors.get(idx).is_some_and(|t| {
16658 crate::prism::is_forward_weight(mm, &t.name)
16659 || crate::prism::is_affine_target(mm, &t.name)
16660 })
16661 }) {
16662 return None;
16663 }
16664 model.get_or_insert_with(|| mm.clone());
16665 experts.push((gi, ui, di));
16666 }
16667 crate::gpu::GraphFfn::Moe {
16668 router,
16669 shared_gate: sgate,
16670 experts,
16671 n_exp: m.experts.len(),
16672 top_k: m.top_k,
16673 inter,
16674 act: moe_act?,
16675 norm_topk: m.norm_topk_prob,
16676 q4tp: q4tp?,
16677 gu_q2: gu_q2.unwrap_or(false),
16678 q82: q82.unwrap_or(false),
16679 sigmoid: m.router_sigmoid,
16680 sigmoid_denom_eps: m.sigmoid_denom_eps,
16681 bias: m.expert_bias.as_deref(),
16682 has_shared,
16683 detached_shared: None,
16684 shared_gated,
16685 route_scale: m.routed_scaling,
16686 }
16687 }
16688 _ => return None,
16689 };
16690 let attn = match &lw.attn {
16691 AttnKind::Full {
16692 wq,
16693 wk,
16694 wv,
16695 wo,
16696 q_norm,
16697 k_norm,
16698 output_gate,
16699 softplus_gate,
16700 bias,
16701 } => {
16702 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16703 if batch_debug {
16704 eprintln!(
16705 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16706 softplus_gate.is_some(),
16707 self.attention_heads_per_layer.is_some()
16708 );
16709 }
16710 return None;
16711 }
16712 let (m, _, _, _) =
16713 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16714 model = Some(m.clone());
16715 crate::gpu::GraphAttn::Full {
16716 wq: gw(wq)?,
16717 wk: gw(wk)?,
16718 wv: gw(wv)?,
16719 wo: gw(wo)?,
16720 q_norm: q_norm.as_deref(),
16721 k_norm: k_norm.as_deref(),
16722 late_qk_norm: self.qk_norm_after_rope,
16723 bias: bias
16724 .as_ref()
16725 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16726 output_gate: *output_gate,
16727 cpu_k: self.kv_cache.layers[li].k_heads(),
16728 cpu_v: self.kv_cache.layers[li].v_heads(),
16729 cpu_base: self.kv_cache.layers[li].base(),
16730 geom: self.graph_attn_geom(li),
16731 head_gate: None,
16734 }
16735 }
16736 AttnKind::LinearGdn(w) => {
16737 let Some(cfg) = self.gdn_cfg else {
16738 if batch_debug {
16739 eprintln!("batch graph: no GDN config at layer {li}");
16740 }
16741 return None;
16742 };
16743 let (m, _, _, _) = w
16744 .in_proj_qkv
16745 .graph_weight()
16746 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16747 model = Some(m.clone());
16748 crate::gpu::GraphAttn::Gdn {
16749 qkv: gw(&w.in_proj_qkv)?,
16750 z: gw(&w.in_proj_z)?,
16751 a: gw(&w.in_proj_a)?,
16752 b: gw(&w.in_proj_b)?,
16753 out: gw(&w.out_proj)?,
16754 conv1d: &w.conv1d,
16755 a_log: &w.a_log,
16756 dt_bias: &w.dt_bias,
16757 norm: &w.norm,
16758 nv: cfg.num_v_heads,
16759 nk: cfg.num_k_heads,
16760 dk: cfg.key_head_dim,
16761 dv: cfg.value_head_dim,
16762 kk: cfg.conv_kernel,
16763 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16764 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16765 }
16766 }
16767 _ => return None,
16768 };
16769 let g4 = self.global_attn.is_some() || self.attn_v_norm;
16772 layers.push(crate::gpu::GraphLayer {
16773 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
16774 input_norm: &lw.input_norm,
16775 attn,
16776 post_norm: &lw.post_norm,
16777 attn_out_norm: if g4 {
16778 lw.attn_out_norm.as_deref()
16779 } else {
16780 None
16781 },
16782 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16783 layer_scale: if g4 { lw.layer_scale } else { None },
16784 ffn: gffn,
16785 });
16786 }
16787 Some((layers, model?))
16788 })();
16789 let Some((layers, model)) = built else {
16790 {
16791 use std::sync::atomic::{AtomicBool, Ordering};
16792 static SAID: AtomicBool = AtomicBool::new(false);
16793 if !SAID.swap(true, Ordering::Relaxed) {
16794 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16795 }
16796 }
16797 return crate::gpu::BatchGraphOutcome::Declined;
16798 };
16799 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16800 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16801 }
16802 #[cfg(feature = "gpu")]
16805 crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
16806 #[cfg(feature = "gpu")]
16808 let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
16809 let half = rd / 2;
16810 let mut tab = vec![0f32; k * 64];
16811 for (i, &p) in positions.iter().enumerate() {
16812 for j in 0..half.min(64) {
16813 let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
16814 tab[i * 64 + j] = a * self.inv_freq[j];
16815 }
16816 }
16817 crate::gpu_wgpu::set_batch_mrope(Some(tab));
16818 struct Clear;
16819 impl Drop for Clear {
16820 fn drop(&mut self) {
16821 crate::gpu_wgpu::set_batch_mrope(None);
16822 }
16823 }
16824 Clear
16825 });
16826 crate::gpu::forward_batch_graph(
16827 &model,
16828 self.graph_kv_id,
16829 &layers,
16830 &self.inv_freq,
16831 hiddens,
16832 nh,
16833 nkv,
16834 hd,
16835 rd,
16836 self.hidden_size,
16837 self.intermediate_size,
16838 positions,
16839 self.kv_cache.max_seq_len,
16840 gemma,
16841 self.rms_eps as f32,
16842 self.attn_scale,
16843 k,
16844 &(0..graph_end)
16845 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16846 .collect::<Vec<_>>(),
16847 self.o1_epoch,
16848 spec,
16849 layers_run,
16850 )
16851 }
16852
16853 fn draft_probe() -> bool {
16857 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16858 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16859 }
16860
16861 #[cfg(feature = "gpu")]
16873 fn dsv4_spec_on() -> bool {
16874 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16875 *ON.get_or_init(|| {
16876 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16880 return v != "0";
16881 }
16882 std::env::var("CMF_DSV4_SPEC")
16889 .map(|v| v != "0")
16890 .unwrap_or_else(|_| {
16891 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16892 })
16893 })
16894 }
16895
16896 #[cfg(feature = "gpu")]
16903 fn dsv4_spec_step(
16904 &mut self,
16905 tip_token: u32,
16906 t_next: u32,
16907 next_pos: usize,
16908 max_extra: usize,
16909 drafted: &mut usize,
16910 accepted_ctr: &mut usize,
16911 ) -> Option<(Vec<u32>, usize)> {
16912 let t_all = std::time::Instant::now();
16913 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16914 thread_local! {
16915 static LAST: std::cell::Cell<Option<std::time::Instant>> =
16916 const { std::cell::Cell::new(None) };
16917 }
16918 LAST.with(|l| {
16919 if let Some(prev) = l.get() {
16920 eprintln!(
16921 "between rounds {:.1} ms",
16922 prev.elapsed().as_secs_f64() * 1e3
16923 );
16924 }
16925 l.set(Some(std::time::Instant::now()));
16926 });
16927 }
16928 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16929 eprintln!("spec_step: enter pos={next_pos}");
16930 }
16931 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16932 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16933 if self.dspark.is_none() {
16935 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16936 if t.is_empty() {
16937 return None;
16938 }
16939 crate::dsv4::dspark_arm(&t, cfg.dim);
16940 self.dspark = Some(crate::dsv4::DsparkState::new(
16941 self.dsv4_mtp.len(),
16942 &cfg,
16943 t.len(),
16944 ));
16945 }
16946 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16947 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16948 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16949 eprintln!("spec_step: the pack was not built (targets {targets:?})");
16950 }
16951 let pack = pack?;
16952 let block = crate::dsv4::dspark_block();
16953 let b_box = self.dsv4.as_mut()?;
16954 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16955 let ds = self.dspark.as_mut()?;
16956 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16959 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16960 if dbg {
16961 eprintln!("spec_step: no capture");
16962 }
16963 return None;
16964 }
16965 ds.have_hidden = true;
16966 let tip_pos = next_pos.checked_sub(1)?;
16967 let draft_started = std::time::Instant::now();
16968 let mut conf = Vec::new();
16969 let props = crate::dsv4::dspark_draft_gpu(
16970 g,
16971 &self.dsv4_mtp,
16972 &cfg,
16973 ds,
16974 pack,
16975 st.kv_id,
16976 tip_token,
16977 tip_pos,
16978 self.pool.as_deref(),
16979 &mut conf,
16980 );
16981 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16982 *drafted += block;
16983 if props.is_empty() || props[0] != t_next {
16984 if dbg {
16985 eprintln!(
16986 "spec_step: draft {} (props0={:?} t_next={t_next})",
16987 if props.is_empty() {
16988 "empty"
16989 } else {
16990 "miss"
16991 },
16992 props.first()
16993 );
16994 }
16995 return None;
16996 }
16997 let mut k_verify = crate::dsv4::dspark_verify_k()
17004 .min(props.len())
17005 .min(max_extra.saturating_add(1));
17006 let conf_min = {
17012 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17013 *M.get_or_init(|| {
17014 std::env::var("CMF_DSPARK_CONF_MIN")
17015 .ok()
17016 .and_then(|v| v.parse().ok())
17017 .unwrap_or(0.0)
17018 })
17019 };
17020 if conf_min > 0.0 && conf.len() >= props.len() {
17021 let mut keep = 1usize;
17022 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
17023 keep += 1;
17024 }
17025 k_verify = k_verify.min(keep.max(2));
17026 }
17027 if k_verify < 2 {
17028 return None;
17029 }
17030 let mut fed = Vec::with_capacity(k_verify);
17031 fed.push(t_next);
17032 fed.extend_from_slice(&props[1..k_verify]);
17033 let mut argmax = Vec::new();
17034 let mut logits_all = Vec::new();
17035 let mut walked = Vec::new();
17036 let txn = crate::dsv4::dsv4_verify_chunk(
17037 g,
17038 layers,
17039 &cfg,
17040 st,
17041 &fed,
17042 next_pos,
17043 &self.inv_freq,
17044 self.pool.as_deref(),
17045 &targets,
17046 &mut argmax,
17047 &mut logits_all,
17048 &mut walked,
17049 );
17050 if txn.is_none() && dbg {
17051 eprintln!("spec_step: verify declined");
17052 }
17053 let txn = txn?;
17054 let spec_gpu_end = txn.gpu_end;
17055 let b = fed.len();
17056 let mut accepted = 1usize;
17057 while accepted < b && fed[accepted] == argmax[accepted - 1] {
17058 accepted += 1;
17059 }
17060 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
17065 accepted = 1;
17066 }
17067 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
17068 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
17069 }
17070 let t_fin = std::time::Instant::now();
17071 if !crate::dsv4::dsv4_spec_finish(
17072 g,
17073 layers,
17074 &cfg,
17075 st,
17076 txn,
17077 accepted,
17078 &fed,
17079 &self.inv_freq,
17080 self.pool.as_deref(),
17081 ) {
17082 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
17083 return None;
17084 }
17085 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17086 eprintln!(
17087 "finish(k={accepted}): {:.1} ms",
17088 t_fin.elapsed().as_secs_f64() * 1e3
17089 );
17090 }
17091 *accepted_ctr += accepted - 1;
17092 let (hc, dim) = (cfg.hc_mult, cfg.dim);
17097 let dev_caps: Vec<usize> = targets
17102 .iter()
17103 .copied()
17104 .filter(|&t| t < spec_gpu_end)
17105 .collect();
17106 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
17107 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
17108 return None;
17109 }
17110 for t in 0..accepted {
17111 let tip = t + 1 == accepted;
17112 for (slot, &tl) in targets.iter().enumerate() {
17113 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
17114 let lo = (di * b + t) * hc * dim;
17115 crate::dsv4::dspark_capture(
17116 &caps_all[lo..lo + hc * dim],
17117 &cfg,
17118 slot,
17119 &mut ds.main_hidden,
17120 );
17121 } else if tip
17122 && crate::dsv4::dspark_peek_slot(slot, dim, {
17123 let lo = slot * dim;
17124 &mut ds.main_hidden[lo..lo + dim]
17125 })
17126 {
17127 } else {
17132 crate::dsv4::dspark_capture(
17136 &walked[t * hc * dim..(t + 1) * hc * dim],
17137 &cfg,
17138 slot,
17139 &mut ds.main_hidden,
17140 );
17141 }
17142 }
17143 crate::dsv4::dspark_ring_append(
17144 g,
17145 &self.dsv4_mtp,
17146 &cfg,
17147 ds,
17148 next_pos + t,
17149 self.pool.as_deref(),
17150 );
17151 }
17152 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
17153 self.graph_logits = Some(row);
17154 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
17159 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
17160 crate::dsv4::pick_tally_arm();
17161 }
17162 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17163 eprintln!(
17164 "spec_step total {:.1} ms (k={accepted})",
17165 t_all.elapsed().as_secs_f64() * 1e3
17166 );
17167 }
17168 Some((fed[1..accepted].to_vec(), next_pos + accepted))
17169 }
17170
17171 fn dspark_probe(&mut self, position: usize, token_id: u32) {
17172 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
17173 return;
17174 }
17175 let trunk_now = crate::dsv4::pick_tally_take();
17177 crate::dsv4::trunk_freq_note(&trunk_now);
17178 if !trunk_now.is_empty() {
17179 self.dspark_trunk_picks.push(trunk_now);
17180 let keep = crate::dsv4::dspark_block();
17181 if self.dspark_trunk_picks.len() > keep {
17182 self.dspark_trunk_picks.remove(0);
17183 }
17184 }
17185 for p in std::mem::take(&mut self.dspark_pending) {
17188 let Some(i) = position.checked_sub(p.0 + 1) else {
17189 continue;
17190 };
17191 let mut p = p;
17192 if i < p.1.len() {
17193 if p.2 && p.1[i] == token_id {
17194 p.3 = i + 1;
17195 } else {
17196 p.2 = false;
17197 }
17198 if i + 1 < p.1.len() {
17199 self.dspark_pending.push(p);
17200 continue;
17201 }
17202 }
17203 self.dspark_hist.push(p.3);
17204 self.dspark_real.push(token_id);
17205 }
17206 let Some(b) = &mut self.dsv4 else { return };
17207 let (g, layers, cfg) = (&b.0, &b.1, b.2);
17208 let n_layers = layers.len();
17209 if self.dspark.is_none() {
17210 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
17211 if t.is_empty() {
17212 return;
17213 }
17214 eprintln!(
17215 "DSpark: capture from layers {t:?}, block {}",
17216 crate::dsv4::dspark_block()
17217 );
17218 crate::dsv4::dspark_arm(&t, cfg.dim);
17219 self.dspark = Some(crate::dsv4::DsparkState::new(
17220 self.dsv4_mtp.len(),
17221 &cfg,
17222 t.len(),
17223 ));
17224 }
17225 let ds = self.dspark.as_mut().unwrap();
17226 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
17227 return; }
17229 let mut conf = Vec::new();
17230 crate::dsv4::pick_tally_arm();
17231 let draft_started = std::time::Instant::now();
17236 #[cfg(feature = "gpu")]
17237 let gpu_draft = crate::dsv4::dspark_gpu_on();
17238 #[cfg(not(feature = "gpu"))]
17239 let gpu_draft = false;
17240 let props = if gpu_draft {
17241 #[cfg(feature = "gpu")]
17242 {
17243 let kv_id = b.3.kv_id;
17244 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
17245 Some(pk) => crate::dsv4::dspark_draft_gpu(
17246 g,
17247 &self.dsv4_mtp,
17248 &cfg,
17249 ds,
17250 pk,
17251 kv_id,
17252 token_id,
17253 position,
17254 self.pool.as_deref(),
17255 &mut conf,
17256 ),
17257 None => Vec::new(),
17258 }
17259 }
17260 #[cfg(not(feature = "gpu"))]
17261 Vec::new()
17262 } else {
17263 crate::gpu::cpu_scope(|| {
17264 crate::dsv4::dspark_draft(
17265 g,
17266 &self.dsv4_mtp,
17267 &cfg,
17268 ds,
17269 token_id,
17270 position,
17271 self.pool.as_deref(),
17272 &mut conf,
17273 )
17274 })
17275 };
17276 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
17277 let draft_picks = crate::dsv4::pick_tally_take();
17278 crate::dsv4::dspark_freq_note(&draft_picks);
17279 crate::dsv4::pick_tally_arm();
17282 if !props.is_empty() {
17283 let (tu, tt) = {
17287 let flat: Vec<(usize, Vec<usize>)> = self
17288 .dspark_trunk_picks
17289 .iter()
17290 .flat_map(|v| v.iter().cloned())
17291 .collect();
17292 let mut per: std::collections::HashMap<usize, Vec<usize>> =
17294 std::collections::HashMap::new();
17295 for (li, picks) in flat {
17296 per.entry(li).or_default().extend(picks);
17297 }
17298 let n = per.len().max(1);
17299 let mut u = 0usize;
17300 let mut t = 0usize;
17301 for (_, v) in per {
17302 t += v.len();
17303 u += v.iter().collect::<std::collections::HashSet<_>>().len();
17304 }
17305 (u / n, t / n)
17306 };
17307 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
17308 self.dspark_exp.push((tu, tt, du, dt));
17309 self.dspark_pending.push((position, props, true, 0));
17310 }
17311 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
17312 let n = self.dspark_hist.len() as f32;
17313 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
17314 let block = crate::dsv4::dspark_block();
17315 let mut at = vec![0usize; block + 1];
17316 for &k in &self.dspark_hist {
17317 at[k] += 1;
17318 }
17319 let mut surv = Vec::with_capacity(block);
17321 for i in 1..=block {
17322 let k = at[i..].iter().sum::<usize>() as f32 / n;
17323 surv.push(format!("{k:.2}"));
17324 }
17325 let distinct = self
17326 .dspark_real
17327 .iter()
17328 .collect::<std::collections::HashSet<_>>()
17329 .len();
17330 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
17331 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
17332 });
17333 let m = self.dspark_exp.len().max(1);
17334 eprintln!(
17335 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
17336 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
17337 self.dspark_hist.len(),
17338 mean + 1.0,
17339 surv.join(" ")
17340 );
17341 eprintln!(
17342 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
17343 trunk experts {}/{} per layer over {block} tokens, \
17344 draft {}/{} per block, draft {:.2} ms/block",
17345 self.dspark_real.len(),
17346 tu / m,
17347 tt / m,
17348 du / m,
17349 dt / m,
17350 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
17351 );
17352 }
17353 }
17354
17355 fn forward_layers_upto(
17356 &mut self,
17357 hidden: &[f32],
17358 position: usize,
17359 task_mask: Option<&TaskMask>,
17360 upto: Option<usize>,
17361 ) -> Vec<f32> {
17362 if let Some(plan) = self.gpu_plan.clone() {
17368 if upto.is_none() && plan.len() > 1 {
17369 let mut h = hidden.to_vec();
17370 for &(dev, from, upto_incl) in plan.iter() {
17371 h = crate::gpu::with_device(dev, || {
17372 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17373 });
17374 }
17375 return h;
17376 }
17377 }
17378 self.forward_layers_span(hidden, position, task_mask, 0, upto)
17379 }
17380
17381 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17386 self.set_gpu_plan_at(devices, None)
17387 }
17388
17389 pub fn set_gpu_plan_at(
17393 &mut self,
17394 devices: Option<&[usize]>,
17395 at: Option<usize>,
17396 ) -> Result<(), String> {
17397 let Some(devs) = devices.filter(|d| d.len() > 1) else {
17398 self.gpu_plan = None;
17399 return Ok(());
17400 };
17401 self.split_supported()?;
17402 let n = self.num_layers;
17403 if devs.len() > n {
17404 return Err(format!("{} devices for {n} layers", devs.len()));
17405 }
17406 if let Some(k) = at {
17407 if k == 0 || k >= n {
17408 return Err(format!("split at {k}: the model has {n} layers"));
17409 }
17410 if devs.len() == 2 {
17411 self.gpu_plan = Some(std::sync::Arc::new(vec![
17412 (devs[0], 0, k - 1),
17413 (devs[1], k, n - 1),
17414 ]));
17415 return Ok(());
17416 }
17417 return Err(format!(
17418 "an explicit split point takes exactly 2 devices, got {}",
17419 devs.len()
17420 ));
17421 }
17422 let per = n.div_ceil(devs.len());
17423 let mut plan = Vec::with_capacity(devs.len());
17424 let mut from = 0usize;
17425 for &d in devs {
17426 if from >= n {
17427 break;
17428 }
17429 let upto = (from + per - 1).min(n - 1);
17430 plan.push((d, from, upto));
17431 from = upto + 1;
17432 }
17433 self.gpu_plan = Some(std::sync::Arc::new(plan));
17434 Ok(())
17435 }
17436
17437 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17439 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17440 }
17441
17442 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17448 if let Some(x) = t.as_f32() {
17449 return x.to_vec();
17450 }
17451 let mut out = vec![0.0; t.rows() * t.cols()];
17452 for r in 0..t.rows() {
17453 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17454 }
17455 out
17456 }
17457
17458 fn embryo_resident_eligible(&self) -> bool {
17459 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17462 || self.num_layers != self.physical_layers
17463 || self.loop_final_norm
17464 || self.weights.layers.len() != self.num_layers
17465 || self.head_clusters.is_none()
17466 || self.final_softcap.is_some()
17467 || self.logit_multiplier.is_some()
17468 || self.attn_softcap != 0.0
17469 || self.mtp.is_some()
17470 || self.per_token_stack()
17471 || self.dsv4.is_some()
17472 || self.dsv41.is_some()
17473 || self.qwen4_exp.is_some()
17474 || self.dyn_router.is_some()
17481 || self.dyn_phi_layer.is_some()
17482 || self.dyn_blend_loaded
17483 || self.o1_cfg.is_some()
17484 || self.swa.is_some()
17485 || self.sliding_layers.is_some()
17486 || self.global_attn.is_some()
17487 || self.attention_heads_per_layer.is_some()
17488 || self.attn_v_norm
17489 || self
17490 .kv_cache
17491 .layers
17492 .iter()
17493 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17494 || self.rope_scale != 1.0
17495 || self.rope_scale_local != 1.0
17496 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17497 || self.hidden_size == 0
17498 || self.hidden_size > 1024
17499 || self.intermediate_size > 1024
17500 || self.num_heads == 0
17501 || self.num_kv_heads == 0
17502 || self.num_heads % self.num_kv_heads != 0
17503 || self.num_heads.saturating_mul(self.head_dim) > 1024
17504 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17505 || self.vocab_size == 0
17506 || self.kv_cache.max_seq_len == 0
17507 || self.rotary_dim == 0
17508 || self.rotary_dim > self.head_dim
17509 || self.rotary_dim % 2 != 0
17510 || self.inv_freq.len() < self.rotary_dim / 2
17511 {
17512 return false;
17513 }
17514 if self.weights.lm_head.as_f32().is_none()
17525 || self.weights.embed_tokens.as_f32().is_none()
17526 || self.weights.lm_head.rows() < self.vocab_size
17527 || self.weights.lm_head.cols() != self.hidden_size
17528 || self.weights.embed_tokens.rows() < self.vocab_size
17529 || self.weights.embed_tokens.cols() != self.hidden_size
17530 || self.weights.final_norm.len() != self.hidden_size
17531 {
17532 return false;
17533 }
17534 if let Some(cfg) = self.vmf_cfg {
17535 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17536 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17537 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17538 || cfg.state_len() == 0
17539 {
17540 return false;
17541 }
17542 }
17543 if let Some(g) = self.gdn_cfg {
17544 if g.num_v_heads == 0
17549 || g.num_k_heads == 0
17550 || g.num_v_heads % g.num_k_heads != 0
17551 || g.key_head_dim == 0
17552 || g.key_head_dim > 128
17553 || g.value_head_dim == 0
17554 || g.value_head_dim > 256
17555 || g.value_head_dim % 4 != 0
17556 || g.conv_kernel == 0
17557 || g.num_v_heads > 512
17558 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17559 || g.conv_dim() > 2048
17560 || g.conv_dim() % 4 != 0
17561 || g.hidden_size != self.hidden_size
17562 || g.output_gate_sigmoid
17563 || g.rms_eps != self.rms_eps
17564 || g.state_len() == 0
17565 {
17566 return false;
17567 }
17568 }
17569 let mut full_seen = false;
17570 for lw in &self.weights.layers {
17571 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17572 return false;
17573 }
17574 match &lw.attn {
17575 AttnKind::LinearGdn(w) => {
17576 let Some(g) = self.gdn_cfg else {
17577 return false;
17578 };
17579 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17580 if w.in_proj_qkv.rows() != g.conv_dim()
17581 || w.in_proj_qkv.cols() != self.hidden_size
17582 || w.in_proj_qkv.as_f32().is_none()
17583 || w.in_proj_z.rows() != nv * dv
17584 || w.in_proj_z.cols() != self.hidden_size
17585 || w.in_proj_z.as_f32().is_none()
17586 || w.in_proj_a.rows() != nv
17587 || w.in_proj_a.cols() != self.hidden_size
17588 || w.in_proj_a.as_f32().is_none()
17589 || w.in_proj_b.rows() != nv
17590 || w.in_proj_b.cols() != self.hidden_size
17591 || w.in_proj_b.as_f32().is_none()
17592 || w.conv1d.len() != g.conv_dim() * kk
17593 || w.a_log.len() != nv
17594 || w.dt_bias.len() != nv
17595 || w.norm.len() != dv
17596 || w.out_proj.rows() != self.hidden_size
17597 || w.out_proj.cols() != nv * dv
17598 || w.out_proj.as_f32().is_none()
17599 {
17600 return false;
17601 }
17602 }
17603 AttnKind::Linear(w) => {
17604 let Some(cfg) = self.vmf_cfg else {
17605 return false;
17606 };
17607 if w.thq.rows() != cfg.num_heads * cfg.nphase
17608 || w.thq.cols() != self.hidden_size
17609 || w.thq.as_f32().is_none()
17610 || w.thk.rows() != cfg.num_heads * cfg.nphase
17611 || w.thk.cols() != self.hidden_size
17612 || w.thk.as_f32().is_none()
17613 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17614 || w.v_proj.cols() != self.hidden_size
17615 || w.v_proj.as_f32().is_none()
17616 || w.out_proj.rows() != self.hidden_size
17617 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17618 || w.out_proj.as_f32().is_none()
17619 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17620 {
17621 return false;
17622 }
17623 if let Some((kg, kb)) = &w.k_gate {
17624 if kg.rows() != cfg.num_heads
17625 || kg.cols() != self.hidden_size
17626 || kg.as_f32().is_none()
17627 || kb.len() != cfg.num_heads
17628 {
17629 return false;
17630 }
17631 }
17632 if let Some(conv) = &w.conv {
17633 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17634 return false;
17635 }
17636 }
17637 }
17638 AttnKind::Full {
17639 wq,
17640 wk,
17641 wv,
17642 wo,
17643 q_norm,
17644 k_norm,
17645 output_gate,
17646 softplus_gate,
17647 bias,
17648 } => {
17649 if full_seen
17650 || q_norm.is_some()
17651 || k_norm.is_some()
17652 || *output_gate
17653 || softplus_gate.is_some()
17654 || bias.is_some()
17655 || wq.as_f32().is_none()
17656 || wk.as_f32().is_none()
17657 || wv.as_f32().is_none()
17658 || wo.as_f32().is_none()
17659 || wq.rows() != self.num_heads * self.head_dim
17660 || wk.rows() != self.num_kv_heads * self.head_dim
17661 || wv.rows() != self.num_kv_heads * self.head_dim
17662 || wq.cols() != self.hidden_size
17663 || wk.cols() != self.hidden_size
17664 || wv.cols() != self.hidden_size
17665 || wo.rows() != self.hidden_size
17666 || wo.cols() != self.num_heads * self.head_dim
17667 {
17668 return false;
17669 }
17670 full_seen = true;
17671 }
17672 AttnKind::Bounded(w) => {
17673 let Some(ac) = self.anchor_core.as_ref() else {
17677 return false;
17678 };
17679 if self.bounded_rope.is_none()
17680 || w.window != ac.window
17681 || w.sink != ac.sink
17682 || w.group_windows != ac.group_windows
17683 || w.window == 0
17684 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17685 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17686 || w.wq.as_f32().is_none()
17687 || w.wk.as_f32().is_none()
17688 || w.wv.as_f32().is_none()
17689 || w.wo.as_f32().is_none()
17690 || w.wq.rows() != self.num_heads * self.head_dim
17691 || w.wk.rows() != self.num_kv_heads * self.head_dim
17692 || w.wv.rows() != self.num_kv_heads * self.head_dim
17693 || w.wq.cols() != self.hidden_size
17694 || w.wk.cols() != self.hidden_size
17695 || w.wv.cols() != self.hidden_size
17696 || w.wo.rows() != self.hidden_size
17697 || w.wo.cols() != self.num_heads * self.head_dim
17698 {
17699 return false;
17700 }
17701 }
17702 _ => return false,
17703 }
17704 match &lw.ffn {
17705 FfnKind::Dense(d) => {
17706 if d.act != Act::Silu
17707 || !d.segs.is_empty()
17708 || d.gate_proj.as_f32().is_none()
17709 || d.up_proj.as_f32().is_none()
17710 || d.down_proj.as_f32().is_none()
17711 || d.gate_proj.rows() != self.intermediate_size
17712 || d.gate_proj.cols() != self.hidden_size
17713 || d.up_proj.rows() != self.intermediate_size
17714 || d.up_proj.cols() != self.hidden_size
17715 || d.down_proj.rows() != self.hidden_size
17716 || d.down_proj.cols() != self.intermediate_size
17717 {
17718 return false;
17719 }
17720 }
17721 FfnKind::Moe(m) => {
17722 if m.resonance.is_none()
17723 || m.top_k != 1
17724 || m.router_sigmoid
17725 || !m.norm_topk_prob
17726 || m.expert_bias.is_some()
17727 || m.routed_scaling != 1.0
17728 || m.route_tau.is_some()
17729 || m.shared.is_none()
17730 || m.mask.is_some()
17731 || m.per_expert_scale.is_some()
17732 || m.router_input_norm
17733 || m.experts.is_empty()
17734 || m.experts.len() > 8
17735 {
17736 return false;
17737 }
17738 let r = m.resonance.as_ref().unwrap();
17739 if r.mu.len() != m.experts.len() * self.hidden_size
17740 || r.bias.len() != m.experts.len()
17741 || r.u.len() != m.experts.len() * r.k * self.hidden_size
17742 || r.k > 128
17743 {
17744 return false;
17745 }
17746 let Some((shared, gate)) = &m.shared else {
17747 return false;
17748 };
17749 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17750 return false;
17751 }
17752 if shared.gate_proj.as_f32().is_none()
17753 || shared.up_proj.as_f32().is_none()
17754 || shared.down_proj.as_f32().is_none()
17755 || shared.gate_proj.rows() != self.intermediate_size
17756 || shared.gate_proj.cols() != self.hidden_size
17757 || shared.up_proj.rows() != self.intermediate_size
17758 || shared.up_proj.cols() != self.hidden_size
17759 || shared.down_proj.rows() != self.hidden_size
17760 || shared.down_proj.cols() != self.intermediate_size
17761 {
17762 return false;
17763 }
17764 for e in &m.experts {
17765 if e.act != Act::Silu
17766 || !e.segs.is_empty()
17767 || e.gate_proj.as_f32().is_none()
17768 || e.up_proj.as_f32().is_none()
17769 || e.down_proj.as_f32().is_none()
17770 || e.gate_proj.rows() != self.intermediate_size
17771 || e.gate_proj.cols() != self.hidden_size
17772 || e.up_proj.rows() != self.intermediate_size
17773 || e.up_proj.cols() != self.hidden_size
17774 || e.down_proj.rows() != self.hidden_size
17775 || e.down_proj.cols() != self.intermediate_size
17776 {
17777 return false;
17778 }
17779 }
17780 }
17781 FfnKind::DenseMoe(_) => return false,
17782 }
17783 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17784 return false;
17785 }
17786 }
17787 if full_seen && self.anchor_core.is_some() {
17788 return false;
17789 }
17790 full_seen || self.num_layers > 0
17791 }
17792
17793 fn embryo_resident_wanted(&self) -> bool {
17798 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17799 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17800 && matches!(
17801 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17802 Ok("1") | Ok("parallel")
17803 )
17804 && crate::gpu::enabled_here()
17805 && !self.graph_refused()
17806 && self.embryo_resident_eligible()
17807 }
17808
17809 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17817 if ids.len() < 2
17818 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17819 || !self.embryo_resident_wanted()
17820 {
17821 return None;
17822 }
17823 let model = self.ensure_embryo_graph()?;
17824 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17825 .ok()
17826 .and_then(|v| v.parse::<usize>().ok())
17827 .filter(|&v| v >= 1)
17828 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17829 .min(crate::gpu::EMBRYO_CHUNK_MAX);
17830 let hs = self.hidden_size;
17831 let n = ids.len();
17832 let mut pos = start;
17833 let mut last = None;
17834 let mut rows = Vec::with_capacity(cmax * hs);
17835 while pos < n {
17836 let end = (pos + cmax).min(n);
17837 rows.clear();
17838 for &id in &ids[pos..end] {
17839 rows.extend_from_slice(&self.embed_single(id));
17840 }
17841 let mut lg = Vec::new();
17842 if !crate::gpu::forward_embryo_graph_chunk(
17843 &model,
17844 self.graph_kv_id,
17845 &rows,
17846 pos,
17847 end - pos,
17848 &mut lg,
17849 ) {
17850 if pos == start {
17851 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17852 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17853 }
17854 return None;
17855 }
17856 self.kv_cache.clear();
17860 self.clear_history();
17861 crate::gpu::graph_kv_reset(self.graph_kv_id);
17862 panic!(
17863 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17864 );
17865 }
17866 last = Some(lg);
17867 pos = end;
17868 }
17869 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17870 eprintln!(
17871 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17872 n - start,
17873 (n - start).div_ceil(cmax)
17874 );
17875 }
17876 last
17877 }
17878
17879 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17880 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17881 const UMAX: u32 = u32::MAX;
17882 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17883 const REC: usize = 64;
17884 struct Pack {
17885 data: Vec<f32>,
17886 }
17887 impl Pack {
17888 fn put(&mut self, x: &[f32]) -> u32 {
17889 if x.is_empty() {
17890 return u32::MAX;
17891 }
17892 let off = self.data.len();
17893 self.data.extend_from_slice(x);
17894 off as u32
17895 }
17896 }
17897 let vmf = self.vmf_cfg;
17901 let gdn = self.gdn_cfg;
17902 let mut pack = Pack { data: Vec::new() };
17903 let mut meta = vec![0u32; HEADER];
17904 meta[0] = self.hidden_size as u32;
17905 meta[1] = self.intermediate_size as u32;
17906 meta[2] = self.vocab_size as u32;
17907 meta[3] = self.num_layers as u32;
17908 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17909 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17910 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17911 if let Some(g) = gdn {
17912 meta[24] = g.num_v_heads as u32;
17913 meta[25] = g.num_k_heads as u32;
17914 meta[26] = g.key_head_dim as u32;
17915 meta[27] = g.value_head_dim as u32;
17916 meta[28] = g.conv_kernel as u32;
17917 meta[29] = g.conv_dim() as u32;
17918 }
17919 meta[7] = self.num_heads as u32;
17920 meta[8] = self.num_kv_heads as u32;
17921 meta[9] = self.head_dim as u32;
17922 meta[10] = self.kv_cache.max_seq_len as u32;
17923 let clusters = self.head_clusters.as_ref().unwrap();
17924 let cluster_count = clusters.len() / self.hidden_size;
17925 if clusters.len() % self.hidden_size != 0
17926 || cluster_count == 0
17927 || cluster_count > 1024
17928 || self.vocab_size % cluster_count != 0
17929 || self.weights.lm_head.rows() < self.vocab_size
17930 || self.weights.final_norm.len() != self.hidden_size
17931 {
17932 return None;
17933 }
17934 meta[11] = cluster_count as u32;
17935 meta[12] = (self.vocab_size / cluster_count) as u32;
17936 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17937 meta[16] = self.rotary_dim as u32;
17938 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17939 meta[19] = (self.rms_eps as f32).to_bits();
17940 let max_conv = self
17941 .weights
17942 .layers
17943 .iter()
17944 .filter_map(|lw| match &lw.attn {
17945 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17946 _ => None,
17947 })
17948 .max()
17949 .unwrap_or(1);
17950 let phase_stride = vmf
17956 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17957 .unwrap_or(0);
17958 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17959 let state_stride = phase_stride.max(gdn_stride);
17960 let bounded = self.anchor_core.clone();
17964 let (anchor_window, anchor_sink) = bounded
17965 .as_ref()
17966 .map(|ac| (ac.window, ac.sink))
17967 .unwrap_or((0, 0));
17968 let kv_stride = if bounded.is_some() {
17969 2usize
17970 .saturating_mul(self.num_kv_heads)
17971 .saturating_mul(anchor_window)
17972 .saturating_mul(self.head_dim)
17973 } else {
17974 2usize
17975 .saturating_mul(self.num_kv_heads)
17976 .saturating_mul(self.kv_cache.max_seq_len)
17977 .saturating_mul(self.head_dim)
17978 };
17979 meta[14] = state_stride as u32;
17980 meta[15] = kv_stride as u32;
17981 meta[18] = anchor_window as u32;
17982 meta[20] = anchor_sink as u32;
17983 meta[21] = match &self.bounded_rope {
17984 Some(rope) => {
17985 let off = pack.put(&rope.cos);
17987 let _ = pack.put(&rope.sin);
17988 off
17989 }
17990 None => UMAX,
17991 };
17992 meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
17995 Some(ac) => {
17996 let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
17997 pack.put(&gw)
17998 }
17999 None => UMAX,
18000 };
18001 let mut full_seen = false;
18002 let mut bounded_seen = 0usize;
18003 let mut phase_seen = 0usize;
18007 let mut gdn_seen = 0usize;
18008 for (li, lw) in self.weights.layers.iter().enumerate() {
18009 let base = meta.len();
18010 meta.resize(base + REC, UMAX);
18011 meta[base] = match &lw.attn {
18012 AttnKind::Linear(w) if w.phase_delta => 1,
18013 AttnKind::Linear(_) => 0,
18014 AttnKind::Full { .. } => 2,
18015 AttnKind::Bounded(_) => 3,
18016 AttnKind::LinearGdn(_) => 4,
18017 _ => UMAX,
18018 };
18019 meta[base + 1] = pack.put(&lw.input_norm);
18020 meta[base + 2] = pack.put(&lw.post_norm);
18021 meta[base + 25] = match &lw.attn {
18022 AttnKind::Linear(_) => {
18023 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18024 phase_seen += 1;
18025 off
18026 }
18027 AttnKind::LinearGdn(_) => {
18028 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18029 gdn_seen += 1;
18030 off
18031 }
18032 _ => UMAX,
18033 };
18034 match &lw.attn {
18035 AttnKind::LinearGdn(w) => {
18036 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
18039 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
18040 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
18041 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
18042 meta[base + 60] = pack.put(&w.conv1d);
18043 meta[base + 61] = pack.put(&w.a_log);
18044 meta[base + 62] = pack.put(&w.dt_bias);
18045 meta[base + 63] = pack.put(&w.norm);
18046 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18047 meta[base + 24] = 0;
18048 }
18049 AttnKind::Linear(w) => {
18050 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
18051 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
18052 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
18053 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18054 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
18055 meta[base + 7] = pack.put(&decay);
18056 if let Some((kg, kb)) = &w.k_gate {
18057 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
18058 meta[base + 9] = pack.put(kb);
18059 }
18060 if let Some(conv) = &w.conv {
18061 meta[base + 10] = pack.put(conv);
18062 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
18063 } else {
18064 meta[base + 24] = 0;
18065 }
18066 }
18067 AttnKind::Full { wq, wk, wv, wo, .. } => {
18068 full_seen = true;
18069 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
18070 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
18071 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
18072 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
18073 meta[base + 26] = (li * kv_stride) as u32;
18074 }
18075 AttnKind::Bounded(w) => {
18076 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
18077 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
18078 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
18079 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
18080 meta[base + 26] = (bounded_seen * kv_stride) as u32;
18082 meta[base + 27] = pack.put(&w.sink_k);
18083 meta[base + 28] = pack.put(&w.sink_v);
18084 bounded_seen += 1;
18085 }
18086 _ => return None,
18087 }
18088 match &lw.ffn {
18089 FfnKind::Dense(d) => {
18090 meta[base + 15] = 0;
18091 meta[base + 16] = 0;
18092 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
18093 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
18094 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
18095 }
18096 FfnKind::Moe(m) => {
18097 let r = m.resonance.as_ref().unwrap();
18098 let (shared, _) = m.shared.as_ref().unwrap();
18099 meta[base + 15] = 1;
18100 meta[base + 16] = m.experts.len() as u32;
18101 meta[base + 17] = pack.put(&r.mu);
18102 meta[base + 18] = pack.put(&r.u);
18103 meta[base + 19] = pack.put(&r.bias);
18104 meta[base + 20] = r.k as u32;
18105 let mut shell = r.effective_shell(m.experts.len());
18114 shell.push(f32::NEG_INFINITY);
18115 meta[base + 30] = pack.put(&shell);
18116 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
18117 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
18118 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
18119 for (e, ex) in m.experts.iter().enumerate() {
18120 meta[base + 32 + e * 3] =
18121 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
18122 meta[base + 33 + e * 3] =
18123 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
18124 meta[base + 34 + e * 3] =
18125 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
18126 }
18127 }
18128 FfnKind::DenseMoe(_) => return None,
18129 }
18130 }
18131 if !full_seen && self.num_layers == 0 {
18132 return None;
18133 }
18134 let id = {
18135 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
18136 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
18137 };
18138 let model = crate::gpu::EmbryoGraphModel {
18139 id,
18140 hidden: self.hidden_size,
18141 intermediate: self.intermediate_size,
18142 vocab: self.vocab_size,
18143 layers: self.num_layers,
18144 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
18145 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
18146 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
18147 anchor_q_heads: self.num_heads,
18148 anchor_kv_heads: self.num_kv_heads,
18149 anchor_head_dim: self.head_dim,
18150 rotary_dim: self.rotary_dim,
18151 max_seq: self.kv_cache.max_seq_len,
18152 cluster_count,
18153 cluster_size: self.vocab_size / cluster_count,
18154 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
18155 state_stride,
18156 kv_stride,
18157 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
18158 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
18159 weights: pack.data,
18160 meta,
18161 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
18162 clusters: clusters.as_ref().clone(),
18163 final_norm: self.weights.final_norm.clone(),
18164 inv_freq: self.inv_freq.as_ref().clone(),
18165 bounded: bounded.is_some(),
18166 kv_layers: if bounded.is_some() {
18167 bounded_seen
18168 } else {
18169 self.num_layers
18170 },
18171 state_layers: phase_seen + gdn_seen,
18172 anchor_window,
18173 anchor_sink,
18174 phase_layers: phase_seen,
18175 gdn_layers: gdn_seen,
18176 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
18177 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
18178 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
18179 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
18180 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
18181 };
18182 self.embryo_graph = Some(std::sync::Arc::new(model));
18183 }
18184 self.embryo_graph.clone()
18185 }
18186
18187 fn forward_layers_span(
18188 &mut self,
18189 hidden: &[f32],
18190 position: usize,
18191 task_mask: Option<&TaskMask>,
18192 from: usize,
18193 upto: Option<usize>,
18194 ) -> Vec<f32> {
18195 #[cfg(target_os = "macos")]
18196 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
18197 position,
18198 from,
18199 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
18200 );
18201 debug_assert!(
18202 from == 0
18203 || (self.dsv4.is_none()
18204 && self.dsv41.is_none()
18205 && self.qwen4_exp.is_none()
18206 && !self.per_token_stack())
18207 );
18208 #[cfg(target_os = "macos")]
18214 if !crate::gpu_metal::wait_replay() {
18215 self.fail_metal_graph("the pending async replay failed before a plain forward");
18216 return vec![0.0; self.hidden_size];
18217 }
18218 if let Some(b) = &mut self.qwen4_exp {
18219 let _ = (task_mask, upto);
18220 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18221 let mut logits = Vec::new();
18222 crate::qwen4_exp::forward_token(
18223 &b.0,
18224 &b.1,
18225 &b.2,
18226 &mut b.3,
18227 token_id,
18228 position,
18229 &self.inv_freq,
18230 self.pool.as_deref(),
18231 &mut logits,
18232 true,
18233 );
18234 self.graph_logits = Some(logits);
18235 return vec![0.0; self.hidden_size];
18236 }
18237 if let Some(b) = &mut self.dsv4 {
18243 let _ = (task_mask, upto);
18244 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18245 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
18246 st.pos = position;
18247 let mut logits = Vec::new();
18248 crate::dsv4::forward_token(
18249 g,
18250 layers,
18251 &cfg,
18252 st,
18253 token_id,
18254 &self.inv_freq,
18255 self.pool.as_deref(),
18256 &mut logits,
18257 );
18258 self.graph_logits = Some(logits);
18259 self.dspark_probe(position, token_id);
18260 return vec![0.0; self.hidden_size];
18263 }
18264 if let Some(b) = &mut self.dsv41 {
18266 let _ = (task_mask, upto);
18267 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18268 let mut logits = Vec::new();
18269 crate::dsv41::forward_token(
18270 &b.0,
18271 &b.1,
18272 &b.2,
18273 &mut b.3,
18274 token_id,
18275 position,
18276 self.pool.as_deref(),
18277 &mut logits,
18278 );
18279 self.graph_logits = Some(logits);
18280 return vec![0.0; self.hidden_size];
18281 }
18282 if let Some(b) = &self.g4e {
18288 let _ = (task_mask, upto);
18289 return crate::g4e::g4e_forward(
18290 &b.0,
18291 &b.1,
18292 hidden,
18293 position,
18294 &mut self.kv_cache.layers,
18295 self.num_heads,
18296 self.num_kv_heads,
18297 self.pool.as_deref(),
18298 );
18299 }
18300 if let Some(b) = &self.g3n {
18301 let _ = (task_mask, upto);
18302 return crate::g3n::g3n_forward(
18303 &b.0,
18304 &b.1,
18305 hidden,
18306 position,
18307 &mut self.kv_cache.layers,
18308 self.num_heads,
18309 self.num_kv_heads,
18310 self.head_dim,
18311 self.pool.as_deref(),
18312 );
18313 }
18314 if from == 0
18321 && upto.is_none()
18322 && task_mask.is_none()
18323 && self.anchor_core.is_some()
18324 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
18325 {
18326 static ONCE: std::sync::Once = std::sync::Once::new();
18327 ONCE.call_once(|| {
18328 eprintln!(
18329 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
18330 unsupported={} eligible={}",
18331 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
18332 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
18333 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
18334 crate::gpu::enabled_here(),
18335 self.graph_refused(),
18336 self.embryo_resident_eligible(),
18337 );
18338 });
18339 }
18340 if from == 0
18341 && upto.is_none()
18342 && task_mask.is_none()
18343 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
18347 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
18348 && matches!(
18352 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
18353 Ok("1") | Ok("parallel")
18354 )
18355 && crate::gpu::enabled_here()
18356 && !self.graph_refused()
18357 && (position == 0 || self.device_sequence_position().is_some())
18363 && self.embryo_resident_eligible()
18364 && let Some(model) = self.ensure_embryo_graph()
18365 {
18366 let mut lg = Vec::new();
18367 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
18368 {
18369 self.graph_logits = Some(lg);
18370 return vec![0.0; self.hidden_size];
18371 }
18372 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
18373 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
18374 }
18375 self.mark_graph_refused();
18381 if position != 0 {
18382 self.kv_cache.clear();
18386 self.clear_history();
18387 crate::gpu::graph_kv_reset(self.graph_kv_id);
18388 panic!(
18389 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18390 );
18391 }
18392 }
18393 let mut h = hidden.to_vec();
18394 self.mimo_moe_prepare();
18397 let _mimo_q8 = self
18398 .mimo_moe
18399 .is_on()
18400 .then(crate::qtensor::enter_full_gpu_q8_scope);
18401 let (nh, _nkv, _hd, hs, _rd, eps) = (
18404 self.num_heads,
18405 self.num_kv_heads,
18406 self.head_dim,
18407 self.hidden_size,
18408 self.rotary_dim,
18409 self.rms_eps,
18410 );
18411 let pool = self.pool.clone();
18412 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18424 let graph_on = match graph_env.as_deref() {
18425 Some("0") => false,
18426 Some("prefill") => false, Some(_) => true,
18428 None => crate::gpu::wgpu_graph_default(),
18434 };
18435 let graph_trusted =
18436 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18437 let race_eligible =
18438 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18439 let mut tail_start = 0usize;
18440 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18441 let t_graph = std::time::Instant::now();
18442 let mut lg = Vec::new();
18443 let mut gl = 0usize;
18444 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18445 let declined = built.is_none();
18446 let built = match built {
18447 Some(Ok(hh)) => Some(hh),
18448 Some(Err(())) => {
18449 self.clear_sequence_state();
18453 self.graph_failed
18454 .store(true, std::sync::atomic::Ordering::Relaxed);
18455 self.cancel
18456 .store(true, std::sync::atomic::Ordering::Relaxed);
18457 tracing::error!("token graph failed after admission; sequence state cleared");
18458 return vec![0.0; self.hidden_size];
18459 }
18460 None => None,
18461 };
18462 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18467 self.mark_graph_refused();
18468 }
18469 graph_note(built.is_some(), gl, self.num_layers);
18470 if let Some(hh) = built {
18471 let dur = t_graph.elapsed();
18472 if std::env::var("CMF_GRAPH_PROF").is_ok() {
18473 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18474 }
18475 if gl > 0 && gl < self.num_layers {
18476 h = hh;
18482 tail_start = gl;
18483 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18484 if !graph_trusted {
18485 crate::gpu::graph_race_record(true, dur);
18486 }
18487 if !lg.is_empty() {
18488 lg.resize(self.vocab_size, 0.0);
18491 if let Some(c) = self.final_softcap {
18492 softcap_in_place(&mut lg, c, self.pool.as_deref());
18493 }
18494 self.graph_logits = Some(lg);
18495 }
18496 return hh;
18497 }
18498 }
18504 }
18505 let span = from > 0 || upto.is_some();
18529 if span && graph_on && task_mask.is_none() && graph_trusted {
18530 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18531 let mut lg = Vec::new();
18532 let mut gl = 0usize;
18533 let span_res =
18534 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18535 let span_res = match span_res {
18536 Some(Ok(hh)) => Some(hh),
18537 Some(Err(())) => {
18538 self.clear_sequence_state();
18539 self.graph_failed
18540 .store(true, std::sync::atomic::Ordering::Relaxed);
18541 self.cancel
18542 .store(true, std::sync::atomic::Ordering::Relaxed);
18543 tracing::error!(
18544 "span token graph failed after admission; sequence state cleared"
18545 );
18546 return vec![0.0; self.hidden_size];
18547 }
18548 None => None,
18549 };
18550 graph_note(span_res.is_some(), gl, upto_excl - from);
18551 if std::env::var("CMF_GPU_DEBUG").is_ok() {
18552 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18556 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18557 eprintln!(
18558 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18559 upto_excl - from,
18560 span_res.is_some()
18561 );
18562 }
18563 }
18564 if let Some(hh) = span_res {
18565 if gl == upto_excl - from {
18566 if !lg.is_empty() {
18567 lg.resize(self.vocab_size, 0.0);
18568 if let Some(c) = self.final_softcap {
18569 softcap_in_place(&mut lg, c, self.pool.as_deref());
18570 }
18571 self.graph_logits = Some(lg);
18572 }
18573 crate::gpu::set_layer(-1);
18574 return hh;
18575 }
18576 h = hh;
18578 tail_start = from + gl;
18579 }
18580 }
18581 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18586
18587 let host_tail = tail_start > from;
18597 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18598 let automatic_gpu_prefix = self.automatic_gpu_prefix();
18599
18600 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18601 #[cfg(target_os = "macos")]
18602 let mut gpu_skip_until = 0usize;
18603 for li in tail_start.max(from)..self.num_layers {
18604 let _capacity_tail = automatic_gpu_prefix
18605 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18606 .map(|_| crate::gpu::enter_cpu_scope());
18607 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
18609 if li > u {
18610 break;
18611 }
18612 }
18613 if let Some(mask) = task_mask {
18614 if !mask.layer_alive(li) {
18615 continue; }
18617 }
18618 if host_tail && task_mask.is_none() {
18623 match self.m3_pooled_layer(li, position, &mut h) {
18624 Ok(true) => {
18625 crate::gpu::set_layer(-1);
18626 continue;
18627 }
18628 Ok(false) => {}
18629 Err(()) => {
18630 self.clear_sequence_state();
18631 self.graph_failed
18632 .store(true, std::sync::atomic::Ordering::Relaxed);
18633 self.cancel
18634 .store(true, std::sync::atomic::Ordering::Relaxed);
18635 tracing::error!(
18636 "pooled tail token graph failed after admission; sequence state cleared"
18637 );
18638 return vec![0.0; self.hidden_size];
18639 }
18640 }
18641 }
18642 #[cfg(target_os = "macos")]
18646 {
18647 if li < gpu_skip_until {
18648 continue;
18649 }
18650 if task_mask.is_none() {
18651 let end = self.q1_graph_gpu(li, upto, position, &mut h);
18652 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18653 return vec![0.0; self.hidden_size];
18657 }
18658 if end > li {
18659 gpu_skip_until = end;
18660 if self.is_loop_end(end - 1) && end < self.num_layers {
18663 h = inference::rms_norm(
18664 &h,
18665 &self.weights.final_norm,
18666 self.rms_eps,
18667 self.norm_style,
18668 );
18669 }
18670 continue;
18671 }
18672 }
18673 }
18674
18675 if task_mask.is_none() {
18676 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18677 crate::gpu::BatchGraphOutcome::Completed => continue,
18678 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18679 crate::gpu::BatchGraphOutcome::Declined => {}
18680 }
18681 }
18682 #[cfg(feature = "gpu")]
18683 self.pull_lagging_host_kv(li, li + 1, position);
18684 let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
18687 let lw = &self.weights.layers[self.phys_layer(li)];
18688 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18689 if tp.parse::<usize>().ok() == Some(position) {
18690 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18691 eprintln!(
18692 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18693 h[0], h[1]
18694 );
18695 }
18696 }
18697 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18700 inference::rms_norm_into(
18701 &h,
18702 &lw.input_norm,
18703 self.rms_eps,
18704 self.norm_style,
18705 &mut self.ws.n1,
18706 );
18707 drop(prof);
18708
18709 let attn_out = match &lw.attn {
18710 AttnKind::Mla(w) => {
18711 let inv_freq_l = self.layer_inv_freq(li);
18712 let rs = self.layer_rope_scale(li);
18713 let eps = self.rms_eps;
18714 let pool = self.pool.clone();
18715 mla_attention(
18716 w,
18717 &self.ws.n1,
18718 &mut self.kv_cache.layers[li],
18719 position,
18720 &inv_freq_l,
18721 rs,
18722 eps,
18723 pool.as_deref(),
18724 )
18725 }
18726 AttnKind::Linear(w) => {
18727 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18728 vmf_phase_forward(
18729 &self.ws.n1,
18730 w,
18731 &cfg,
18732 &mut self.kv_cache.layers[li].linear_state,
18733 self.pool.as_deref(),
18734 )
18735 }
18736 AttnKind::Kda(w) => {
18737 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18738 crate::linear_core::kda_forward(
18739 &self.ws.n1,
18740 w,
18741 &cfg,
18742 &mut self.kv_cache.layers[li].linear_state,
18743 self.pool.as_deref(),
18744 )
18745 }
18746 AttnKind::LinearGdn(w) => {
18747 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18748 gdn_forward(
18749 &self.ws.n1,
18750 w,
18751 &cfg,
18752 &mut self.kv_cache.layers[li].linear_state,
18753 self.pool.as_deref(),
18754 )
18755 }
18756 AttnKind::ShortConv(w) => {
18757 let cfg = self
18758 .short_conv_cfg
18759 .expect("short-conv layer without short_conv_cfg");
18760 short_conv_forward(
18761 &self.ws.n1,
18762 w,
18763 &cfg,
18764 &mut self.kv_cache.layers[li].linear_state,
18765 self.pool.as_deref(),
18766 )
18767 }
18768 AttnKind::Bounded(w) => {
18769 let rope = self
18772 .bounded_rope
18773 .clone()
18774 .expect("bounded layer without an installed rotation table");
18775 let cfg = crate::bounded::BoundedAttnCfg {
18776 num_heads: self.num_heads,
18777 num_kv_heads: self.num_kv_heads,
18778 head_dim: self.head_dim,
18779 hidden_size: hs,
18780 scale: self.attn_scale,
18781 rope: &rope,
18782 pool: pool.as_deref(),
18783 };
18784 crate::bounded::bounded_attention(
18785 &self.ws.n1,
18786 w,
18787 &mut self.kv_cache.layers[li],
18788 &cfg,
18789 )
18790 }
18791 AttnKind::Full {
18792 wq,
18793 wk,
18794 wv,
18795 wo,
18796 q_norm,
18797 k_norm,
18798 output_gate,
18799 softplus_gate,
18800 bias,
18801 } if self.kv_cache.layers[li].o1_sealed() => {
18802 let inv_freq_l = self.layer_inv_freq(li);
18805 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18806 let cfg = QwenAttnCfg {
18807 mrope: None,
18808 num_heads: self.layer_num_heads(li),
18809 num_kv_heads: nkv_l,
18810 head_dim: hd_l,
18811 hidden_size: hs,
18812 position,
18813 inv_freq: &inv_freq_l,
18814 rotary_dim: rd_l,
18815 scale: self.attn_scale,
18816 softcap: self.attn_softcap,
18817 window: None,
18818 v_norm: self.attn_v_norm,
18819 qk_norm_after_rope: self.qk_norm_after_rope,
18820 gate_sigmoid: self.proj_gate_sigmoid,
18821 q_norm: q_norm.as_deref(),
18822 k_norm: k_norm.as_deref(),
18823 output_gate: *output_gate,
18824 softplus_gate: softplus_gate
18825 .as_ref()
18826 .map(|(gate, per_head)| (gate, *per_head)),
18827 rope_scale: self.layer_rope_scale(li),
18828 bias: bias
18829 .as_ref()
18830 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18831 rms_eps: eps,
18832 norm_style: self.norm_style,
18833 pool: pool.as_deref(),
18834 v_head_dim: self.layer_v_dim(li),
18835 };
18836 attention::qwen_attention_nystrom(
18837 &self.ws.n1,
18838 wq,
18839 wk,
18840 wv,
18841 wo,
18842 &mut self.kv_cache.layers[li],
18843 &cfg,
18844 )
18845 }
18846 AttnKind::Full {
18847 wq,
18848 wk,
18849 wv,
18850 wo,
18851 q_norm,
18852 k_norm,
18853 output_gate,
18854 softplus_gate,
18855 bias,
18856 } => 'attn: {
18857 let dropin_reason =
18862 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18863 if let Some(reason) = dropin_reason {
18864 self.note_graph_decline("wgpu attn dropin", reason);
18865 }
18866 if graph_on
18867 && dropin_reason.is_none()
18868 && !*output_gate
18869 && softplus_gate.is_none()
18870 && self.attention_heads_per_layer.is_none()
18871 && bias.is_none()
18872 && task_mask.is_none()
18873 {
18874 let inv_freq_l = self.layer_inv_freq(li);
18875 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18876 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18877 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18878 wq.mapped_q1(),
18879 wk.mapped_q1(),
18880 wv.mapped_q1(),
18881 wo.mapped_q1(),
18882 ) {
18883 let gm = gm.clone();
18884 let mut out = vec![0f32; hs];
18885 let cache = &self.kv_cache.layers[li];
18886 if crate::gpu::attn_dropin(
18887 &gm,
18888 self.graph_kv_id,
18889 li,
18890 &self.ws.n1,
18891 qi,
18892 ki,
18893 vi,
18894 oi,
18895 q_norm.as_deref(),
18896 k_norm.as_deref(),
18897 self.qk_norm_after_rope,
18898 &inv_freq_l,
18899 nh,
18900 nkv_l,
18901 hd_l,
18902 rd_l,
18903 hs,
18904 position,
18905 self.kv_cache.max_seq_len,
18906 gemma,
18907 eps as f32,
18908 cache.k_heads(),
18909 cache.v_heads(),
18910 &mut out,
18911 ) {
18912 break 'attn out;
18913 }
18914 }
18915 }
18916 let masked = task_mask
18917 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18918 .unwrap_or(false);
18919 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18920 let plain = self.layer_attn_plain(li);
18923 match (masked, f32_view) {
18924 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18927 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18928 attention::multi_head_attention(
18929 &self.ws.n1,
18930 q,
18931 k,
18932 v,
18933 o,
18934 &mut self.kv_cache.layers[li],
18935 self.num_heads,
18936 self.num_kv_heads,
18937 self.head_dim,
18938 self.hidden_size,
18939 position,
18940 &active_heads,
18941 &self.inv_freq,
18942 )
18943 }
18944 (masked, _) => {
18945 if masked {
18946 tracing::warn!(
18947 "layer {li}: head mask on quantized weights or on a \
18948 window/sink/per-layer-geometry layer not supported \
18949 yet — executing dense"
18950 );
18951 }
18952 let inv_freq_l = self.layer_inv_freq(li);
18953 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18954 let cfg = QwenAttnCfg {
18955 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
18956 num_heads: self.layer_num_heads(li),
18957 num_kv_heads: nkv_l,
18958 head_dim: hd_l,
18959 hidden_size: hs,
18960 position,
18961 inv_freq: &inv_freq_l,
18962 rotary_dim: rd_l,
18963 scale: self.attn_scale,
18964 softcap: self.attn_softcap,
18965 window: self.layer_window(li),
18966 v_norm: self.attn_v_norm,
18967 qk_norm_after_rope: self.qk_norm_after_rope,
18968 gate_sigmoid: self.proj_gate_sigmoid,
18969 q_norm: q_norm.as_deref(),
18970 k_norm: k_norm.as_deref(),
18971 output_gate: *output_gate,
18972 softplus_gate: softplus_gate
18973 .as_ref()
18974 .map(|(gate, per_head)| (gate, *per_head)),
18975 rope_scale: self.layer_rope_scale(li),
18976 bias: bias
18977 .as_ref()
18978 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18979 rms_eps: eps,
18980 norm_style: self.norm_style,
18981 pool: pool.as_deref(),
18982 v_head_dim: self.layer_v_dim(li),
18983 };
18984 attention::qwen_attention(
18985 &self.ws.n1,
18986 wq,
18987 wk,
18988 wv,
18989 wo,
18990 &mut self.kv_cache.layers[li],
18991 &cfg,
18992 )
18993 }
18994 }
18995 }
18996 };
18997 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
19000 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
19001 None => attn_out,
19002 };
19003 let mhc_ffn = match mhc_attn {
19004 Some(c) => {
19005 self.mhc_post(li, false, &attn_out, &c, &mut h);
19008 let cf = self.mhc_pre(li, true, &mut h);
19009 let lw = &self.weights.layers[self.phys_layer(li)];
19010 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
19011 Some(cf)
19012 }
19013 None => {
19014 let lw = &self.weights.layers[self.phys_layer(li)];
19015 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
19016 inference::add_rmsnorm_fused_into(
19017 &mut h,
19018 &attn_out,
19019 &lw.post_norm,
19020 self.rms_eps,
19021 self.norm_style,
19022 &mut self.ws.p1,
19023 );
19024 drop(prof);
19025 None
19026 }
19027 };
19028 let lw = &self.weights.layers[self.phys_layer(li)];
19029 let mut attn_out = attn_out;
19030 attention::recycle_buf(&mut attn_out);
19031 let post_normed = &self.ws.p1;
19032
19033 let ffn_masked = task_mask
19034 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
19035 .unwrap_or(false);
19036 let ffn_out = match (ffn_masked, &lw.ffn) {
19048 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
19052 let row = task_mask
19053 .and_then(|tm| tm.ffn_masks.get(li))
19054 .map(|v| v.as_slice());
19055 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
19056 }
19057 (true, FfnKind::Dense(d)) => {
19058 let tm = task_mask.unwrap();
19059 let alive = tm.ffn_active_count(li);
19060 let deep = alive * 2 <= self.intermediate_size;
19061 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
19062 let active = tm.ffn_active_indices(li);
19063 sparse_ffn_quant(
19064 d,
19065 post_normed,
19066 &active,
19067 self.hidden_size,
19068 self.pool.as_deref(),
19069 )
19070 } else if deep
19071 && let (Some(g), Some(u), Some(dn)) = (
19072 d.gate_proj.as_f32(),
19073 d.up_proj.as_f32(),
19074 d.down_proj.as_f32(),
19075 )
19076 {
19077 let active = tm.ffn_active_indices(li);
19078 inference::sparse_ffn_forward(
19079 post_normed,
19080 g,
19081 u,
19082 dn,
19083 self.hidden_size,
19084 self.intermediate_size,
19085 &active,
19086 self.pool.as_deref(),
19087 )
19088 } else {
19089 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
19090 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
19091 }
19092 }
19093 (true, FfnKind::Moe(m)) => {
19094 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
19098 ffn_forward(
19099 &lw.ffn,
19100 post_normed,
19101 self.pool.as_deref(),
19102 allowed.as_deref(),
19103 )
19104 }
19105 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
19106 dm,
19107 post_normed,
19108 &h,
19109 self.rms_eps,
19110 self.norm_style,
19111 self.pool.as_deref(),
19112 ),
19113 (false, _) => match &lw.ffn {
19114 FfnKind::DenseMoe(dm) => dense_moe_ffn(
19115 dm,
19116 post_normed,
19117 &h,
19118 self.rms_eps,
19119 self.norm_style,
19120 self.pool.as_deref(),
19121 ),
19122 FfnKind::Moe(m)
19123 if task_mask.is_none()
19124 && host_tail
19125 && li >= tail_start
19126 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
19127 {
19128 moe_ffn_minicpm_tail(
19129 &mut self.minicpm_tail,
19130 li,
19131 self.num_layers,
19132 m,
19133 post_normed,
19134 self.pool.as_deref(),
19135 )
19136 }
19137 FfnKind::Moe(m)
19138 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
19139 {
19140 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
19141 }
19142 _ => {
19143 let allowed = match (&lw.ffn, task_mask) {
19144 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
19145 _ => None,
19146 };
19147 ffn_forward(
19148 &lw.ffn,
19149 post_normed,
19150 self.pool.as_deref(),
19151 allowed.as_deref(),
19152 )
19153 }
19154 },
19155 };
19156 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
19157 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
19158 None => ffn_out,
19159 };
19160 match mhc_ffn {
19161 Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
19162 None => {
19163 for (i, &f) in ffn_out.iter().enumerate() {
19164 h[i] += f;
19165 }
19166 }
19167 }
19168 let mut ffn_out = ffn_out;
19169 attention::recycle_buf(&mut ffn_out);
19170
19171 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
19173 for v in h.iter_mut() {
19174 *v *= sc;
19175 }
19176 }
19177 if self.layer_dump.is_some() {
19179 self.dump_layer_row(position, li, &h);
19180 }
19181
19182 if self.is_loop_end(li) && li + 1 < self.num_layers {
19185 h = inference::rms_norm(
19186 &h,
19187 &self.weights.final_norm,
19188 self.rms_eps,
19189 self.norm_style,
19190 );
19191 }
19192
19193 if self.dyn_phi_layer == Some(li) {
19197 self.update_dyn_phi(&h);
19198 }
19199 }
19200 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
19202 crate::gpu::graph_race_record(false, t.elapsed());
19203 }
19204
19205 h
19206 }
19207
19208 fn update_dyn_phi(&mut self, h: &[f32]) {
19211 const A: f32 = 0.2;
19212 if self.dyn_phi_ema.len() != h.len() {
19213 self.dyn_phi_ema = vec![0.0; h.len()];
19214 self.dyn_phi_seen = 0;
19215 }
19216 if self.dyn_phi_seen == 0 {
19217 self.dyn_phi_ema.copy_from_slice(h);
19218 } else {
19219 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
19220 *e = (1.0 - A) * *e + A * v;
19221 }
19222 }
19223 self.dyn_phi_seen += 1;
19224 }
19225
19226 pub fn dyn_phi(&self) -> &[f32] {
19228 &self.dyn_phi_ema
19229 }
19230
19231 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
19233 self.dyn_phi_layer = layer;
19234 self.dyn_phi_ema.clear();
19235 self.dyn_phi_seen = 0;
19236 }
19237
19238 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
19240 let Some(model) = &self.model else {
19241 return Vec::new();
19242 };
19243 model
19244 .header
19245 .skills
19246 .iter()
19247 .enumerate()
19248 .filter_map(|(i, sk)| {
19249 if sk.is_v2() {
19253 return None;
19254 }
19255 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
19256 let sel = sk.selection.as_ref()?;
19257 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
19258 })
19259 .collect()
19260 }
19261
19262 pub fn active_skill(&self) -> Option<usize> {
19264 self.dyn_active
19265 }
19266
19267 pub fn enable_dynamic_routing(&mut self) -> usize {
19272 use crate::swarm::{DynRouter, RoutableSkill};
19273 let Some(model) = self.model.clone() else {
19274 return 0;
19275 };
19276 if let Some(r) = &model.header.router {
19283 tracing::warn!(
19284 "dynamic routing disabled: this file declares router policy '{}' with \
19285 granularity \"{}\" — the request-level decision applies instead",
19286 r.policy,
19287 r.granularity
19288 );
19289 return 0;
19290 }
19291 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
19297 || model.header.skills.iter().any(|s| s.is_v2())
19298 {
19299 tracing::warn!(
19300 "dynamic routing disabled: this file carries format-v2 skill records \
19301 (SKILLS_V2) — they route per request through a router policy only"
19302 );
19303 return 0;
19304 }
19305 if self.dyn_blend_loaded {
19308 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
19309 return 0;
19310 }
19311 if let Some(a) = self.dyn_active {
19315 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
19316 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
19317 return 0;
19318 }
19319 }
19320 let hidden = self.hidden_size;
19321 let mut skills = Vec::new();
19322 for (idx, id, _phi) in self.dynamic_skills() {
19323 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
19324 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
19325 skills.push(rs);
19326 }
19327 }
19328 }
19329 if skills.is_empty() {
19330 return 0;
19331 }
19332 let phi = skills[0].phi_layer;
19334 if skills.iter().any(|s| s.phi_layer != phi) {
19335 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
19336 }
19337 let n = skills.len();
19338 self.set_dyn_phi_layer(Some(phi));
19339 self.dyn_router = Some(DynRouter::new(skills));
19340 n
19341 }
19342
19343 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
19345 self.dyn_router
19346 .as_ref()
19347 .map(|r| r.switches.clone())
19348 .unwrap_or_default()
19349 }
19350
19351 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
19354 let _mimo_q8 = self
19355 .mimo_moe
19356 .is_on()
19357 .then(crate::qtensor::enter_full_gpu_q8_scope);
19358 let rows = self.weights.lm_head.rows();
19359 let mut logits = attention::take_buf(rows.min(self.vocab_size));
19360 let served = self.mimo_moe.is_on()
19364 && crate::gpu::mimo_q8_short_enabled()
19365 && rows == self.vocab_size
19366 && !self.weights.lm_head.has_prism_contract()
19367 && self
19368 .weights
19369 .lm_head
19370 .graph_weight()
19371 .is_some_and(|(model, idx, kind, _)| {
19372 kind == 7
19373 && crate::gpu::q82_short_rows(
19374 model,
19375 idx,
19376 hidden,
19377 1,
19378 rows,
19379 self.hidden_size,
19380 &mut logits,
19381 )
19382 });
19383 if !served {
19384 self.weights
19385 .lm_head
19386 .matvec(hidden, &mut logits, self.pool.as_deref());
19387 }
19388 logits.resize(self.vocab_size, 0.0);
19389 if let Some(m) = self.logit_multiplier {
19390 for l in logits.iter_mut() {
19391 *l *= m;
19392 }
19393 }
19394 if let Some(c) = self.final_softcap {
19395 softcap_in_place(&mut logits, c, self.pool.as_deref());
19396 }
19397 if let Some(cm) = self.head_clusters.as_ref() {
19398 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
19399 }
19400 logits
19401 }
19402
19403 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
19406 let h = hidden.len();
19407 let ncl = cm.len() / h.max(1);
19408 if ncl == 0 || logits.len() % ncl != 0 {
19409 return;
19410 }
19411 let cs = logits.len() / ncl;
19412 let mut lc = vec![0.0f32; ncl];
19414 for c in 0..ncl {
19415 let row = &cm[c * h..(c + 1) * h];
19416 let mut s = 0.0f32;
19417 for j in 0..h {
19418 s += row[j] * hidden[j];
19419 }
19420 lc[c] = s;
19421 }
19422 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19423 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19424 for c in 0..ncl {
19425 let blk = &mut logits[c * cs..(c + 1) * cs];
19426 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19427 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19428 let add = lc[c] - lse - bl;
19429 for v in blk.iter_mut() {
19430 *v += add;
19431 }
19432 }
19433 }
19434
19435 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19440 #[cfg(target_os = "macos")]
19441 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19442 self.clear_sequence_state();
19443 crate::gpu::graph_race_begin_generation();
19447 if task_mask.is_none() {
19448 self.o1_begin();
19449 }
19450 let mut hidden = vec![0.0f32; self.hidden_size];
19451 for (pos, &id) in ids.iter().enumerate() {
19452 let emb = self.embed_single(id);
19453 hidden = self.forward_layers(&emb, pos, task_mask);
19454 }
19455 if let Err(err) = self.o1_seal_checked() {
19456 self.o1_fail(err);
19457 }
19458 if let Some(logits) = self.graph_logits.take() {
19461 return logits;
19462 }
19463 inference::rms_norm_into(
19464 &hidden,
19465 &self.weights.final_norm,
19466 self.rms_eps,
19467 self.norm_style,
19468 &mut self.ws.n1,
19469 );
19470 self.lm_head_forward(&self.ws.n1)
19471 }
19472}
19473
19474pub fn create_test_pipeline(
19476 hidden_size: usize,
19477 intermediate_size: usize,
19478 num_heads: usize,
19479 num_kv_heads: usize,
19480 head_dim: usize,
19481 num_layers: usize,
19482 vocab_size: usize,
19483) -> Pipeline {
19484 let synth = |n: usize, salt: usize| -> Vec<f32> {
19487 (0..n)
19488 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19489 .collect()
19490 };
19491 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19492 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19493 };
19494 let layer_weights: Vec<LayerWeights> = (0..num_layers)
19495 .map(|li| LayerWeights {
19496 input_norm: vec![1.0; hidden_size],
19497 post_norm: vec![1.0; hidden_size],
19498 attn_out_norm: None,
19499 ffn_out_norm: None,
19500 layer_scale: None,
19501 ffn: FfnKind::Dense(DenseFfn {
19502 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19503 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19504 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19505 act: Act::Silu,
19506 down_t: None,
19507 segs: Vec::new(),
19508 }),
19509 attn: AttnKind::Full {
19510 bias: None,
19511 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19512 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19513 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19514 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19515 q_norm: None,
19516 k_norm: None,
19517 output_gate: false,
19518 softplus_gate: None,
19519 },
19520 })
19521 .collect();
19522
19523 Pipeline::new(
19524 Tokenizer::byte_level(),
19525 PipelineWeights {
19526 embed_tokens: qt(vocab_size, hidden_size, 100),
19527 layers: layer_weights,
19528 lm_head: qt(vocab_size, hidden_size, 200),
19529 final_norm: vec![1.0; hidden_size],
19530 },
19531 hidden_size,
19532 intermediate_size,
19533 num_heads,
19534 num_kv_heads,
19535 head_dim,
19536 num_layers,
19537 num_layers, false, vocab_size,
19540 1e-6,
19541 10_000.0,
19542 NormStyle::Qwen,
19543 4096,
19544 SamplerConfig {
19545 seed: Some(42),
19546 ..Default::default()
19547 },
19548 )
19549}
19550
19551#[inline]
19556fn mask_bit(row: &[u8], j: usize) -> bool {
19557 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19558}
19559
19560fn mask_gain() -> f32 {
19571 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19572 *G.get_or_init(|| {
19573 std::env::var("CMF_FFN_MASK_GAIN")
19574 .ok()
19575 .and_then(|v| v.parse().ok())
19576 .unwrap_or(1.0)
19577 })
19578}
19579
19580fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19581 let fill = meanfill().and_then(|(i, v)| {
19584 let li = crate::gpu::cur_layer();
19585 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19586 });
19587 for r in 0..rows {
19588 let base = r * inter;
19589 for (bi, &byte) in row.iter().enumerate() {
19590 if byte == 0xFF {
19591 continue;
19592 }
19593 let j0 = bi * 8;
19594 for bit in 0..8 {
19595 let j = j0 + bit;
19596 if j < inter && byte & (1 << bit) == 0 {
19597 g[base + j] = fill.map_or(0.0, |f| f[j]);
19598 }
19599 }
19600 }
19601 }
19602 let gain = mask_gain();
19603 if gain != 1.0 {
19604 for v in g[..rows * inter].iter_mut() {
19605 *v *= gain;
19606 }
19607 }
19608}
19609
19610#[inline]
19612fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19613 row.is_none_or(|r| mask_bit(r, i))
19614}
19615
19616fn all_bits_on(row: &[u8], n: usize) -> bool {
19619 (0..n).all(|i| mask_bit(row, i))
19620}
19621
19622fn tube_topk() -> usize {
19630 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19631 *K.get_or_init(|| {
19632 std::env::var("CMF_TUBE_TOPK")
19633 .ok()
19634 .and_then(|v| v.parse().ok())
19635 .unwrap_or(0)
19636 })
19637}
19638
19639fn tube_score_oracle() -> bool {
19640 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19641 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19642}
19643
19644fn tube_ffn_routed(
19651 d: &DenseFfn,
19652 xs: &[f32],
19653 b: usize,
19654 pool: Option<&Pool>,
19655 mask_row: Option<&[u8]>,
19656 k: usize,
19657) -> Vec<f32> {
19658 let hidden = d.down_proj.rows();
19659 let core = d.gate_proj.rows();
19660 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19661 let mut out = match (b, core_full, mask_row) {
19662 (1, true, _) => dense_ffn(d, xs, pool),
19663 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19664 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19665 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19666 };
19667 let cand: Vec<usize> = (0..d.segs.len())
19668 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19669 .collect();
19670 if cand.is_empty() {
19671 return out;
19672 }
19673 let oracle = tube_score_oracle();
19677 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19678 let mut scores = vec![0f32; b * cand.len()];
19679 for (ci, &i) in cand.iter().enumerate() {
19680 let seg = &d.segs[i];
19681 let w = seg.width;
19682 let mut g = vec![0.0f32; b * w];
19683 if b == 1 {
19684 seg.gate.matvec(xs, &mut g, pool);
19685 } else {
19686 seg.gate.matmat(xs, b, &mut g, pool);
19687 }
19688 for v in g.iter_mut() {
19689 *v = Act::Silu.combine(*v, 1.0);
19690 }
19691 if !oracle {
19692 for t in 0..b {
19693 scores[t * cand.len() + ci] =
19694 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19695 }
19696 }
19697 if oracle || b > 1 {
19698 let mut u = vec![0.0f32; b * w];
19699 if b == 1 {
19700 seg.up.matvec(xs, &mut u, pool);
19701 } else {
19702 seg.up.matmat(xs, b, &mut u, pool);
19703 }
19704 for (a, &v) in g.iter_mut().zip(u.iter()) {
19705 *a *= v;
19706 }
19707 if oracle {
19708 for t in 0..b {
19709 scores[t * cand.len() + ci] =
19710 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19711 }
19712 }
19713 }
19714 acts.push(g);
19715 }
19716 let keep = k.min(cand.len());
19718 let mut scratch: Vec<f32> = Vec::new();
19719 for t in 0..b {
19720 let mut sc: Vec<(f32, usize)> = (0..cand.len())
19721 .map(|ci| (scores[t * cand.len() + ci], ci))
19722 .collect();
19723 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19724 let mut alive = vec![false; cand.len()];
19725 for &(_, ci) in sc.iter().take(keep) {
19726 alive[ci] = true;
19727 }
19728 if b > 1 {
19729 for (ci, a) in acts.iter_mut().enumerate() {
19730 if !alive[ci] {
19731 let w = d.segs[cand[ci]].width;
19732 a[t * w..(t + 1) * w].fill(0.0);
19733 }
19734 }
19735 } else {
19736 for (ci, &i) in cand.iter().enumerate() {
19740 if !alive[ci] {
19741 continue;
19742 }
19743 let seg = &d.segs[i];
19744 let w = seg.width;
19745 let g = &mut acts[ci];
19746 if !tube_score_oracle() {
19747 scratch.clear();
19748 scratch.resize(w, 0.0);
19749 seg.up.matvec(xs, &mut scratch, pool);
19750 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19751 *a *= v;
19752 }
19753 }
19754 let mut acc = vec![0.0f32; hidden];
19755 seg.down.matvec(g, &mut acc, pool);
19756 for (o, a) in out.iter_mut().zip(&acc) {
19757 *o += *a;
19758 }
19759 }
19760 }
19761 }
19762 if b > 1 {
19763 for (ci, &i) in cand.iter().enumerate() {
19764 let seg = &d.segs[i];
19765 let mut acc = vec![0.0f32; b * hidden];
19766 seg.down.matmat(&acts[ci], b, &mut acc, pool);
19767 for (o, a) in out.iter_mut().zip(&acc) {
19768 *o += *a;
19769 }
19770 }
19771 }
19772 out
19773}
19774
19775fn tube_ffn(
19781 d: &DenseFfn,
19782 xs: &[f32],
19783 b: usize,
19784 pool: Option<&Pool>,
19785 mask_row: Option<&[u8]>,
19786) -> Vec<f32> {
19787 if tube_topk() > 0 {
19788 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19789 }
19790 let hidden = d.down_proj.rows();
19791 let core = d.gate_proj.rows();
19792 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19793 let mut out = match (b, core_full, mask_row) {
19794 (1, true, _) => dense_ffn(d, xs, pool),
19795 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19796 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19797 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19798 };
19799 TUBE_SCRATCH.with(|sc| {
19800 let mut sc = sc.borrow_mut();
19801 let [g, u, acc] = &mut *sc;
19802 for seg in &d.segs {
19803 if !tube_bit(mask_row, seg.start) {
19804 continue;
19805 }
19806 let w = seg.width;
19807 g.resize(b * w, 0.0);
19808 if b == 1
19809 && d.act == Act::Silu
19810 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19811 {
19812 } else {
19814 u.resize(b * w, 0.0);
19815 if b == 1 {
19816 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19817 } else {
19818 seg.gate.matmat(xs, b, g, pool);
19819 seg.up.matmat(xs, b, u, pool);
19820 }
19821 for i in 0..b * w {
19822 g[i] = d.act.combine(g[i], u[i]);
19823 }
19824 }
19825 acc.resize(b * hidden, 0.0);
19826 acc.fill(0.0);
19827 if b == 1 {
19828 seg.down.matvec(g, acc, pool);
19829 } else {
19830 seg.down.matmat(g, b, acc, pool);
19831 }
19832 for (o, a) in out.iter_mut().zip(acc.iter()) {
19833 *o += *a;
19834 }
19835 }
19836 out
19837 })
19838}
19839
19840thread_local! {
19841 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19845 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19846}
19847
19848static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19851 std::sync::atomic::AtomicU64::new(0),
19852 std::sync::atomic::AtomicU64::new(0),
19853];
19854
19855fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19859 #[cfg(feature = "gpu")]
19860 {
19861 use std::sync::atomic::Ordering::Relaxed;
19862 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19863 (
19864 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19865 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19866 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19867 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19868 )
19869 }
19870 #[cfg(not(feature = "gpu"))]
19871 (0.0, 0.0, 0.0, 0)
19872}
19873
19874fn wgpu_mirror_counters() -> [u64; 6] {
19877 #[cfg(feature = "gpu")]
19878 {
19879 crate::gpu_wgpu::MIRROR_EVENTS
19880 .each_ref()
19881 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19882 }
19883 #[cfg(not(feature = "gpu"))]
19884 [0; 6]
19885}
19886
19887fn prefill_prof_on() -> bool {
19888 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19889 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19890}
19891
19892pub(crate) fn accumulate_dense_ffn_activation_mass(
19901 d: &DenseFfn,
19902 xs: &[f32],
19903 b: usize,
19904 pool: Option<&Pool>,
19905 mass: &mut [f64],
19906 squared_mass: &mut [f64],
19907) -> Result<(), String> {
19908 let inter = d.gate_proj.rows();
19909 let hidden = d.gate_proj.cols();
19910 if mass.len() != inter || squared_mass.len() != inter {
19911 return Err(format!(
19912 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19913 mass.len(),
19914 squared_mass.len()
19915 ));
19916 }
19917 if d.up_proj.rows() != inter
19918 || d.up_proj.cols() != hidden
19919 || d.down_proj.cols() != inter
19920 || d.down_proj.rows() != hidden
19921 {
19922 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19923 }
19924 if !d.segs.is_empty() {
19925 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19926 }
19927 if xs.len() != b.saturating_mul(hidden) {
19928 return Err(format!(
19929 "MTP FFN probe: {} input values for {b} × {hidden} rows",
19930 xs.len()
19931 ));
19932 }
19933 if b == 0 {
19934 return Ok(());
19935 }
19936 let mut gate = vec![0.0f32; b * inter];
19937 let mut up = vec![0.0f32; b * inter];
19938 d.gate_proj.matmat(xs, b, &mut gate, pool);
19939 d.up_proj.matmat(xs, b, &mut up, pool);
19940 for row in 0..b {
19941 for i in 0..inter {
19942 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19943 let a = a as f64;
19944 mass[i] += a.abs();
19945 squared_mass[i] += a * a;
19946 }
19947 }
19948 Ok(())
19949}
19950
19951fn dense_ffn_batch(
19952 d: &DenseFfn,
19953 xs: &[f32],
19954 b: usize,
19955 pool: Option<&Pool>,
19956 mask_row: Option<&[u8]>,
19957) -> Vec<f32> {
19958 let inter = d.gate_proj.rows();
19959 let hidden = d.down_proj.rows();
19960 let fused_act = d.act.wgpu_graph_act();
19972 if mask_row.is_none()
19973 && fused_act.is_some()
19974 && b >= 32
19975 && crate::gpu::enabled_here()
19976 && !crate::gpu::mm_killed()
19977 && refit_dir().is_none()
19982 && !ffn_probe_active()
19987 && !crate::gptq_capture::capturing()
19991 {
19992 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19993 d.gate_proj.mapped_q4t(),
19994 d.up_proj.mapped_q4t(),
19995 d.down_proj.mapped_q4t(),
19996 ) {
19997 let mut out = vec![0.0f32; b * hidden];
19998 let act = fused_act.expect("checked above");
19999 if crate::gpu::q4_ffn_act(
20000 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
20001 ) {
20002 return out;
20003 }
20004 }
20005 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20010 d.gate_proj.mapped_q4tp(),
20011 d.up_proj.mapped_q4tp(),
20012 d.down_proj.mapped_q4tp(),
20013 ) {
20014 let mut out = vec![0.0f32; b * hidden];
20015 let act = fused_act.expect("checked above");
20016 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
20017 {
20018 return out;
20019 }
20020 }
20021 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20027 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
20028 d.gate_proj.mapped_device_gemm(),
20029 d.up_proj.mapped_device_gemm(),
20030 d.down_proj.mapped_device_gemm(),
20031 ) {
20032 let mut out = vec![0.0f32; b * hidden];
20033 let act = fused_act.expect("checked above");
20034 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
20035 return out;
20036 }
20037 }
20038 }
20039 let mut g = vec![0.0f32; b * inter];
20040 d.gate_proj.matmat(xs, b, &mut g, pool);
20041 let mut u = vec![0.0f32; b * inter];
20042 d.up_proj.matmat(xs, b, &mut u, pool);
20043 if gate_topk() > 0 && d.act == Act::Silu {
20044 for t in 0..b {
20045 let row = &mut g[t * inter..(t + 1) * inter];
20046 for v in row.iter_mut() {
20047 *v = Act::Silu.combine(*v, 1.0);
20048 }
20049 keep_top_k(row, gate_topk());
20050 }
20051 for i in 0..b * inter {
20052 g[i] *= u[i];
20053 }
20054 } else {
20055 for i in 0..b * inter {
20056 g[i] = d.act.combine(g[i], u[i]);
20057 }
20058 }
20059 if let Some(row) = mask_row {
20060 zero_masked_cols(&mut g, b, inter, row);
20061 }
20062 if oracle_topk() > 0 {
20063 for t in 0..b {
20064 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
20065 }
20066 }
20067 let mut out = vec![0.0f32; b * hidden];
20068 d.down_proj.matmat(&g, b, &mut out, pool);
20069 if refit_dir().is_some() {
20070 let li = crate::gpu::cur_layer();
20071 if li >= 0 {
20072 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
20073 }
20074 }
20075 FFN_PROBE.with(|pr| {
20079 if let Some(acc) = pr.borrow_mut().as_mut() {
20080 let li = crate::gpu::cur_layer();
20081 if li < 0 {
20082 return;
20083 }
20084 let Some(row) = acc.get_mut(li as usize) else {
20085 return;
20086 };
20087 let sq = probe_sq();
20088 for t in 0..b {
20089 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
20090 *a += if sq {
20091 (v as f64) * (v as f64)
20092 } else {
20093 (v as f64).abs()
20094 };
20095 }
20096 }
20097 }
20098 });
20099 out
20100}
20101
20102fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
20107 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20108 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20109 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
20110 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
20111 if (!on && !dump) || b == 0 {
20112 return;
20113 }
20114 let hidden = xs.len() / b;
20115 if on {
20116 let mut acc = m.act_sq.borrow_mut();
20117 if acc.len() < hidden {
20118 acc.resize(hidden, 0.0);
20119 }
20120 for t in 0..b {
20121 let row = &xs[t * hidden..(t + 1) * hidden];
20122 for (a, &v) in acc.iter_mut().zip(row) {
20123 *a += (v as f64) * (v as f64);
20124 }
20125 }
20126 }
20127 if dump {
20128 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
20131 .ok()
20132 .and_then(|v| v.parse().ok())
20133 .unwrap_or(4096);
20134 let mut rows = m.act_rows.borrow_mut();
20135 if rows.len() < cap * hidden {
20136 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
20137 rows.extend_from_slice(&xs[..take * hidden]);
20138 }
20139 }
20140}
20141
20142#[derive(Clone, Copy)]
20145struct SendVecs(*mut Vec<f32>);
20146unsafe impl Send for SendVecs {}
20147unsafe impl Sync for SendVecs {}
20148impl SendVecs {
20149 #[inline]
20150 fn at(self, i: usize) -> *mut Vec<f32> {
20151 unsafe { self.0.add(i) }
20152 }
20153}
20154
20155fn moe_ffn_batch(
20156 m: &MoeFfn,
20157 xs: &[f32],
20158 b: usize,
20159 hidden: usize,
20160 pool: Option<&Pool>,
20161 allowed: Option<&[bool]>,
20162) -> Vec<f32> {
20163 accumulate_act(m, xs, b);
20164 let ne = m.experts.len();
20165 let mut logits = vec![0.0f32; b * ne];
20166 match &m.resonance {
20167 Some(r) => {
20168 let hdim = xs.len() / b.max(1);
20169 for bi in 0..b {
20170 r.scores(
20171 &xs[bi * hdim..(bi + 1) * hdim],
20172 &mut logits[bi * ne..(bi + 1) * ne],
20173 );
20174 }
20175 }
20176 None => m.router.matmat(xs, b, &mut logits, pool),
20177 }
20178
20179 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
20182 {
20183 let mut st = m.stats.borrow_mut();
20184 if st.len() < ne {
20185 st.resize(ne, 0);
20186 }
20187 for bi in 0..b {
20188 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
20189 for &e in &idx {
20190 st[e] += 1;
20191 assign[e].push((bi, p[e] / wsum));
20192 }
20193 }
20194 }
20195
20196 let mut out = vec![0.0f32; b * hidden];
20197 let cols = m.experts[0].gate_proj.cols();
20198 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
20199 let sb = list.len();
20200 let mut sub = vec![0.0f32; sb * cols];
20201 for (k, &(bi, _)) in list.iter().enumerate() {
20202 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20203 }
20204 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
20205 for (k, &(bi, w)) in list.iter().enumerate() {
20206 for i in 0..hidden {
20207 out[bi * hidden + i] += w * eo[k * hidden + i];
20208 }
20209 }
20210 };
20211 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
20217 if pool.is_some() && active.len() >= 8 {
20218 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
20219 {
20220 let panel_ptr = SendVecs(panels.as_mut_ptr());
20221 let experts = &m.experts;
20224 let (active_r, assign_r) = (&active, &assign);
20225 let inherit_cpu = crate::gpu::inherit_cpu_scope();
20226 let run = |start: usize, end: usize| {
20227 let _cpu_scope = inherit_cpu();
20228 #[cfg(target_os = "macos")]
20236 let _metal_cpu = crate::gpu::enter_cpu_scope();
20237 for ai in start..end {
20238 let e = active_r[ai];
20239 let list = &assign_r[e];
20240 let sb = list.len();
20241 let mut sub = vec![0.0f32; sb * cols];
20242 for (k, &(bi, _)) in list.iter().enumerate() {
20243 sub[k * cols..(k + 1) * cols]
20244 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20245 }
20246 unsafe {
20248 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
20249 }
20250 }
20251 };
20252 match pool {
20253 Some(p) => p.run_rows(active.len(), &run),
20254 None => run(0, active.len()),
20255 }
20256 }
20257 for (ai, &e) in active.iter().enumerate() {
20258 for (k, &(bi, w)) in assign[e].iter().enumerate() {
20259 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
20260 for i in 0..hidden {
20261 out[bi * hidden + i] += w * eo[i];
20262 }
20263 }
20264 }
20265 } else {
20266 for &e in &active {
20267 run_expert(&m.experts[e], &assign[e], &mut out);
20268 }
20269 }
20270 if let Some((se, gate)) = &m.shared {
20271 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
20272 let mut gl = vec![0.0f32; b];
20273 gate.matmat(xs, b, &mut gl, pool);
20274 (0..b)
20275 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
20276 .collect()
20277 } else {
20278 (0..b).map(|bi| (bi, 1.0)).collect()
20279 };
20280 run_expert(se, &all, &mut out);
20281 }
20282 out
20283}
20284
20285fn moe_ffn_rows_exact(
20297 m: &MoeFfn,
20298 xs: &[f32],
20299 b: usize,
20300 hidden: usize,
20301 pool: Option<&Pool>,
20302) -> Vec<f32> {
20303 let mut out = vec![0.0f32; b * hidden];
20304 let per_row = |out: &mut [f32]| {
20305 for r in 0..b {
20306 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
20307 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
20308 }
20309 };
20310 let covered = !crate::gpu::enabled_here()
20311 && moe_batch_enabled()
20312 && m.shared.is_none()
20313 && m.resonance.is_none()
20314 && FFN_PROBE.with(|pr| pr.borrow().is_none())
20315 && m.experts.iter().all(|d| d.act == Act::Silu);
20316 if !covered {
20317 per_row(&mut out);
20318 return out;
20319 }
20320 let ne = m.experts.len();
20321 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
20323 for r in 0..b {
20324 let x = &xs[r * hidden..(r + 1) * hidden];
20325 accumulate_act(m, x, 1);
20326 let mut logits = vec![0.0f32; ne];
20327 m.router.matvec(x, &mut logits, pool);
20328 let (idx, p, wsum) = moe_route(&logits, m, None);
20329 {
20330 let mut st = m.stats.borrow_mut();
20331 if st.len() < ne {
20332 st.resize(ne, 0);
20333 }
20334 for &e in &idx {
20335 st[e] += 1;
20336 }
20337 }
20338 let w: Vec<f32> = idx
20339 .iter()
20340 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
20341 .collect();
20342 routes.push((idx, w));
20343 }
20344 if routes.iter().any(|(idx, _)| idx.is_empty()) {
20345 per_row(&mut out);
20346 return out;
20347 }
20348 let mut experts: Vec<usize> = Vec::new();
20350 let mut groups: Vec<Vec<usize>> = Vec::new();
20351 for (r, (idx, _)) in routes.iter().enumerate() {
20352 for &e in idx {
20353 match experts.iter().position(|&x| x == e) {
20354 Some(g) => groups[g].push(r),
20355 None => {
20356 experts.push(e);
20357 groups.push(vec![r]);
20358 }
20359 }
20360 }
20361 }
20362 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
20363 let inter = m.experts[experts[0]].gate_proj.rows();
20364 let pairs: Vec<(&QTensor, &QTensor)> = experts
20365 .iter()
20366 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
20367 .collect();
20368 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
20369 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
20370 per_row(&mut out);
20371 return out;
20372 }
20373 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
20374 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
20375 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
20376 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
20377 per_row(&mut out);
20378 return out;
20379 }
20380 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
20382 let mut p = 0usize;
20383 for (g, &e) in experts.iter().enumerate() {
20384 for &r in &groups[g] {
20385 slot.insert((r, e), p);
20386 p += 1;
20387 }
20388 }
20389 for (r, (idx, w)) in routes.iter().enumerate() {
20390 let terms: Vec<(&[f32], f32)> = idx
20391 .iter()
20392 .zip(w)
20393 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
20394 .collect();
20395 let row = &mut out[r * hidden..(r + 1) * hidden];
20396 for (i, dst) in row.iter_mut().enumerate() {
20397 let mut acc = 0f32;
20399 for (d, we) in &terms {
20400 acc += we * d[i];
20401 }
20402 *dst = acc;
20403 }
20404 }
20405 out
20406}
20407
20408pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20412 match pool {
20413 Some(p) if v.len() >= 1 << 15 => {
20414 let base = v.as_mut_ptr() as usize;
20415 p.run_rows(v.len(), &|s, e| {
20416 let sl =
20419 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20420 for x in sl {
20421 *x = c * (*x / c).tanh();
20422 }
20423 });
20424 }
20425 _ => {
20426 for x in v.iter_mut() {
20427 *x = c * (*x / c).tanh();
20428 }
20429 }
20430 }
20431}
20432
20433thread_local! {
20434 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20438 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20439}
20440
20441fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20443 if gate_topk() > 0
20446 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20447 {
20448 return out;
20449 }
20450 let prism_body = d.gate_proj.has_prism_contract()
20466 || d.up_proj.has_prism_contract()
20467 || d.down_proj.has_prism_contract();
20468 if !prism_body
20469 && crate::gpu::enabled_here()
20470 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20471 {
20472 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20473 crate::gpu::ProbeArm::Gpu
20474 } else {
20475 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20476 };
20477 match arm {
20478 crate::gpu::ProbeArm::Gpu => {
20479 let t0 = std::time::Instant::now();
20480 if let Some(out) = dense_ffn_gpu(d, x, pool) {
20481 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20482 return out;
20483 }
20484 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20488 }
20489 crate::gpu::ProbeArm::CpuTimed => {
20490 let t0 = std::time::Instant::now();
20491 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20492 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20493 return out;
20494 }
20495 crate::gpu::ProbeArm::Cpu => {
20496 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20497 }
20498 }
20499 }
20500 dense_ffn_cpu(d, x, pool)
20501}
20502
20503fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20505 let inter = d.gate_proj.rows();
20506 FFN_SCRATCH.with(|s| {
20507 let mut s = s.borrow_mut();
20508 let [g, u, ..] = &mut *s;
20509 g.resize(inter, 0.0);
20510 if gate_topk() > 0 {
20513 u.resize(inter, 0.0);
20517 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20518 for i in 0..inter {
20519 g[i] = Act::Silu.combine(g[i], 1.0);
20520 }
20521 keep_top_k(g, gate_topk());
20522 for i in 0..inter {
20523 g[i] *= u[i];
20524 }
20525 } else if d.act == Act::Silu && {
20526 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20527 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20528 } {
20529 } else {
20531 u.resize(inter, 0.0);
20532 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20534 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20535 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20536 for i in 0..inter {
20537 g[i] = d.act.combine(g[i], u[i]);
20538 }
20539 }
20540 FFN_PROBE.with(|pr| {
20548 if let Some(acc) = pr.borrow_mut().as_mut() {
20549 let li = crate::gpu::cur_layer();
20550 if li >= 0 {
20551 if let Some(row) = acc.get_mut(li as usize) {
20552 match probe_topk() {
20553 0 if probe_sq() => {
20554 for (a, &v) in row.iter_mut().zip(g.iter()) {
20555 *a += (v as f64) * (v as f64);
20556 }
20557 }
20558 0 if probe_signed() => {
20559 for (a, &v) in row.iter_mut().zip(g.iter()) {
20560 *a += v as f64;
20561 }
20562 }
20563 0 => {
20564 for (a, &v) in row.iter_mut().zip(g.iter()) {
20565 *a += (v as f64).abs();
20566 }
20567 }
20568 k => {
20569 let n = g.len();
20570 let k = k.min(n);
20571 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20572 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20573 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20574 });
20575 let thr = *kth;
20576 for (a, &v) in row.iter_mut().zip(g.iter()) {
20577 if v.abs() >= thr {
20578 *a += 1.0;
20579 }
20580 }
20581 }
20582 }
20583 }
20584 }
20585 }
20586 });
20587 if oracle_topk() > 0 {
20588 keep_top_k(g, oracle_topk());
20589 }
20590 {
20591 let li = crate::gpu::cur_layer();
20592 if li >= 0 {
20593 adump_row(li as usize, g);
20594 }
20595 }
20596 let mut out = attention::take_buf(d.down_proj.rows());
20597 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20598 d.down_proj.matvec(g, &mut out, pool);
20599 out
20600 })
20601}
20602
20603pub struct RefitAcc {
20616 pub support: Vec<u32>,
20617 pub gss: Vec<f32>,
20618 pub ya: Vec<f32>,
20619 pub hidden: usize,
20620 pub tokens: u64,
20621 pub buf_g: Vec<f32>,
20627 pub buf_o: Vec<f32>,
20628 pub buf_t: usize,
20629}
20630
20631type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20635
20636static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20637 std::sync::OnceLock::new();
20638
20639fn ffn_probe_active() -> bool {
20642 FFN_PROBE.with(|p| p.borrow().is_some())
20643}
20644
20645fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20646 REFIT
20647 .get_or_init(|| {
20648 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20649 (
20650 d,
20651 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20652 )
20653 })
20654 })
20655 .as_ref()
20656}
20657
20658fn refit_accumulate(
20660 li: usize,
20661 g: &[f32],
20662 b: usize,
20663 inter: usize,
20664 out: &[f32],
20665 hidden: usize,
20666 pool: Option<&Pool>,
20667) {
20668 let Some((dir, map)) = refit_dir() else {
20669 return;
20670 };
20671 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20672 let (from, to) = *SPAN.get_or_init(|| {
20673 let g = |k: &str, d: usize| {
20674 std::env::var(k)
20675 .ok()
20676 .and_then(|v| v.parse().ok())
20677 .unwrap_or(d)
20678 };
20679 (
20680 g("CMF_FFN_REFIT_FROM", 0),
20681 g("CMF_FFN_REFIT_TO", usize::MAX),
20682 )
20683 });
20684 if li < from || li > to {
20685 return;
20686 }
20687 let mut guard = map.lock().unwrap();
20688 let (map, shared) = &mut *guard;
20689 let acc = match map.entry(li) {
20690 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20691 std::collections::hash_map::Entry::Vacant(e) => {
20692 let path = format!("{dir}/support.{li}.u32");
20693 let Ok(bytes) = std::fs::read(&path) else {
20694 eprintln!("refit: no {path} — layer {li} skipped");
20695 return;
20696 };
20697 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20698 let support: Vec<u32> = bytes[4..4 + n * 4]
20699 .chunks_exact(4)
20700 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20701 .collect();
20702 eprintln!(
20703 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20704 (n * n + hidden * n) as f64 * 4.0 / 1e6
20705 );
20706 e.insert(RefitAcc {
20707 gss: vec![0.0; n * n],
20708 ya: vec![0.0; hidden * n],
20709 buf_g: Vec::new(),
20710 buf_o: Vec::new(),
20711 buf_t: 0,
20712 support,
20713 hidden,
20714 tokens: 0,
20715 })
20716 }
20717 };
20718 let ns = acc.support.len();
20719 let cap = refit_batch();
20721 if acc.buf_g.is_empty() {
20722 acc.buf_g = vec![0.0; ns * cap];
20723 acc.buf_o = vec![0.0; hidden * cap];
20724 }
20725 let take = b.min(cap - acc.buf_t);
20726 for t in 0..take {
20727 let col = acc.buf_t + t;
20728 for (j, &n) in acc.support.iter().enumerate() {
20729 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20730 }
20731 for h in 0..hidden {
20732 acc.buf_o[h * cap + col] = out[t * hidden + h];
20733 }
20734 }
20735 acc.buf_t += take;
20736 acc.tokens += take as u64;
20737 if acc.buf_t < cap {
20738 return;
20739 }
20740 let bt = acc.buf_t;
20741 acc.buf_t = 0;
20742 let RefitAcc {
20752 gss,
20753 ya,
20754 buf_g,
20755 buf_o,
20756 ..
20757 } = acc;
20758 let need = (ns * ns).max(hidden * ns);
20759 if shared.len() < need {
20760 shared.resize(need, 0.0);
20761 }
20762 let scratch = &mut shared[..];
20763 let _ = bt;
20764 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20765 add_into(gss, &scratch[..ns * ns], pool);
20766 if crate::gpu::gemm_nt_f32_transient(
20767 buf_o,
20768 buf_g,
20769 &mut scratch[..hidden * ns],
20770 hidden,
20771 cap,
20772 ns,
20773 ) {
20774 add_into(ya, &scratch[..hidden * ns], pool);
20775 } else {
20776 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20777 }
20778 } else {
20779 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20780 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20781 }
20782 }
20786
20787fn refit_batch() -> usize {
20789 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20790 *B.get_or_init(|| {
20791 std::env::var("CMF_FFN_REFIT_BATCH")
20792 .ok()
20793 .and_then(|v| v.parse().ok())
20794 .unwrap_or(4096)
20795 })
20796}
20797
20798fn accum_outer_t(
20801 c: &mut [f32],
20802 m: usize,
20803 n: usize,
20804 b: usize,
20805 left: &[f32],
20806 right: &[f32],
20807 pool: Option<&Pool>,
20808) {
20809 let ptr = SendMut(c.as_mut_ptr());
20810 let body = |i: usize| {
20811 let ptr = &ptr;
20812 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20813 for t in 0..b {
20814 let a = left[i * b + t];
20815 if a == 0.0 {
20816 continue;
20817 }
20818 for (j, o) in row.iter_mut().enumerate() {
20819 *o += a * right[j * b + t];
20820 }
20821 }
20822 };
20823 match pool {
20824 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20825 for i in s..e {
20826 body(i);
20827 }
20828 }),
20829 _ => {
20830 for i in 0..m {
20831 body(i);
20832 }
20833 }
20834 }
20835}
20836
20837fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20840 let n = dst.len().min(src.len());
20841 match pool {
20842 Some(p) if n >= 1 << 16 => {
20843 let ptr = SendMut(dst.as_mut_ptr());
20844 let f = |s: usize, e: usize| {
20845 let ptr = &ptr;
20846 for blk in s..e {
20847 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20848 for i in a..b {
20849 unsafe { *ptr.0.add(i) += src[i] };
20850 }
20851 }
20852 };
20853 p.run_rows(n.div_ceil(4096), &f);
20854 }
20855 _ => {
20856 for (d, v) in dst.iter_mut().zip(&src[..n]) {
20857 *d += *v;
20858 }
20859 }
20860 }
20861}
20862
20863fn accum_outer(
20868 c: &mut [f32],
20869 m: usize,
20870 n: usize,
20871 b: usize,
20872 left: &[f32],
20873 right: &[f32],
20874 pool: Option<&Pool>,
20875) {
20876 const TILE: usize = 32;
20877 let tiles = m.div_ceil(TILE);
20878 let cp = SendMut(c.as_mut_ptr());
20879 let body = |ti: usize| {
20880 let cp = &cp;
20881 let i0 = ti * TILE;
20882 let i1 = (i0 + TILE).min(m);
20883 for t in 0..b {
20884 let r = &right[t * n..t * n + n];
20885 for i in i0..i1 {
20886 let a = left[i * b + t];
20887 if a == 0.0 {
20888 continue;
20889 }
20890 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20892 for (o, v) in row.iter_mut().zip(r) {
20893 *o += a * *v;
20894 }
20895 }
20896 }
20897 };
20898 match pool {
20899 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20900 for ti in s..e {
20901 body(ti);
20902 }
20903 }),
20904 _ => {
20905 for ti in 0..tiles {
20906 body(ti);
20907 }
20908 }
20909 }
20910}
20911
20912pub fn refit_flush() -> usize {
20914 let Some((dir, map)) = refit_dir() else {
20915 return 0;
20916 };
20917 let guard = map.lock().unwrap();
20918 let mut n = 0;
20919 for (li, acc) in guard.0.iter() {
20920 let w = |name: &str, v: &[f32]| {
20923 let path = format!("{dir}/{name}.{li}.f32");
20924 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20925 match std::fs::write(&path, &bytes) {
20926 Ok(()) => {}
20927 Err(e) => eprintln!(
20928 "refit: FAILED to write {path} ({} MB): {e}",
20929 bytes.len() / 1_000_000
20930 ),
20931 }
20932 };
20933 w("gss", &acc.gss);
20934 w("ya", &acc.ya);
20935 println!(
20936 "refit L{li}: {} support, {} tokens, hidden {}",
20937 acc.support.len(),
20938 acc.tokens,
20939 acc.hidden
20940 );
20941 n += 1;
20942 }
20943 n
20944}
20945
20946fn adump_row(li: usize, g: &[f32]) {
20951 use std::io::Write as _;
20952 static FILES: std::sync::OnceLock<
20953 Option<(
20954 String,
20955 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20956 )>,
20957 > = std::sync::OnceLock::new();
20958 let Some((prefix, map)) = FILES
20959 .get_or_init(|| {
20960 std::env::var("CMF_FFN_ADUMP")
20961 .ok()
20962 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20963 })
20964 .as_ref()
20965 else {
20966 return;
20967 };
20968 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20971 let (from, to) = *SPAN.get_or_init(|| {
20972 let g = |k: &str, d: usize| {
20973 std::env::var(k)
20974 .ok()
20975 .and_then(|v| v.parse().ok())
20976 .unwrap_or(d)
20977 };
20978 (
20979 g("CMF_FFN_ADUMP_FROM", 0),
20980 g("CMF_FFN_ADUMP_TO", usize::MAX),
20981 )
20982 });
20983 if li < from || li > to {
20984 return;
20985 }
20986 let mut map = map.lock().unwrap();
20987 let f = map.entry(li).or_insert_with(|| {
20988 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20989 });
20990 let mut bytes = Vec::with_capacity(g.len() * 2);
20991 for v in g {
20992 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20993 }
20994 let _ = f.write_all(&bytes);
20995}
20996
20997fn oracle_topk() -> usize {
21003 if exact_mtp_calibration_active() {
21004 return 0;
21005 }
21006 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21007 *K.get_or_init(|| {
21008 std::env::var("CMF_FFN_ORACLE_TOPK")
21009 .ok()
21010 .and_then(|v| v.parse().ok())
21011 .unwrap_or(0)
21012 })
21013}
21014
21015fn gate_topk() -> usize {
21021 if exact_mtp_calibration_active() {
21022 return 0;
21023 }
21024 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21025 *K.get_or_init(|| {
21026 std::env::var("CMF_FFN_GATE_TOPK")
21027 .ok()
21028 .and_then(|v| v.parse().ok())
21029 .unwrap_or(0)
21030 })
21031}
21032
21033fn gate_block() -> usize {
21040 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21041 *B.get_or_init(|| {
21042 std::env::var("CMF_FFN_GATE_BLOCK")
21043 .ok()
21044 .and_then(|v| v.parse().ok())
21045 .unwrap_or(1)
21046 })
21047}
21048
21049fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
21051 let n = g.len();
21052 let nb = n.div_ceil(block);
21053 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
21054 if kb >= nb {
21055 return;
21056 }
21057 let mut score: Vec<f32> = (0..nb)
21058 .map(|b| {
21059 g[b * block..((b + 1) * block).min(n)]
21060 .iter()
21061 .map(|v| v * v)
21062 .sum::<f32>()
21063 })
21064 .collect();
21065 let mut ord = score.clone();
21066 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
21067 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21068 });
21069 let thr = *kth;
21070 for b in 0..nb {
21071 if score[b] < thr {
21072 g[b * block..((b + 1) * block).min(n)].fill(0.0);
21073 }
21074 }
21075 score.clear();
21076}
21077
21078fn keep_top_k(g: &mut [f32], k: usize) {
21080 if gate_block() > 1 {
21081 return keep_top_blocks(g, k, gate_block());
21082 }
21083 let n = g.len();
21084 if k == 0 || k >= n {
21085 return;
21086 }
21087 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
21088 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21089 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21090 });
21091 let thr = *kth;
21092 for v in g.iter_mut() {
21093 if v.abs() < thr {
21094 *v = 0.0;
21095 }
21096 }
21097}
21098
21099fn probe_sq() -> bool {
21103 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21104 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
21105}
21106
21107fn probe_signed() -> bool {
21111 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21112 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
21113}
21114
21115fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
21123 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
21124 M.get_or_init(|| {
21125 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
21126 let b = std::fs::read(&p).ok()?;
21127 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
21128 let vals: Vec<f32> = b[8..]
21129 .chunks_exact(4)
21130 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
21131 .collect();
21132 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
21133 Some((inter, vals))
21134 })
21135 .as_ref()
21136}
21137
21138fn probe_topk() -> usize {
21141 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21142 *K.get_or_init(|| {
21143 std::env::var("CMF_FFN_PROBE_TOPK")
21144 .ok()
21145 .and_then(|v| v.parse().ok())
21146 .unwrap_or(0)
21147 })
21148}
21149
21150thread_local! {
21151 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
21154 const { std::cell::RefCell::new(None) };
21155}
21156
21157fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
21170 if d.gate_proj.has_prism_contract()
21175 || d.up_proj.has_prism_contract()
21176 || d.down_proj.has_prism_contract()
21177 {
21178 return None;
21179 }
21180 let dt = d.down_t.as_ref()?;
21181 let inter = d.gate_proj.rows();
21182 let hidden = dt.cols();
21183 if k == 0 || k >= inter || d.act != Act::Silu {
21184 return None;
21185 }
21186 DYN_SCRATCH.with(|sc| {
21187 let mut sc = sc.borrow_mut();
21188 let DynScratch {
21189 g,
21190 mag,
21191 live,
21192 parts,
21193 } = &mut *sc;
21194 g.resize(inter, 0.0);
21195 d.gate_proj.matvec(x, g, pool);
21196 for v in g.iter_mut() {
21197 *v = inference::silu(*v);
21198 }
21199 mag.clear();
21202 mag.extend(g.iter().map(|v| v.abs()));
21203 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21204 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21205 });
21206 let thr = *kth;
21207 live.clear();
21208 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
21209 let mut out = vec![0.0f32; hidden];
21210 match pool {
21211 Some(p) if live.len() >= 64 => {
21212 let nw = p.n_workers() + 1;
21213 parts.clear();
21214 parts.resize(nw * hidden, 0.0);
21215 let ptr = SendMut(parts.as_mut_ptr());
21216 let n = live.len();
21217 let live_ref: &[u32] = live;
21218 let g_ref: &[f32] = g;
21219 p.run(&|w, workers| {
21220 let chunk = n.div_ceil(workers);
21221 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
21222 if s >= e {
21223 return;
21224 }
21225 WORKER_SCRATCH.with(|ws| {
21226 let mut ws = ws.borrow_mut();
21227 let [scratch, acc] = &mut *ws;
21228 scratch.resize(hidden.max(x.len()), 0.0);
21229 acc.clear();
21230 acc.resize(hidden, 0.0);
21231 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
21232 if let Some(&nx) = live_ref[s..e].get(o + 1) {
21235 d.up_proj.prefetch_row(nx as usize);
21236 dt.prefetch_row(nx as usize);
21237 }
21238 let idx = nrm as usize;
21239 let up = d.up_proj.row_dot(idx, x, scratch);
21240 let a = g_ref[idx] * up;
21241 if a != 0.0 {
21242 dt.add_row_scaled(idx, a, acc, scratch);
21243 }
21244 }
21245 for (j, v) in acc.iter().enumerate() {
21246 unsafe { *ptr.at(w * hidden + j) = *v };
21247 }
21248 });
21249 });
21250 for w in 0..nw {
21251 for (j, o) in out.iter_mut().enumerate() {
21252 *o += parts[w * hidden + j];
21253 }
21254 }
21255 }
21256 _ => {
21257 WORKER_SCRATCH.with(|ws| {
21258 let mut ws = ws.borrow_mut();
21259 let [scratch, _acc] = &mut *ws;
21260 scratch.resize(hidden.max(x.len()), 0.0);
21261 for &nrm in live.iter() {
21262 let idx = nrm as usize;
21263 let up = d.up_proj.row_dot(idx, x, scratch);
21264 let a = g[idx] * up;
21265 if a != 0.0 {
21266 dt.add_row_scaled(idx, a, &mut out, scratch);
21267 }
21268 }
21269 });
21270 }
21271 }
21272 Some(out)
21273 })
21274}
21275
21276struct DynScratch {
21279 g: Vec<f32>,
21280 mag: Vec<f32>,
21281 live: Vec<u32>,
21282 parts: Vec<f32>,
21283}
21284
21285thread_local! {
21286 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
21287 std::cell::RefCell::new(DynScratch {
21288 g: Vec::new(),
21289 mag: Vec::new(),
21290 live: Vec::new(),
21291 parts: Vec::new(),
21292 })
21293 };
21294 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
21296 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
21297}
21298
21299fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
21304 let inter = d.gate_proj.rows();
21305 FFN_SCRATCH.with(|s| {
21306 let mut s = s.borrow_mut();
21307 let [g, u, ..] = &mut *s;
21308 g.resize(inter, 0.0);
21309 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
21310 } else {
21312 u.resize(inter, 0.0);
21313 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
21314 for i in 0..inter {
21315 g[i] = d.act.combine(g[i], u[i]);
21316 }
21317 }
21318 zero_masked_cols(g, 1, inter, mask_row);
21319 let mut out = attention::take_buf(d.down_proj.rows());
21320 d.down_proj.matvec(g, &mut out, pool);
21321 out
21322 })
21323}
21324
21325fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
21331 if d.gate_proj.has_prism_contract()
21332 || d.up_proj.has_prism_contract()
21333 || d.down_proj.has_prism_contract()
21334 {
21335 return None;
21336 }
21337 d.act.moe_job_graph_contract()?;
21340 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
21343 return None;
21344 }
21345 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
21346 let mut model_ref = None;
21347 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
21348 let model = model_ref?;
21349 let hidden = jobs[0].down.1;
21350 let mut out = attention::take_buf(hidden);
21351 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21352 Some(out)
21353 } else {
21354 let mut out = out;
21355 attention::recycle_buf(&mut out);
21356 None
21357 }
21358}
21359
21360#[allow(clippy::type_complexity)]
21365#[allow(clippy::type_complexity)]
21366pub(crate) fn moe_parts(
21367 t: &QTensor,
21368) -> Option<(
21369 &std::sync::Arc<cortiq_core::CmfModel>,
21370 usize,
21371 usize,
21372 usize,
21373 &[f32],
21374 &[f32],
21375 bool,
21376 bool,
21377 bool,
21378)> {
21379 match t {
21380 QTensor::Mapped {
21381 model,
21382 idx,
21383 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
21384 rows,
21385 cols,
21386 row_scale,
21387 col_field,
21388 ..
21389 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
21390 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
21391 )),
21392 QTensor::Mapped {
21394 model,
21395 idx,
21396 dtype: cortiq_core::TensorDtype::Q1,
21397 rows,
21398 cols,
21399 ..
21400 } => Some((
21401 model,
21402 *idx,
21403 *rows,
21404 *cols,
21405 &[][..],
21406 &[][..],
21407 true,
21408 false,
21409 false,
21410 )),
21411 QTensor::Mapped {
21413 model,
21414 idx,
21415 dtype: cortiq_core::TensorDtype::Q4Tiled,
21416 rows,
21417 cols,
21418 ..
21419 } => Some((
21420 model,
21421 *idx,
21422 *rows,
21423 *cols,
21424 &[][..],
21425 &[][..],
21426 false,
21427 true,
21428 false,
21429 )),
21430 QTensor::Mapped {
21432 model,
21433 idx,
21434 dtype: cortiq_core::TensorDtype::Q4TiledP,
21435 rows,
21436 cols,
21437 ..
21438 } => Some((
21439 model,
21440 *idx,
21441 *rows,
21442 *cols,
21443 &[][..],
21444 &[][..],
21445 false,
21446 true,
21447 false,
21448 )),
21449 QTensor::Mapped {
21453 model,
21454 idx,
21455 dtype: cortiq_core::TensorDtype::Q2TiledP,
21456 rows,
21457 cols,
21458 ..
21459 } => Some((
21460 model,
21461 *idx,
21462 *rows,
21463 *cols,
21464 &[][..],
21465 &[][..],
21466 false,
21467 true,
21468 true,
21469 )),
21470 _ => None,
21471 }
21472}
21473
21474#[cfg(target_os = "macos")]
21481#[allow(clippy::type_complexity)]
21482fn chunk_moe_parts(
21483 m: &MoeFfn,
21484 hidden: usize,
21485) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21486 if m.shared.is_some()
21487 || m.per_expert_scale.is_some()
21488 || m.resonance.is_some()
21489 || m.route_tau.is_some()
21490 || m.router_input_norm
21491 || m.experts.is_empty()
21492 || m.top_k == 0
21493 || std::env::var("CMF_RMS_TRACE").is_ok()
21494 || std::env::var("CMF_ACT_DUMP").is_ok()
21495 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21496 {
21497 return None;
21498 }
21499 let (rf, rr, rc) = m.router.f32_parts()?;
21500 if rr != m.experts.len() || rc != hidden {
21501 return None;
21502 }
21503 let inter = m.experts[0].gate_proj.rows();
21504 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21507 let experts = m
21508 .experts
21509 .iter()
21510 .map(|e| {
21511 if e.act != Act::Silu
21512 || !e.segs.is_empty()
21513 || e.gate_proj.rows() != inter
21514 || e.up_proj.rows() != inter
21515 || e.down_proj.cols() != inter
21516 || e.gate_proj.cols() != hidden
21517 || e.down_proj.rows() != hidden
21518 {
21519 return None;
21520 }
21521 if q8 {
21522 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21523 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21524 }
21525 Some((
21526 e.gate_proj.mapped_q4tp()?.1,
21527 e.up_proj.mapped_q4tp()?.1,
21528 e.down_proj.mapped_q4tp()?.1,
21529 ))
21530 })
21531 .collect::<Option<Vec<_>>>()?;
21532 Some((rf, experts, q8))
21533}
21534
21535#[cfg(target_os = "macos")]
21548fn metal_flint_moe_graph_parts(
21549 m: &MoeFfn,
21550 hidden: usize,
21551 scratch_key: usize,
21552) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21553 use std::sync::Arc;
21554
21555 const HIDDEN: usize = 2_048;
21556 const ROUTED_INTER: usize = 512;
21557 const SHARED_INTER: usize = 1_024;
21558 const EXPERTS: usize = 256;
21559 const TOP_K: usize = 6;
21560
21561 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21562 return None;
21563 }
21564 let routed0 = m.experts.first()?;
21565 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21566 let (router, router_rows, router_cols) = m.router.f32_parts()?;
21567 if router_rows != EXPERTS || router_cols != HIDDEN {
21568 return None;
21569 }
21570 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21571 return None;
21572 }
21573 let same_q2 = |t: &QTensor| -> Option<usize> {
21574 let (owner, idx) = t.mapped_q2tp()?;
21575 Arc::ptr_eq(&owner, &model).then_some(idx)
21576 };
21577 let same_q4 = |t: &QTensor| -> Option<usize> {
21578 let (owner, idx) = t.mapped_q4tp()?;
21579 Arc::ptr_eq(&owner, &model).then_some(idx)
21580 };
21581 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21582 if e.act != Act::Silu
21583 || !e.segs.is_empty()
21584 || e.gate_proj.rows() != ROUTED_INTER
21585 || e.gate_proj.cols() != HIDDEN
21586 || e.up_proj.rows() != ROUTED_INTER
21587 || e.up_proj.cols() != HIDDEN
21588 || e.down_proj.rows() != HIDDEN
21589 || e.down_proj.cols() != ROUTED_INTER
21590 {
21591 return None;
21592 }
21593 Some((
21594 same_q2(&e.gate_proj)?,
21595 same_q2(&e.up_proj)?,
21596 same_q4(&e.down_proj)?,
21597 ))
21598 };
21599 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21600 if e.act != Act::Silu
21601 || !e.segs.is_empty()
21602 || e.gate_proj.rows() != SHARED_INTER
21603 || e.gate_proj.cols() != HIDDEN
21604 || e.up_proj.rows() != SHARED_INTER
21605 || e.up_proj.cols() != HIDDEN
21606 || e.down_proj.rows() != HIDDEN
21607 || e.down_proj.cols() != SHARED_INTER
21608 {
21609 return None;
21610 }
21611 Some((
21612 same_q4(&e.gate_proj)?,
21613 same_q4(&e.up_proj)?,
21614 same_q4(&e.down_proj)?,
21615 ))
21616 };
21617 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21618 if experts.len() != EXPERTS {
21619 return None;
21620 }
21621 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21622 if shared_gate.is_some() {
21623 return None;
21624 }
21625 Some(crate::gpu::GpuFlintMoe {
21626 router,
21627 bias: m.expert_bias.as_deref(),
21628 experts,
21629 scratch_key,
21630 shared: shared(shared_ffn)?,
21631 top_k: m.top_k,
21632 norm_topk: m.norm_topk_prob,
21633 route_scale: m.routed_scaling,
21634 sigmoid: m.router_sigmoid,
21635 sigmoid_denom_eps: m.sigmoid_denom_eps,
21636 })
21637}
21638
21639#[cfg(target_os = "macos")]
21640fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21641 if m.router_input_norm
21642 || m.route_tau.is_some()
21643 || m.mask.is_some()
21644 || m.per_expert_scale.is_some()
21645 || m.experts.is_empty()
21646 || m.top_k == 0
21647 || m.resonance.is_some()
21648 {
21649 return None;
21650 }
21651 let (sh, sg) = match &m.shared {
21656 Some((sh, sg)) => (Some(sh), sg.as_ref()),
21657 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21658 None => return None,
21659 };
21660 let (rf, rr, rc) = m.router.f32_parts()?;
21661 if rr != m.experts.len() || rc != hidden {
21662 return None;
21663 }
21664 let shared_gated = sg.is_some();
21665 let sf = match sg {
21666 Some(sg) => {
21667 let (sf, sr, sc) = sg.f32_parts()?;
21668 if sr * sc != hidden {
21669 return None;
21670 }
21671 sf
21672 }
21673 None => &rf[..hidden],
21676 };
21677 if let Some(b) = &m.expert_bias {
21678 if b.len() != m.experts.len() {
21679 return None;
21680 }
21681 }
21682 let inter = m.experts[0].gate_proj.rows();
21683 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21686 let q8 = crate::gpu_metal::moe_q8::enabled()
21690 && m.shared.is_none()
21691 && m.experts[0].gate_proj.q8_2f_parts().is_some();
21692 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21693 if e.act != Act::Silu
21694 || e.gate_proj.rows() != inter
21695 || e.gate_proj.cols() != hidden
21696 || e.up_proj.rows() != inter
21697 || e.up_proj.cols() != hidden
21698 || e.down_proj.rows() != hidden
21699 || e.down_proj.cols() != inter
21700 {
21701 return None;
21702 }
21703 if q8 {
21704 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21705 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21706 }
21707 let pick = |t: &QTensor| -> Option<usize> {
21708 if gu_q2 {
21709 t.mapped_q2tp().map(|(_, i)| i)
21710 } else {
21711 t.mapped_q4tp().map(|(_, i)| i)
21712 }
21713 };
21714 Some((
21715 pick(&e.gate_proj)?,
21716 pick(&e.up_proj)?,
21717 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21718 ))
21719 };
21720 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21721 let shared = match sh {
21722 Some(sh) => Some(trio(sh)?),
21723 None => None,
21724 };
21725 Some(crate::gpu::GpuMoe {
21726 router: rf,
21727 sgate: sf,
21728 experts,
21729 shared,
21730 n_exp: m.experts.len(),
21731 top_k: m.top_k,
21732 inter,
21733 norm_topk: m.norm_topk_prob,
21734 route_scale: m.routed_scaling,
21735 gu_q2,
21736 q8,
21737 sigmoid: m.router_sigmoid,
21738 sigmoid_denom_eps: m.sigmoid_denom_eps,
21739 bias: m.expert_bias.as_deref(),
21740 shared_gated,
21741 })
21742}
21743
21744fn forced_ids_at(step: usize) -> Option<u32> {
21750 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21751 IDS.get_or_init(|| {
21752 std::env::var("CMF_FORCE_IDS")
21753 .ok()
21754 .and_then(|p| std::fs::read_to_string(p).ok())
21755 .map(|s| {
21756 s.split_whitespace()
21757 .filter_map(|t| t.parse().ok())
21758 .collect()
21759 })
21760 .unwrap_or_default()
21761 })
21762 .get(step)
21763 .copied()
21764}
21765
21766pub(crate) fn moe_push_job_parts<'a>(
21770 gate: &'a QTensor,
21771 up: &'a QTensor,
21772 down: &'a QTensor,
21773 x: &[f32],
21774 w: f32,
21775 swiglu_limit: f32,
21776 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21777 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21778) -> Option<()> {
21779 use crate::qtensor::prescale;
21780 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21781 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21782 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21783 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21784 return None; }
21786 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21789 return None;
21790 }
21791 if !gq2 && dq2 {
21792 return None;
21793 }
21794 model_ref.get_or_insert_with(|| gm.clone());
21795 let dt = |cf: &[f32]| {
21796 if cf.is_empty() {
21797 cortiq_core::TensorDtype::Q8Row
21798 } else {
21799 cortiq_core::TensorDtype::Q8_2f
21800 }
21801 };
21802 jobs.push(crate::gpu::MoeJob {
21803 gate: (gi, gr, gc, grs),
21804 up: (ui, ur, uc, urs),
21805 down: (di, dr, dc, drs),
21806 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21807 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21808 down_col: dcf,
21809 w,
21810 q1: gq1,
21811 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21812 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21813 gu_q2: gq2,
21814 swiglu_limit,
21815 act: 0,
21816 });
21817 Some(())
21818}
21819
21820fn moe_push_job<'a>(
21822 d: &'a DenseFfn,
21823 x: &[f32],
21824 w: f32,
21825 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21826 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21827) -> Option<()> {
21828 use crate::qtensor::prescale;
21829 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21834 let act = graph_act.code();
21835 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21836 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21837 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21838 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21839 return None; }
21841 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21842 return None;
21843 }
21844 if !gq2 && dq2 {
21845 return None;
21846 }
21847 model_ref.get_or_insert_with(|| gm.clone());
21848 let gdt = if gcf.is_empty() {
21849 cortiq_core::TensorDtype::Q8Row
21850 } else {
21851 cortiq_core::TensorDtype::Q8_2f
21852 };
21853 let udt = if ucf.is_empty() {
21854 cortiq_core::TensorDtype::Q8Row
21855 } else {
21856 cortiq_core::TensorDtype::Q8_2f
21857 };
21858 jobs.push(crate::gpu::MoeJob {
21859 gate: (gi, gr, gc, grs),
21860 up: (ui, ur, uc, urs),
21861 down: (di, dr, dc, drs),
21862 xs_gate: prescale(x, gcf, gdt).into_owned(),
21863 xs_up: prescale(x, ucf, udt).into_owned(),
21864 down_col: dcf,
21865 w,
21866 q1: gq1,
21867 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21868 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21869 gu_q2: gq2,
21870 swiglu_limit,
21871 act,
21872 });
21873 Some(())
21874}
21875
21876fn sparse_ffn_quant(
21883 d: &DenseFfn,
21884 x: &[f32],
21885 active: &[u16],
21886 hidden: usize,
21887 pool: Option<&Pool>,
21888) -> Vec<f32> {
21889 let n = active.len();
21890 let inter = d.gate_proj.rows();
21891 let mut act = vec![0.0f32; n];
21892 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21895 let compute = |ai: usize| -> f32 {
21896 let idx = active[ai] as usize;
21897 if idx >= inter {
21898 return 0.0; }
21900 let mut s = if need_scratch {
21901 vec![0.0f32; hidden]
21902 } else {
21903 Vec::new()
21904 };
21905 let gate = d.gate_proj.row_dot(idx, x, &mut s);
21906 let up = d.up_proj.row_dot(idx, x, &mut s);
21907 d.act.combine(gate, up)
21908 };
21909 match pool {
21910 Some(p) if n >= 256 => {
21911 let ptr = SendMut(act.as_mut_ptr());
21912 p.run(&|widx, nw| {
21913 let chunk = n.div_ceil(nw);
21914 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21915 for ai in s..e {
21916 unsafe { *ptr.at(ai) = compute(ai) };
21917 }
21918 });
21919 }
21920 _ => {
21921 for (ai, a) in act.iter_mut().enumerate() {
21922 *a = compute(ai);
21923 }
21924 }
21925 }
21926 let mut out = vec![0.0f32; hidden];
21928 for (ai, &idx) in active.iter().enumerate() {
21929 let w = act[ai];
21930 if w.abs() >= 1e-12 && (idx as usize) < inter {
21931 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21932 }
21933 }
21934 out
21935}
21936
21937#[doc(hidden)]
21939pub fn sparse_ffn_quant_for_test(
21940 d: &DenseFfn,
21941 x: &[f32],
21942 active: &[u16],
21943 hidden: usize,
21944) -> Vec<f32> {
21945 sparse_ffn_quant(d, x, active, hidden, None)
21946}
21947
21948fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21952 let deq = |t: &QTensor| -> Vec<f32> {
21953 let (rows, cols) = (t.rows(), t.cols());
21954 let mut out = vec![0.0f32; rows * cols];
21955 for r in 0..rows {
21956 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21957 }
21958 out
21959 };
21960 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21961}
21962
21963struct SendMut(*mut f32);
21965unsafe impl Send for SendMut {}
21966unsafe impl Sync for SendMut {}
21967impl SendMut {
21968 #[inline]
21969 #[allow(clippy::mut_from_ref)]
21972 unsafe fn at(&self, i: usize) -> &mut f32 {
21973 unsafe { &mut *self.0.add(i) }
21974 }
21975}
21976
21977pub(crate) fn moe_route(
21989 logits: &[f32],
21990 m: &MoeFfn,
21991 allowed: Option<&[bool]>,
21992) -> (Vec<usize>, Vec<f32>, f32) {
21993 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21994}
21995
21996pub(crate) fn moe_route_with_eps(
22006 logits: &[f32],
22007 m: &MoeFfn,
22008 allowed: Option<&[bool]>,
22009 sigmoid_denom_eps: f32,
22010) -> (Vec<usize>, Vec<f32>, f32) {
22011 let ne = logits.len();
22012 let admit = |e: usize| {
22018 m.mask.as_ref().is_none_or(|mk| mk[e])
22019 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
22020 };
22021 if m.resonance.is_some() && m.top_k == 1 {
22034 let mut best: Option<usize> = None;
22035 for e in (0..ne).filter(|&e| admit(e)) {
22036 let l = logits[e];
22037 if l == f32::NEG_INFINITY || l.is_nan() {
22038 continue;
22039 }
22040 if best.is_none_or(|b| l > logits[b]) {
22041 best = Some(e);
22042 }
22043 }
22044 if let Some(b) = best {
22045 let mut p = vec![0.0f32; ne];
22046 p[b] = 1.0;
22047 return (vec![b], p, 1.0 / m.routed_scaling);
22048 }
22049 }
22050 let p: Vec<f32> = if m.router_sigmoid {
22056 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
22057 } else {
22058 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
22059 if mx == f32::NEG_INFINITY {
22060 vec![1.0 / ne.max(1) as f32; ne]
22061 } else {
22062 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
22063 let s: f32 = e.iter().sum();
22064 for v in &mut e {
22065 *v /= s;
22066 }
22067 e
22068 }
22069 };
22070 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
22071 match &m.expert_bias {
22073 Some(b) => idx.sort_unstable_by(|&x, &y| {
22074 (p[y] + b[y])
22075 .partial_cmp(&(p[x] + b[x]))
22076 .unwrap()
22077 .then(x.cmp(&y))
22078 }),
22079 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
22080 }
22081 idx.truncate(m.top_k);
22082 if let Some(tau) = m.route_tau {
22086 let total: f32 = idx.iter().map(|&e| p[e]).sum();
22087 if total > 0.0 {
22088 let mut acc = 0.0f32;
22089 let mut keep = idx.len();
22090 for (i, &e) in idx.iter().enumerate() {
22091 acc += p[e];
22092 if acc >= tau * total {
22093 keep = i + 1;
22094 break;
22095 }
22096 }
22097 idx.truncate(keep);
22098 }
22099 }
22100 let wsum: f32 = if m.norm_topk_prob {
22101 let s: f32 = idx.iter().map(|&e| p[e]).sum();
22102 (if m.router_sigmoid {
22106 s + sigmoid_denom_eps
22107 } else {
22108 s
22109 }) / m.routed_scaling
22110 } else {
22111 1.0 / m.routed_scaling
22112 };
22113 (idx, p, wsum)
22114}
22115
22116fn moe_trace(idx: &[usize]) {
22118 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
22119}
22120
22121pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
22124 use std::io::Write;
22125 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
22126 std::sync::OnceLock::new();
22127 let Some(f) = F.get_or_init(|| {
22128 let p = std::env::var("CMF_MOE_TRACE").ok()?;
22129 Some(std::sync::Mutex::new(
22130 std::fs::OpenOptions::new()
22131 .create(true)
22132 .append(true)
22133 .open(p)
22134 .ok()?,
22135 ))
22136 }) else {
22137 return;
22138 };
22139 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
22140 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
22141}
22142
22143pub(crate) fn moe_ffn(
22146 m: &MoeFfn,
22147 x: &[f32],
22148 pool: Option<&Pool>,
22149 allowed: Option<&[bool]>,
22150) -> Vec<f32> {
22151 let r = moe_ffn_route(m, x, pool, allowed);
22152 moe_ffn_experts(m, x, &r, pool)
22153}
22154
22155pub(crate) struct MoeRoute {
22159 pub idx: Vec<usize>,
22160 pub p: Vec<f32>,
22161 pub wsum: f32,
22162 pub logits: Vec<f32>,
22163}
22164
22165pub(crate) fn moe_ffn_route(
22171 m: &MoeFfn,
22172 x: &[f32],
22173 pool: Option<&Pool>,
22174 allowed: Option<&[bool]>,
22175) -> MoeRoute {
22176 accumulate_act(m, x, 1);
22177 let ne = m.experts.len();
22178 let mut logits = vec![0.0f32; ne];
22179 match &m.resonance {
22180 Some(r) => r.scores(x, &mut logits),
22181 None => m.router.matvec(x, &mut logits, pool),
22182 }
22183 let (idx, p, wsum) = moe_route(&logits, m, allowed);
22184 {
22185 let mut st = m.stats.borrow_mut();
22186 if st.len() < ne {
22187 st.resize(ne, 0);
22188 }
22189 for &e in &idx {
22190 st[e] += 1;
22191 }
22192 }
22193 moe_trace(&idx);
22199 MoeRoute {
22200 idx,
22201 p,
22202 wsum,
22203 logits,
22204 }
22205}
22206
22207#[cfg(target_os = "macos")]
22216fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
22217 const HIDDEN: usize = 2_048;
22218 const ROUTED_INTER: usize = 512;
22219 const SHARED_INTER: usize = 1_024;
22220 const EXPERTS: usize = 256;
22221 const TOP_K: usize = 6;
22222
22223 let Some(routed) = m.experts.first() else {
22224 return false;
22225 };
22226 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
22227 return false;
22228 };
22229 let Some((shared, shared_gate)) = &m.shared else {
22230 return false;
22231 };
22232 if shared_gate.is_some()
22233 || model.header.arch.arch_name != "flint"
22234 || model.header.arch.prism_hadamard.is_some()
22235 || model.header.arch.hidden_size != HIDDEN
22236 || m.experts.len() != EXPERTS
22237 || m.top_k != TOP_K
22238 || !m.router_sigmoid
22239 || !m.norm_topk_prob
22240 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
22241 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
22242 || m.route_tau.is_some()
22243 || m.mask.is_some()
22244 || m.per_expert_scale.is_some()
22245 || m.router_input_norm
22246 || m.resonance.is_some()
22247 || !m.grown.is_empty()
22248 {
22249 return false;
22250 }
22251
22252 let routed_q2 = routed.act == Act::Silu
22253 && routed.segs.is_empty()
22254 && routed.gate_proj.rows() == ROUTED_INTER
22255 && routed.gate_proj.cols() == HIDDEN
22256 && routed.up_proj.rows() == ROUTED_INTER
22257 && routed.up_proj.cols() == HIDDEN
22258 && routed.down_proj.rows() == HIDDEN
22259 && routed.down_proj.cols() == ROUTED_INTER
22260 && routed.up_proj.mapped_q2tp().is_some()
22261 && routed.down_proj.mapped_q4tp().is_some();
22262 let shared_q4 = shared.act == Act::Silu
22263 && shared.segs.is_empty()
22264 && shared.gate_proj.rows() == SHARED_INTER
22265 && shared.gate_proj.cols() == HIDDEN
22266 && shared.up_proj.rows() == SHARED_INTER
22267 && shared.up_proj.cols() == HIDDEN
22268 && shared.down_proj.rows() == HIDDEN
22269 && shared.down_proj.cols() == SHARED_INTER
22270 && shared.gate_proj.mapped_q4tp().is_some()
22271 && shared.up_proj.mapped_q4tp().is_some()
22272 && shared.down_proj.mapped_q4tp().is_some();
22273 routed_q2 && shared_q4
22274}
22275
22276pub(crate) fn moe_ffn_experts(
22279 m: &MoeFfn,
22280 x: &[f32],
22281 r: &MoeRoute,
22282 pool: Option<&Pool>,
22283) -> Vec<f32> {
22284 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
22285 if crate::gpu::enabled_here() {
22290 #[cfg(target_os = "macos")]
22291 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
22292 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
22298 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
22299 });
22300 }
22301 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
22302 crate::gpu::ProbeArm::Gpu => {
22303 let t0 = std::time::Instant::now();
22304 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
22305 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
22306 return out;
22307 }
22308 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
22309 }
22310 crate::gpu::ProbeArm::CpuTimed => {
22311 let t0 = std::time::Instant::now();
22312 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22313 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
22314 return out;
22315 }
22316 crate::gpu::ProbeArm::Cpu => {
22317 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22318 }
22319 }
22320 }
22321 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
22322}
22323
22324fn moe_ffn_minicpm_tail(
22328 slot: &mut crate::minicpm_tail::MiniCpmTail,
22329 li: usize,
22330 total_layers: usize,
22331 m: &MoeFfn,
22332 x: &[f32],
22333 pool: Option<&Pool>,
22334) -> Vec<f32> {
22335 let route = moe_ffn_route(m, x, pool, None);
22336 match slot.forward(li, m, x, &route, pool, total_layers) {
22337 Some(out) => out,
22338 None => moe_ffn_experts(m, x, &route, pool),
22343 }
22344}
22345
22346fn moe_ffn_banked(
22350 slot: &mut crate::mimo_moe::Slot,
22351 li: usize,
22352 m: &MoeFfn,
22353 x: &[f32],
22354 pool: Option<&Pool>,
22355) -> Vec<f32> {
22356 let t0 = std::time::Instant::now();
22357 let r = moe_ffn_route(m, x, pool, None);
22358 slot.note_route(t0.elapsed().as_nanos() as u64);
22359 match slot.forward(li, m, x, &r, pool) {
22360 Some(out) => out,
22361 None => crate::qtensor::float_activations_scope(|| {
22362 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
22363 }),
22364 }
22365}
22366
22367fn moe_ffn_banked_rows(
22369 slot: &mut crate::mimo_moe::Slot,
22370 li: usize,
22371 m: &MoeFfn,
22372 xs: &[f32],
22373 b: usize,
22374 hidden: usize,
22375 pool: Option<&Pool>,
22376) -> Vec<f32> {
22377 let t0 = std::time::Instant::now();
22378 let routes: Vec<_> = xs
22379 .chunks_exact(hidden)
22380 .map(|x| moe_ffn_route(m, x, pool, None))
22381 .collect();
22382 slot.note_route(t0.elapsed().as_nanos() as u64);
22383 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
22384 return out;
22385 }
22386 let mut out = Vec::with_capacity(b * hidden);
22387 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
22388 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
22389 crate::qtensor::float_activations_scope(|| {
22391 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
22392 })
22393 });
22394 out.extend(row);
22395 }
22396 out
22397}
22398
22399fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
22404 use std::sync::atomic::{AtomicBool, Ordering};
22405 if built {
22406 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22407 if total_layers > 0 && layers_run < total_layers {
22408 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22409 } else {
22410 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22411 }
22412 } else {
22413 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22414 }
22415 static SAID: AtomicBool = AtomicBool::new(false);
22416 if !SAID.swap(true, Ordering::Relaxed) {
22417 if built {
22418 tracing::info!("wgpu whole-token graph: ACTIVE");
22419 } else {
22420 tracing::warn!("wgpu whole-token graph refused — per-op path");
22421 }
22422 }
22423}
22424
22425pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22429pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22430pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22434pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22436
22437pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22441pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22442pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22443 std::sync::atomic::AtomicU64::new(0);
22444pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22445pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22446pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22450 std::sync::atomic::AtomicU64::new(0);
22451pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22452pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22453 std::sync::atomic::AtomicU64::new(0);
22454pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22455 std::sync::atomic::AtomicU64::new(0);
22456
22457fn moe_batch_enabled() -> bool {
22460 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22461 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22462}
22463
22464fn moe_ffn_cpu_batched(
22470 m: &MoeFfn,
22471 x: &[f32],
22472 idx: &[usize],
22473 p: &[f32],
22474 wsum: f32,
22475 pool: Option<&Pool>,
22476) -> Option<Vec<f32>> {
22477 if idx.is_empty() || !moe_batch_enabled() {
22478 return None;
22479 }
22480 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22484 return None;
22485 }
22486 let n = idx.len() + usize::from(m.shared.is_some());
22487 let mut pairs = Vec::with_capacity(n);
22488 let mut downs = Vec::with_capacity(n);
22489 let mut ws = Vec::with_capacity(n);
22490 for &e in idx {
22491 let d = &m.experts[e];
22492 if d.act != Act::Silu {
22493 return None;
22494 }
22495 pairs.push((&d.gate_proj, &d.up_proj));
22496 downs.push(&d.down_proj);
22497 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22498 }
22499 if let Some((se, gate)) = &m.shared {
22502 if se.act != Act::Silu {
22503 return None;
22504 }
22505 let g = gate.as_ref().map_or(1.0, |gate| {
22506 let mut gl = [0.0f32; 1];
22507 gate.matvec(x, &mut gl, pool);
22508 1.0 / (1.0 + (-gl[0]).exp())
22509 });
22510 pairs.push((&se.gate_proj, &se.up_proj));
22511 downs.push(&se.down_proj);
22512 ws.push(g);
22513 }
22514 let inter = pairs[0].0.rows();
22515 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22516 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22517 return None;
22518 }
22519 let mut out = attention::take_buf(x.len());
22520 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22521 attention::recycle_buf(&mut out);
22522 return None;
22523 }
22524 Some(out)
22525}
22526
22527pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22535pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22536pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22537pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22538pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22539
22540pub(crate) fn moe_cold_experts_cpu(
22541 experts: &[(&DenseFfn, f32)],
22542 x: &[f32],
22543 pool: Option<&Pool>,
22544) -> Vec<f32> {
22545 let mut out = attention::take_buf(x.len());
22546 if experts.is_empty() {
22547 return out;
22548 }
22549 let pairs: Vec<_> = experts
22550 .iter()
22551 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22552 .collect();
22553 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22554 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22555 let inter = experts[0].0.gate_proj.rows();
22556 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22557 let silu_only = experts
22565 .iter()
22566 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22567 if silu_only
22568 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22569 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22570 {
22571 return out;
22572 }
22573 out.fill(0.0);
22574 for &(expert, weight) in experts {
22575 let mut one = dense_ffn(expert, x, pool);
22576 for (o, v) in out.iter_mut().zip(&one) {
22577 *o += weight * v;
22578 }
22579 attention::recycle_buf(&mut one);
22580 }
22581 out
22582}
22583
22584pub(crate) fn moe_cold_experts_rows_cpu(
22588 jobs: &[Vec<(&DenseFfn, f32)>],
22589 xs: &[f32],
22590 hidden: usize,
22591 pool: Option<&Pool>,
22592) -> Vec<f32> {
22593 let mut out = vec![0.0; xs.len()];
22594 let mut experts: Vec<&DenseFfn> = Vec::new();
22595 let mut groups: Vec<Vec<usize>> = Vec::new();
22596 let mut terms = vec![Vec::new(); jobs.len()];
22597 for (r, row) in jobs.iter().enumerate() {
22598 for &(e, w) in row {
22599 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22600 Some(g) => g,
22601 None => {
22602 experts.push(e);
22603 groups.push(Vec::new());
22604 groups.len() - 1
22605 }
22606 };
22607 terms[r].push((g, groups[g].len(), w));
22608 groups[g].push(r);
22609 }
22610 }
22611 if experts.is_empty() {
22612 return out;
22613 }
22614 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22615 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22616 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22617 let count: usize = lens.iter().sum();
22618 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22619 let mut ds = vec![vec![0.0; hidden]; count];
22620 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22621 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22622 {
22623 let mut offset = 0;
22624 let offsets: Vec<_> = lens
22625 .iter()
22626 .map(|&n| {
22627 let start = offset;
22628 offset += n;
22629 start
22630 })
22631 .collect();
22632 for (r, terms) in terms.iter().enumerate() {
22633 for &(g, slot, w) in terms {
22634 for (o, &v) in out[r * hidden..(r + 1) * hidden]
22635 .iter_mut()
22636 .zip(&ds[offsets[g] + slot])
22637 {
22638 *o += w * v;
22639 }
22640 }
22641 }
22642 } else {
22643 for (r, jobs) in jobs.iter().enumerate() {
22644 if !jobs.is_empty() {
22645 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22646 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22647 attention::recycle_buf(&mut row);
22648 }
22649 }
22650 }
22651 out
22652}
22653
22654fn moe_ffn_cpu(
22656 m: &MoeFfn,
22657 x: &[f32],
22658 idx: &[usize],
22659 p: &[f32],
22660 wsum: f32,
22661 pool: Option<&Pool>,
22662) -> Vec<f32> {
22663 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22664 return out;
22665 }
22666 let mut out = attention::take_buf(x.len());
22667 for &e in idx {
22668 let mut eo = dense_ffn(&m.experts[e], x, pool);
22669 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22670 for i in 0..out.len() {
22671 out[i] += w * eo[i];
22672 }
22673 attention::recycle_buf(&mut eo);
22674 }
22675 if let Some((se, gate)) = &m.shared {
22676 let mut so = dense_ffn(se, x, pool);
22677 let g = gate.as_ref().map_or(1.0, |gate| {
22678 let mut gl = [0.0f32; 1];
22679 gate.matvec(x, &mut gl, pool);
22680 1.0 / (1.0 + (-gl[0]).exp())
22681 });
22682 for i in 0..out.len() {
22683 out[i] += g * so[i];
22684 }
22685 attention::recycle_buf(&mut so);
22686 }
22687 out
22688}
22689
22690#[allow(clippy::too_many_arguments)]
22698pub(crate) fn mla_attention(
22699 w: &MlaWeights,
22700 normed: &[f32],
22701 cache: &mut crate::kv_cache::LayerKvCache,
22702 position: usize,
22703 inv_freq: &[f32],
22704 rope_scale: f32,
22705 eps: f64,
22706 pool: Option<&Pool>,
22707) -> Vec<f32> {
22708 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22709 let hd = dr + dn;
22710 let mut q = vec![0.0f32; nh * hd];
22711 match (&w.q_a, &w.q_a_norm) {
22712 (Some(qa), Some(qn)) => {
22713 let mut t = vec![0.0f32; qa.rows()];
22714 qa.matvec(normed, &mut t, pool);
22715 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22716 w.q_proj.matvec(&tn, &mut q, pool);
22717 }
22718 _ => w.q_proj.matvec(normed, &mut q, pool),
22719 }
22720 let mut ca = vec![0.0f32; lora + dr];
22721 w.kv_a.matvec(normed, &mut ca, pool);
22722 let (c_lat, k_rope) = ca.split_at_mut(lora);
22723 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22724 let mut kvb = vec![0.0f32; nh * (dn + dv)];
22725 w.kv_b.matvec(&latn, &mut kvb, pool);
22726 if !w.nope {
22727 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22728 }
22729 for h in 0..nh {
22730 if !w.nope {
22731 attention::rope_rotate_scaled(
22732 &mut q[h * hd..h * hd + dr],
22733 position,
22734 inv_freq,
22735 rope_scale,
22736 );
22737 }
22738 }
22739 let mut k = vec![0.0f32; nh * hd];
22740 let mut v = vec![0.0f32; nh * hd];
22741 for h in 0..nh {
22742 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22743 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22744 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22745 }
22746 cache.append(&k, &v, &vec![true; nh]);
22747 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22748 attention::recycle_buf(&mut imp);
22749 let mut ov = vec![0.0f32; nh * dv];
22750 for h in 0..nh {
22751 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22752 }
22753 let mut out = vec![0.0f32; w.o_proj.rows()];
22754 w.o_proj.matvec(&ov, &mut out, pool);
22755 out
22756}
22757
22758fn dense_moe_ffn(
22765 dm: &DenseMoeFfn,
22766 x_normed: &[f32],
22767 h_raw: &[f32],
22768 eps: f64,
22769 norm_style: NormStyle,
22770 pool: Option<&Pool>,
22771) -> Vec<f32> {
22772 let mut d = dense_ffn(&dm.dense, x_normed, pool);
22773 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22774 let m = &dm.moe;
22775 let ne = m.experts.len();
22776 let mut logits = vec![0.0f32; ne];
22777 if m.router_input_norm {
22778 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22779 let inv = 1.0 / (ss + eps as f32).sqrt();
22780 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22781 m.router.matvec(&xr, &mut logits, pool);
22782 } else {
22783 m.router.matvec(h_raw, &mut logits, pool);
22784 }
22785 let (idx, p, wsum) = moe_route(&logits, m, None);
22786 {
22787 let mut st = m.stats.borrow_mut();
22788 if st.len() < ne {
22789 st.resize(ne, 0);
22790 }
22791 for &e in &idx {
22792 st[e] += 1;
22793 }
22794 }
22795 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22796 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22797 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22798 for (di, mi) in d.iter_mut().zip(&mo) {
22799 *di += mi;
22800 }
22801 d
22802}
22803
22804fn moe_gpu_refused(why: &'static str) {
22811 use std::sync::atomic::{AtomicBool, Ordering};
22812 static SAID: AtomicBool = AtomicBool::new(false);
22813 if !SAID.swap(true, Ordering::Relaxed) {
22814 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22815 }
22816}
22817
22818fn moe_ffn_gpu(
22819 m: &MoeFfn,
22820 x: &[f32],
22821 idx: &[usize],
22822 p: &[f32],
22823 wsum: f32,
22824 pool: Option<&Pool>,
22825) -> Option<Vec<f32>> {
22826 use crate::gpu::MoeJob;
22827
22828 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22829 let mut model_ref = None;
22830 for &e in idx {
22831 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22832 moe_gpu_refused("push_job(expert)");
22833 return None;
22834 }
22835 }
22836 if let Some((se, gate)) = &m.shared {
22837 let g = gate.as_ref().map_or(1.0, |gate| {
22838 let mut gl = [0.0f32; 1];
22839 gate.matvec(x, &mut gl, pool);
22840 1.0 / (1.0 + (-gl[0]).exp())
22841 });
22842 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22843 moe_gpu_refused("push_job(shared)");
22844 return None;
22845 }
22846 }
22847 let Some(model) = model_ref else {
22848 moe_gpu_refused("no model_ref");
22849 return None;
22850 };
22851 let hidden = jobs[0].down.1;
22852 let mut out = vec![0.0f32; hidden];
22853 if crate::gpu::moe_block(&model, &jobs, &mut out) {
22854 Some(out)
22855 } else {
22856 moe_gpu_refused("gpu::moe_block");
22857 None
22858 }
22859}
22860
22861fn ffn_forward(
22863 ffn: &FfnKind,
22864 x: &[f32],
22865 pool: Option<&Pool>,
22866 experts_allowed: Option<&[bool]>,
22867) -> Vec<f32> {
22868 match ffn {
22869 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22870 FfnKind::Dense(d) => dense_ffn(d, x, pool),
22871 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22872 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22876 }
22877}
22878
22879fn ffn_forward_pair(
22883 ffn: &FfnKind,
22884 x1: &[f32],
22885 x2: &[f32],
22886 pool: Option<&Pool>,
22887 experts_allowed: Option<&[bool]>,
22888) -> (Vec<f32>, Vec<f32>) {
22889 let d = match ffn {
22890 FfnKind::Dense(d) if !d.segs.is_empty() => {
22893 return (
22894 tube_ffn(d, x1, 1, pool, None),
22895 tube_ffn(d, x2, 1, pool, None),
22896 );
22897 }
22898 FfnKind::Dense(d) => d,
22899 FfnKind::Moe(m) => {
22900 return (
22901 moe_ffn(m, x1, pool, experts_allowed),
22902 moe_ffn(m, x2, pool, experts_allowed),
22903 );
22904 }
22905 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22906 };
22907 let inter = d.gate_proj.rows();
22908 FFN_SCRATCH.with(|s| {
22909 let mut s = s.borrow_mut();
22910 let [g1, g2, u1, u2] = &mut *s;
22911 g1.resize(inter, 0.0);
22912 g2.resize(inter, 0.0);
22913 u1.resize(inter, 0.0);
22914 u2.resize(inter, 0.0);
22915 QTensor::matvec2_many(
22918 [&d.gate_proj, &d.up_proj],
22919 x1,
22920 x2,
22921 [g1.as_mut_slice(), u1.as_mut_slice()],
22922 [g2.as_mut_slice(), u2.as_mut_slice()],
22923 pool,
22924 );
22925 for i in 0..inter {
22926 g1[i] = d.act.combine(g1[i], u1[i]);
22927 g2[i] = d.act.combine(g2[i], u2[i]);
22928 }
22929 let mut o1 = attention::take_buf(d.down_proj.rows());
22930 let mut o2 = attention::take_buf(d.down_proj.rows());
22931 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22932 (o1, o2)
22933 })
22934}
22935
22936#[cfg(test)]
22937mod tests {
22938 use std::ffi::OsString;
22939 use std::sync::Mutex;
22940
22941 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22944
22945 struct ScopedTestEnv {
22946 previous: Vec<(&'static str, Option<OsString>)>,
22947 }
22948
22949 impl ScopedTestEnv {
22950 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22951 let previous = vars
22952 .iter()
22953 .map(|(name, _)| (*name, std::env::var_os(name)))
22954 .collect();
22955 for &(name, value) in vars {
22956 unsafe {
22957 match value {
22958 Some(value) => std::env::set_var(name, value),
22959 None => std::env::remove_var(name),
22960 }
22961 }
22962 }
22963 Self { previous }
22964 }
22965 }
22966
22967 impl Drop for ScopedTestEnv {
22968 fn drop(&mut self) {
22969 for (name, value) in self.previous.drain(..).rev() {
22970 unsafe {
22971 match value {
22972 Some(value) => std::env::set_var(name, value),
22973 None => std::env::remove_var(name),
22974 }
22975 }
22976 }
22977 }
22978 }
22979
22980 #[test]
22981 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22982 use super::pad_mtp_draft_logits;
22983
22984 let mut logits = vec![-3.0, -1.0, -2.0];
22985 pad_mtp_draft_logits(&mut logits, 6);
22986 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22987 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22988 assert_eq!(
22989 logits
22990 .iter()
22991 .enumerate()
22992 .max_by(|(_, a), (_, b)| a.total_cmp(b))
22993 .map(|(i, _)| i),
22994 Some(1),
22995 "an unevaluated row cannot win an all-negative shortlist"
22996 );
22997 }
22998
22999 #[test]
23000 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
23001 use super::{Pipeline, SpecMon, SpecTrial};
23002
23003 let mut mon = SpecMon::default();
23004 let start = std::time::Instant::now();
23005 mon.round(1.0, 1);
23006 let first = Pipeline::spec_trial_round(
23007 SpecTrial::Spec {
23008 t0: start,
23009 gen0: 0,
23010 rounds: 0,
23011 },
23012 &mut mon,
23013 1,
23014 );
23015 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
23016
23017 mon.round(1.0, 1);
23018 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
23019 assert!(
23020 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
23021 "two verifier-tip-only rounds must stop the losing MTP trial"
23022 );
23023
23024 let mut viable = SpecMon::default();
23027 viable.round(1.0, 1);
23028 viable.round(1.0, 2);
23029 let continued = Pipeline::spec_trial_round(
23030 SpecTrial::Spec {
23031 t0: start,
23032 gen0: 0,
23033 rounds: 1,
23034 },
23035 &mut viable,
23036 3,
23037 );
23038 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
23039 }
23040
23041 #[test]
23042 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
23043 let mut p = spark_test_pipeline(None);
23044 let head_rows = 248_070;
23045 p.draft_full_streak = 7;
23046 assert_eq!(p.draft_head_rows(head_rows), head_rows);
23047
23048 p.reset_fresh_mtp_draft_state();
23049 assert_eq!(
23050 p.draft_head_rows(head_rows),
23051 super::Pipeline::draft_vocab_rows(head_rows),
23052 "a fresh request returns to its configured shortlist"
23053 );
23054
23055 p.note_draft_id(u32::MAX);
23058 assert_eq!(p.draft_head_rows(head_rows), head_rows);
23059 }
23060
23061 #[test]
23062 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
23063 use super::graph_spec_wanted_by_policy;
23064
23065 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
23069 assert!(!graph_spec_wanted_by_policy(
23070 Some("anything-but-1"),
23071 true,
23072 false,
23073 false,
23074 true,
23075 ));
23076 assert!(graph_spec_wanted_by_policy(
23077 Some("1"),
23078 false,
23079 true,
23080 false,
23081 true,
23082 ));
23083 assert!(!graph_spec_wanted_by_policy(
23084 Some("0"),
23085 true,
23086 false,
23087 false,
23088 true,
23089 ));
23090
23091 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
23094 assert!(graph_spec_wanted_by_policy(
23095 Some("legacy-force"),
23096 false,
23097 true,
23098 true,
23099 false,
23100 ));
23101 }
23102
23103 #[test]
23104 fn encoder_hidden_states_are_normalized_and_reusable() {
23105 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
23106 let ids = [1, 7, 11, 19];
23107
23108 let first = p.encode_hidden_states(&ids).expect("encoder forward");
23109 let second = p
23110 .encode_hidden_states(&ids)
23111 .expect("reusable encoder forward");
23112 assert_eq!(first.len(), ids.len() * 8);
23113 assert!(first.iter().all(|v| v.is_finite()));
23114 assert_eq!(
23115 first, second,
23116 "encoder forward must start from a fresh state"
23117 );
23118
23119 assert!(p.forward_ids(&ids, None).is_ok());
23122 }
23123
23124 #[test]
23129 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
23130 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
23131 let dense_card = ChunkStackFacts {
23132 plain_dense: true,
23133 discrete: true,
23134 gpu_on: true,
23135 ..Default::default()
23136 };
23137 assert!(dense_card.dense_on_discrete());
23138 assert_eq!(
23140 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
23141 DISCRETE_DENSE_PREFILL_CHUNK
23142 );
23143 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
23144 for (label, facts) in [
23145 (
23146 "GDN hybrid / MoE / DeepSeek stack",
23147 ChunkStackFacts {
23148 plain_dense: false,
23149 ..dense_card
23150 },
23151 ),
23152 (
23153 "integrated GPU",
23154 ChunkStackFacts {
23155 discrete: false,
23156 ..dense_card
23157 },
23158 ),
23159 (
23160 "CPU only",
23161 ChunkStackFacts {
23162 gpu_on: false,
23163 discrete: false,
23164 ..dense_card
23165 },
23166 ),
23167 (
23168 "capacity split",
23169 ChunkStackFacts {
23170 capacity_split: true,
23171 ..dense_card
23172 },
23173 ),
23174 (
23175 "multi-GPU plan",
23176 ChunkStackFacts {
23177 multi_gpu: true,
23178 ..dense_card
23179 },
23180 ),
23181 (
23182 "O(1) layers",
23183 ChunkStackFacts {
23184 o1: true,
23185 ..dense_card
23186 },
23187 ),
23188 ] {
23189 assert!(!facts.dense_on_discrete(), "{label}");
23190 assert_eq!(
23191 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
23192 48,
23193 "{label} keeps the historical x86 chunk"
23194 );
23195 }
23196 for dense in [false, true] {
23198 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
23199 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
23200 }
23201 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
23203 for dense in [false, true] {
23204 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
23205 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
23206 }
23207 }
23208 }
23209
23210 #[test]
23211 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
23212 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
23213 let full = |host_rows, device_rows| ReuseLayer {
23214 full: true,
23215 host_rows,
23216 device_rows,
23217 device_state: false,
23218 };
23219 assert_eq!(
23222 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
23223 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
23224 );
23225 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
23227 assert_eq!(
23229 kv_reuse_plan(339, &[full(339, Some(345))]),
23230 ReusePlan::Ready
23231 );
23232 assert_eq!(
23234 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
23235 ReusePlan::Pull(vec![(0, 300, 339)])
23236 );
23237 assert_eq!(
23239 kv_reuse_plan(339, &[full(300, Some(320))]),
23240 ReusePlan::Fresh
23241 );
23242 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
23243 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
23244 let conv = |device_state| ReuseLayer {
23247 full: false,
23248 host_rows: 0,
23249 device_rows: None,
23250 device_state,
23251 };
23252 assert_eq!(
23253 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
23254 ReusePlan::Fresh
23255 );
23256 assert_eq!(
23257 kv_reuse_plan(339, &[conv(false), full(339, None)]),
23258 ReusePlan::Ready
23259 );
23260 }
23261
23262 #[test]
23263 fn nll_graph_policy_scopes_only_the_fused_head() {
23264 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
23265 ("vulkan graph", true, true, false, true, false),
23267 ("native Metal graph", true, true, true, true, true),
23269 ("masked", false, true, false, false, false),
23271 ("graph disabled", true, false, true, false, false),
23272 ] {
23273 let (graph_quality, graph_head_required) =
23274 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
23275 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
23276 assert_eq!(graph_head_required, want_head, "{label}: fused head");
23277 }
23278 }
23279
23280 #[test]
23281 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
23282 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
23283 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
23284 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
23285 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
23286 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
23287 }
23288
23289 #[test]
23290 fn cancel_flag_stops_generation() {
23291 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
23292 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23295 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
23296 assert_eq!(r.finish_reason, "cancelled");
23297 assert!(
23298 r.token_ids.is_empty(),
23299 "no tokens after cancel: {:?}",
23300 r.token_ids
23301 );
23302 assert_eq!(p.kv_cache.seq_len(), 0);
23303 assert!(p.kv_history.is_empty());
23304 assert!(!p.graph_want_logits);
23305 assert!(p.graph_logits.is_none());
23306 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
23308 assert_ne!(r2.finish_reason, "cancelled");
23309 }
23310 use super::*;
23311
23312 #[test]
23313 fn minimax_m3_arch_selects_checked_oai_activation_only() {
23314 let arch = |arch_name: &str, hidden_act: &str| {
23318 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
23319 "arch_name": arch_name,
23320 "hidden_size": 8,
23321 "intermediate_size": 16,
23322 "num_layers": 1,
23323 "num_attention_heads": 1,
23324 "num_kv_heads": 1,
23325 "head_dim": 8,
23326 "vocab_size": 16,
23327 "layer_types": ["FullAttention"],
23328 "rms_norm_eps": 1e-6,
23329 "max_position_embeddings": 16,
23330 "hidden_act": hidden_act
23331 }))
23332 .expect("minimal architecture")
23333 };
23334 let minimax = arch("minimax_m3_vl", "swigluoai");
23335 assert_eq!(
23336 Act::from_arch_full(&minimax),
23337 Act::SwiGluOai {
23338 alpha: 1.702,
23339 limit: 7.0,
23340 }
23341 );
23342
23343 let unrelated = arch("qwen2", "swigluoai");
23346 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
23347
23348 let non_oai = arch("minimax_m3_vl", "gelu");
23352 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
23353 }
23354
23355 #[test]
23356 fn minimax_swiglu_oai_act_matches_scalar_contract() {
23357 let act = Act::swiglu_oai(1.702, 7.0);
23358 let got = act.combine(10.0, -10.0);
23359 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
23360 assert!((got - expected).abs() < 1e-6);
23361 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
23362 assert_eq!(
23363 act.wgpu_graph_act(),
23364 Some(crate::gpu::GraphAct::SwiGluOaiM3),
23365 "the WGPU-only arm accepts only the frozen M3 constants"
23366 );
23367 assert_eq!(
23368 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
23369 None,
23370 "nearby OAI constants must not borrow MiniMax's kernel"
23371 );
23372 assert_eq!(
23373 act.moe_job_graph_contract(),
23374 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
23375 "the direct MoE path carries the frozen clamp with the marker"
23376 );
23377 assert_eq!(
23378 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
23379 None,
23380 "a nearby clamp must not borrow MiniMax's device contract"
23381 );
23382
23383 let gate_only = act.apply(-10.0);
23386 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
23387 assert!((gate_only - expected_gate).abs() < 1e-10);
23388 }
23389
23390 #[test]
23391 fn explicit_sliding_schedule_selects_local_rope_profile() {
23392 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
23397 p.swa = Some((1024, usize::MAX));
23398 p.sliding_layers = Some(vec![true, true, true, false]);
23399 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
23400 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
23401 p.rope_scale = 1.277_258_9;
23402 p.rope_scale_local = 1.0;
23403
23404 for li in 0..3 {
23405 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
23406 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23407 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23408 }
23409 assert_eq!(p.layer_window(3), None);
23410 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23411 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23412 }
23413
23414 #[test]
23422 fn dynamic_ffn_equals_the_zeroing_arm() {
23423 let (hidden, inter) = (8usize, 32usize);
23424 let synth = |n: usize, salt: usize| -> Vec<f32> {
23425 (0..n)
23426 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23427 .collect()
23428 };
23429 let down = synth(hidden * inter, 3);
23430 let mut down_t = vec![0.0f32; inter * hidden];
23431 for r in 0..hidden {
23432 for c in 0..inter {
23433 down_t[c * hidden + r] = down[r * inter + c];
23434 }
23435 }
23436 let d = DenseFfn {
23437 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23438 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23439 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23440 act: Act::Silu,
23441 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23442 segs: Vec::new(),
23443 };
23444 let x = synth(hidden, 11);
23445 let k = 12usize;
23446 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23447 let mut g = vec![0.0f32; inter];
23449 d.gate_proj.matvec(&x, &mut g, None);
23450 let mut u = vec![0.0f32; inter];
23451 d.up_proj.matvec(&x, &mut u, None);
23452 for v in g.iter_mut() {
23453 *v = inference::silu(*v);
23454 }
23455 keep_top_k(&mut g, k);
23456 for i in 0..inter {
23457 g[i] *= u[i];
23458 }
23459 let mut want = vec![0.0f32; hidden];
23460 d.down_proj.matvec(&g, &mut want, None);
23461 for (a, b) in want.iter().zip(&got) {
23462 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23463 }
23464 }
23465
23466 #[test]
23472 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23473 let (hidden, core, tube) = (8usize, 12usize, 8usize);
23474 let inter = core + tube;
23475 let synth = |n: usize, salt: usize| -> Vec<f32> {
23476 (0..n)
23477 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23478 .collect()
23479 };
23480 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23481 let d_all = synth(hidden * inter, 3);
23482 let dense = DenseFfn {
23484 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23485 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23486 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23487 act: Act::Silu,
23488 down_t: None,
23489 segs: Vec::new(),
23490 };
23491 let rows =
23492 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23493 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23494 let mut o = Vec::with_capacity(hidden * (b - a));
23495 for r in 0..hidden {
23496 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23497 }
23498 o
23499 };
23500 let tubed = DenseFfn {
23501 down_t: None,
23502 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23503 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23504 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23505 act: Act::Silu,
23506 segs: vec![FfnSeg {
23507 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23508 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23509 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23510 start: core,
23511 width: tube,
23512 }],
23513 };
23514 let x = synth(hidden, 7);
23515 let want = dense_ffn(&dense, &x, None);
23516 let got = tube_ffn(&tubed, &x, 1, None, None);
23517 for (a, b) in want.iter().zip(&got) {
23518 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23519 }
23520 let mut bits = vec![0u8; inter.div_ceil(8)];
23522 for n in 0..core {
23523 bits[n / 8] |= 1 << (n % 8);
23524 }
23525 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23526 let masked = dense_ffn_masked(&dense, &x, None, &bits);
23527 for (a, b) in masked.iter().zip(&closed) {
23528 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23529 }
23530 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23532 for (a, b) in closed.iter().zip(&batch) {
23533 assert_eq!(a, b, "batch arm disagrees with decode arm");
23534 }
23535 }
23536
23537 #[test]
23539 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23540 let (hidden, inter) = (16usize, 40usize);
23541 let synth = |n: usize, salt: usize| -> Vec<f32> {
23542 (0..n)
23543 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23544 .collect()
23545 };
23546 let d = DenseFfn {
23547 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23548 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23549 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23550 act: Act::Silu,
23551 down_t: None,
23552 segs: Vec::new(),
23553 };
23554 let x = synth(hidden, 9);
23555 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23557
23558 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23559
23560 let mut g = vec![0.0f32; inter];
23562 d.gate_proj.matvec(&x, &mut g, None);
23563 let mut u = vec![0.0f32; inter];
23564 d.up_proj.matvec(&x, &mut u, None);
23565 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23566 for i in 0..inter {
23567 g[i] = if act_set.contains(&(i as u16)) {
23568 inference::silu(g[i]) * u[i]
23569 } else {
23570 0.0
23571 };
23572 }
23573 let mut reference = vec![0.0f32; hidden];
23574 d.down_proj.matvec(&g, &mut reference, None);
23575
23576 let max_d = sparse
23577 .iter()
23578 .zip(&reference)
23579 .map(|(a, b)| (a - b).abs())
23580 .fold(0.0f32, f32::max);
23581 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23582 }
23583
23584 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23587 let (h, inter, heads, kv, hd) = (
23588 p.hidden_size,
23589 inter,
23590 p.num_heads,
23591 p.num_kv_heads,
23592 p.head_dim,
23593 );
23594 let synth = |n: usize, salt: usize| -> Vec<f32> {
23595 (0..n)
23596 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23597 .collect()
23598 };
23599 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23600 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23601 };
23602 p.mtp = Some(MtpModule {
23603 enorm: vec![1.0; h],
23604 hnorm: vec![1.0; h],
23605 eh_proj: qt(h, 2 * h, 301),
23606 layer: LayerWeights {
23607 input_norm: vec![1.0; h],
23608 post_norm: vec![1.0; h],
23609 attn_out_norm: None,
23610 ffn_out_norm: None,
23611 layer_scale: None,
23612 ffn: FfnKind::Dense(DenseFfn {
23613 gate_proj: qt(inter, h, 315),
23614 up_proj: qt(inter, h, 316),
23615 down_proj: qt(h, inter, 317),
23616 act: Act::Silu,
23617 down_t: None,
23618 segs: Vec::new(),
23619 }),
23620 attn: AttnKind::Full {
23621 bias: None,
23622 wq: qt(heads * hd, h, 311),
23623 wk: qt(kv * hd, h, 312),
23624 wv: qt(kv * hd, h, 313),
23625 wo: qt(h, heads * hd, 314),
23626 q_norm: None,
23627 k_norm: None,
23628 output_gate: false,
23629 softplus_gate: None,
23630 },
23631 },
23632 final_norm: vec![1.0; h],
23633 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23634 });
23635 }
23636
23637 fn attach_test_mtp(p: &mut Pipeline) {
23639 attach_test_mtp_width(p, p.intermediate_size);
23640 }
23641
23642 #[test]
23643 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23644 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23645 attach_test_mtp_width(&mut p, 11);
23648 let ids = [3, 9, 4, 12, 7, 2];
23649 let trunk = p.probe_ffn_mass(&ids);
23650 assert_eq!(trunk[0].len(), p.intermediate_size);
23651
23652 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23653 assert_eq!(got.len(), 1);
23654 assert_eq!(got[0].mass.len(), 11);
23655 assert_eq!(got[0].squared_mass.len(), 11);
23656 assert_eq!(got[0].observed_positions, ids.len() - 1);
23657 assert!(got[0].mass.iter().any(|&v| v > 0.0));
23658 assert!(got[0]
23659 .mass
23660 .iter()
23661 .zip(&got[0].squared_mass)
23662 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23663 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23664 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23665
23666 let h = p.hidden_size;
23670 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23671 panic!("synthetic MTP must be dense")
23672 };
23673 dense.segs.push(FfnSeg {
23674 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23675 up: QTensor::from_f32(vec![0.0; h], 1, h),
23676 down: QTensor::from_f32(vec![0.0; h], h, 1),
23677 start: dense.gate_proj.rows(),
23678 width: 1,
23679 });
23680 let err = p.probe_mtp_ffn(&ids);
23681 assert!(err
23682 .unwrap_err()
23683 .contains("tube-segmented draft FFNs are not supported"));
23684 assert_eq!(p.kv_cache.seq_len(), 0);
23685 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23686 }
23687
23688 #[test]
23689 fn speculative_equals_vanilla_greedy() {
23690 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23691 let _env = ScopedTestEnv::set(&[
23694 ("CMF_GPU", Some("0")),
23695 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23696 ("CMF_MTP_PAIR", Some("1")),
23697 ]);
23698 let run = |spec: bool| {
23699 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23700 p.sampler_config.temperature = 0.0;
23701 attach_test_mtp(&mut p);
23702 p.speculative = spec;
23703 let r = p.generate("abcdef", 12, None, None).unwrap();
23704 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23705 };
23706 let (vanilla, d0, _) = run(false);
23707 let (spec, d1, a1) = run(true);
23708 assert_eq!(d0, 0, "vanilla path must not draft");
23709 assert!(d1 > 0, "speculative path must draft");
23710 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23711 assert_eq!(
23712 vanilla, spec,
23713 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23714 );
23715 }
23716
23717 #[test]
23718 fn speculative_accepts_constant_oracle() {
23719 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23720 let _env = ScopedTestEnv::set(&[
23721 ("CMF_GPU", Some("0")),
23722 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23723 ("CMF_MTP_PAIR", Some("1")),
23724 ]);
23725 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23726 p.sampler_config.temperature = 0.0;
23727 p.sampler_config.repetition_penalty = 1.0;
23728 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23731 attach_test_mtp(&mut p);
23732 p.speculative = true;
23733 let r = p.generate("abcd", 10, None, None).unwrap();
23734 assert!(r.mtp_drafted > 0);
23735 assert_eq!(
23736 r.mtp_accepted, r.mtp_drafted,
23737 "constant logits → every draft accepted"
23738 );
23739 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23742 }
23743
23744 #[test]
23745 fn host_fused_mtp_pair_is_opt_in() {
23746 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23747 let _env = ScopedTestEnv::set(&[
23748 ("CMF_GPU", Some("0")),
23749 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23750 ("CMF_MTP_PAIR", None),
23751 ]);
23752 let run = |spec: bool| {
23753 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23754 p.sampler_config.temperature = 0.0;
23755 attach_test_mtp(&mut p);
23756 p.speculative = spec;
23757 let r = p.generate("abcdef", 12, None, None).unwrap();
23758 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23759 };
23760 let (plain, d0, _) = run(false);
23761 let (guarded, d1, a1) = run(true);
23762 assert_eq!(d0, 0);
23763 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23764 assert_eq!(a1, 0);
23765 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23766 }
23767
23768 #[test]
23769 fn empty_prompt_is_an_error_not_a_panic() {
23770 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23771 let r = p.generate("", 4, None, None);
23772 assert!(r.is_err(), "empty prompt must be a clean error");
23773 }
23774
23775 #[test]
23776 fn every_token_enters_kv_exactly_once() {
23777 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23778 p.sampler_config.temperature = 0.0;
23780 let r = p.generate("abc", 2, None, None).unwrap();
23781 assert_eq!(r.prompt_tokens, 3);
23782 assert_eq!(
23786 p.kv_cache.seq_len(),
23787 3 + r.tokens_generated - 1,
23788 "each token must be cached exactly once (v1 cached the last prompt token twice)"
23789 );
23790 }
23791
23792 #[test]
23793 fn generation_is_reproducible_with_seed() {
23794 let run = || {
23795 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23796 p.generate("hello", 8, None, None).unwrap().token_ids
23797 };
23798 assert_eq!(run(), run());
23799 }
23800
23801 #[test]
23802 fn resetting_sampler_restarts_the_seeded_stream() {
23803 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23804 let config = SamplerConfig {
23805 seed: Some(1234),
23806 ..SamplerConfig::default()
23807 };
23808 p.set_sampler_config(config.clone());
23809 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23810 p.set_sampler_config(config);
23811 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23812 assert_eq!(first, second);
23813 }
23814
23815 #[test]
23816 fn eviction_bounds_the_cache() {
23817 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23818 p.kv_cache.max_seq_len = 6;
23819 p.sampler_config.temperature = 0.0;
23820 let _ = p.generate("abcd", 12, None, None).unwrap();
23821 assert!(
23822 p.kv_cache.seq_len() <= 6 + 1,
23823 "cache must stay bounded by max_seq_len (got {})",
23824 p.kv_cache.seq_len()
23825 );
23826 }
23827
23828 #[test]
23829 fn confidence_matches_tokens_and_is_a_probability() {
23830 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23831 p.sampler_config.temperature = 0.0;
23832 p.sampler_config.repetition_penalty = 1.0;
23833 let r = p.generate("abcd", 10, None, None).unwrap();
23834 assert_eq!(
23835 r.token_confidence.len(),
23836 r.token_ids.len(),
23837 "one confidence per emitted token"
23838 );
23839 for &c in &r.token_confidence {
23840 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23841 }
23842 let logits = [1.0f32, 3.0, 0.5, 3.0];
23844 let p0 = top1_prob_t(&logits, 1, 1.0);
23845 let p1 = top1_prob_t(&logits, 3, 1.0);
23846 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23847 assert!(p0 > 0.0 && p0 < 1.0);
23848 let sharp = top1_prob_t(&logits, 1, 1.0);
23850 let soft = top1_prob_t(&logits, 1, 2.0);
23851 assert!(soft < sharp, "higher temperature lowers peak confidence");
23852 }
23853
23854 #[test]
23855 fn trace_is_opt_in_and_parallels_the_output() {
23856 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23858 p.sampler_config.temperature = 0.0;
23859 p.sampler_config.repetition_penalty = 1.0;
23860 let r = p.generate("abcd", 10, None, None).unwrap();
23861 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23862
23863 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23865 p.sampler_config.temperature = 0.0;
23866 p.sampler_config.repetition_penalty = 1.0;
23867 p.set_trace(true);
23868 let r = p.generate("abcd", 10, None, None).unwrap();
23869 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23870 for (i, tr) in r.traces.iter().enumerate() {
23871 assert_eq!(tr.t, i, "trace index is sequential");
23872 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23873 assert_eq!(
23874 tr.confidence, r.token_confidence[i],
23875 "trace confidence matches the confidence channel"
23876 );
23877 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23879 }
23880 }
23881
23882 #[test]
23883 fn explain_prefill_logits_match_greedy_first_token() {
23884 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23888 p.sampler_config.temperature = 0.0;
23889 p.sampler_config.repetition_penalty = 1.0;
23890 let ids = p.tokenizer.encode("abcd");
23891 let logits = p.prefill_next_logits(&ids, None);
23892 let argmax = logits
23893 .iter()
23894 .enumerate()
23895 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23896 .unwrap()
23897 .0 as u32;
23898 let r = p.generate("abcd", 1, None, None).unwrap();
23899 assert_eq!(
23900 argmax, r.token_ids[0],
23901 "explain preview must match greedy emit"
23902 );
23903 }
23904
23905 #[test]
23906 fn laguna_shared_expert_is_unconditionally_added() {
23907 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23908 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23909 let zero_dense = || DenseFfn {
23910 gate_proj: matrix(vec![0.0; 4]),
23911 up_proj: matrix(vec![0.0; 4]),
23912 down_proj: matrix(vec![0.0; 4]),
23913 act: Act::Silu,
23914 down_t: None,
23915 segs: Vec::new(),
23916 };
23917 let shared = DenseFfn {
23918 gate_proj: identity(),
23919 up_proj: identity(),
23920 down_proj: identity(),
23921 act: Act::Silu,
23922 down_t: None,
23923 segs: Vec::new(),
23924 };
23925 let x = [1.0, 2.0];
23926 let expected = dense_ffn(&shared, &x, None);
23927 let moe = MoeFfn {
23928 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23929 experts: vec![zero_dense()],
23930 top_k: 1,
23931 norm_topk_prob: true,
23932 router_sigmoid: true,
23933 sigmoid_denom_eps: 1e-6,
23934 expert_bias: None,
23935 routed_scaling: 1.0,
23936 route_tau: None,
23937 shared: Some((shared, None)),
23938 stats: std::cell::RefCell::new(Vec::new()),
23939 act_sq: std::cell::RefCell::new(Vec::new()),
23940 act_rows: std::cell::RefCell::new(Vec::new()),
23941 mask: None,
23942 per_expert_scale: None,
23943 router_input_norm: false,
23944 resonance: None,
23945 grown: Vec::new(),
23946 };
23947 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23948 for (actual, expected) in actual.iter().zip(expected) {
23949 assert!((actual - expected).abs() < 1e-6);
23950 }
23951 }
23952
23953 fn mimo_test_pipeline() -> Pipeline {
23962 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23963 let kvh = [1usize, 2, 2, 1];
23964 let synth = |n: usize, salt: usize| -> Vec<f32> {
23965 (0..n)
23966 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23967 .collect()
23968 };
23969 let qt = |rows: usize, cols: usize, salt: usize| {
23970 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23971 };
23972 let dense = |inter: usize, salt: usize| DenseFfn {
23973 gate_proj: qt(inter, hs, salt),
23974 up_proj: qt(inter, hs, salt + 1),
23975 down_proj: qt(hs, inter, salt + 2),
23976 act: Act::Silu,
23977 down_t: None,
23978 segs: Vec::new(),
23979 };
23980 let layers: Vec<LayerWeights> = (0..4)
23981 .map(|li| LayerWeights {
23982 input_norm: vec![1.0; hs],
23983 post_norm: vec![1.0; hs],
23984 attn_out_norm: None,
23985 ffn_out_norm: None,
23986 layer_scale: None,
23987 ffn: if li == 0 {
23988 FfnKind::Dense(dense(inter, 50))
23989 } else {
23990 FfnKind::Moe(MoeFfn {
23991 router: qt(4, hs, 60 + li),
23992 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23993 top_k: 2,
23994 norm_topk_prob: true,
23995 router_sigmoid: true,
23996 sigmoid_denom_eps: 1e-6,
23997 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23998 routed_scaling: 1.0,
23999 route_tau: None,
24000 shared: None,
24001 stats: std::cell::RefCell::new(Vec::new()),
24002 act_sq: std::cell::RefCell::new(Vec::new()),
24003 act_rows: std::cell::RefCell::new(Vec::new()),
24004 mask: None,
24005 per_expert_scale: None,
24006 router_input_norm: false,
24007 resonance: None,
24008 grown: Vec::new(),
24009 })
24010 },
24011 attn: AttnKind::Full {
24012 wq: qt(nh * hd, hs, li * 10 + 1),
24013 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
24014 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
24015 wo: qt(hs, nh * vd, li * 10 + 4),
24016 q_norm: None,
24017 k_norm: None,
24018 output_gate: false,
24019 softplus_gate: None,
24020 bias: None,
24021 },
24022 })
24023 .collect();
24024 let mut p = Pipeline::new(
24025 Tokenizer::byte_level(),
24026 PipelineWeights {
24027 embed_tokens: qt(vocab, hs, 100),
24028 layers,
24029 lm_head: qt(vocab, hs, 200),
24030 final_norm: vec![1.0; hs],
24031 },
24032 hs,
24033 inter,
24034 nh,
24035 1, hd,
24037 4,
24038 4,
24039 false,
24040 vocab,
24041 1e-6,
24042 1e7,
24043 NormStyle::Qwen,
24044 4096,
24045 SamplerConfig {
24046 seed: Some(7),
24047 ..Default::default()
24048 },
24049 );
24050 p.layer_dump = None;
24052 p.set_rotary(4, 1e7);
24053 p.sliding_layers = Some(vec![false, true, true, false]);
24054 p.swa = Some((3, usize::MAX));
24055 p.rotary_dim_local = Some(4);
24056 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
24057 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
24058 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
24059 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
24060 p
24061 }
24062
24063 #[test]
24064 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
24065 let mut p = mimo_test_pipeline();
24066 p.speculative = false;
24067 p.ignore_eos = true;
24068 p.sampler_config.temperature = 0.0;
24069 p.sampler_config.repetition_penalty = 1.0;
24070 let a = vec![3, 5, 7, 9, 11, 13];
24071 let b = vec![4, 8, 12, 16, 20, 24];
24072 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
24073 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
24074 let actual = p
24078 .generate_from_embeds(&a, &rows, 8, None, None)
24079 .unwrap()
24080 .token_ids;
24081 assert_eq!(actual, expected);
24082 assert!(p.kv_history.is_empty());
24083 let mut extended = a.clone();
24084 extended.push(17);
24085 let after_media = p
24086 .generate_from_ids(&extended, 8, None, None)
24087 .unwrap()
24088 .token_ids;
24089 p.reset_session();
24090 let fresh = p
24091 .generate_from_ids(&extended, 8, None, None)
24092 .unwrap()
24093 .token_ids;
24094 assert_eq!(after_media, fresh);
24095 assert!(
24096 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
24097 .is_err()
24098 );
24099 p.reset_session();
24103 p.generate_from_ids(&a, 1, None, None).unwrap();
24104 let mut media_ids = p.kv_history.clone();
24105 assert!(!media_ids.is_empty());
24106 media_ids.extend_from_slice(&[19, 21, 23]);
24107 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
24108 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
24109 let mut oracle = mimo_test_pipeline();
24110 oracle.speculative = false;
24111 oracle.ignore_eos = true;
24112 oracle.sampler_config.temperature = 0.0;
24113 oracle.sampler_config.repetition_penalty = 1.0;
24114 let expected = oracle
24115 .generate_from_ids(&source_ids, 8, None, None)
24116 .unwrap()
24117 .token_ids;
24118 assert_eq!(
24119 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
24120 .unwrap()
24121 .token_ids,
24122 expected
24123 );
24124 assert!(p.kv_history.is_empty());
24125 let mut bad = rows;
24126 bad[0] = f32::NAN;
24127 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
24128 }
24129
24130 fn f32_bits(v: &[f32]) -> Vec<u32> {
24131 v.iter().map(|x| x.to_bits()).collect()
24132 }
24133
24134 #[test]
24141 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
24142 let mut p = mimo_test_pipeline();
24143 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
24144 assert_eq!(kv, vec![1, 2, 2, 1]);
24145 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
24146 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
24147 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
24148 let hs = p.hidden_size;
24149 let mut decode = Vec::new();
24150 for (pos, &id) in ids.iter().enumerate() {
24151 let e = p.embed_single(id);
24152 let h = p.forward_layers(&e, pos, None);
24153 decode.push(p.logits_from_hidden(&h));
24154 }
24155 for l in &p.kv_cache.layers {
24156 assert_eq!(l.seq_len, 12);
24157 assert_eq!(l.head_values(0).len(), 12 * 8);
24159 }
24160 assert!(decode.iter().flatten().all(|v| v.is_finite()));
24161
24162 p.clear_sequence_state();
24163 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24164 for pos in 0..ids.len() {
24165 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
24166 assert_eq!(
24167 f32_bits(&decode[pos]),
24168 f32_bits(&lg),
24169 "whole prompt, pos {pos}"
24170 );
24171 }
24172
24173 p.clear_sequence_state();
24174 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
24175 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
24176 for pos in 0..ids.len() {
24177 let row = if pos < 5 {
24178 &a[pos * hs..(pos + 1) * hs]
24179 } else {
24180 &b[(pos - 5) * hs..(pos - 4) * hs]
24181 };
24182 let lg = p.logits_from_hidden(row);
24183 assert_eq!(
24184 f32_bits(&decode[pos]),
24185 f32_bits(&lg),
24186 "two chunks, pos {pos}"
24187 );
24188 }
24189
24190 let last = |p: &mut Pipeline| {
24193 p.clear_sequence_state();
24194 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24195 p.logits_from_hidden(&hb[11 * hs..12 * hs])
24196 };
24197 let base = last(&mut p);
24198 let mut no_sinks = mimo_test_pipeline();
24199 for l in &mut no_sinks.kv_cache.layers {
24200 l.sinks = None;
24201 }
24202 assert_ne!(
24203 f32_bits(&last(&mut no_sinks)),
24204 f32_bits(&base),
24205 "sinks are live"
24206 );
24207 let mut wide = mimo_test_pipeline();
24208 wide.swa = Some((64, usize::MAX));
24209 assert_ne!(
24210 f32_bits(&last(&mut wide)),
24211 f32_bits(&base),
24212 "window is live"
24213 );
24214
24215 p.clear_sequence_state();
24217 p.ignore_eos = true;
24218 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
24219 assert_eq!(r.token_ids.len(), 4);
24220 }
24221
24222 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
24225 let (hs, nh) = (16usize, 4usize);
24226 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
24227 p.layer_dump = None;
24228 p.swa = Some((6, 4));
24229 p.proj_gate_sigmoid = true;
24230 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
24231 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
24232 let g: Vec<f32> = (0..nh * hs)
24233 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
24234 .collect();
24235 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
24236 }
24237 }
24238 p.swa_trim = trim;
24239 p
24240 }
24241
24242 #[test]
24248 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
24249 let mut a = spark_test_pipeline(None);
24250 let mut b = spark_test_pipeline(Some((2, 4)));
24251 assert_eq!(
24252 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
24253 vec![Some(6), Some(6), Some(6), None]
24254 );
24255 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
24256 let mut pos = 0usize;
24257 for &n in [5usize, 13, 7, 16].iter().cycle() {
24258 if pos >= ids.len() {
24259 break;
24260 }
24261 let end = (pos + n).min(ids.len());
24262 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24263 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24264 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
24265 pos = end;
24266 }
24267 for _ in 0..23 {
24268 let e = a.embed_single(((pos * 7) % 64) as u32);
24269 let ha = a.forward_layers(&e, pos, None);
24270 let hb = b.forward_layers(&e, pos, None);
24271 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
24272 for li in 0..3 {
24273 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
24274 }
24275 pos += 1;
24276 }
24277 for round in 0..9 {
24278 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
24279 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
24280 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
24281 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
24282 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
24283 if round % 2 == 1 {
24284 for p in [&mut a, &mut b] {
24286 for l in &mut p.kv_cache.layers {
24287 l.truncate_last(2);
24288 }
24289 }
24290 } else {
24291 pos += 2;
24292 }
24293 }
24294 let e = a.embed_single(9);
24295 let (ha, hb) = (
24296 a.forward_layers(&e, pos, None),
24297 b.forward_layers(&e, pos, None),
24298 );
24299 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
24300 for li in 0..3 {
24301 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
24302 assert!(lb.base() > 0, "layer {li} trimmed");
24303 assert_eq!(la.base(), 0);
24304 assert_eq!(lb.pos_len(), la.seq_len);
24305 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
24306 }
24307 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
24308 assert_eq!(
24309 (gb.base(), gb.seq_len),
24310 (0, ga.seq_len),
24311 "the global layer keeps all"
24312 );
24313 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
24314 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
24315 }
24316
24317 #[test]
24322 fn swa_trim_wire_handoff_continues_bitwise() {
24323 let mut a = spark_test_pipeline(None);
24324 let mut b = spark_test_pipeline(Some((2, 4)));
24325 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
24326 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24327 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24328 assert_eq!(f32_bits(&ha), f32_bits(&hb));
24329 let mut pos = ids.len();
24330 for _ in 0..5 {
24331 let e = a.embed_single(((pos * 5) % 64) as u32);
24332 assert_eq!(
24333 f32_bits(&a.forward_layers(&e, pos, None)),
24334 f32_bits(&b.forward_layers(&e, pos, None))
24335 );
24336 pos += 1;
24337 }
24338 let mut c = spark_test_pipeline(Some((2, 4)));
24339 for li in 0..4 {
24340 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
24341 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
24342 }
24343 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
24344 assert_eq!(c.kv_cache.seq_len(), pos);
24345 for step in 0..17 {
24346 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
24347 let ha = a.forward_layers(&e, pos, None);
24348 let hc = c.forward_layers(&e, pos, None);
24349 assert_eq!(
24350 f32_bits(&ha),
24351 f32_bits(&hc),
24352 "after the hand-off, step {step}"
24353 );
24354 pos += 1;
24355 }
24356 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
24357 }
24358
24359 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
24364 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
24365 let synth = |len: usize, salt: usize| -> Vec<f32> {
24366 (0..len)
24367 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
24368 .collect()
24369 };
24370 let qt = |rows: usize, cols: usize, salt: usize| {
24371 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24372 };
24373 let layers = widths
24374 .iter()
24375 .copied()
24376 .enumerate()
24377 .map(|(k, inter)| {
24378 let s = 500 + k * 40;
24379 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
24380 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
24381 MtpModule {
24382 enorm: vec![1.0; hs],
24383 hnorm: vec![1.0; hs],
24384 eh_proj: qt(hs, 2 * hs, s),
24385 layer: LayerWeights {
24386 input_norm: vec![1.0; hs],
24387 post_norm: vec![1.0; hs],
24388 attn_out_norm: None,
24389 ffn_out_norm: None,
24390 layer_scale: None,
24391 attn: AttnKind::Full {
24392 wq: qt(nh * hd, hs, s + 1),
24393 wk: qt(nkv * hd, hs, s + 2),
24394 wv: qt(nkv * vd, hs, s + 3),
24395 wo: qt(hs, nh * vd, s + 4),
24396 q_norm: None,
24397 k_norm: None,
24398 output_gate: false,
24399 softplus_gate: None,
24400 bias: None,
24401 },
24402 ffn: FfnKind::Dense(DenseFfn {
24403 gate_proj: qt(inter, hs, s + 5),
24404 up_proj: qt(inter, hs, s + 6),
24405 down_proj: qt(hs, inter, s + 7),
24406 act: Act::Silu,
24407 down_t: None,
24408 segs: Vec::new(),
24409 }),
24410 },
24411 final_norm: vec![1.0; hs],
24412 kv,
24413 }
24414 })
24415 .collect();
24416 mimo_mtp::MimoMtp::from_layers(layers)
24417 }
24418
24419 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24420 mimo_test_mtp_widths(&vec![24; n], gain)
24421 }
24422
24423 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24424 p.clear_sequence_state();
24425 p.speculative = spec;
24426 p.ignore_eos = true;
24427 p.sampler_config.temperature = 0.0;
24428 p.generate_from_ids(ids, n, None, None).unwrap()
24429 }
24430
24431 #[test]
24432 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24433 let mut p = mimo_test_pipeline();
24434 let widths = [7usize, 13, 29];
24435 assert_ne!(widths[0], p.intermediate_size);
24436 assert_ne!(widths[1], p.intermediate_size);
24437 assert_ne!(widths[2], p.intermediate_size);
24438 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24439 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24440
24441 let trunk = p.probe_ffn_mass(&ids);
24442 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24443 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24444 assert_eq!(got.len(), widths.len());
24445 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24446 assert_eq!(stat.mass.len(), width, "draft layer {k}");
24447 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24448 assert_eq!(stat.observed_positions, ids.len() - k - 1);
24449 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24450 assert!(stat
24451 .mass
24452 .iter()
24453 .zip(&stat.squared_mass)
24454 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24455 }
24456 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24457 let st = p.mimo_mtp.as_ref().unwrap();
24458 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24459
24460 let h = p.hidden_size;
24463 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24464 panic!("synthetic MiMo MTP must be dense")
24465 };
24466 dense.segs.push(FfnSeg {
24467 gate: QTensor::from_f32(vec![0.0; h], 1, h),
24468 up: QTensor::from_f32(vec![0.0; h], 1, h),
24469 down: QTensor::from_f32(vec![0.0; h], h, 1),
24470 start: dense.gate_proj.rows(),
24471 width: 1,
24472 });
24473 assert!(p
24474 .probe_mtp_ffn(&ids)
24475 .unwrap_err()
24476 .contains("tube-segmented draft FFNs are not supported"));
24477 assert_eq!(p.kv_cache.seq_len(), 0);
24478 assert!(p
24479 .mimo_mtp
24480 .as_ref()
24481 .unwrap()
24482 .layers
24483 .iter()
24484 .all(|m| m.kv.seq_len == 0));
24485 }
24486
24487 #[test]
24488 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24489 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24490 p.clear_sequence_state();
24491 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24492 let _exact = ExactMtpCalibrationGuard::enter();
24493 let got = crate::gpu::cpu_scope(|| {
24494 let trunk = p.probe_trunk_rows(ids)?;
24495 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24496 })
24497 .expect("teacher-forced MiMo probe");
24498 st.reset();
24499 p.mimo_mtp = Some(st);
24500 p.reset_mtp_probe_state();
24501 got
24502 }
24503
24504 for post_norm_hidden in [false, true] {
24505 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24506 let make = || {
24507 let mut p = mimo_test_pipeline();
24508 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24509 st.post_norm_hidden = post_norm_hidden;
24510 p.mimo_mtp = Some(st);
24511 p
24512 };
24513 let mut full = make();
24514 let expected = collect(&mut full, &ids, usize::MAX);
24515 for rows in [1usize, 7, 128] {
24516 let mut chunked = make();
24517 let got = collect(&mut chunked, &ids, rows);
24518 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24519 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24520 assert_eq!(
24521 got.observed_positions, expected.observed_positions,
24522 "post={post_norm_hidden}, rows={rows}, layer={layer}"
24523 );
24524 assert_eq!(
24525 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24526 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24527 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24528 );
24529 assert_eq!(
24530 got.squared_mass
24531 .iter()
24532 .map(|v| v.to_bits())
24533 .collect::<Vec<_>>(),
24534 expected
24535 .squared_mass
24536 .iter()
24537 .map(|v| v.to_bits())
24538 .collect::<Vec<_>>(),
24539 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24540 );
24541 }
24542 }
24543 }
24544 }
24545
24546 #[test]
24552 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24553 for post in [false, true] {
24556 let mut p = mimo_test_pipeline();
24557 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24559 let mut st0 = mimo_test_mtp(3, 1.0);
24560 st0.post_norm_hidden = post;
24561 p.mimo_mtp = Some(st0);
24562 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24563 let hs = p.hidden_size;
24564 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24565 p.mimo_note_rows(&hb, 0);
24566 let mut st = p.mimo_mtp.take().unwrap();
24567 let k = 3;
24570 let mut inc = Vec::new();
24571 for t in 0..ids.len() - k - 1 {
24572 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24573 }
24574 let s = ids.len();
24577 let mut reference = vec![vec![0u32; k]; s - k - 1];
24578 let mut fresh = mimo_test_mtp(3, 1.0);
24579 for (layer, m) in fresh.layers.iter_mut().enumerate() {
24580 let n = s - layer - 1;
24581 let mut cats = vec![0.0f32; n * 2 * hs];
24582 for j in 0..n {
24583 let e = p.embed_single(ids[j + layer + 1]);
24584 let raw = &hb[j * hs..(j + 1) * hs];
24585 let g = if post {
24586 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24587 } else {
24588 raw.to_vec()
24589 };
24590 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24591 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24592 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24593 }
24594 let mut x = vec![0.0f32; n * hs];
24595 m.eh_proj.matmat(&cats, n, &mut x, None);
24596 p.mimo_mtp_block(m, &mut x, n, 0);
24597 for (t, row) in reference.iter_mut().enumerate() {
24598 let y = inference::rms_norm(
24599 &x[t * hs..(t + 1) * hs],
24600 &m.final_norm,
24601 p.rms_eps,
24602 p.norm_style,
24603 );
24604 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24605 }
24606 }
24607 assert_eq!(inc, reference, "post_norm_hidden = {post}");
24608 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24610 assert!(distinct.len() > 3, "{inc:?}");
24611 let last_t = ids.len() - k - 2;
24613 for m in &st.layers {
24614 assert_eq!(m.kv.seq_len, last_t + 1);
24615 }
24616 }
24617 }
24618
24619 #[test]
24626 fn mimo_speculative_greedy_equals_plain_greedy() {
24627 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24628 let _env = ScopedTestEnv::set(&[
24629 ("CMF_GPU", Some("0")),
24630 ("CMF_GPU_WGPU_GRAPH", Some("0")),
24631 ]);
24632 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24633 let n = 24;
24634 let mut p = mimo_test_pipeline();
24635 let plain = mimo_greedy(&mut p, &ids, n, false);
24636 assert_eq!(plain.mtp_drafted, 0);
24637 assert_eq!(plain.token_ids.len(), n);
24638 let plain_kv = p.kv_cache.layers[0].seq_len;
24639
24640 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24642 let spec = mimo_greedy(&mut p, &ids, n, true);
24643 assert!(spec.mtp_drafted > 0, "the round must draft");
24644 assert_eq!(spec.token_ids, plain.token_ids);
24645 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24646
24647 let mut truth: Vec<u32> = ids.clone();
24650 truth.extend(&plain.token_ids);
24651 let mut noisy = truth.clone();
24652 for (i, t) in noisy.iter_mut().enumerate() {
24653 if i % 5 == 0 {
24654 *t = (*t + 1) % 64;
24655 }
24656 }
24657 let mut st = mimo_test_mtp(3, 1.0);
24658 st.draft_override = Some(noisy);
24659 p.mimo_mtp = Some(st);
24660 let spec = mimo_greedy(&mut p, &ids, n, true);
24661 assert_eq!(spec.token_ids, plain.token_ids);
24662 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24663 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24664 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24665 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24666 assert!(
24667 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24668 "{:?}",
24669 stats.accept_hist
24670 );
24671 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24672
24673 let mut st = mimo_test_mtp(3, 1.0);
24676 st.draft_override = Some(truth);
24677 p.mimo_mtp = Some(st);
24678 let spec = mimo_greedy(&mut p, &ids, n, true);
24679 assert_eq!(spec.token_ids, plain.token_ids);
24680 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24681 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24682
24683 let off = mimo_greedy(&mut p, &ids, n, false);
24685 assert_eq!(off.token_ids, plain.token_ids);
24686 assert_eq!(off.mtp_drafted, 0);
24687 }
24688
24689 #[test]
24698 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24699 let p = mimo_test_pipeline();
24700 assert_eq!(
24701 p.graph_attn_decline_reason(),
24702 Some("per-layer KV head counts")
24703 );
24704 assert_eq!(p.wgpu_graph_attn_decline(), None);
24705 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24706 assert_eq!(
24707 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24708 (1, 4, 4, None, false)
24709 );
24710 assert_eq!(g0.invf, p.inv_freq.as_slice());
24711 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24712 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24713 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24714 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24715 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24716 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24717 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24718
24719 let emb = p.embed_single(3);
24722 let mut lg = Vec::new();
24723 assert!(
24724 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24725 .is_none()
24726 );
24727 let mut hid = emb.clone();
24728 assert_eq!(
24729 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24730 crate::gpu::BatchGraphOutcome::Declined
24731 );
24732 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24733 assert!(p.try_multi_burst(3, 0, 4).is_none());
24734 assert!(
24735 p.graph_declines().is_empty(),
24736 "no attention decline logged: {:?}",
24737 p.graph_declines()
24738 );
24739 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24744 assert_eq!(plain().graph_attn_decline_reason(), None);
24745 assert_eq!(plain().wgpu_graph_attn_decline(), None);
24746 assert!(
24747 plain().graph_attn_geom(0).is_none(),
24748 "uniform models keep the historical arms"
24749 );
24750 let mut q = plain();
24751 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24752 assert_eq!(
24753 q.graph_attn_decline_reason(),
24754 Some("learned attention sinks")
24755 );
24756 assert_eq!(
24757 q.graph_attn_geom(1).unwrap().sink,
24758 Some(&[0.25f32, -0.25][..])
24759 );
24760 let mut q = plain();
24761 q.set_attn_geometry(None, Some(2)).unwrap();
24762 assert_eq!(
24763 q.graph_attn_decline_reason(),
24764 Some("V heads narrower than Q/K heads")
24765 );
24766 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24767 let mut q = plain();
24768 q.sliding_layers = Some(vec![true, false]);
24769 q.swa = Some((4, usize::MAX));
24770 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24771 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24772 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24773
24774 let mut q = mimo_test_pipeline();
24778 q.rope_scale = 2.0;
24779 q.rope_scale_local = 1.25;
24780 assert_eq!(q.wgpu_graph_attn_decline(), None);
24781 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24782 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24783
24784 let mut q = plain();
24787 q.rope_scale = 2.0;
24788 assert_eq!(
24789 q.wgpu_graph_attn_decline(),
24790 Some("scaled RoPE without per-layer geometry")
24791 );
24792 }
24793
24794 #[cfg(target_os = "macos")]
24800 #[test]
24801 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24802 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24806 p.swa = Some((16, usize::MAX));
24807 p.sliding_layers = Some(vec![true, true]);
24808 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24809 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24810
24811 assert_eq!(
24812 p.graph_attn_decline_reason(),
24813 Some("learned attention sinks")
24814 );
24815 let swa_graph = p.metal_graph_swa();
24816 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24817 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24818 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24819 assert!(
24820 native_sink_attend,
24821 "the q1 sink admission must follow the SWA door"
24822 );
24823 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24826 }
24827
24828 #[test]
24829 fn mimo_verify_rewind_preserves_lagging_host_caches() {
24830 let mut p = mimo_test_pipeline();
24831 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24832 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24833 for _ in 0..if li == 0 { 2 } else { 12 } {
24834 layer.append(&row, &row, &[]);
24835 }
24836 }
24837 p.mimo_verify_rewind(9).unwrap();
24838 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24839 for layer in &p.kv_cache.layers[1..] {
24840 assert_eq!(layer.seq_len, 9);
24841 }
24842 }
24843
24844 #[test]
24848 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24849 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24850 let _ = std::fs::remove_dir_all(&dir);
24851 let mut p = mimo_test_pipeline();
24852 let hs = p.hidden_size;
24853 let ids = [5u32, 9, 11, 2, 40];
24854 p.layer_dump = Some(dir.join("decode"));
24855 for (pos, &id) in ids.iter().enumerate() {
24856 let e = p.embed_single(id);
24857 let _ = p.forward_layers(&e, pos, None);
24858 }
24859 p.clear_sequence_state();
24860 p.layer_dump = Some(dir.join("prefill"));
24861 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24862 for pos in 0..ids.len() {
24863 for li in 0..p.num_layers {
24864 let name = format!("p{pos:06}_l{li:02}.f32");
24865 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24866 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24867 assert_eq!(a.len(), hs * 4, "{name}");
24868 assert_eq!(a, b, "{name}");
24869 }
24870 }
24871 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24872 let vals: Vec<f32> = last
24873 .chunks(4)
24874 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24875 .collect();
24876 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24877 let _ = std::fs::remove_dir_all(&dir);
24878 }
24879
24880 #[test]
24881 fn attn_geometry_and_sinks_are_validated() {
24882 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24883 assert!(
24884 p.set_attn_geometry(Some(vec![2]), None).is_err(),
24885 "one entry per layer"
24886 );
24887 assert!(
24888 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24889 "3 does not divide 4"
24890 );
24891 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24892 assert!(p.set_attn_geometry(None, Some(0)).is_err());
24893 assert!(
24894 p.set_attn_geometry(None, Some(5)).is_err(),
24895 "V wider than the head"
24896 );
24897 p.set_attn_geometry(None, Some(4)).unwrap();
24898 assert_eq!(
24899 p.v_head_dim, None,
24900 "v_head_dim == head_dim is the uniform case"
24901 );
24902 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24903 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24904 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24905 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24906 assert!(
24907 p.kv_cache.layers[1].sinks.is_some(),
24908 "a reshape keeps the layer's sinks"
24909 );
24910 assert_eq!(p.layer_geom(1).0, 4);
24911 assert!(
24912 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24913 "one sink per Q head"
24914 );
24915 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24916 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24917 }
24918
24919 #[test]
24922 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24923 let cfg = || {
24924 Some(crate::nystrom::O1Cfg {
24925 layers: crate::nystrom::O1Layers::All,
24926 m: 4,
24927 w: 8,
24928 sink: 2,
24929 rect: crate::nystrom::O1Rect::Aggregate,
24930 })
24931 };
24932 let mut p = mimo_test_pipeline();
24933 p.set_o1(cfg());
24934 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24935 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24936 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24937 q.sliding_layers = Some(vec![false, false, true]);
24938 q.swa = Some((4, usize::MAX));
24939 q.set_o1(cfg());
24940 assert_eq!(q.o1_flags, vec![true, false, false]);
24941 }
24942
24943 #[test]
24944 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24945 const B: usize = 19;
24946 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24947 p.set_o1(Some(crate::nystrom::O1Cfg {
24948 layers: crate::nystrom::O1Layers::All,
24949 m: 4,
24950 w: 8,
24951 sink: 2,
24952 rect: crate::nystrom::O1Rect::Aggregate,
24953 }));
24954 p.o1_begin_with_prefix(Some(B));
24955 let ids: Vec<u32> = (0..B as u32).collect();
24956 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24957
24958 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24959 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24960 let next = p.embed_single(B as u32);
24961 let _ = p.forward_layers(&next, B, None);
24962 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24963 }
24964
24965 #[test]
24966 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24967 const B: usize = 19;
24968 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24969 let gdn_cfg = crate::linear_core::GdnCfg {
24973 num_v_heads: 2,
24974 num_k_heads: 1,
24975 key_head_dim: 2,
24976 value_head_dim: 4,
24977 conv_kernel: 3,
24978 hidden_size: 8,
24979 rms_eps: 1e-6,
24980 output_gate_sigmoid: false,
24981 };
24982 let synth = |n: usize, salt: usize| -> Vec<f32> {
24983 (0..n)
24984 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24985 .collect()
24986 };
24987 let qt = |rows: usize, cols: usize, salt: usize| {
24988 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24989 };
24990 let c_dim = gdn_cfg.conv_dim();
24991 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24992 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24993 in_proj_qkv: qt(c_dim, 8, 1),
24994 in_proj_z: qt(vd, 8, 2),
24995 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24996 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24997 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24998 a_log: vec![0.2, 0.5],
24999 dt_bias: synth(gdn_cfg.num_v_heads, 6),
25000 norm: vec![1.0; gdn_cfg.value_head_dim],
25001 out_proj: qt(8, vd, 7),
25002 });
25003 p.gdn_cfg = Some(gdn_cfg);
25004 p.set_o1(Some(crate::nystrom::O1Cfg {
25005 layers: crate::nystrom::O1Layers::All,
25006 m: 4,
25007 w: 8,
25008 sink: 2,
25009 rect: crate::nystrom::O1Rect::Aggregate,
25010 }));
25011 p.o1_begin_with_prefix(Some(B));
25012 for pos in 0..B - 2 {
25013 let emb = p.embed_single(pos as u32);
25014 let _ = p.forward_layers(&emb, pos, None);
25015 }
25016 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
25017
25018 let e1 = p.embed_single((B - 2) as u32);
25019 let e2 = p.embed_single((B - 1) as u32);
25020 let _ = p.forward_pair(&e1, &e2, B - 2);
25021
25022 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
25023 assert!(
25024 p.kv_cache
25025 .layers
25026 .iter()
25027 .enumerate()
25028 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
25029 );
25030 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
25031 assert_ne!(
25032 p.kv_cache.layers[0].linear_state, lane1_state,
25033 "real pair must commit GDN lane 2 before returning"
25034 );
25035 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
25036 let next = p.embed_single(B as u32);
25037 let _ = p.forward_layers(&next, B, None);
25038 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
25039 }
25040
25041 #[test]
25042 fn o1_error_observation_stays_terminal_until_reset() {
25043 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25044 p.set_o1(Some(crate::nystrom::O1Cfg {
25045 layers: crate::nystrom::O1Layers::All,
25046 m: 4,
25047 w: 8,
25048 sink: 2,
25049 rect: crate::nystrom::O1Rect::Aggregate,
25050 }));
25051 p.o1_begin();
25052 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
25053
25054 assert!(p.o1_seal_checked().is_err());
25055 assert!(
25056 p.o1_seal_checked().is_err(),
25057 "retry must see the sticky error"
25058 );
25059 let k = vec![0.2f32; 4];
25060 let v = vec![0.3f32; 4];
25061 p.kv_cache.layers[0].append(&k, &v, &[]);
25062 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
25063
25064 p.reset_session();
25065 p.o1_begin();
25066 p.kv_cache.layers[0].append(&k, &v, &[]);
25067 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
25068 }
25069
25070 #[test]
25071 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
25072 let ids = vec![1u32, 2, 3, 4, 5, 6];
25073 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25074 p.graph_logits = Some(vec![123.0]);
25075 p.graph_want_logits = true;
25076 p.graph_failed
25077 .store(true, std::sync::atomic::Ordering::Relaxed);
25078 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25079 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
25080 assert!(err.contains("before NLL"));
25081 assert!(p.graph_logits.is_none());
25082 assert!(!p.graph_want_logits);
25083 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25084 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25085
25086 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25087 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25088 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25089 assert_eq!(actual.1, expected.1);
25090 assert!((actual.0 - expected.0).abs() < 1e-9);
25091 }
25092
25093 #[test]
25094 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
25095 let ids = vec![1u32, 2, 3, 4, 5, 6];
25096 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25097 p.nll_test_fail_at = Some(1);
25098 let err = p
25099 .nll_ids_from(&ids, 0)
25100 .expect_err("one-shot forward failure");
25101 assert!(err.contains("forward") || err.contains("score row"));
25102 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25103 assert!(!p.graph_want_logits);
25104 assert!(p.graph_logits.is_none());
25105 assert!(p.kv_history.is_empty());
25106
25107 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25108 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25109 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25110 assert_eq!(actual.1, expected.1);
25111 assert!((actual.0 - expected.0).abs() < 1e-9);
25112 }
25113
25114 #[test]
25115 fn nll_serial_failure_before_first_row_is_reported() {
25116 let ids = vec![1u32, 2, 3, 4];
25117 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25118 p.nll_test_force_serial = true;
25119 p.nll_test_fail_at = Some(0);
25120 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
25121 assert!(err.contains("serial forward"));
25122 assert!(p.kv_history.is_empty());
25123 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25124 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25125 }
25126
25127 #[test]
25128 fn ffn_probe_failure_discards_recorder_and_state() {
25129 let ids = vec![1u32, 2, 3, 4];
25130 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25131 p.nll_test_fail_at = Some(0);
25132 let err = p
25133 .probe_ffn_mass_batch(&ids)
25134 .expect_err("probe forward failure");
25135 assert!(err.contains("NLL"));
25136 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
25137 assert!(p.kv_history.is_empty());
25138 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25139 }
25140
25141 #[test]
25142 fn nll_test_controls_are_pipeline_scoped() {
25143 let ids = vec![1u32, 2, 3, 4];
25144 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25145 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25146 failing.nll_test_force_serial = true;
25147 failing.nll_test_fail_at = Some(0);
25148
25149 assert!(!failing.can_prefill_batched());
25150 assert!(unaffected.can_prefill_batched());
25151 let expected = unaffected
25152 .nll_ids_from(&ids, 0)
25153 .expect("unaffected pipeline remains usable");
25154 let err = failing
25155 .nll_ids_from(&ids, 0)
25156 .expect_err("failure injection belongs to failing pipeline");
25157 assert!(err.contains("serial forward"));
25158 assert!(failing.nll_test_fail_at.is_none());
25159 assert!(unaffected.can_prefill_batched());
25160 let actual = unaffected
25161 .nll_ids_from(&ids, 0)
25162 .expect("unaffected pipeline remains reusable");
25163 assert_eq!(actual.1, expected.1);
25164 assert!((actual.0 - expected.0).abs() < 1e-9);
25165 }
25166
25167 #[test]
25168 fn forward_ids_failure_channel_is_terminal_and_reusable() {
25169 let ids = vec![1u32, 2, 3, 4, 5, 6];
25170 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25171 p.graph_logits = Some(vec![123.0]);
25172 p.graph_want_logits = true;
25173 p.graph_failed
25174 .store(true, std::sync::atomic::Ordering::Relaxed);
25175 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25176
25177 let err = p
25178 .forward_ids(&ids, None)
25179 .expect_err("a failed forward must not become a valid head result");
25180 assert!(err.contains("forward_ids setup"));
25181 assert!(p.graph_logits.is_none());
25182 assert!(!p.graph_want_logits);
25183 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25184 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25185 assert_eq!(p.kv_cache.seq_len(), 0);
25186
25187 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
25188 .forward_ids(&ids, None)
25189 .expect("fresh forward_ids");
25190 let actual = p
25191 .forward_ids(&ids, None)
25192 .expect("pipeline remains reusable after a failed forward");
25193 assert_eq!(actual.len(), expected.len());
25194 assert!(
25195 actual
25196 .iter()
25197 .zip(expected)
25198 .all(|(a, b)| (a - b).abs() < 1e-9)
25199 );
25200 assert_eq!(p.kv_cache.seq_len(), ids.len());
25201 }
25202
25203 #[test]
25204 fn sigmoid_router_floor_is_explicit_per_architecture() {
25205 let zero = || DenseFfn {
25211 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25212 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25213 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25214 act: Act::Silu,
25215 down_t: None,
25216 segs: Vec::new(),
25217 };
25218 let m = MoeFfn {
25219 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25220 experts: vec![zero(), zero()],
25221 top_k: 1,
25222 norm_topk_prob: true,
25223 router_sigmoid: true,
25224 sigmoid_denom_eps: 1e-20,
25225 expert_bias: None,
25226 routed_scaling: 2.5,
25227 route_tau: None,
25228 shared: None,
25229 stats: std::cell::RefCell::new(Vec::new()),
25230 act_sq: std::cell::RefCell::new(Vec::new()),
25231 act_rows: std::cell::RefCell::new(Vec::new()),
25232 mask: None,
25233 per_expert_scale: None,
25234 router_input_norm: false,
25235 resonance: None,
25236 grown: Vec::new(),
25237 };
25238 let logits = [-20.0f32, -20.0];
25239 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
25240 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25241 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
25242 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
25243 assert!((flint_wsum - reference_expected).abs() < 1e-15);
25244 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
25245 assert!(legacy_wsum > minimax_wsum * 100.0);
25246 }
25247
25248 #[test]
25249 fn resonance_scores_match_formula_and_stable_tie() {
25250 let r = Resonance {
25251 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
25253 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
25254 k: 1,
25255 bias: vec![1.5, 0.5, 0.0],
25256 shell: Vec::new(),
25257 };
25258 let x = [1.0f32, 1.0];
25259 let mut got = vec![0.0; 3];
25260 r.scores(&x, &mut got);
25261 assert!((got[0] - 0.5).abs() < 1e-6);
25265 assert!((got[1] - 0.5).abs() < 1e-6);
25266 assert!(got[2].abs() < 1e-6);
25267 let best = got
25268 .iter()
25269 .enumerate()
25270 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
25271 .map(|(i, _)| i);
25272 assert_eq!(best, Some(0));
25273 assert!(got.iter().all(|v| v.is_finite()));
25274 }
25275
25276 #[test]
25281 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
25282 let plain = Resonance {
25288 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
25289 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
25290 k: 1,
25291 bias: vec![1.5, 0.5, 0.0, 0.0],
25292 shell: Vec::new(),
25293 };
25294 let shelled = Resonance {
25295 mu: plain.mu.clone(),
25296 u: plain.u.clone(),
25297 k: 1,
25298 bias: plain.bias.clone(),
25299 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
25300 };
25301 assert!(!plain.has_shell());
25302 assert!(shelled.has_shell());
25303 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
25304 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
25305 set_growth_shell(Some(true));
25306 assert!(growth_shell_enabled());
25307 let x = [1.0f32, 1.0];
25310 plain.scores(&x, &mut a);
25311 shelled.scores(&x, &mut b);
25312 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
25313 assert!(a[2] == 0.0 && a[3] == 0.0);
25314 let xo = [3.0f32, 0.0];
25317 plain.scores(&xo, &mut a);
25318 shelled.scores(&xo, &mut b);
25319 assert_eq!(a[2], -6.0);
25320 assert_eq!(a[3], -6.0);
25321 assert_eq!(
25322 bits(&a[..3]),
25323 bits(&b[..3]),
25324 "trunk rows + the boundary row"
25325 );
25326 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
25327 assert_eq!(shelled.effective_shell(4), shelled.shell);
25328 set_growth_shell(Some(false));
25331 assert!(!growth_shell_enabled());
25332 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
25333 shelled.scores(&xo, &mut b);
25334 assert_eq!(bits(&a), bits(&b));
25335 set_growth_shell(None);
25336 let short = Resonance {
25339 shell: vec![f32::INFINITY, f32::INFINITY],
25340 ..shelled
25341 };
25342 set_growth_shell(Some(true));
25343 short.scores(&xo, &mut b);
25344 assert_eq!(bits(&a), bits(&b));
25345 set_growth_shell(None);
25346 }
25347
25348 #[test]
25352 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
25353 let zero = || DenseFfn {
25354 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25355 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25356 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25357 act: Act::Silu,
25358 down_t: None,
25359 segs: Vec::new(),
25360 };
25361 let moe = |sigmoid: bool| MoeFfn {
25362 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
25363 experts: vec![zero(), zero(), zero(), zero()],
25364 top_k: 1,
25365 norm_topk_prob: true,
25366 router_sigmoid: sigmoid,
25367 sigmoid_denom_eps: 1e-6,
25368 expert_bias: None,
25369 routed_scaling: 1.0,
25370 route_tau: None,
25371 shared: None,
25372 stats: std::cell::RefCell::new(Vec::new()),
25373 act_sq: std::cell::RefCell::new(Vec::new()),
25374 act_rows: std::cell::RefCell::new(Vec::new()),
25375 mask: None,
25376 per_expert_scale: None,
25377 router_input_norm: false,
25378 resonance: None,
25379 grown: Vec::new(),
25380 };
25381 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
25382 for sigmoid in [false, true] {
25383 let m = moe(sigmoid);
25384 let (idx, p, wsum) = moe_route(&logits, &m, None);
25385 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
25386 assert_eq!(p[1], 0.0);
25387 assert_eq!(p[3], 0.0);
25388 assert!(p[2] > p[0] && p[0] > 0.0);
25389 assert!(p.iter().all(|v| v.is_finite()));
25390 let w = p[2] / wsum;
25391 if sigmoid {
25392 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
25394 } else {
25395 assert_eq!(
25396 w, 1.0,
25397 "softmax: the renormalized top-1 weight is exactly 1"
25398 );
25399 }
25400 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
25403 assert_eq!(idx, vec![2]);
25404 }
25405 let m = moe(false);
25407 let (idx, _, _) = moe_route(
25408 &[
25409 f32::NEG_INFINITY,
25410 f32::NEG_INFINITY,
25411 f32::NEG_INFINITY,
25412 -9.0,
25413 ],
25414 &m,
25415 None,
25416 );
25417 assert_eq!(idx, vec![3]);
25418 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25421 assert_eq!(idx, vec![0]);
25422 assert!(p.iter().all(|&v| v == 0.25));
25423 assert!(wsum.is_finite() && wsum > 0.0);
25424 }
25425
25426 #[cfg(feature = "gpu")]
25432 #[test]
25433 fn minimax_device_route_matches_host_sigmoid_contract() {
25434 match crate::gpu_wgpu::selected_and_up() {
25435 None => {
25436 crate::gpu_wgpu::skip_or_fail(module_path!());
25437 return;
25438 }
25439 Some(false) => {
25447 eprintln!(
25448 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25449 module_path!()
25450 );
25451 return;
25452 }
25453 Some(true) => {}
25454 }
25455 let n = 128usize;
25456 let top_k = 4usize;
25457 let logits: Vec<f32> = (0..n)
25458 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25459 .collect();
25460 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25461 let m = MoeFfn {
25462 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25463 experts: Vec::new(),
25464 top_k,
25465 norm_topk_prob: true,
25466 router_sigmoid: true,
25467 sigmoid_denom_eps: 1e-20,
25468 expert_bias: Some(bias.clone()),
25469 routed_scaling: 2.0,
25470 route_tau: None,
25471 shared: None,
25472 stats: std::cell::RefCell::new(Vec::new()),
25473 act_sq: std::cell::RefCell::new(Vec::new()),
25474 act_rows: std::cell::RefCell::new(Vec::new()),
25475 mask: None,
25476 per_expert_scale: None,
25477 router_input_norm: false,
25478 resonance: None,
25479 grown: Vec::new(),
25480 };
25481 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25482 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25483
25484 for shared in [Some(n as u32), None] {
25485 let mut gi = Vec::new();
25486 let mut gw = Vec::new();
25487 assert!(
25488 crate::gpu_wgpu::minimax_moe_route_for_test(
25489 &logits,
25490 Some(&bias),
25491 top_k,
25492 2.0,
25493 shared,
25494 &mut gi,
25495 &mut gw,
25496 ),
25497 "the device did not come up — the test checked nothing"
25498 );
25499 let routed = if shared.is_some() { top_k } else { gi.len() };
25500 assert_eq!(
25501 &gi[..routed],
25502 &ci[..],
25503 "different experts or order (shared={shared:?})"
25504 );
25505 let d = cw
25506 .iter()
25507 .zip(&gw[..routed])
25508 .map(|(a, b)| (a - b).abs())
25509 .fold(0.0f32, f32::max);
25510 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25511 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25512 if let Some(s) = shared {
25513 assert_eq!(gi[top_k], s as usize, "shared pin slot");
25514 assert_eq!(gw[top_k], 1.0, "shared weight");
25515 }
25516 }
25517 }
25518
25519 #[cfg(feature = "gpu")]
25524 #[test]
25525 fn minimax_device_cold_weights_match_host_contract() {
25526 match crate::gpu_wgpu::selected_and_up() {
25527 None => {
25528 crate::gpu_wgpu::skip_or_fail(module_path!());
25529 return;
25530 }
25531 Some(false) => {
25539 eprintln!(
25540 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25541 module_path!()
25542 );
25543 return;
25544 }
25545 Some(true) => {}
25546 }
25547 let n = 128usize;
25548 let top_k = 4usize;
25549 let logits: Vec<f32> = (0..n)
25550 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25551 .collect();
25552 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25553 let m = MoeFfn {
25554 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25555 experts: Vec::new(),
25556 top_k,
25557 norm_topk_prob: true,
25558 router_sigmoid: true,
25559 sigmoid_denom_eps: 1e-20,
25560 expert_bias: Some(bias.clone()),
25561 routed_scaling: 2.0,
25562 route_tau: None,
25563 shared: None,
25564 stats: std::cell::RefCell::new(Vec::new()),
25565 act_sq: std::cell::RefCell::new(Vec::new()),
25566 act_rows: std::cell::RefCell::new(Vec::new()),
25567 mask: None,
25568 per_expert_scale: None,
25569 router_input_norm: false,
25570 resonance: None,
25571 grown: Vec::new(),
25572 };
25573 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25574 assert_eq!(ci.len(), top_k);
25575
25576 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25578 let mut cold = Vec::new();
25579 let mut idx = Vec::new();
25580 let mut dw = Vec::new();
25581 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25582 &logits,
25583 Some(&bias),
25584 top_k,
25585 2.0,
25586 &resident,
25587 n as u32,
25588 &mut cold,
25589 &mut idx,
25590 &mut dw,
25591 ));
25592 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25593 for (r, &e) in ci.iter().enumerate() {
25598 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25599 let host = p[e] / wsum;
25600 let d = (dw[r] - host).abs();
25601 assert!(
25602 d <= 1e-6 * host.abs().max(1.0),
25603 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25604 dw[r]
25605 );
25606 }
25607 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25608 assert_eq!(dw[top_k], 1.0, "shared weight");
25609
25610 let mut remap = resident.clone();
25612 remap[ci[0]] = u32::MAX;
25613 remap[ci[2]] = u32::MAX;
25614 let mut cold = Vec::new();
25615 let mut idx2 = Vec::new();
25616 let mut dw2 = Vec::new();
25617 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25618 &logits,
25619 Some(&bias),
25620 top_k,
25621 2.0,
25622 &remap,
25623 n as u32,
25624 &mut cold,
25625 &mut idx2,
25626 &mut dw2,
25627 ));
25628 let mut resident_weight_checked = 0usize;
25636 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25637 if dw2[r] == 0.0 {
25638 continue; }
25640 let e = slot as usize;
25641 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25642 let host = p[e] / wsum;
25643 let d = (dw2[r] - host).abs();
25644 assert!(
25645 d <= 1e-6 * host.abs().max(1.0),
25646 "partial-bank resident weight expert {e}: Δ={d:.3e}"
25647 );
25648 resident_weight_checked += 1;
25649 }
25650 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25651 println!("partial bank: checked {resident_weight_checked} resident weights");
25652 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25653 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25654 ids.sort_unstable();
25655 let mut want = vec![ci[0], ci[2]];
25656 want.sort_unstable();
25657 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25658 for &(e, w) in &cold {
25659 let host = p[e] / wsum;
25660 let d = (w - host).abs();
25661 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25662 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25663 }
25664 }
25665
25666 #[test]
25671 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25672 let zero = || DenseFfn {
25673 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25674 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25675 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25676 act: Act::Silu,
25677 down_t: None,
25678 segs: Vec::new(),
25679 };
25680 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25681 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25682 experts: vec![zero(), zero(), zero()],
25683 top_k: 1,
25684 norm_topk_prob: norm_topk,
25685 router_sigmoid: false,
25686 sigmoid_denom_eps: 1e-6,
25687 expert_bias: None,
25688 routed_scaling: 1.0,
25689 route_tau: None,
25690 shared: None,
25691 stats: std::cell::RefCell::new(Vec::new()),
25692 act_sq: std::cell::RefCell::new(Vec::new()),
25693 act_rows: std::cell::RefCell::new(Vec::new()),
25694 mask: None,
25695 per_expert_scale: None,
25696 router_input_norm: false,
25697 resonance: resonant.then(|| Resonance {
25698 mu: vec![0.0; 6],
25699 u: Vec::new(),
25700 k: 0,
25701 bias: vec![0.0; 3],
25702 shell: Vec::new(),
25703 }),
25704 grown: Vec::new(),
25705 };
25706 let lo = -0.1f32;
25709 let hi = f32::from_bits(lo.to_bits() - 1);
25710 assert!(hi > lo && hi - lo < 2f32.powi(-25));
25711 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25712 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25714 assert_eq!(
25715 idx,
25716 vec![0],
25717 "softmax tie → lower index (the gated contract)"
25718 );
25719 for norm in [true, false] {
25722 let m = moe(true, norm);
25723 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25724 assert_eq!(idx, vec![1], "norm_topk {norm}");
25725 assert_eq!(p, vec![0.0, 1.0, 0.0]);
25726 assert_eq!(p[1] / wsum, 1.0);
25727 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25730 assert_eq!(idx, vec![0]);
25731 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25733 assert_eq!(idx, vec![2]);
25734 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25735 assert_eq!(idx, vec![0]);
25736 assert_eq!(p[0] / wsum, 1.0);
25737 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25740 assert_eq!(idx, vec![0]);
25741 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25742 }
25743 }
25744}