1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16use crate::kv_cache::KvCache;
17use crate::linear_core::{
18 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
19 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
20 vmf_phase_pair,
21};
22use crate::pool::Pool;
23use crate::qtensor::QTensor;
24use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
25use crate::tokenizer::Tokenizer;
26use cortiq_core::mask::TaskMask;
27use cortiq_core::types::NormStyle;
28
29pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
30 std::sync::atomic::AtomicBool::new(false);
31
32thread_local! {
37 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
38}
39
40struct ExactMtpCalibrationGuard;
41
42impl ExactMtpCalibrationGuard {
43 fn enter() -> Self {
44 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
45 Self
46 }
47}
48
49impl Drop for ExactMtpCalibrationGuard {
50 fn drop(&mut self) {
51 EXACT_MTP_CALIBRATION.with(|depth| {
52 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
53 depth.set(depth.get().saturating_sub(1));
54 });
55 }
56}
57
58fn exact_mtp_calibration_active() -> bool {
59 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
60}
61
62fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
66 logits.resize(vocab_size, f32::NEG_INFINITY);
67}
68
69struct ForwardScratch {
73 n1: Vec<f32>,
74 n2: Vec<f32>,
75 p1: Vec<f32>,
76 p2: Vec<f32>,
77}
78
79impl ForwardScratch {
80 fn new(hidden: usize) -> Self {
81 Self {
82 n1: vec![0.0; hidden],
83 n2: vec![0.0; hidden],
84 p1: vec![0.0; hidden],
85 p2: vec![0.0; hidden],
86 }
87 }
88}
89
90pub struct Pipeline {
92 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
97 pub tokenizer: std::sync::Arc<Tokenizer>,
100 pub kv_cache: KvCache,
101 pub sampler_config: SamplerConfig,
102 pub weights: PipelineWeights,
103 pub hidden_size: usize,
104 pub intermediate_size: usize,
105 pub num_heads: usize,
106 pub num_kv_heads: usize,
107 pub head_dim: usize,
108 pub num_layers: usize,
110 pub physical_layers: usize,
112 pub loop_final_norm: bool,
114 pub vocab_size: usize,
115 pub rms_eps: f64,
116 pub rope_base: f32,
117 pub norm_style: NormStyle,
118 pub rotary_dim: usize,
120 pub attention_heads_per_layer: Option<Vec<usize>>,
122 pub kv_heads_per_layer: Option<Vec<usize>>,
127 pub v_head_dim: Option<usize>,
132 pub layer_dump: Option<std::path::PathBuf>,
145 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
148 pub(crate) mimo_moe: crate::mimo_moe::Slot,
151 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
156 pub vmf_cfg: Option<VmfPhaseCfg>,
158 pub gdn_cfg: Option<GdnCfg>,
160 pub logit_multiplier: Option<f32>,
162 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
167 graph_failed: std::sync::atomic::AtomicBool,
172 pub kv_history: Vec<u32>,
177 pub kv_history_device: bool,
180 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
182 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
185 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
188 pub dsv4: Option<
192 Box<(
193 crate::dsv4::Dsv4Globals,
194 Vec<crate::dsv4::Dsv4Layer>,
195 crate::dsv4::Dsv4Cfg,
196 crate::dsv4::Dsv4State,
197 )>,
198 >,
199 pub dsv41: Option<
203 Box<(
204 crate::dsv41::Dsv41Globals,
205 Vec<crate::dsv41::Dsv41Layer>,
206 crate::dsv41::Dsv41Cfg,
207 crate::dsv41::Dsv41State,
208 )>,
209 >,
210 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
212 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
214 pub qwen4_exp: Option<
217 Box<(
218 crate::qwen4_exp::Globals,
219 Vec<crate::qwen4_exp::Layer>,
220 crate::qwen4_exp::Cfg,
221 crate::qwen4_exp::State,
222 )>,
223 >,
224 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
228 pub dspark: Option<crate::dsv4::DsparkState>,
230 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
233 pub dspark_hist: Vec<usize>,
235 pub dspark_real: Vec<u32>,
239 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
243 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
245 pub dspark_draft_ns: u128,
249 m3_winners: Vec<Vec<u32>>,
251 m3_counts: Vec<u32>,
253 m3_n_exp: usize,
254 m3_tokens: u64,
255 m3_prewarmed: bool,
256 pub short_conv_cfg: Option<ShortConvCfg>,
259 pub mtp: Option<MtpModule>,
261 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
265 verify_exact_moe: bool,
268 pub speculative: bool,
270 pub ignore_eos: bool,
276 pub draft_full_streak: u32,
282 pub spec_k_adapt: Option<usize>,
290 pub spec_acc_ewma: f32,
292 rng: SplitMix64,
293 sampler_scratch: SamplerScratch,
294 spec_forced: Option<u32>,
300 spec_q: Vec<Vec<f32>>,
301 spec_p: Vec<f32>,
302 spec_res: Vec<f32>,
303 spec_qs: Vec<sampler::Sparse>,
305 spec_ps: sampler::Sparse,
306 spec_ress: sampler::Sparse,
307 mtp_graph_mode: Option<bool>,
314 #[cfg(target_os = "macos")]
317 metal_verify: Option<MetalVerifyPending>,
318 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
322 ws: ForwardScratch,
326 pool: Option<std::sync::Arc<Pool>>,
328 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
332 pub(crate) dyn_force_f32: bool,
334 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
339 pub(crate) dyn_active: Option<usize>,
345 pub(crate) dyn_blend_loaded: bool,
349 pub(crate) dyn_phi_layer: Option<usize>,
352 dyn_phi_ema: Vec<f32>,
354 dyn_phi_seen: usize,
355 pub dyn_router: Option<crate::swarm::DynRouter>,
358 o1_cfg: Option<crate::nystrom::O1Cfg>,
361 o1_epoch: u64,
364 o1_flags: Vec<bool>,
366 trace: bool,
369 calib_temp: f32,
372 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
374 graph_kv_id: u64,
375 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
378 graph_want_logits: bool,
379 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
383 graph_head_required: bool,
384 graph_logits: Option<Vec<f32>>,
387 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
390 graph_refused: std::sync::atomic::AtomicBool,
396 pub embed_multiplier: f32,
398 pub attn_scale: f32,
401 pub swa: Option<(usize, usize)>,
404 pub swa_trim: Option<(usize, usize)>,
411 pub sliding_layers: Option<Vec<bool>>,
414 pub no_rope_layers: Option<Vec<bool>>,
419 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
425 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
428 pub kv_prefix: KvPrefix,
432 pub last_prefill_tokens: usize,
435 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
438 pub rotary_dim_local: Option<usize>,
439 pub rope_scale: f32,
440 pub rope_scale_local: f32,
441 pub global_attn: Option<(usize, usize)>,
444 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
447 pub attn_v_norm: bool,
449 pub qk_norm_after_rope: bool,
451 pub proj_gate_sigmoid: bool,
454 pub final_softcap: Option<f32>,
456 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
460 pub attn_softcap: f32,
462 confidence_on: bool,
466 #[cfg(test)]
469 nll_test_fail_at: Option<usize>,
470 #[cfg(test)]
473 nll_test_force_serial: bool,
474}
475
476#[cfg(target_os = "macos")]
477impl Drop for Pipeline {
478 fn drop(&mut self) {
479 let _ = crate::gpu_metal::wait_replay();
481 crate::gpu::kv_mirror_drop(self.graph_kv_id);
482 }
483}
484
485#[cfg(not(target_os = "macos"))]
486impl Drop for Pipeline {
487 fn drop(&mut self) {
488 crate::gpu::graph_kv_reset(self.graph_kv_id);
492 }
493}
494
495pub struct PipelineWeights {
500 pub embed_tokens: QTensor,
502 pub layers: Vec<LayerWeights>,
504 pub lm_head: QTensor,
506 pub final_norm: Vec<f32>,
508}
509
510pub struct LayerWeights {
512 pub input_norm: Vec<f32>,
513 pub post_norm: Vec<f32>,
516 pub attn_out_norm: Option<Vec<f32>>,
519 pub layer_scale: Option<f32>,
521 pub ffn_out_norm: Option<Vec<f32>>,
524 pub ffn: FfnKind,
525 pub attn: AttnKind,
526}
527
528#[derive(Clone, Copy, PartialEq, Debug, Default)]
532pub enum Act {
533 #[default]
534 Silu,
535 GeluTanh,
536 Gelu,
538 Situ {
541 beta: f32,
542 linear_beta: f32,
543 },
544 SwiGluOai {
549 alpha: f32,
550 limit: f32,
551 },
552}
553
554impl Act {
555 pub fn from_arch(name: &str) -> Self {
556 if name == "gelu_tanh" {
557 Self::GeluTanh
558 } else if name == "gelu" {
559 Self::Gelu
560 } else {
561 Self::Silu
562 }
563 }
564
565 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
567 match arch.hidden_act.as_str() {
568 "situ" => Self::Situ {
569 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
570 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
571 },
572 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
577 alpha: 1.702,
578 limit: 7.0,
579 },
580 other => Self::from_arch(other),
581 }
582 }
583
584 #[inline]
589 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
590 Self::SwiGluOai { alpha, limit }
591 }
592
593 #[inline]
594 pub fn apply(self, x: f32) -> f32 {
595 match self {
596 Self::Silu => inference::silu(x),
597 Self::GeluTanh => inference::gelu_tanh(x),
598 Self::Gelu => inference::gelu_erf(x),
599 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
600 Self::SwiGluOai { alpha, limit } => {
601 let gate = x.min(limit);
602 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
603 }
604 }
605 }
606
607 #[inline]
610 pub fn combine(self, g: f32, u: f32) -> f32 {
611 match self {
612 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
613 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
614 }
615 Self::SwiGluOai { alpha, limit } => {
616 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
617 }
618 _ => self.apply(g) * u,
619 }
620 }
621
622 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
626 match self {
627 Self::Silu => Some(crate::gpu::GraphAct::Silu),
628 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
629 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
630 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
631 }
632 }
633
634 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
640 self.moe_job_graph_contract().map(|(act, _)| act)
641 }
642
643 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
649 match self {
650 Self::SwiGluOai { alpha, limit }
651 if alpha.to_bits() == 1.702f32.to_bits()
652 && limit.to_bits() == 7.0f32.to_bits() =>
653 {
654 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
655 }
656 _ => self.graph_act().map(|act| (act, 0.0)),
657 }
658 }
659}
660
661pub struct DenseFfn {
663 pub gate_proj: QTensor,
664 pub up_proj: QTensor,
665 pub down_proj: QTensor,
666 pub act: Act,
668 pub down_t: Option<QTensor>,
674 pub segs: Vec<FfnSeg>,
681}
682
683pub struct FfnSeg {
688 pub gate: QTensor,
689 pub up: QTensor,
690 pub down: QTensor,
691 pub start: usize,
692 pub width: usize,
693}
694
695pub enum FfnKind {
698 Dense(DenseFfn),
699 Moe(MoeFfn),
703 DenseMoe(Box<DenseMoeFfn>),
710}
711
712pub struct DenseMoeFfn {
714 pub dense: DenseFfn,
715 pub moe: MoeFfn,
716 pub post_norm_1: Vec<f32>,
718 pub pre_norm_2: Vec<f32>,
721 pub post_norm_2: Vec<f32>,
723}
724
725pub struct MoeFfn {
726 pub router: QTensor,
728 pub experts: Vec<DenseFfn>,
729 pub top_k: usize,
730 pub norm_topk_prob: bool,
731 pub router_sigmoid: bool,
734 pub sigmoid_denom_eps: f32,
740 pub expert_bias: Option<Vec<f32>>,
744 pub routed_scaling: f32,
747 pub route_tau: Option<f32>,
753 pub shared: Option<(DenseFfn, Option<QTensor>)>,
756 pub stats: std::cell::RefCell<Vec<u64>>,
760 pub act_sq: std::cell::RefCell<Vec<f64>>,
767 pub act_rows: std::cell::RefCell<Vec<f32>>,
773 pub mask: Option<Vec<bool>>,
778 pub per_expert_scale: Option<Vec<f32>>,
781 pub router_input_norm: bool,
785 pub resonance: Option<Resonance>,
789 pub grown: Vec<GrownExpert>,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
799pub struct GrownExpert {
800 pub record: String,
802 pub record_index: usize,
804 pub layer: usize,
805 pub expert: usize,
809}
810
811pub struct Resonance {
813 pub mu: Vec<f32>,
815 pub u: Vec<f32>,
817 pub k: usize,
818 pub bias: Vec<f32>,
820 pub shell: Vec<f32>,
826}
827
828static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
831
832pub fn growth_shell_enabled() -> bool {
837 use std::sync::atomic::Ordering;
838 match GROWTH_SHELL.load(Ordering::Relaxed) {
839 1 => true,
840 2 => false,
841 _ => {
842 let off = std::env::var("CMF_GROWTH_SHELL")
843 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
844 .unwrap_or(false);
845 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
846 !off
847 }
848 }
849}
850
851pub fn set_growth_shell(on: Option<bool>) {
856 GROWTH_SHELL.store(
857 match on {
858 Some(true) => 1,
859 Some(false) => 2,
860 None => 0,
861 },
862 std::sync::atomic::Ordering::Relaxed,
863 );
864}
865
866impl Resonance {
867 pub fn has_shell(&self) -> bool {
869 self.shell.iter().any(|s| s.is_finite())
870 }
871
872 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
875 let mut out = vec![f32::INFINITY; ne];
876 if growth_shell_enabled() {
877 for (o, s) in out.iter_mut().zip(&self.shell) {
878 *o = *s;
879 }
880 }
881 out
882 }
883
884 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
889 let h = x.len();
890 let ne = out.len();
891 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
892 for e in 0..ne {
893 let mu = &self.mu[e * h..(e + 1) * h];
894 let mut d2 = 0.0f32;
895 for j in 0..h {
896 let d = x[j] - mu[j];
897 d2 += d * d;
898 }
899 let mut proj = 0.0f32;
900 for i in 0..self.k {
901 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
902 let mut p = 0.0f32;
903 for j in 0..h {
904 p += (x[j] - mu[j]) * u[j];
905 }
906 proj += p * p;
907 }
908 let err = d2 - proj;
909 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
910 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
911 out[e] = f32::NEG_INFINITY;
912 }
913 }
914 }
915}
916
917pub enum AttnKind {
920 Full {
922 wq: QTensor,
923 wk: QTensor,
924 wv: QTensor,
925 wo: QTensor,
926 q_norm: Option<Vec<f32>>,
927 k_norm: Option<Vec<f32>>,
928 output_gate: bool,
929 softplus_gate: Option<(QTensor, bool)>,
933 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
935 },
936 Linear(VmfPhaseWeights),
938 LinearGdn(GdnWeights),
940 ShortConv(ShortConvWeights),
943 Mla(Box<MlaWeights>),
951 Kda(Box<crate::linear_core::KdaWeights>),
955 Bounded(Box<crate::bounded::BoundedWeights>),
960}
961
962pub struct MlaWeights {
964 pub q_proj: QTensor,
968 pub q_a: Option<QTensor>,
971 pub q_a_norm: Option<Vec<f32>>,
972 pub kv_a: QTensor,
974 pub kv_a_norm: Vec<f32>,
976 pub kv_b: QTensor,
978 pub o_proj: QTensor,
980 pub nh: usize,
981 pub qk_rope: usize,
982 pub qk_nope: usize,
983 pub v_dim: usize,
984 pub lora: usize,
985 pub scale: f32,
987 pub nope: bool,
989}
990
991pub struct MtpModule {
996 pub enorm: Vec<f32>,
997 pub hnorm: Vec<f32>,
998 pub eh_proj: QTensor,
1000 pub layer: LayerWeights,
1001 pub final_norm: Vec<f32>,
1002 pub kv: crate::kv_cache::LayerKvCache,
1003}
1004
1005#[derive(Clone, Debug, PartialEq)]
1017pub struct MtpFfnProbe {
1018 pub mass: Vec<f64>,
1019 pub squared_mass: Vec<f64>,
1020 pub observed_positions: usize,
1021}
1022
1023#[cfg(target_os = "macos")]
1030enum MetalRowsItem<'a> {
1031 Gdn {
1032 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1033 first: usize,
1034 },
1035 Attn {
1036 l: crate::gpu_metal::AttnGpuLayer<'a>,
1037 li: usize,
1038 q_norm: Option<&'a [f32]>,
1039 k_norm: Option<&'a [f32]>,
1040 output_gate: bool,
1041 },
1042}
1043
1044#[cfg(target_os = "macos")]
1045struct MetalVerifyPending {
1046 graph: crate::gpu_metal::VerifyGraph,
1047 gdn_layers: Vec<usize>,
1048 attn_layers: Vec<(usize, usize)>,
1049}
1050
1051#[cfg(target_os = "macos")]
1055struct MetalWarmPending {
1056 graph: crate::gpu_metal::VerifyGraph,
1057 cpu_stored: usize,
1058 b: usize,
1059}
1060
1061#[cfg(target_os = "macos")]
1062enum MetalRowsRun {
1063 Declined,
1065 Failed,
1068 Completed(MetalVerifyPending),
1069}
1070
1071#[cfg(target_os = "macos")]
1072enum MetalPrefillOutcome {
1073 Declined,
1074 Failed,
1075 Completed(Vec<f32>),
1076}
1077
1078#[cfg(target_os = "macos")]
1079enum MetalBatchNllOutcome {
1080 Declined,
1081 Failed(String),
1082 Completed(f64, usize),
1083}
1084
1085#[derive(Clone, Copy)]
1091enum SpecTrial {
1092 Spec {
1093 t0: std::time::Instant,
1094 gen0: usize,
1095 rounds: usize,
1096 },
1097 Plain {
1098 t0: std::time::Instant,
1099 gen0: usize,
1100 },
1101 Decided {
1102 spec: bool,
1103 recheck_at: usize,
1104 },
1105}
1106
1107pub(crate) fn spec_time_level() -> u8 {
1111 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1112 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1113 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1114 Err(_) => 0,
1115 })
1116}
1117
1118fn graph_spec_wanted_by_policy(
1126 spec_env: Option<&str>,
1127 spec_default_ok: bool,
1128 penalized: bool,
1129 metal_wgpu: bool,
1130 native_metal: bool,
1131) -> bool {
1132 match spec_env {
1133 Some("0") => false,
1134 Some("1") => true,
1139 Some(_) => !native_metal,
1140 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1143 }
1144}
1145
1146struct SpecStampLog {
1152 t_last: std::time::Instant,
1153 items: Vec<(&'static str, f32)>,
1154}
1155
1156static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1157
1158pub(crate) fn spec_stamp(name: &'static str) {
1159 if spec_time_level() == 0 {
1160 return;
1161 }
1162 if let Ok(mut g) = SPEC_STAMPS.lock() {
1163 if let Some(log) = g.as_mut() {
1164 let now = std::time::Instant::now();
1165 log.items
1166 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1167 log.t_last = now;
1168 }
1169 }
1170}
1171
1172fn spec_stamps_begin() {
1173 if spec_time_level() == 0 {
1174 return;
1175 }
1176 if let Ok(mut g) = SPEC_STAMPS.lock() {
1177 *g = Some(SpecStampLog {
1178 t_last: std::time::Instant::now(),
1179 items: Vec::with_capacity(64),
1180 });
1181 }
1182}
1183
1184fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1185 SPEC_STAMPS
1186 .lock()
1187 .ok()
1188 .and_then(|mut g| g.take())
1189 .map(|l| l.items)
1190 .unwrap_or_default()
1191}
1192
1193fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1196 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1197 for &(n, ms) in items {
1198 match agg.iter_mut().find(|e| e.0 == n) {
1199 Some(e) => {
1200 e.1 += ms;
1201 e.2 += 1;
1202 }
1203 None => agg.push((n, ms, 1)),
1204 }
1205 }
1206 let mut s = String::with_capacity(agg.len() * 16);
1207 for (n, ms, k) in agg {
1208 if k > 1 {
1209 s.push_str(&format!("{n} {ms:.1}/{k} "));
1210 } else {
1211 s.push_str(&format!("{n} {ms:.1} "));
1212 }
1213 }
1214 s
1215}
1216
1217#[derive(Default, Clone, Copy)]
1239struct SpecMon {
1240 round_ms: f64,
1241 tokens: f64,
1242 plain_ms: f64,
1243 n: u32,
1244 fails: u32,
1245 zero_accept_rounds: u8,
1250 metal: bool,
1251}
1252
1253const SPEC_PROXY_TOKENS: f64 = 3.5;
1256const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1259
1260impl SpecMon {
1261 fn round(&mut self, dt_ms: f64, produced: usize) {
1262 if produced <= 1 {
1263 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1264 } else {
1265 self.zero_accept_rounds = 0;
1266 }
1267 self.n += 1;
1268 if self.n == 1 {
1269 return; }
1271 let a = if self.n == 2 { 1.0 } else { 0.3 };
1272 self.round_ms += a * (dt_ms - self.round_ms);
1273 self.tokens += a * (produced as f64 - self.tokens);
1274 }
1275 fn pays(&self) -> bool {
1276 if self.plain_ms > 0.0 {
1277 self.tokens * self.plain_ms > self.round_ms * 1.03
1278 } else {
1279 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1280 }
1281 }
1282 fn repeated_zero_acceptance(&self) -> bool {
1283 self.zero_accept_rounds >= 2
1284 }
1285 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1287 let n = generated.saturating_sub(gen0);
1288 if n >= 8 {
1289 return true;
1290 }
1291 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1292 }
1293}
1294
1295pub const KV_PREFIX_TAIL: usize = 128;
1298
1299#[derive(Debug, Clone, Default)]
1305pub struct KvPrefix {
1306 len: usize,
1307 hash: u64,
1308 tail: Vec<u32>,
1309 device: bool,
1314}
1315
1316impl KvPrefix {
1317 #[inline]
1318 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1319 for &id in ids {
1320 h ^= id as u64;
1321 h = h.wrapping_mul(0x100000001b3);
1322 h ^= h >> 29;
1323 }
1324 h
1325 }
1326
1327 pub fn clear(&mut self) {
1328 self.len = 0;
1329 self.hash = 0xcbf29ce484222325;
1330 self.tail.clear();
1331 self.device = false;
1332 }
1333
1334 pub fn on_device(&self) -> bool {
1336 self.device
1337 }
1338
1339 pub fn set_on_device(&mut self, device: bool) {
1341 self.device = device;
1342 }
1343
1344 pub fn len(&self) -> usize {
1346 self.len
1347 }
1348
1349 pub fn is_empty(&self) -> bool {
1350 self.len == 0
1351 }
1352
1353 pub fn tail_len(&self) -> usize {
1355 self.tail.len()
1356 }
1357
1358 pub fn set(&mut self, ids: &[u32]) {
1360 self.clear();
1361 self.extend(ids);
1362 }
1363
1364 pub fn extend(&mut self, more: &[u32]) {
1366 if self.len == 0 && self.hash == 0 {
1367 self.hash = 0xcbf29ce484222325;
1368 }
1369 self.hash = Self::fold(self.hash, more);
1370 self.len += more.len();
1371 if more.len() >= KV_PREFIX_TAIL {
1372 self.tail.clear();
1373 self.tail
1374 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1375 } else {
1376 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1377 self.tail.drain(..drop);
1378 self.tail.extend_from_slice(more);
1379 }
1380 }
1381
1382 pub fn extension(&self, ids: &[u32]) -> usize {
1386 if self.len == 0 || ids.len() <= self.len {
1387 return 0;
1388 }
1389 let t = self.tail.len();
1390 if ids[self.len - t..self.len] != self.tail[..] {
1391 return 0;
1392 }
1393 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1394 return 0;
1395 }
1396 self.len
1397 }
1398}
1399
1400pub struct GenerateResult {
1402 pub text: String,
1403 pub token_ids: Vec<u32>,
1404 pub prompt_tokens: usize,
1405 pub tokens_generated: usize,
1406 pub finish_reason: String,
1407 pub mtp_drafted: usize,
1409 pub mtp_accepted: usize,
1410 pub token_confidence: Vec<f32>,
1415 pub traces: Vec<TokenTrace>,
1418}
1419
1420#[derive(Clone, Debug)]
1425pub struct TokenTrace {
1426 pub t: usize,
1428 pub token_id: u32,
1430 pub confidence: f32,
1432 pub active_skill: Option<String>,
1434 pub recon: Option<f32>,
1438 pub switched: bool,
1441}
1442
1443#[cfg_attr(not(test), allow(dead_code))]
1448fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1449 let t = if temp > 1e-3 { temp } else { 1.0 };
1450 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1451 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1452 if sum > 0.0 {
1453 (((logits[id as usize] - max) / t).exp()) / sum
1454 } else {
1455 0.0
1456 }
1457}
1458
1459fn prefill_batched() -> bool {
1462 std::env::var("CMF_PREFILL")
1463 .map(|v| v != "seq")
1464 .unwrap_or(true)
1465}
1466
1467#[inline]
1471fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1472 let graph_quality = unmasked && prefer_graph;
1473 let fused_head_quality = graph_quality && native_metal;
1474 (graph_quality, fused_head_quality)
1475}
1476
1477#[derive(Clone, Copy)]
1481enum PrefillIn<'a> {
1482 Ids(&'a [u32]),
1483 Hidden(&'a [f32]),
1484}
1485
1486impl Pipeline {
1493 pub(crate) fn can_prefill_batched(&self) -> bool {
1494 #[cfg(test)]
1495 let force_serial = self.nll_test_force_serial;
1496 #[cfg(not(test))]
1497 let force_serial = false;
1498 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1499 }
1500
1501 fn automatic_gpu_prefix(&self) -> Option<usize> {
1504 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1505 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1506 }
1507
1508 pub fn prefill_chunk(&self) -> usize {
1512 let env = env_prefill_chunk();
1513 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1514 return prefill_chunk_rule(env, ChunkHost::here(), false);
1515 }
1516 prefill_chunk_rule(
1517 None,
1518 ChunkHost::Other,
1519 self.chunk_stack_facts().dense_on_discrete(),
1520 )
1521 }
1522
1523 fn per_token_stack(&self) -> bool {
1527 self.g3n.is_some() || self.g4e.is_some()
1528 }
1529
1530 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1531 let plain_dense = !self.weights.layers.is_empty()
1532 && !self.per_token_stack()
1533 && self.dsv4.is_none()
1534 && self.dsv41.is_none()
1535 && self.qwen4_exp.is_none()
1536 && self.weights.layers.iter().all(|lw| {
1537 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1538 });
1539 let gpu_on = crate::gpu::enabled();
1540 ChunkStackFacts {
1541 plain_dense,
1542 discrete: gpu_on && crate::gpu::discrete(),
1543 gpu_on,
1544 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1547 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1548 multi_gpu: self.gpu_plan.is_some(),
1549 o1: self.o1_active(),
1550 }
1551 }
1552}
1553
1554pub fn prefill_chunk() -> usize {
1563 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1564}
1565
1566fn env_prefill_chunk() -> Option<usize> {
1567 std::env::var("CMF_PREFILL_CHUNK")
1568 .ok()
1569 .and_then(|v| v.parse::<usize>().ok())
1570}
1571
1572#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1574enum ChunkHost {
1575 Macos,
1576 Aarch64,
1578 Other,
1580}
1581
1582impl ChunkHost {
1583 fn here() -> Self {
1584 if cfg!(target_os = "macos") {
1585 ChunkHost::Macos
1586 } else if cfg!(target_arch = "aarch64") {
1587 ChunkHost::Aarch64
1588 } else {
1589 ChunkHost::Other
1590 }
1591 }
1592}
1593
1594const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1601
1602fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1608 if let Some(n) = env {
1609 return n.max(1);
1610 }
1611 match host {
1612 ChunkHost::Macos => 512,
1613 ChunkHost::Aarch64 => 256,
1616 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1617 ChunkHost::Other => 48,
1618 }
1619}
1620
1621#[derive(Clone, Copy, Debug, Default)]
1623struct ChunkStackFacts {
1624 plain_dense: bool,
1627 discrete: bool,
1629 gpu_on: bool,
1631 capacity_split: bool,
1633 multi_gpu: bool,
1635 o1: bool,
1637}
1638
1639impl ChunkStackFacts {
1640 fn dense_on_discrete(self) -> bool {
1641 self.plain_dense
1642 && self.discrete
1643 && self.gpu_on
1644 && !self.capacity_split
1645 && !self.multi_gpu
1646 && !self.o1
1647 }
1648}
1649
1650#[inline]
1656fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1657 if end <= start || start >= input_len {
1658 return 0;
1659 }
1660 let rows = (end.min(input_len) - start).min(input_len - start);
1661 if end < input_len {
1662 rows
1663 } else {
1664 rows.saturating_sub(1)
1665 }
1666}
1667
1668pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1673pub(crate) struct ReuseLayer {
1674 pub full: bool,
1677 pub host_rows: usize,
1680 pub device_rows: Option<usize>,
1682 pub device_state: bool,
1684}
1685
1686#[derive(Debug, Clone, PartialEq, Eq)]
1688pub(crate) enum ReusePlan {
1689 Ready,
1691 Pull(Vec<(usize, usize, usize)>),
1694 Fresh,
1696}
1697
1698pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1708 let mut pulls = Vec::new();
1709 for (li, l) in layers.iter().enumerate() {
1710 if !l.full {
1711 if l.device_state {
1712 return ReusePlan::Fresh;
1713 }
1714 continue;
1715 }
1716 if l.host_rows == reuse_from {
1717 continue;
1718 }
1719 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1720 pulls.push((li, l.host_rows, reuse_from));
1721 continue;
1722 }
1723 return ReusePlan::Fresh;
1724 }
1725 if pulls.is_empty() {
1726 ReusePlan::Ready
1727 } else {
1728 ReusePlan::Pull(pulls)
1729 }
1730}
1731
1732impl Pipeline {
1733 fn reset_fresh_mtp_draft_state(&mut self) {
1736 self.draft_full_streak = 0;
1737 }
1738
1739 fn clear_sequence_state(&mut self) {
1747 #[cfg(target_os = "macos")]
1750 let _ = crate::gpu_metal::wait_replay();
1751 self.kv_cache.clear();
1752 self.clear_history();
1755 self.graph_logits = None;
1756 if let Some(b) = &mut self.dsv41 {
1757 b.3.clear();
1758 }
1759 crate::gpu::graph_kv_reset(self.graph_kv_id);
1760 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1765 }
1766
1767 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1774 if self.graph_prefill_preferred() {
1775 return true;
1776 }
1777 let kv_id = self.graph_kv_id;
1778 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1779 .map(|li| {
1780 let full = matches!(
1781 self.weights.layers[self.phys_layer(li)].attn,
1782 AttnKind::Full { .. }
1783 );
1784 ReuseLayer {
1785 full,
1786 host_rows: self.kv_cache.layers[li].pos_len(),
1789 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1790 device_state: crate::gpu::graph_state_resident(kv_id, li),
1791 }
1792 })
1793 .collect();
1794 if layers
1798 .iter()
1799 .all(|l| l.device_rows.is_none() && !l.device_state)
1800 {
1801 return true;
1802 }
1803 let plan = kv_reuse_plan(reuse_from, &layers);
1804 let (what, rows, n) = match &plan {
1805 ReusePlan::Ready => ("host ready", 0, 0),
1806 ReusePlan::Fresh => ("fresh", 0, 0),
1807 ReusePlan::Pull(p) => (
1808 "pull",
1809 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1810 p.len(),
1811 ),
1812 };
1813 let t0 = std::time::Instant::now();
1814 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1815 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1816 eprintln!(
1817 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1818 if ok { "" } else { " (failed → fresh)" },
1819 t0.elapsed().as_secs_f64() * 1e3
1820 );
1821 }
1822 ok
1823 }
1824
1825 fn apply_kv_reuse_plan(
1826 &mut self,
1827 reuse_from: usize,
1828 plan: ReusePlan,
1829 layers: &[ReuseLayer],
1830 ) -> bool {
1831 let kv_id = self.graph_kv_id;
1832 match plan {
1833 ReusePlan::Fresh => return false,
1834 ReusePlan::Ready => {}
1835 ReusePlan::Pull(pulls) => {
1836 let (nkv, hd) = {
1841 let c = &self.kv_cache.layers[pulls[0].0];
1842 (c.num_kv_heads, c.head_dim)
1843 };
1844 let uniform = pulls.iter().all(|&(li, _, _)| {
1845 let c = &self.kv_cache.layers[li];
1846 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1847 });
1848 let batched = if uniform {
1849 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1850 } else {
1851 None
1852 };
1853 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1854 Some(rows) => rows,
1855 None => {
1856 let mut rows = Vec::with_capacity(pulls.len());
1857 for &(li, from, to) in &pulls {
1858 let (lnkv, lhd) = {
1859 let c = &self.kv_cache.layers[li];
1860 (c.num_kv_heads, c.head_dim)
1861 };
1862 let Some((k, v, first_valid)) =
1863 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1864 else {
1865 return false;
1866 };
1867 let need_from = match self.layer_window(li) {
1871 Some(w) => from.max((to + 1).saturating_sub(w)),
1872 None => from,
1873 };
1874 if first_valid > need_from {
1875 return false;
1876 }
1877 rows.push((k, v));
1878 }
1879 rows
1880 }
1881 };
1882 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1883 let cache = &mut self.kv_cache.layers[li];
1884 let row = cache.num_kv_heads * cache.head_dim;
1885 for p in 0..to - from {
1886 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1887 }
1888 if cache.pos_len() != to {
1889 return false;
1890 }
1891 }
1892 }
1893 }
1894 for (li, l) in layers.iter().enumerate() {
1898 if l.full
1899 && l.device_rows.is_some_and(|d| d > reuse_from)
1900 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1901 {
1902 return false;
1903 }
1904 }
1905 true
1906 }
1907
1908 fn finish_generation(
1914 &mut self,
1915 mtp: &mut Option<MtpModule>,
1916 router: &mut Option<crate::swarm::DynRouter>,
1917 clear_sequence: bool,
1918 ) {
1919 if router.is_some() {
1923 let _ = self.set_active_skill(None);
1924 }
1925 #[cfg(target_os = "macos")]
1932 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1933 if clear_sequence {
1934 self.clear_sequence_state();
1935 if let Some(m) = mtp.as_mut() {
1936 m.kv.clear();
1942 }
1943 if let Some(m) = self.mtp.as_mut() {
1944 m.kv.clear();
1948 }
1949 }
1950 self.graph_want_logits = false;
1951 self.graph_head_required = false;
1952 self.graph_logits = None;
1953 self.graph_failed
1954 .store(false, std::sync::atomic::Ordering::Relaxed);
1955 self.cancel
1956 .store(false, std::sync::atomic::Ordering::Relaxed);
1957 self.dyn_router = router.take().or(self.dyn_router.take());
1958 self.mtp = mtp.take().or(self.mtp.take());
1959 self.mtp_graph_mode = None;
1960 self.spec_forced = None;
1961 }
1962
1963 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1967 if self
1968 .graph_failed
1969 .swap(false, std::sync::atomic::Ordering::Relaxed)
1970 {
1971 self.cancel
1972 .store(false, std::sync::atomic::Ordering::Relaxed);
1973 self.clear_sequence_state();
1974 self.graph_logits = None;
1975 self.graph_want_logits = false;
1976 self.graph_head_required = false;
1977 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1978 }
1979 Ok(())
1980 }
1981
1982 #[cfg(target_os = "macos")]
1983 fn fail_metal_graph(&mut self, reason: &str) {
1984 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1985 self.clear_sequence_state();
1986 self.graph_logits = None;
1987 self.graph_failed
1988 .store(true, std::sync::atomic::Ordering::Relaxed);
1989 self.cancel
1990 .store(true, std::sync::atomic::Ordering::Relaxed);
1991 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1992 }
1993
1994 fn nll_begin(&mut self) -> Result<(), String> {
1999 if self
2000 .graph_failed
2001 .swap(false, std::sync::atomic::Ordering::Relaxed)
2002 {
2003 self.cancel
2004 .store(false, std::sync::atomic::Ordering::Relaxed);
2005 self.clear_sequence_state();
2006 self.graph_logits = None;
2007 self.graph_want_logits = false;
2008 self.graph_head_required = false;
2009 return Err("GPU graph failed before NLL scoring".to_string());
2010 }
2011 self.clear_sequence_state();
2012 self.graph_logits = None;
2013 self.graph_want_logits = false;
2014 self.graph_head_required = false;
2015 Ok(())
2016 }
2017
2018 fn nll_end(&mut self) {
2022 self.clear_sequence_state();
2023 self.graph_logits = None;
2024 self.graph_want_logits = false;
2025 self.graph_head_required = false;
2026 self.graph_failed
2027 .store(false, std::sync::atomic::Ordering::Relaxed);
2028 }
2029
2030 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2033 #[cfg(test)]
2034 if self.nll_test_fail_at == Some(pos) {
2035 self.nll_test_fail_at = None;
2036 self.graph_failed
2037 .store(true, std::sync::atomic::Ordering::Relaxed);
2038 self.cancel
2039 .store(true, std::sync::atomic::Ordering::Relaxed);
2040 }
2041 if self
2042 .graph_failed
2043 .swap(false, std::sync::atomic::Ordering::Relaxed)
2044 {
2045 self.cancel
2046 .store(false, std::sync::atomic::Ordering::Relaxed);
2047 self.clear_sequence_state();
2048 self.graph_logits = None;
2049 self.graph_want_logits = false;
2050 return Err(format!(
2051 "GPU graph failed during NLL {phase} at position {pos}"
2052 ));
2053 }
2054 Ok(())
2055 }
2056
2057 #[inline]
2061 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2062 virtual_idx % self.physical_layers
2063 }
2064
2065 #[inline]
2068 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2069 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2070 }
2071
2072 #[allow(clippy::too_many_arguments)]
2074
2075 #[cfg(target_os = "macos")]
2094 fn graph_prefill_preferred(&self) -> bool {
2095 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2096 if !crate::gpu::enabled_here()
2097 || !graph_force
2098 || std::env::var("CMF_GPU_BLOCK")
2099 .map(|v| v == "0")
2100 .unwrap_or(false)
2101 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2104 {
2105 return false;
2106 }
2107 self.weights
2108 .layers
2109 .iter()
2110 .any(|lw| {
2111 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2112 })
2113 }
2114
2115 #[cfg(not(target_os = "macos"))]
2122 fn batch_prefix_prefill(&self) -> bool {
2123 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2124 Ok("0") => return false,
2125 Ok("1") => true,
2126 _ => false,
2127 };
2128 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2129 && crate::gpu::enabled_here()
2130 && !self.graph_refused()
2131 && (forced || self.graph_attn_decline_reason().is_some())
2132 && self.wgpu_graph_attn_decline().is_none()
2133 && self.attn_softcap == 0.0
2134 && self
2135 .weights
2136 .layers
2137 .iter()
2138 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2139 && self.automatic_gpu_prefix().is_some()
2140 }
2141
2142 fn g4_batch_prefill(&self) -> bool {
2148 #[cfg(target_os = "macos")]
2149 {
2150 false
2151 }
2152 #[cfg(not(target_os = "macos"))]
2153 {
2154 !self.weights.layers.is_empty()
2158 && !self.per_token_stack()
2159 && (self.global_attn.is_some() || self.attn_v_norm)
2160 && crate::gpu::g4_batch_on()
2161 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2162 && crate::gpu::enabled_here()
2163 && crate::gpu::discrete()
2164 && !self.graph_refused()
2165 && !self.o1_active()
2166 && self.attn_softcap == 0.0
2167 && self.wgpu_graph_attn_decline().is_none()
2168 && self.weights.layers.iter().all(|lw| {
2169 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2170 })
2171 && self.automatic_gpu_prefix().is_none()
2172 }
2173 }
2174
2175 #[cfg(not(target_os = "macos"))]
2182 fn g4_batch_ingest(
2183 &mut self,
2184 ids: &[u32],
2185 pos0: usize,
2186 keep_all: bool,
2187 ) -> Result<(usize, Vec<f32>), String> {
2188 let hs = self.hidden_size;
2189 let chunk = self.generation_batch_k().max(1);
2190 let end_all = pos0 + ids.len();
2191 let mut pos = pos0;
2192 let mut out = Vec::new();
2193 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2194 let end = (pos + chunk).min(end_all);
2195 let bk = end - pos;
2196 let mut hiddens = vec![0f32; bk * hs];
2197 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2198 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2199 }
2200 let positions: Vec<usize> = (pos..end).collect();
2201 let t_chunk = std::time::Instant::now();
2202 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2203 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2204 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2205 eprintln!(
2206 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2207 bk as f64 / (ms / 1000.0)
2208 );
2209 }
2210 match outcome {
2211 crate::gpu::BatchGraphOutcome::Completed => {
2212 if keep_all {
2213 out.extend_from_slice(&hiddens);
2214 } else {
2215 out = hiddens[(bk - 1) * hs..].to_vec();
2216 }
2217 pos = end;
2218 }
2219 crate::gpu::BatchGraphOutcome::Failed => {
2220 return Err("Gemma-4 batched prefill failed after admission".into());
2221 }
2222 crate::gpu::BatchGraphOutcome::Declined => {
2223 #[cfg(feature = "gpu")]
2224 if pos > pos0 {
2225 self.pull_lagging_host_kv(0, self.num_layers, pos);
2226 }
2227 break;
2228 }
2229 }
2230 }
2231 Ok((pos - pos0, out))
2232 }
2233
2234 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2239 #[cfg(not(target_os = "macos"))]
2240 {
2241 if ids.is_empty() || !self.g4_batch_prefill() {
2242 return None;
2243 }
2244 self.clear_sequence_state();
2245 match self.g4_batch_ingest(ids, 0, true) {
2246 Ok((n, rows)) if n == ids.len() => Some(rows),
2247 _ => {
2248 self.clear_sequence_state();
2249 None
2250 }
2251 }
2252 }
2253 #[cfg(target_os = "macos")]
2254 {
2255 let _ = ids;
2256 None
2257 }
2258 }
2259
2260 #[cfg(not(target_os = "macos"))]
2261 fn graph_prefill_preferred(&self) -> bool {
2262 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2270 if !graph_on || !crate::gpu::enabled_here() {
2271 return false;
2272 }
2273 if self.embryo_resident_eligible() {
2277 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2282 }
2283 if self.o1_active() {
2297 return false;
2298 }
2299 if self.wgpu_graph_attn_decline().is_some() {
2303 return false;
2304 }
2305 if self
2306 .weights
2307 .layers
2308 .iter()
2309 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2310 {
2311 return true;
2312 }
2313 self.weights
2324 .layers
2325 .iter()
2326 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2327 && self.automatic_gpu_prefix().is_none()
2328 }
2329
2330 #[cfg(target_os = "macos")]
2331 fn q1_graph_gpu(
2332 &mut self,
2333 start: usize,
2334 upto: Option<usize>,
2335 position: usize,
2336 h: &mut [f32],
2337 ) -> usize {
2338 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2340 const MOE_WORKING_SET_REASON: &str =
2341 "model weights exceed Metal working set for native MoE graph";
2342 if self
2346 .graph_declines
2347 .borrow()
2348 .iter()
2349 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2350 {
2351 return start;
2352 }
2353 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2354 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2356 || !crate::gpu::enabled_here()
2357 || !graph_force
2358 || std::env::var("CMF_GPU_BLOCK")
2359 .map(|v| v == "0")
2360 .unwrap_or(false)
2361 {
2362 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2363 eprintln!(
2364 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2365 self.attn_softcap > 0.0,
2366 crate::gpu::enabled_here(),
2367 graph_force,
2368 );
2369 }
2370 if self.graph_head_required {
2371 self.fail_metal_graph("native graph front gate refused");
2372 }
2373 return start;
2374 }
2375 let gemma4 = self.metal_gemma4();
2385 let swa_graph = self.metal_graph_swa() || gemma4;
2386 let graph_attn_reason = self.graph_attn_decline_reason();
2393 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2394 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2395 if (self.swa.is_some() && !swa_graph)
2396 || (self.global_attn.is_some() && !gemma4)
2397 || self.attention_heads_per_layer.is_some()
2398 || (self.attn_v_norm && !gemma4)
2399 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2402 || self.weights.layers.iter().any(|lw| {
2403 ((lw.attn_out_norm.is_some()
2404 || lw.ffn_out_norm.is_some()
2405 || lw.layer_scale.is_some())
2406 && !gemma4)
2407 || matches!(&lw.ffn, FfnKind::Dense(d)
2408 if !(matches!(d.act, Act::Silu | Act::Gelu)
2409 || (gemma4 && d.act == Act::GeluTanh)))
2410 })
2411 {
2412 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2415 self.note_graph_decline("metal block graph", reason);
2416 }
2417 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2418 eprintln!(
2419 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2420 self.swa.is_some(),
2421 self.global_attn.is_some(),
2422 self.attention_heads_per_layer.is_some(),
2423 self.attn_v_norm,
2424 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2425 );
2426 }
2427 if self.graph_head_required {
2428 self.fail_metal_graph("native graph architecture gate refused");
2429 }
2430 return start;
2431 }
2432 let limit = upto
2435 .map(|u| u + 1)
2436 .unwrap_or(self.num_layers)
2437 .min(self.num_layers);
2438
2439 enum Item<'a> {
2440 Gdn {
2441 run: Vec<GdnGpuLayer<'a>>,
2442 first: usize,
2443 },
2444 Attn {
2445 l: AttnGpuLayer<'a>,
2446 li: usize,
2447 q_norm: Option<&'a [f32]>,
2448 k_norm: Option<&'a [f32]>,
2449 output_gate: bool,
2450 proj_gate: Option<(&'a QTensor, bool)>,
2454 head_gate_w: Option<&'a [f32]>,
2457 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2458 full_gpu: bool,
2461 },
2462 }
2463
2464 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2471 let attend_contract = attend_mode != "0"
2472 && attend_mode != "off"
2473 && self.head_dim % 4 == 0
2474 && self.head_dim <= 256
2475 && (self.rotary_dim == 0
2479 || (self.rotary_dim >= 2
2480 && self.rotary_dim <= self.head_dim
2481 && (self.rotary_dim / 2) % 32 == 0))
2482 && self.num_kv_heads > 0
2483 && self.num_heads % self.num_kv_heads == 0;
2484
2485 let mut plan: Vec<Item> = Vec::new();
2486 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2487 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2489 let mut scan = start;
2490 while scan < limit {
2491 let lw = &self.weights.layers[self.phys_layer(scan)];
2492 let ffn = match &lw.ffn {
2493 FfnKind::Dense(d) if d.segs.is_empty() => {
2494 let (Some(g), Some(u), Some(dn)) = (
2495 d.gate_proj.metal_graph_parts(),
2496 d.up_proj.metal_graph_parts(),
2497 d.down_proj.metal_graph_parts(),
2498 ) else {
2499 if block_diag {
2500 eprintln!(
2501 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2502 );
2503 }
2504 break;
2505 };
2506 MetalFfn::Dense {
2507 gate: g,
2508 up: u,
2509 down: dn,
2510 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2513 }
2514 }
2515 FfnKind::Moe(m) => {
2516 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2522 {
2523 MetalFfn::FlintMoe(flint)
2524 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2525 MetalFfn::Moe(moe)
2526 } else {
2527 if block_diag {
2528 eprintln!(
2529 "block-graph: L{scan} MoE outside the graph contract — run ends"
2530 );
2531 }
2532 break;
2533 };
2534 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2535 model_ref.get_or_insert_with(|| model.clone());
2536 }
2537 ffn
2538 }
2539 _ => {
2540 if block_diag {
2541 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2542 }
2543 break;
2544 }
2545 };
2546 match &lw.attn {
2547 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2548 let parts = (
2549 w.in_proj_qkv.metal_graph_parts(),
2550 w.in_proj_z.metal_graph_parts(),
2551 w.in_proj_a.f32_parts(),
2552 w.in_proj_b.f32_parts(),
2553 w.out_proj.metal_graph_parts(),
2554 );
2555 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2556 if block_diag {
2557 eprintln!(
2558 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2559 w.in_proj_qkv.metal_graph_parts().is_some(),
2560 w.in_proj_z.metal_graph_parts().is_some(),
2561 w.in_proj_a.f32_parts().is_some(),
2562 w.in_proj_b.f32_parts().is_some(),
2563 w.out_proj.metal_graph_parts().is_some(),
2564 );
2565 }
2566 break;
2567 };
2568 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2569 model_ref.get_or_insert_with(|| model.clone());
2570 }
2571 let gl = GdnGpuLayer {
2572 attn_norm: &lw.input_norm,
2573 post_norm: &lw.post_norm,
2574 qkv,
2575 z,
2576 a,
2577 b,
2578 out,
2579 ffn,
2580 conv1d: &w.conv1d,
2581 a_log: &w.a_log,
2582 dt_bias: &w.dt_bias,
2583 gnorm: &w.norm,
2584 };
2585 match plan.last_mut() {
2586 Some(Item::Gdn { run, .. }) => run.push(gl),
2587 _ => plan.push(Item::Gdn {
2588 run: vec![gl],
2589 first: scan,
2590 }),
2591 }
2592 }
2593 AttnKind::Full {
2594 wq,
2595 wk,
2596 wv,
2597 wo,
2598 q_norm,
2599 k_norm,
2600 output_gate,
2601 softplus_gate,
2602 bias,
2603 } if (!self.kv_cache.layers[scan].o1_sealed()
2604 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2609 && softplus_gate
2613 .as_ref()
2614 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2615 {
2616 let parts = (
2617 wq.metal_graph_parts(),
2618 wk.metal_graph_parts(),
2619 wv.metal_graph_parts(),
2620 wo.metal_graph_parts(),
2621 );
2622 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2623 break;
2624 };
2625 if let QTensor::Mapped { model, .. } = wq {
2626 model_ref.get_or_insert_with(|| model.clone());
2627 }
2628 let cache = &self.kv_cache.layers[scan];
2629 let o1_metal = cache.o1.is_some()
2633 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2634 && cache.o1_views().is_some();
2635 let head_gate_w = softplus_gate
2639 .as_ref()
2640 .and_then(|(g, _)| g.f32_parts())
2641 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2642 .map(|(d, _, _)| d);
2643 let (nkv_g, hd_g) = if gemma4 {
2646 let (a, b, _) = self.layer_geom(scan);
2647 (a, b)
2648 } else {
2649 (self.num_kv_heads, self.head_dim)
2650 };
2651 let full_gpu = attend_contract
2652 && softplus_gate.is_none() == head_gate_w.is_none()
2653 && cache.mode == crate::kv_cache::KvMode::F32
2654 && (cache.o1.is_none() || o1_metal)
2655 && bias.is_none()
2656 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2657 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2658 && pk.1 == nkv_g * hd_g
2659 && pv.1 == nkv_g * hd_g
2660 && po.2 == self.num_heads * hd_g;
2661 plan.push(Item::Attn {
2662 l: AttnGpuLayer {
2663 attn_norm: &lw.input_norm,
2664 post_norm: &lw.post_norm,
2665 wq: pq,
2666 wk: pk,
2667 wv: pv,
2668 wo: po,
2669 ffn,
2670 attn_out_norm: lw.attn_out_norm.as_deref(),
2671 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2672 layer_scale: lw.layer_scale,
2673 },
2674 li: scan,
2675 q_norm: q_norm.as_deref(),
2676 k_norm: k_norm.as_deref(),
2677 output_gate: *output_gate,
2678 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2679 head_gate_w,
2680 bias: bias
2681 .as_ref()
2682 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2683 full_gpu,
2684 });
2685 }
2686 _ => break,
2687 }
2688 scan += 1;
2689 }
2690 let Some(model) = model_ref else {
2691 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2692 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2693 }
2694 if self.graph_head_required {
2695 self.fail_metal_graph("native graph has no mapped model reference");
2696 }
2697 return start;
2698 };
2699 if plan.is_empty() {
2700 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2701 eprintln!("q1-graph: empty plan at layer {start}");
2702 }
2703 if self.graph_head_required {
2704 self.fail_metal_graph("native graph plan is empty");
2705 }
2706 return start;
2707 }
2708 let has_moe = plan.iter().any(|it| match it {
2709 Item::Gdn { run, .. } => run
2710 .iter()
2711 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2712 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2713 });
2714 let has_flint_moe = plan.iter().any(|it| match it {
2715 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2716 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2717 });
2718 let metal_working_set = crate::gpu_metal::working_set_bytes();
2719 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2720 let model_bytes = model.primary_bytes().len() as u64;
2721 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2727 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2728 let pager_cap = if !has_moe {
2737 None
2738 } else if has_flint_moe {
2739 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2743 } else {
2744 crate::gpu_metal::moe_pager_cap_bytes()
2745 };
2746 let pager_eligible =
2747 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2748 let mut pager_span: Option<(usize, usize)> = None;
2749 if pager_eligible {
2750 let cap = pager_cap.expect("checked above");
2751 let tail_span = (self.graph_want_logits
2755 && upto.is_none()
2756 && scan == self.num_layers
2757 && std::env::var("CMF_GPU_LMHEAD")
2758 .map(|v| v != "0")
2759 .unwrap_or(true))
2760 .then(|| self.weights.lm_head.metal_graph_parts())
2761 .flatten()
2762 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2763 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2764 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2765 Some(match span {
2766 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2767 None => (lo, hi),
2768 })
2769 };
2770 let page_span = |span: (usize, usize), include_tail: bool| {
2771 let (mut lo, mut hi) = span;
2772 if include_tail {
2773 if let Some((tail_lo, tail_hi)) = tail_span {
2774 lo = lo.min(tail_lo);
2775 hi = hi.max(tail_hi);
2776 }
2777 }
2778 crate::gpu_metal::page_round_span(lo, hi)
2779 };
2780 let mut selected = 0usize;
2781 let mut selected_span: Option<(usize, usize)> = None;
2782 let mut gdn_prefix: Option<(usize, usize)> = None;
2790 'items: for (i, item) in plan.iter().enumerate() {
2791 match item {
2792 Item::Attn { li, .. } => {
2793 let Some(span) = add_layer_span(selected_span, *li) else {
2794 break;
2795 };
2796 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2797 break;
2798 };
2799 if page_hi - page_lo > cap {
2800 break;
2801 }
2802 selected = i + 1;
2803 selected_span = Some((page_lo, page_hi));
2804 }
2805 Item::Gdn { first, run } => {
2806 let mut whole = None;
2813 for take in 1..=run.len() {
2814 whole = add_layer_span(whole, *first + take - 1);
2815 if whole.is_none() {
2816 break;
2817 }
2818 }
2819 let whole_fits_empty_page = whole
2820 .and_then(|span| page_span(span, false))
2821 .is_some_and(|(lo, hi)| hi - lo <= cap);
2822 if whole_fits_empty_page {
2823 let mut span = selected_span;
2824 for take in 1..=run.len() {
2825 let Some(next) = add_layer_span(span, *first + take - 1) else {
2826 break 'items;
2827 };
2828 span = Some(next);
2829 }
2830 let Some(span) = span else {
2831 break;
2832 };
2833 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2834 else {
2835 break;
2836 };
2837 if page_hi - page_lo > cap {
2838 break;
2839 }
2840 selected = i + 1;
2841 selected_span = Some((page_lo, page_hi));
2842 continue;
2843 }
2844 if selected != 0 {
2845 break;
2846 }
2847 let mut span = None;
2848 let mut best: Option<(usize, (usize, usize))> = None;
2849 for take in 1..=run.len() {
2850 let Some(next) = add_layer_span(span, *first + take - 1) else {
2851 break;
2852 };
2853 let Some((page_lo, page_hi)) = page_span(next, false) else {
2856 break;
2857 };
2858 if page_hi - page_lo > cap {
2859 break;
2860 }
2861 span = Some((page_lo, page_hi));
2862 best = Some((take, (page_lo, page_hi)));
2863 }
2864 let Some((take, page)) = best else {
2865 break;
2866 };
2867 selected = i + 1;
2868 selected_span = Some(page);
2869 if take != run.len() {
2870 gdn_prefix = Some((i, take));
2871 break 'items;
2872 }
2873 }
2874 }
2875 }
2876 if selected != 0 {
2877 if let Some((item, take)) = gdn_prefix {
2878 match &mut plan[item] {
2879 Item::Gdn { run, .. } => run.truncate(take),
2880 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2881 }
2882 }
2883 let (page_lo, page_hi) = selected_span.expect("selected page span");
2884 tracing::info!(
2885 page_start = page_lo,
2886 page_end = page_hi,
2887 page_mb = (page_hi - page_lo) / (1024 * 1024),
2888 cap_mb = cap / (1024 * 1024),
2889 graph_items = selected,
2890 "native Metal MoE graph uses transient bounded weight page"
2891 );
2892 plan.truncate(selected);
2893 pager_span = Some((page_lo, page_hi));
2894 } else {
2895 tracing::warn!(
2896 cap_mb = cap / (1024 * 1024),
2897 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2898 );
2899 }
2900 }
2901 if has_moe && needs_bounded_pager && pager_span.is_none() {
2910 tracing::info!(
2911 model_bytes,
2912 metal_working_set,
2913 "native Metal MoE graph declined before allocation"
2914 );
2915 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2916 if self.graph_head_required {
2917 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2918 }
2919 return start;
2920 }
2921 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2922 let dev_attend = attend_contract
2923 && (self.head_dim <= 128
2924 || has_moe
2925 || (self.head_dim <= 256 && has_gdn)
2931 || (self.head_dim <= 256 && swa_graph)
2935 || attend_mode == "force"
2936 || attend_mode == "256");
2937 if !dev_attend {
2938 for it in &mut plan {
2939 if let Item::Attn { li, full_gpu, .. } = it {
2940 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2943 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2944 if !keep_o1 {
2945 *full_gpu = false;
2946 }
2947 }
2948 }
2949 }
2950 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2951 use std::sync::atomic::{AtomicBool, Ordering};
2952 static SAID: AtomicBool = AtomicBool::new(false);
2953 if !SAID.swap(true, Ordering::Relaxed) {
2954 let fg = plan
2955 .iter()
2956 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2957 .count();
2958 let att = plan
2959 .iter()
2960 .filter(|it| matches!(it, Item::Attn { .. }))
2961 .count();
2962 eprintln!(
2963 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2964 plan.len(),
2965 self.head_dim,
2966 self.rotary_dim,
2967 self.num_kv_heads,
2968 self.num_heads,
2969 );
2970 }
2971 }
2972 let dims = GraphDims {
2973 hidden: self.hidden_size,
2974 eps: self.rms_eps as f32,
2975 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2976 };
2977 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2981 crate::gpu_metal::MoePagerPageScope::new(
2982 _mt0,
2983 position,
2984 start,
2985 page_lo,
2986 page_hi,
2987 pager_cap.unwrap_or(0),
2991 )
2992 });
2993 let graph_new = match pager_span {
2994 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2995 None => TokenGraph::new(&model, dims, h),
2996 };
2997 let Some(mut graph) = graph_new else {
2998 if self.graph_head_required {
2999 self.fail_metal_graph("native TokenGraph allocation refused");
3000 }
3001 return start;
3002 };
3003 if swa_graph && self.head_dim > 128 {
3004 graph.set_attend_blk_from(64);
3010 }
3011 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3012 nv: cfg.num_v_heads,
3013 nk: cfg.num_k_heads,
3014 dk: cfg.key_head_dim,
3015 dv: cfg.value_head_dim,
3016 kk: cfg.conv_kernel,
3017 hidden: self.hidden_size,
3018 inter: self.intermediate_size,
3019 c_dim: cfg.conv_dim(),
3020 eps: cfg.rms_eps as f32,
3021 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3022 });
3023 let mut valid = 0usize;
3027 let mut end = start;
3028 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3030 static ONCE: std::sync::Once = std::sync::Once::new();
3031 ONCE.call_once(|| {
3032 for it in &plan {
3033 match it {
3034 Item::Gdn { first, run } => {
3035 eprintln!("plan: Gdn first={first} len={}", run.len())
3036 }
3037 Item::Attn { li, full_gpu, .. } => {
3038 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3039 }
3040 }
3041 }
3042 });
3043 }
3044 for item in &plan {
3045 let ok = match item {
3046 Item::Gdn { run, .. } => gcfg
3047 .as_ref()
3048 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3049 .unwrap_or(false),
3050 Item::Attn { l, .. } => graph.attn_ok(l),
3051 };
3052 if !ok {
3053 if block_diag {
3054 eprintln!(
3055 "block-graph: plan item {} ({}) failed graph preflight",
3056 valid,
3057 match item {
3058 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3059 Item::Attn { li, .. } => format!("Attn L{li}"),
3060 }
3061 );
3062 }
3063 break;
3064 }
3065 valid += 1;
3066 end += match item {
3067 Item::Gdn { run, .. } => run.len(),
3068 Item::Attn { .. } => 1,
3069 };
3070 }
3071 plan.truncate(valid);
3072 if plan.is_empty() {
3073 if self.graph_head_required {
3074 self.fail_metal_graph("native graph preflight produced no valid items");
3075 }
3076 return start;
3077 }
3078
3079 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3080 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3081 return start;
3082 }
3083
3084 let one_pass = |t: (usize, usize, usize)| {
3093 use cortiq_core::TensorDtype as D;
3094 matches!(
3095 model.tensors[t.0].dtype,
3096 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3097 )
3098 };
3099 let dense_fast = plan.iter().all(|it| match it {
3100 Item::Attn {
3101 l, li, full_gpu, ..
3102 } => {
3103 *full_gpu
3104 && self.kv_cache.layers[*li].o1.is_none()
3105 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3106 && match l.ffn {
3107 MetalFfn::Dense { gate, up, down, .. } => {
3108 one_pass(gate) && one_pass(up) && one_pass(down)
3109 }
3110 _ => false,
3111 }
3112 }
3113 Item::Gdn { .. } => false,
3114 });
3115 let moe_plan = !dense_fast
3119 && plan.iter().all(|it| match it {
3120 Item::Attn {
3121 l, li, full_gpu, ..
3122 } => {
3123 *full_gpu
3124 && self.kv_cache.layers[*li].o1.is_none()
3125 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3126 }
3127 Item::Gdn { .. } => false,
3128 });
3129 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3130 if block_diag {
3131 use std::sync::atomic::{AtomicBool, Ordering};
3132 static SAID_AB: AtomicBool = AtomicBool::new(false);
3133 if !SAID_AB.swap(true, Ordering::Relaxed) {
3134 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3135 }
3136 }
3137 let _mv_fast = match ab {
3138 Some((bits, _)) => {
3139 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3140 crate::gpu_metal::MvFastGuard::set_raw(bits)
3141 }
3142 None if moe_plan => {
3143 graph.set_dense_concurrent(
3144 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3145 );
3146 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3147 }
3148 None => {
3149 graph.set_dense_concurrent(dense_fast);
3150 let q8r4 = if swa_graph {
3153 crate::gpu_metal::DENSE_Q8R4
3154 } else {
3155 0
3156 };
3157 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3158 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3159 } else {
3160 0
3161 })
3162 }
3163 };
3164
3165 let pool = self.pool.clone();
3168 let (nh, nkv, hd, hs, eps) = (
3169 self.num_heads,
3170 self.num_kv_heads,
3171 self.head_dim,
3172 self.hidden_size,
3173 self.rms_eps,
3174 );
3175 let norm_style = self.norm_style;
3176 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3177 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3178 let kv_id = self.graph_kv_id;
3179 let mut pending: Vec<(usize, usize)> = Vec::new();
3182 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3185 for item in &plan {
3186 let _xt0 = std::time::Instant::now();
3187 let _xkind: u32 = match item {
3188 Item::Gdn { .. } => 2,
3189 Item::Attn { .. } => 3,
3190 };
3191 if self.loop_final_norm {
3193 let item_start = match item {
3194 Item::Gdn { first, .. } => *first,
3195 Item::Attn { li, .. } => *li,
3196 };
3197 if item_start > start && self.is_loop_end(item_start - 1) {
3198 graph.encode_loop_norm(&self.weights.final_norm);
3199 }
3200 }
3201 match item {
3202 Item::Gdn { run, first } => {
3203 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3204 if l.linear_state.len() != want {
3205 l.linear_state = vec![0f32; want];
3206 }
3207 }
3208 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3209 .iter()
3210 .map(|l| l.linear_state.as_slice())
3211 .collect();
3212 let _ig = std::time::Instant::now();
3213 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3214 tracing::error!("q1 graph: GDN run refused after validation");
3216 return start;
3217 }
3218 graph.commit_kind = 2;
3221 graph.commit();
3222 crate::gpu::stageprof(0, _ig.elapsed());
3223 pending.push((*first, run.len()));
3224 }
3225 Item::Attn {
3226 l,
3227 li,
3228 q_norm,
3229 k_norm,
3230 output_gate,
3231 proj_gate,
3232 head_gate_w,
3233 bias,
3234 full_gpu,
3235 } => {
3236 let _ia = std::time::Instant::now();
3237 let inv_freq_l = self.layer_inv_freq(*li);
3243 let rd_l = self.layer_geom(*li).2;
3244 let window_l = self.layer_window(*li);
3245 let head_gate_w = *head_gate_w;
3246 let (nkv, hd) = if gemma4 {
3249 let (a, b, _) = self.layer_geom(*li);
3250 (a, b)
3251 } else {
3252 (nkv, hd)
3253 };
3254 if *full_gpu {
3256 let cache = &self.kv_cache.layers[*li];
3257 let o1p = if cache.o1.is_some() {
3258 match cache.o1_views() {
3259 Some(views) => Some(crate::gpu::O1AttnParams {
3260 views,
3261 epoch: self.o1_epoch,
3262 }),
3263 None => None,
3265 }
3266 } else {
3267 None
3268 };
3269 let o1_layer = cache.o1.is_some();
3270 if o1_layer && o1p.is_none() {
3271 }
3273 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3274 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3275 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3276 debug_assert!(
3279 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3280 );
3281 let p = crate::gpu::AttnDeviceParams {
3282 kv_id,
3283 layer: *li,
3284 nh,
3285 nkv,
3286 hd,
3287 rd: rd_l,
3288 position,
3289 scale: self.attn_scale,
3290 eps: eps as f32,
3291 gemma,
3292 late_qk_norm: self.qk_norm_after_rope,
3293 output_gate: *output_gate,
3294 q_norm: *q_norm,
3295 k_norm: *k_norm,
3296 inv_freq: &inv_freq_l,
3297 rope_scale: self.layer_rope_scale(*li),
3298 cpu_k,
3299 cpu_v,
3300 cpu_stored,
3301 cpu_gen: cache.generation(),
3302 o1: o1p,
3303 window: window_l,
3304 softmax_sink: cache.sinks.as_deref(),
3305 head_gate: head_gate_w,
3306 v_norm: self.attn_v_norm,
3307 };
3308 let o1_bad = o1_layer && p.o1.is_none();
3309 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3310 {
3311 if p.o1.is_none() {
3313 dev_attn.push((*li, nkv, hd));
3314 }
3315 graph.commit_kind = 3;
3316 graph.commit();
3317 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3321 continue;
3322 }
3323 }
3325 graph.encode_attn_prefix(l);
3326 if let Err(err) = graph.sync_checked() {
3327 self.fail_metal_graph(&err);
3328 return start;
3329 }
3330 if !pending.is_empty() {
3331 let idxs: Vec<usize> =
3332 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3333 let mut outs: Vec<&mut [f32]> = self
3334 .kv_cache
3335 .layers
3336 .iter_mut()
3337 .enumerate()
3338 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3339 .map(|(_, s)| s.linear_state.as_mut_slice())
3340 .collect();
3341 graph.read_states(&mut outs);
3342 }
3343 let mut q_raw = attention::take_buf(l.wq.1);
3344 let mut k = attention::take_buf(l.wk.1);
3345 let mut v = attention::take_buf(l.wv.1);
3346 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3347 let mut gate_raw = proj_gate.map(|(gp, _)| {
3350 let mut normed = attention::take_buf(hs);
3351 graph.read_normed(&mut normed);
3352 let mut raw = attention::take_buf(gp.rows());
3353 gp.matvec(&normed, &mut raw, pool.as_deref());
3354 attention::recycle_buf(&mut normed);
3355 raw
3356 });
3357 let cfg = QwenAttnCfg {
3358 num_heads: nh,
3359 num_kv_heads: nkv,
3360 head_dim: hd,
3361 hidden_size: hs,
3362 position,
3363 inv_freq: &inv_freq_l,
3364 rotary_dim: rd_l,
3365 scale: self.attn_scale,
3366 softcap: self.attn_softcap,
3367 window: window_l,
3368 v_norm: self.attn_v_norm,
3371 qk_norm_after_rope: self.qk_norm_after_rope,
3372 gate_sigmoid: self.proj_gate_sigmoid,
3373 q_norm: *q_norm,
3374 k_norm: *k_norm,
3375 output_gate: *output_gate,
3376 softplus_gate: None,
3377 rope_scale: self.layer_rope_scale(*li),
3378 bias: *bias,
3379 rms_eps: eps,
3380 norm_style,
3381 pool: pool.as_deref(),
3382 v_head_dim: hd,
3383 };
3384 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3387 || std::env::var("CMF_ATTN_DUMP").is_ok();
3388 let _ = full_gpu;
3389 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3390 let mut ao = attention::qwen_attention_core(
3391 q_raw,
3392 k,
3393 v,
3394 &mut self.kv_cache.layers[*li],
3395 &cfg,
3396 );
3397 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3401 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3402 let (cq, _cg, _ck, _cv) =
3403 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3404 let cache = &self.kv_cache.layers[*li];
3405 let n = cache.head_keys(0).len() / hd;
3406 let mut bytes: Vec<u8> = Vec::new();
3407 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3408 bytes.extend_from_slice(&v.to_le_bytes());
3409 }
3410 for v in &cq {
3411 bytes.extend_from_slice(&v.to_le_bytes());
3412 }
3413 for g in 0..nkv {
3414 for v in cache.head_keys(g) {
3415 bytes.extend_from_slice(&v.to_le_bytes());
3416 }
3417 }
3418 for g in 0..nkv {
3419 for v in cache.head_values(g) {
3420 bytes.extend_from_slice(&v.to_le_bytes());
3421 }
3422 }
3423 let _ =
3424 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3425 }
3426 }
3427 if let Some((qr0, k0, v0)) =
3428 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3429 {
3430 let (cq, _cg, ck, cv) =
3431 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3432 let mut h_now = vec![0f32; hs];
3433 graph.read_h(&mut h_now);
3434 let cache = &self.kv_cache.layers[*li];
3435 let n_after = cache.head_keys(0).len() / hd;
3436 let stored = n_after.saturating_sub(1);
3440 let cpu_k: Vec<&[f32]> = (0..nkv)
3441 .map(|g| &cache.head_keys(g)[..stored * hd])
3442 .collect();
3443 let cpu_v: Vec<&[f32]> = (0..nkv)
3444 .map(|g| &cache.head_values(g)[..stored * hd])
3445 .collect();
3446 let p = crate::gpu::AttnDeviceParams {
3447 kv_id,
3448 layer: *li,
3449 nh,
3450 nkv,
3451 hd,
3452 rd: rd_l,
3456 position,
3457 scale: self.attn_scale,
3458 eps: eps as f32,
3459 gemma,
3460 late_qk_norm: self.qk_norm_after_rope,
3461 output_gate: *output_gate,
3462 q_norm: *q_norm,
3463 k_norm: *k_norm,
3464 inv_freq: &inv_freq_l,
3465 rope_scale: self.layer_rope_scale(*li),
3466 cpu_k,
3467 cpu_v,
3468 cpu_stored: stored,
3469 cpu_gen: cache.generation(),
3470 o1: None,
3471 window: window_l,
3472 softmax_sink: cache.sinks.as_deref(),
3473 head_gate: None,
3474 v_norm: self.attn_v_norm,
3475 };
3476 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3477 let md = |a: &[f32], b: &[f32]| {
3478 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3479 };
3480 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3481 eprintln!(
3482 "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}",
3483 nn(&cq),
3484 md(&cq, &dq),
3485 nn(&ck),
3486 md(&ck, &dk),
3487 nn(&cv),
3488 md(&cv, &dv),
3489 nn(&ao),
3490 md(&ao, &dao)
3491 );
3492 } else {
3493 eprintln!("attn-oracle L{li}: device probe declined");
3494 }
3495 }
3496 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3497 attention::apply_projected_gate(
3500 &mut ao,
3501 raw,
3502 per_head,
3503 hd,
3504 self.proj_gate_sigmoid,
3505 );
3506 }
3507 if let Some(mut raw) = gate_raw.take() {
3508 attention::recycle_buf(&mut raw);
3509 }
3510 graph.encode_attn_suffix(l, &ao);
3511 graph.commit();
3514 attention::recycle_buf(&mut ao);
3515 }
3516 }
3517
3518 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3519 }
3520 let mut lm_rows = None;
3525 if self.graph_want_logits
3526 && upto.is_none()
3527 && end == self.num_layers
3528 && std::env::var("CMF_GPU_LMHEAD")
3529 .map(|v| v != "0")
3530 .unwrap_or(true)
3531 {
3532 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3533 if graph.lm_head_ok(lm) {
3534 graph.encode_lm_head(&self.weights.final_norm, lm);
3535 lm_rows = Some(lm.1);
3536 }
3537 }
3538 }
3539 if self.graph_head_required && lm_rows.is_none() {
3540 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3541 self.fail_metal_graph("fused graph head was requested but not encodable");
3542 return start;
3543 }
3544 let _sy0 = std::time::Instant::now();
3545 if let Err(err) = graph.sync_checked() {
3546 if let Some(profile) = pager_profile_page.as_mut() {
3547 profile.finish(start, graph.pager_page_stats(), "sync-error");
3548 }
3549 self.fail_metal_graph(&err);
3550 return start;
3551 }
3552 let _rs0 = std::time::Instant::now();
3553 if !pending.is_empty() {
3554 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3555 let mut outs: Vec<&mut [f32]> = self
3556 .kv_cache
3557 .layers
3558 .iter_mut()
3559 .enumerate()
3560 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3561 .map(|(_, s)| s.linear_state.as_mut_slice())
3562 .collect();
3563 graph.read_states(&mut outs);
3564 }
3565 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3566 use std::sync::atomic::{AtomicU64, Ordering};
3567 static SY: AtomicU64 = AtomicU64::new(0);
3568 static RS: AtomicU64 = AtomicU64::new(0);
3569 static N: AtomicU64 = AtomicU64::new(0);
3570 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3571 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3572 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3573 if n % 100 == 0 {
3574 eprintln!(
3575 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3576 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3577 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3578 );
3579 }
3580 }
3581 if let Some(rows) = lm_rows {
3582 crate::gpu::hostprof_encode_done(_mt0);
3583 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3584 graph.read_logits(&mut lg);
3585 crate::gpu::hostprof_total(_mt0);
3586 lg.resize(self.vocab_size, 0.0);
3587 if let Some(c) = self.final_softcap {
3588 softcap_in_place(&mut lg, c, self.pool.as_deref());
3589 }
3590 self.graph_logits = Some(lg);
3591 }
3592 graph.read_h(h);
3593 if self.graph_head_required && self.graph_logits.is_none() {
3594 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3595 self.fail_metal_graph("fused graph head completed without logits readback");
3596 return start;
3597 }
3598 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3599 METAL_GRAPH_LAYERS.fetch_add(
3600 end.saturating_sub(start) as u64,
3601 std::sync::atomic::Ordering::Relaxed,
3602 );
3603 if self.graph_head_required {
3604 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3605 }
3606 for (li, nkv, hd) in dev_attn {
3610 let mut krow = attention::take_buf(nkv * hd);
3611 let mut vrow = attention::take_buf(nkv * hd);
3612 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3613 let cache = &mut self.kv_cache.layers[li];
3614 cache.append(&krow, &vrow, &[]);
3615 let n = cache.seq_len;
3616 let mut imp = attention::take_buf(n);
3617 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3618 cache.accumulate_imp(&imp);
3619 attention::recycle_buf(&mut imp);
3620 }
3621 attention::recycle_buf(&mut krow);
3622 attention::recycle_buf(&mut vrow);
3623 }
3624 if let Some(profile) = pager_profile_page.as_mut() {
3625 profile.finish(end, graph.pager_page_stats(), "page-complete");
3626 }
3627 if let Some((_, arm)) = ab {
3628 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3629 }
3630 end
3631 }
3632
3633 pub fn new(
3634 tokenizer: Tokenizer,
3635 weights: PipelineWeights,
3636 hidden_size: usize,
3637 intermediate_size: usize,
3638 num_heads: usize,
3639 num_kv_heads: usize,
3640 head_dim: usize,
3641 num_layers: usize,
3642 physical_layers: usize,
3643 loop_final_norm: bool,
3644 vocab_size: usize,
3645 rms_eps: f64,
3646 rope_base: f32,
3647 norm_style: NormStyle,
3648 max_seq_len: usize,
3649 sampler_config: SamplerConfig,
3650 ) -> Self {
3651 let rng = match sampler_config.seed {
3652 Some(s) => SplitMix64::new(s),
3653 None => SplitMix64::from_entropy(),
3654 };
3655 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3656 let pool = Pool::from_env();
3657 if let Some(p) = &pool {
3658 tracing::info!("worker pool: {} threads", p.n_workers());
3659 if let Some(model) = weights
3661 .lm_head
3662 .model_arc()
3663 .or_else(|| weights.embed_tokens.model_arc())
3664 {
3665 let regions: Vec<&[u8]> =
3666 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3667 p.bind_numa(®ions);
3668 }
3669 }
3670 Self {
3671 gpu_plan: None,
3672 tokenizer: std::sync::Arc::new(tokenizer),
3673 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3674 sampler_config,
3675 weights,
3676 hidden_size,
3677 intermediate_size,
3678 num_heads,
3679 num_kv_heads,
3680 head_dim,
3681 num_layers,
3682 physical_layers,
3683 loop_final_norm,
3684 vocab_size,
3685 rms_eps,
3686 rope_base,
3687 norm_style,
3688 rotary_dim: head_dim,
3689 attention_heads_per_layer: None,
3690 kv_heads_per_layer: None,
3691 v_head_dim: None,
3692 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3693 .filter(|v| !v.is_empty())
3694 .map(std::path::PathBuf::from),
3695 graph_declines: std::cell::RefCell::new(Vec::new()),
3696 mimo_moe: Default::default(),
3697 minicpm_tail: Default::default(),
3698 vmf_cfg: None,
3699 gdn_cfg: None,
3700 kda_cfg: None,
3701 g3n: None,
3702 g4e: None,
3703 dsv4: None,
3704 dsv41: None,
3705 dsv41_vision: None,
3706 dsv41_prefill: None,
3707 qwen4_exp: None,
3708 dsv4_mtp: Vec::new(),
3709 dspark: None,
3710 dspark_pending: Vec::new(),
3711 dspark_hist: Vec::new(),
3712 dspark_real: Vec::new(),
3713 dspark_trunk_picks: Vec::new(),
3714 dspark_exp: Vec::new(),
3715 dspark_draft_ns: 0,
3716 m3_winners: Vec::new(),
3717 m3_counts: Vec::new(),
3718 m3_n_exp: 0,
3719 m3_tokens: 0,
3720 m3_prewarmed: false,
3721 logit_multiplier: None,
3722 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3723 graph_failed: std::sync::atomic::AtomicBool::new(false),
3724 kv_history: Vec::new(),
3725 kv_history_device: false,
3726 short_conv_cfg: None,
3727 mtp: None,
3728 mimo_mtp: None,
3729 verify_exact_moe: false,
3730 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3731 ignore_eos: false,
3732 draft_full_streak: 0,
3733 spec_k_adapt: None,
3734 spec_acc_ewma: 0.7,
3735 rng,
3736 sampler_scratch: SamplerScratch::default(),
3737 spec_forced: None,
3738 spec_q: Vec::new(),
3739 spec_p: Vec::new(),
3740 spec_res: Vec::new(),
3741 spec_qs: Vec::new(),
3742 spec_ps: Vec::new(),
3743 spec_ress: Vec::new(),
3744 mtp_graph_mode: None,
3745 #[cfg(target_os = "macos")]
3746 metal_verify: None,
3747 inv_freq,
3748 ws: ForwardScratch::new(hidden_size),
3749 pool,
3750 model: None,
3751 dyn_force_f32: false,
3752 dyn_skill_layers: Vec::new(),
3753 dyn_active: None,
3754 dyn_blend_loaded: false,
3755 dyn_phi_layer: None,
3756 dyn_phi_ema: Vec::new(),
3757 dyn_phi_seen: 0,
3758 dyn_router: None,
3759 o1_cfg: None,
3760 o1_epoch: 0,
3761 o1_flags: Vec::new(),
3762 trace: false,
3763 calib_temp: 1.0,
3764 confidence_on: true,
3765 embed_multiplier: 1.0,
3766 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3767 swa: None,
3768 swa_trim: None,
3769 sliding_layers: None,
3770 no_rope_layers: None,
3771 anchor_core: None,
3772 bounded_rope: None,
3773 kv_prefix: KvPrefix::default(),
3774 last_prefill_tokens: 0,
3775 inv_freq_local: None,
3776 rotary_dim_local: None,
3777 rope_scale: 1.0,
3778 rope_scale_local: 1.0,
3779 global_attn: None,
3780 inv_freq_global: None,
3781 attn_v_norm: false,
3782 qk_norm_after_rope: false,
3783 proj_gate_sigmoid: false,
3784 final_softcap: None,
3785 head_clusters: None,
3786 attn_softcap: 0.0,
3787 graph_want_logits: false,
3788 graph_head_required: false,
3789 graph_logits: None,
3790 embryo_graph: None,
3791 graph_refused: std::sync::atomic::AtomicBool::new(false),
3792 graph_kv_id: {
3793 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3794 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3795 },
3796 #[cfg(test)]
3797 nll_test_fail_at: None,
3798 #[cfg(test)]
3799 nll_test_force_serial: false,
3800 }
3801 }
3802
3803 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3811 if let Err(e) = self.try_set_o1(cfg) {
3812 tracing::error!("{e}");
3813 }
3814 }
3815
3816 pub fn bounded_native(&self) -> bool {
3820 self.anchor_core.is_some()
3821 }
3822
3823 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3826 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3827 }
3828
3829 pub fn o1_refusal(&self) -> Option<String> {
3831 self.anchor_core.as_ref().map(|ac| {
3832 format!(
3833 "--o1 / CMF_O1 refused: the anchor is native bounded \
3834 (anchor_core kind={} window={} sink={}); the file's operator \
3835 is executed as-is and no post-hoc Nyström overlay applies",
3836 ac.kind, ac.window, ac.sink
3837 )
3838 })
3839 }
3840
3841 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3843 if let Some(c) = &cfg {
3844 if let Some(why) = self.o1_refusal() {
3845 self.o1_flags = Vec::new();
3846 self.o1_cfg = None;
3847 return Err(why);
3848 }
3849 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3850 self.o1_flags.clear();
3851 self.o1_cfg = None;
3852 return Err(format!(
3853 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3854 c.w, c.sink
3855 ));
3856 }
3857 }
3858 self.o1_flags = match &cfg {
3859 Some(c) => {
3860 let mut flags = c.layer_flags(self.num_layers);
3861 for (li, f) in flags.iter_mut().enumerate() {
3862 if *f
3868 && (!matches!(
3869 self.weights.layers[self.phys_layer(li)].attn,
3870 AttnKind::Full { .. }
3871 ) || self.layer_window(li).is_some()
3872 || self.kv_cache.layers[li].sinks.is_some()
3873 || self.layer_v_dim(li) != self.layer_geom(li).1)
3874 {
3875 *f = false;
3876 }
3877 }
3878 flags
3879 }
3880 None => Vec::new(),
3881 };
3882 if let Some(c) = &cfg {
3883 let n = self.o1_flags.iter().filter(|&&f| f).count();
3884 tracing::info!(
3885 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3886 self.num_layers,
3887 c.m,
3888 c.w,
3889 c.sink,
3890 c.rect
3891 );
3892 }
3893 self.o1_cfg = cfg;
3894 Ok(())
3895 }
3896
3897 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3903 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3904 return Err(format!(
3905 "anchor_core kind '{}' is not executable by this runtime",
3906 cfg.kind
3907 ));
3908 }
3909 if cfg.window == 0 {
3910 return Err("anchor_core.window must be >= 1".into());
3911 }
3912 let mut n = 0usize;
3913 for li in 0..self.num_layers {
3914 let pl = self.phys_layer(li);
3915 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3916 if w.window != cfg.window || w.sink != cfg.sink {
3917 return Err(format!(
3918 "layer {li}: bounded weights (window {} sink {}) disagree with \
3919 anchor_core (window {} sink {})",
3920 w.window, w.sink, cfg.window, cfg.sink
3921 ));
3922 }
3923 self.kv_cache.layers[li].install_bounded(cfg.window);
3924 n += 1;
3925 }
3926 }
3927 if n == 0 {
3928 return Err("anchor_core is present but no layer executes it".into());
3929 }
3930 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3931 self.bounded_rope = Some(std::sync::Arc::new(rope));
3932 self.anchor_core = Some(cfg.clone());
3933 self.embryo_graph = None;
3934 tracing::info!(
3935 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3936 cfg.kind,
3937 cfg.window,
3938 cfg.sink,
3939 self.kv_cache
3940 .layers
3941 .iter()
3942 .map(|l| l.bounded_state_bytes())
3943 .max()
3944 .unwrap_or(0)
3945 );
3946 Ok(())
3947 }
3948
3949 pub fn install_wire_identity(&mut self, identity: u64) {
3953 for li in 0..self.kv_cache.layers.len() {
3954 let pl = self.phys_layer(li);
3955 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3956 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3957 Some(AttnKind::Linear(_))
3958 | Some(AttnKind::LinearGdn(_))
3959 | Some(AttnKind::ShortConv(_))
3960 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3961 _ => crate::kv_cache::WireKind::Full,
3962 };
3963 let l = &mut self.kv_cache.layers[li];
3964 l.wire_kind = kind;
3965 l.wire_identity = identity;
3966 l.wire_layer = li as u32;
3969 }
3970 }
3971
3972 pub fn clear_history(&mut self) {
3974 self.kv_history.clear();
3975 self.kv_history_device = false;
3976 self.kv_prefix.clear();
3977 }
3978
3979 pub fn graph_refused(&self) -> bool {
3983 self.graph_refused
3984 .load(std::sync::atomic::Ordering::Relaxed)
3985 }
3986
3987 pub fn mark_graph_refused(&self) {
3989 if !self
3990 .graph_refused
3991 .swap(true, std::sync::atomic::Ordering::Relaxed)
3992 {
3993 tracing::info!(
3994 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3995 self.graph_kv_id
3996 );
3997 }
3998 }
3999
4000 pub fn device_sequence_position(&self) -> Option<usize> {
4004 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4005 }
4006
4007 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4012 let dev = self.device_sequence_position();
4013 if recorded_on_device {
4014 dev == Some(n) && self.embryo_resident_wanted()
4015 } else {
4016 dev.is_none()
4017 }
4018 }
4019
4020 pub(crate) fn invalidate_for_weight_change(&mut self) {
4028 self.clear_sequence_state();
4029 self.embryo_graph = None;
4030 }
4031
4032 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4038 let (n, on_device) = if self.bounded_native() {
4039 (
4040 self.kv_prefix.extension(input_ids),
4041 self.kv_prefix.on_device(),
4042 )
4043 } else {
4044 let h = &self.kv_history;
4045 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4046 (h.len(), self.kv_history_device)
4047 } else {
4048 (0, false)
4049 }
4050 };
4051 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4054 tracing::warn!(
4055 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4056 the device now holds {:?} — re-prefilling from zero",
4057 if on_device { "resident device" } else { "host" },
4058 self.device_sequence_position()
4059 );
4060 return 0;
4061 }
4062 n
4063 }
4064
4065 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4068 self.cached_prefix_len(input_ids)
4069 }
4070
4071 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4077 let on_device = self.device_sequence_position().is_some();
4078 if self.bounded_native() {
4079 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4080 let prev_device = self.kv_prefix.on_device();
4081 self.kv_history.clear();
4082 self.kv_history_device = false;
4083 if keep && prev_device == on_device {
4084 self.kv_prefix.extend(&consumed[reused..]);
4085 } else {
4086 self.kv_prefix.set(consumed);
4087 }
4088 self.kv_prefix.set_on_device(on_device);
4089 } else {
4090 self.kv_history = consumed.to_vec();
4091 self.kv_history_device = on_device;
4092 }
4093 }
4094
4095 pub fn o1_active(&self) -> bool {
4097 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4098 }
4099
4100 pub fn generation_batch_k(&self) -> usize {
4118 if let Some(k) = std::env::var("CMF_BATCH_K")
4119 .ok()
4120 .and_then(|v| v.parse::<usize>().ok())
4121 {
4122 return k;
4123 }
4124 if self.g4_batch_prefill() {
4127 return 128;
4128 }
4129 #[cfg(not(target_os = "macos"))]
4130 if self.graph_prefill_preferred() && !self.o1_active() {
4131 let moe = self
4134 .weights
4135 .layers
4136 .iter()
4137 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4138 let q82 = crate::gpu::moe_q82_graph_on()
4144 && self.weights.layers.iter().any(|lw| {
4145 matches!(&lw.ffn, FfnKind::Moe(m)
4146 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4147 });
4148 return if q82 {
4149 128
4150 } else if moe {
4151 32
4152 } else {
4153 128
4154 };
4155 }
4156 0
4157 }
4158
4159 pub fn generation_graph_prefill(&self) -> bool {
4160 let graph = self.graph_prefill_preferred();
4161 #[cfg(not(target_os = "macos"))]
4172 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4173 return false;
4174 }
4175 graph
4176 }
4177
4178 pub fn o1_device_stats(&self) -> (usize, u64) {
4183 crate::gpu::o1_device_stats(self.graph_kv_id)
4184 }
4185
4186 pub fn o1_begin(&mut self) {
4191 self.o1_begin_with_prefix(None);
4192 }
4193
4194 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4198 if let Some(c) = &self.o1_cfg {
4199 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4200 let boundary = requested_prefix.map(|p| {
4201 p.max(
4202 crate::nystrom::o1_deferred_boundary(w, sink)
4203 .expect("o1 config boundary validated in set_o1"),
4204 )
4205 });
4206 for (li, &f) in self.o1_flags.iter().enumerate() {
4207 if f {
4208 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4209 }
4210 }
4211 }
4212 }
4213
4214 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4216 self.o1_cfg.as_ref().and_then(|c| {
4217 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4218 .map(|floor| requested_prefix.max(floor))
4219 })
4220 }
4221
4222 fn o1_note_transition(&mut self) {
4223 let mut transitioned = false;
4227 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4228 if flagged {
4229 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4230 }
4231 }
4232 if transitioned {
4233 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4234 }
4235 }
4236
4237 fn o1_pending(&self) -> bool {
4238 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4239 f && self.kv_cache.layers[li].seq_len > 0
4240 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4241 })
4242 }
4243
4244 fn o1_fail(&mut self, err: String) {
4245 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4246 self.clear_sequence_state();
4247 self.graph_failed
4248 .store(true, std::sync::atomic::Ordering::Relaxed);
4249 self.cancel
4250 .store(true, std::sync::atomic::Ordering::Relaxed);
4251 }
4252
4253 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4258 if self.o1_cfg.is_none() {
4259 return Ok(false);
4260 }
4261 let mut participating = false;
4262 for li in 0..self.num_layers {
4263 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4264 continue;
4265 }
4266 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4267 return Err(err);
4268 }
4269 if self.kv_cache.layers[li].seq_len == 0 {
4270 continue;
4271 }
4272 participating = true;
4273 let num_heads = self.layer_num_heads(li);
4274 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4275 }
4276 self.o1_note_transition();
4277 for li in 0..self.num_layers {
4278 if self.o1_flags.get(li).copied().unwrap_or(false) {
4279 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4280 return Err(err);
4281 }
4282 }
4283 }
4284 Ok(participating
4285 && (0..self.num_layers).all(|li| {
4286 !self.o1_flags.get(li).copied().unwrap_or(false)
4287 || self.kv_cache.layers[li].seq_len == 0
4288 || self.kv_cache.layers[li].o1_sealed()
4289 }))
4290 }
4291
4292 fn o1_progress(&mut self) {
4295 if !self.o1_active() {
4296 return;
4297 }
4298 for li in 0..self.num_layers {
4299 if self.o1_flags.get(li).copied().unwrap_or(false) {
4300 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4301 self.o1_fail(err);
4302 return;
4303 }
4304 }
4305 }
4306 self.o1_note_transition();
4310 if !self.o1_pending() {
4311 return;
4312 }
4313 if let Err(err) = self.o1_seal_checked() {
4314 self.o1_fail(err);
4315 }
4316 }
4317
4318 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4323 if self
4324 .graph_failed
4325 .swap(false, std::sync::atomic::Ordering::Relaxed)
4326 {
4327 self.cancel
4328 .store(false, std::sync::atomic::Ordering::Relaxed);
4329 self.clear_sequence_state();
4330 return Err(format!("{phase}: deferred O(1) transition failed"));
4331 }
4332 Ok(())
4333 }
4334
4335 pub fn o1_seal(&mut self) {
4339 if let Err(err) = self.o1_seal_checked() {
4340 self.o1_fail(err);
4341 }
4342 }
4343
4344 pub fn set_trace(&mut self, on: bool) {
4346 self.trace = on;
4347 }
4348
4349 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4352 self.rng = match config.seed {
4353 Some(seed) => SplitMix64::new(seed),
4354 None => SplitMix64::from_entropy(),
4355 };
4356 self.sampler_config = config;
4357 }
4358
4359 pub fn set_confidence(&mut self, on: bool) {
4364 self.confidence_on = on;
4365 }
4366
4367 pub fn set_calib_temp(&mut self, t: f32) {
4370 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4371 }
4372
4373 pub fn calib_temp(&self) -> f32 {
4375 self.calib_temp
4376 }
4377
4378 pub fn rope_inv_freq(&self) -> &[f32] {
4382 self.inv_freq.as_slice()
4383 }
4384
4385 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4388 self.rotary_dim = rotary_dim.min(self.head_dim);
4389 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4390 self.embryo_graph = None;
4394 }
4395
4396 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4397 QwenAttnCfg {
4398 num_heads: self.num_heads,
4399 num_kv_heads: self.num_kv_heads,
4400 head_dim: self.head_dim,
4401 hidden_size: self.hidden_size,
4402 position,
4403 inv_freq: &self.inv_freq,
4404 rotary_dim: self.rotary_dim,
4405 scale: self.attn_scale,
4406 softcap: self.attn_softcap,
4407 window: None,
4408 v_norm: false,
4409 qk_norm_after_rope: self.qk_norm_after_rope,
4410 gate_sigmoid: self.proj_gate_sigmoid,
4411 q_norm: None,
4412 k_norm: None,
4413 output_gate: false,
4414 softplus_gate: None,
4415 rope_scale: self.rope_scale,
4416 bias: None,
4417 rms_eps: self.rms_eps,
4418 norm_style: self.norm_style,
4419 pool: self.pool.as_deref(),
4420 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4421 }
4422 }
4423
4424 pub fn generate(
4426 &mut self,
4427 prompt: &str,
4428 max_tokens: usize,
4429 task_mask: Option<&TaskMask>,
4430 on_token: Option<TokenCallback>,
4431 ) -> Result<GenerateResult, String> {
4432 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4433 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4434 }
4435
4436 pub fn generate_from_vl(
4439 &mut self,
4440 input: &crate::dsv41_vision::PreparedVlInputs,
4441 max_tokens: usize,
4442 task_mask: Option<&TaskMask>,
4443 on_token: Option<TokenCallback>,
4444 ) -> Result<GenerateResult, String> {
4445 let Some(dsv41) = &self.dsv41 else {
4446 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4447 };
4448 if input.token_ids.is_empty() {
4449 return Err("empty V4.1 multimodal prompt".into());
4450 }
4451 if input.token_types.len() != input.token_ids.len() {
4452 return Err(format!(
4453 "V4.1 token type count {} != token count {}",
4454 input.token_types.len(),
4455 input.token_ids.len()
4456 ));
4457 }
4458 let dim = dsv41.2.dim;
4459 let mut embeddings = vec![None; input.token_ids.len()];
4460 let mut participates = vec![true; input.token_ids.len()];
4461 if !input.images.is_empty() {
4462 let vision = self
4463 .dsv41_vision
4464 .as_ref()
4465 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4466 for image in &input.images {
4467 let end = image.start.saturating_add(image.types.len());
4468 if end > input.token_ids.len() {
4469 return Err(format!(
4470 "V4.1 image span {}..{} exceeds prompt length {}",
4471 image.start,
4472 end,
4473 input.token_ids.len()
4474 ));
4475 }
4476 let mut span = vec![0.0f32; image.types.len() * dim];
4477 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4478 for (offset, &kind) in image.types.iter().enumerate() {
4479 let pos = image.start + offset;
4480 if input.token_types[pos] != kind {
4481 return Err(format!(
4482 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4483 input.token_types[pos]
4484 ));
4485 }
4486 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4487 participates[pos] = false;
4488 }
4489 }
4490 }
4491 for (pos, &kind) in input.token_types.iter().enumerate() {
4492 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4493 return Err(format!("V4.1 text position {pos} has an image embedding"));
4494 }
4495 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4496 return Err(format!("V4.1 image position {pos} has no image embedding"));
4497 }
4498 }
4499 self.dsv41_prefill = Some((embeddings, participates));
4500 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4501 self.dsv41_prefill = None;
4502 result
4503 }
4504
4505 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4507 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4508 }
4509
4510 pub fn generate_from_ids(
4518 &mut self,
4519 input_ids: &[u32],
4520 max_tokens: usize,
4521 task_mask: Option<&TaskMask>,
4522 on_token: Option<TokenCallback>,
4523 ) -> Result<GenerateResult, String> {
4524 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4525 }
4526
4527 pub fn generate_from_embeds(
4533 &mut self,
4534 input_ids: &[u32],
4535 prompt_rows: &[f32],
4536 max_tokens: usize,
4537 task_mask: Option<&TaskMask>,
4538 on_token: Option<TokenCallback>,
4539 ) -> Result<GenerateResult, String> {
4540 if input_ids.is_empty()
4541 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4542 {
4543 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4544 }
4545 if prompt_rows.iter().any(|x| !x.is_finite()) {
4546 return Err("embedded prompt contains non-finite values".into());
4547 }
4548 if !self.can_prefill_batched()
4549 || self.dyn_router.is_some()
4550 || self.o1_active()
4551 || self.mtp.is_some()
4552 || self.gpu_plan.is_some()
4553 {
4554 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4555 }
4556 self.generate_with_prompt_rows(
4557 input_ids,
4558 Some(prompt_rows),
4559 max_tokens,
4560 task_mask,
4561 on_token,
4562 )
4563 }
4564
4565 fn generate_with_prompt_rows(
4566 &mut self,
4567 input_ids: &[u32],
4568 prompt_rows: Option<&[f32]>,
4569 max_tokens: usize,
4570 task_mask: Option<&TaskMask>,
4571 mut on_token: Option<TokenCallback>,
4572 ) -> Result<GenerateResult, String> {
4573 #[cfg(target_os = "macos")]
4574 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4575 if std::env::var("CMF_TRACE_H").is_ok() {
4576 eprintln!("input_ids: {input_ids:?}");
4577 }
4578 if input_ids.is_empty() {
4579 return Err("empty prompt: nothing to generate from".to_string());
4580 }
4581 self.graph_failed
4585 .store(false, std::sync::atomic::Ordering::Relaxed);
4586 let task_mask = self.drop_open_mask(task_mask);
4591
4592 let mut reuse_from = {
4600 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4601 if on
4602 && prompt_rows.is_none()
4603 && task_mask.is_none()
4604 && self.mtp.is_none()
4605 && !(self.mimo_mtp.is_some() && self.speculative)
4606 && self.o1_cfg.is_none()
4607 && self.dsv41.is_none()
4608 {
4609 self.cached_prefix_len(input_ids)
4610 } else {
4611 0
4612 }
4613 };
4614 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4617 reuse_from = 0;
4618 }
4619 self.last_prefill_tokens = input_ids.len() - reuse_from;
4620 let bounded_native = self.bounded_native();
4621 if reuse_from == 0 {
4622 self.reset_fresh_mtp_draft_state();
4627 self.clear_sequence_state();
4628 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4629 eprintln!(
4630 "kv-reuse: {} of {} prompt positions already cached",
4631 reuse_from,
4632 input_ids.len()
4633 );
4634 }
4635 crate::gpu::graph_race_begin_generation();
4636 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4640 std::env::var("CMF_O1_PREFILL")
4641 .ok()
4642 .and_then(|v| v.parse::<usize>().ok())
4643 .filter(|&p| p > 0)
4644 } else {
4645 None
4646 };
4647 if task_mask.is_none() {
4648 self.o1_begin_with_prefix(o1_prefill);
4649 }
4650
4651 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4657 #[cfg(target_os = "macos")]
4695 let metal_graph = crate::gpu::q1_force()
4696 && crate::gpu::enabled_here()
4697 && std::env::var("CMF_GPU_BLOCK")
4698 .map(|v| v != "0")
4699 .unwrap_or(true);
4700 #[cfg(target_os = "macos")]
4706 let native_metal_backend = crate::gpu::q1_force();
4707 #[cfg(not(target_os = "macos"))]
4708 let metal_graph = false;
4709 #[cfg(not(target_os = "macos"))]
4710 let native_metal_backend = false;
4711 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4712 let spec_cheap_round =
4716 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4717 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4718 || match spec_sample_env.as_deref() {
4719 Some("1") => true,
4720 Some(_) => false,
4721 None => metal_graph && spec_cheap_round,
4722 };
4723 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4739 for lw in &self.weights.layers {
4740 if let FfnKind::Dense(d) = &lw.ffn {
4741 dense_n += 1;
4742 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4743 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4744 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4745 {
4746 dense_q4tp += 1;
4747 }
4748 }
4749 }
4750 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4751 let penalized = !metal_graph
4763 && (self.sampler_config.repetition_penalty != 1.0
4764 || self.sampler_config.presence_penalty != 0.0
4765 || !self.sampler_config.suppress_tokens.is_empty());
4766 #[cfg(feature = "gpu")]
4771 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4772 #[cfg(not(feature = "gpu"))]
4773 let metal_wgpu = false;
4774 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4775 let spec_wanted = graph_spec_wanted_by_policy(
4776 spec_env.as_deref(),
4777 spec_default_ok,
4778 penalized,
4779 metal_wgpu,
4780 native_metal_backend,
4781 );
4782 if spec_wanted && metal_wgpu {
4783 tracing::warn!(
4784 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4785 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4786 );
4787 }
4788 let graph_spec = self.speculative
4792 && (graph_on || metal_graph)
4793 && self.mtp.is_some()
4794 && task_mask.is_none()
4795 && !self.o1_active()
4796 && spec_sampling_ok
4797 && spec_wanted;
4798 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4811 let pair_pays = host_pair_opt_in
4812 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4813 #[cfg(target_os = "macos")]
4817 if metal_graph {
4818 static SAID: std::sync::Once = std::sync::Once::new();
4819 SAID.call_once(|| {
4820 let spec = if graph_spec {
4821 let k = std::env::var("CMF_GRAPH_SPEC_K")
4822 .ok()
4823 .and_then(|v| v.parse::<usize>().ok())
4824 .filter(|&v| (1..=8).contains(&v))
4825 .unwrap_or(7);
4826 let arm = if self.sampler_config.temperature < 1e-6 {
4827 "greedy"
4828 } else {
4829 "sampling"
4830 };
4831 format!(
4832 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4833 Self::draft_vocab_rows(usize::MAX)
4834 )
4835 } else if !self.speculative {
4836 "spec off (CMF_MTP=0)".to_string()
4837 } else if self.mtp.is_none() {
4838 "spec off (no MTP head)".to_string()
4839 } else if !spec_sampling_ok {
4840 if spec_cheap_round {
4841 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4842 } else {
4843 "spec off (sampling without a top-k: the dense chain \
4844 costs more than it saves)"
4845 .to_string()
4846 }
4847 } else if !spec_wanted {
4848 if spec_env.is_none() {
4849 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
4850 } else {
4851 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
4852 }
4853 } else if task_mask.is_some() {
4854 "spec off (task mask)".to_string()
4855 } else if !graph_spec && !pair_pays {
4856 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
4857 .to_string()
4858 } else {
4859 "spec off (O(1) attention)".to_string()
4860 };
4861 let on = |var: &str| {
4862 if std::env::var(var).as_deref() == Ok("0") {
4863 "off"
4864 } else {
4865 "on"
4866 }
4867 };
4868 tracing::info!(
4869 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4870 MTP graph {}, attend {}, probe {}",
4871 if crate::gpu_metal::state4_on() {
4872 "on"
4873 } else {
4874 "off"
4875 },
4876 if crate::gpu_metal::async_replay_on() {
4877 "on"
4878 } else {
4879 "off"
4880 },
4881 on("CMF_METAL_PREFILL"),
4882 on("CMF_MTP_GRAPH"),
4883 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4884 if crate::gpu::probe_enabled() {
4885 "bypassed (q1 force)"
4886 } else {
4887 "off"
4888 },
4889 );
4890 });
4891 }
4892 let spec_active = self.speculative
4893 && self.mtp.is_some()
4894 && task_mask.is_none()
4895 && !self.o1_active()
4896 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4897 let mut mtp = if spec_active { self.mtp.take() } else { None };
4900 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4901 eprintln!(
4902 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4903 mtp.is_some(),
4904 self.speculative,
4905 self.sampler_config.temperature < 1e-6,
4906 );
4907 }
4908 if let Some(m) = &mut mtp {
4909 m.kv.clear();
4910 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4912 self.mtp_graph_mode = None;
4913 }
4914 let mimo_spec = self.speculative
4918 && self.mimo_mtp.is_some()
4919 && task_mask.is_none()
4920 && !self.o1_active()
4921 && self.dyn_router.is_none()
4922 && self.sampler_config.temperature < 1e-6
4923 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4924 if let Some(st) = self.mimo_mtp.as_mut() {
4925 st.reset();
4926 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4927 Self::mimo_mtp_hist_cap(st, input_ids.len());
4928 }
4929 }
4930 let mut router = if mtp.is_none() {
4934 self.dyn_router.take()
4935 } else {
4936 None
4937 };
4938 let mut reuse_from = reuse_from;
4939 if let Some(r) = &mut router {
4940 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4943 let _ = self.set_active_skill(None);
4946 reuse_from = 0;
4947 self.last_prefill_tokens = input_ids.len();
4948 }
4949 }
4950
4951 let mut all_ids = input_ids.to_vec();
4952 let mut generated = 0usize;
4953 let mut finish_reason = "max_tokens".to_string();
4954 let mut drafted = 0usize;
4955 let mut accepted = 0usize;
4956 let mut dsv4_spec_bad = 0usize;
4963 let mut dsv4_spec_retry_at = 0usize;
4964 let mut confidence: Vec<f32> = Vec::new();
4965 let trace_on = self.trace;
4966 let calib_temp = self.calib_temp;
4967 let mut traces: Vec<TokenTrace> = Vec::new();
4968
4969 let mut hidden = vec![0.0f32; self.hidden_size];
4975 let mut pos = reuse_from;
4976 let fuse_lm = mtp.is_none()
4985 && router.is_none()
4986 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4987 self.graph_logits = None;
4988 self.graph_want_logits = false;
4989 let _tpf = std::time::Instant::now();
4990 let batch_k = self.generation_batch_k();
4991 if let Some(rows) = prompt_rows {
4992 let hs = self.hidden_size;
4993 let chunk = self.prefill_chunk().max(1);
4994 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4995 let end = (pos + chunk).min(input_ids.len());
4996 let hb = match self.prefill_input_rows(
4997 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4998 pos,
4999 task_mask,
5000 ) {
5001 Ok(hb) => hb,
5002 Err(err) => {
5003 self.finish_generation(&mut mtp, &mut router, true);
5004 return Err(err);
5005 }
5006 };
5007 if mimo_spec {
5008 self.mimo_note_rows(&hb, pos);
5009 }
5010 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5011 pos = end;
5012 }
5013 }
5014 while self.qwen4_exp.is_some()
5025 && mtp.is_none()
5026 && pos < input_ids.len()
5027 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5028 {
5029 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5032 let want_logits = end == input_ids.len();
5033 let mut lg = Vec::new();
5034 if let Some(b) = &mut self.qwen4_exp {
5035 crate::qwen4_exp::forward_tokens(
5036 &b.0,
5037 &b.1,
5038 &b.2,
5039 &mut b.3,
5040 &input_ids[pos..end],
5041 pos,
5042 &self.inv_freq,
5043 self.pool.as_deref(),
5044 &mut lg,
5045 want_logits,
5046 );
5047 }
5048 if want_logits {
5049 self.graph_logits = Some(lg);
5050 }
5051 pos = end;
5052 hidden.fill(0.0);
5053 }
5054 while self.dsv4.is_some()
5055 && mtp.is_none()
5056 && pos < input_ids.len()
5057 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5058 {
5059 let end = (pos + prefill_chunk()).min(input_ids.len());
5060 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5061 let mut lg = Vec::new();
5062 if let Some(b) = &mut self.dsv4 {
5063 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5064 crate::dsv4::forward_chunk(
5065 g,
5066 layers,
5067 &cfg,
5068 st,
5069 &ids,
5070 pos,
5071 &self.inv_freq,
5072 self.pool.as_deref(),
5073 &mut lg,
5074 end == input_ids.len(),
5075 );
5076 }
5077 if end == input_ids.len() {
5078 self.graph_logits = Some(lg);
5079 }
5080 pos = end;
5081 hidden = vec![0.0; self.hidden_size];
5082 }
5083 let dsv41_prefill = self.dsv41_prefill.take();
5084 while self.dsv41.is_some()
5085 && mtp.is_none()
5086 && pos < input_ids.len()
5087 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5088 {
5089 let end = (pos + prefill_chunk()).min(input_ids.len());
5090 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5091 let mut lg = Vec::new();
5092 if let Some(b) = &mut self.dsv41 {
5093 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5094 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5095 crate::dsv41::forward_chunk_masked_with_embeddings(
5096 g,
5097 layers,
5098 cfg,
5099 st,
5100 &ids,
5101 pos,
5102 &embeddings[pos..end],
5103 &participates[pos..end],
5104 self.pool.as_deref(),
5105 &mut lg,
5106 );
5107 } else {
5108 crate::dsv41::forward_chunk(
5109 g,
5110 layers,
5111 cfg,
5112 st,
5113 &ids,
5114 pos,
5115 self.pool.as_deref(),
5116 &mut lg,
5117 );
5118 }
5119 }
5120 if end == input_ids.len() {
5121 self.graph_logits = Some(lg);
5122 }
5123 pos = end;
5124 hidden = vec![0.0; self.hidden_size];
5125 }
5126 let dyn_prefill = router.is_some();
5131 let o1_prefill_limit = o1_prefill
5139 .and_then(|requested| self.o1_effective_boundary(requested))
5140 .map(|boundary| boundary.min(input_ids.len()));
5141 let mut o1_sealed = false;
5142 if let Some(limit) = o1_prefill_limit {
5143 if self.can_prefill_batched() && limit > 2 {
5146 let chunk = self.prefill_chunk();
5147 let hs = self.hidden_size;
5148 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5149 let end = (pos + chunk).min(limit);
5150 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5151 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5152 pos = end;
5153 }
5154 } else {
5155 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5156 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5157 pos += 1;
5158 }
5159 }
5160 if pos >= limit {
5161 o1_sealed = match self.o1_seal_checked() {
5162 Ok(sealed) => sealed,
5163 Err(err) => {
5164 self.finish_generation(&mut mtp, &mut router, true);
5165 return Err(err);
5166 }
5167 };
5168 tracing::info!(
5169 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5170 o1_prefill.unwrap_or(0),
5171 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5172 .unwrap_or(limit),
5173 limit,
5174 input_ids.len()
5175 );
5176 }
5177 }
5178 let graph_prefill = self.graph_prefill_preferred();
5184 #[cfg(target_os = "macos")]
5192 if task_mask.is_none()
5193 && !dyn_prefill
5194 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5195 && crate::gpu::enabled_here()
5196 && self.gdn_cfg.is_some()
5197 && !self.per_token_stack()
5198 && input_ids.len() > 8
5199 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5200 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5201 {
5202 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5203 .ok()
5204 .and_then(|v| v.parse().ok())
5205 .filter(|&v| (16..=512).contains(&v))
5206 .unwrap_or(256);
5207 let hs = self.hidden_size;
5208 let _tp = std::time::Instant::now();
5209 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5210 let end = (pos + chunk).min(input_ids.len());
5211 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5212 MetalPrefillOutcome::Completed(hb) => hb,
5213 MetalPrefillOutcome::Declined => break,
5214 MetalPrefillOutcome::Failed => {
5215 self.finish_generation(&mut mtp, &mut router, true);
5216 return Err("ordinary Metal prefill failed after admission".into());
5217 }
5218 };
5219 if let Some(m) = &mut mtp {
5220 let n_pairs = if end < input_ids.len() {
5221 end - pos
5222 } else {
5223 end - pos - 1
5224 };
5225 if n_pairs > 0 {
5226 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5227 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5228 .collect();
5229 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5230 for (j, (h, t)) in pairs.iter().enumerate() {
5231 let h = h.to_vec();
5232 let _ = self.mtp_step(m, &h, *t, pos + j);
5233 }
5234 }
5235 }
5236 }
5237 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5238 pos = end;
5239 }
5240 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5241 eprintln!(
5242 "metal-prefill: {} of {} tokens in {:.1} ms",
5243 pos,
5244 input_ids.len(),
5245 _tp.elapsed().as_secs_f64() * 1e3
5246 );
5247 }
5248 }
5249 #[cfg(not(target_os = "macos"))]
5254 if task_mask.is_none()
5255 && !dyn_prefill
5256 && mtp.is_none()
5257 && o1_prefill.is_none()
5258 && batch_k > 0
5259 && pos + 1 < input_ids.len()
5260 && self.g4_batch_prefill()
5261 {
5262 let t_g4 = std::time::Instant::now();
5263 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5264 Ok((n, last)) => {
5265 if n > 0 {
5266 hidden.copy_from_slice(&last);
5267 }
5268 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5269 eprintln!(
5270 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5271 input_ids.len() - pos,
5272 t_g4.elapsed().as_secs_f64() * 1e3
5273 );
5274 }
5275 pos += n;
5276 }
5277 Err(e) => {
5278 self.finish_generation(&mut mtp, &mut router, true);
5279 return Err(e);
5280 }
5281 }
5282 }
5283 self.mimo_moe_prepare();
5284 #[cfg(not(target_os = "macos"))]
5290 if task_mask.is_none()
5291 && !dyn_prefill
5292 && !graph_prefill
5293 && mtp.is_none()
5294 && o1_prefill.is_none()
5295 && !self.o1_active()
5296 && input_ids.len() > 2
5297 && self.batch_prefix_prefill()
5298 {
5299 let chunk = self.prefill_chunk().max(1);
5300 let hs = self.hidden_size;
5301 let t_bp = std::time::Instant::now();
5302 let pos0 = pos;
5303 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5304 let end = (pos + chunk).min(input_ids.len());
5305 let bk = end - pos;
5306 let mut hiddens = vec![0f32; bk * hs];
5307 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5308 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5309 }
5310 let positions: Vec<usize> = (pos..end).collect();
5311 let mut run = 0usize;
5312 let outcome = self.try_batch_graph_wgpu_prefix(
5313 &mut hiddens,
5314 &positions,
5315 bk,
5316 None,
5317 Some(&mut run),
5318 );
5319 match outcome {
5320 crate::gpu::BatchGraphOutcome::Completed => {
5321 let hb = if run < self.num_layers {
5322 self.prefill_batch_span(
5323 PrefillIn::Hidden(&hiddens),
5324 pos,
5325 None,
5326 run,
5327 self.num_layers,
5328 )
5329 } else {
5330 hiddens
5331 };
5332 if mimo_spec {
5333 self.mimo_note_rows(&hb, pos);
5334 }
5335 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5336 pos = end;
5337 }
5338 crate::gpu::BatchGraphOutcome::Failed => {
5339 self.finish_generation(&mut mtp, &mut router, true);
5340 return Err("batched prefix prefill failed after admission".into());
5341 }
5342 crate::gpu::BatchGraphOutcome::Declined => {
5343 #[cfg(feature = "gpu")]
5346 if pos > pos0 {
5347 self.pull_lagging_host_kv(0, self.num_layers, pos);
5348 }
5349 break;
5350 }
5351 }
5352 }
5353 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5354 eprintln!(
5355 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5356 pos - pos0,
5357 input_ids.len(),
5358 t_bp.elapsed().as_secs_f64() * 1e3
5359 );
5360 }
5361 }
5362 if task_mask.is_none()
5363 && !dyn_prefill
5364 && !graph_prefill
5365 && self.can_prefill_batched()
5366 && !self.per_token_stack()
5367 && o1_prefill.is_none()
5368 && input_ids.len() > 2
5369 {
5370 let chunk = self.prefill_chunk();
5376 let hs = self.hidden_size;
5377 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5378 let end = (pos + chunk).min(input_ids.len());
5379 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5380 if mimo_spec {
5381 self.mimo_note_rows(&hb, pos);
5382 }
5383 if let Some(m) = &mut mtp {
5384 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5385 .ok()
5386 .and_then(|v| v.parse().ok())
5387 .unwrap_or(0);
5388 for p in pos..end {
5389 if p + 1 < input_ids.len() {
5390 if probe >= 1 && p + 2 < input_ids.len() {
5391 let (d1, mut hx) = self.mtp_step_h(
5395 m,
5396 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5397 input_ids[p + 1],
5398 p,
5399 );
5400 let mut ok = d1 == input_ids[p + 2];
5401 Self::chain_probe_note(0, ok);
5402 let mut d_prev = d1;
5403 let mut extra = 0usize;
5404 for j in 1..probe {
5405 if p + 2 + j >= input_ids.len() {
5406 break;
5407 }
5408 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5409 extra += 1;
5410 ok = ok && dj == input_ids[p + 2 + j];
5411 Self::chain_probe_note(j, ok);
5412 d_prev = dj;
5413 hx = hj;
5414 }
5415 m.kv.truncate_last(extra);
5416 } else {
5417 let _ = self.mtp_step(
5418 m,
5419 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5420 input_ids[p + 1],
5421 p,
5422 );
5423 }
5424 }
5425 }
5426 }
5427 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5428 pos = end;
5429 }
5430 }
5431 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5432 if task_mask.is_none()
5433 && !dyn_prefill
5434 && !graph_prefill
5435 && !pair_off
5436 && self.pair_supported()
5437 && o1_prefill.is_none()
5438 {
5439 while pos + 1 < input_ids.len()
5440 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5441 {
5442 let e1 = self.embed_single(input_ids[pos]);
5443 let e2 = self.embed_single(input_ids[pos + 1]);
5444 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5445 if mimo_spec {
5446 self.mimo_note_rows(&h1, pos);
5447 self.mimo_note_rows(&h2, pos + 1);
5448 }
5449 self.commit_linear_scratch();
5451 if let Some(m) = &mut mtp {
5452 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5453 if pos + 2 < input_ids.len() {
5454 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5455 .ok()
5456 .and_then(|v| v.parse().ok())
5457 .unwrap_or(0);
5458 if probe >= 1 && pos + 3 < input_ids.len() {
5459 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5463 let mut ok = d1 == input_ids[pos + 3];
5464 Self::chain_probe_note(0, ok);
5465 let mut d_prev = d1;
5466 let mut extra = 0usize;
5467 for j in 1..probe {
5468 if pos + 3 + j >= input_ids.len() {
5469 break;
5470 }
5471 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5472 extra += 1;
5473 ok = ok && dj == input_ids[pos + 3 + j];
5474 Self::chain_probe_note(j, ok);
5475 d_prev = dj;
5476 hx = hj;
5477 }
5478 m.kv.truncate_last(extra);
5479 } else {
5480 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5481 }
5482 }
5483 }
5484 hidden = h2;
5485 pos += 2;
5486 }
5487 }
5488 let o1_batch_ready = o1_sealed
5501 && o1_prefill.is_some()
5502 && mtp.is_none()
5503 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5504 && (0..self.num_layers).all(|li| {
5505 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5506 cache.o1.is_none() || cache.o1_views().is_some()
5507 });
5508 let mtp_batch_prefill = mtp.is_some()
5513 && graph_prefill
5514 && task_mask.is_none()
5515 && !dyn_prefill
5516 && !self.o1_active()
5517 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5518 if batch_k > 0
5519 && (graph_prefill || o1_batch_ready)
5520 && task_mask.is_none()
5521 && (!self.o1_active() || o1_batch_ready)
5522 && (mtp.is_none() || mtp_batch_prefill)
5523 && !dyn_prefill
5524 && pos + 1 < input_ids.len()
5525 {
5526 let hs = self.hidden_size;
5527 let chunk = batch_k;
5528 while pos < input_ids.len() {
5529 let end = (pos + chunk).min(input_ids.len());
5530 let bk = end - pos;
5531 let mut hiddens = vec![0f32; bk * hs];
5532 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5533 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5534 }
5535 let positions: Vec<usize> = (pos..end).collect();
5536 let t_chunk = std::time::Instant::now();
5537 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5538 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5539 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5540 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5541 eprintln!(
5542 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5543 if o1_batch_ready {
5544 "o1"
5545 } else if mtp_batch_prefill {
5546 "ordinary_mtp"
5547 } else {
5548 "ordinary"
5549 },
5550 bk as f64 / (ms / 1000.0)
5551 );
5552 }
5553 {
5554 use std::sync::atomic::{AtomicBool, Ordering};
5555 static SAID: AtomicBool = AtomicBool::new(false);
5556 if !SAID.swap(true, Ordering::Relaxed) {
5557 if ok_b {
5558 tracing::info!(
5559 "batched prefill: ACTIVE mode={} (k={bk})",
5560 if o1_batch_ready {
5561 "o1"
5562 } else if mtp_batch_prefill {
5563 "ordinary_mtp"
5564 } else {
5565 "ordinary"
5566 }
5567 );
5568 } else {
5569 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5570 }
5571 }
5572 }
5573 if ok_b {
5574 if mimo_spec {
5575 self.mimo_note_rows(&hiddens, pos);
5576 }
5577 if mtp_batch_prefill {
5578 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5579 if n_pairs > 0 {
5580 let rows: Vec<Vec<f32>> = (0..n_pairs)
5586 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5587 .collect();
5588 let pairs: Vec<(&[f32], u32)> = rows
5589 .iter()
5590 .enumerate()
5591 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5592 .collect();
5593 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5594 eprintln!(
5595 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5596 pos,
5597 n_pairs,
5598 pos + n_pairs - 1,
5599 );
5600 }
5601 let warm_error = if let Some(m) = mtp.as_mut() {
5602 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5603 } else {
5604 None
5605 };
5606 if let Some(err) = warm_error {
5607 self.finish_generation(&mut mtp, &mut router, true);
5612 return Err(err.to_string());
5613 }
5614 }
5615 }
5616 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5617 pos = end;
5618 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5619 self.finish_generation(&mut mtp, &mut router, true);
5624 return Err(if o1_batch_ready {
5625 "sealed O(1) batch graph failed after admission".to_string()
5626 } else {
5627 "ordinary recurrent batch graph failed after admission".to_string()
5628 });
5629 } else {
5630 break; }
5632 }
5633 }
5634 if graph_prefill
5638 && task_mask.is_none()
5639 && mtp.is_none()
5640 && !dyn_prefill
5641 && pos == 0
5642 && input_ids.len() > 1
5643 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5644 {
5645 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5646 self.graph_logits = Some(lg);
5647 hidden = vec![0.0; self.hidden_size];
5648 pos = input_ids.len();
5649 }
5650 }
5651 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5652 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5653 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5654 if mimo_spec {
5655 self.mimo_note_rows(&hidden, pos);
5656 }
5657 if let Some(m) = &mut mtp {
5658 if pos + 1 < input_ids.len() {
5659 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5665 .ok()
5666 .and_then(|v| v.parse().ok())
5667 .unwrap_or(0);
5668 if probe >= 1 && pos + 2 < input_ids.len() {
5669 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5670 let mut ok = d1 == input_ids[pos + 2];
5671 Self::chain_probe_note(0, ok);
5672 let mut d_prev = d1;
5673 let mut extra = 0usize;
5674 for j in 1..probe {
5675 if pos + 2 + j >= input_ids.len() {
5676 break;
5677 }
5678 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5679 extra += 1;
5680 ok = ok && dj == input_ids[pos + 2 + j];
5681 Self::chain_probe_note(j, ok);
5682 d_prev = dj;
5683 hx = hj;
5684 }
5685 m.kv.truncate_last(extra);
5688 } else {
5689 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5690 }
5691 }
5692 }
5693 pos += 1;
5694 }
5695 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5696 eprintln!(
5697 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5698 input_ids.len(),
5699 _tpf.elapsed().as_secs_f64() * 1000.0
5700 );
5701 }
5702 if self
5703 .graph_failed
5704 .swap(false, std::sync::atomic::Ordering::Relaxed)
5705 {
5706 self.finish_generation(&mut mtp, &mut router, true);
5711 return Err("GPU token graph failed during prefill".to_string());
5712 }
5713 if self
5716 .cancel
5717 .swap(false, std::sync::atomic::Ordering::Relaxed)
5718 {
5719 self.finish_generation(&mut mtp, &mut router, true);
5723 return Ok(GenerateResult {
5724 text: String::new(),
5725 token_ids: Vec::new(),
5726 prompt_tokens: input_ids.len(),
5727 tokens_generated: 0,
5728 finish_reason: "cancelled".to_string(),
5729 mtp_drafted: 0,
5730 mtp_accepted: 0,
5731 token_confidence: Vec::new(),
5732 traces: Vec::new(),
5733 });
5734 }
5735
5736 if !o1_sealed {
5739 match self.o1_seal_checked() {
5740 Ok(_) => {}
5741 Err(err) => {
5742 self.finish_generation(&mut mtp, &mut router, true);
5743 return Err(err);
5744 }
5745 }
5746 }
5747
5748 macro_rules! commit {
5750 ($id:expr) => {{
5751 all_ids.push($id);
5752 generated += 1;
5753 self.note_draft_id($id);
5754 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5755 finish_reason = "stop".to_string();
5756 false
5757 } else {
5758 let token_text = self.tokenizer.decode_token($id);
5759 let mut go = true;
5760 if let Some(ref mut cb) = on_token {
5761 if !cb(&token_text) {
5762 finish_reason = "cancelled".to_string();
5763 go = false;
5764 }
5765 }
5766 go
5767 }
5768 }};
5769 }
5770
5771 let mut spec_trial = SpecTrial::Spec {
5782 t0: std::time::Instant::now(),
5783 gen0: generated,
5784 rounds: 0,
5785 };
5786 let mut spec_mon = SpecMon {
5792 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5793 ..SpecMon::default()
5794 };
5795 let mut spec_watchdog_off = false;
5796 let mut spec_walls: Vec<f32> = Vec::new();
5799 let mut spec_round_end: Option<std::time::Instant> = None;
5802 if mimo_spec {
5803 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5804 if let Some(mut st) = self.mimo_mtp.take() {
5805 self.mimo_mtp_probe(&mut st, input_ids, &path);
5806 self.mimo_mtp = Some(st);
5807 }
5808 }
5809 }
5810 let mut next_pos = input_ids.len();
5812 'decode: while generated < max_tokens {
5813 if self
5814 .graph_failed
5815 .swap(false, std::sync::atomic::Ordering::Relaxed)
5816 {
5817 self.finish_generation(&mut mtp, &mut router, true);
5822 return Err("GPU token graph failed during decode".to_string());
5823 }
5824 if self
5825 .cancel
5826 .swap(false, std::sync::atomic::Ordering::Relaxed)
5827 {
5828 finish_reason = "cancelled".to_string();
5829 break 'decode;
5830 }
5831 if mimo_spec && next_pos > 0 {
5836 self.mimo_note_rows(&hidden, next_pos - 1);
5839 }
5840 let forced = self.spec_forced.take();
5841 let mut logits = match (forced, self.graph_logits.take()) {
5842 (Some(_), _) => Vec::new(),
5843 (None, Some(lg)) => lg,
5844 (None, None) => {
5845 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5846 #[cfg(feature = "gpu")]
5851 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5852 lg
5853 } else {
5854 inference::rms_norm_into(
5855 &hidden,
5856 &self.weights.final_norm,
5857 self.rms_eps,
5858 self.norm_style,
5859 &mut self.ws.n1,
5860 );
5861 self.lm_head_forward(&self.ws.n1)
5862 };
5863 #[cfg(not(feature = "gpu"))]
5864 let logits_out = {
5865 inference::rms_norm_into(
5866 &hidden,
5867 &self.weights.final_norm,
5868 self.rms_eps,
5869 self.norm_style,
5870 &mut self.ws.n1,
5871 );
5872 self.lm_head_forward(&self.ws.n1)
5873 };
5874 logits_out
5875 }
5876 };
5877 if generated
5880 == std::env::var("CMF_LOGIT_DUMP_STEP")
5881 .ok()
5882 .and_then(|v| v.parse().ok())
5883 .unwrap_or(0)
5884 {
5885 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5886 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5887 for v in hidden.iter().chain(logits.iter()) {
5888 bytes.extend_from_slice(&v.to_le_bytes());
5889 }
5890 if let Err(e) = std::fs::write(&path, &bytes) {
5891 eprintln!("logit dump: failed to write {path}: {e}");
5892 self.finish_generation(&mut mtp, &mut router, true);
5893 return Err(format!("logit dump write failed: {e}"));
5894 }
5895 }
5896 }
5897 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5901 if !logits.is_empty() {
5902 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5903 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5904 if let Err(e) =
5905 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5906 {
5907 eprintln!("logit dump: failed to write {}: {e}", path.display());
5908 }
5909 }
5910 }
5911 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5912 Some(c) => c,
5913 None => {
5914 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5915 sampler::sample_with_scratch_pool(
5916 &logits,
5917 &self.sampler_config,
5918 self.sampler_config.penalty_past(&all_ids, bounded_native),
5919 &mut self.rng,
5920 &mut self.sampler_scratch,
5921 self.pool.as_deref(),
5922 )
5923 }
5924 };
5925 if self.confidence_on {
5926 confidence.push(if logits.is_empty() {
5927 0.0
5928 } else {
5929 sampler::top1_prob_pool(
5930 self.pool.as_deref(),
5931 &mut self.sampler_scratch,
5932 &logits,
5933 t_next,
5934 calib_temp,
5935 )
5936 });
5937 }
5938 if !logits.is_empty() {
5939 attention::recycle_buf(&mut logits);
5940 }
5941 if trace_on {
5942 let skill = router.as_ref().and_then(|r| r.active_id());
5946 traces.push(TokenTrace {
5947 t: generated,
5948 token_id: t_next,
5949 confidence: confidence.last().copied().unwrap_or(0.0),
5950 active_skill: skill,
5951 recon: None,
5952 switched: false,
5953 });
5954 }
5955 if !commit!(t_next) {
5956 break 'decode;
5957 }
5958 if generated >= max_tokens {
5959 break 'decode;
5960 }
5961
5962 self.swa_trim_tails();
5965 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5966 static SAID: std::sync::Once = std::sync::Once::new();
5972 SAID.call_once(|| {
5973 tracing::warn!(
5974 "KV cache full at {} positions — evicting half; quality \
5975 will degrade. Raise CMF_MAX_SEQ.",
5976 self.kv_cache.max_seq_len,
5977 );
5978 });
5979 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5980 self.kv_cache.evict(keep);
5981 }
5982
5983 if graph_spec {
5986 match spec_trial {
5987 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5988 spec_mon.plain_ms =
5989 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5990 let keep = spec_mon.pays();
5991 tracing::info!(
5992 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5993 spec_mon.tokens,
5994 spec_mon.round_ms,
5995 spec_mon.plain_ms,
5996 if keep { "speculating" } else { "plain" }
5997 );
5998 spec_mon.fails = 0;
5999 spec_trial = SpecTrial::Decided {
6000 spec: keep,
6001 recheck_at: if keep { usize::MAX } else { generated + 128 },
6002 };
6003 }
6004 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6005 spec_mon.n = 0;
6006 spec_mon.zero_accept_rounds = 0;
6007 spec_trial = SpecTrial::Spec {
6008 t0: std::time::Instant::now(),
6009 gen0: generated,
6010 rounds: 0,
6011 };
6012 }
6013 _ => {}
6014 }
6015 spec_watchdog_off = matches!(
6016 spec_trial,
6017 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6018 );
6019 }
6020 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6022 let budget = max_tokens - generated - 1;
6023 if let Some(mut st) = self.mimo_mtp.take() {
6024 let k = st.depth.min(budget);
6025 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6026 self.mimo_mtp = Some(st);
6027 let r = match r {
6028 Ok(r) => r,
6029 Err(err) => {
6030 self.finish_generation(&mut mtp, &mut router, true);
6031 return Err(err);
6032 }
6033 };
6034 if let Some(r) = r {
6035 drafted += r.drafted;
6036 accepted += r.accepted.len();
6037 let mut stopped = false;
6038 for &id in &r.accepted {
6039 if self.confidence_on {
6040 confidence.push(0.0);
6041 }
6042 if !commit!(id) {
6043 stopped = true;
6044 break;
6045 }
6046 }
6047 if stopped {
6048 break 'decode;
6049 }
6050 next_pos += r.accepted.len() + 1;
6051 hidden = r.hidden;
6052 self.graph_logits = Some(r.logits);
6055 continue 'decode;
6056 }
6057 }
6058 }
6059 #[cfg(feature = "gpu")]
6062 if self.speculative
6063 && self.qwen4_exp.is_some()
6064 && task_mask.is_none()
6065 && self.sampler_config.temperature < 1e-6
6066 && generated + 1 < max_tokens
6067 && next_pos > 0
6068 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6069 {
6070 let r = match &mut self.qwen4_exp {
6071 Some(b) => crate::qwen4_exp::spec_round(
6072 &b.0,
6073 &b.1,
6074 &b.2,
6075 &mut b.3,
6076 next_pos,
6077 &all_ids,
6078 &self.inv_freq,
6079 self.pool.as_deref(),
6080 ),
6081 None => None,
6082 };
6083 if let Some(r) = r {
6084 drafted += r.drafted;
6085 accepted += r.accepted.len();
6086 let mut stopped = false;
6087 for &id in &r.accepted {
6088 if self.confidence_on {
6089 confidence.push(0.0);
6090 }
6091 if !commit!(id) {
6092 stopped = true;
6093 break;
6094 }
6095 }
6096 if stopped {
6097 break 'decode;
6098 }
6099 next_pos += r.accepted.len() + 1;
6100 hidden.fill(0.0);
6101 self.graph_logits = Some(r.logits);
6102 continue 'decode;
6103 }
6104 }
6105 match &mut mtp {
6106 #[cfg(feature = "gpu")]
6108 Some(m)
6109 if graph_spec
6110 && !spec_watchdog_off
6111 && generated + 1 < max_tokens
6112 && next_pos > 0 =>
6113 {
6114 let t_round = std::time::Instant::now();
6115 if spec_time_level() >= 2 {
6116 if let Some(t) = spec_round_end.take() {
6117 eprintln!(
6118 "spec-gap {:.2} ms (host between rounds)",
6119 t.elapsed().as_secs_f64() * 1e3
6120 );
6121 }
6122 }
6123 spec_stamps_begin();
6124 #[cfg(target_os = "macos")]
6129 let allocs0 =
6130 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6131 #[cfg(not(target_os = "macos"))]
6132 let allocs0 = 0u64;
6133 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6134 m,
6135 &hidden,
6136 t_next,
6137 next_pos,
6138 &mut drafted,
6139 &mut accepted,
6140 &mut all_ids,
6141 max_tokens - generated,
6142 ) {
6143 next_pos = n_pos;
6144 hidden = new_h;
6145 let level = spec_time_level();
6146 if level > 0 {
6147 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6148 let stamps = spec_stamps_take();
6149 let median = if spec_walls.len() >= 3 {
6152 let mut s = spec_walls.clone();
6153 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6154 Some(s[s.len() / 2])
6155 } else {
6156 None
6157 };
6158 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6159 #[cfg(target_os = "macos")]
6160 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6161 .load(std::sync::atomic::Ordering::Relaxed)
6162 - allocs0;
6163 #[cfg(not(target_os = "macos"))]
6164 let allocs = allocs0;
6165 eprintln!(
6166 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6167 extra.len() + 1,
6168 if allocs > 0 {
6169 format!(" [{allocs} new device buffers]")
6170 } else {
6171 String::new()
6172 },
6173 match (outlier, median) {
6174 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6175 _ => String::new(),
6176 }
6177 );
6178 if level >= 2 || outlier {
6179 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6180 eprintln!(
6181 "spec-stamps: {}| untracked {:.1}",
6182 spec_stamps_format(&stamps),
6183 wall - sum
6184 );
6185 }
6186 if spec_mon.n >= 1 {
6187 spec_walls.push(wall);
6188 }
6189 }
6190 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6194 spec_trial = Self::spec_trial_round(
6197 spec_trial,
6198 &mut spec_mon,
6199 generated + extra.len() + 1,
6200 );
6201 let mut stopped = false;
6202 for &id in &extra {
6203 if self.confidence_on {
6204 confidence.push(0.0);
6205 }
6206 if trace_on {
6219 traces.push(TokenTrace {
6220 t: generated,
6221 token_id: id,
6222 confidence: confidence.last().copied().unwrap_or(0.0),
6223 active_skill: None,
6224 recon: None,
6225 switched: false,
6226 });
6227 }
6228 if !commit!(id) {
6229 stopped = true;
6230 break;
6231 }
6232 }
6233 if stopped {
6234 break 'decode;
6235 }
6236 if spec_time_level() >= 2 {
6237 spec_round_end = Some(std::time::Instant::now());
6238 }
6239 continue 'decode;
6240 }
6241 if self
6242 .graph_failed
6243 .swap(false, std::sync::atomic::Ordering::Relaxed)
6244 {
6245 self.finish_generation(&mut mtp, &mut router, true);
6251 return Err("GPU MTP graph failed during speculative decode".to_string());
6252 }
6253 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6264 spec_mon.tokens = 0.0;
6265 spec_mon.fails = 3;
6266 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6267 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6268 next_pos += 1;
6269 continue 'decode;
6270 }
6271 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6273 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6274 drafted += 1;
6275 let emb1 = self.embed_single(t_next);
6276 let emb2 = self.embed_single(draft);
6277 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6278
6279 inference::rms_norm_into(
6280 &h1,
6281 &self.weights.final_norm,
6282 self.rms_eps,
6283 self.norm_style,
6284 &mut self.ws.n1,
6285 );
6286 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6287 let t_after = sampler::sample_with_scratch_pool(
6288 &logits1,
6289 &self.sampler_config,
6290 self.sampler_config.penalty_past(&all_ids, bounded_native),
6291 &mut self.rng,
6292 &mut self.sampler_scratch,
6293 self.pool.as_deref(),
6294 );
6295 if self.confidence_on {
6296 confidence.push(sampler::top1_prob_pool(
6297 self.pool.as_deref(),
6298 &mut self.sampler_scratch,
6299 &logits1,
6300 t_after,
6301 calib_temp,
6302 ));
6303 }
6304 attention::recycle_buf(&mut logits1);
6305 if trace_on {
6306 traces.push(TokenTrace {
6309 t: generated,
6310 token_id: t_after,
6311 confidence: confidence.last().copied().unwrap_or(0.0),
6312 active_skill: None,
6313 recon: None,
6314 switched: false,
6315 });
6316 }
6317 let stop = !commit!(t_after);
6318
6319 if t_after == draft {
6320 accepted += 1;
6321 self.commit_linear_scratch();
6322 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6323 hidden = h2;
6324 next_pos += 2;
6325 } else {
6326 for layer in &mut self.kv_cache.layers {
6328 layer.truncate_last(1);
6329 }
6330 if !stop {
6331 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6332 hidden = self.forward_layers(
6333 &self.embed_single(t_after),
6334 next_pos + 1,
6335 None,
6336 );
6337 }
6338 next_pos += 2;
6339 }
6340 if stop {
6341 break 'decode;
6342 }
6343 }
6344 _ => {
6346 #[cfg(feature = "gpu")]
6351 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6352 static SAID: std::sync::Once = std::sync::Once::new();
6353 SAID.call_once(|| {
6354 eprintln!(
6355 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6356 !self.dsv4_mtp.is_empty(),
6357 task_mask.is_none(),
6358 router.is_none(),
6359 !trace_on,
6360 self.sampler_config.temperature < 1e-6,
6361 self.sampler_config.repetition_penalty == 1.0,
6362 );
6363 });
6364 }
6365 #[cfg(feature = "gpu")]
6366 if Self::dsv4_spec_on()
6367 && self.dsv4.is_some()
6368 && !self.dsv4_mtp.is_empty()
6369 && task_mask.is_none()
6370 && router.is_none()
6371 && !trace_on
6372 && self.sampler_config.temperature < 1e-6
6373 && self.sampler_config.repetition_penalty == 1.0
6374 && generated + 1 < max_tokens
6375 && all_ids.len() >= 2
6376 && generated >= dsv4_spec_retry_at
6377 {
6378 let tip_token = all_ids[all_ids.len() - 2];
6379 let drafted0 = drafted;
6380 let round = self.dsv4_spec_step(
6381 tip_token,
6382 t_next,
6383 next_pos,
6384 max_tokens.saturating_sub(generated),
6385 &mut drafted,
6386 &mut accepted,
6387 );
6388 if drafted > drafted0 {
6389 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6390 if useful {
6391 dsv4_spec_bad = 0;
6392 } else {
6393 dsv4_spec_bad += 1;
6394 if dsv4_spec_bad >= 2 {
6395 dsv4_spec_bad = 0;
6396 dsv4_spec_retry_at = generated.saturating_add(32);
6397 tracing::info!(
6398 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6399 );
6400 }
6401 }
6402 }
6403 if let Some((extra, n_pos)) = round {
6404 next_pos = n_pos;
6405 let mut stopped = false;
6406 for &id in &extra {
6407 if self.confidence_on {
6408 confidence.push(0.0);
6409 }
6410 if !commit!(id) {
6411 stopped = true;
6412 break;
6413 }
6414 }
6415 if stopped {
6416 break 'decode;
6417 }
6418 continue 'decode;
6419 }
6420 }
6421 self.graph_want_logits = fuse_lm;
6422 let mut t_fwd = t_next;
6428 let pure_greedy = self.sampler_config.temperature < 1e-6
6429 && self.sampler_config.repetition_penalty == 1.0
6430 && self.sampler_config.presence_penalty == 0.0
6431 && self.sampler_config.suppress_tokens.is_empty();
6432 let burst_k = std::env::var("CMF_MULTISTEP")
6437 .ok()
6438 .and_then(|v| v.parse::<usize>().ok())
6439 .unwrap_or(0);
6440 if pure_greedy
6441 && burst_k >= 1
6442 && fuse_lm
6443 && task_mask.is_none()
6444 && router.is_none()
6445 && !trace_on
6446 && !self.confidence_on
6447 && std::env::var_os("CMF_FORCE_IDS").is_none()
6452 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6453 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6454 {
6455 let mut stopped = false;
6456 loop {
6457 if self
6462 .cancel
6463 .swap(false, std::sync::atomic::Ordering::Relaxed)
6464 {
6465 finish_reason = "cancelled".to_string();
6466 stopped = true;
6467 break;
6468 }
6469 let room = max_tokens.saturating_sub(generated);
6470 if room <= 2 {
6471 break;
6472 }
6473 let k = burst_k.min(room - 1);
6474 if k < 1 {
6475 break;
6476 }
6477 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6478 if self
6479 .graph_failed
6480 .swap(false, std::sync::atomic::Ordering::Relaxed)
6481 {
6482 self.finish_generation(&mut mtp, &mut router, true);
6483 return Err(
6484 "GPU token graph failed during greedy burst".to_string()
6485 );
6486 }
6487 break;
6488 };
6489 next_pos += k;
6490 for &id in &ids {
6491 if !commit!(id) {
6492 stopped = true;
6493 break;
6494 }
6495 }
6496 if stopped {
6497 break;
6498 }
6499 t_fwd = *ids.last().unwrap();
6500 }
6501 if stopped {
6502 break 'decode;
6503 }
6504 }
6505 #[cfg(target_os = "macos")]
6515 if graph_spec
6516 && spec_watchdog_off
6517 && next_pos > 0
6518 && self.mtp_graph_mode == Some(true)
6519 && crate::gpu::q1_force()
6520 {
6521 if let Some(m) = mtp.as_mut() {
6522 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6523 }
6524 }
6525 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6526 next_pos += 1;
6527 if let Some(r) = &mut router {
6530 let phi = self.dyn_phi_ema.clone();
6531 let decision = r.step(&phi, generated);
6532 if let Some(new_active) = decision {
6533 let _ = self.set_active_skill(new_active);
6534 }
6535 if trace_on {
6538 if let Some(last) = traces.last_mut() {
6539 let e = r.last_best_e();
6540 last.recon = e.is_finite().then_some(e);
6541 last.switched = decision.is_some();
6542 }
6543 }
6544 }
6545 }
6546 }
6547 }
6548
6549 let cancelled = finish_reason == "cancelled";
6550 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6554 if mimo_spec {
6555 if let Some(st) = self.mimo_mtp.as_ref() {
6556 let line = st.stats.line();
6557 tracing::info!("{line}");
6558 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6559 eprintln!("{line}");
6560 }
6561 }
6562 }
6563 self.finish_generation(&mut mtp, &mut router, cancelled);
6564
6565 let output_ids = &all_ids[input_ids.len()..];
6566 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6570 let consumed = std::mem::take(&mut all_ids);
6575 if dyn_switched {
6576 self.clear_sequence_state();
6577 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6578 self.clear_history();
6579 } else {
6580 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6581 }
6582 all_ids = consumed;
6583 let output_ids = &all_ids[input_ids.len()..];
6584 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6586 Ok(GenerateResult {
6587 text: self.tokenizer.decode(output_ids),
6588 token_ids: output_ids.to_vec(),
6589 prompt_tokens: input_ids.len(),
6590 tokens_generated: generated,
6591 finish_reason,
6592 mtp_drafted: drafted,
6593 mtp_accepted: accepted,
6594 token_confidence: confidence,
6595 traces,
6596 })
6597 }
6598
6599 fn mtp_step(
6603 &mut self,
6604 m: &mut MtpModule,
6605 hidden: &[f32],
6606 next_token: u32,
6607 position: usize,
6608 ) -> u32 {
6609 self.mtp_step_h(m, hidden, next_token, position).0
6610 }
6611
6612 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6616 use std::sync::Mutex;
6617 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6618 let mut t = T.lock().unwrap();
6619 if t.len() <= depth {
6620 t.resize(depth + 1, (0, 0));
6621 }
6622 t[depth].0 += 1;
6623 t[depth].1 += prefix_ok as u64;
6624 if depth == 0 && t[0].0 % 128 == 0 {
6625 let line: Vec<String> = t
6626 .iter()
6627 .enumerate()
6628 .map(|(d, (n, k))| {
6629 format!(
6630 "d{}={:.0}%({n})",
6631 d + 1,
6632 100.0 * *k as f64 / (*n).max(1) as f64
6633 )
6634 })
6635 .collect();
6636 eprintln!("mtp-chain: {}", line.join(" "));
6637 }
6638 }
6639
6640 fn mtp_step_hl(
6648 &mut self,
6649 m: &mut MtpModule,
6650 hidden: &[f32],
6651 next_token: u32,
6652 position: usize,
6653 ) -> (Vec<f32>, Vec<f32>) {
6654 #[cfg(target_os = "macos")]
6659 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6660 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6661 self.mtp_graph_mode = Some(true);
6662 return r;
6663 }
6664 if self.mtp_graph_mode == Some(true) {
6665 tracing::error!("mtp Metal graph failed after admission");
6666 self.clear_sequence_state();
6667 self.graph_failed
6668 .store(true, std::sync::atomic::Ordering::Relaxed);
6669 self.cancel
6670 .store(true, std::sync::atomic::Ordering::Relaxed);
6671 return (Vec::new(), Vec::new());
6672 }
6673 self.mtp_graph_mode = Some(false);
6674 }
6675 #[cfg(feature = "gpu")]
6676 if self.mtp_graph_mode != Some(false) {
6677 if !self.mtp_graph_ok(m) {
6678 if self.mtp_graph_mode == Some(true) {
6679 tracing::error!("mtp graph became unavailable after admission");
6684 self.clear_sequence_state();
6685 self.graph_failed
6686 .store(true, std::sync::atomic::Ordering::Relaxed);
6687 self.cancel
6688 .store(true, std::sync::atomic::Ordering::Relaxed);
6689 return (Vec::new(), Vec::new());
6690 }
6691 self.mtp_graph_mode = Some(false);
6692 } else {
6693 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6694 self.mtp_graph_mode = Some(true);
6695 return r;
6696 }
6697 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6698 return (Vec::new(), Vec::new());
6705 }
6706 tracing::error!("mtp graph failed or declined after admission");
6710 self.clear_sequence_state();
6711 self.graph_failed
6712 .store(true, std::sync::atomic::Ordering::Relaxed);
6713 self.cancel
6714 .store(true, std::sync::atomic::Ordering::Relaxed);
6715 return (Vec::new(), Vec::new());
6716 }
6717 }
6718 let e = self.embed_single(next_token);
6722 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6723 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6724 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6725 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6726 let mut x = vec![0.0f32; self.hidden_size];
6727 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6728
6729 let lw = &m.layer;
6731 inference::rms_norm_into(
6732 &x,
6733 &lw.input_norm,
6734 self.rms_eps,
6735 self.norm_style,
6736 &mut self.ws.n1,
6737 );
6738 let attn = match &lw.attn {
6739 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6741 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6742 AttnKind::Full {
6743 wq,
6744 wk,
6745 wv,
6746 wo,
6747 q_norm,
6748 k_norm,
6749 output_gate,
6750 softplus_gate,
6751 bias,
6752 } => {
6753 let mut cfg = self.attn_cfg(position);
6754 cfg.q_norm = q_norm.as_deref();
6755 cfg.k_norm = k_norm.as_deref();
6756 cfg.output_gate = *output_gate;
6757 cfg.softplus_gate = softplus_gate
6758 .as_ref()
6759 .map(|(gate, per_head)| (gate, *per_head));
6760 cfg.bias = bias
6761 .as_ref()
6762 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6763 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6764 }
6765 AttnKind::Linear(_)
6766 | AttnKind::LinearGdn(_)
6767 | AttnKind::ShortConv(_)
6768 | AttnKind::Bounded(_) => {
6769 unreachable!("MTP block is full attention")
6770 }
6771 };
6772 for (i, &a) in attn.iter().enumerate() {
6773 x[i] += a;
6774 }
6775 inference::rms_norm_into(
6776 &x,
6777 &lw.post_norm,
6778 self.rms_eps,
6779 self.norm_style,
6780 &mut self.ws.p1,
6781 );
6782 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6783 for (i, &f) in ffn.iter().enumerate() {
6784 x[i] += f;
6785 }
6786
6787 inference::rms_norm_into(
6788 &x,
6789 &m.final_norm,
6790 self.rms_eps,
6791 self.norm_style,
6792 &mut self.ws.n1,
6793 );
6794 let lg = self.lm_head_forward(&self.ws.n1);
6795 (lg, x)
6796 }
6797
6798 fn mtp_step_h(
6800 &mut self,
6801 m: &mut MtpModule,
6802 hidden: &[f32],
6803 next_token: u32,
6804 position: usize,
6805 ) -> (u32, Vec<f32>) {
6806 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6807 let draft = sampler::argmax(&lg);
6808 attention::recycle_buf(&mut lg);
6809 (draft, x)
6810 }
6811
6812 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6818 match trial {
6819 SpecTrial::Spec { t0, gen0, rounds } => {
6820 let rounds = rounds + 1;
6821 if mon.repeated_zero_acceptance() {
6829 tracing::info!(
6830 "speculation trial: no accepted drafts in two rounds — timing plain"
6831 );
6832 return SpecTrial::Plain {
6833 t0: std::time::Instant::now(),
6834 gen0: generated,
6835 };
6836 }
6837 if rounds >= 5 {
6838 if mon.plain_ms > 0.0 {
6839 let keep = mon.pays();
6840 mon.fails = 0;
6841 tracing::info!(
6842 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6843 mon.tokens,
6844 mon.round_ms,
6845 mon.plain_ms,
6846 if keep { "speculating" } else { "plain" }
6847 );
6848 SpecTrial::Decided {
6849 spec: keep,
6850 recheck_at: if keep { usize::MAX } else { generated + 128 },
6851 }
6852 } else if mon.pays() {
6853 mon.fails = 0;
6858 tracing::info!(
6859 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6860 mon.tokens,
6861 mon.round_ms,
6862 );
6863 SpecTrial::Decided {
6864 spec: true,
6865 recheck_at: usize::MAX,
6866 }
6867 } else {
6868 SpecTrial::Plain {
6869 t0: std::time::Instant::now(),
6870 gen0: generated,
6871 }
6872 }
6873 } else {
6874 SpecTrial::Spec { t0, gen0, rounds }
6875 }
6876 }
6877 SpecTrial::Decided { spec: true, .. } => {
6878 if mon.pays() {
6879 mon.fails = 0;
6880 trial
6881 } else {
6882 mon.fails += 1;
6883 if mon.fails >= 4 {
6884 if mon.plain_ms <= 0.0 {
6885 tracing::info!(
6889 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6890 mon.tokens,
6891 mon.round_ms,
6892 );
6893 return SpecTrial::Plain {
6894 t0: std::time::Instant::now(),
6895 gen0: generated,
6896 };
6897 }
6898 tracing::info!(
6899 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6900 mon.tokens,
6901 mon.round_ms,
6902 mon.plain_ms
6903 );
6904 SpecTrial::Decided {
6905 spec: false,
6906 recheck_at: generated + 128,
6907 }
6908 } else {
6909 trial
6910 }
6911 }
6912 }
6913 other => other,
6914 }
6915 }
6916
6917 fn mtp_kv_id(&self) -> u64 {
6920 self.graph_kv_id | (1u64 << 40)
6921 }
6922
6923 const MTP_LAYER_BASE: usize = 0;
6928
6929 #[cfg(feature = "gpu")]
6936 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6937 self.mtp_graph_mode != Some(true)
6938 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6939 }
6940
6941 #[cfg(feature = "gpu")]
6947 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6948 let mut ok = true;
6949 let mut expected = false;
6950 for li in 0..self.num_layers {
6951 if matches!(
6952 self.weights.layers[self.phys_layer(li)].attn,
6953 AttnKind::Full { .. }
6954 ) {
6955 expected = true;
6956 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6957 }
6958 }
6959 !expected || ok
6960 }
6961
6962 fn graph_gdn_layer_count(&self) -> usize {
6966 (0..self.num_layers)
6967 .filter(|&li| {
6968 matches!(
6969 &self.weights.layers[self.phys_layer(li)].attn,
6970 AttnKind::LinearGdn(_)
6971 )
6972 })
6973 .count()
6974 }
6975
6976 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6979 let e = self.embed_single(next_token);
6980 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6981 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6982 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6983 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6984 let mut x = vec![0.0f32; self.hidden_size];
6985 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6986 x
6987 }
6988
6989 #[cfg(feature = "gpu")]
6992 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6993 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6994 return false;
6995 }
6996 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6997 || !crate::gpu::enabled_here()
6998 || self.attn_softcap > 0.0
6999 || self.attention_heads_per_layer.is_some()
7000 || self.v_head_dim.is_some()
7003 {
7004 return false;
7005 }
7006 matches!(
7007 &m.layer.attn,
7008 AttnKind::Full {
7009 softplus_gate: None,
7010 ..
7011 }
7012 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7013 }
7014
7015 #[cfg(feature = "gpu")]
7019 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7020 if !self.mtp_block_graph_ok(m) {
7021 return false;
7022 }
7023 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7024 return false;
7025 };
7026 let FfnKind::Dense(d) = &m.layer.ffn else {
7027 return false;
7028 };
7029 d.segs.is_empty()
7030 && wq.graph_weight().is_some()
7031 && wk.graph_weight().is_some()
7032 && wv.graph_weight().is_some()
7033 && wo.graph_weight().is_some()
7034 && d.gate_proj.graph_weight().is_some()
7035 && d.up_proj.graph_weight().is_some()
7036 && d.down_proj.graph_weight().is_some()
7037 && self.weights.lm_head.graph_weight().is_some()
7038 }
7039
7040 #[cfg(feature = "gpu")]
7046 fn mtp_step_graph(
7047 &mut self,
7048 m: &mut MtpModule,
7049 hidden: &[f32],
7050 next_token: u32,
7051 position: usize,
7052 ) -> Option<(Vec<f32>, Vec<f32>)> {
7053 if !self.mtp_graph_ok(m) {
7054 return None;
7055 }
7056 let lw = &m.layer;
7057 let AttnKind::Full {
7058 wq,
7059 wk,
7060 wv,
7061 wo,
7062 q_norm,
7063 k_norm,
7064 output_gate,
7065 softplus_gate,
7066 bias,
7067 } = &lw.attn
7068 else {
7069 return None;
7070 };
7071 if softplus_gate.is_some() {
7072 return None;
7073 }
7074 let FfnKind::Dense(d) = &lw.ffn else {
7075 return None;
7076 };
7077 if !d.segs.is_empty() {
7078 return None; }
7080 let mut x = self.mtp_block_input(m, hidden, next_token);
7083 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7084 let (_, i, kind, rs) = t.graph_weight()?;
7085 Some(crate::gpu::GraphW {
7086 idx: i,
7087 kind,
7088 row_scale: rs,
7089 data: &[],
7090 prism: crate::gpu::GraphPrismOp::None,
7091 affine: false,
7092 })
7093 }
7094 let (model, _, _, _) = wq.graph_weight()?;
7095 let model = model.clone();
7096 let (lm_gw, lm_rows) = {
7097 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7098 let rows = if kind == 6 {
7102 self.draft_head_rows(self.weights.lm_head.rows())
7103 } else {
7104 self.weights.lm_head.rows()
7105 };
7106 (
7107 crate::gpu::GraphW {
7108 idx: i,
7109 kind,
7110 row_scale: rs,
7111 data: &[],
7112 prism: crate::gpu::GraphPrismOp::None,
7113 affine: false,
7114 },
7115 rows,
7116 )
7117 };
7118 let layer = crate::gpu::GraphLayer {
7119 input_norm: &lw.input_norm,
7120 attn: crate::gpu::GraphAttn::Full {
7121 wq: gw(wq)?,
7122 wk: gw(wk)?,
7123 wv: gw(wv)?,
7124 wo: gw(wo)?,
7125 q_norm: q_norm.as_deref(),
7126 k_norm: k_norm.as_deref(),
7127 late_qk_norm: self.qk_norm_after_rope,
7128 bias: bias
7129 .as_ref()
7130 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7131 output_gate: *output_gate,
7132 cpu_k: m.kv.k_heads(),
7133 cpu_v: m.kv.v_heads(),
7134 cpu_base: m.kv.base(),
7135 geom: None,
7136 head_gate: None,
7137 },
7138 post_norm: &lw.post_norm,
7139 attn_out_norm: None,
7140 ffn_out_norm: None,
7141 layer_scale: None,
7142 ffn: crate::gpu::GraphFfn::Dense {
7143 gate: gw(&d.gate_proj)?,
7144 up: gw(&d.up_proj)?,
7145 down: gw(&d.down_proj)?,
7146 act: d.act.graph_act()?,
7147 },
7148 };
7149 let nh = self.num_heads;
7150 let (nkv, hd, rd) = self.layer_geom(0);
7151 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7152 let mut logits = Vec::new();
7153 let ok = crate::gpu::forward_token_graph(
7154 &model,
7155 self.mtp_kv_id(),
7156 std::slice::from_ref(&layer),
7157 &[None],
7158 self.o1_epoch,
7159 &self.inv_freq,
7160 &mut x,
7161 nh,
7162 nkv,
7163 hd,
7164 self.attn_scale,
7165 rd,
7166 self.hidden_size,
7167 d.gate_proj.rows(),
7171 position,
7172 self.kv_cache.max_seq_len,
7173 gemma,
7174 self.rms_eps as f32,
7175 Some((&lm_gw, lm_rows)),
7176 &m.final_norm,
7177 &mut logits,
7178 &[],
7179 1,
7180 None,
7181 None,
7182 None,
7183 Self::MTP_LAYER_BASE,
7184 true,
7185 );
7186 match ok {
7187 crate::gpu::TokenGraphOutcome::Completed => {}
7188 crate::gpu::TokenGraphOutcome::Declined => return None,
7189 crate::gpu::TokenGraphOutcome::Failed => {
7190 self.clear_sequence_state();
7194 self.graph_failed
7195 .store(true, std::sync::atomic::Ordering::Relaxed);
7196 self.cancel
7197 .store(true, std::sync::atomic::Ordering::Relaxed);
7198 return None;
7199 }
7200 }
7201 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7205 Some((logits, x))
7206 }
7207
7208 #[cfg(feature = "gpu")]
7216 fn mtp_warm_graph(
7217 &mut self,
7218 m: &mut MtpModule,
7219 pairs: &[(&[f32], u32)],
7220 first_pos: usize,
7221 ) -> crate::gpu::BatchGraphOutcome {
7222 if pairs.is_empty() {
7223 return crate::gpu::BatchGraphOutcome::Completed;
7224 }
7225 if !self.mtp_block_graph_ok(m) {
7226 return crate::gpu::BatchGraphOutcome::Declined;
7227 }
7228 let hs = self.hidden_size;
7229 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7232 for (h, t) in pairs {
7233 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7234 }
7235 let lw = &m.layer;
7236 let AttnKind::Full {
7237 wq,
7238 wk,
7239 wv,
7240 wo,
7241 q_norm,
7242 k_norm,
7243 output_gate,
7244 bias,
7245 ..
7246 } = &lw.attn
7247 else {
7248 return crate::gpu::BatchGraphOutcome::Declined;
7249 };
7250 let FfnKind::Dense(d) = &lw.ffn else {
7251 return crate::gpu::BatchGraphOutcome::Declined;
7252 };
7253 if !d.segs.is_empty() {
7254 return crate::gpu::BatchGraphOutcome::Declined; }
7256 let (
7259 AttnKind::Full {
7260 softplus_gate: None,
7261 ..
7262 },
7263 Some(gact),
7264 ) = (&lw.attn, d.act.graph_act())
7265 else {
7266 return crate::gpu::BatchGraphOutcome::Declined;
7267 };
7268 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7269 let (_, i, kind, rs) = t.graph_weight()?;
7270 Some(crate::gpu::GraphW {
7271 idx: i,
7272 kind,
7273 row_scale: rs,
7274 data: &[],
7275 prism: crate::gpu::GraphPrismOp::None,
7276 affine: false,
7277 })
7278 }
7279 let Some((model, _, _, _)) = wq.graph_weight() else {
7280 return crate::gpu::BatchGraphOutcome::Declined;
7281 };
7282 let model = model.clone();
7283 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7284 gw(wq),
7285 gw(wk),
7286 gw(wv),
7287 gw(wo),
7288 gw(&d.gate_proj),
7289 gw(&d.up_proj),
7290 gw(&d.down_proj),
7291 ) else {
7292 return crate::gpu::BatchGraphOutcome::Declined;
7293 };
7294 let layer = crate::gpu::GraphLayer {
7295 input_norm: &lw.input_norm,
7296 attn: crate::gpu::GraphAttn::Full {
7297 wq: gwq,
7298 wk: gwk,
7299 wv: gwv,
7300 wo: gwo,
7301 q_norm: q_norm.as_deref(),
7302 k_norm: k_norm.as_deref(),
7303 late_qk_norm: self.qk_norm_after_rope,
7304 bias: bias
7305 .as_ref()
7306 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7307 output_gate: *output_gate,
7308 cpu_k: m.kv.k_heads(),
7309 cpu_v: m.kv.v_heads(),
7310 cpu_base: m.kv.base(),
7311 geom: None,
7312 head_gate: None,
7313 },
7314 post_norm: &lw.post_norm,
7315 attn_out_norm: None,
7316 ffn_out_norm: None,
7317 layer_scale: None,
7318 ffn: crate::gpu::GraphFfn::Dense {
7319 gate: gg,
7320 up: gu,
7321 down: gd,
7322 act: gact,
7323 },
7324 };
7325 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7326 let nh = self.num_heads;
7327 let (nkv, hd, rd) = self.layer_geom(0);
7328 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7329 crate::gpu::forward_batch_graph(
7330 &model,
7331 self.mtp_kv_id(),
7332 std::slice::from_ref(&layer),
7333 &self.inv_freq,
7334 &mut hiddens,
7335 nh,
7336 nkv,
7337 hd,
7338 rd,
7339 hs,
7340 d.gate_proj.rows(),
7343 &positions,
7344 self.kv_cache.max_seq_len,
7345 gemma,
7346 self.rms_eps as f32,
7347 self.attn_scale,
7348 pairs.len(),
7349 &[],
7350 0,
7351 None,
7352 None,
7353 )
7354 }
7355
7356 #[cfg(feature = "gpu")]
7363 fn mtp_warm_graph_fallback(
7364 &mut self,
7365 m: &mut MtpModule,
7366 pairs: &[(&[f32], u32)],
7367 first_pos: usize,
7368 ) -> bool {
7369 if pairs.is_empty() {
7370 return true;
7371 }
7372 let graphable = self.mtp_block_graph_ok(m);
7373 if !graphable {
7374 if self.mtp_graph_mode == Some(true) {
7378 return false;
7379 }
7380 self.mtp_graph_mode = Some(false);
7381 for (j, (h, t)) in pairs.iter().enumerate() {
7382 self.mtp_warm(m, h, *t, first_pos + j);
7383 }
7384 return true;
7385 }
7386
7387 for (j, (h, t)) in pairs.iter().enumerate() {
7392 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7393 return false;
7394 }
7395 }
7396 self.mtp_graph_mode = Some(true);
7397 true
7398 }
7399
7400 #[cfg(feature = "gpu")]
7405 fn mtp_warm_prefill_pairs(
7406 &mut self,
7407 m: &mut MtpModule,
7408 pairs: &[(&[f32], u32)],
7409 first_pos: usize,
7410 ) -> Result<(), &'static str> {
7411 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7416 if self.mtp_graph_mode == Some(true) {
7417 return Err("MTP token graph became unavailable after admission");
7418 }
7419 self.mtp_graph_mode = Some(false);
7420 for (j, (h, t)) in pairs.iter().enumerate() {
7421 self.mtp_warm(m, h, *t, first_pos + j);
7422 }
7423 return Ok(());
7424 }
7425 match self.mtp_warm_graph(m, pairs, first_pos) {
7426 crate::gpu::BatchGraphOutcome::Completed => {
7427 if !pairs.is_empty() {
7428 self.mtp_graph_mode = Some(true);
7429 }
7430 Ok(())
7431 }
7432 crate::gpu::BatchGraphOutcome::Declined => {
7433 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7434 Ok(())
7435 } else {
7436 Err("MTP warm-up fallback failed after device admission")
7437 }
7438 }
7439 crate::gpu::BatchGraphOutcome::Failed => {
7440 Err("MTP warm batch graph failed after admission")
7441 }
7442 }
7443 }
7444
7445 #[cfg(not(feature = "gpu"))]
7446 fn mtp_warm_prefill_pairs(
7447 &mut self,
7448 m: &mut MtpModule,
7449 pairs: &[(&[f32], u32)],
7450 first_pos: usize,
7451 ) -> Result<(), &'static str> {
7452 for (j, (h, t)) in pairs.iter().enumerate() {
7453 self.mtp_warm(m, h, *t, first_pos + j);
7454 }
7455 Ok(())
7456 }
7457
7458 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7462 let e = self.embed_single(next_token);
7463 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7464 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7465 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7466 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7467 let mut x = vec![0.0f32; self.hidden_size];
7468 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7469 inference::rms_norm_into(
7470 &x,
7471 &m.layer.input_norm,
7472 self.rms_eps,
7473 self.norm_style,
7474 &mut self.ws.n1,
7475 );
7476 let attn = match &m.layer.attn {
7477 AttnKind::Full {
7478 wq,
7479 wk,
7480 wv,
7481 wo,
7482 q_norm,
7483 k_norm,
7484 output_gate,
7485 softplus_gate,
7486 bias,
7487 } => {
7488 let mut cfg = self.attn_cfg(position);
7489 cfg.q_norm = q_norm.as_deref();
7490 cfg.k_norm = k_norm.as_deref();
7491 cfg.output_gate = *output_gate;
7492 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7493 cfg.bias = bias
7494 .as_ref()
7495 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7496 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7497 }
7498 _ => return,
7499 };
7500 let _ = attn;
7501 }
7502
7503 #[cfg(feature = "gpu")]
7510 #[allow(clippy::too_many_arguments)]
7511 fn graph_spec_step(
7512 &mut self,
7513 m: &mut MtpModule,
7514 hidden: &[f32],
7515 t_next: u32,
7516 next_pos: usize,
7517 drafted: &mut usize,
7518 accepted: &mut usize,
7519 all_ids: &mut Vec<u32>,
7523 room: usize,
7528 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7529 #[cfg(target_os = "macos")]
7540 let metal_native = crate::gpu::q1_force();
7541 #[cfg(not(target_os = "macos"))]
7542 let metal_native = false;
7543 #[cfg(feature = "gpu")]
7544 let k_default = if metal_native {
7545 7
7548 } else if crate::gpu_wgpu::verify_i8_on() {
7549 5
7550 } else {
7551 4
7552 };
7553 #[cfg(not(feature = "gpu"))]
7554 let k_default = 4;
7555 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7556 .ok()
7557 .and_then(|v| v.parse().ok())
7558 .filter(|&v| (1..=8).contains(&v));
7559 let (k_start, k_max) = if metal_native {
7564 (7, 7)
7565 } else {
7566 (3, k_default.max(5))
7567 };
7568 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7569 let k_spec = k_full.min(room).max(1);
7570 let k_capped = k_spec < k_full;
7573 if next_pos == 0 {
7574 return None;
7575 }
7576 let t_round = std::time::Instant::now();
7577 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7593 let sub0 = subs();
7594 let cfg = self.sampler_config.clone();
7599 let penalized = !(cfg.repetition_penalty == 1.0
7600 && cfg.presence_penalty == 0.0
7601 && cfg.suppress_tokens.is_empty());
7602 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7607 let sampling = cfg.temperature >= 1e-6;
7608 let sparse = sampling && sampler::sparse_ok(&cfg);
7614 let base_len = all_ids.len();
7615 if sampling && !sparse && self.spec_q.len() < k_spec {
7616 self.spec_q.resize_with(k_spec, Vec::new);
7617 }
7618 if sparse && self.spec_qs.len() < k_spec {
7619 self.spec_qs.resize_with(k_spec, Vec::new);
7620 }
7621 let mut drafts = Vec::with_capacity(k_spec);
7626 let mut hx = hidden.to_vec();
7627 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7630 spec_stamp("pro");
7631 #[cfg(target_os = "macos")]
7637 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7638 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7639 Ok(ids) => {
7640 self.mtp_graph_mode = Some(true);
7641 drafts = ids;
7642 }
7643 Err(true) => {
7644 tracing::error!("mtp Metal draft chain failed after commit");
7645 self.clear_sequence_state();
7646 self.graph_failed
7647 .store(true, std::sync::atomic::Ordering::Relaxed);
7648 self.cancel
7649 .store(true, std::sync::atomic::Ordering::Relaxed);
7650 return None;
7651 }
7652 Err(false) => {}
7653 }
7654 }
7655 for j in drafts.len()..k_spec {
7656 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7657 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7658 if spec_dbg {
7659 let saved = self.mtp_graph_mode;
7660 self.mtp_graph_mode = Some(false);
7661 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7662 self.mtp_graph_mode = saved;
7663 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7664 return None;
7665 }
7666 m.kv.truncate_last(1);
7667 dbg_ref = Some(r);
7668 }
7669 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7670 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7671 return None;
7672 }
7673 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7674 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7675 let dl = lg
7676 .iter()
7677 .zip(&lg_cpu)
7678 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7679 let dh = hj
7680 .iter()
7681 .zip(&h_cpu)
7682 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7683 eprintln!(
7684 "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 {}",
7685 next_pos - 1 + j,
7686 sampler::argmax(&lg_cpu),
7687 sampler::argmax(&lg),
7688 n(&h_cpu),
7689 n(&hj),
7690 m.kv.seq_len
7691 );
7692 }
7693 let dj = if sparse {
7694 let mut q = std::mem::take(&mut self.spec_qs[j]);
7695 let ok = sampler::sparse_distribution_into(
7696 &lg,
7697 &cfg,
7698 all_ids,
7699 &mut self.sampler_scratch,
7700 self.pool.as_deref(),
7701 &mut q,
7702 );
7703 let d = if ok {
7704 sampler::draw_sparse(&q, &mut self.rng)
7705 } else {
7706 let t = sampler::argmax(&lg);
7708 q.clear();
7709 q.push((t, 1.0));
7710 t
7711 };
7712 self.spec_qs[j] = q;
7713 all_ids.push(d);
7714 d
7715 } else if sampling {
7716 let mut q = std::mem::take(&mut self.spec_q[j]);
7717 sampler::distribution_into(
7718 &lg,
7719 &cfg,
7720 all_ids,
7721 &mut self.sampler_scratch,
7722 self.pool.as_deref(),
7723 &mut q,
7724 );
7725 let d = sampler::draw(&q, &mut self.rng);
7726 self.spec_q[j] = q;
7727 all_ids.push(d); d
7729 } else if greedy_pen {
7730 let d = sampler::argmax_penalized(
7731 &lg,
7732 &cfg,
7733 all_ids,
7734 &mut self.sampler_scratch,
7735 self.pool.as_deref(),
7736 );
7737 all_ids.push(d);
7738 d
7739 } else {
7740 sampler::argmax(&lg)
7741 };
7742 attention::recycle_buf(&mut lg);
7743 drafts.push(dj);
7744 hx = hj;
7745 spec_stamp("d.pick");
7746 }
7747 all_ids.truncate(base_len);
7748 *drafted += k_spec;
7749 let t_draft = t_round.elapsed();
7750 let sub_draft = subs();
7751 let b = k_spec + 1;
7754 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7755 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7756 let e = self.embed_single(t);
7757 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7758 }
7759 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7760 spec_stamp("v.emb");
7761 let (lm_gw, lm_rows) = {
7762 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7763 (
7764 crate::gpu::GraphW {
7765 idx: i,
7766 kind,
7767 row_scale: rs,
7768 data: &[],
7769 prism: crate::gpu::GraphPrismOp::None,
7770 affine: false,
7771 },
7772 self.weights.lm_head.rows(),
7773 )
7774 };
7775 let mut logits = Vec::new();
7776 let final_norm = self.weights.final_norm.clone();
7777 #[cfg(target_os = "macos")]
7786 let greedy_dev = metal_native
7787 && !sampling
7788 && !greedy_pen
7789 && !self.confidence_on
7790 && self.final_softcap.is_none()
7791 && self.vocab_size == lm_rows
7797 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7798 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7799 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7800 #[cfg(not(target_os = "macos"))]
7801 let greedy_dev = false;
7802 let mut dev_ids: Vec<u32> = Vec::new();
7803 #[cfg(target_os = "macos")]
7804 let verify_outcome = if metal_native {
7805 let lm = self.weights.lm_head.q1_parts()?;
7806 let n_score = self.vocab_size.min(lm_rows);
7807 self.try_batch_graph_metal(
7808 &mut hiddens,
7809 &positions,
7810 b,
7811 Some((lm, &final_norm, &mut logits)),
7812 if greedy_dev {
7813 Some((n_score, &mut dev_ids))
7814 } else {
7815 None
7816 },
7817 )
7818 } else {
7819 self.try_batch_graph_wgpu(
7820 &mut hiddens,
7821 &positions,
7822 b,
7823 Some(crate::gpu::SpecTail {
7824 lm: lm_gw,
7825 lm_rows,
7826 final_norm: &final_norm,
7827 logits_out: &mut logits,
7828 }),
7829 )
7830 };
7831 #[cfg(not(target_os = "macos"))]
7832 let verify_outcome = self.try_batch_graph_wgpu(
7833 &mut hiddens,
7834 &positions,
7835 b,
7836 Some(crate::gpu::SpecTail {
7837 lm: lm_gw,
7838 lm_rows,
7839 final_norm: &final_norm,
7840 logits_out: &mut logits,
7841 }),
7842 );
7843 match verify_outcome {
7844 crate::gpu::BatchGraphOutcome::Completed => {}
7845 crate::gpu::BatchGraphOutcome::Declined => {
7846 m.kv.truncate_last(k_spec);
7850 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7851 self.clear_sequence_state();
7852 self.graph_failed
7853 .store(true, std::sync::atomic::Ordering::Relaxed);
7854 self.cancel
7855 .store(true, std::sync::atomic::Ordering::Relaxed);
7856 tracing::error!("MTP graph mirror rewind failed after verify decline");
7857 }
7858 return None;
7859 }
7860 crate::gpu::BatchGraphOutcome::Failed => {
7861 self.clear_sequence_state();
7865 self.graph_failed
7866 .store(true, std::sync::atomic::Ordering::Relaxed);
7867 self.cancel
7868 .store(true, std::sync::atomic::Ordering::Relaxed);
7869 tracing::error!("MTP verify batch graph failed after admission");
7870 return None;
7871 }
7872 }
7873 #[cfg(target_os = "macos")]
7879 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7880 let snap: Vec<Vec<f32>> = self
7881 .kv_cache
7882 .layers
7883 .iter()
7884 .map(|l| l.linear_state.clone())
7885 .collect();
7886 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7887 let toks: Vec<u32> = std::iter::once(t_next)
7888 .chain(drafts.iter().copied())
7889 .collect();
7890 let want_save = self.graph_want_logits;
7891 self.graph_want_logits = false;
7892 for (i, &t) in toks.iter().enumerate() {
7893 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7894 let _ = self.graph_logits.take();
7895 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7899 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7900 }
7901 let ref_lg = self.logits_from_hidden(&hi);
7902 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7903 let ra = sampler::argmax(&ref_lg);
7904 let va = sampler::argmax(row);
7905 let mut md = 0f32;
7906 let mut rms = 0f64;
7907 for j in 0..lm_rows.min(ref_lg.len()) {
7908 let d = (ref_lg[j] - row[j]).abs();
7909 md = md.max(d);
7910 rms += (d as f64) * (d as f64);
7911 }
7912 let mut hd = 0f32;
7913 for j in 0..self.hidden_size {
7914 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7915 }
7916 eprintln!(
7917 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7918 next_pos + i,
7919 if ra == va { "OK" } else { "MISMATCH" },
7920 (rms / lm_rows as f64).sqrt()
7921 );
7922 }
7923 self.graph_want_logits = want_save;
7924 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7927 if l.linear_state.len() == st.len() {
7928 l.linear_state.copy_from_slice(&st);
7929 } else {
7930 l.linear_state = st;
7931 }
7932 }
7933 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7934 let extra = l.seq_len.saturating_sub(n0);
7935 if extra > 0 {
7936 l.truncate_last(extra);
7937 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7938 }
7939 }
7940 }
7941 let t_verify = t_round.elapsed();
7942 let sub_verify = subs();
7943 let mut a = 0usize;
7948 let mut forced: Option<u32> = None;
7949 let ids: Vec<u32> = if sparse {
7950 let mut p = std::mem::take(&mut self.spec_ps);
7951 let mut res = std::mem::take(&mut self.spec_ress);
7952 while a < k_spec {
7953 let ok = sampler::sparse_distribution_into(
7954 &logits[a * lm_rows..(a + 1) * lm_rows],
7955 &cfg,
7956 all_ids,
7957 &mut self.sampler_scratch,
7958 self.pool.as_deref(),
7959 &mut p,
7960 );
7961 if !ok {
7962 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7963 p.clear();
7964 p.push((t, 1.0));
7965 }
7966 match sampler::spec_accept_or_correct_sparse(
7967 &p,
7968 &self.spec_qs[a],
7969 drafts[a],
7970 &mut self.rng,
7971 &mut res,
7972 ) {
7973 None => {
7974 all_ids.push(drafts[a]);
7975 a += 1;
7976 }
7977 Some(c) => {
7978 forced = Some(c);
7979 break;
7980 }
7981 }
7982 }
7983 all_ids.truncate(base_len);
7984 self.spec_ps = p;
7985 self.spec_ress = res;
7986 drafts.clone()
7987 } else if sampling {
7988 let mut p = std::mem::take(&mut self.spec_p);
7989 let mut res = std::mem::take(&mut self.spec_res);
7990 while a < k_spec {
7991 sampler::distribution_into(
7992 &logits[a * lm_rows..(a + 1) * lm_rows],
7993 &cfg,
7994 all_ids,
7995 &mut self.sampler_scratch,
7996 self.pool.as_deref(),
7997 &mut p,
7998 );
7999 match sampler::spec_accept_or_correct(
8000 &p,
8001 &self.spec_q[a],
8002 drafts[a],
8003 &mut self.rng,
8004 &mut res,
8005 self.pool.as_deref(),
8006 ) {
8007 None => {
8008 all_ids.push(drafts[a]);
8009 a += 1;
8010 }
8011 Some(c) => {
8012 forced = Some(c);
8013 break;
8014 }
8015 }
8016 }
8017 all_ids.truncate(base_len);
8018 self.spec_p = p;
8019 self.spec_res = res;
8020 drafts.clone()
8022 } else if greedy_pen {
8023 let mut ids: Vec<u32> = Vec::with_capacity(b);
8027 for i in 0..b {
8028 let t = sampler::argmax_penalized(
8029 &logits[i * lm_rows..(i + 1) * lm_rows],
8030 &cfg,
8031 all_ids,
8032 &mut self.sampler_scratch,
8033 self.pool.as_deref(),
8034 );
8035 ids.push(t);
8036 if i < k_spec && t == drafts[i] {
8037 all_ids.push(t);
8038 } else {
8039 break;
8040 }
8041 }
8042 all_ids.truncate(base_len);
8043 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8044 a += 1;
8045 }
8046 ids
8049 } else if greedy_dev && dev_ids.len() == b {
8050 let ids = std::mem::take(&mut dev_ids);
8051 while a < k_spec && ids[a] == drafts[a] {
8052 a += 1;
8053 }
8054 ids
8055 } else {
8056 if logits.len() < b * lm_rows {
8057 self.clear_sequence_state();
8060 self.graph_failed
8061 .store(true, std::sync::atomic::Ordering::Relaxed);
8062 self.cancel
8063 .store(true, std::sync::atomic::Ordering::Relaxed);
8064 tracing::error!("Metal verify returned neither logits nor argmax ids");
8065 return None;
8066 }
8067 let ids: Vec<u32> = (0..b)
8068 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8069 .collect();
8070 while a < k_spec && ids[a] == drafts[a] {
8071 a += 1;
8072 }
8073 ids
8074 };
8075 spec_stamp("acc");
8076 if spec_dbg {
8077 eprintln!(
8078 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8079 drafts, ids
8080 );
8081 }
8082 #[cfg(target_os = "macos")]
8086 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8087 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8088 {
8089 let snap: Vec<Vec<f32>> = self
8090 .kv_cache
8091 .layers
8092 .iter()
8093 .map(|l| l.linear_state.clone())
8094 .collect();
8095 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8096 let toks: Vec<u32> = std::iter::once(t_next)
8097 .chain(drafts.iter().copied())
8098 .collect();
8099 let want_save = self.graph_want_logits;
8100 self.graph_want_logits = false;
8101 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8102 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8103 let _ = self.graph_logits.take();
8104 }
8105 self.graph_want_logits = want_save;
8106 let plain_states: Vec<Vec<f32>> = self
8107 .kv_cache
8108 .layers
8109 .iter()
8110 .map(|l| l.linear_state.clone())
8111 .collect();
8112 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8113 let mut rows = Vec::new();
8114 for (li, (l, n0)) in self
8115 .kv_cache
8116 .layers
8117 .iter_mut()
8118 .zip(attn_lens.iter())
8119 .enumerate()
8120 {
8121 let extra = l.seq_len.saturating_sub(*n0);
8122 if extra > 0 {
8123 let mut kk = Vec::new();
8124 let mut vv = Vec::new();
8125 for g in 0..nkv {
8126 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8127 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8128 }
8129 rows.push((li, kk, vv));
8130 l.truncate_last(extra);
8131 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8132 }
8133 }
8134 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8135 if l.linear_state.len() == st.len() {
8136 l.linear_state.copy_from_slice(&st);
8137 } else {
8138 l.linear_state = st;
8139 }
8140 }
8141 Some((plain_states, rows))
8142 } else {
8143 None
8144 };
8145 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8146 #[cfg(target_os = "macos")]
8155 let mut warm_pending: Option<MetalWarmPending> = None;
8156 #[cfg(target_os = "macos")]
8157 if metal_native {
8158 m.kv.truncate_last(k_spec.saturating_sub(1));
8159 if self.mtp_graph_mode == Some(true) {
8160 crate::gpu_metal::kv_mirror_set_stored(
8163 self.mtp_kv_id(),
8164 Self::MTP_LAYER_BASE,
8165 m.kv.seq_len,
8166 );
8167 if !warm_off && a > 0 {
8168 let pairs: Vec<(&[f32], u32)> = (0..a)
8169 .map(|j| {
8170 (
8171 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8172 ids[j],
8173 )
8174 })
8175 .collect();
8176 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8177 }
8178 }
8179 spec_stamp("c.wsub");
8180 }
8181 #[cfg(target_os = "macos")]
8183 if metal_native {
8184 if !self.metal_verify_commit(a) {
8187 self.clear_sequence_state();
8188 self.graph_failed
8189 .store(true, std::sync::atomic::Ordering::Relaxed);
8190 self.cancel
8191 .store(true, std::sync::atomic::Ordering::Relaxed);
8192 tracing::error!("Metal verify state/KV handoff failed after admission");
8193 return None;
8194 }
8195 if let Some((plain_states, rows)) = commit_ref {
8196 crate::gpu_metal::queue_fence();
8197 let _ = crate::gpu_metal::wait_replay();
8200 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8201 let mut worst_s = 0f32;
8202 let mut worst_li = 0usize;
8203 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8204 if l.linear_state.len() != ps.len() || ps.is_empty() {
8205 continue;
8206 }
8207 let d = l
8208 .linear_state
8209 .iter()
8210 .zip(ps)
8211 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8212 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8213 let rel = d / n.max(1e-6);
8214 if rel > worst_s {
8215 worst_s = rel;
8216 worst_li = li;
8217 }
8218 }
8219 let mut worst_k = 0f32;
8220 for (li, kk, vv) in &rows {
8221 let l = &self.kv_cache.layers[*li];
8222 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8223 let mut ck = Vec::new();
8224 let mut cv = Vec::new();
8225 for g in 0..nkv {
8226 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8227 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8228 }
8229 if ck.len() == kk.len() {
8230 let dk = ck
8231 .iter()
8232 .zip(kk)
8233 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8234 let dv = cv
8235 .iter()
8236 .zip(vv)
8237 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8238 worst_k = worst_k.max(dk).max(dv);
8239 } else {
8240 eprintln!(
8241 "commit-check L{li}: kv row count mismatch {} vs {}",
8242 ck.len(),
8243 kk.len()
8244 );
8245 }
8246 }
8247 eprintln!(
8248 "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}"
8249 );
8250 }
8251 }
8252 if !metal_native && a + 1 < b {
8253 let expected_gdn_layers = self.graph_gdn_layer_count();
8254 if expected_gdn_layers > 0
8255 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8256 {
8257 self.clear_sequence_state();
8258 self.graph_failed
8259 .store(true, std::sync::atomic::Ordering::Relaxed);
8260 self.cancel
8261 .store(true, std::sync::atomic::Ordering::Relaxed);
8262 tracing::error!("GDN speculative restore failed after verify");
8263 return None;
8264 }
8265 }
8266 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8267 self.clear_sequence_state();
8272 self.graph_failed
8273 .store(true, std::sync::atomic::Ordering::Relaxed);
8274 self.cancel
8275 .store(true, std::sync::atomic::Ordering::Relaxed);
8276 tracing::error!("trunk graph KV rewind failed after speculative verify");
8277 return None;
8278 }
8279 *accepted += a;
8280 if !metal_native {
8291 m.kv.truncate_last(k_spec.saturating_sub(1));
8293 }
8294 spec_stamp("c.trunc");
8295 if !metal_native
8296 && self.mtp_graph_mode == Some(true)
8297 && !self.rewind_mtp_graph_mirror(next_pos)
8298 {
8299 self.clear_sequence_state();
8303 self.graph_failed
8304 .store(true, std::sync::atomic::Ordering::Relaxed);
8305 self.cancel
8306 .store(true, std::sync::atomic::Ordering::Relaxed);
8307 tracing::error!("MTP graph mirror rewind failed after verify commit");
8308 return None;
8309 }
8310 if !warm_off && a > 0 {
8311 let mut warmed = false;
8314 #[cfg(target_os = "macos")]
8315 if metal_native && self.mtp_graph_mode == Some(true) {
8316 warmed = match warm_pending.take() {
8320 Some(p) => self.mtp_warm_batch_finish(m, p),
8321 None => false,
8322 };
8323 if !warmed {
8324 warmed = true;
8325 for j in 0..a {
8326 let row =
8327 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8328 if self
8329 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8330 .is_none()
8331 {
8332 warmed = false;
8333 break;
8334 }
8335 }
8336 }
8337 }
8338 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8339 let rows: Vec<Vec<f32>> = (0..a)
8340 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8341 .collect();
8342 let pairs: Vec<(&[f32], u32)> = rows
8343 .iter()
8344 .zip(ids.iter())
8345 .map(|(r, &t)| (r.as_slice(), t))
8346 .collect();
8347 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8348 Ok(()) => warmed = true,
8349 Err(err) => {
8350 tracing::error!("{err}");
8356 self.clear_sequence_state();
8357 self.graph_failed
8358 .store(true, std::sync::atomic::Ordering::Relaxed);
8359 self.cancel
8360 .store(true, std::sync::atomic::Ordering::Relaxed);
8361 return None;
8362 }
8363 }
8364 }
8365 if !warmed {
8366 for j in 0..a {
8367 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8368 let row = row.to_vec();
8369 self.mtp_warm(m, &row, ids[j], next_pos + j);
8370 }
8371 }
8372 }
8373 spec_stamp("c.warm");
8377 if let Some(c) = forced {
8378 self.spec_forced = Some(c);
8379 self.graph_logits = None;
8380 } else if greedy_dev && logits.is_empty() {
8381 self.spec_forced = Some(ids[a]);
8384 self.graph_logits = None;
8385 } else {
8386 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8387 row.resize(self.vocab_size, 0.0);
8388 if let Some(c) = self.final_softcap {
8389 for l in row.iter_mut() {
8390 *l = c * (*l / c).tanh();
8391 }
8392 }
8393 self.graph_logits = Some(row);
8394 }
8395 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8396 spec_stamp("c.row");
8397 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8403 let end = subs();
8404 eprintln!(
8405 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8406 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8407 t_draft.as_secs_f64() * 1e3,
8408 sub_draft - sub0,
8409 (t_verify - t_draft).as_secs_f64() * 1e3,
8410 sub_verify - sub_draft,
8411 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8412 end - sub_verify,
8413 self.draft_full_streak,
8414 );
8415 }
8416 if k_env.is_none() && !metal_native && !k_capped {
8421 let f = a as f32 / k_spec.max(1) as f32;
8425 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8426 let mut k_next = k_spec;
8427 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8428 k_next = k_spec + 1;
8429 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8430 k_next = k_spec - 1;
8431 }
8432 if k_next != k_spec {
8433 self.spec_acc_ewma = 0.6;
8434 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8435 eprintln!("spec-k: {k_spec} → {k_next}");
8436 }
8437 }
8438 self.spec_k_adapt = Some(k_next);
8439 }
8440 spec_stamp("end");
8441 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8442 }
8443
8444 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8453 if !self.pair_supported() {
8454 return (0.0, 0.0);
8455 }
8456 if self.gdn_cfg.is_some()
8464 && self.graph_prefill_preferred()
8465 && self.automatic_gpu_prefix().is_some()
8466 {
8467 tracing::info!(
8468 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8469 );
8470 return (0.0, 0.0);
8471 }
8472 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8479 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8480 let emb1 = self.embed_single(1);
8481 let emb2 = self.embed_single(2);
8482 let pos = self.kv_cache.seq_len();
8483
8484 let t0 = std::time::Instant::now();
8485 for _ in 0..iters {
8486 let _ = self.forward_layers(&emb1, pos, None);
8487 let _ = self.forward_layers(&emb2, pos + 1, None);
8488 for l in &mut self.kv_cache.layers {
8489 l.truncate_last(2);
8490 }
8491 }
8492 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8493
8494 let t1 = std::time::Instant::now();
8495 for _ in 0..iters {
8496 let _ = self.forward_pair(&emb1, &emb2, pos);
8497 for l in &mut self.kv_cache.layers {
8498 l.truncate_last(2);
8499 }
8500 }
8501 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8502 match graph_env {
8503 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8504 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8505 }
8506 (singles_ms, pair_ms)
8507 }
8508
8509 fn pair_supported(&self) -> bool {
8517 !self.weights.layers.is_empty()
8524 && !self.per_token_stack()
8525 && !self
8526 .weights
8527 .layers
8528 .iter()
8529 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8530 }
8531
8532 fn forward_pair(
8533 &mut self,
8534 emb1: &[f32],
8535 emb2: &[f32],
8536 position: usize,
8537 ) -> (Vec<f32>, Vec<f32>) {
8538 self.mimo_moe_prepare();
8541 let mut h1 = emb1.to_vec();
8542 let mut h2 = emb2.to_vec();
8543 let (_nkv, _hd, hs, _rd, eps) = (
8544 self.num_kv_heads,
8545 self.head_dim,
8546 self.hidden_size,
8547 self.rotary_dim,
8548 self.rms_eps,
8549 );
8550 let pool = self.pool.clone();
8551 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8558
8559 for li in 0..self.num_layers {
8560 let _capacity_tail = automatic_gpu_prefix
8561 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8562 .map(|_| crate::gpu::enter_cpu_scope());
8563 crate::gpu::set_layer(li as i64);
8566 let lw = &self.weights.layers[self.phys_layer(li)];
8567 inference::rms_norm_into(
8570 &h1,
8571 &lw.input_norm,
8572 self.rms_eps,
8573 self.norm_style,
8574 &mut self.ws.n1,
8575 );
8576 inference::rms_norm_into(
8577 &h2,
8578 &lw.input_norm,
8579 self.rms_eps,
8580 self.norm_style,
8581 &mut self.ws.n2,
8582 );
8583
8584 let (a1, a2) = match &lw.attn {
8585 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8586 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8587 AttnKind::Bounded(w) => {
8588 let rope = self
8591 .bounded_rope
8592 .clone()
8593 .expect("bounded layer without an installed rotation table");
8594 let cfg = crate::bounded::BoundedAttnCfg {
8595 num_heads: self.num_heads,
8596 num_kv_heads: self.num_kv_heads,
8597 head_dim: self.head_dim,
8598 hidden_size: hs,
8599 scale: self.attn_scale,
8600 rope: &rope,
8601 pool: pool.as_deref(),
8602 };
8603 let a1 = crate::bounded::bounded_attention(
8604 &self.ws.n1,
8605 w,
8606 &mut self.kv_cache.layers[li],
8607 &cfg,
8608 );
8609 let a2 = crate::bounded::bounded_attention(
8610 &self.ws.n2,
8611 w,
8612 &mut self.kv_cache.layers[li],
8613 &cfg,
8614 );
8615 (a1, a2)
8616 }
8617 AttnKind::Linear(w) => {
8618 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8619 let layer = &mut self.kv_cache.layers[li];
8620 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8621 vmf_phase_pair(
8622 &self.ws.n1,
8623 &self.ws.n2,
8624 w,
8625 &cfg,
8626 state,
8627 scratch,
8628 self.pool.as_deref(),
8629 )
8630 }
8631 AttnKind::LinearGdn(w) => {
8632 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8633 let layer = &mut self.kv_cache.layers[li];
8634 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8635 gdn_pair(
8636 &self.ws.n1,
8637 &self.ws.n2,
8638 w,
8639 &cfg,
8640 state,
8641 scratch,
8642 self.pool.as_deref(),
8643 )
8644 }
8645 AttnKind::ShortConv(w) => {
8646 let cfg = self
8647 .short_conv_cfg
8648 .expect("short-conv layer without short_conv_cfg");
8649 let layer = &mut self.kv_cache.layers[li];
8650 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8651 short_conv_pair(
8652 &self.ws.n1,
8653 &self.ws.n2,
8654 w,
8655 &cfg,
8656 state,
8657 scratch,
8658 self.pool.as_deref(),
8659 )
8660 }
8661 AttnKind::Full {
8662 wq,
8663 wk,
8664 wv,
8665 wo,
8666 q_norm,
8667 k_norm,
8668 output_gate,
8669 softplus_gate,
8670 bias,
8671 } => {
8672 let inv_freq_l = self.layer_inv_freq(li);
8673 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8674 let cfg = QwenAttnCfg {
8675 num_heads: self.layer_num_heads(li),
8676 num_kv_heads: nkv_l,
8677 head_dim: hd_l,
8678 hidden_size: hs,
8679 position,
8680 inv_freq: &inv_freq_l,
8681 rotary_dim: rd_l,
8682 scale: self.attn_scale,
8683 softcap: self.attn_softcap,
8684 window: self.layer_window(li),
8685 v_norm: self.attn_v_norm,
8686 qk_norm_after_rope: self.qk_norm_after_rope,
8687 gate_sigmoid: self.proj_gate_sigmoid,
8688 q_norm: q_norm.as_deref(),
8689 k_norm: k_norm.as_deref(),
8690 output_gate: *output_gate,
8691 softplus_gate: softplus_gate
8692 .as_ref()
8693 .map(|(gate, per_head)| (gate, *per_head)),
8694 rope_scale: self.layer_rope_scale(li),
8695 bias: bias
8696 .as_ref()
8697 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8698 rms_eps: eps,
8699 norm_style: self.norm_style,
8700 pool: pool.as_deref(),
8701 v_head_dim: self.layer_v_dim(li),
8702 };
8703 attention::qwen_attention_pair(
8704 &self.ws.n1,
8705 &self.ws.n2,
8706 wq,
8707 wk,
8708 wv,
8709 wo,
8710 &mut self.kv_cache.layers[li],
8711 &cfg,
8712 )
8713 }
8714 };
8715 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8716 Some(w) => (
8717 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8718 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8719 ),
8720 None => (a1, a2),
8721 };
8722 for i in 0..self.hidden_size {
8723 h1[i] += a1[i];
8724 h2[i] += a2[i];
8725 }
8726 let (mut a1, mut a2) = (a1, a2);
8727 attention::recycle_buf(&mut a1);
8728 attention::recycle_buf(&mut a2);
8729
8730 let lw = &self.weights.layers[self.phys_layer(li)];
8731 inference::rms_norm_into(
8732 &h1,
8733 &lw.post_norm,
8734 self.rms_eps,
8735 self.norm_style,
8736 &mut self.ws.p1,
8737 );
8738 inference::rms_norm_into(
8739 &h2,
8740 &lw.post_norm,
8741 self.rms_eps,
8742 self.norm_style,
8743 &mut self.ws.p2,
8744 );
8745 let (f1, f2) = match &lw.ffn {
8746 FfnKind::DenseMoe(dm) => (
8749 dense_moe_ffn(
8750 dm,
8751 &self.ws.p1,
8752 &h1,
8753 self.rms_eps,
8754 self.norm_style,
8755 self.pool.as_deref(),
8756 ),
8757 dense_moe_ffn(
8758 dm,
8759 &self.ws.p2,
8760 &h2,
8761 self.rms_eps,
8762 self.norm_style,
8763 self.pool.as_deref(),
8764 ),
8765 ),
8766 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8767 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8768 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8769 ),
8770 _ => ffn_forward_pair(
8771 &lw.ffn,
8772 &self.ws.p1,
8773 &self.ws.p2,
8774 self.pool.as_deref(),
8775 None,
8776 ),
8777 };
8778 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8779 Some(w) => (
8780 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8781 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8782 ),
8783 None => (f1, f2),
8784 };
8785 for i in 0..self.hidden_size {
8786 h1[i] += f1[i];
8787 h2[i] += f2[i];
8788 }
8789 let (mut f1, mut f2) = (f1, f2);
8790 attention::recycle_buf(&mut f1);
8791 attention::recycle_buf(&mut f2);
8792 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8793 for i in 0..self.hidden_size {
8794 h1[i] *= sc;
8795 h2[i] *= sc;
8796 }
8797 }
8798 if self.is_loop_end(li) && li + 1 < self.num_layers {
8800 h1 = inference::rms_norm(
8801 &h1,
8802 &self.weights.final_norm,
8803 self.rms_eps,
8804 self.norm_style,
8805 );
8806 h2 = inference::rms_norm(
8807 &h2,
8808 &self.weights.final_norm,
8809 self.rms_eps,
8810 self.norm_style,
8811 );
8812 }
8813 }
8814 crate::gpu::set_layer(-1); if self.o1_active() {
8821 self.commit_linear_scratch();
8822 }
8823 self.o1_progress();
8824 self.swa_trim_tails();
8825 (h1, h2)
8826 }
8827
8828 fn commit_linear_scratch(&mut self) {
8830 for layer in &mut self.kv_cache.layers {
8831 if !layer.linear_scratch.is_empty() {
8832 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8833 layer.linear_scratch.clear();
8834 }
8835 }
8836 }
8837
8838 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8846 #[cfg(target_os = "macos")]
8847 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8848 if ids.is_empty() {
8849 return Err("empty id sequence".to_string());
8850 }
8851
8852 self.nll_begin()?;
8853 let result = (|| {
8854 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8855 self.nll_check_graph("encoder prefill", 0)?;
8856 let expected = ids.len() * self.hidden_size;
8857 if hidden.len() != expected {
8858 return Err(format!(
8859 "encoder prefill returned {} values for {} rows of hidden size {}",
8860 hidden.len(),
8861 ids.len(),
8862 self.hidden_size
8863 ));
8864 }
8865
8866 let mut out = vec![0.0f32; expected];
8867 for (src, dst) in hidden
8868 .chunks_exact(self.hidden_size)
8869 .zip(out.chunks_exact_mut(self.hidden_size))
8870 {
8871 inference::rms_norm_into(
8872 src,
8873 &self.weights.final_norm,
8874 self.rms_eps,
8875 self.norm_style,
8876 dst,
8877 );
8878 }
8879 Ok(out)
8880 })();
8881 self.nll_end();
8882 result
8883 }
8884
8885 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8890 #[cfg(target_os = "macos")]
8891 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8892 if ids.is_empty() {
8893 return Err("empty id sequence".to_string());
8894 }
8895 self.nll_begin()?;
8896 let result = (|| {
8897 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8898 self.nll_check_graph("hidden prefill", 0)?;
8899 if hidden.len() != ids.len() * self.hidden_size {
8900 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
8901 }
8902 Ok(hidden)
8903 })();
8904 self.nll_end();
8905 result
8906 }
8907
8908 pub fn forward_ids(
8911 &mut self,
8912 ids: &[u32],
8913 task_mask: Option<&TaskMask>,
8914 ) -> Result<Vec<f32>, String> {
8915 #[cfg(target_os = "macos")]
8916 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8917 if ids.is_empty() {
8918 return Err("empty id sequence".to_string());
8919 }
8920 self.clear_sequence_state();
8921 self.check_forward_graph("forward_ids setup", 0)?;
8922 if task_mask.is_none() {
8923 self.o1_begin();
8924 }
8925 let mut hidden = vec![0.0f32; self.hidden_size];
8926 let mut pos = 0usize;
8927 if let Some(b) = &mut self.dsv41 {
8928 let pool = self.pool.clone();
8929 let mut logits = Vec::new();
8930 crate::dsv41::forward_chunk(
8931 &b.0,
8932 &b.1,
8933 &b.2,
8934 &mut b.3,
8935 ids,
8936 0,
8937 pool.as_deref(),
8938 &mut logits,
8939 );
8940 if let Err(err) = self.o1_seal_checked() {
8941 self.clear_sequence_state();
8942 return Err(err);
8943 }
8944 return Ok(logits);
8945 }
8946 #[cfg(not(target_os = "macos"))]
8957 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8958 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8959 Ok(r) => r,
8960 Err(e) => {
8961 self.clear_sequence_state();
8962 return Err(e);
8963 }
8964 };
8965 if n > 0 {
8966 hidden.copy_from_slice(&last);
8967 pos = n;
8968 }
8969 }
8970 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8971 let chunk = self.prefill_chunk();
8975 let hs = self.hidden_size;
8976 while pos < ids.len() {
8977 let end = (pos + chunk).min(ids.len());
8978 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8979 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8980 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8981 pos = end;
8982 }
8983 }
8984 if task_mask.is_none()
8993 && !self.graph_prefill_preferred()
8994 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8995 && self.pair_supported()
8996 {
8997 while pos + 1 < ids.len() {
8998 let e1 = self.embed_single(ids[pos]);
8999 let e2 = self.embed_single(ids[pos + 1]);
9000 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9001 self.check_forward_graph("forward_ids pair", pos + 1)?;
9002 self.commit_linear_scratch();
9003 hidden = h2;
9004 pos += 2;
9005 }
9006 }
9007 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9010 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9011 self.graph_logits = Some(lg);
9012 hidden = vec![0.0; self.hidden_size];
9013 pos = ids.len();
9014 }
9015 }
9016 while pos < ids.len() {
9017 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9018 self.check_forward_graph("forward_ids", pos)?;
9019 pos += 1;
9020 }
9021 if let Some(logits) = self.graph_logits.take() {
9022 if let Err(err) = self.o1_seal_checked() {
9026 self.clear_sequence_state();
9027 return Err(err);
9028 }
9029 return Ok(logits);
9030 }
9031 if let Err(err) = self.o1_seal_checked() {
9035 self.clear_sequence_state();
9036 return Err(err);
9037 }
9038 let normed = inference::rms_norm(
9039 &hidden,
9040 &self.weights.final_norm,
9041 self.rms_eps,
9042 self.norm_style,
9043 );
9044 Ok(self.lm_head_forward(&normed))
9045 }
9046
9047 #[doc(hidden)]
9051 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9052 #[cfg(target_os = "macos")]
9053 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9054 if ids.is_empty() {
9055 return Err("empty id sequence".to_string());
9056 }
9057 self.clear_sequence_state();
9058 self.dsv41
9059 .as_ref()
9060 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9061 self.o1_begin();
9062 let rows = {
9063 let pool = self.pool.clone();
9064 let b = self
9065 .dsv41
9066 .as_mut()
9067 .expect("dsv41 checked above; state cannot change during forward");
9068 let mut rows = Vec::with_capacity(ids.len());
9069 for (position, &id) in ids.iter().enumerate() {
9070 let mut logits = Vec::new();
9071 crate::dsv41::forward_token(
9072 &b.0,
9073 &b.1,
9074 &b.2,
9075 &mut b.3,
9076 id,
9077 position,
9078 pool.as_deref(),
9079 &mut logits,
9080 );
9081 rows.push(logits);
9082 }
9083 rows
9084 };
9085 self.o1_seal();
9086 Ok(rows)
9087 }
9088
9089 pub fn dsv41_moe_capture(
9093 &mut self,
9094 ids: &[u32],
9095 lo: usize,
9096 hi: usize,
9097 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9098 #[cfg(target_os = "macos")]
9099 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9100 if ids.is_empty() {
9101 return Err("empty id sequence".to_string());
9102 }
9103 self.dsv41
9104 .as_ref()
9105 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9106 self.clear_sequence_state();
9107 let res = {
9108 let pool = self.pool.clone();
9109 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9110 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9111 };
9112 self.clear_sequence_state();
9113 res
9114 }
9115
9116 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9123 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9124 Ok((nll / cnt.max(1) as f64).exp())
9125 }
9126
9127 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9134 if !self.can_prefill_batched() {
9135 return self.nll_ids_from(ids, 0).map(|_| ());
9136 }
9137 self.nll_begin()?;
9138 let result: Result<(), String> = (|| {
9139 let chunk = chunk.max(1);
9140 let mut pos = 0usize;
9141 while pos < ids.len() {
9142 let end = (pos + chunk).min(ids.len());
9143 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9144 self.nll_check_graph("calibration prefill", pos)?;
9145 pos = end;
9146 }
9147 Ok(())
9148 })();
9149 self.nll_end();
9150 result
9151 }
9152
9153 pub fn calib_window_span(
9162 &mut self,
9163 ids: &[u32],
9164 input: Option<&[f32]>,
9165 from: usize,
9166 mid: usize,
9167 upto: usize,
9168 ) -> Result<Vec<f32>, String> {
9169 if !self.can_prefill_batched() {
9170 return Err("calib_window_span: this architecture has no batched prefill".into());
9171 }
9172 self.nll_begin()?;
9173 let result: Result<Vec<f32>, String> = (|| {
9174 let h = if mid > from {
9175 let inp = match input {
9176 Some(h) => PrefillIn::Hidden(h),
9177 None => PrefillIn::Ids(ids),
9178 };
9179 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9180 self.nll_check_graph("calibration span", 0)?;
9181 h
9182 } else {
9183 input.map(<[f32]>::to_vec).unwrap_or_default()
9184 };
9185 if upto > mid {
9186 let inp = if mid > from || input.is_some() {
9187 PrefillIn::Hidden(&h)
9188 } else {
9189 PrefillIn::Ids(ids)
9190 };
9191 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9192 self.nll_check_graph("calibration span", 0)?;
9193 }
9194 Ok(h)
9195 })();
9196 self.nll_end();
9197 result
9198 }
9199
9200 #[cfg(feature = "gpu")]
9204 pub fn qwen4_capture(
9205 &mut self,
9206 ids: &[u32],
9207 lo: usize,
9208 hi: usize,
9209 ) -> Result<Vec<Vec<f32>>, String> {
9210 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9211 crate::qwen4_exp::capture_window(
9212 &b.0,
9213 &b.1,
9214 &b.2,
9215 &mut b.3,
9216 ids,
9217 lo,
9218 hi,
9219 &self.inv_freq,
9220 self.pool.as_deref(),
9221 )
9222 }
9223
9224 pub fn qwen4_expert_dirs(
9226 &self,
9227 li: usize,
9228 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9229 let b = self.qwen4_exp.as_ref()?;
9230 crate::qwen4_exp::expert_dirs(&b.1, li)
9231 }
9232
9233 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9248 self.reset_mtp_probe_state();
9249 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9253 let result = if ids.len() < 2 {
9254 Err("MTP FFN probe needs at least two token ids".into())
9255 } else if ids
9256 .iter()
9257 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9258 {
9259 Err("MTP FFN probe received a token id outside the embedding table".into())
9260 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9261 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9264 } else if self.mimo_mtp.is_some() {
9265 let mut st = self
9266 .mimo_mtp
9267 .take()
9268 .expect("checked MiMo MTP before detaching it");
9269 let result = crate::gpu::cpu_scope(|| {
9270 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids)
9271 });
9272 st.reset();
9275 self.mimo_mtp = Some(st);
9276 result
9277 } else if self.mtp.is_some() {
9278 let mut mtp = self
9279 .mtp
9280 .take()
9281 .expect("checked canonical MTP before detaching it");
9282 let result = crate::gpu::cpu_scope(|| self.mtp_ffn_probe_teacher_forced(&mut mtp, ids));
9283 mtp.kv.clear();
9284 self.mtp = Some(mtp);
9285 result
9286 } else {
9287 Err("MTP FFN probe: this pipeline has no draft stack".into())
9288 };
9289 self.reset_mtp_probe_state();
9292 result
9293 }
9294
9295 fn reset_mtp_probe_state(&mut self) {
9297 self.clear_sequence_state();
9298 if let Some(mtp) = self.mtp.as_mut() {
9299 mtp.kv.clear();
9300 }
9301 if let Some(mimo) = self.mimo_mtp.as_mut() {
9302 mimo.reset();
9303 }
9304 self.mtp_graph_mode = None;
9305 }
9306
9307 fn mtp_ffn_probe_teacher_forced(
9313 &mut self,
9314 mtp: &mut MtpModule,
9315 ids: &[u32],
9316 ) -> Result<Vec<MtpFfnProbe>, String> {
9317 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9318 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9319 };
9320 let mut mass = vec![0.0f64; dense.gate_proj.rows()];
9321 let mut squared_mass = vec![0.0f64; dense.gate_proj.rows()];
9322 for position in 0..ids.len() - 1 {
9323 let hidden = self.forward_layers(&self.embed_single(ids[position]), position, None);
9324 if hidden.len() != self.hidden_size {
9325 return Err(format!(
9326 "MTP FFN probe: trunk hidden width {} != {}",
9327 hidden.len(),
9328 self.hidden_size
9329 ));
9330 }
9331 self.mtp_probe_step_ffn(
9332 mtp,
9333 &hidden,
9334 ids[position + 1],
9335 position,
9336 &mut mass,
9337 &mut squared_mass,
9338 )?;
9339 }
9340 Ok(vec![MtpFfnProbe {
9341 mass,
9342 squared_mass,
9343 observed_positions: ids.len() - 1,
9344 }])
9345 }
9346
9347 fn mtp_probe_step_ffn(
9352 &mut self,
9353 mtp: &mut MtpModule,
9354 hidden: &[f32],
9355 next_token: u32,
9356 position: usize,
9357 mass: &mut [f64],
9358 squared_mass: &mut [f64],
9359 ) -> Result<(), String> {
9360 let e = self.embed_single(next_token);
9361 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9362 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9363 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9364 inference::rms_norm_into(
9365 hidden,
9366 &mtp.hnorm,
9367 self.rms_eps,
9368 self.norm_style,
9369 cat_h,
9370 );
9371 let mut x = vec![0.0f32; self.hidden_size];
9372 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9373
9374 let lw = &mtp.layer;
9375 inference::rms_norm_into(
9376 &x,
9377 &lw.input_norm,
9378 self.rms_eps,
9379 self.norm_style,
9380 &mut self.ws.n1,
9381 );
9382 let attn = match &lw.attn {
9383 AttnKind::Full {
9384 wq,
9385 wk,
9386 wv,
9387 wo,
9388 q_norm,
9389 k_norm,
9390 output_gate,
9391 softplus_gate,
9392 bias,
9393 } => {
9394 let mut cfg = self.attn_cfg(position);
9395 cfg.q_norm = q_norm.as_deref();
9396 cfg.k_norm = k_norm.as_deref();
9397 cfg.output_gate = *output_gate;
9398 cfg.softplus_gate = softplus_gate
9399 .as_ref()
9400 .map(|(gate, per_head)| (gate, *per_head));
9401 cfg.bias = bias
9402 .as_ref()
9403 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9404 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9405 }
9406 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9407 };
9408 for (dst, src) in x.iter_mut().zip(attn) {
9409 *dst += src;
9410 }
9411 inference::rms_norm_into(
9412 &x,
9413 &lw.post_norm,
9414 self.rms_eps,
9415 self.norm_style,
9416 &mut self.ws.p1,
9417 );
9418 let FfnKind::Dense(dense) = &lw.ffn else {
9419 return Err("MTP FFN probe: draft FFN is not dense".into());
9420 };
9421 accumulate_dense_ffn_activation_mass(
9422 dense,
9423 &self.ws.p1,
9424 1,
9425 self.pool.as_deref(),
9426 mass,
9427 squared_mass,
9428 )?;
9429 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9432 for (dst, src) in x.iter_mut().zip(ffn) {
9433 *dst += src;
9434 }
9435 Ok(())
9436 }
9437
9438 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9443 self.clear_sequence_state();
9444 FFN_PROBE.with(|p| {
9445 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9446 });
9447 crate::gpu::cpu_scope(|| {
9448 for (pos, &id) in ids.iter().enumerate() {
9449 let emb = self.embed_single(id);
9450 let _ = self.forward_layers(&emb, pos, None);
9451 }
9452 });
9453 self.clear_sequence_state();
9454 FFN_PROBE
9455 .with(|p| p.borrow_mut().take())
9456 .unwrap_or_default()
9457 }
9458
9459 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9463 if let Err(err) = self.nll_begin() {
9464 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9468 self.nll_end();
9469 return Err(err);
9470 }
9471 FFN_PROBE.with(|p| {
9472 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9473 });
9474 let result: Result<(), String> = (|| {
9475 for chunk in ids.chunks(256) {
9476 if chunk.len() < 2 {
9477 continue;
9478 }
9479 self.nll_ids_masked(chunk, 0, None)?;
9480 }
9481 Ok(())
9482 })();
9483 self.nll_end();
9484 let probe = FFN_PROBE
9485 .with(|p| p.borrow_mut().take())
9486 .unwrap_or_default();
9487 match result {
9488 Ok(()) => Ok(probe),
9489 Err(err) => {
9490 drop(probe);
9491 Err(err)
9492 }
9493 }
9494 }
9495
9496 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9500 self.nll_begin()?;
9501 let result: Result<f64, String> = (|| {
9502 let mut nll = 0f64;
9503 let mut cnt = 0usize;
9504 let mut hidden = vec![0f32; self.hidden_size];
9505 for (pos, &id) in ids.iter().enumerate() {
9506 if pos > 0 {
9507 inference::rms_norm_into(
9508 &hidden,
9509 &self.weights.final_norm,
9510 self.rms_eps,
9511 self.norm_style,
9512 &mut self.ws.n1,
9513 );
9514 let mut logits = self.lm_head_forward(&self.ws.n1);
9515 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9516 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9517 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9518 nll -= p.max(1e-300).ln();
9519 cnt += 1;
9520 attention::recycle_buf(&mut logits);
9521 }
9522 let emb = self.embed_single(id);
9523 hidden = self.forward_layers(&emb, pos, Some(mask));
9524 self.nll_check_graph("masked serial forward", pos)?;
9525 let _ = self.graph_logits.take();
9529 }
9530 Ok((nll / cnt.max(1) as f64).exp())
9531 })();
9532 self.nll_end();
9533 result
9534 }
9535
9536 pub fn nll_ids_masked(
9555 &mut self,
9556 ids: &[u32],
9557 start: usize,
9558 task_mask: Option<&TaskMask>,
9559 ) -> Result<(f64, usize), String> {
9560 let task_mask = self.drop_open_mask(task_mask);
9561 self.nll_ids_inner(ids, start, task_mask)
9562 }
9563
9564 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9565 self.nll_ids_inner(ids, start, None)
9566 }
9567
9568 fn nll_ids_inner(
9569 &mut self,
9570 ids: &[u32],
9571 start: usize,
9572 task_mask: Option<&TaskMask>,
9573 ) -> Result<(f64, usize), String> {
9574 self.nll_begin()?;
9575 let result: Result<(f64, usize), String> = (|| {
9576 let mut nll = 0f64;
9577 let mut cnt = 0usize;
9578 let (graph_quality, fused_head_quality) = nll_graph_policy(
9591 task_mask.is_none(),
9592 self.graph_prefill_preferred(),
9593 crate::gpu::q1_force(),
9594 );
9595 self.graph_head_required = fused_head_quality;
9596 self.graph_want_logits = fused_head_quality;
9597 #[cfg(target_os = "macos")]
9598 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9599 match self.nll_batch_metal(ids, start) {
9600 MetalBatchNllOutcome::Completed(nll, count) => {
9601 return Ok((nll, count));
9602 }
9603 MetalBatchNllOutcome::Declined => {}
9604 MetalBatchNllOutcome::Failed(err) => return Err(err),
9605 }
9606 }
9607 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9612 if self.can_prefill_batched() && !graph_quality && !force_serial {
9613 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9622 .ok()
9623 .and_then(|v| v.parse().ok())
9624 .filter(|&c: &usize| c > 0)
9625 .unwrap_or(128);
9626 const LM_SUB: usize = 32;
9627 let n = ids.len().saturating_sub(1);
9628 let hs = self.hidden_size;
9629 let rows = self.weights.lm_head.rows();
9630 let mut pos = 0usize;
9631 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9632 while pos < n {
9633 let end = (pos + chunk_len).min(n);
9634 let bsz = end - pos;
9635 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9636 self.nll_check_graph("batched prefill", pos)?;
9637 if state_trace && end % 256 == 0 {
9638 self.trace_recurrent_state(end);
9639 }
9640 let mut k0 = 0usize;
9641 while k0 < bsz {
9642 let k1 = (k0 + LM_SUB).min(bsz);
9643 let sb = k1 - k0;
9644 if pos + k1 <= start {
9647 k0 = k1;
9648 continue;
9649 }
9650 let mut normed = vec![0.0f32; sb * hs];
9651 for k in 0..sb {
9652 let r = inference::rms_norm(
9653 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9654 &self.weights.final_norm,
9655 self.rms_eps,
9656 self.norm_style,
9657 );
9658 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9659 }
9660 let mut logits = vec![0.0f32; sb * rows];
9661 self.weights
9662 .lm_head
9663 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9664 for k in 0..sb {
9665 if pos + k0 + k < start {
9666 continue;
9667 }
9668 self.nll_check_graph("batched score row", pos + k0 + k)?;
9669 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9670 if let Some(mu) = self.logit_multiplier {
9671 for v in lg.iter_mut() {
9672 *v *= mu;
9673 }
9674 }
9675 if let Some(c) = self.final_softcap {
9679 softcap_in_place(lg, c, self.pool.as_deref());
9680 }
9681 if let Some(cm) = self.head_clusters.clone() {
9684 self.hierarchical_head_logprobs(
9685 &normed[k * hs..(k + 1) * hs],
9686 &cm,
9687 lg,
9688 );
9689 }
9690 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9691 let target = ids[pos + k0 + k + 1] as usize;
9692 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9693 let lse: f64 = lg
9694 .iter()
9695 .map(|&v| ((v - max) as f64).exp())
9696 .sum::<f64>()
9697 .ln()
9698 + max as f64;
9699 nll += lse - lg[target] as f64;
9700 cnt += 1;
9701 if std::env::var("CMF_PPL_TRACE").is_ok() {
9702 let top = lg
9703 .iter()
9704 .enumerate()
9705 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9706 .map(|(i, _)| i)
9707 .unwrap_or(0);
9708 eprintln!(
9709 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9710 pos + k0 + k,
9711 target,
9712 lse - lg[target] as f64,
9713 top,
9714 lg[target],
9715 lg[top]
9716 );
9717 }
9718 }
9719 k0 = k1;
9720 }
9721 pos = end;
9722 }
9723 return Ok((nll, cnt));
9724 }
9725 for pos in 0..ids.len().saturating_sub(1) {
9726 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9727 self.nll_check_graph("serial forward", pos)?;
9728 let out_of_band = self.graph_logits.take();
9736 if self.graph_head_required && out_of_band.is_none() {
9737 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9738 return Err(format!(
9739 "fused Metal graph head did not complete at NLL position {pos}"
9740 ));
9741 }
9742 if pos < start {
9743 continue;
9744 }
9745 let logits = match out_of_band {
9746 Some(lg) => lg,
9747 None => {
9748 let normed = inference::rms_norm(
9749 &hidden,
9750 &self.weights.final_norm,
9751 self.rms_eps,
9752 self.norm_style,
9753 );
9754 self.lm_head_forward(&normed)
9758 }
9759 };
9760 let target = ids[pos + 1] as usize;
9761 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9762 let lse: f64 = logits
9763 .iter()
9764 .map(|&v| ((v - max) as f64).exp())
9765 .sum::<f64>()
9766 .ln()
9767 + max as f64;
9768 let tok_nll = lse - logits[target] as f64;
9769 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9770 let top = logits
9771 .iter()
9772 .enumerate()
9773 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9774 .map(|(i, _)| i)
9775 .unwrap_or(0);
9776 eprintln!(
9777 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9778 logits[target], logits[top]
9779 );
9780 }
9781 nll += tok_nll;
9782 cnt += 1;
9783 }
9784 Ok((nll, cnt))
9785 })();
9786 self.nll_end();
9787 result
9788 }
9789
9790 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9795 let normed = inference::rms_norm(
9796 hidden,
9797 &self.weights.final_norm,
9798 self.rms_eps,
9799 self.norm_style,
9800 );
9801 let mut logits = self.lm_head_forward(&normed);
9804 let target = target as usize;
9805 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9806 let lse: f64 = logits
9807 .iter()
9808 .map(|&v| ((v - max) as f64).exp())
9809 .sum::<f64>()
9810 .ln()
9811 + max as f64;
9812 let tok_nll = lse - logits[target] as f64;
9813 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9814 let top = logits
9815 .iter()
9816 .enumerate()
9817 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9818 .map(|(i, _)| i)
9819 .unwrap_or(0);
9820 eprintln!(
9821 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9822 logits[target], logits[top]
9823 );
9824 }
9825 attention::recycle_buf(&mut logits);
9826 tok_nll
9827 }
9828
9829 fn trace_recurrent_state(&self, pos: usize) {
9837 let stats = |v: &[f32]| -> (f64, f64) {
9838 if v.is_empty() {
9839 return (0.0, 0.0);
9840 }
9841 let (mut ss, mut mx) = (0f64, 0f64);
9842 for &x in v {
9843 ss += (x as f64) * (x as f64);
9844 mx = mx.max((x as f64).abs());
9845 }
9846 ((ss / v.len() as f64).sqrt(), mx)
9847 };
9848 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9849 let lw = &self.weights.layers[self.phys_layer(li)];
9850 let (kind, s_len) = match &lw.attn {
9851 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9852 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9853 AttnKind::Bounded(_) => ("bounded", 0),
9854 AttnKind::Full { .. } => ("full", 0),
9855 _ => ("other", 0),
9856 };
9857 let (rms, max) = stats(&l.linear_state);
9858 let s_part = if kind == "vmf" {
9859 &l.linear_state[..s_len.min(l.linear_state.len())]
9860 } else if kind == "gdn" {
9861 let ring = l
9862 .linear_state
9863 .len()
9864 .saturating_sub(s_len)
9865 .min(l.linear_state.len());
9866 &l.linear_state[ring..]
9867 } else {
9868 &l.linear_state[..0]
9869 };
9870 let (s_rms, s_max) = stats(s_part);
9871 let (ring_rms, ring_len) = match &l.bounded {
9872 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9873 None => (0.0, 0),
9874 };
9875 eprintln!(
9876 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9877 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9878 l.linear_state.len(),
9879 l.seq_len
9880 );
9881 }
9882 }
9883
9884 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9902 self.nll_begin()?;
9907 let requested_prefix = (prefill > 0).then_some(prefill);
9908 self.o1_begin_with_prefix(requested_prefix);
9909 let n = ids.len().saturating_sub(1);
9910 let requested_start = prefill.min(n);
9911 let exact_end = if self.o1_active() {
9916 match requested_prefix {
9917 Some(requested) => self.o1_effective_boundary(requested),
9918 None => self
9919 .o1_cfg
9920 .as_ref()
9921 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9922 }
9923 .unwrap_or(requested_start)
9924 .min(n)
9925 } else {
9926 requested_start
9927 };
9928 let mut nll = 0f64;
9929 let mut cnt = 0usize;
9930
9931 let mut pos = 0usize;
9935 if self.can_prefill_batched() {
9936 const CHUNK: usize = 128;
9937 while pos < exact_end {
9938 let end = (pos + CHUNK).min(exact_end);
9939 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9940 if self
9941 .graph_failed
9942 .swap(false, std::sync::atomic::Ordering::Relaxed)
9943 {
9944 self.cancel
9945 .store(false, std::sync::atomic::Ordering::Relaxed);
9946 self.nll_end();
9947 return Err("GPU graph failed during O(1) NLL prefix".into());
9948 }
9949 for row in 0..end - pos {
9950 let score_pos = pos + row;
9951 if score_pos >= requested_start && score_pos < n {
9952 nll += self.nll_from_hidden(
9953 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9954 ids[score_pos + 1],
9955 score_pos,
9956 );
9957 cnt += 1;
9958 }
9959 }
9960 pos = end;
9961 }
9962 } else {
9963 while pos < exact_end {
9964 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9965 if self
9966 .graph_failed
9967 .swap(false, std::sync::atomic::Ordering::Relaxed)
9968 {
9969 self.cancel
9970 .store(false, std::sync::atomic::Ordering::Relaxed);
9971 self.nll_end();
9972 return Err("GPU graph failed during O(1) NLL prefix".into());
9973 }
9974 if pos >= requested_start && pos < n {
9975 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9976 cnt += 1;
9977 }
9978 pos += 1;
9979 }
9980 }
9981 self.o1_seal_checked().map_err(|err| {
9982 self.nll_end();
9983 err
9984 })?;
9985
9986 let batch_k = std::env::var("CMF_BATCH_K")
9995 .ok()
9996 .and_then(|v| v.parse::<usize>().ok())
9997 .unwrap_or(0);
9998 let batch_admitted = batch_k > 0
9999 && self.can_prefill_batched()
10000 && self.o1_active()
10001 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10002 && (0..self.num_layers).all(|li| {
10003 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10004 cache.o1.is_none() || cache.o1_views().is_some()
10005 });
10006 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10007 eprintln!(
10008 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10009 batch_admitted,
10010 batch_k,
10011 n.saturating_sub(exact_end),
10012 );
10013 }
10014 let mut batch_completed = false;
10015 if batch_admitted && exact_end < n {
10016 let hs = self.hidden_size;
10017 let mut batch_pos = exact_end;
10018 while batch_pos < n {
10019 let end = (batch_pos + batch_k).min(n);
10020 let bk = end - batch_pos;
10021 let mut hiddens = vec![0.0f32; bk * hs];
10022 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10023 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10024 }
10025 let positions: Vec<usize> = (batch_pos..end).collect();
10026 let t_batch = std::time::Instant::now();
10027 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10028 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10029 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10030 eprintln!(
10031 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10032 batch_pos,
10033 end.saturating_sub(1),
10034 bk as f64 / (ms / 1000.0),
10035 );
10036 }
10037 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10038 self.nll_end();
10039 return Err(err);
10040 }
10041 match outcome {
10042 crate::gpu::BatchGraphOutcome::Completed => {
10043 batch_completed = true;
10044 for row in 0..bk {
10045 nll += self.nll_from_hidden(
10046 &hiddens[row * hs..(row + 1) * hs],
10047 ids[batch_pos + row + 1],
10048 batch_pos + row,
10049 );
10050 cnt += 1;
10051 }
10052 batch_pos = end;
10053 }
10054 crate::gpu::BatchGraphOutcome::Declined => {
10055 if batch_completed {
10056 self.nll_end();
10057 return Err(format!(
10058 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10059 ));
10060 }
10061 break;
10062 }
10063 crate::gpu::BatchGraphOutcome::Failed => {
10064 self.nll_end();
10065 return Err(format!(
10066 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10067 ));
10068 }
10069 }
10070 }
10071 if batch_completed && cnt == n.saturating_sub(requested_start) {
10072 self.nll_end();
10073 return Ok((nll, cnt));
10074 }
10075 }
10076
10077 for pos in exact_end..n {
10082 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10083 if self
10084 .graph_failed
10085 .swap(false, std::sync::atomic::Ordering::Relaxed)
10086 {
10087 self.cancel
10088 .store(false, std::sync::atomic::Ordering::Relaxed);
10089 self.nll_end();
10090 return Err(format!(
10091 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10092 ));
10093 }
10094 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10095 cnt += 1;
10096 }
10097 self.nll_end();
10098 Ok((nll, cnt))
10099 }
10100
10101 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10109 self.clear_sequence_state();
10110 let n = ids.len().saturating_sub(1);
10111 let mut correct = Vec::with_capacity(n);
10112 let mut pmax = Vec::with_capacity(n);
10113 for pos in 0..n {
10114 let emb = self.embed_single(ids[pos]);
10115 let hidden = self.forward_layers(&emb, pos, None);
10116 let logits = if let Some(logits) = self.graph_logits.take() {
10117 logits
10118 } else {
10119 let normed = inference::rms_norm(
10120 &hidden,
10121 &self.weights.final_norm,
10122 self.rms_eps,
10123 self.norm_style,
10124 );
10125 self.lm_head_forward(&normed)
10129 };
10130 let target = ids[pos + 1] as usize;
10131 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10132 for (i, &v) in logits.iter().enumerate() {
10133 if v > mval {
10134 mval = v;
10135 amax = i;
10136 }
10137 }
10138 correct.push(amax == target);
10139 let row: Vec<f32> = temps
10140 .iter()
10141 .map(|&t| {
10142 let tt = t.max(1e-3);
10143 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10144 1.0 / s.max(1e-12) })
10146 .collect();
10147 pmax.push(row);
10148 }
10149 self.clear_sequence_state();
10150 (correct, pmax)
10151 }
10152
10153 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10160 if self.dyn_router.is_none() {
10161 return Ok((self.ppl_ids(ids)?, 0));
10162 }
10163 self.nll_begin()?;
10164 let saved_active = self.dyn_active;
10165 let mut router = self
10166 .dyn_router
10167 .take()
10168 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10169 router.reset();
10170 self.dyn_phi_seen = 0;
10171 let _ = self.set_active_skill(None);
10172
10173 let result: Result<(f64, usize), String> = (|| {
10174 let mut nll = 0f64;
10175 let mut cnt = 0usize;
10176 for pos in 0..ids.len().saturating_sub(1) {
10177 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10178 self.nll_check_graph("dynamic serial forward", pos)?;
10179 let out_of_band = self.graph_logits.take();
10180 let mut logits = match out_of_band {
10181 Some(lg) => lg,
10182 None => {
10183 let normed = inference::rms_norm(
10184 &hidden,
10185 &self.weights.final_norm,
10186 self.rms_eps,
10187 self.norm_style,
10188 );
10189 self.lm_head_forward(&normed)
10193 }
10194 };
10195 let target = ids[pos + 1] as usize;
10196 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10197 let lse: f64 = logits
10198 .iter()
10199 .map(|&v| ((v - max) as f64).exp())
10200 .sum::<f64>()
10201 .ln()
10202 + max as f64;
10203 let tok_nll = lse - logits[target] as f64;
10204 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10205 let top = logits
10206 .iter()
10207 .enumerate()
10208 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10209 .map(|(i, _)| i)
10210 .unwrap_or(0);
10211 eprintln!(
10212 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10213 logits[target], logits[top]
10214 );
10215 }
10216 nll += tok_nll;
10217 cnt += 1;
10218 attention::recycle_buf(&mut logits);
10219 let phi = self.dyn_phi_ema.clone();
10221 if let Some(new_active) = router.step(&phi, pos) {
10222 let _ = self.set_active_skill(new_active);
10223 }
10224 }
10225 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10226 })();
10227
10228 let _ = self.set_active_skill(saved_active);
10231 self.dyn_router = Some(router);
10232 self.nll_end();
10233 result
10234 }
10235
10236 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10238 self.clear_sequence_state();
10239 let mut acc = vec![0f32; self.hidden_size];
10240 for (pos, &id) in ids.iter().enumerate() {
10241 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10242 for (a, v) in acc.iter_mut().zip(&h) {
10243 *a += v;
10244 }
10245 }
10246 let n = ids.len().max(1) as f32;
10247 for a in acc.iter_mut() {
10248 *a /= n;
10249 }
10250 self.clear_sequence_state();
10251 acc
10252 }
10253
10254 pub fn probe_phi_span(
10268 &mut self,
10269 ids: &[u32],
10270 layer: usize,
10271 span: std::ops::Range<usize>,
10272 ) -> Vec<f32> {
10273 #[cfg(target_os = "macos")]
10274 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10275 let end = span.end.min(ids.len());
10276 let start = span.start.min(end);
10277 let reset = |p: &mut Self| p.clear_sequence_state();
10278 reset(self);
10279 let mut acc = vec![0f32; self.hidden_size];
10280 for (pos, &id) in ids[..end].iter().enumerate() {
10281 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10282 if pos >= start {
10283 for (a, v) in acc.iter_mut().zip(&h) {
10284 *a += v;
10285 }
10286 }
10287 }
10288 let n = end - start;
10289 if n > 0 {
10290 let n = n as f32;
10291 for a in acc.iter_mut() {
10292 *a /= n;
10293 }
10294 }
10295 reset(self);
10296 acc
10297 }
10298
10299 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10306 #[cfg(target_os = "macos")]
10307 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10308 self.graph_logits = None;
10309 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10310 if let Some(logits) = self.graph_logits.take() {
10311 return logits;
10312 }
10313 inference::rms_norm_into(
10314 &hidden,
10315 &self.weights.final_norm,
10316 self.rms_eps,
10317 self.norm_style,
10318 &mut self.ws.n1,
10319 );
10320 self.lm_head_forward(&self.ws.n1)
10321 }
10322
10323 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10329 self.prefill_batch_masked(ids, start_pos, None)
10330 }
10331
10332 fn prefill_batch_masked(
10338 &mut self,
10339 ids: &[u32],
10340 start_pos: usize,
10341 task_mask: Option<&TaskMask>,
10342 ) -> Vec<f32> {
10343 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10344 }
10345
10346 fn prefill_rows(
10354 &mut self,
10355 ids: &[u32],
10356 pos: usize,
10357 task_mask: Option<&TaskMask>,
10358 ) -> Result<Vec<f32>, String> {
10359 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10360 }
10361
10362 fn prefill_input_rows(
10363 &mut self,
10364 input: PrefillIn<'_>,
10365 pos: usize,
10366 task_mask: Option<&TaskMask>,
10367 ) -> Result<Vec<f32>, String> {
10368 self.mimo_moe_prepare();
10369 let hs = self.hidden_size;
10370 let bk = match input {
10371 PrefillIn::Ids(ids) => ids.len(),
10372 PrefillIn::Hidden(rows) => rows.len() / hs,
10373 };
10374 #[cfg(not(target_os = "macos"))]
10375 if task_mask.is_none()
10376 && !self.o1_active()
10377 && bk > 1
10378 && (self.batch_prefix_prefill()
10379 || (self.verify_exact_moe
10380 && crate::gpu::enabled_here()
10381 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10382 {
10383 let mut hiddens = match input {
10384 PrefillIn::Hidden(rows) => rows.to_vec(),
10385 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10386 };
10387 let positions: Vec<usize> = (pos..pos + bk).collect();
10388 let mut run = 0usize;
10389 match self.try_batch_graph_wgpu_prefix(
10390 &mut hiddens,
10391 &positions,
10392 bk,
10393 None,
10394 Some(&mut run),
10395 ) {
10396 crate::gpu::BatchGraphOutcome::Completed => {
10397 let out = if run < self.num_layers {
10398 self.prefill_batch_span(
10399 PrefillIn::Hidden(&hiddens),
10400 pos,
10401 None,
10402 run,
10403 self.num_layers,
10404 )
10405 } else {
10406 hiddens
10407 };
10408 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10409 Err("MiMo attention graph failed after admission".into())
10410 } else {
10411 Ok(out)
10412 };
10413 }
10414 crate::gpu::BatchGraphOutcome::Failed => {
10415 return Err("batched prefix prefill failed after admission".into());
10416 }
10417 crate::gpu::BatchGraphOutcome::Declined => {
10418 #[cfg(feature = "gpu")]
10420 self.pull_lagging_host_kv(0, self.num_layers, pos);
10421 }
10422 }
10423 }
10424 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10425 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10426 Err("batch tail graph failed after admission".into())
10427 } else {
10428 Ok(out)
10429 }
10430 }
10431
10432 fn prefill_batch_span(
10438 &mut self,
10439 input: PrefillIn<'_>,
10440 start_pos: usize,
10441 task_mask: Option<&TaskMask>,
10442 from: usize,
10443 upto_excl: usize,
10444 ) -> Vec<f32> {
10445 let hs = self.hidden_size;
10446 let b = match input {
10447 PrefillIn::Ids(ids) => ids.len(),
10448 PrefillIn::Hidden(hb) => hb.len() / hs,
10449 };
10450 let upto_excl = upto_excl.min(self.num_layers);
10451 let mut h: Vec<f32>;
10455 let mut h_ready;
10456 match input {
10457 PrefillIn::Ids(_) => {
10458 h = vec![0.0; b * hs];
10459 h_ready = false;
10460 }
10461 PrefillIn::Hidden(hb) => {
10462 h = hb.to_vec();
10463 h_ready = true;
10464 }
10465 }
10466 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10467 if let PrefillIn::Ids(ids) = input {
10468 for (bi, &id) in ids.iter().enumerate() {
10469 let e = me.embed_single(id);
10470 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10471 }
10472 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10473 if let Ok(t) = tp.parse::<usize>() {
10474 if t >= start_pos && t < start_pos + ids.len() {
10475 let bi = t - start_pos;
10476 let row = &h[bi * hs..(bi + 1) * hs];
10477 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10478 eprintln!(
10479 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10480 ids[bi],
10481 row[0],
10482 row[1],
10483 ids.len(),
10484 &ids[..ids.len().min(8)]
10485 );
10486 }
10487 }
10488 }
10489 }
10490 };
10491 let (_nkv, _hd, _rd, eps) = (
10492 self.num_kv_heads,
10493 self.head_dim,
10494 self.rotary_dim,
10495 self.rms_eps,
10496 );
10497 let pool = self.pool.clone();
10498 let norm_style = self.norm_style;
10499 self.mimo_moe_prepare();
10500 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10501
10502 #[cfg(target_os = "macos")]
10503 let mut chunk_skip_until = 0usize;
10504 for li in from..upto_excl {
10505 let _capacity_tail = automatic_gpu_prefix
10506 .filter(|&prefix| {
10507 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10508 })
10509 .map(|_| crate::gpu::enter_cpu_scope());
10510 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10515 let _fast_gemm = self
10516 .proj_gate_sigmoid
10517 .then(crate::gpu::enter_prefill_fast_gemm);
10518 #[cfg(target_os = "macos")]
10524 if task_mask.is_none() {
10525 if li < chunk_skip_until {
10526 continue;
10527 }
10528 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10534 fill_h(&mut h, self);
10535 h_ready = true;
10536 }
10537 let ids_for_embed = match input {
10538 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10539 PrefillIn::Hidden(_) => None,
10540 };
10541 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10542 if end > li {
10543 h_ready = true;
10544 chunk_skip_until = end;
10545 if self.is_loop_end(end - 1) && end < self.num_layers {
10548 for bi in 0..b {
10549 let normed = inference::rms_norm(
10550 &h[bi * hs..(bi + 1) * hs],
10551 &self.weights.final_norm,
10552 eps,
10553 norm_style,
10554 );
10555 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10556 }
10557 }
10558 continue;
10559 }
10560 }
10561 if !h_ready {
10562 fill_h(&mut h, self);
10563 h_ready = true;
10564 }
10565 if task_mask.is_none() && self.verify_exact_moe {
10566 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10567 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10568 crate::gpu::BatchGraphOutcome::Completed => continue,
10569 crate::gpu::BatchGraphOutcome::Failed => return h,
10570 crate::gpu::BatchGraphOutcome::Declined => {}
10571 }
10572 }
10573 #[cfg(feature = "gpu")]
10574 self.pull_lagging_host_kv(li, li + 1, start_pos);
10575 let lw = &self.weights.layers[self.phys_layer(li)];
10576 let t_attn = std::time::Instant::now();
10577 match &lw.attn {
10579 AttnKind::Kda(w) => {
10580 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10582 let mut normed = vec![0.0f32; b * hs];
10583 for bi in 0..b {
10584 inference::rms_norm_into(
10585 &h[bi * hs..(bi + 1) * hs],
10586 &lw.input_norm,
10587 eps,
10588 norm_style,
10589 &mut normed[bi * hs..(bi + 1) * hs],
10590 );
10591 }
10592 let attn = crate::linear_core::kda_forward_batch(
10593 &normed,
10594 b,
10595 w,
10596 &cfg,
10597 &mut self.kv_cache.layers[li].linear_state,
10598 pool.as_deref(),
10599 );
10600 for (dst, &a) in h.iter_mut().zip(&attn) {
10601 *dst += a;
10602 }
10603 }
10604 AttnKind::LinearGdn(w) => {
10605 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10607 let mut normed = vec![0.0f32; b * hs];
10608 for bi in 0..b {
10609 let r = inference::rms_norm(
10610 &h[bi * hs..(bi + 1) * hs],
10611 &lw.input_norm,
10612 eps,
10613 norm_style,
10614 );
10615 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10616 }
10617 let attn = crate::linear_core::gdn_forward_batch(
10618 &normed,
10619 b,
10620 w,
10621 &cfg,
10622 &mut self.kv_cache.layers[li].linear_state,
10623 pool.as_deref(),
10624 );
10625 for (dst, &a) in h.iter_mut().zip(&attn) {
10626 *dst += a;
10627 }
10628 }
10629 AttnKind::ShortConv(w) => {
10630 let cfg = self
10633 .short_conv_cfg
10634 .expect("short-conv layer without short_conv_cfg");
10635 let mut normed = vec![0.0f32; b * hs];
10636 for bi in 0..b {
10637 inference::rms_norm_into(
10638 &h[bi * hs..(bi + 1) * hs],
10639 &lw.input_norm,
10640 eps,
10641 norm_style,
10642 &mut normed[bi * hs..(bi + 1) * hs],
10643 );
10644 }
10645 let attn = short_conv_forward_batch(
10646 &normed,
10647 b,
10648 w,
10649 &cfg,
10650 &mut self.kv_cache.layers[li].linear_state,
10651 pool.as_deref(),
10652 );
10653 for (dst, &a) in h.iter_mut().zip(&attn) {
10654 *dst += a;
10655 }
10656 }
10657 AttnKind::Mla(w) => {
10658 let inv_freq_l = self.layer_inv_freq(li);
10661 let rs = self.layer_rope_scale(li);
10662 let mut normed = vec![0.0f32; hs];
10663 for bi in 0..b {
10664 inference::rms_norm_into(
10665 &h[bi * hs..(bi + 1) * hs],
10666 &lw.input_norm,
10667 eps,
10668 norm_style,
10669 &mut normed,
10670 );
10671 let ao = mla_attention(
10672 w,
10673 &normed,
10674 &mut self.kv_cache.layers[li],
10675 start_pos + bi,
10676 &inv_freq_l,
10677 rs,
10678 eps,
10679 pool.as_deref(),
10680 );
10681 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10682 *dst += a;
10683 }
10684 }
10685 }
10686 AttnKind::Full {
10687 wq,
10688 wk,
10689 wv,
10690 wo,
10691 q_norm,
10692 k_norm,
10693 output_gate,
10694 softplus_gate,
10695 bias,
10696 } => {
10697 let mut normed = vec![0.0f32; b * hs];
10701 for bi in 0..b {
10702 inference::rms_norm_into(
10703 &h[bi * hs..(bi + 1) * hs],
10704 &lw.input_norm,
10705 eps,
10706 norm_style,
10707 &mut normed[bi * hs..(bi + 1) * hs],
10708 );
10709 }
10710 let inv_freq_l = self.layer_inv_freq(li);
10711 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10712 let cfg = QwenAttnCfg {
10713 num_heads: self.layer_num_heads(li),
10714 num_kv_heads: nkv_l,
10715 head_dim: hd_l,
10716 hidden_size: hs,
10717 position: start_pos,
10718 inv_freq: &inv_freq_l,
10719 rotary_dim: rd_l,
10720 scale: self.attn_scale,
10721 softcap: self.attn_softcap,
10722 window: self.layer_window(li),
10723 v_norm: self.attn_v_norm,
10724 qk_norm_after_rope: self.qk_norm_after_rope,
10725 gate_sigmoid: self.proj_gate_sigmoid,
10726 q_norm: q_norm.as_deref(),
10727 k_norm: k_norm.as_deref(),
10728 output_gate: *output_gate,
10729 softplus_gate: softplus_gate
10730 .as_ref()
10731 .map(|(gate, per_head)| (gate, *per_head)),
10732 rope_scale: self.layer_rope_scale(li),
10733 bias: bias
10734 .as_ref()
10735 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10736 rms_eps: eps,
10737 norm_style,
10738 pool: pool.as_deref(),
10739 v_head_dim: self.layer_v_dim(li),
10740 };
10741 #[cfg(feature = "gpu")]
10742 let _mirror = self
10743 .prefill_mirror_target(li)
10744 .map(crate::gpu::enter_prefill_mirror);
10745 let mut attn = attention::qwen_attention_batch(
10746 &normed,
10747 b,
10748 wq,
10749 wk,
10750 wv,
10751 wo,
10752 &mut self.kv_cache.layers[li],
10753 &cfg,
10754 );
10755 if let Some(w) = &lw.attn_out_norm {
10756 for bi in 0..b {
10757 inference::rms_norm_into(
10758 &attn[bi * hs..(bi + 1) * hs],
10759 w,
10760 eps,
10761 norm_style,
10762 &mut normed[bi * hs..(bi + 1) * hs],
10763 );
10764 }
10765 attn.copy_from_slice(&normed);
10766 }
10767 for (dst, &a) in h.iter_mut().zip(&attn) {
10768 *dst += a;
10769 }
10770 }
10771 AttnKind::Bounded(w) => {
10772 let mut normed = vec![0.0f32; b * hs];
10775 for bi in 0..b {
10776 inference::rms_norm_into(
10777 &h[bi * hs..(bi + 1) * hs],
10778 &lw.input_norm,
10779 eps,
10780 norm_style,
10781 &mut normed[bi * hs..(bi + 1) * hs],
10782 );
10783 }
10784 let rope = self
10785 .bounded_rope
10786 .clone()
10787 .expect("bounded layer without an installed rotation table");
10788 let cfg = crate::bounded::BoundedAttnCfg {
10789 num_heads: self.num_heads,
10790 num_kv_heads: self.num_kv_heads,
10791 head_dim: self.head_dim,
10792 hidden_size: hs,
10793 scale: self.attn_scale,
10794 rope: &rope,
10795 pool: pool.as_deref(),
10796 };
10797 let mut attn = crate::bounded::bounded_attention_batch(
10798 &normed,
10799 b,
10800 w,
10801 &mut self.kv_cache.layers[li],
10802 &cfg,
10803 );
10804 if let Some(wn) = &lw.attn_out_norm {
10805 for bi in 0..b {
10806 inference::rms_norm_into(
10807 &attn[bi * hs..(bi + 1) * hs],
10808 wn,
10809 eps,
10810 norm_style,
10811 &mut normed[bi * hs..(bi + 1) * hs],
10812 );
10813 }
10814 attn.copy_from_slice(&normed);
10815 }
10816 for (dst, &a) in h.iter_mut().zip(&attn) {
10817 *dst += a;
10818 }
10819 attention::recycle_buf(&mut attn);
10820 }
10821 AttnKind::Linear(w) => {
10822 for bi in 0..b {
10823 let normed = inference::rms_norm(
10824 &h[bi * hs..(bi + 1) * hs],
10825 &lw.input_norm,
10826 eps,
10827 norm_style,
10828 );
10829 vmf_phase_forward(
10830 &normed,
10831 w,
10832 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10833 &mut self.kv_cache.layers[li].linear_state,
10834 pool.as_deref(),
10835 )
10836 .iter()
10837 .enumerate()
10838 .for_each(|(i, &a)| h[bi * hs + i] += a);
10839 }
10840 }
10841 }
10842
10843 let lw = &self.weights.layers[self.phys_layer(li)];
10845 let mut post = vec![0.0f32; b * hs];
10846 for bi in 0..b {
10847 let r =
10848 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10849 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10850 }
10851 let mask_row = task_mask
10854 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10855 .and_then(|m| m.ffn_masks.get(li))
10856 .map(|v| v.as_slice());
10857 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10858 let t_ffn = std::time::Instant::now();
10859 let mut ffn = match &lw.ffn {
10860 FfnKind::Dense(d) if !d.segs.is_empty() => {
10861 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10862 }
10863 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10864 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10865 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10866 }
10867 FfnKind::Moe(m) if self.verify_exact_moe => {
10868 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10869 }
10870 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10873 let before = m.stats.borrow().clone();
10874 let out = crate::gpu::cpu_scope(|| {
10875 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10876 });
10877 self.mimo_moe.prime(li, m, &before);
10878 out
10879 }
10880 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10881 FfnKind::DenseMoe(dm) => {
10884 let mut out = vec![0.0f32; b * hs];
10885 for bi in 0..b {
10886 let r = dense_moe_ffn(
10887 dm,
10888 &post[bi * hs..(bi + 1) * hs],
10889 &h[bi * hs..(bi + 1) * hs],
10890 eps,
10891 norm_style,
10892 pool.as_deref(),
10893 );
10894 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10895 }
10896 out
10897 }
10898 };
10899 if prefill_prof_on() {
10900 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10901 PREFILL_SPLIT[1].fetch_add(
10902 t_ffn.elapsed().as_nanos() as u64,
10903 std::sync::atomic::Ordering::Relaxed,
10904 );
10905 }
10906 if let Some(w) = &lw.ffn_out_norm {
10907 for bi in 0..b {
10908 inference::rms_norm_into(
10909 &ffn[bi * hs..(bi + 1) * hs],
10910 w,
10911 eps,
10912 norm_style,
10913 &mut post[bi * hs..(bi + 1) * hs],
10914 );
10915 }
10916 ffn.copy_from_slice(&post);
10917 }
10918 for (dst, &f) in h.iter_mut().zip(&ffn) {
10919 *dst += f;
10920 }
10921 if let Some(sc) = lw.layer_scale {
10922 for v in h.iter_mut() {
10923 *v *= sc;
10924 }
10925 }
10926 if self.layer_dump.is_some() {
10928 for bi in 0..b {
10929 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10930 }
10931 }
10932 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10933 if let Ok(t) = tp.parse::<usize>() {
10934 if t >= start_pos && t < start_pos + b {
10935 let bi = t - start_pos;
10936 let row = &h[bi * hs..(bi + 1) * hs];
10937 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10938 eprintln!(
10939 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10940 row[0], row[1]
10941 );
10942 }
10943 }
10944 }
10945 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10949 let row = &h[(b - 1) * hs..b * hs];
10950 let rms =
10951 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10952 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10953 eprintln!(
10954 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10955 match &self.weights.layers[self.phys_layer(li)].attn {
10956 AttnKind::LinearGdn(_) => "gdn",
10957 AttnKind::Linear(_) => "vmf",
10958 AttnKind::ShortConv(_) => "conv",
10959 _ => "attn",
10960 },
10961 match &lw.ffn {
10962 FfnKind::Moe(_) => "moe",
10963 FfnKind::Dense(_) => "dense",
10964 FfnKind::DenseMoe(_) => "dense+moe",
10965 },
10966 );
10967 }
10968 if self.is_loop_end(li) && li + 1 < self.num_layers {
10970 for bi in 0..b {
10971 let normed = inference::rms_norm(
10972 &h[bi * hs..(bi + 1) * hs],
10973 &self.weights.final_norm,
10974 eps,
10975 norm_style,
10976 );
10977 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10978 }
10979 }
10980 if std::env::var("CMF_TRACE_H").is_ok() {
10981 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10982 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10983 eprintln!(
10984 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10985 lw.layer_scale
10986 );
10987 }
10988 }
10989 crate::gpu::set_layer(-1); if prefill_prof_on() {
10991 let ms = |a: &std::sync::atomic::AtomicU64| {
10992 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10993 };
10994 let sp = &attention::ATTN_SPLIT;
10995 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10996 eprintln!(
10997 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10998 host gate {:.1}], host loop {:.1}, attend {:.1} \
10999 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11000 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11001 ms(&PREFILL_SPLIT[0]),
11002 ms(&sp[0]),
11003 qkv_enq,
11004 qkv_wait,
11005 ms(&sp[6]),
11006 ms(&sp[1]),
11007 ms(&sp[2]),
11008 ms(&sp[4]),
11009 ms(&sp[5]),
11010 ca_wait,
11011 ms(&sp[3]),
11012 ms(&PREFILL_SPLIT[1]),
11013 );
11014 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11015 eprintln!(
11016 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11017 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11018 fallbacks (cumulative)",
11019 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11020 );
11021 }
11022 self.o1_progress();
11027 self.swa_trim_tails();
11029 h
11030 }
11031
11032 fn embed_single(&self, id: u32) -> Vec<f32> {
11034 let mut out = vec![0.0f32; self.hidden_size];
11035 if (id as usize) < self.weights.embed_tokens.rows() {
11036 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11037 }
11038 if self.embed_multiplier != 1.0 {
11039 for v in out.iter_mut() {
11040 *v *= self.embed_multiplier;
11041 }
11042 }
11043 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11047 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11048 v[0] = id as f32;
11049 return v;
11050 }
11051 if let Some(b) = &self.g4e {
11055 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11056 }
11057 if let Some(b) = &self.g3n {
11058 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11059 }
11060 out
11061 }
11062
11063 #[cfg(target_os = "macos")]
11069 fn chunk_run_gpu(
11070 &mut self,
11071 li0: usize,
11072 h: &mut [f32],
11073 b: usize,
11074 pos0: usize,
11075 embed_ids: Option<&[u32]>,
11076 cap: usize,
11077 ) -> usize {
11078 let gemma4 =
11085 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11086 if !crate::gpu::enabled_here()
11087 || std::env::var("CMF_GPU_CHUNK")
11088 .map(|v| v == "0")
11089 .unwrap_or(false)
11090 || b < 32
11091 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11094 || (self.global_attn.is_some() && !gemma4)
11095 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11099 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11102 || self.o1_active()
11105 || (self.attn_v_norm && !gemma4)
11106 || !self.metal_rope_scale_ok()
11107 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11108 {
11109 return li0;
11110 }
11111 let Some(model) = self.model.clone() else {
11112 return li0;
11113 };
11114 let (nh, nkv, hd, hs) = (
11115 self.num_heads,
11116 self.num_kv_heads,
11117 self.head_dim,
11118 self.hidden_size,
11119 );
11120 let loop_end = if self.loop_final_norm {
11124 ((li0 / self.physical_layers) + 1) * self.physical_layers
11125 } else {
11126 self.num_layers
11127 };
11128 let run_end = self.num_layers.min(loop_end).min(cap);
11129 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11134 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11135 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11136 let mut stored_at: Vec<usize> = Vec::new();
11137 for li in li0..run_end {
11138 let lw = &self.weights.layers[self.phys_layer(li)];
11139 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11140 && !gemma4
11141 {
11142 break;
11143 }
11144 let AttnKind::Full {
11145 wq,
11146 wk,
11147 wv,
11148 wo,
11149 q_norm,
11150 k_norm,
11151 output_gate: false,
11152 softplus_gate,
11153 bias,
11154 } = &lw.attn
11155 else {
11156 break;
11157 };
11158 let head_gate = match softplus_gate {
11160 None => None,
11161 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11162 Some((d, r, c)) if r == nh && c == hs => Some(d),
11163 _ => break,
11164 },
11165 Some(_) => break,
11166 };
11167 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11171 FfnKind::Dense(d)
11172 if (matches!(d.act, Act::Silu | Act::Gelu)
11173 || (gemma4 && d.act == Act::GeluTanh))
11174 && d.segs.is_empty() =>
11175 {
11176 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11177 }
11178 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11179 Some((router, experts, q8)) => {
11180 let e0 = &m.experts[0];
11181 let route = Box::new(move |lg: &[f32]| {
11182 let (idx, p, wsum) = moe_route(lg, m, None);
11184 let mut st = m.stats.borrow_mut();
11185 if st.len() < m.experts.len() {
11186 st.resize(m.experts.len(), 0);
11187 }
11188 for &e in &idx {
11189 st[e] += 1;
11190 }
11191 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11192 (idx, w)
11193 });
11194 (
11195 &e0.gate_proj,
11196 &e0.up_proj,
11197 &e0.down_proj,
11198 Act::Silu,
11199 Some(crate::gpu_metal::ChunkMoe {
11200 router,
11201 experts,
11202 q8,
11203 route,
11204 }),
11205 )
11206 }
11207 None => break,
11208 },
11209 _ => break,
11210 };
11211 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11217 t.q8_row_parts()
11218 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11219 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11220 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11221 }
11222 fn cf(t: &QTensor) -> &[f32] {
11223 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11224 }
11225 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11226 if moe.is_none()
11235 && !gemma4
11236 && (col.iter().any(|c| !c.is_empty())
11237 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
11238 {
11239 break;
11240 }
11241 let parts = (
11242 cw(wq),
11243 cw(wk),
11244 cw(wv),
11245 cw(wo),
11246 cw(gate_t),
11247 cw(up_t),
11248 cw(down_t),
11249 );
11250 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11251 else {
11252 break;
11253 };
11254 let layer = &self.kv_cache.layers[li];
11255 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11256 break;
11257 }
11258 debug_assert!(
11260 layer.base() == 0
11261 || self
11262 .layer_window(li)
11263 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11264 );
11265 stored_at.push(layer.head_len(0));
11266 let (nkv, hd) = if gemma4 {
11269 let (a, bb, _) = self.layer_geom(li);
11270 (a, bb)
11271 } else {
11272 (nkv, hd)
11273 };
11274 layers.push(crate::gpu_metal::ChunkLayer {
11275 model: &model,
11276 kv_id: self.graph_kv_id,
11277 layer: li,
11278 wq: pq,
11279 wk: pk,
11280 wv: pv,
11281 wo: po,
11282 gate: pg,
11283 up: pu,
11284 down: pd,
11285 input_norm: &lw.input_norm,
11286 post_norm: &lw.post_norm,
11287 bias: bias
11288 .as_ref()
11289 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11290 q_norm: q_norm.as_deref(),
11291 k_norm: k_norm.as_deref(),
11292 inv_freq: &tables[li - li0],
11293 rope_scale: self.layer_rope_scale(li),
11294 rd: self.layer_geom(li).2,
11295 nh,
11296 nkv,
11297 hd,
11298 hs,
11299 inter: gate_t.rows(),
11300 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11301 late_qk_norm: self.qk_norm_after_rope,
11302 eps: self.rms_eps as f32,
11303 window: self.layer_window(li),
11304 head_gate,
11305 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11306 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11307 v_norm: self.attn_v_norm,
11308 scale: self.attn_scale,
11309 attn_out_norm: lw.attn_out_norm.as_deref(),
11310 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11311 layer_scale: lw.layer_scale,
11312 }),
11313 col,
11314 moe,
11315 });
11316 }
11317 if layers.is_empty() {
11318 return li0;
11319 }
11320 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11322 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11323 .iter()
11324 .zip(&rows_kv)
11325 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11326 .collect();
11327 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11328 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11329 let li = layers[i].layer;
11330 let layer = &self.kv_cache.layers[li];
11331 let nkv = rows_kv[i].0;
11332 io.push(crate::gpu_metal::ChunkIo {
11333 cpu_stored: stored_at[i],
11334 cpu_gen: layer.generation(),
11335 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11336 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11337 out_k: ok,
11338 out_v: ov,
11339 imp: oi,
11340 });
11341 }
11342 let n_run = layers.len();
11343 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11344 let ep = embed_ids.and_then(|ids| {
11347 self.weights
11348 .embed_tokens
11349 .q8_row_parts()
11350 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11351 idx,
11352 rows,
11353 row_scale: rs,
11354 ids,
11355 mult: self.embed_multiplier,
11356 })
11357 });
11358 if embed_ids.is_some() && ep.is_none() {
11359 return li0;
11360 }
11361 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11362 return li0;
11363 }
11364 drop(io);
11365 drop(layers);
11366 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11369 let li = li0 + i;
11370 let row = rows_kv[i].1;
11371 let layer = &mut self.kv_cache.layers[li];
11372 for bi in 0..b {
11373 layer.append(
11374 &ok[bi * row..(bi + 1) * row],
11375 &ov[bi * row..(bi + 1) * row],
11376 &[],
11377 );
11378 }
11379 layer.accumulate_imp(oi);
11380 }
11381 last
11382 }
11383
11384 fn layer_is_local(&self, li: usize) -> bool {
11387 if let Some(layers) = &self.sliding_layers {
11388 return layers.get(li).copied().unwrap_or(false);
11389 }
11390 match self.swa {
11391 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11392 None => false,
11393 }
11394 }
11395
11396 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11399 if self.layer_is_local(li) {
11400 if let Some(f) = &self.inv_freq_local {
11401 return f.clone();
11402 }
11403 } else if let Some(f) = &self.inv_freq_global {
11404 return f.clone();
11405 }
11406 self.inv_freq.clone()
11407 }
11408
11409 fn layer_window(&self, li: usize) -> Option<usize> {
11411 self.swa
11412 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11413 }
11414
11415 fn swa_trim_tails(&mut self) {
11424 let Some((slack, align)) = self.swa_trim else {
11425 return;
11426 };
11427 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11428 return;
11429 }
11430 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11431 if let Some(w) = self.layer_window(li) {
11432 self.kv_cache.layers[li].trim_window(w, slack, align);
11433 }
11434 }
11435 }
11436
11437 fn layer_num_heads(&self, li: usize) -> usize {
11438 self.attention_heads_per_layer
11439 .as_ref()
11440 .and_then(|v| v.get(li).copied())
11441 .unwrap_or(self.num_heads)
11442 }
11443
11444 fn layer_rope_scale(&self, li: usize) -> f32 {
11445 if self.layer_is_local(li) {
11446 self.rope_scale_local
11447 } else {
11448 self.rope_scale
11449 }
11450 }
11451
11452 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11455 if !self.layer_is_local(li) {
11456 if let Some((ghd, gkv)) = self.global_attn {
11457 return (gkv, ghd, ghd);
11458 }
11459 }
11460 let rd = if self.layer_is_local(li) {
11461 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11462 } else {
11463 self.rotary_dim
11464 };
11465 let rd = if self
11467 .no_rope_layers
11468 .as_ref()
11469 .and_then(|v| v.get(li))
11470 .copied()
11471 == Some(true)
11472 {
11473 0
11474 } else {
11475 rd
11476 };
11477 (self.layer_num_kv_heads(li), self.head_dim, rd)
11478 }
11479
11480 fn layer_num_kv_heads(&self, li: usize) -> usize {
11483 self.kv_heads_per_layer
11484 .as_ref()
11485 .and_then(|v| v.get(self.phys_layer(li)).copied())
11486 .unwrap_or(self.num_kv_heads)
11487 }
11488
11489 fn layer_v_dim(&self, li: usize) -> usize {
11491 let (_, hd, _) = self.layer_geom(li);
11492 self.v_head_dim.unwrap_or(hd).min(hd)
11493 }
11494
11495 pub fn set_attn_geometry(
11504 &mut self,
11505 kv_heads_per_layer: Option<Vec<usize>>,
11506 v_head_dim: Option<usize>,
11507 ) -> Result<(), String> {
11508 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11509 if self.global_attn.is_some() {
11510 return Err(
11511 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11512 attention geometry"
11513 .into(),
11514 );
11515 }
11516 if self
11517 .weights
11518 .layers
11519 .iter()
11520 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11521 {
11522 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11523 }
11524 }
11525 if let Some(vd) = v_head_dim {
11526 if vd == 0 || vd > self.head_dim {
11527 return Err(format!(
11528 "v_head_dim {vd} must be in 1..={} (head_dim)",
11529 self.head_dim
11530 ));
11531 }
11532 }
11533 if let Some(v) = &kv_heads_per_layer {
11534 if v.len() != self.physical_layers {
11535 return Err(format!(
11536 "kv_heads_per_layer has {} entries, expected {} layers",
11537 v.len(),
11538 self.physical_layers
11539 ));
11540 }
11541 for (li, &nkv) in v.iter().enumerate() {
11542 let is_attn = matches!(
11543 self.weights.layers.get(li).map(|lw| &lw.attn),
11544 Some(AttnKind::Full { .. }) | None
11545 );
11546 if !is_attn {
11547 continue;
11548 }
11549 let nh = self
11550 .attention_heads_per_layer
11551 .as_ref()
11552 .and_then(|h| h.get(li).copied())
11553 .unwrap_or(self.num_heads);
11554 if nkv == 0 || nh % nkv != 0 {
11555 return Err(format!(
11556 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11557 ));
11558 }
11559 }
11560 }
11561 self.kv_heads_per_layer = kv_heads_per_layer;
11562 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11563 if self.kv_heads_per_layer.is_some() {
11564 for li in 0..self.kv_cache.layers.len() {
11565 let full = matches!(
11566 self.weights
11567 .layers
11568 .get(self.phys_layer(li))
11569 .map(|lw| &lw.attn),
11570 Some(AttnKind::Full { .. })
11571 );
11572 let nkv = self.layer_num_kv_heads(li);
11573 let cache = &self.kv_cache.layers[li];
11574 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11575 let sinks = cache.sinks.clone();
11576 self.kv_cache.layers[li] =
11577 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11578 self.kv_cache.layers[li].sinks = sinks;
11579 }
11580 }
11581 }
11582 Ok(())
11583 }
11584
11585 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11589 let Some(lw) = self.weights.layers.get(phys) else {
11590 return Err(format!("sinks for layer {phys}: no such layer"));
11591 };
11592 if !matches!(lw.attn, AttnKind::Full { .. }) {
11593 return Err(format!(
11594 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11595 ));
11596 }
11597 let nh = self
11598 .attention_heads_per_layer
11599 .as_ref()
11600 .and_then(|h| h.get(phys).copied())
11601 .unwrap_or(self.num_heads);
11602 if sinks.len() != nh {
11603 return Err(format!(
11604 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11605 sinks.len()
11606 ));
11607 }
11608 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11609 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11610 }
11611 for li in 0..self.kv_cache.layers.len() {
11612 if self.phys_layer(li) == phys {
11613 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11614 }
11615 }
11616 Ok(())
11617 }
11618
11619 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11629 if self.kv_heads_per_layer.is_some() {
11630 return Some("per-layer KV head counts");
11631 }
11632 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11633 return Some("V heads narrower than Q/K heads");
11634 }
11635 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11636 return Some("learned attention sinks");
11637 }
11638 if self.swa.is_some() || self.sliding_layers.is_some() {
11639 return Some("sliding-window layers");
11640 }
11641 None
11642 }
11643
11644 #[cfg(target_os = "macos")]
11652 fn metal_rope_scale_ok(&self) -> bool {
11653 !(self.qk_norm_after_rope
11654 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11655 }
11656
11657 #[cfg(target_os = "macos")]
11674 fn metal_graph_swa(&self) -> bool {
11675 (self.swa.is_some() || self.sliding_layers.is_some())
11676 && self.attention_heads_per_layer.is_none()
11677 && !self.attn_v_norm
11678 && self.attn_softcap == 0.0
11679 && self.kv_heads_per_layer.is_none()
11680 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11681 && self.global_attn.is_none()
11682 && self.inv_freq_global.is_none()
11683 && self.metal_rope_scale_ok()
11684 && !(0..self.num_layers).any(|li| {
11685 self.layer_is_local(li)
11686 && self.inv_freq_local.is_none()
11687 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11688 })
11689 }
11690
11691 #[cfg(target_os = "macos")]
11699 fn metal_gemma4(&self) -> bool {
11700 crate::gpu_metal::gemma4::enabled()
11701 && (self.global_attn.is_some() || self.attn_v_norm)
11702 && self.attention_heads_per_layer.is_none()
11703 && self.kv_heads_per_layer.is_none()
11704 && self.v_head_dim.is_none()
11705 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11706 && self.attn_softcap == 0.0
11707 && !self.qk_norm_after_rope
11708 && self.metal_rope_scale_ok()
11709 && self
11710 .weights
11711 .layers
11712 .iter()
11713 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11714 && !(0..self.num_layers).any(|li| {
11715 self.layer_is_local(li)
11716 && self.inv_freq_local.is_none()
11717 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11718 })
11719 }
11720
11721 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11728 let geometry_reason = self.graph_attn_decline_reason();
11732 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11733 if has_scaled_rope && geometry_reason.is_none() {
11734 return Some("scaled RoPE without per-layer geometry");
11735 }
11736 if has_scaled_rope && self.qk_norm_after_rope {
11741 return Some("scaled RoPE with post-RoPE Q/K norm");
11742 }
11743 geometry_reason?;
11744 if self.attention_heads_per_layer.is_some() {
11750 return Some("per-layer Q head counts with per-layer geometry");
11751 }
11752 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11756 if !gemma4
11757 && self.weights.layers.iter().any(|lw| {
11758 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11759 })
11760 {
11761 return Some("sandwich norms / layer scale with per-layer geometry");
11762 }
11763 if self.weights.layers.iter().any(|lw| {
11767 matches!(
11768 &lw.attn,
11769 AttnKind::Full {
11770 output_gate: true,
11771 ..
11772 }
11773 )
11774 }) && self.v_head_dim.is_some()
11775 {
11776 return Some("gated attention with V narrower than K");
11777 }
11778 if (0..self.num_layers).any(|li| {
11779 self.layer_is_local(li)
11780 && self.inv_freq_local.is_none()
11781 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11782 }) {
11783 return Some("local rotary width without a local RoPE table");
11784 }
11785 None
11786 }
11787
11788 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11792 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11795 return None;
11796 }
11797 if self.global_attn.is_some() {
11798 return Some("per-layer head width (Gemma-4 global layers)");
11799 }
11800 if self.attn_v_norm {
11801 return Some("V norm");
11802 }
11803 if self.weights.layers.iter().any(|lw| {
11804 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11805 }) {
11806 return Some("sandwich norms / layer scale");
11807 }
11808 None
11809 }
11810
11811 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11816 self.graph_attn_decline_reason()?;
11817 let (nkv, hd, rd) = self.layer_geom(li);
11818 let invf: &[f32] = if self.layer_is_local(li) {
11819 match &self.inv_freq_local {
11820 Some(f) => f.as_slice(),
11821 None => self.inv_freq.as_slice(),
11822 }
11823 } else {
11824 match &self.inv_freq_global {
11825 Some(f) => f.as_slice(),
11826 None => self.inv_freq.as_slice(),
11827 }
11828 };
11829 Some(crate::gpu::GraphAttnGeom {
11830 nkv,
11831 hd,
11832 dv: self.layer_v_dim(li),
11833 rd,
11834 invf,
11835 rope_scale: self.layer_rope_scale(li),
11836 window: self.layer_window(li),
11837 sink: self.kv_cache.layers[li].sinks.as_deref(),
11838 v_norm: self.attn_v_norm,
11839 })
11840 }
11841
11842 #[cfg(feature = "gpu")]
11854 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11855 if !self.proj_gate_sigmoid
11856 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11857 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11858 || self.graph_refused()
11859 || self.o1_active()
11860 || self.attn_softcap > 0.0
11861 || self.wgpu_graph_attn_decline().is_some()
11862 {
11863 return None;
11864 }
11865 let g = self.graph_attn_geom(li)?;
11866 let (nkv, hd, _) = self.layer_geom(li);
11867 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11869 return None;
11870 }
11871 Some(crate::gpu::PrefillMirror {
11872 kv_id: self.graph_kv_id,
11873 layer: li,
11874 limit: self.kv_cache.max_seq_len,
11875 })
11876 }
11877
11878 #[cfg(feature = "gpu")]
11887 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11888 let kv_id = self.graph_kv_id;
11889 for li in from..upto.min(self.num_layers) {
11890 if !matches!(
11891 self.weights.layers[self.phys_layer(li)].attn,
11892 AttnKind::Full { .. }
11893 ) {
11894 continue;
11895 }
11896 let host = self.kv_cache.layers[li].pos_len();
11898 if host >= position {
11899 continue;
11900 }
11901 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11902 continue;
11903 };
11904 let to = dev.min(position);
11905 if to <= host {
11906 continue;
11907 }
11908 let (nkv, hd) = {
11909 let c = &self.kv_cache.layers[li];
11910 (c.num_kv_heads, c.head_dim)
11911 };
11912 let Some((k, v, first_valid)) =
11913 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11914 else {
11915 continue;
11916 };
11917 let need_from = match self.layer_window(li) {
11920 Some(w) => host.max((position + 1).saturating_sub(w)),
11921 None => host,
11922 };
11923 if first_valid > need_from {
11924 tracing::warn!(
11925 "layer {li}: device KV rows {host}..{to} no longer resident \
11926 (from {first_valid}); host attention will miss them"
11927 );
11928 }
11929 let row = nkv * hd;
11930 let cache = &mut self.kv_cache.layers[li];
11931 for p in 0..to - host {
11932 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11933 }
11934 }
11935 }
11936
11937 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11940 let mut seen = self.graph_declines.borrow_mut();
11941 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11942 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11943 seen.push((site, reason));
11944 }
11945 }
11946
11947 pub fn graph_declines(&self) -> Vec<String> {
11950 self.graph_declines
11951 .borrow()
11952 .iter()
11953 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11954 .collect()
11955 }
11956
11957 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11966 let Some(dir) = &self.layer_dump else {
11967 return;
11968 };
11969 let mut bytes = Vec::with_capacity(row.len() * 4);
11970 for v in row {
11971 bytes.extend_from_slice(&v.to_le_bytes());
11972 }
11973 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11974 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11975 use std::sync::atomic::{AtomicBool, Ordering};
11976 static SAID: AtomicBool = AtomicBool::new(false);
11977 if !SAID.swap(true, Ordering::Relaxed) {
11978 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11979 }
11980 }
11981 }
11982
11983 fn mimo_moe_prepare(&mut self) {
11986 if !self.mimo_moe.is_undecided() {
11987 return;
11988 }
11989 let slot = {
11990 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11991 .filter_map(
11992 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11993 FfnKind::Moe(m) => Some((li, m)),
11994 _ => None,
11995 },
11996 )
11997 .collect();
11998 if layers.is_empty()
12001 || self.physical_layers != self.num_layers
12002 || self.gpu_plan.is_some()
12003 {
12004 crate::mimo_moe::Slot::Off
12005 } else {
12006 let graph_prefix =
12010 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12011 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12012 }
12013 };
12014 self.mimo_moe = slot;
12015 }
12016
12017 #[cfg(test)]
12018 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12019 self.graph_kv_id
12020 }
12021
12022 pub(crate) fn mimo_graph_layer_rows(
12026 &mut self,
12027 li: usize,
12028 h: &mut [f32],
12029 positions: &[usize],
12030 ) -> crate::gpu::BatchGraphOutcome {
12031 use crate::gpu::BatchGraphOutcome as Out;
12032 let b = positions.len();
12033 if !(1..=4).contains(&b)
12034 || h.len() != b * self.hidden_size
12035 || !self.mimo_moe.is_dynamic(li, true)
12036 || !crate::gpu::enabled_here()
12037 || !crate::gpu::wgpu_active()
12038 || self.o1_active()
12039 || self.physical_layers != self.num_layers
12040 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12044 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12045 || self.wgpu_graph_attn_decline().is_some()
12046 {
12047 return Out::Declined;
12048 }
12049 let attn_started = std::time::Instant::now();
12050 let outcome = {
12051 let lw = &self.weights.layers[li];
12052 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12053 return Out::Declined;
12054 }
12055 let FfnKind::Moe(m) = &lw.ffn else {
12056 return Out::Declined;
12057 };
12058 let AttnKind::Full {
12059 wq,
12060 wk,
12061 wv,
12062 wo,
12063 q_norm,
12064 k_norm,
12065 output_gate,
12066 softplus_gate,
12067 bias,
12068 } = &lw.attn
12069 else {
12070 return Out::Declined;
12071 };
12072 if *output_gate || softplus_gate.is_some() {
12073 return Out::Declined;
12074 }
12075 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12076 m.experts
12077 .first()?
12078 .gate_proj
12079 .mapped_q4tp()
12080 .map(|(m, _)| m.clone())
12081 }) else {
12082 return Out::Declined;
12083 };
12084 fn gw<'a>(
12085 t: &'a QTensor,
12086 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12087 ) -> Option<crate::gpu::GraphW<'a>> {
12088 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12089 if m.uid() != owner.uid() || t.has_prism_contract() {
12090 return None;
12091 }
12092 return Some(crate::gpu::GraphW {
12093 idx,
12094 kind,
12095 row_scale: rs,
12096 data: &[],
12097 prism: crate::gpu::GraphPrismOp::None,
12098 affine: false,
12099 });
12100 }
12101 t.as_f32().map(|data| crate::gpu::GraphW {
12102 idx: 0,
12103 kind: 4,
12104 row_scale: &[],
12105 data,
12106 prism: crate::gpu::GraphPrismOp::None,
12107 affine: false,
12108 })
12109 }
12110 let (Some(q), Some(k), Some(v), Some(o)) = (
12111 gw(wq, &model),
12112 gw(wk, &model),
12113 gw(wv, &model),
12114 gw(wo, &model),
12115 ) else {
12116 return Out::Declined;
12117 };
12118 let layer = crate::gpu::GraphLayer {
12119 input_norm: &lw.input_norm,
12120 post_norm: &lw.post_norm,
12121 attn_out_norm: None,
12122 ffn_out_norm: None,
12123 layer_scale: None,
12124 ffn: crate::gpu::GraphFfn::AttentionOnly,
12125 attn: crate::gpu::GraphAttn::Full {
12126 wq: q,
12127 wk: k,
12128 wv: v,
12129 wo: o,
12130 q_norm: q_norm.as_deref(),
12131 k_norm: k_norm.as_deref(),
12132 late_qk_norm: self.qk_norm_after_rope,
12133 bias: bias
12134 .as_ref()
12135 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12136 output_gate: false,
12137 cpu_k: self.kv_cache.layers[li].k_heads(),
12138 cpu_v: self.kv_cache.layers[li].v_heads(),
12139 cpu_base: self.kv_cache.layers[li].base(),
12140 geom: self.graph_attn_geom(li),
12141 head_gate: None,
12142 },
12143 };
12144 let (nkv, hd, rd) = self.layer_geom(li);
12145 crate::gpu::forward_batch_graph_at(
12146 &model,
12147 self.graph_kv_id,
12148 li,
12149 &[layer],
12150 &self.inv_freq,
12151 h,
12152 self.layer_num_heads(li),
12153 nkv,
12154 hd,
12155 rd,
12156 self.hidden_size,
12157 1,
12158 positions,
12159 self.kv_cache.max_seq_len,
12160 self.norm_style == cortiq_core::NormStyle::Gemma,
12161 self.rms_eps as f32,
12162 self.attn_scale,
12163 b,
12164 &[],
12165 self.o1_epoch,
12166 None,
12167 None,
12168 )
12169 };
12170 match outcome {
12171 Out::Completed => {}
12172 Out::Declined => return Out::Declined,
12173 Out::Failed => {
12174 self.graph_failed
12175 .store(true, std::sync::atomic::Ordering::Relaxed);
12176 return Out::Failed;
12177 }
12178 }
12179 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12180 let hs = self.hidden_size;
12181 let lw = &self.weights.layers[li];
12182 let FfnKind::Moe(m) = &lw.ffn else {
12183 unreachable!()
12184 };
12185 let mut post = vec![0.0; h.len()];
12186 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12187 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12188 }
12189 let mut ffn = if b == 1 {
12190 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12191 } else {
12192 moe_ffn_banked_rows(
12193 &mut self.mimo_moe,
12194 li,
12195 m,
12196 &post,
12197 b,
12198 hs,
12199 self.pool.as_deref(),
12200 )
12201 };
12202 for (x, &f) in h.iter_mut().zip(&ffn) {
12203 *x += f;
12204 }
12205 attention::recycle_buf(&mut ffn);
12206 if self.layer_dump.is_some() {
12207 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12208 self.dump_layer_row(pos, li, row);
12209 }
12210 }
12211 crate::mimo_moe::note_attention_graph(b, attn_ns);
12212 Out::Completed
12213 }
12214
12215 fn layer_attn_plain(&self, li: usize) -> bool {
12216 self.kv_heads_per_layer.is_none()
12217 && self.v_head_dim.is_none()
12218 && self.global_attn.is_none()
12219 && self.layer_window(li).is_none()
12220 && self.kv_cache.layers[li].sinks.is_none()
12221 }
12222
12223 fn forward_layers(
12225 &mut self,
12226 hidden: &[f32],
12227 position: usize,
12228 task_mask: Option<&TaskMask>,
12229 ) -> Vec<f32> {
12230 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12231 self.o1_progress();
12232 self.swa_trim_tails();
12233 out
12234 }
12235
12236 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12244 self.embed_single(id)
12245 }
12246
12247 pub fn split_supported(&self) -> Result<(), String> {
12251 if self.dsv4.is_some() {
12252 return Err(
12253 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12254 );
12255 }
12256 if self.dsv41.is_some() {
12257 return Err(
12258 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12259 .into(),
12260 );
12261 }
12262 if self.qwen4_exp.is_some() {
12263 return Err(
12264 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12265 );
12266 }
12267 if self.per_token_stack() {
12268 return Err(
12269 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12270 );
12271 }
12272 Ok(())
12273 }
12274
12275 pub fn forward_span(
12280 &mut self,
12281 hidden: &[f32],
12282 position: usize,
12283 from: usize,
12284 upto: usize,
12285 task_mask: Option<&TaskMask>,
12286 ) -> Result<Vec<f32>, String> {
12287 self.split_supported()?;
12288 if from > upto || upto >= self.num_layers {
12289 return Err(format!(
12290 "forward_span: layer range {from}..={upto} outside 0..{}",
12291 self.num_layers
12292 ));
12293 }
12294 if hidden.len() != self.hidden_size {
12295 return Err(format!(
12296 "forward_span: hidden len {} ≠ hidden_size {}",
12297 hidden.len(),
12298 self.hidden_size
12299 ));
12300 }
12301 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12302 self.o1_progress();
12303 self.swa_trim_tails();
12304 if self
12305 .graph_failed
12306 .swap(false, std::sync::atomic::Ordering::Relaxed)
12307 {
12308 self.cancel
12309 .store(false, std::sync::atomic::Ordering::Relaxed);
12310 self.clear_sequence_state();
12311 return Err("forward_span: deferred O(1) transition failed".into());
12312 }
12313 Ok(out)
12314 }
12315
12316 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12319 let normed = inference::rms_norm(
12320 hidden,
12321 &self.weights.final_norm,
12322 self.rms_eps,
12323 self.norm_style,
12324 );
12325 self.lm_head_forward(&normed)
12326 }
12327
12328 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12330 sampler::sample_with_scratch(
12331 logits,
12332 &self.sampler_config,
12333 past_tokens,
12334 &mut self.rng,
12335 &mut self.sampler_scratch,
12336 )
12337 }
12338
12339 pub fn reset_session(&mut self) {
12341 self.clear_sequence_state();
12342 }
12343
12344 pub fn prefill_span_ids(
12350 &mut self,
12351 ids: &[u32],
12352 start_pos: usize,
12353 upto: usize,
12354 task_mask: Option<&TaskMask>,
12355 ) -> Result<Vec<f32>, String> {
12356 self.split_supported()?;
12357 if upto >= self.num_layers {
12358 return Err(format!(
12359 "prefill_span_ids: upto {upto} outside 0..{}",
12360 self.num_layers
12361 ));
12362 }
12363 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12367 let out =
12368 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12369 self.check_o1_progress_failure("prefill_span_ids")?;
12370 Ok(out)
12371 } else {
12372 let hs = self.hidden_size;
12373 let mut out = Vec::with_capacity(ids.len() * hs);
12374 for (i, &id) in ids.iter().enumerate() {
12375 let emb = self.embed_id(id);
12376 out.extend_from_slice(&self.forward_span(
12377 &emb,
12378 start_pos + i,
12379 0,
12380 upto,
12381 task_mask,
12382 )?);
12383 }
12384 Ok(out)
12385 }
12386 }
12387
12388 pub fn prefill_span_hidden(
12391 &mut self,
12392 hidden: &[f32],
12393 start_pos: usize,
12394 from: usize,
12395 upto: usize,
12396 task_mask: Option<&TaskMask>,
12397 ) -> Result<Vec<f32>, String> {
12398 self.split_supported()?;
12399 let hs = self.hidden_size;
12400 if hidden.is_empty() || hidden.len() % hs != 0 {
12401 return Err(format!(
12402 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12403 hidden.len()
12404 ));
12405 }
12406 if from > upto || upto >= self.num_layers {
12407 return Err(format!(
12408 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12409 self.num_layers
12410 ));
12411 }
12412 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12413 let out = self.prefill_batch_span(
12414 PrefillIn::Hidden(hidden),
12415 start_pos,
12416 task_mask,
12417 from,
12418 upto + 1,
12419 );
12420 self.check_o1_progress_failure("prefill_span_hidden")?;
12421 Ok(out)
12422 } else {
12423 let b = hidden.len() / hs;
12424 let mut out = Vec::with_capacity(hidden.len());
12425 for i in 0..b {
12426 let h = self.forward_span(
12427 &hidden[i * hs..(i + 1) * hs],
12428 start_pos + i,
12429 from,
12430 upto,
12431 task_mask,
12432 )?;
12433 out.extend_from_slice(&h);
12434 }
12435 Ok(out)
12436 }
12437 }
12438
12439 fn try_token_graph_wgpu(
12443 &self,
12444 hidden: &[f32],
12445 position: usize,
12446 logits_out: &mut Vec<f32>,
12447 layers_run: &mut usize,
12448 ) -> Option<Result<Vec<f32>, ()>> {
12449 self.try_token_graph_wgpu_steps(
12450 hidden,
12451 position,
12452 logits_out,
12453 1,
12454 None,
12455 Some(layers_run),
12456 0,
12457 self.num_layers,
12458 )
12459 }
12460
12461 fn try_token_graph_wgpu_span(
12465 &self,
12466 hidden: &[f32],
12467 position: usize,
12468 logits_out: &mut Vec<f32>,
12469 from: usize,
12470 upto_excl: usize,
12471 layers_run: &mut usize,
12472 ) -> Option<Result<Vec<f32>, ()>> {
12473 self.try_token_graph_wgpu_steps(
12474 hidden,
12475 position,
12476 logits_out,
12477 1,
12478 None,
12479 Some(layers_run),
12480 from,
12481 upto_excl,
12482 )
12483 }
12484
12485 #[cfg(feature = "gpu")]
12489 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12490 if !crate::minicpm_tail::minimax_pool_on()
12491 || !crate::gpu::enabled_here()
12492 || self.head_clusters.is_some()
12493 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12494 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12495 {
12496 return None;
12497 }
12498 let (model, i, kind, rs) = self
12499 .weights
12500 .lm_head
12501 .graph_weight()
12502 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12503 let name = &model.tensors[i].name;
12504 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12505 crate::gpu::GraphPrismOp::InverseEmbedding
12506 } else if crate::prism::is_forward_weight(&model, name) {
12507 crate::gpu::GraphPrismOp::Forward
12508 } else {
12509 crate::gpu::GraphPrismOp::None
12510 };
12511 let gw = crate::gpu::GraphW {
12512 idx: i,
12513 kind,
12514 row_scale: rs,
12515 data: &[],
12516 prism,
12517 affine: crate::prism::is_affine_target(&model, name),
12518 };
12519 let rows = self.weights.lm_head.rows();
12520 let mut h = hidden.to_vec();
12521 let mut lg = Vec::new();
12522 let mut layers_run = 0usize;
12523 let outcome = crate::gpu::forward_token_graph(
12524 &model,
12525 self.graph_kv_id,
12526 &[],
12527 &[],
12528 self.o1_epoch,
12529 &self.inv_freq,
12530 &mut h,
12531 self.num_heads,
12532 self.num_kv_heads,
12533 self.head_dim,
12534 self.attn_scale,
12535 self.head_dim,
12536 self.hidden_size,
12537 self.intermediate_size,
12538 0,
12539 self.kv_cache.max_seq_len,
12540 self.norm_style == cortiq_core::NormStyle::Gemma,
12541 self.rms_eps as f32,
12542 Some((&gw, rows)),
12543 &self.weights.final_norm,
12544 &mut lg,
12545 &[],
12546 1,
12547 None,
12548 None,
12549 Some(&mut layers_run),
12550 0,
12551 false,
12552 );
12553 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12554 return None;
12555 }
12556 lg.resize(self.vocab_size, 0.0);
12559 if let Some(m) = self.logit_multiplier {
12560 for l in lg.iter_mut() {
12561 *l *= m;
12562 }
12563 }
12564 if let Some(c) = self.final_softcap {
12565 softcap_in_place(&mut lg, c, self.pool.as_deref());
12566 }
12567 Some(lg)
12568 }
12569
12570 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12573 if winners.is_empty() {
12574 return;
12575 }
12576 if self.m3_n_exp == 0 {
12577 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12578 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12579 }
12580 for &e in winners {
12581 let e = e as usize;
12582 if e < self.m3_n_exp && li < self.num_layers {
12583 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12584 }
12585 }
12586 if li + 1 == self.num_layers {
12587 self.m3_tokens += 1;
12588 if self.m3_tokens % 16 == 0 {
12589 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12590 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12591 let _ = std::fs::write(&path, bytes);
12592 }
12593 }
12594 }
12595 }
12596
12597 fn m3_prewarm(&mut self) {
12602 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12603 return;
12604 };
12605 let Ok(bytes) = std::fs::read(&path) else {
12606 tracing::warn!("m3 prewarm: cannot read profile {path}");
12607 return;
12608 };
12609 let counts: Vec<u32> = bytes
12610 .chunks_exact(4)
12611 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12612 .collect();
12613 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12614 if counts.len() != self.num_layers * n_exp {
12615 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12616 return;
12617 }
12618 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12619 .ok()
12620 .and_then(|v| v.parse().ok())
12621 .unwrap_or(300);
12622 let mut ranked: Vec<(u32, usize, usize)> = counts
12623 .iter()
12624 .enumerate()
12625 .filter(|(_, c)| **c > 0)
12626 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12627 .collect();
12628 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12629 ranked.truncate(limit);
12630 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12631 for (_, li, e) in ranked {
12632 per_layer[li].push(e);
12633 }
12634 let mut admitted = 0usize;
12635 for (li, ids) in per_layer.iter().enumerate() {
12636 if ids.is_empty() {
12637 continue;
12638 }
12639 let lw = &self.weights.layers[self.phys_layer(li)];
12640 let FfnKind::Moe(m) = &lw.ffn else {
12641 continue;
12642 };
12643 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12645 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12646 admitted += ids.len();
12647 }
12648 }
12649 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12650 }
12651
12652 #[cfg(feature = "gpu")]
12663 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12664 if self.o1_active() || self.attn_softcap > 0.0 {
12665 return Ok(false);
12666 }
12667 if !self.m3_prewarmed {
12668 self.m3_prewarmed = true;
12669 self.m3_prewarm();
12670 }
12671 let frame = {
12672 let lw = &self.weights.layers[self.phys_layer(li)];
12673 let FfnKind::Moe(m) = &lw.ffn else {
12674 return Ok(false);
12675 };
12676 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12677 return Ok(false);
12678 }
12679 let t_arm = std::time::Instant::now();
12680 if self.m3_winners.len() != self.num_layers {
12681 self.m3_winners = vec![Vec::new(); self.num_layers];
12682 }
12683 let pred = std::mem::take(&mut self.m3_winners[li]);
12684 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12685 if armed.is_none() {
12686 self.m3_winners[li] = pred; }
12688 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12689 match armed {
12690 Some(frame) => frame,
12691 None => return Ok(false),
12694 }
12695 };
12696 crate::gpu_wgpu::pooled_frame_enter(frame);
12697 let want_logits = self.graph_want_logits;
12701 self.graph_want_logits = false;
12702 let mut lg = Vec::new();
12703 let mut gl = 0usize;
12704 let t_span = std::time::Instant::now();
12705 let span = crate::gpu::gpu_scope(|| {
12706 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12707 });
12708 self.graph_want_logits = want_logits;
12709 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12710 let hh = match span {
12711 Some(Ok(hh)) => hh,
12712 Some(Err(())) => return Err(()),
12713 None => {
12714 return Ok(false);
12717 }
12718 };
12719 if gl != 1 {
12720 return if gl == 0 { Ok(false) } else { Err(()) };
12723 }
12724 *h = hh;
12725 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12726 self.m3_note_route(li, &winners);
12727 if self.m3_winners.len() != self.num_layers {
12728 self.m3_winners = vec![Vec::new(); self.num_layers];
12729 }
12730 self.m3_winners[li] = winners;
12731 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12732 cold.retain(|&(_, w)| {
12735 if w.is_finite() {
12736 true
12737 } else {
12738 tracing::error!("pooled tail: non-finite cold weight from expert route");
12739 false
12740 }
12741 });
12742 if cold.is_empty() {
12743 return Ok(true);
12744 }
12745 let x = crate::gpu_wgpu::pooled_frame_take_input();
12746 if x.len() != self.hidden_size {
12747 return Err(());
12748 }
12749 let lw = &self.weights.layers[self.phys_layer(li)];
12755 let FfnKind::Moe(m) = &lw.ffn else {
12756 return Err(());
12757 };
12758 let t_cold = std::time::Instant::now();
12759 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12760 let mut out = crate::attention::take_buf(self.hidden_size);
12761 let on_device = !force_host
12762 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12763 if !on_device {
12764 crate::attention::recycle_buf(&mut out);
12765 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12766 .iter()
12767 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12768 .collect::<Result<_, ()>>()?;
12769 out = crate::gpu::cpu_scope(|| {
12770 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12771 });
12772 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12773 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12774 }
12775 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12776 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12777 if out.len() != h.len() {
12778 crate::attention::recycle_buf(&mut out);
12779 return Err(());
12780 }
12781 for (dst, src) in h.iter_mut().zip(out.iter()) {
12782 *dst += src;
12783 }
12784 crate::attention::recycle_buf(&mut out);
12785 if on_device {
12786 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12787 }
12788 Ok(true)
12789 }
12790
12791 #[cfg(not(feature = "gpu"))]
12792 fn m3_pooled_layer(
12793 &mut self,
12794 _li: usize,
12795 _position: usize,
12796 _h: &mut Vec<f32>,
12797 ) -> Result<bool, ()> {
12798 Ok(false)
12799 }
12800
12801 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12805 if self.o1_active() || self.attn_softcap > 0.0 {
12806 return None;
12807 }
12808 if let Some(reason) = self.wgpu_graph_attn_decline() {
12811 self.note_graph_decline("wgpu multi-burst", reason);
12812 return None;
12813 }
12814 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12815 if !graph_on || self.graph_refused() {
12816 return None;
12823 }
12824 let emb = self.embed_single(t_next);
12825 let mut lg = Vec::new();
12826 let mut ids = Vec::new();
12827 match self.try_token_graph_wgpu_steps(
12828 &emb,
12829 position,
12830 &mut lg,
12831 k,
12832 Some(&mut ids),
12833 None,
12834 0,
12835 self.num_layers,
12836 ) {
12837 Some(Ok(_)) => {}
12838 Some(Err(())) => {
12839 self.graph_failed
12844 .store(true, std::sync::atomic::Ordering::Relaxed);
12845 return None;
12846 }
12847 None => return None,
12848 }
12849 (ids.len() == k).then_some(ids)
12850 }
12851
12852 fn try_token_graph_wgpu_steps(
12856 &self,
12857 hidden: &[f32],
12858 position: usize,
12859 logits_out: &mut Vec<f32>,
12860 steps: usize,
12861 ids_out: Option<&mut Vec<u32>>,
12862 layers_run: Option<&mut usize>,
12863 from: usize,
12864 upto_excl: usize,
12865 ) -> Option<Result<Vec<f32>, ()>> {
12866 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12869 Some(end) if end < upto_excl => {
12870 if steps != 1 || layers_run.is_none() || from >= end {
12871 return None;
12872 }
12873 end
12874 }
12875 _ => upto_excl,
12876 };
12877 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12880 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12881 return None;
12885 }
12886 if let Some(reason) = self.wgpu_graph_attn_decline() {
12893 self.note_graph_decline("wgpu token graph", reason);
12894 return None;
12895 }
12896 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12901 .map(|li| {
12902 if !o1_gpu {
12903 return None;
12904 }
12905 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12906 })
12907 .collect();
12908 if self.o1_active() && o1_gpu {
12909 let want: usize = (from..upto_excl)
12912 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12913 .count();
12914 let have = o1_views.iter().filter(|v| v.is_some()).count();
12915 if want == 0 || have != want {
12916 use std::sync::atomic::{AtomicUsize, Ordering};
12926 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12927 let code = have * 1000 + want;
12928 if LAST.swap(code, Ordering::Relaxed) != code {
12929 tracing::warn!(
12930 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12931 );
12932 }
12933 return None;
12934 }
12935 }
12936 let nh = self.num_heads;
12937 let (nkv, hd, rd) = self.layer_geom(0);
12938 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12939 let mut layers = Vec::with_capacity(upto_excl - from);
12940 let mut model = None;
12941 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12942 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12943 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12945 let name = &m.tensors[i].name;
12946 let prism = if crate::prism::is_inverse_embedding(m, name) {
12947 crate::gpu::GraphPrismOp::InverseEmbedding
12948 } else if crate::prism::is_forward_weight(m, name) {
12949 crate::gpu::GraphPrismOp::Forward
12950 } else {
12951 crate::gpu::GraphPrismOp::None
12952 };
12953 return Some(crate::gpu::GraphW {
12954 idx: i,
12955 kind,
12956 row_scale: rs,
12957 data: &[],
12958 prism,
12959 affine: crate::prism::is_affine_target(m, name),
12960 });
12961 }
12962 match t.as_f32() {
12964 Some(d) => Some(crate::gpu::GraphW {
12965 idx: 0,
12966 kind: 4,
12967 row_scale: &[],
12968 data: d,
12969 prism: crate::gpu::GraphPrismOp::None,
12970 affine: false,
12971 }),
12972 None => {
12973 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12974 eprintln!("batch graph: weight has no graph/f32 representation");
12975 }
12976 None
12977 }
12978 }
12979 }
12980 for li in from..upto_excl {
12981 let lw = &self.weights.layers[self.phys_layer(li)];
12982 if dbg {
12983 let ak = match &lw.attn {
12984 AttnKind::Mla(_) => "Mla".into(),
12985 AttnKind::Full {
12986 output_gate, bias, ..
12987 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12988 AttnKind::LinearGdn(_) => "LinearGdn".into(),
12989 AttnKind::Kda(_) => "Kda".into(),
12990 AttnKind::Linear(_) => "Linear".into(),
12991 AttnKind::ShortConv(_) => "ShortConv".into(),
12992 AttnKind::Bounded(_) => "Bounded".into(),
12993 };
12994 let fk = match &lw.ffn {
12995 FfnKind::Dense(_) => "Dense",
12996 FfnKind::Moe(_) => "Moe",
12997 FfnKind::DenseMoe(_) => "DenseMoe",
12998 };
12999 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13000 }
13001 let gffn = match &lw.ffn {
13002 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13006 FfnKind::Dense(d) => {
13007 let Some(act) = d.act.wgpu_graph_act() else {
13011 self.note_graph_decline(
13012 "wgpu token graph",
13013 "dense FFN activation without a graph kernel",
13014 );
13015 return None;
13016 };
13017 crate::gpu::GraphFfn::Dense {
13018 gate: gw(&d.gate_proj)?,
13019 up: gw(&d.up_proj)?,
13020 down: gw(&d.down_proj)?,
13021 act,
13022 }
13023 }
13024 FfnKind::Moe(m) => {
13025 if m.route_tau.is_some() || m.mask.is_some() {
13033 return None;
13034 }
13035 let shared = m.shared.as_ref();
13036 let inter = m.experts.first()?.gate_proj.rows();
13042 let detached_shared = match shared {
13050 Some((se, None))
13051 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13052 {
13053 if !matches!(se.act, Act::Silu)
13054 || se.gate_proj.mapped_q4tp().is_none()
13055 || se.up_proj.mapped_q4tp().is_none()
13056 || se.down_proj.mapped_q4tp().is_none()
13057 {
13058 return None;
13059 }
13060 Some(crate::gpu::GraphMoeDetachedShared {
13061 gate: gw(&se.gate_proj)?,
13062 up: gw(&se.up_proj)?,
13063 down: gw(&se.down_proj)?,
13064 width: se.gate_proj.rows(),
13065 })
13066 }
13067 Some((se, Some(_)))
13071 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13072 {
13073 return None;
13074 }
13075 _ => None,
13076 };
13077 let has_shared = shared.is_some() && detached_shared.is_none();
13078 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13079 let sgate = match shared {
13080 Some((_, Some(sg))) if has_shared => gw(sg)?,
13081 _ => gw(&m.router)?,
13085 };
13086 let router = gw(&m.router)?;
13087 if router.prism != crate::gpu::GraphPrismOp::None
13093 || sgate.prism != crate::gpu::GraphPrismOp::None
13094 || router.affine
13095 || sgate.affine
13096 {
13097 tracing::warn!(
13098 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13099 );
13100 return None;
13101 }
13102 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13103 let mut q4tp: Option<bool> = None;
13106 let mut gu_q2: Option<bool> = None;
13109 let mut q82: Option<bool> = None;
13111 let mut moe_act = None;
13115 for e in m
13116 .experts
13117 .iter()
13118 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13119 {
13120 let Some(e_act) = e.act.wgpu_graph_act() else {
13121 return None;
13122 };
13123 match moe_act {
13124 Some(existing) if existing != e_act => return None,
13125 None => moe_act = Some(e_act),
13126 _ => {}
13127 }
13128 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13129 return None;
13130 }
13131 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13136 let Some((em, ei, _, _)) = expert_weight
13137 .graph_weight()
13138 .or_else(|| expert_weight.graph_weight_descriptor())
13139 else {
13140 return None;
13141 };
13142 let name = &em.tensors[ei].name;
13143 if crate::prism::is_forward_weight(em, name)
13144 || crate::prism::is_inverse_embedding(em, name)
13145 || crate::prism::is_affine_target(em, name)
13146 {
13147 tracing::warn!(
13148 "resident MoE declined: expert Prism/affine transform is not implemented"
13149 );
13150 return None;
13151 }
13152 }
13153 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13154 Some((mm, gi)) => (
13155 mm,
13156 gi,
13157 e.up_proj.mapped_q4t()?.1,
13158 e.down_proj.mapped_q4t()?.1,
13159 false,
13160 false,
13161 false,
13162 ),
13163 None => match e.gate_proj.mapped_q2tp() {
13164 Some((mm, gi)) => (
13165 mm,
13166 gi,
13167 e.up_proj.mapped_q2tp()?.1,
13168 e.down_proj.mapped_q4tp()?.1,
13169 true,
13170 true,
13171 false,
13172 ),
13173 None => match e.gate_proj.mapped_q4tp() {
13174 Some((mm, gi)) => (
13175 mm,
13176 gi,
13177 e.up_proj.mapped_q4tp()?.1,
13178 e.down_proj.mapped_q4tp()?.1,
13179 true,
13180 false,
13181 false,
13182 ),
13183 None => {
13186 if !crate::gpu::moe_q82_graph_on() {
13187 return None;
13188 }
13189 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13190 (
13191 mm,
13192 gi,
13193 e.up_proj.mapped_q8_2f()?.1,
13194 e.down_proj.mapped_q8_2f()?.1,
13195 false,
13196 false,
13197 true,
13198 )
13199 }
13200 },
13201 },
13202 };
13203 if *q4tp.get_or_insert(is_p) != is_p
13204 || *gu_q2.get_or_insert(is_q2) != is_q2
13205 || *q82.get_or_insert(is_8) != is_8
13206 {
13207 tracing::warn!(
13213 "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."
13214 );
13215 return None;
13216 }
13217 model.get_or_insert_with(|| mm.clone());
13218 experts.push((gi, ui, di));
13219 }
13220 crate::gpu::GraphFfn::Moe {
13221 router,
13222 shared_gate: sgate,
13223 experts,
13224 n_exp: m.experts.len(),
13225 top_k: std::env::var("CMF_TOPK_PROBE")
13231 .ok()
13232 .and_then(|v| v.parse::<usize>().ok())
13233 .filter(|k| *k > 0 && *k <= m.top_k)
13234 .unwrap_or(m.top_k),
13235 inter,
13236 act: moe_act?,
13237 norm_topk: m.norm_topk_prob,
13238 q4tp: q4tp?,
13239 gu_q2: gu_q2.unwrap_or(false),
13240 q82: q82.unwrap_or(false),
13241 sigmoid: m.router_sigmoid,
13242 sigmoid_denom_eps: m.sigmoid_denom_eps,
13243 bias: m.expert_bias.as_deref(),
13244 has_shared,
13245 detached_shared,
13246 shared_gated,
13247 route_scale: m.routed_scaling,
13248 }
13249 }
13250 };
13251 let attn = match &lw.attn {
13252 AttnKind::Full {
13253 wq,
13254 wk,
13255 wv,
13256 wo,
13257 q_norm,
13258 k_norm,
13259 output_gate,
13260 softplus_gate,
13261 bias,
13262 } => {
13263 if self.attention_heads_per_layer.is_some() {
13264 return None;
13265 }
13266 let head_gate = match softplus_gate {
13270 None => None,
13271 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13272 Some(_) => {
13273 self.note_graph_decline(
13274 "wgpu token graph",
13275 "projected softplus / per-element output gate",
13276 );
13277 return None;
13278 }
13279 };
13280 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13281 model = Some(m.clone());
13282 crate::gpu::GraphAttn::Full {
13283 wq: gw(wq)?,
13284 wk: gw(wk)?,
13285 wv: gw(wv)?,
13286 wo: gw(wo)?,
13287 q_norm: q_norm.as_deref(),
13288 k_norm: k_norm.as_deref(),
13289 late_qk_norm: self.qk_norm_after_rope,
13290 bias: bias
13291 .as_ref()
13292 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13293 output_gate: *output_gate,
13294 cpu_k: self.kv_cache.layers[li].k_heads(),
13295 cpu_v: self.kv_cache.layers[li].v_heads(),
13296 cpu_base: self.kv_cache.layers[li].base(),
13297 geom: self.graph_attn_geom(li),
13298 head_gate,
13299 }
13300 }
13301 AttnKind::LinearGdn(w) => {
13302 let cfg = self.gdn_cfg?;
13303 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13304 model = Some(m.clone());
13305 crate::gpu::GraphAttn::Gdn {
13306 qkv: gw(&w.in_proj_qkv)?,
13307 z: gw(&w.in_proj_z)?,
13308 a: gw(&w.in_proj_a)?,
13309 b: gw(&w.in_proj_b)?,
13310 out: gw(&w.out_proj)?,
13311 conv1d: &w.conv1d,
13312 a_log: &w.a_log,
13313 dt_bias: &w.dt_bias,
13314 norm: &w.norm,
13315 nv: cfg.num_v_heads,
13316 nk: cfg.num_k_heads,
13317 dk: cfg.key_head_dim,
13318 dv: cfg.value_head_dim,
13319 kk: cfg.conv_kernel,
13320 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13321 }
13322 }
13323 AttnKind::ShortConv(w) => {
13324 let cfg = self.short_conv_cfg?;
13325 let (m, _, _, _) = w
13326 .in_proj
13327 .graph_weight()
13328 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13329 model = Some(m.clone());
13330 crate::gpu::GraphAttn::ShortConv {
13331 inp: gw(&w.in_proj)?,
13332 out: gw(&w.out_proj)?,
13333 taps: &w.conv,
13334 kernel: cfg.kernel,
13335 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13336 }
13337 }
13338 _ => return None,
13339 };
13340 layers.push(crate::gpu::GraphLayer {
13341 input_norm: &lw.input_norm,
13342 attn,
13343 post_norm: &lw.post_norm,
13344 attn_out_norm: lw.attn_out_norm.as_deref(),
13345 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13346 layer_scale: lw.layer_scale,
13347 ffn: gffn,
13348 });
13349 }
13350 let model = model?;
13351 let lm_gw = if upto_excl == self.num_layers
13357 && self.graph_want_logits
13358 && std::env::var("CMF_GPU_LMHEAD")
13359 .map(|v| v != "0")
13360 .unwrap_or(true)
13361 {
13362 self.weights
13363 .lm_head
13364 .graph_weight()
13365 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13366 .map(|(m, i, kind, rs)| {
13367 let name = &m.tensors[i].name;
13368 let prism = if crate::prism::is_inverse_embedding(m, name) {
13369 crate::gpu::GraphPrismOp::InverseEmbedding
13370 } else if crate::prism::is_forward_weight(m, name) {
13371 crate::gpu::GraphPrismOp::Forward
13372 } else {
13373 crate::gpu::GraphPrismOp::None
13374 };
13375 (
13376 crate::gpu::GraphW {
13377 idx: i,
13378 kind,
13379 row_scale: rs,
13380 data: &[],
13381 prism,
13382 affine: crate::prism::is_affine_target(m, name),
13383 },
13384 self.weights.lm_head.rows(),
13385 )
13386 })
13387 } else {
13388 None
13389 };
13390 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13391 let emb_gw = if steps > 1 {
13393 self.weights
13394 .embed_tokens
13395 .graph_weight()
13396 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13397 .map(|(m, i, kind, rs)| {
13398 let name = &m.tensors[i].name;
13399 let prism = if crate::prism::is_inverse_embedding(m, name) {
13400 crate::gpu::GraphPrismOp::InverseEmbedding
13401 } else if crate::prism::is_forward_weight(m, name) {
13402 crate::gpu::GraphPrismOp::Forward
13403 } else {
13404 crate::gpu::GraphPrismOp::None
13405 };
13406 (
13407 crate::gpu::GraphW {
13408 idx: i,
13409 kind,
13410 row_scale: rs,
13411 data: &[],
13412 prism,
13413 affine: crate::prism::is_affine_target(m, name),
13414 },
13415 self.weights.embed_tokens.rows(),
13416 self.embed_multiplier,
13417 )
13418 })
13419 } else {
13420 None
13421 };
13422
13423 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13429 (from..upto_excl.min(self.num_layers - 1))
13430 .filter(|&li| (li + 1) % self.physical_layers == 0)
13431 .map(|li| li - from)
13432 .collect()
13433 } else {
13434 Vec::new()
13435 };
13436 let mut h = hidden.to_vec();
13437 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13443 let outcome = crate::gpu::forward_token_graph(
13444 &model,
13445 self.graph_kv_id,
13446 &layers,
13447 &o1_views,
13448 self.o1_epoch,
13449 &self.inv_freq,
13450 &mut h,
13451 nh,
13452 nkv,
13453 hd,
13454 self.attn_scale,
13455 rd,
13456 self.hidden_size,
13457 self.intermediate_size,
13458 position,
13459 self.kv_cache.max_seq_len,
13460 gemma,
13461 self.rms_eps as f32,
13462 lm,
13463 &self.weights.final_norm,
13464 logits_out,
13465 &loop_norm_at,
13466 steps,
13467 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13468 ids_out,
13469 layers_run,
13470 from,
13471 dump_hidden,
13472 );
13473 match outcome {
13474 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13475 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13476 crate::gpu::TokenGraphOutcome::Declined => None,
13477 }
13478 }
13479
13480 #[cfg(target_os = "macos")]
13489 #[allow(clippy::type_complexity)]
13490 fn metal_rows_plan(
13491 &self,
13492 ) -> Option<(
13493 Vec<MetalRowsItem<'_>>,
13494 std::sync::Arc<cortiq_core::CmfModel>,
13495 Option<crate::gpu_metal::GdnGpuCfg>,
13496 )> {
13497 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13498 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13499 if !graph_force
13500 || !crate::gpu::enabled_here()
13501 || std::env::var("CMF_GPU_BLOCK")
13502 .map(|v| v == "0")
13503 .unwrap_or(false)
13504 || self.attn_softcap > 0.0
13505 || self.o1_active()
13506 || self.swa.is_some()
13507 || self.global_attn.is_some()
13508 || self.attention_heads_per_layer.is_some()
13509 || self.graph_attn_decline_reason().is_some()
13511 || self.attn_v_norm
13512 || self.loop_final_norm
13513 || !self.metal_rope_scale_ok()
13514 {
13515 return None;
13516 }
13517 let attend_contract = self.head_dim % 4 == 0
13518 && self.head_dim <= 256
13519 && self.rotary_dim >= 2
13520 && self.rotary_dim <= self.head_dim
13521 && (self.rotary_dim / 2) % 32 == 0
13522 && self.num_kv_heads > 0
13523 && self.num_heads % self.num_kv_heads == 0;
13524 if !attend_contract {
13525 return None;
13526 }
13527 let mut plan: Vec<MetalRowsItem> = Vec::new();
13528 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13529 for li in 0..self.num_layers {
13530 let lw = &self.weights.layers[self.phys_layer(li)];
13531 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13532 return None;
13533 }
13534 let ffn = match &lw.ffn {
13535 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13536 let (Some(g), Some(u), Some(dn)) = (
13537 d.gate_proj.metal_graph_parts(),
13538 d.up_proj.metal_graph_parts(),
13539 d.down_proj.metal_graph_parts(),
13540 ) else {
13541 return None;
13542 };
13543 MetalFfn::Dense {
13544 gate: g,
13545 up: u,
13546 down: dn,
13547 act: 0, }
13549 }
13550 _ => return None,
13551 };
13552 match &lw.attn {
13553 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13554 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13555 w.in_proj_qkv.metal_graph_parts(),
13556 w.in_proj_z.metal_graph_parts(),
13557 w.in_proj_a.f32_parts(),
13558 w.in_proj_b.f32_parts(),
13559 w.out_proj.metal_graph_parts(),
13560 ) else {
13561 return None;
13562 };
13563 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13564 model_ref.get_or_insert_with(|| model.clone());
13565 }
13566 let gl = GdnGpuLayer {
13567 attn_norm: &lw.input_norm,
13568 post_norm: &lw.post_norm,
13569 qkv,
13570 z,
13571 a,
13572 b: bb,
13573 out,
13574 ffn,
13575 conv1d: &w.conv1d,
13576 a_log: &w.a_log,
13577 dt_bias: &w.dt_bias,
13578 gnorm: &w.norm,
13579 };
13580 match plan.last_mut() {
13581 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13582 _ => plan.push(MetalRowsItem::Gdn {
13583 run: vec![gl],
13584 first: li,
13585 }),
13586 }
13587 }
13588 AttnKind::Full {
13589 wq,
13590 wk,
13591 wv,
13592 wo,
13593 q_norm,
13594 k_norm,
13595 output_gate,
13596 softplus_gate: None,
13597 bias: None,
13598 } => {
13599 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13600 wq.metal_graph_parts(),
13601 wk.metal_graph_parts(),
13602 wv.metal_graph_parts(),
13603 wo.metal_graph_parts(),
13604 ) else {
13605 return None;
13606 };
13607 if let QTensor::Mapped { model, .. } = wq {
13608 model_ref.get_or_insert_with(|| model.clone());
13609 }
13610 let cache = &self.kv_cache.layers[li];
13611 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13612 return None;
13613 }
13614 plan.push(MetalRowsItem::Attn {
13615 l: AttnGpuLayer {
13616 attn_norm: &lw.input_norm,
13617 post_norm: &lw.post_norm,
13618 wq: pq,
13619 wk: pk,
13620 wv: pv,
13621 wo: po,
13622 ffn,
13623 attn_out_norm: None,
13624 ffn_out_norm: None,
13625 layer_scale: None,
13626 },
13627 li,
13628 q_norm: q_norm.as_deref(),
13629 k_norm: k_norm.as_deref(),
13630 output_gate: *output_gate,
13631 });
13632 }
13633 _ => return None,
13634 }
13635 }
13636 let model = model_ref?;
13637 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13638 nv: cfg.num_v_heads,
13639 nk: cfg.num_k_heads,
13640 dk: cfg.key_head_dim,
13641 dv: cfg.value_head_dim,
13642 kk: cfg.conv_kernel,
13643 hidden: self.hidden_size,
13644 inter: self.intermediate_size,
13645 c_dim: cfg.conv_dim(),
13646 eps: cfg.rms_eps as f32,
13647 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13648 });
13649 Some((plan, model, gcfg))
13650 }
13651
13652 #[cfg(target_os = "macos")]
13654 #[allow(clippy::too_many_arguments)]
13655 fn metal_attn_params<'a>(
13656 li: usize,
13657 cache: &'a crate::kv_cache::LayerKvCache,
13658 q_norm: Option<&'a [f32]>,
13659 k_norm: Option<&'a [f32]>,
13660 output_gate: bool,
13661 inv_freq: &'a [f32],
13662 rope_scale: f32,
13663 geom: (usize, usize, usize, usize),
13664 pos0: usize,
13665 kv_id: u64,
13666 scale: f32,
13667 eps: f32,
13668 gemma: bool,
13669 late_qk_norm: bool,
13670 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13671 let (nh, nkv, hd, rd) = geom;
13672 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13673 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13674 let cpu_stored = cpu_k[0].len() / hd;
13675 (
13676 crate::gpu_metal::AttnDeviceParams {
13677 kv_id,
13678 layer: li,
13679 nh,
13680 nkv,
13681 hd,
13682 rd,
13683 position: pos0,
13684 scale,
13685 eps,
13686 gemma,
13687 late_qk_norm,
13688 output_gate,
13689 q_norm,
13690 k_norm,
13691 inv_freq,
13692 rope_scale,
13693 cpu_k,
13694 cpu_v,
13695 cpu_stored,
13696 cpu_gen: cache.generation(),
13697 o1: None,
13698 window: None,
13699 softmax_sink: cache.sinks.as_deref(),
13700 head_gate: None,
13701 v_norm: false,
13702 },
13703 cpu_stored,
13704 )
13705 }
13706
13707 #[cfg(target_os = "macos")]
13712 #[allow(clippy::type_complexity)]
13713 fn metal_rows_run(
13714 &mut self,
13715 hiddens: &mut [f32],
13716 pos0: usize,
13717 b: usize,
13718 prefill: bool,
13719 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13720 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13724 ) -> MetalRowsRun {
13725 use crate::gpu_metal::{GraphDims, VerifyGraph};
13726 if !crate::gpu_metal::wait_replay() {
13732 tracing::error!("Metal rows graph: the pending async replay failed");
13733 return MetalRowsRun::Failed;
13734 }
13735 spec_stamp("v.wait");
13736 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13742 if want > 0 {
13743 let phys = self.physical_layers.max(1);
13744 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13745 let is_gdn = self
13746 .weights
13747 .layers
13748 .get(li % phys)
13749 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13750 if is_gdn && l.linear_state.len() != want {
13751 l.linear_state = vec![0f32; want];
13752 }
13753 }
13754 }
13755 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13756 return MetalRowsRun::Declined;
13757 };
13758 spec_stamp("v.plan");
13759 let dims = GraphDims {
13760 hidden: self.hidden_size,
13761 eps: self.rms_eps as f32,
13762 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13763 };
13764 let Some(mut graph) = (if prefill {
13765 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13766 } else {
13767 VerifyGraph::new(&model, dims, hiddens, b)
13768 }) else {
13769 return MetalRowsRun::Declined;
13770 };
13771 let geom = (
13772 self.num_heads,
13773 self.num_kv_heads,
13774 self.head_dim,
13775 self.rotary_dim,
13776 );
13777 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13778 let eps = self.rms_eps as f32;
13779 let kv_id = self.graph_kv_id;
13780 let inv_freq = self.inv_freq.clone();
13781 for item in &plan {
13782 let ok = match item {
13783 MetalRowsItem::Gdn { run, .. } => gcfg
13784 .as_ref()
13785 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13786 .unwrap_or(false),
13787 MetalRowsItem::Attn {
13788 l,
13789 li,
13790 q_norm,
13791 k_norm,
13792 output_gate,
13793 } => {
13794 let (p, _) = Self::metal_attn_params(
13795 *li,
13796 &self.kv_cache.layers[*li],
13797 *q_norm,
13798 *k_norm,
13799 *output_gate,
13800 &inv_freq,
13801 self.layer_rope_scale(*li),
13802 geom,
13803 pos0,
13804 kv_id,
13805 self.attn_scale,
13806 eps,
13807 gemma,
13808 self.qk_norm_after_rope,
13809 );
13810 graph.attn_ok(l, &p)
13811 }
13812 };
13813 if !ok {
13814 use std::sync::atomic::{AtomicBool, Ordering};
13815 static SAID: AtomicBool = AtomicBool::new(false);
13816 if !SAID.swap(true, Ordering::Relaxed) {
13817 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13818 }
13819 return MetalRowsRun::Declined;
13820 }
13821 }
13822 let lm = match &spec {
13823 Some((lm, _, _)) => {
13824 if !graph.lm_head_ok(*lm) {
13825 return MetalRowsRun::Declined;
13826 }
13827 Some(*lm)
13828 }
13829 None => None,
13830 };
13831 let mut gdn_layers = Vec::new();
13832 let mut attn_layers = Vec::new();
13833 for item in &plan {
13834 match item {
13835 MetalRowsItem::Gdn { run, first } => {
13836 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13837 .iter()
13838 .map(|l| l.linear_state.as_slice())
13839 .collect();
13840 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13841 return MetalRowsRun::Declined;
13842 }
13843 gdn_layers.extend(*first..*first + run.len());
13844 }
13845 MetalRowsItem::Attn {
13846 l,
13847 li,
13848 q_norm,
13849 k_norm,
13850 output_gate,
13851 } => {
13852 let (p, cpu_stored) = Self::metal_attn_params(
13853 *li,
13854 &self.kv_cache.layers[*li],
13855 *q_norm,
13856 *k_norm,
13857 *output_gate,
13858 &inv_freq,
13859 self.layer_rope_scale(*li),
13860 geom,
13861 pos0,
13862 kv_id,
13863 self.attn_scale,
13864 eps,
13865 gemma,
13866 self.qk_norm_after_rope,
13867 );
13868 if !graph.encode_attn_b(l, &p) {
13869 return MetalRowsRun::Declined;
13870 }
13871 attn_layers.push((*li, cpu_stored));
13872 }
13873 }
13874 }
13875 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13876 if !graph.encode_lm_head_b(final_norm, lm) {
13877 return MetalRowsRun::Declined;
13878 }
13879 if let Some((n, _)) = argmax_out.as_ref() {
13884 if !graph.encode_argmax_b(*n) {
13885 argmax_out = None;
13886 }
13887 }
13888 }
13889 spec_stamp("v.enc");
13890 if !graph.sync() {
13891 return MetalRowsRun::Failed;
13892 }
13893 spec_stamp("v.gpu");
13894 match (spec, argmax_out) {
13895 (Some(_), Some((_, ids))) => {
13896 ids.resize(b, 0);
13897 if !graph.read_argmax(ids) {
13898 return MetalRowsRun::Failed;
13899 }
13900 spec_stamp("v.am");
13901 }
13902 (Some((lm, _, logits)), None) => {
13903 logits.resize(b * lm.1, 0.0);
13904 if !graph.read_logits(logits) {
13905 return MetalRowsRun::Failed;
13906 }
13907 spec_stamp("v.lg");
13908 }
13909 (None, _) => {}
13910 }
13911 if !graph.read_hidden(hiddens) {
13912 return MetalRowsRun::Failed;
13913 }
13914 spec_stamp("v.hid");
13915 MetalRowsRun::Completed(MetalVerifyPending {
13916 graph,
13917 gdn_layers,
13918 attn_layers,
13919 })
13920 }
13921
13922 #[cfg(target_os = "macos")]
13928 fn try_batch_graph_metal(
13929 &mut self,
13930 hiddens: &mut [f32],
13931 positions: &[usize],
13932 b: usize,
13933 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13934 argmax_out: Option<(usize, &mut Vec<u32>)>,
13935 ) -> crate::gpu::BatchGraphOutcome {
13936 let _t0 = std::time::Instant::now();
13937 if positions.len() != b
13938 || positions.windows(2).any(|w| w[1] != w[0] + 1)
13939 || hiddens.len() != b * self.hidden_size
13940 {
13941 return crate::gpu::BatchGraphOutcome::Declined;
13942 }
13943 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13944 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13945 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13946 MetalRowsRun::Completed(pending) => pending,
13947 };
13948 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13949 eprintln!(
13950 "metal-verify: {:.1} ms | b={b}",
13951 _t0.elapsed().as_secs_f64() * 1e3
13952 );
13953 }
13954 self.metal_verify = Some(pending);
13955 crate::gpu::BatchGraphOutcome::Completed
13956 }
13957
13958 #[cfg(target_os = "macos")]
13963 fn prefill_rows_metal(
13964 &mut self,
13965 ids: &[u32],
13966 start_pos: usize,
13967 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13968 ) -> MetalPrefillOutcome {
13969 let b = ids.len();
13970 if b == 0 || b > 512 {
13971 return MetalPrefillOutcome::Declined;
13972 }
13973 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13974 let with_head = spec.is_some();
13975 let hs = self.hidden_size;
13976 let mut hiddens = vec![0f32; b * hs];
13977 for (j, &id) in ids.iter().enumerate() {
13978 let e = self.embed_single(id);
13979 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13980 }
13981 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13982 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13983 MetalRowsRun::Failed => {
13984 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13985 return MetalPrefillOutcome::Failed;
13986 }
13987 MetalRowsRun::Completed(pending) => pending,
13988 };
13989 let idxs = pending.gdn_layers.clone();
13991 let mut outs: Vec<&mut [f32]> = self
13992 .kv_cache
13993 .layers
13994 .iter_mut()
13995 .enumerate()
13996 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13997 .map(|(_, l)| l.linear_state.as_mut_slice())
13998 .collect();
13999 if !pending.graph.finish_states(&mut outs) {
14000 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14001 return MetalPrefillOutcome::Failed;
14002 }
14003 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14004 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14008 for (li, cpu_stored) in &pending.attn_layers {
14009 let mut kbuf = vec![0f32; b * nkv * hd];
14010 let mut vbuf = vec![0f32; b * nkv * hd];
14011 if !crate::gpu_metal::kv_mirror_read_rows(
14012 self.graph_kv_id,
14013 *li,
14014 nkv,
14015 hd,
14016 *cpu_stored,
14017 b,
14018 &mut kbuf,
14019 &mut vbuf,
14020 ) {
14021 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14022 return MetalPrefillOutcome::Failed;
14023 }
14024 rows.push((*li, *cpu_stored, kbuf, vbuf));
14025 }
14026 for (li, cpu_stored, kbuf, vbuf) in rows {
14027 let cache = &mut self.kv_cache.layers[li];
14028 for r in 0..b {
14029 cache.append(
14030 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14031 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14032 &[],
14033 );
14034 }
14035 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14036 }
14037 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14038 if with_head {
14039 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14040 }
14041 MetalPrefillOutcome::Completed(hiddens)
14042 }
14043
14044 #[cfg(target_os = "macos")]
14045 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14046 self.prefill_rows_metal(ids, start_pos, None)
14047 }
14048
14049 #[cfg(target_os = "macos")]
14054 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14055 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14056 return MetalBatchNllOutcome::Declined;
14057 }
14058 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14059 return MetalBatchNllOutcome::Declined;
14060 };
14061 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14062 .ok()
14063 .and_then(|v| v.parse::<usize>().ok())
14064 .filter(|&v| (1..=512).contains(&v))
14065 .unwrap_or(32);
14066 let final_norm = self.weights.final_norm.clone();
14067 let mut nll = 0.0f64;
14068 let mut count = 0usize;
14069 let mut pos = 0usize;
14070 let mut completed = 0usize;
14071 while pos < ids.len() {
14072 let end = (pos + chunk).min(ids.len());
14073 let mut logits = Vec::new();
14074 let outcome =
14075 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14076 match outcome {
14077 MetalPrefillOutcome::Declined => {
14078 return if completed == 0 {
14079 MetalBatchNllOutcome::Declined
14080 } else {
14081 MetalBatchNllOutcome::Failed(format!(
14082 "ordinary Metal NLL batch declined after {completed} chunks"
14083 ))
14084 };
14085 }
14086 MetalPrefillOutcome::Failed => {
14087 return MetalBatchNllOutcome::Failed(
14088 "ordinary Metal NLL batch failed after admission".to_string(),
14089 );
14090 }
14091 MetalPrefillOutcome::Completed(_) => {}
14092 }
14093 completed += 1;
14094 let vocab = self.vocab_size.min(lm.1);
14095 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14096 return MetalBatchNllOutcome::Failed(
14097 "ordinary Metal NLL head returned an invalid shape".to_string(),
14098 );
14099 }
14100 for row in 0..(end - pos) {
14101 let absolute = pos + row;
14102 if absolute < start || absolute + 1 >= ids.len() {
14103 continue;
14104 }
14105 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14106 if let Some(mu) = self.logit_multiplier {
14107 for v in lg.iter_mut() {
14108 *v *= mu;
14109 }
14110 }
14111 if let Some(c) = self.final_softcap {
14112 softcap_in_place(lg, c, self.pool.as_deref());
14113 }
14114 let target = ids[absolute + 1] as usize;
14115 if target >= vocab {
14116 return MetalBatchNllOutcome::Failed(format!(
14117 "target token {target} exceeds Metal head rows {vocab}"
14118 ));
14119 }
14120 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14121 let lse: f64 = lg
14122 .iter()
14123 .map(|&v| ((v - max) as f64).exp())
14124 .sum::<f64>()
14125 .ln()
14126 + max as f64;
14127 nll += lse - lg[target] as f64;
14128 count += 1;
14129 }
14130 pos = end;
14131 }
14132 MetalBatchNllOutcome::Completed(nll, count)
14133 }
14134
14135 #[cfg(target_os = "macos")]
14139 fn metal_verify_commit(&mut self, a: usize) -> bool {
14140 let Some(mut pending) = self.metal_verify.take() else {
14141 return false;
14142 };
14143 let n = a + 1;
14144 let idxs = pending.gdn_layers.clone();
14146 let mut outs: Vec<&mut [f32]> = self
14147 .kv_cache
14148 .layers
14149 .iter_mut()
14150 .enumerate()
14151 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14152 .map(|(_, l)| l.linear_state.as_mut_slice())
14153 .collect();
14154 if !pending.graph.commit(n, &mut outs) {
14155 return false;
14156 }
14157 spec_stamp("c.replay");
14158 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14159 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14163 for (li, cpu_stored) in &pending.attn_layers {
14164 let mut kbuf = vec![0f32; n * nkv * hd];
14165 let mut vbuf = vec![0f32; n * nkv * hd];
14166 if !crate::gpu_metal::kv_mirror_read_rows(
14167 self.graph_kv_id,
14168 *li,
14169 nkv,
14170 hd,
14171 *cpu_stored,
14172 n,
14173 &mut kbuf,
14174 &mut vbuf,
14175 ) {
14176 return false;
14177 }
14178 rows.push((*li, *cpu_stored, kbuf, vbuf));
14179 }
14180 for (li, cpu_stored, kbuf, vbuf) in rows {
14181 let cache = &mut self.kv_cache.layers[li];
14182 for r in 0..n {
14183 cache.append(
14184 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14185 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14186 &[],
14187 );
14188 }
14189 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14190 }
14191 spec_stamp("c.kv");
14192 true
14193 }
14194
14195 #[cfg(target_os = "macos")]
14202 fn mtp_warm_batch_submit(
14203 &mut self,
14204 m: &mut MtpModule,
14205 pairs: &[(&[f32], u32)],
14206 first_pos: usize,
14207 ) -> Option<MetalWarmPending> {
14208 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14209 let b = pairs.len();
14210 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14211 return None;
14212 }
14213 let AttnKind::Full {
14214 wq,
14215 wk,
14216 wv,
14217 wo,
14218 q_norm,
14219 k_norm,
14220 output_gate,
14221 softplus_gate: None,
14222 bias: None,
14223 } = &m.layer.attn
14224 else {
14225 return None;
14226 };
14227 let FfnKind::Dense(d) = &m.layer.ffn else {
14228 return None;
14229 };
14230 if !d.segs.is_empty() {
14231 return None;
14232 }
14233 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14234 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14235 else {
14236 return None;
14237 };
14238 let (Some(g), Some(u), Some(dn)) = (
14239 d.gate_proj.q1_parts(),
14240 d.up_proj.q1_parts(),
14241 d.down_proj.q1_parts(),
14242 ) else {
14243 return None;
14244 };
14245 let Some(eh) = m.eh_proj.q1_parts() else {
14246 return None;
14247 };
14248 let QTensor::Mapped { model, .. } = wq else {
14249 return None;
14250 };
14251 let model = model.clone();
14252 let hs = self.hidden_size;
14253 let mut cat = vec![0f32; b * 2 * hs];
14255 for (j, (h, tok)) in pairs.iter().enumerate() {
14256 let e = self.embed_single(*tok);
14257 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14258 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14259 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14260 }
14261 let dims = GraphDims {
14262 hidden: hs,
14263 eps: self.rms_eps as f32,
14264 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14265 };
14266 spec_stamp("w.cat");
14267 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14268 return None;
14269 };
14270 spec_stamp("w.new");
14271 let l = AttnGpuLayer {
14272 attn_norm: &m.layer.input_norm,
14273 post_norm: &m.layer.post_norm,
14274 wq: pq,
14275 wk: pk,
14276 wv: pv,
14277 wo: po,
14278 ffn: MetalFfn::Dense {
14279 gate: g,
14280 up: u,
14281 down: dn,
14282 act: (d.act == Act::Gelu) as u8,
14283 },
14284 attn_out_norm: None,
14285 ffn_out_norm: None,
14286 layer_scale: None,
14287 };
14288 let (nh, nkv, hd, rd) = (
14289 self.num_heads,
14290 self.num_kv_heads,
14291 self.head_dim,
14292 self.rotary_dim,
14293 );
14294 let inv_freq = self.inv_freq.clone();
14295 let cpu_stored;
14296 {
14297 let cache = &m.kv;
14298 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14299 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14300 cpu_stored = cpu_k[0].len() / hd;
14301 if cpu_stored > first_pos {
14306 spec_stamp("w.decl");
14307 return None;
14308 }
14309 let p = AttnDeviceParams {
14310 kv_id: self.mtp_kv_id(),
14311 layer: Self::MTP_LAYER_BASE,
14312 nh,
14313 nkv,
14314 hd,
14315 rd,
14316 position: first_pos,
14317 scale: self.attn_scale,
14318 eps: self.rms_eps as f32,
14319 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14320 late_qk_norm: self.qk_norm_after_rope,
14321 output_gate: *output_gate,
14322 q_norm: q_norm.as_deref(),
14323 k_norm: k_norm.as_deref(),
14324 inv_freq: &inv_freq,
14325 rope_scale: self.rope_scale,
14326 cpu_k,
14327 cpu_v,
14328 cpu_stored,
14329 cpu_gen: cache.generation(),
14330 o1: None,
14331 window: None,
14332 softmax_sink: None,
14333 head_gate: None,
14334 v_norm: false,
14335 };
14336 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14337 return None;
14338 }
14339 }
14340 spec_stamp("w.enc");
14341 if !graph.submit() {
14342 return None;
14343 }
14344 spec_stamp("w.sub");
14345 Some(MetalWarmPending {
14346 graph,
14347 cpu_stored,
14348 b,
14349 })
14350 }
14351
14352 #[cfg(target_os = "macos")]
14355 fn mtp_warm_batch_metal(
14356 &mut self,
14357 m: &mut MtpModule,
14358 pairs: &[(&[f32], u32)],
14359 first_pos: usize,
14360 ) -> bool {
14361 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14362 Some(p) => self.mtp_warm_batch_finish(m, p),
14363 None => false,
14364 }
14365 }
14366
14367 #[cfg(target_os = "macos")]
14372 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14373 let MetalWarmPending {
14374 mut graph,
14375 cpu_stored,
14376 b,
14377 } = pending;
14378 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14379 if !graph.sync() {
14380 return false;
14381 }
14382 spec_stamp("w.gpu");
14383 let mut kbuf = vec![0f32; b * nkv * hd];
14384 let mut vbuf = vec![0f32; b * nkv * hd];
14385 if !crate::gpu_metal::kv_mirror_read_rows(
14386 self.mtp_kv_id(),
14387 Self::MTP_LAYER_BASE,
14388 nkv,
14389 hd,
14390 cpu_stored,
14391 b,
14392 &mut kbuf,
14393 &mut vbuf,
14394 ) {
14395 return false;
14396 }
14397 for r in 0..b {
14398 m.kv.append(
14399 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14400 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14401 &[],
14402 );
14403 }
14404 crate::gpu_metal::kv_mirror_set_stored(
14405 self.mtp_kv_id(),
14406 Self::MTP_LAYER_BASE,
14407 cpu_stored + b,
14408 );
14409 spec_stamp("w.kv");
14410 true
14411 }
14412
14413 pub(crate) fn note_draft_id(&mut self, id: u32) {
14420 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14421 if (id as usize) >= cut {
14422 self.draft_full_streak = 16;
14423 } else {
14424 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14425 }
14426 }
14427
14428 fn draft_head_rows(&self, head_rows: usize) -> usize {
14431 if self.draft_full_streak > 0 {
14432 head_rows
14433 } else {
14434 Self::draft_vocab_rows(head_rows)
14435 }
14436 }
14437
14438 fn draft_vocab_rows(head_rows: usize) -> usize {
14441 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14442 let n = *N.get_or_init(|| {
14443 std::env::var("CMF_DRAFT_VOCAB")
14444 .ok()
14445 .and_then(|v| v.parse().ok())
14446 .unwrap_or(65536)
14447 });
14448 if n == 0 { head_rows } else { n.min(head_rows) }
14449 }
14450
14451 #[cfg(target_os = "macos")]
14456 fn mtp_step_metal(
14457 &mut self,
14458 m: &mut MtpModule,
14459 hidden: &[f32],
14460 next_token: u32,
14461 position: usize,
14462 want_logits: bool,
14463 ) -> Option<(Vec<f32>, Vec<f32>)> {
14464 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14465 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14466 || !crate::gpu::q1_force()
14467 || !crate::gpu::enabled_here()
14468 || self.attn_softcap > 0.0
14469 || self.attention_heads_per_layer.is_some()
14470 || m.kv.mode != crate::kv_cache::KvMode::F32
14471 || m.kv.o1.is_some()
14472 {
14473 return None;
14474 }
14475 let AttnKind::Full {
14476 wq,
14477 wk,
14478 wv,
14479 wo,
14480 q_norm,
14481 k_norm,
14482 output_gate,
14483 softplus_gate: None,
14484 bias: None,
14485 } = &m.layer.attn
14486 else {
14487 return None;
14488 };
14489 let FfnKind::Dense(d) = &m.layer.ffn else {
14490 return None;
14491 };
14492 if d.act != Act::Silu || !d.segs.is_empty() {
14493 return None;
14494 }
14495 let (pq, pk, pv, po) = (
14496 wq.q1_parts()?,
14497 wk.q1_parts()?,
14498 wv.q1_parts()?,
14499 wo.q1_parts()?,
14500 );
14501 let (g, u, dn) = (
14502 d.gate_proj.q1_parts()?,
14503 d.up_proj.q1_parts()?,
14504 d.down_proj.q1_parts()?,
14505 );
14506 let QTensor::Mapped { model, .. } = wq else {
14507 return None;
14508 };
14509 let model = model.clone();
14510 let lm = if want_logits {
14511 Some(self.weights.lm_head.q1_parts()?)
14512 } else {
14513 None
14514 };
14515 let dims = GraphDims {
14516 hidden: self.hidden_size,
14517 eps: self.rms_eps as f32,
14518 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14519 };
14520 let hs = self.hidden_size;
14523 let mut x = vec![0f32; hs];
14524 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14531 crate::gpu_metal::DENSE_MV,
14532 );
14533 let mut graph = TokenGraph::new(&model, dims, &x)?;
14534 let mut folded = false;
14535 if let Some(eh) = m.eh_proj.q1_parts() {
14536 let e = self.embed_single(next_token);
14537 let mut cat = vec![0.0f32; 2 * hs];
14538 let (cat_e, cat_h) = cat.split_at_mut(hs);
14539 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14540 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14541 folded = graph.encode_input_proj(eh, &cat);
14542 }
14543 if !folded {
14544 x = self.mtp_block_input(m, hidden, next_token);
14545 graph = TokenGraph::new(&model, dims, &x)?;
14546 }
14547 spec_stamp("d.in");
14548 let l = AttnGpuLayer {
14549 attn_norm: &m.layer.input_norm,
14550 post_norm: &m.layer.post_norm,
14551 wq: pq,
14552 wk: pk,
14553 wv: pv,
14554 wo: po,
14555 ffn: MetalFfn::Dense {
14556 gate: g,
14557 up: u,
14558 down: dn,
14559 act: (d.act == Act::Gelu) as u8,
14560 },
14561 attn_out_norm: None,
14562 ffn_out_norm: None,
14563 layer_scale: None,
14564 };
14565 let (nh, nkv, hd, rd) = (
14566 self.num_heads,
14567 self.num_kv_heads,
14568 self.head_dim,
14569 self.rotary_dim,
14570 );
14571 let inv_freq = self.inv_freq.clone();
14572 {
14573 let cache = &m.kv;
14574 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14575 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14576 let cpu_stored = cpu_k[0].len() / hd;
14577 let p = AttnDeviceParams {
14578 kv_id: self.mtp_kv_id(),
14579 layer: Self::MTP_LAYER_BASE,
14580 nh,
14581 nkv,
14582 hd,
14583 rd,
14584 position,
14585 scale: self.attn_scale,
14586 eps: self.rms_eps as f32,
14587 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14588 late_qk_norm: self.qk_norm_after_rope,
14589 output_gate: *output_gate,
14590 q_norm: q_norm.as_deref(),
14591 k_norm: k_norm.as_deref(),
14592 inv_freq: &inv_freq,
14593 rope_scale: self.rope_scale,
14594 cpu_k,
14595 cpu_v,
14596 cpu_stored,
14597 cpu_gen: cache.generation(),
14598 o1: None,
14599 window: None,
14600 softmax_sink: None,
14601 head_gate: None,
14602 v_norm: false,
14603 };
14604 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14605 return None;
14606 }
14607 }
14608 let draft_rows = if let Some(lm) = lm {
14614 self.draft_head_rows(lm.1)
14615 } else {
14616 0
14617 };
14618 if let Some(lm) = lm {
14619 if !graph.lm_head_ok(lm) {
14620 return None;
14621 }
14622 if draft_rows < lm.1 {
14623 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14624 return None;
14625 }
14626 } else {
14627 graph.encode_lm_head(&m.final_norm, lm);
14628 }
14629 }
14630 spec_stamp("d.enc");
14631 if graph.sync_checked().is_err() {
14632 return None;
14633 }
14634 spec_stamp("d.gpu");
14635 let mut logits = Vec::new();
14636 if let Some(lm) = lm {
14637 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14638 logits = attention::take_buf(n_read);
14639 graph.read_logits(&mut logits);
14640 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14642 }
14643 graph.finish(&mut x);
14644 let mut krow = attention::take_buf(nkv * hd);
14645 let mut vrow = attention::take_buf(nkv * hd);
14646 if crate::gpu_metal::kv_mirror_read_last(
14647 self.mtp_kv_id(),
14648 Self::MTP_LAYER_BASE,
14649 nkv,
14650 hd,
14651 &mut krow,
14652 &mut vrow,
14653 ) {
14654 m.kv.append(&krow, &vrow, &[]);
14655 }
14656 attention::recycle_buf(&mut krow);
14657 attention::recycle_buf(&mut vrow);
14658 spec_stamp("d.rd");
14659 Some((logits, x))
14660 }
14661
14662 fn mtp_chain_on() -> bool {
14675 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14676 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14677 }
14678
14679 #[cfg(target_os = "macos")]
14691 fn mtp_draft_chain_metal(
14692 &mut self,
14693 m: &mut MtpModule,
14694 hidden: &[f32],
14695 t_next: u32,
14696 position: usize,
14697 k: usize,
14698 ) -> Result<Vec<u32>, bool> {
14699 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14700 if k == 0
14701 || k > 64
14702 || !Self::mtp_chain_on()
14703 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14704 || !crate::gpu::q1_force()
14705 || !crate::gpu::enabled_here()
14706 || self.attn_softcap > 0.0
14707 || self.attention_heads_per_layer.is_some()
14708 || m.kv.mode != crate::kv_cache::KvMode::F32
14709 || m.kv.o1.is_some()
14710 || self.dsv4.is_some()
14712 || self.dsv41.is_some()
14713 || self.qwen4_exp.is_some()
14714 || self.per_token_stack()
14715 {
14716 return Err(false);
14717 }
14718 let AttnKind::Full {
14719 wq,
14720 wk,
14721 wv,
14722 wo,
14723 q_norm,
14724 k_norm,
14725 output_gate,
14726 softplus_gate: None,
14727 bias: None,
14728 } = &m.layer.attn
14729 else {
14730 return Err(false);
14731 };
14732 let FfnKind::Dense(d) = &m.layer.ffn else {
14733 return Err(false);
14734 };
14735 if d.act != Act::Silu || !d.segs.is_empty() {
14736 return Err(false);
14737 }
14738 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14739 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14740 else {
14741 return Err(false);
14742 };
14743 let (Some(g), Some(u), Some(dn)) = (
14744 d.gate_proj.q1_parts(),
14745 d.up_proj.q1_parts(),
14746 d.down_proj.q1_parts(),
14747 ) else {
14748 return Err(false);
14749 };
14750 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14751 return Err(false);
14752 };
14753 let QTensor::Mapped { model, .. } = wq else {
14754 return Err(false);
14755 };
14756 let model = model.clone();
14757 let QTensor::Mapped {
14760 model: em,
14761 idx: eidx,
14762 dtype: cortiq_core::TensorDtype::Q4TiledP,
14763 ..
14764 } = &self.weights.embed_tokens
14765 else {
14766 return Err(false);
14767 };
14768 if !std::sync::Arc::ptr_eq(em, &model)
14769 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14770 {
14771 return Err(false);
14772 }
14773 let embed = (
14774 *eidx,
14775 self.weights.embed_tokens.rows(),
14776 self.weights.embed_tokens.cols(),
14777 );
14778 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14779 return Err(false);
14780 }
14781 let dims = GraphDims {
14782 hidden: self.hidden_size,
14783 eps: self.rms_eps as f32,
14784 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14785 };
14786 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14790 crate::gpu_metal::DENSE_MV,
14791 );
14792 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14793 return Err(false);
14794 };
14795 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14796 return Err(false);
14797 }
14798 let l = AttnGpuLayer {
14799 attn_norm: &m.layer.input_norm,
14800 post_norm: &m.layer.post_norm,
14801 wq: pq,
14802 wk: pk,
14803 wv: pv,
14804 wo: po,
14805 ffn: MetalFfn::Dense {
14806 gate: g,
14807 up: u,
14808 down: dn,
14809 act: (d.act == Act::Gelu) as u8,
14810 },
14811 attn_out_norm: None,
14812 ffn_out_norm: None,
14813 layer_scale: None,
14814 };
14815 let (nh, nkv, hd, rd) = (
14816 self.num_heads,
14817 self.num_kv_heads,
14818 self.head_dim,
14819 self.rotary_dim,
14820 );
14821 let inv_freq = self.inv_freq.clone();
14822 let draft_rows = self.draft_head_rows(lm.1);
14823 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14824 if n_arg == 0 {
14825 return Err(false);
14826 }
14827 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14834 let t_chain = std::time::Instant::now();
14835 graph.chain_ids_init(t_next, k);
14836 let cpu_stored;
14837 {
14838 let cache = &m.kv;
14839 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14840 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14841 cpu_stored = cpu_k[0].len() / hd;
14842 for j in 0..k {
14843 if !graph.encode_chain_input(
14844 embed,
14845 j as u32,
14846 &m.enorm,
14847 &m.hnorm,
14848 self.embed_multiplier,
14849 eh,
14850 ) {
14851 return Err(false);
14852 }
14853 let p = AttnDeviceParams {
14857 kv_id: self.mtp_kv_id(),
14858 layer: Self::MTP_LAYER_BASE,
14859 nh,
14860 nkv,
14861 hd,
14862 rd,
14863 position: position + j,
14864 scale: self.attn_scale,
14865 eps: self.rms_eps as f32,
14866 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14867 late_qk_norm: self.qk_norm_after_rope,
14868 output_gate: *output_gate,
14869 q_norm: q_norm.as_deref(),
14870 k_norm: k_norm.as_deref(),
14871 inv_freq: &inv_freq,
14872 rope_scale: self.rope_scale,
14873 cpu_k: cpu_k.clone(),
14874 cpu_v: cpu_v.clone(),
14875 cpu_stored: cpu_stored + j,
14876 cpu_gen: cache.generation(),
14877 o1: None,
14878 window: None,
14879 softmax_sink: None,
14880 head_gate: None,
14881 v_norm: false,
14882 };
14883 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14884 return Err(false);
14885 }
14886 if draft_rows < lm.1 {
14887 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14888 return Err(false);
14889 }
14890 } else {
14891 graph.encode_lm_head(&m.final_norm, lm);
14892 }
14893 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14894 return Err(false);
14895 }
14896 if split {
14897 graph.commit();
14900 }
14901 }
14902 }
14903 let t_enc = t_chain.elapsed();
14904 if graph.sync_checked().is_err() {
14905 return Err(true);
14906 }
14907 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14908 eprintln!(
14909 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14910 t_enc.as_secs_f64() * 1e3,
14911 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14912 if split { ", split" } else { "" }
14913 );
14914 }
14915 let mut ids = vec![0u32; k];
14916 if !graph.chain_ids_read(&mut ids) {
14917 return Err(true);
14918 }
14919 let mut kbuf = vec![0f32; k * nkv * hd];
14920 let mut vbuf = vec![0f32; k * nkv * hd];
14921 if !crate::gpu_metal::kv_mirror_read_rows(
14922 self.mtp_kv_id(),
14923 Self::MTP_LAYER_BASE,
14924 nkv,
14925 hd,
14926 cpu_stored,
14927 k,
14928 &mut kbuf,
14929 &mut vbuf,
14930 ) {
14931 return Err(true);
14932 }
14933 for r in 0..k {
14934 m.kv.append(
14935 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14936 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14937 &[],
14938 );
14939 }
14940 Ok(ids)
14941 }
14942
14943 fn try_batch_graph_wgpu(
14944 &self,
14945 hiddens: &mut [f32],
14946 positions: &[usize],
14947 k: usize,
14948 spec: Option<crate::gpu::SpecTail<'_>>,
14949 ) -> crate::gpu::BatchGraphOutcome {
14950 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14951 }
14952
14953 fn try_batch_graph_wgpu_prefix(
14958 &self,
14959 hiddens: &mut [f32],
14960 positions: &[usize],
14961 k: usize,
14962 spec: Option<crate::gpu::SpecTail<'_>>,
14963 layers_run: Option<&mut usize>,
14964 ) -> crate::gpu::BatchGraphOutcome {
14965 let graph_end = match self.mimo_moe.graph_prefix_end() {
14966 Some(end) if end < self.num_layers => {
14967 if layers_run.is_none() || spec.is_some() || end == 0 {
14968 return crate::gpu::BatchGraphOutcome::Declined;
14969 }
14970 end
14971 }
14972 _ => self.num_layers,
14973 };
14974 let _tb = std::time::Instant::now();
14975 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14976 if self.attn_softcap > 0.0 {
14977 return crate::gpu::BatchGraphOutcome::Declined; }
14979 if let Some(reason) = self
14982 .wgpu_graph_attn_decline()
14983 .or_else(|| self.batch_graph_gemma_decline())
14984 {
14985 self.note_graph_decline("wgpu batch graph", reason);
14986 return crate::gpu::BatchGraphOutcome::Declined;
14987 }
14988 let nh = self.num_heads;
14989 let (nkv, hd, rd) = self.layer_geom(0);
14990 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14991 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14992 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14993 {
14994 let name = &m.tensors[i].name;
14995 let prism = if crate::prism::is_inverse_embedding(m, name) {
14996 crate::gpu::GraphPrismOp::InverseEmbedding
14997 } else if crate::prism::is_forward_weight(m, name) {
14998 crate::gpu::GraphPrismOp::Forward
14999 } else {
15000 crate::gpu::GraphPrismOp::None
15001 };
15002 return Some(crate::gpu::GraphW {
15003 idx: i,
15004 kind,
15005 row_scale: rs,
15006 data: &[],
15007 prism,
15008 affine: crate::prism::is_affine_target(m, name),
15009 });
15010 }
15011 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15012 eprintln!(
15013 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15014 t.rows(),
15015 t.cols()
15016 );
15017 }
15018 t.as_f32().map(|d| crate::gpu::GraphW {
15019 idx: 0,
15020 kind: 4,
15021 row_scale: &[],
15022 data: d,
15023 prism: crate::gpu::GraphPrismOp::None,
15024 affine: false,
15025 })
15026 }
15027 let built: Option<(
15028 Vec<crate::gpu::GraphLayer<'_>>,
15029 std::sync::Arc<cortiq_core::CmfModel>,
15030 )> = (|| {
15031 let mut layers = Vec::with_capacity(graph_end);
15032 let mut model = None;
15033 for li in 0..graph_end {
15034 let lw = &self.weights.layers[self.phys_layer(li)];
15035 let gffn = match &lw.ffn {
15042 FfnKind::Dense(d) if !d.segs.is_empty() => {
15043 if batch_debug {
15044 eprintln!("batch graph: dense segmented FFN at layer {li}");
15045 }
15046 return None;
15047 }
15048 FfnKind::Dense(d) => {
15049 let Some(act) = d.act.wgpu_graph_act() else {
15050 if batch_debug {
15051 eprintln!(
15052 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15053 d.act
15054 );
15055 }
15056 return None;
15057 };
15058 crate::gpu::GraphFfn::Dense {
15059 gate: gw(&d.gate_proj)?,
15060 up: gw(&d.up_proj)?,
15061 down: gw(&d.down_proj)?,
15062 act,
15063 }
15064 }
15065 FfnKind::Moe(m) => {
15066 if m.route_tau.is_some() || m.mask.is_some() {
15073 return None;
15074 }
15075 let shared = m.shared.as_ref();
15079 let has_shared = shared.is_some();
15080 let shared_gated = matches!(shared, Some((_, Some(_))));
15081 let sgate = match shared {
15082 Some((_, Some(sg))) => gw(sg)?,
15083 _ => gw(&m.router)?,
15087 };
15088 let router = gw(&m.router)?;
15089 if router.prism != crate::gpu::GraphPrismOp::None
15095 || router.affine
15096 || sgate.prism != crate::gpu::GraphPrismOp::None
15097 || sgate.affine
15098 {
15099 return None;
15100 }
15101 let inter = m.experts.first()?.gate_proj.rows();
15102 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15103 let mut q4tp: Option<bool> = None;
15104 let mut gu_q2: Option<bool> = None;
15105 let mut q82: Option<bool> = None;
15106 let mut moe_act = None;
15110 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15111 let Some(e_act) = e.act.wgpu_graph_act() else {
15112 return None;
15113 };
15114 match moe_act {
15115 Some(existing) if existing != e_act => return None,
15116 None => moe_act = Some(e_act),
15117 _ => {}
15118 }
15119 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15120 return None;
15121 }
15122 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15126 {
15127 Some((mm, gi)) => (
15128 mm,
15129 gi,
15130 e.up_proj.mapped_q4t()?.1,
15131 e.down_proj.mapped_q4t()?.1,
15132 false,
15133 false,
15134 false,
15135 ),
15136 None => match e.gate_proj.mapped_q2tp() {
15137 Some((mm, gi)) => (
15138 mm,
15139 gi,
15140 e.up_proj.mapped_q2tp()?.1,
15141 e.down_proj.mapped_q4tp()?.1,
15142 true,
15143 true,
15144 false,
15145 ),
15146 None => match e.gate_proj.mapped_q4tp() {
15147 Some((mm, gi)) => (
15148 mm,
15149 gi,
15150 e.up_proj.mapped_q4tp()?.1,
15151 e.down_proj.mapped_q4tp()?.1,
15152 true,
15153 false,
15154 false,
15155 ),
15156 None => {
15157 if !crate::gpu::moe_q82_graph_on() {
15158 return None;
15159 }
15160 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15161 (
15162 mm,
15163 gi,
15164 e.up_proj.mapped_q8_2f()?.1,
15165 e.down_proj.mapped_q8_2f()?.1,
15166 false,
15167 false,
15168 true,
15169 )
15170 }
15171 },
15172 },
15173 };
15174 if *q4tp.get_or_insert(is_p) != is_p
15175 || *gu_q2.get_or_insert(is_q2) != is_q2
15176 || *q82.get_or_insert(is_8) != is_8
15177 {
15178 return None;
15179 }
15180 if [gi, ui, di].into_iter().any(|idx| {
15181 mm.tensors.get(idx).is_some_and(|t| {
15182 crate::prism::is_forward_weight(mm, &t.name)
15183 || crate::prism::is_affine_target(mm, &t.name)
15184 })
15185 }) {
15186 return None;
15187 }
15188 model.get_or_insert_with(|| mm.clone());
15189 experts.push((gi, ui, di));
15190 }
15191 crate::gpu::GraphFfn::Moe {
15192 router,
15193 shared_gate: sgate,
15194 experts,
15195 n_exp: m.experts.len(),
15196 top_k: m.top_k,
15197 inter,
15198 act: moe_act?,
15199 norm_topk: m.norm_topk_prob,
15200 q4tp: q4tp?,
15201 gu_q2: gu_q2.unwrap_or(false),
15202 q82: q82.unwrap_or(false),
15203 sigmoid: m.router_sigmoid,
15204 sigmoid_denom_eps: m.sigmoid_denom_eps,
15205 bias: m.expert_bias.as_deref(),
15206 has_shared,
15207 detached_shared: None,
15208 shared_gated,
15209 route_scale: m.routed_scaling,
15210 }
15211 }
15212 _ => return None,
15213 };
15214 let attn = match &lw.attn {
15215 AttnKind::Full {
15216 wq,
15217 wk,
15218 wv,
15219 wo,
15220 q_norm,
15221 k_norm,
15222 output_gate,
15223 softplus_gate,
15224 bias,
15225 } => {
15226 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15227 if batch_debug {
15228 eprintln!(
15229 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15230 softplus_gate.is_some(),
15231 self.attention_heads_per_layer.is_some()
15232 );
15233 }
15234 return None;
15235 }
15236 let (m, _, _, _) =
15237 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15238 model = Some(m.clone());
15239 crate::gpu::GraphAttn::Full {
15240 wq: gw(wq)?,
15241 wk: gw(wk)?,
15242 wv: gw(wv)?,
15243 wo: gw(wo)?,
15244 q_norm: q_norm.as_deref(),
15245 k_norm: k_norm.as_deref(),
15246 late_qk_norm: self.qk_norm_after_rope,
15247 bias: bias
15248 .as_ref()
15249 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15250 output_gate: *output_gate,
15251 cpu_k: self.kv_cache.layers[li].k_heads(),
15252 cpu_v: self.kv_cache.layers[li].v_heads(),
15253 cpu_base: self.kv_cache.layers[li].base(),
15254 geom: self.graph_attn_geom(li),
15255 head_gate: None,
15258 }
15259 }
15260 AttnKind::LinearGdn(w) => {
15261 let Some(cfg) = self.gdn_cfg else {
15262 if batch_debug {
15263 eprintln!("batch graph: no GDN config at layer {li}");
15264 }
15265 return None;
15266 };
15267 let (m, _, _, _) = w
15268 .in_proj_qkv
15269 .graph_weight()
15270 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15271 model = Some(m.clone());
15272 crate::gpu::GraphAttn::Gdn {
15273 qkv: gw(&w.in_proj_qkv)?,
15274 z: gw(&w.in_proj_z)?,
15275 a: gw(&w.in_proj_a)?,
15276 b: gw(&w.in_proj_b)?,
15277 out: gw(&w.out_proj)?,
15278 conv1d: &w.conv1d,
15279 a_log: &w.a_log,
15280 dt_bias: &w.dt_bias,
15281 norm: &w.norm,
15282 nv: cfg.num_v_heads,
15283 nk: cfg.num_k_heads,
15284 dk: cfg.key_head_dim,
15285 dv: cfg.value_head_dim,
15286 kk: cfg.conv_kernel,
15287 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15288 }
15289 }
15290 _ => return None,
15291 };
15292 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15295 layers.push(crate::gpu::GraphLayer {
15296 input_norm: &lw.input_norm,
15297 attn,
15298 post_norm: &lw.post_norm,
15299 attn_out_norm: if g4 {
15300 lw.attn_out_norm.as_deref()
15301 } else {
15302 None
15303 },
15304 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15305 layer_scale: if g4 { lw.layer_scale } else { None },
15306 ffn: gffn,
15307 });
15308 }
15309 Some((layers, model?))
15310 })();
15311 let Some((layers, model)) = built else {
15312 {
15313 use std::sync::atomic::{AtomicBool, Ordering};
15314 static SAID: AtomicBool = AtomicBool::new(false);
15315 if !SAID.swap(true, Ordering::Relaxed) {
15316 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15317 }
15318 }
15319 return crate::gpu::BatchGraphOutcome::Declined;
15320 };
15321 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15322 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15323 }
15324 crate::gpu::forward_batch_graph(
15325 &model,
15326 self.graph_kv_id,
15327 &layers,
15328 &self.inv_freq,
15329 hiddens,
15330 nh,
15331 nkv,
15332 hd,
15333 rd,
15334 self.hidden_size,
15335 self.intermediate_size,
15336 positions,
15337 self.kv_cache.max_seq_len,
15338 gemma,
15339 self.rms_eps as f32,
15340 self.attn_scale,
15341 k,
15342 &(0..graph_end)
15343 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15344 .collect::<Vec<_>>(),
15345 self.o1_epoch,
15346 spec,
15347 layers_run,
15348 )
15349 }
15350
15351 fn draft_probe() -> bool {
15355 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15356 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15357 }
15358
15359 #[cfg(feature = "gpu")]
15371 fn dsv4_spec_on() -> bool {
15372 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15373 *ON.get_or_init(|| {
15374 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15378 return v != "0";
15379 }
15380 std::env::var("CMF_DSV4_SPEC")
15387 .map(|v| v != "0")
15388 .unwrap_or_else(|_| {
15389 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15390 })
15391 })
15392 }
15393
15394 #[cfg(feature = "gpu")]
15401 fn dsv4_spec_step(
15402 &mut self,
15403 tip_token: u32,
15404 t_next: u32,
15405 next_pos: usize,
15406 max_extra: usize,
15407 drafted: &mut usize,
15408 accepted_ctr: &mut usize,
15409 ) -> Option<(Vec<u32>, usize)> {
15410 let t_all = std::time::Instant::now();
15411 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15412 thread_local! {
15413 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15414 const { std::cell::Cell::new(None) };
15415 }
15416 LAST.with(|l| {
15417 if let Some(prev) = l.get() {
15418 eprintln!(
15419 "between rounds {:.1} ms",
15420 prev.elapsed().as_secs_f64() * 1e3
15421 );
15422 }
15423 l.set(Some(std::time::Instant::now()));
15424 });
15425 }
15426 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15427 eprintln!("spec_step: enter pos={next_pos}");
15428 }
15429 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15430 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15431 if self.dspark.is_none() {
15433 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15434 if t.is_empty() {
15435 return None;
15436 }
15437 crate::dsv4::dspark_arm(&t, cfg.dim);
15438 self.dspark = Some(crate::dsv4::DsparkState::new(
15439 self.dsv4_mtp.len(),
15440 &cfg,
15441 t.len(),
15442 ));
15443 }
15444 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15445 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15446 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15447 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15448 }
15449 let pack = pack?;
15450 let block = crate::dsv4::dspark_block();
15451 let b_box = self.dsv4.as_mut()?;
15452 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15453 let ds = self.dspark.as_mut()?;
15454 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15457 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15458 if dbg {
15459 eprintln!("spec_step: no capture");
15460 }
15461 return None;
15462 }
15463 ds.have_hidden = true;
15464 let tip_pos = next_pos.checked_sub(1)?;
15465 let draft_started = std::time::Instant::now();
15466 let mut conf = Vec::new();
15467 let props = crate::dsv4::dspark_draft_gpu(
15468 g,
15469 &self.dsv4_mtp,
15470 &cfg,
15471 ds,
15472 pack,
15473 st.kv_id,
15474 tip_token,
15475 tip_pos,
15476 self.pool.as_deref(),
15477 &mut conf,
15478 );
15479 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15480 *drafted += block;
15481 if props.is_empty() || props[0] != t_next {
15482 if dbg {
15483 eprintln!(
15484 "spec_step: draft {} (props0={:?} t_next={t_next})",
15485 if props.is_empty() {
15486 "empty"
15487 } else {
15488 "miss"
15489 },
15490 props.first()
15491 );
15492 }
15493 return None;
15494 }
15495 let mut k_verify = crate::dsv4::dspark_verify_k()
15502 .min(props.len())
15503 .min(max_extra.saturating_add(1));
15504 let conf_min = {
15510 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15511 *M.get_or_init(|| {
15512 std::env::var("CMF_DSPARK_CONF_MIN")
15513 .ok()
15514 .and_then(|v| v.parse().ok())
15515 .unwrap_or(0.0)
15516 })
15517 };
15518 if conf_min > 0.0 && conf.len() >= props.len() {
15519 let mut keep = 1usize;
15520 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15521 keep += 1;
15522 }
15523 k_verify = k_verify.min(keep.max(2));
15524 }
15525 if k_verify < 2 {
15526 return None;
15527 }
15528 let mut fed = Vec::with_capacity(k_verify);
15529 fed.push(t_next);
15530 fed.extend_from_slice(&props[1..k_verify]);
15531 let mut argmax = Vec::new();
15532 let mut logits_all = Vec::new();
15533 let mut walked = Vec::new();
15534 let txn = crate::dsv4::dsv4_verify_chunk(
15535 g,
15536 layers,
15537 &cfg,
15538 st,
15539 &fed,
15540 next_pos,
15541 &self.inv_freq,
15542 self.pool.as_deref(),
15543 &targets,
15544 &mut argmax,
15545 &mut logits_all,
15546 &mut walked,
15547 );
15548 if txn.is_none() && dbg {
15549 eprintln!("spec_step: verify declined");
15550 }
15551 let txn = txn?;
15552 let spec_gpu_end = txn.gpu_end;
15553 let b = fed.len();
15554 let mut accepted = 1usize;
15555 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15556 accepted += 1;
15557 }
15558 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15563 accepted = 1;
15564 }
15565 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15566 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15567 }
15568 let t_fin = std::time::Instant::now();
15569 if !crate::dsv4::dsv4_spec_finish(
15570 g,
15571 layers,
15572 &cfg,
15573 st,
15574 txn,
15575 accepted,
15576 &fed,
15577 &self.inv_freq,
15578 self.pool.as_deref(),
15579 ) {
15580 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15581 return None;
15582 }
15583 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15584 eprintln!(
15585 "finish(k={accepted}): {:.1} ms",
15586 t_fin.elapsed().as_secs_f64() * 1e3
15587 );
15588 }
15589 *accepted_ctr += accepted - 1;
15590 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15595 let dev_caps: Vec<usize> = targets
15600 .iter()
15601 .copied()
15602 .filter(|&t| t < spec_gpu_end)
15603 .collect();
15604 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15605 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15606 return None;
15607 }
15608 for t in 0..accepted {
15609 let tip = t + 1 == accepted;
15610 for (slot, &tl) in targets.iter().enumerate() {
15611 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15612 let lo = (di * b + t) * hc * dim;
15613 crate::dsv4::dspark_capture(
15614 &caps_all[lo..lo + hc * dim],
15615 &cfg,
15616 slot,
15617 &mut ds.main_hidden,
15618 );
15619 } else if tip
15620 && crate::dsv4::dspark_peek_slot(slot, dim, {
15621 let lo = slot * dim;
15622 &mut ds.main_hidden[lo..lo + dim]
15623 })
15624 {
15625 } else {
15630 crate::dsv4::dspark_capture(
15634 &walked[t * hc * dim..(t + 1) * hc * dim],
15635 &cfg,
15636 slot,
15637 &mut ds.main_hidden,
15638 );
15639 }
15640 }
15641 crate::dsv4::dspark_ring_append(
15642 g,
15643 &self.dsv4_mtp,
15644 &cfg,
15645 ds,
15646 next_pos + t,
15647 self.pool.as_deref(),
15648 );
15649 }
15650 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15651 self.graph_logits = Some(row);
15652 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15657 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15658 crate::dsv4::pick_tally_arm();
15659 }
15660 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15661 eprintln!(
15662 "spec_step total {:.1} ms (k={accepted})",
15663 t_all.elapsed().as_secs_f64() * 1e3
15664 );
15665 }
15666 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15667 }
15668
15669 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15670 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15671 return;
15672 }
15673 let trunk_now = crate::dsv4::pick_tally_take();
15675 crate::dsv4::trunk_freq_note(&trunk_now);
15676 if !trunk_now.is_empty() {
15677 self.dspark_trunk_picks.push(trunk_now);
15678 let keep = crate::dsv4::dspark_block();
15679 if self.dspark_trunk_picks.len() > keep {
15680 self.dspark_trunk_picks.remove(0);
15681 }
15682 }
15683 for p in std::mem::take(&mut self.dspark_pending) {
15686 let Some(i) = position.checked_sub(p.0 + 1) else {
15687 continue;
15688 };
15689 let mut p = p;
15690 if i < p.1.len() {
15691 if p.2 && p.1[i] == token_id {
15692 p.3 = i + 1;
15693 } else {
15694 p.2 = false;
15695 }
15696 if i + 1 < p.1.len() {
15697 self.dspark_pending.push(p);
15698 continue;
15699 }
15700 }
15701 self.dspark_hist.push(p.3);
15702 self.dspark_real.push(token_id);
15703 }
15704 let Some(b) = &mut self.dsv4 else { return };
15705 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15706 let n_layers = layers.len();
15707 if self.dspark.is_none() {
15708 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15709 if t.is_empty() {
15710 return;
15711 }
15712 eprintln!(
15713 "DSpark: capture from layers {t:?}, block {}",
15714 crate::dsv4::dspark_block()
15715 );
15716 crate::dsv4::dspark_arm(&t, cfg.dim);
15717 self.dspark = Some(crate::dsv4::DsparkState::new(
15718 self.dsv4_mtp.len(),
15719 &cfg,
15720 t.len(),
15721 ));
15722 }
15723 let ds = self.dspark.as_mut().unwrap();
15724 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15725 return; }
15727 let mut conf = Vec::new();
15728 crate::dsv4::pick_tally_arm();
15729 let draft_started = std::time::Instant::now();
15734 #[cfg(feature = "gpu")]
15735 let gpu_draft = crate::dsv4::dspark_gpu_on();
15736 #[cfg(not(feature = "gpu"))]
15737 let gpu_draft = false;
15738 let props = if gpu_draft {
15739 #[cfg(feature = "gpu")]
15740 {
15741 let kv_id = b.3.kv_id;
15742 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15743 Some(pk) => crate::dsv4::dspark_draft_gpu(
15744 g,
15745 &self.dsv4_mtp,
15746 &cfg,
15747 ds,
15748 pk,
15749 kv_id,
15750 token_id,
15751 position,
15752 self.pool.as_deref(),
15753 &mut conf,
15754 ),
15755 None => Vec::new(),
15756 }
15757 }
15758 #[cfg(not(feature = "gpu"))]
15759 Vec::new()
15760 } else {
15761 crate::gpu::cpu_scope(|| {
15762 crate::dsv4::dspark_draft(
15763 g,
15764 &self.dsv4_mtp,
15765 &cfg,
15766 ds,
15767 token_id,
15768 position,
15769 self.pool.as_deref(),
15770 &mut conf,
15771 )
15772 })
15773 };
15774 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15775 let draft_picks = crate::dsv4::pick_tally_take();
15776 crate::dsv4::dspark_freq_note(&draft_picks);
15777 crate::dsv4::pick_tally_arm();
15780 if !props.is_empty() {
15781 let (tu, tt) = {
15785 let flat: Vec<(usize, Vec<usize>)> = self
15786 .dspark_trunk_picks
15787 .iter()
15788 .flat_map(|v| v.iter().cloned())
15789 .collect();
15790 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15792 std::collections::HashMap::new();
15793 for (li, picks) in flat {
15794 per.entry(li).or_default().extend(picks);
15795 }
15796 let n = per.len().max(1);
15797 let mut u = 0usize;
15798 let mut t = 0usize;
15799 for (_, v) in per {
15800 t += v.len();
15801 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15802 }
15803 (u / n, t / n)
15804 };
15805 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15806 self.dspark_exp.push((tu, tt, du, dt));
15807 self.dspark_pending.push((position, props, true, 0));
15808 }
15809 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15810 let n = self.dspark_hist.len() as f32;
15811 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15812 let block = crate::dsv4::dspark_block();
15813 let mut at = vec![0usize; block + 1];
15814 for &k in &self.dspark_hist {
15815 at[k] += 1;
15816 }
15817 let mut surv = Vec::with_capacity(block);
15819 for i in 1..=block {
15820 let k = at[i..].iter().sum::<usize>() as f32 / n;
15821 surv.push(format!("{k:.2}"));
15822 }
15823 let distinct = self
15824 .dspark_real
15825 .iter()
15826 .collect::<std::collections::HashSet<_>>()
15827 .len();
15828 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15829 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15830 });
15831 let m = self.dspark_exp.len().max(1);
15832 eprintln!(
15833 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
15834 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
15835 self.dspark_hist.len(),
15836 mean + 1.0,
15837 surv.join(" ")
15838 );
15839 eprintln!(
15840 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
15841 trunk experts {}/{} per layer over {block} tokens, \
15842 draft {}/{} per block, draft {:.2} ms/block",
15843 self.dspark_real.len(),
15844 tu / m,
15845 tt / m,
15846 du / m,
15847 dt / m,
15848 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15849 );
15850 }
15851 }
15852
15853 fn forward_layers_upto(
15854 &mut self,
15855 hidden: &[f32],
15856 position: usize,
15857 task_mask: Option<&TaskMask>,
15858 upto: Option<usize>,
15859 ) -> Vec<f32> {
15860 if let Some(plan) = self.gpu_plan.clone() {
15866 if upto.is_none() && plan.len() > 1 {
15867 let mut h = hidden.to_vec();
15868 for &(dev, from, upto_incl) in plan.iter() {
15869 h = crate::gpu::with_device(dev, || {
15870 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15871 });
15872 }
15873 return h;
15874 }
15875 }
15876 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15877 }
15878
15879 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15884 self.set_gpu_plan_at(devices, None)
15885 }
15886
15887 pub fn set_gpu_plan_at(
15891 &mut self,
15892 devices: Option<&[usize]>,
15893 at: Option<usize>,
15894 ) -> Result<(), String> {
15895 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15896 self.gpu_plan = None;
15897 return Ok(());
15898 };
15899 self.split_supported()?;
15900 let n = self.num_layers;
15901 if devs.len() > n {
15902 return Err(format!("{} devices for {n} layers", devs.len()));
15903 }
15904 if let Some(k) = at {
15905 if k == 0 || k >= n {
15906 return Err(format!("split at {k}: the model has {n} layers"));
15907 }
15908 if devs.len() == 2 {
15909 self.gpu_plan = Some(std::sync::Arc::new(vec![
15910 (devs[0], 0, k - 1),
15911 (devs[1], k, n - 1),
15912 ]));
15913 return Ok(());
15914 }
15915 return Err(format!(
15916 "an explicit split point takes exactly 2 devices, got {}",
15917 devs.len()
15918 ));
15919 }
15920 let per = n.div_ceil(devs.len());
15921 let mut plan = Vec::with_capacity(devs.len());
15922 let mut from = 0usize;
15923 for &d in devs {
15924 if from >= n {
15925 break;
15926 }
15927 let upto = (from + per - 1).min(n - 1);
15928 plan.push((d, from, upto));
15929 from = upto + 1;
15930 }
15931 self.gpu_plan = Some(std::sync::Arc::new(plan));
15932 Ok(())
15933 }
15934
15935 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15937 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15938 }
15939
15940 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15946 if let Some(x) = t.as_f32() {
15947 return x.to_vec();
15948 }
15949 let mut out = vec![0.0; t.rows() * t.cols()];
15950 for r in 0..t.rows() {
15951 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15952 }
15953 out
15954 }
15955
15956 fn embryo_resident_eligible(&self) -> bool {
15957 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15960 || self.num_layers != self.physical_layers
15961 || self.loop_final_norm
15962 || self.weights.layers.len() != self.num_layers
15963 || self.head_clusters.is_none()
15964 || self.final_softcap.is_some()
15965 || self.logit_multiplier.is_some()
15966 || self.attn_softcap != 0.0
15967 || self.mtp.is_some()
15968 || self.per_token_stack()
15969 || self.dsv4.is_some()
15970 || self.dsv41.is_some()
15971 || self.qwen4_exp.is_some()
15972 || self.dyn_router.is_some()
15979 || self.dyn_phi_layer.is_some()
15980 || self.dyn_blend_loaded
15981 || self.o1_cfg.is_some()
15982 || self.swa.is_some()
15983 || self.sliding_layers.is_some()
15984 || self.global_attn.is_some()
15985 || self.attention_heads_per_layer.is_some()
15986 || self.attn_v_norm
15987 || self
15988 .kv_cache
15989 .layers
15990 .iter()
15991 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15992 || self.rope_scale != 1.0
15993 || self.rope_scale_local != 1.0
15994 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15995 || self.hidden_size == 0
15996 || self.hidden_size > 1024
15997 || self.intermediate_size > 1024
15998 || self.num_heads == 0
15999 || self.num_kv_heads == 0
16000 || self.num_heads % self.num_kv_heads != 0
16001 || self.num_heads.saturating_mul(self.head_dim) > 1024
16002 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16003 || self.vocab_size == 0
16004 || self.kv_cache.max_seq_len == 0
16005 || self.rotary_dim == 0
16006 || self.rotary_dim > self.head_dim
16007 || self.rotary_dim % 2 != 0
16008 || self.inv_freq.len() < self.rotary_dim / 2
16009 {
16010 return false;
16011 }
16012 if self.weights.lm_head.as_f32().is_none()
16023 || self.weights.embed_tokens.as_f32().is_none()
16024 || self.weights.lm_head.rows() < self.vocab_size
16025 || self.weights.lm_head.cols() != self.hidden_size
16026 || self.weights.embed_tokens.rows() < self.vocab_size
16027 || self.weights.embed_tokens.cols() != self.hidden_size
16028 || self.weights.final_norm.len() != self.hidden_size
16029 {
16030 return false;
16031 }
16032 if let Some(cfg) = self.vmf_cfg {
16033 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16034 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16035 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16036 || cfg.state_len() == 0
16037 {
16038 return false;
16039 }
16040 }
16041 if let Some(g) = self.gdn_cfg {
16042 if g.num_v_heads == 0
16047 || g.num_k_heads == 0
16048 || g.num_v_heads % g.num_k_heads != 0
16049 || g.key_head_dim == 0
16050 || g.key_head_dim > 128
16051 || g.value_head_dim == 0
16052 || g.value_head_dim > 256
16053 || g.value_head_dim % 4 != 0
16054 || g.conv_kernel == 0
16055 || g.num_v_heads > 512
16056 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16057 || g.conv_dim() > 2048
16058 || g.conv_dim() % 4 != 0
16059 || g.hidden_size != self.hidden_size
16060 || g.output_gate_sigmoid
16061 || g.rms_eps != self.rms_eps
16062 || g.state_len() == 0
16063 {
16064 return false;
16065 }
16066 }
16067 let mut full_seen = false;
16068 for lw in &self.weights.layers {
16069 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16070 return false;
16071 }
16072 match &lw.attn {
16073 AttnKind::LinearGdn(w) => {
16074 let Some(g) = self.gdn_cfg else {
16075 return false;
16076 };
16077 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16078 if w.in_proj_qkv.rows() != g.conv_dim()
16079 || w.in_proj_qkv.cols() != self.hidden_size
16080 || w.in_proj_qkv.as_f32().is_none()
16081 || w.in_proj_z.rows() != nv * dv
16082 || w.in_proj_z.cols() != self.hidden_size
16083 || w.in_proj_z.as_f32().is_none()
16084 || w.in_proj_a.rows() != nv
16085 || w.in_proj_a.cols() != self.hidden_size
16086 || w.in_proj_a.as_f32().is_none()
16087 || w.in_proj_b.rows() != nv
16088 || w.in_proj_b.cols() != self.hidden_size
16089 || w.in_proj_b.as_f32().is_none()
16090 || w.conv1d.len() != g.conv_dim() * kk
16091 || w.a_log.len() != nv
16092 || w.dt_bias.len() != nv
16093 || w.norm.len() != dv
16094 || w.out_proj.rows() != self.hidden_size
16095 || w.out_proj.cols() != nv * dv
16096 || w.out_proj.as_f32().is_none()
16097 {
16098 return false;
16099 }
16100 }
16101 AttnKind::Linear(w) => {
16102 let Some(cfg) = self.vmf_cfg else {
16103 return false;
16104 };
16105 if w.thq.rows() != cfg.num_heads * cfg.nphase
16106 || w.thq.cols() != self.hidden_size
16107 || w.thq.as_f32().is_none()
16108 || w.thk.rows() != cfg.num_heads * cfg.nphase
16109 || w.thk.cols() != self.hidden_size
16110 || w.thk.as_f32().is_none()
16111 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16112 || w.v_proj.cols() != self.hidden_size
16113 || w.v_proj.as_f32().is_none()
16114 || w.out_proj.rows() != self.hidden_size
16115 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16116 || w.out_proj.as_f32().is_none()
16117 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16118 {
16119 return false;
16120 }
16121 if let Some((kg, kb)) = &w.k_gate {
16122 if kg.rows() != cfg.num_heads
16123 || kg.cols() != self.hidden_size
16124 || kg.as_f32().is_none()
16125 || kb.len() != cfg.num_heads
16126 {
16127 return false;
16128 }
16129 }
16130 if let Some(conv) = &w.conv {
16131 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16132 return false;
16133 }
16134 }
16135 }
16136 AttnKind::Full {
16137 wq,
16138 wk,
16139 wv,
16140 wo,
16141 q_norm,
16142 k_norm,
16143 output_gate,
16144 softplus_gate,
16145 bias,
16146 } => {
16147 if full_seen
16148 || q_norm.is_some()
16149 || k_norm.is_some()
16150 || *output_gate
16151 || softplus_gate.is_some()
16152 || bias.is_some()
16153 || wq.as_f32().is_none()
16154 || wk.as_f32().is_none()
16155 || wv.as_f32().is_none()
16156 || wo.as_f32().is_none()
16157 || wq.rows() != self.num_heads * self.head_dim
16158 || wk.rows() != self.num_kv_heads * self.head_dim
16159 || wv.rows() != self.num_kv_heads * self.head_dim
16160 || wq.cols() != self.hidden_size
16161 || wk.cols() != self.hidden_size
16162 || wv.cols() != self.hidden_size
16163 || wo.rows() != self.hidden_size
16164 || wo.cols() != self.num_heads * self.head_dim
16165 {
16166 return false;
16167 }
16168 full_seen = true;
16169 }
16170 AttnKind::Bounded(w) => {
16171 let Some(ac) = self.anchor_core.as_ref() else {
16174 return false;
16175 };
16176 if self.bounded_rope.is_none()
16177 || w.window != ac.window
16178 || w.sink != ac.sink
16179 || w.window == 0
16180 || w.window + w.sink > 256
16181 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16182 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16183 || w.wq.as_f32().is_none()
16184 || w.wk.as_f32().is_none()
16185 || w.wv.as_f32().is_none()
16186 || w.wo.as_f32().is_none()
16187 || w.wq.rows() != self.num_heads * self.head_dim
16188 || w.wk.rows() != self.num_kv_heads * self.head_dim
16189 || w.wv.rows() != self.num_kv_heads * self.head_dim
16190 || w.wq.cols() != self.hidden_size
16191 || w.wk.cols() != self.hidden_size
16192 || w.wv.cols() != self.hidden_size
16193 || w.wo.rows() != self.hidden_size
16194 || w.wo.cols() != self.num_heads * self.head_dim
16195 {
16196 return false;
16197 }
16198 }
16199 _ => return false,
16200 }
16201 match &lw.ffn {
16202 FfnKind::Dense(d) => {
16203 if d.act != Act::Silu
16204 || !d.segs.is_empty()
16205 || d.gate_proj.as_f32().is_none()
16206 || d.up_proj.as_f32().is_none()
16207 || d.down_proj.as_f32().is_none()
16208 || d.gate_proj.rows() != self.intermediate_size
16209 || d.gate_proj.cols() != self.hidden_size
16210 || d.up_proj.rows() != self.intermediate_size
16211 || d.up_proj.cols() != self.hidden_size
16212 || d.down_proj.rows() != self.hidden_size
16213 || d.down_proj.cols() != self.intermediate_size
16214 {
16215 return false;
16216 }
16217 }
16218 FfnKind::Moe(m) => {
16219 if m.resonance.is_none()
16220 || m.top_k != 1
16221 || m.router_sigmoid
16222 || !m.norm_topk_prob
16223 || m.expert_bias.is_some()
16224 || m.routed_scaling != 1.0
16225 || m.route_tau.is_some()
16226 || m.shared.is_none()
16227 || m.mask.is_some()
16228 || m.per_expert_scale.is_some()
16229 || m.router_input_norm
16230 || m.experts.is_empty()
16231 || m.experts.len() > 8
16232 {
16233 return false;
16234 }
16235 let r = m.resonance.as_ref().unwrap();
16236 if r.mu.len() != m.experts.len() * self.hidden_size
16237 || r.bias.len() != m.experts.len()
16238 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16239 || r.k > 128
16240 {
16241 return false;
16242 }
16243 let Some((shared, gate)) = &m.shared else {
16244 return false;
16245 };
16246 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16247 return false;
16248 }
16249 if shared.gate_proj.as_f32().is_none()
16250 || shared.up_proj.as_f32().is_none()
16251 || shared.down_proj.as_f32().is_none()
16252 || shared.gate_proj.rows() != self.intermediate_size
16253 || shared.gate_proj.cols() != self.hidden_size
16254 || shared.up_proj.rows() != self.intermediate_size
16255 || shared.up_proj.cols() != self.hidden_size
16256 || shared.down_proj.rows() != self.hidden_size
16257 || shared.down_proj.cols() != self.intermediate_size
16258 {
16259 return false;
16260 }
16261 for e in &m.experts {
16262 if e.act != Act::Silu
16263 || !e.segs.is_empty()
16264 || e.gate_proj.as_f32().is_none()
16265 || e.up_proj.as_f32().is_none()
16266 || e.down_proj.as_f32().is_none()
16267 || e.gate_proj.rows() != self.intermediate_size
16268 || e.gate_proj.cols() != self.hidden_size
16269 || e.up_proj.rows() != self.intermediate_size
16270 || e.up_proj.cols() != self.hidden_size
16271 || e.down_proj.rows() != self.hidden_size
16272 || e.down_proj.cols() != self.intermediate_size
16273 {
16274 return false;
16275 }
16276 }
16277 }
16278 FfnKind::DenseMoe(_) => return false,
16279 }
16280 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16281 return false;
16282 }
16283 }
16284 if full_seen && self.anchor_core.is_some() {
16285 return false;
16286 }
16287 full_seen || self.num_layers > 0
16288 }
16289
16290 fn embryo_resident_wanted(&self) -> bool {
16295 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16296 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16297 && matches!(
16298 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16299 Ok("1") | Ok("parallel")
16300 )
16301 && crate::gpu::enabled_here()
16302 && !self.graph_refused()
16303 && self.embryo_resident_eligible()
16304 }
16305
16306 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16314 if ids.len() < 2
16315 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16316 || !self.embryo_resident_wanted()
16317 {
16318 return None;
16319 }
16320 let model = self.ensure_embryo_graph()?;
16321 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16322 .ok()
16323 .and_then(|v| v.parse::<usize>().ok())
16324 .filter(|&v| v >= 1)
16325 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16326 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16327 let hs = self.hidden_size;
16328 let n = ids.len();
16329 let mut pos = start;
16330 let mut last = None;
16331 let mut rows = Vec::with_capacity(cmax * hs);
16332 while pos < n {
16333 let end = (pos + cmax).min(n);
16334 rows.clear();
16335 for &id in &ids[pos..end] {
16336 rows.extend_from_slice(&self.embed_single(id));
16337 }
16338 let mut lg = Vec::new();
16339 if !crate::gpu::forward_embryo_graph_chunk(
16340 &model,
16341 self.graph_kv_id,
16342 &rows,
16343 pos,
16344 end - pos,
16345 &mut lg,
16346 ) {
16347 if pos == start {
16348 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16349 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16350 }
16351 return None;
16352 }
16353 self.kv_cache.clear();
16357 self.clear_history();
16358 crate::gpu::graph_kv_reset(self.graph_kv_id);
16359 panic!(
16360 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16361 );
16362 }
16363 last = Some(lg);
16364 pos = end;
16365 }
16366 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16367 eprintln!(
16368 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16369 n - start,
16370 (n - start).div_ceil(cmax)
16371 );
16372 }
16373 last
16374 }
16375
16376 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16377 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16378 const UMAX: u32 = u32::MAX;
16379 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16380 const REC: usize = 64;
16381 struct Pack {
16382 data: Vec<f32>,
16383 }
16384 impl Pack {
16385 fn put(&mut self, x: &[f32]) -> u32 {
16386 if x.is_empty() {
16387 return u32::MAX;
16388 }
16389 let off = self.data.len();
16390 self.data.extend_from_slice(x);
16391 off as u32
16392 }
16393 }
16394 let vmf = self.vmf_cfg;
16398 let gdn = self.gdn_cfg;
16399 let mut pack = Pack { data: Vec::new() };
16400 let mut meta = vec![0u32; HEADER];
16401 meta[0] = self.hidden_size as u32;
16402 meta[1] = self.intermediate_size as u32;
16403 meta[2] = self.vocab_size as u32;
16404 meta[3] = self.num_layers as u32;
16405 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16406 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16407 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16408 if let Some(g) = gdn {
16409 meta[24] = g.num_v_heads as u32;
16410 meta[25] = g.num_k_heads as u32;
16411 meta[26] = g.key_head_dim as u32;
16412 meta[27] = g.value_head_dim as u32;
16413 meta[28] = g.conv_kernel as u32;
16414 meta[29] = g.conv_dim() as u32;
16415 }
16416 meta[7] = self.num_heads as u32;
16417 meta[8] = self.num_kv_heads as u32;
16418 meta[9] = self.head_dim as u32;
16419 meta[10] = self.kv_cache.max_seq_len as u32;
16420 let clusters = self.head_clusters.as_ref().unwrap();
16421 let cluster_count = clusters.len() / self.hidden_size;
16422 if clusters.len() % self.hidden_size != 0
16423 || cluster_count == 0
16424 || cluster_count > 1024
16425 || self.vocab_size % cluster_count != 0
16426 || self.weights.lm_head.rows() < self.vocab_size
16427 || self.weights.final_norm.len() != self.hidden_size
16428 {
16429 return None;
16430 }
16431 meta[11] = cluster_count as u32;
16432 meta[12] = (self.vocab_size / cluster_count) as u32;
16433 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16434 meta[16] = self.rotary_dim as u32;
16435 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16436 meta[19] = (self.rms_eps as f32).to_bits();
16437 let max_conv = self
16438 .weights
16439 .layers
16440 .iter()
16441 .filter_map(|lw| match &lw.attn {
16442 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16443 _ => None,
16444 })
16445 .max()
16446 .unwrap_or(1);
16447 let phase_stride = vmf
16453 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16454 .unwrap_or(0);
16455 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16456 let state_stride = phase_stride.max(gdn_stride);
16457 let bounded = self.anchor_core.clone();
16461 let (anchor_window, anchor_sink) = bounded
16462 .as_ref()
16463 .map(|ac| (ac.window, ac.sink))
16464 .unwrap_or((0, 0));
16465 let kv_stride = if bounded.is_some() {
16466 2usize
16467 .saturating_mul(self.num_kv_heads)
16468 .saturating_mul(anchor_window)
16469 .saturating_mul(self.head_dim)
16470 } else {
16471 2usize
16472 .saturating_mul(self.num_kv_heads)
16473 .saturating_mul(self.kv_cache.max_seq_len)
16474 .saturating_mul(self.head_dim)
16475 };
16476 meta[14] = state_stride as u32;
16477 meta[15] = kv_stride as u32;
16478 meta[18] = anchor_window as u32;
16479 meta[20] = anchor_sink as u32;
16480 meta[21] = match &self.bounded_rope {
16481 Some(rope) => {
16482 let off = pack.put(&rope.cos);
16484 let _ = pack.put(&rope.sin);
16485 off
16486 }
16487 None => UMAX,
16488 };
16489 let mut full_seen = false;
16490 let mut bounded_seen = 0usize;
16491 let mut phase_seen = 0usize;
16495 let mut gdn_seen = 0usize;
16496 for (li, lw) in self.weights.layers.iter().enumerate() {
16497 let base = meta.len();
16498 meta.resize(base + REC, UMAX);
16499 meta[base] = match &lw.attn {
16500 AttnKind::Linear(w) if w.phase_delta => 1,
16501 AttnKind::Linear(_) => 0,
16502 AttnKind::Full { .. } => 2,
16503 AttnKind::Bounded(_) => 3,
16504 AttnKind::LinearGdn(_) => 4,
16505 _ => UMAX,
16506 };
16507 meta[base + 1] = pack.put(&lw.input_norm);
16508 meta[base + 2] = pack.put(&lw.post_norm);
16509 meta[base + 25] = match &lw.attn {
16510 AttnKind::Linear(_) => {
16511 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16512 phase_seen += 1;
16513 off
16514 }
16515 AttnKind::LinearGdn(_) => {
16516 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16517 gdn_seen += 1;
16518 off
16519 }
16520 _ => UMAX,
16521 };
16522 match &lw.attn {
16523 AttnKind::LinearGdn(w) => {
16524 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16527 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16528 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16529 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16530 meta[base + 60] = pack.put(&w.conv1d);
16531 meta[base + 61] = pack.put(&w.a_log);
16532 meta[base + 62] = pack.put(&w.dt_bias);
16533 meta[base + 63] = pack.put(&w.norm);
16534 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16535 meta[base + 24] = 0;
16536 }
16537 AttnKind::Linear(w) => {
16538 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16539 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16540 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16541 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16542 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16543 meta[base + 7] = pack.put(&decay);
16544 if let Some((kg, kb)) = &w.k_gate {
16545 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16546 meta[base + 9] = pack.put(kb);
16547 }
16548 if let Some(conv) = &w.conv {
16549 meta[base + 10] = pack.put(conv);
16550 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16551 } else {
16552 meta[base + 24] = 0;
16553 }
16554 }
16555 AttnKind::Full { wq, wk, wv, wo, .. } => {
16556 full_seen = true;
16557 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16558 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16559 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16560 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16561 meta[base + 26] = (li * kv_stride) as u32;
16562 }
16563 AttnKind::Bounded(w) => {
16564 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16565 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16566 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16567 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16568 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16570 meta[base + 27] = pack.put(&w.sink_k);
16571 meta[base + 28] = pack.put(&w.sink_v);
16572 bounded_seen += 1;
16573 }
16574 _ => return None,
16575 }
16576 match &lw.ffn {
16577 FfnKind::Dense(d) => {
16578 meta[base + 15] = 0;
16579 meta[base + 16] = 0;
16580 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16581 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16582 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16583 }
16584 FfnKind::Moe(m) => {
16585 let r = m.resonance.as_ref().unwrap();
16586 let (shared, _) = m.shared.as_ref().unwrap();
16587 meta[base + 15] = 1;
16588 meta[base + 16] = m.experts.len() as u32;
16589 meta[base + 17] = pack.put(&r.mu);
16590 meta[base + 18] = pack.put(&r.u);
16591 meta[base + 19] = pack.put(&r.bias);
16592 meta[base + 20] = r.k as u32;
16593 let mut shell = r.effective_shell(m.experts.len());
16602 shell.push(f32::NEG_INFINITY);
16603 meta[base + 30] = pack.put(&shell);
16604 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16605 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16606 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16607 for (e, ex) in m.experts.iter().enumerate() {
16608 meta[base + 32 + e * 3] =
16609 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16610 meta[base + 33 + e * 3] =
16611 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16612 meta[base + 34 + e * 3] =
16613 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16614 }
16615 }
16616 FfnKind::DenseMoe(_) => return None,
16617 }
16618 }
16619 if !full_seen && self.num_layers == 0 {
16620 return None;
16621 }
16622 let id = {
16623 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16624 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16625 };
16626 let model = crate::gpu::EmbryoGraphModel {
16627 id,
16628 hidden: self.hidden_size,
16629 intermediate: self.intermediate_size,
16630 vocab: self.vocab_size,
16631 layers: self.num_layers,
16632 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16633 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16634 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16635 anchor_q_heads: self.num_heads,
16636 anchor_kv_heads: self.num_kv_heads,
16637 anchor_head_dim: self.head_dim,
16638 rotary_dim: self.rotary_dim,
16639 max_seq: self.kv_cache.max_seq_len,
16640 cluster_count,
16641 cluster_size: self.vocab_size / cluster_count,
16642 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16643 state_stride,
16644 kv_stride,
16645 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16646 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16647 weights: pack.data,
16648 meta,
16649 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16650 clusters: clusters.as_ref().clone(),
16651 final_norm: self.weights.final_norm.clone(),
16652 inv_freq: self.inv_freq.as_ref().clone(),
16653 bounded: bounded.is_some(),
16654 kv_layers: if bounded.is_some() {
16655 bounded_seen
16656 } else {
16657 self.num_layers
16658 },
16659 state_layers: phase_seen + gdn_seen,
16660 anchor_window,
16661 anchor_sink,
16662 phase_layers: phase_seen,
16663 gdn_layers: gdn_seen,
16664 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16665 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16666 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16667 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16668 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16669 };
16670 self.embryo_graph = Some(std::sync::Arc::new(model));
16671 }
16672 self.embryo_graph.clone()
16673 }
16674
16675 fn forward_layers_span(
16676 &mut self,
16677 hidden: &[f32],
16678 position: usize,
16679 task_mask: Option<&TaskMask>,
16680 from: usize,
16681 upto: Option<usize>,
16682 ) -> Vec<f32> {
16683 #[cfg(target_os = "macos")]
16684 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16685 position,
16686 from,
16687 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16688 );
16689 debug_assert!(
16690 from == 0
16691 || (self.dsv4.is_none()
16692 && self.dsv41.is_none()
16693 && self.qwen4_exp.is_none()
16694 && !self.per_token_stack())
16695 );
16696 #[cfg(target_os = "macos")]
16702 if !crate::gpu_metal::wait_replay() {
16703 self.fail_metal_graph("the pending async replay failed before a plain forward");
16704 return vec![0.0; self.hidden_size];
16705 }
16706 if let Some(b) = &mut self.qwen4_exp {
16707 let _ = (task_mask, upto);
16708 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16709 let mut logits = Vec::new();
16710 crate::qwen4_exp::forward_token(
16711 &b.0,
16712 &b.1,
16713 &b.2,
16714 &mut b.3,
16715 token_id,
16716 position,
16717 &self.inv_freq,
16718 self.pool.as_deref(),
16719 &mut logits,
16720 true,
16721 );
16722 self.graph_logits = Some(logits);
16723 return vec![0.0; self.hidden_size];
16724 }
16725 if let Some(b) = &mut self.dsv4 {
16731 let _ = (task_mask, upto);
16732 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16733 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16734 st.pos = position;
16735 let mut logits = Vec::new();
16736 crate::dsv4::forward_token(
16737 g,
16738 layers,
16739 &cfg,
16740 st,
16741 token_id,
16742 &self.inv_freq,
16743 self.pool.as_deref(),
16744 &mut logits,
16745 );
16746 self.graph_logits = Some(logits);
16747 self.dspark_probe(position, token_id);
16748 return vec![0.0; self.hidden_size];
16751 }
16752 if let Some(b) = &mut self.dsv41 {
16754 let _ = (task_mask, upto);
16755 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16756 let mut logits = Vec::new();
16757 crate::dsv41::forward_token(
16758 &b.0,
16759 &b.1,
16760 &b.2,
16761 &mut b.3,
16762 token_id,
16763 position,
16764 self.pool.as_deref(),
16765 &mut logits,
16766 );
16767 self.graph_logits = Some(logits);
16768 return vec![0.0; self.hidden_size];
16769 }
16770 if let Some(b) = &self.g4e {
16776 let _ = (task_mask, upto);
16777 return crate::g4e::g4e_forward(
16778 &b.0,
16779 &b.1,
16780 hidden,
16781 position,
16782 &mut self.kv_cache.layers,
16783 self.num_heads,
16784 self.num_kv_heads,
16785 self.pool.as_deref(),
16786 );
16787 }
16788 if let Some(b) = &self.g3n {
16789 let _ = (task_mask, upto);
16790 return crate::g3n::g3n_forward(
16791 &b.0,
16792 &b.1,
16793 hidden,
16794 position,
16795 &mut self.kv_cache.layers,
16796 self.num_heads,
16797 self.num_kv_heads,
16798 self.head_dim,
16799 self.pool.as_deref(),
16800 );
16801 }
16802 if from == 0
16809 && upto.is_none()
16810 && task_mask.is_none()
16811 && self.anchor_core.is_some()
16812 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16813 {
16814 static ONCE: std::sync::Once = std::sync::Once::new();
16815 ONCE.call_once(|| {
16816 eprintln!(
16817 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16818 unsupported={} eligible={}",
16819 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16820 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16821 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16822 crate::gpu::enabled_here(),
16823 self.graph_refused(),
16824 self.embryo_resident_eligible(),
16825 );
16826 });
16827 }
16828 if from == 0
16829 && upto.is_none()
16830 && task_mask.is_none()
16831 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16835 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16836 && matches!(
16840 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16841 Ok("1") | Ok("parallel")
16842 )
16843 && crate::gpu::enabled_here()
16844 && !self.graph_refused()
16845 && (position == 0 || self.device_sequence_position().is_some())
16851 && self.embryo_resident_eligible()
16852 && let Some(model) = self.ensure_embryo_graph()
16853 {
16854 let mut lg = Vec::new();
16855 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16856 {
16857 self.graph_logits = Some(lg);
16858 return vec![0.0; self.hidden_size];
16859 }
16860 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16861 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16862 }
16863 self.mark_graph_refused();
16869 if position != 0 {
16870 self.kv_cache.clear();
16874 self.clear_history();
16875 crate::gpu::graph_kv_reset(self.graph_kv_id);
16876 panic!(
16877 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16878 );
16879 }
16880 }
16881 let mut h = hidden.to_vec();
16882 self.mimo_moe_prepare();
16885 let _mimo_q8 = self
16886 .mimo_moe
16887 .is_on()
16888 .then(crate::qtensor::enter_full_gpu_q8_scope);
16889 let (nh, _nkv, _hd, hs, _rd, eps) = (
16892 self.num_heads,
16893 self.num_kv_heads,
16894 self.head_dim,
16895 self.hidden_size,
16896 self.rotary_dim,
16897 self.rms_eps,
16898 );
16899 let pool = self.pool.clone();
16900 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
16912 let graph_on = match graph_env.as_deref() {
16913 Some("0") => false,
16914 Some("prefill") => false, Some(_) => true,
16916 None => crate::gpu::wgpu_graph_default(),
16922 };
16923 let graph_trusted =
16924 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
16925 let race_eligible =
16926 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
16927 let mut tail_start = 0usize;
16928 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
16929 let t_graph = std::time::Instant::now();
16930 let mut lg = Vec::new();
16931 let mut gl = 0usize;
16932 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16933 let declined = built.is_none();
16934 let built = match built {
16935 Some(Ok(hh)) => Some(hh),
16936 Some(Err(())) => {
16937 self.clear_sequence_state();
16941 self.graph_failed
16942 .store(true, std::sync::atomic::Ordering::Relaxed);
16943 self.cancel
16944 .store(true, std::sync::atomic::Ordering::Relaxed);
16945 tracing::error!("token graph failed after admission; sequence state cleared");
16946 return vec![0.0; self.hidden_size];
16947 }
16948 None => None,
16949 };
16950 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16955 self.mark_graph_refused();
16956 }
16957 graph_note(built.is_some(), gl, self.num_layers);
16958 if let Some(hh) = built {
16959 let dur = t_graph.elapsed();
16960 if std::env::var("CMF_GRAPH_PROF").is_ok() {
16961 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16962 }
16963 if gl > 0 && gl < self.num_layers {
16964 h = hh;
16970 tail_start = gl;
16971 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16972 if !graph_trusted {
16973 crate::gpu::graph_race_record(true, dur);
16974 }
16975 if !lg.is_empty() {
16976 lg.resize(self.vocab_size, 0.0);
16979 if let Some(c) = self.final_softcap {
16980 softcap_in_place(&mut lg, c, self.pool.as_deref());
16981 }
16982 self.graph_logits = Some(lg);
16983 }
16984 return hh;
16985 }
16986 }
16992 }
16993 let span = from > 0 || upto.is_some();
17017 if span && graph_on && task_mask.is_none() && graph_trusted {
17018 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17019 let mut lg = Vec::new();
17020 let mut gl = 0usize;
17021 let span_res =
17022 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17023 let span_res = match span_res {
17024 Some(Ok(hh)) => Some(hh),
17025 Some(Err(())) => {
17026 self.clear_sequence_state();
17027 self.graph_failed
17028 .store(true, std::sync::atomic::Ordering::Relaxed);
17029 self.cancel
17030 .store(true, std::sync::atomic::Ordering::Relaxed);
17031 tracing::error!(
17032 "span token graph failed after admission; sequence state cleared"
17033 );
17034 return vec![0.0; self.hidden_size];
17035 }
17036 None => None,
17037 };
17038 graph_note(span_res.is_some(), gl, upto_excl - from);
17039 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17040 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17044 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17045 eprintln!(
17046 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17047 upto_excl - from,
17048 span_res.is_some()
17049 );
17050 }
17051 }
17052 if let Some(hh) = span_res {
17053 if gl == upto_excl - from {
17054 if !lg.is_empty() {
17055 lg.resize(self.vocab_size, 0.0);
17056 if let Some(c) = self.final_softcap {
17057 softcap_in_place(&mut lg, c, self.pool.as_deref());
17058 }
17059 self.graph_logits = Some(lg);
17060 }
17061 crate::gpu::set_layer(-1);
17062 return hh;
17063 }
17064 h = hh;
17066 tail_start = from + gl;
17067 }
17068 }
17069 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17074
17075 let host_tail = tail_start > from;
17085 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17086 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17087
17088 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17089 #[cfg(target_os = "macos")]
17090 let mut gpu_skip_until = 0usize;
17091 for li in tail_start.max(from)..self.num_layers {
17092 let _capacity_tail = automatic_gpu_prefix
17093 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17094 .map(|_| crate::gpu::enter_cpu_scope());
17095 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17097 if li > u {
17098 break;
17099 }
17100 }
17101 if let Some(mask) = task_mask {
17102 if !mask.layer_alive(li) {
17103 continue; }
17105 }
17106 if host_tail && task_mask.is_none() {
17111 match self.m3_pooled_layer(li, position, &mut h) {
17112 Ok(true) => {
17113 crate::gpu::set_layer(-1);
17114 continue;
17115 }
17116 Ok(false) => {}
17117 Err(()) => {
17118 self.clear_sequence_state();
17119 self.graph_failed
17120 .store(true, std::sync::atomic::Ordering::Relaxed);
17121 self.cancel
17122 .store(true, std::sync::atomic::Ordering::Relaxed);
17123 tracing::error!(
17124 "pooled tail token graph failed after admission; sequence state cleared"
17125 );
17126 return vec![0.0; self.hidden_size];
17127 }
17128 }
17129 }
17130 #[cfg(target_os = "macos")]
17134 {
17135 if li < gpu_skip_until {
17136 continue;
17137 }
17138 if task_mask.is_none() {
17139 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17140 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17141 return vec![0.0; self.hidden_size];
17145 }
17146 if end > li {
17147 gpu_skip_until = end;
17148 if self.is_loop_end(end - 1) && end < self.num_layers {
17151 h = inference::rms_norm(
17152 &h,
17153 &self.weights.final_norm,
17154 self.rms_eps,
17155 self.norm_style,
17156 );
17157 }
17158 continue;
17159 }
17160 }
17161 }
17162
17163 if task_mask.is_none() {
17164 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17165 crate::gpu::BatchGraphOutcome::Completed => continue,
17166 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17167 crate::gpu::BatchGraphOutcome::Declined => {}
17168 }
17169 }
17170 #[cfg(feature = "gpu")]
17171 self.pull_lagging_host_kv(li, li + 1, position);
17172 let lw = &self.weights.layers[self.phys_layer(li)];
17173 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17174 if tp.parse::<usize>().ok() == Some(position) {
17175 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17176 eprintln!(
17177 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17178 h[0], h[1]
17179 );
17180 }
17181 }
17182 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17185 inference::rms_norm_into(
17186 &h,
17187 &lw.input_norm,
17188 self.rms_eps,
17189 self.norm_style,
17190 &mut self.ws.n1,
17191 );
17192 drop(prof);
17193
17194 let attn_out = match &lw.attn {
17195 AttnKind::Mla(w) => {
17196 let inv_freq_l = self.layer_inv_freq(li);
17197 let rs = self.layer_rope_scale(li);
17198 let eps = self.rms_eps;
17199 let pool = self.pool.clone();
17200 mla_attention(
17201 w,
17202 &self.ws.n1,
17203 &mut self.kv_cache.layers[li],
17204 position,
17205 &inv_freq_l,
17206 rs,
17207 eps,
17208 pool.as_deref(),
17209 )
17210 }
17211 AttnKind::Linear(w) => {
17212 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17213 vmf_phase_forward(
17214 &self.ws.n1,
17215 w,
17216 &cfg,
17217 &mut self.kv_cache.layers[li].linear_state,
17218 self.pool.as_deref(),
17219 )
17220 }
17221 AttnKind::Kda(w) => {
17222 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17223 crate::linear_core::kda_forward(
17224 &self.ws.n1,
17225 w,
17226 &cfg,
17227 &mut self.kv_cache.layers[li].linear_state,
17228 self.pool.as_deref(),
17229 )
17230 }
17231 AttnKind::LinearGdn(w) => {
17232 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17233 gdn_forward(
17234 &self.ws.n1,
17235 w,
17236 &cfg,
17237 &mut self.kv_cache.layers[li].linear_state,
17238 self.pool.as_deref(),
17239 )
17240 }
17241 AttnKind::ShortConv(w) => {
17242 let cfg = self
17243 .short_conv_cfg
17244 .expect("short-conv layer without short_conv_cfg");
17245 short_conv_forward(
17246 &self.ws.n1,
17247 w,
17248 &cfg,
17249 &mut self.kv_cache.layers[li].linear_state,
17250 self.pool.as_deref(),
17251 )
17252 }
17253 AttnKind::Bounded(w) => {
17254 let rope = self
17257 .bounded_rope
17258 .clone()
17259 .expect("bounded layer without an installed rotation table");
17260 let cfg = crate::bounded::BoundedAttnCfg {
17261 num_heads: self.num_heads,
17262 num_kv_heads: self.num_kv_heads,
17263 head_dim: self.head_dim,
17264 hidden_size: hs,
17265 scale: self.attn_scale,
17266 rope: &rope,
17267 pool: pool.as_deref(),
17268 };
17269 crate::bounded::bounded_attention(
17270 &self.ws.n1,
17271 w,
17272 &mut self.kv_cache.layers[li],
17273 &cfg,
17274 )
17275 }
17276 AttnKind::Full {
17277 wq,
17278 wk,
17279 wv,
17280 wo,
17281 q_norm,
17282 k_norm,
17283 output_gate,
17284 softplus_gate,
17285 bias,
17286 } if self.kv_cache.layers[li].o1_sealed() => {
17287 let inv_freq_l = self.layer_inv_freq(li);
17290 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17291 let cfg = QwenAttnCfg {
17292 num_heads: self.layer_num_heads(li),
17293 num_kv_heads: nkv_l,
17294 head_dim: hd_l,
17295 hidden_size: hs,
17296 position,
17297 inv_freq: &inv_freq_l,
17298 rotary_dim: rd_l,
17299 scale: self.attn_scale,
17300 softcap: self.attn_softcap,
17301 window: None,
17302 v_norm: self.attn_v_norm,
17303 qk_norm_after_rope: self.qk_norm_after_rope,
17304 gate_sigmoid: self.proj_gate_sigmoid,
17305 q_norm: q_norm.as_deref(),
17306 k_norm: k_norm.as_deref(),
17307 output_gate: *output_gate,
17308 softplus_gate: softplus_gate
17309 .as_ref()
17310 .map(|(gate, per_head)| (gate, *per_head)),
17311 rope_scale: self.layer_rope_scale(li),
17312 bias: bias
17313 .as_ref()
17314 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17315 rms_eps: eps,
17316 norm_style: self.norm_style,
17317 pool: pool.as_deref(),
17318 v_head_dim: self.layer_v_dim(li),
17319 };
17320 attention::qwen_attention_nystrom(
17321 &self.ws.n1,
17322 wq,
17323 wk,
17324 wv,
17325 wo,
17326 &mut self.kv_cache.layers[li],
17327 &cfg,
17328 )
17329 }
17330 AttnKind::Full {
17331 wq,
17332 wk,
17333 wv,
17334 wo,
17335 q_norm,
17336 k_norm,
17337 output_gate,
17338 softplus_gate,
17339 bias,
17340 } => 'attn: {
17341 let dropin_reason =
17346 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17347 if let Some(reason) = dropin_reason {
17348 self.note_graph_decline("wgpu attn dropin", reason);
17349 }
17350 if graph_on
17351 && dropin_reason.is_none()
17352 && !*output_gate
17353 && softplus_gate.is_none()
17354 && self.attention_heads_per_layer.is_none()
17355 && bias.is_none()
17356 && task_mask.is_none()
17357 {
17358 let inv_freq_l = self.layer_inv_freq(li);
17359 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17360 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17361 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17362 wq.mapped_q1(),
17363 wk.mapped_q1(),
17364 wv.mapped_q1(),
17365 wo.mapped_q1(),
17366 ) {
17367 let gm = gm.clone();
17368 let mut out = vec![0f32; hs];
17369 let cache = &self.kv_cache.layers[li];
17370 if crate::gpu::attn_dropin(
17371 &gm,
17372 self.graph_kv_id,
17373 li,
17374 &self.ws.n1,
17375 qi,
17376 ki,
17377 vi,
17378 oi,
17379 q_norm.as_deref(),
17380 k_norm.as_deref(),
17381 self.qk_norm_after_rope,
17382 &inv_freq_l,
17383 nh,
17384 nkv_l,
17385 hd_l,
17386 rd_l,
17387 hs,
17388 position,
17389 self.kv_cache.max_seq_len,
17390 gemma,
17391 eps as f32,
17392 cache.k_heads(),
17393 cache.v_heads(),
17394 &mut out,
17395 ) {
17396 break 'attn out;
17397 }
17398 }
17399 }
17400 let masked = task_mask
17401 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17402 .unwrap_or(false);
17403 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17404 let plain = self.layer_attn_plain(li);
17407 match (masked, f32_view) {
17408 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17411 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17412 attention::multi_head_attention(
17413 &self.ws.n1,
17414 q,
17415 k,
17416 v,
17417 o,
17418 &mut self.kv_cache.layers[li],
17419 self.num_heads,
17420 self.num_kv_heads,
17421 self.head_dim,
17422 self.hidden_size,
17423 position,
17424 &active_heads,
17425 &self.inv_freq,
17426 )
17427 }
17428 (masked, _) => {
17429 if masked {
17430 tracing::warn!(
17431 "layer {li}: head mask on quantized weights or on a \
17432 window/sink/per-layer-geometry layer not supported \
17433 yet — executing dense"
17434 );
17435 }
17436 let inv_freq_l = self.layer_inv_freq(li);
17437 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17438 let cfg = QwenAttnCfg {
17439 num_heads: self.layer_num_heads(li),
17440 num_kv_heads: nkv_l,
17441 head_dim: hd_l,
17442 hidden_size: hs,
17443 position,
17444 inv_freq: &inv_freq_l,
17445 rotary_dim: rd_l,
17446 scale: self.attn_scale,
17447 softcap: self.attn_softcap,
17448 window: self.layer_window(li),
17449 v_norm: self.attn_v_norm,
17450 qk_norm_after_rope: self.qk_norm_after_rope,
17451 gate_sigmoid: self.proj_gate_sigmoid,
17452 q_norm: q_norm.as_deref(),
17453 k_norm: k_norm.as_deref(),
17454 output_gate: *output_gate,
17455 softplus_gate: softplus_gate
17456 .as_ref()
17457 .map(|(gate, per_head)| (gate, *per_head)),
17458 rope_scale: self.layer_rope_scale(li),
17459 bias: bias
17460 .as_ref()
17461 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17462 rms_eps: eps,
17463 norm_style: self.norm_style,
17464 pool: pool.as_deref(),
17465 v_head_dim: self.layer_v_dim(li),
17466 };
17467 attention::qwen_attention(
17468 &self.ws.n1,
17469 wq,
17470 wk,
17471 wv,
17472 wo,
17473 &mut self.kv_cache.layers[li],
17474 &cfg,
17475 )
17476 }
17477 }
17478 }
17479 };
17480 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17483 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17484 None => attn_out,
17485 };
17486 let lw = &self.weights.layers[self.phys_layer(li)];
17487 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17488 inference::add_rmsnorm_fused_into(
17489 &mut h,
17490 &attn_out,
17491 &lw.post_norm,
17492 self.rms_eps,
17493 self.norm_style,
17494 &mut self.ws.p1,
17495 );
17496 drop(prof);
17497 let mut attn_out = attn_out;
17498 attention::recycle_buf(&mut attn_out);
17499 let post_normed = &self.ws.p1;
17500
17501 let ffn_masked = task_mask
17502 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17503 .unwrap_or(false);
17504 let ffn_out = match (ffn_masked, &lw.ffn) {
17516 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17520 let row = task_mask
17521 .and_then(|tm| tm.ffn_masks.get(li))
17522 .map(|v| v.as_slice());
17523 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17524 }
17525 (true, FfnKind::Dense(d)) => {
17526 let tm = task_mask.unwrap();
17527 let alive = tm.ffn_active_count(li);
17528 let deep = alive * 2 <= self.intermediate_size;
17529 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17530 let active = tm.ffn_active_indices(li);
17531 sparse_ffn_quant(
17532 d,
17533 post_normed,
17534 &active,
17535 self.hidden_size,
17536 self.pool.as_deref(),
17537 )
17538 } else if deep
17539 && let (Some(g), Some(u), Some(dn)) = (
17540 d.gate_proj.as_f32(),
17541 d.up_proj.as_f32(),
17542 d.down_proj.as_f32(),
17543 )
17544 {
17545 let active = tm.ffn_active_indices(li);
17546 inference::sparse_ffn_forward(
17547 post_normed,
17548 g,
17549 u,
17550 dn,
17551 self.hidden_size,
17552 self.intermediate_size,
17553 &active,
17554 self.pool.as_deref(),
17555 )
17556 } else {
17557 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17558 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17559 }
17560 }
17561 (true, FfnKind::Moe(m)) => {
17562 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17566 ffn_forward(
17567 &lw.ffn,
17568 post_normed,
17569 self.pool.as_deref(),
17570 allowed.as_deref(),
17571 )
17572 }
17573 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17574 dm,
17575 post_normed,
17576 &h,
17577 self.rms_eps,
17578 self.norm_style,
17579 self.pool.as_deref(),
17580 ),
17581 (false, _) => match &lw.ffn {
17582 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17583 dm,
17584 post_normed,
17585 &h,
17586 self.rms_eps,
17587 self.norm_style,
17588 self.pool.as_deref(),
17589 ),
17590 FfnKind::Moe(m)
17591 if task_mask.is_none()
17592 && host_tail
17593 && li >= tail_start
17594 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17595 {
17596 moe_ffn_minicpm_tail(
17597 &mut self.minicpm_tail,
17598 li,
17599 self.num_layers,
17600 m,
17601 post_normed,
17602 self.pool.as_deref(),
17603 )
17604 }
17605 FfnKind::Moe(m)
17606 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17607 {
17608 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17609 }
17610 _ => {
17611 let allowed = match (&lw.ffn, task_mask) {
17612 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17613 _ => None,
17614 };
17615 ffn_forward(
17616 &lw.ffn,
17617 post_normed,
17618 self.pool.as_deref(),
17619 allowed.as_deref(),
17620 )
17621 }
17622 },
17623 };
17624 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17625 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17626 None => ffn_out,
17627 };
17628 for (i, &f) in ffn_out.iter().enumerate() {
17629 h[i] += f;
17630 }
17631 let mut ffn_out = ffn_out;
17632 attention::recycle_buf(&mut ffn_out);
17633
17634 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17636 for v in h.iter_mut() {
17637 *v *= sc;
17638 }
17639 }
17640 if self.layer_dump.is_some() {
17642 self.dump_layer_row(position, li, &h);
17643 }
17644
17645 if self.is_loop_end(li) && li + 1 < self.num_layers {
17648 h = inference::rms_norm(
17649 &h,
17650 &self.weights.final_norm,
17651 self.rms_eps,
17652 self.norm_style,
17653 );
17654 }
17655
17656 if self.dyn_phi_layer == Some(li) {
17660 self.update_dyn_phi(&h);
17661 }
17662 }
17663 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17665 crate::gpu::graph_race_record(false, t.elapsed());
17666 }
17667
17668 h
17669 }
17670
17671 fn update_dyn_phi(&mut self, h: &[f32]) {
17674 const A: f32 = 0.2;
17675 if self.dyn_phi_ema.len() != h.len() {
17676 self.dyn_phi_ema = vec![0.0; h.len()];
17677 self.dyn_phi_seen = 0;
17678 }
17679 if self.dyn_phi_seen == 0 {
17680 self.dyn_phi_ema.copy_from_slice(h);
17681 } else {
17682 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17683 *e = (1.0 - A) * *e + A * v;
17684 }
17685 }
17686 self.dyn_phi_seen += 1;
17687 }
17688
17689 pub fn dyn_phi(&self) -> &[f32] {
17691 &self.dyn_phi_ema
17692 }
17693
17694 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17696 self.dyn_phi_layer = layer;
17697 self.dyn_phi_ema.clear();
17698 self.dyn_phi_seen = 0;
17699 }
17700
17701 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17703 let Some(model) = &self.model else {
17704 return Vec::new();
17705 };
17706 model
17707 .header
17708 .skills
17709 .iter()
17710 .enumerate()
17711 .filter_map(|(i, sk)| {
17712 if sk.is_v2() {
17716 return None;
17717 }
17718 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17719 let sel = sk.selection.as_ref()?;
17720 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17721 })
17722 .collect()
17723 }
17724
17725 pub fn active_skill(&self) -> Option<usize> {
17727 self.dyn_active
17728 }
17729
17730 pub fn enable_dynamic_routing(&mut self) -> usize {
17735 use crate::swarm::{DynRouter, RoutableSkill};
17736 let Some(model) = self.model.clone() else {
17737 return 0;
17738 };
17739 if let Some(r) = &model.header.router {
17746 tracing::warn!(
17747 "dynamic routing disabled: this file declares router policy '{}' with \
17748 granularity \"{}\" — the request-level decision applies instead",
17749 r.policy,
17750 r.granularity
17751 );
17752 return 0;
17753 }
17754 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17760 || model.header.skills.iter().any(|s| s.is_v2())
17761 {
17762 tracing::warn!(
17763 "dynamic routing disabled: this file carries format-v2 skill records \
17764 (SKILLS_V2) — they route per request through a router policy only"
17765 );
17766 return 0;
17767 }
17768 if self.dyn_blend_loaded {
17771 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17772 return 0;
17773 }
17774 if let Some(a) = self.dyn_active {
17778 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17779 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17780 return 0;
17781 }
17782 }
17783 let hidden = self.hidden_size;
17784 let mut skills = Vec::new();
17785 for (idx, id, _phi) in self.dynamic_skills() {
17786 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17787 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17788 skills.push(rs);
17789 }
17790 }
17791 }
17792 if skills.is_empty() {
17793 return 0;
17794 }
17795 let phi = skills[0].phi_layer;
17797 if skills.iter().any(|s| s.phi_layer != phi) {
17798 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17799 }
17800 let n = skills.len();
17801 self.set_dyn_phi_layer(Some(phi));
17802 self.dyn_router = Some(DynRouter::new(skills));
17803 n
17804 }
17805
17806 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17808 self.dyn_router
17809 .as_ref()
17810 .map(|r| r.switches.clone())
17811 .unwrap_or_default()
17812 }
17813
17814 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17817 let _mimo_q8 = self
17818 .mimo_moe
17819 .is_on()
17820 .then(crate::qtensor::enter_full_gpu_q8_scope);
17821 let rows = self.weights.lm_head.rows();
17822 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17823 let served = self.mimo_moe.is_on()
17827 && crate::gpu::mimo_q8_short_enabled()
17828 && rows == self.vocab_size
17829 && !self.weights.lm_head.has_prism_contract()
17830 && self
17831 .weights
17832 .lm_head
17833 .graph_weight()
17834 .is_some_and(|(model, idx, kind, _)| {
17835 kind == 7
17836 && crate::gpu::q82_short_rows(
17837 model,
17838 idx,
17839 hidden,
17840 1,
17841 rows,
17842 self.hidden_size,
17843 &mut logits,
17844 )
17845 });
17846 if !served {
17847 self.weights
17848 .lm_head
17849 .matvec(hidden, &mut logits, self.pool.as_deref());
17850 }
17851 logits.resize(self.vocab_size, 0.0);
17852 if let Some(m) = self.logit_multiplier {
17853 for l in logits.iter_mut() {
17854 *l *= m;
17855 }
17856 }
17857 if let Some(c) = self.final_softcap {
17858 softcap_in_place(&mut logits, c, self.pool.as_deref());
17859 }
17860 if let Some(cm) = self.head_clusters.as_ref() {
17861 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17862 }
17863 logits
17864 }
17865
17866 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17869 let h = hidden.len();
17870 let ncl = cm.len() / h.max(1);
17871 if ncl == 0 || logits.len() % ncl != 0 {
17872 return;
17873 }
17874 let cs = logits.len() / ncl;
17875 let mut lc = vec![0.0f32; ncl];
17877 for c in 0..ncl {
17878 let row = &cm[c * h..(c + 1) * h];
17879 let mut s = 0.0f32;
17880 for j in 0..h {
17881 s += row[j] * hidden[j];
17882 }
17883 lc[c] = s;
17884 }
17885 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17886 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17887 for c in 0..ncl {
17888 let blk = &mut logits[c * cs..(c + 1) * cs];
17889 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17890 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17891 let add = lc[c] - lse - bl;
17892 for v in blk.iter_mut() {
17893 *v += add;
17894 }
17895 }
17896 }
17897
17898 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17903 #[cfg(target_os = "macos")]
17904 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17905 self.clear_sequence_state();
17906 crate::gpu::graph_race_begin_generation();
17910 if task_mask.is_none() {
17911 self.o1_begin();
17912 }
17913 let mut hidden = vec![0.0f32; self.hidden_size];
17914 for (pos, &id) in ids.iter().enumerate() {
17915 let emb = self.embed_single(id);
17916 hidden = self.forward_layers(&emb, pos, task_mask);
17917 }
17918 if let Err(err) = self.o1_seal_checked() {
17919 self.o1_fail(err);
17920 }
17921 if let Some(logits) = self.graph_logits.take() {
17924 return logits;
17925 }
17926 inference::rms_norm_into(
17927 &hidden,
17928 &self.weights.final_norm,
17929 self.rms_eps,
17930 self.norm_style,
17931 &mut self.ws.n1,
17932 );
17933 self.lm_head_forward(&self.ws.n1)
17934 }
17935}
17936
17937pub fn create_test_pipeline(
17939 hidden_size: usize,
17940 intermediate_size: usize,
17941 num_heads: usize,
17942 num_kv_heads: usize,
17943 head_dim: usize,
17944 num_layers: usize,
17945 vocab_size: usize,
17946) -> Pipeline {
17947 let synth = |n: usize, salt: usize| -> Vec<f32> {
17950 (0..n)
17951 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
17952 .collect()
17953 };
17954 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
17955 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17956 };
17957 let layer_weights: Vec<LayerWeights> = (0..num_layers)
17958 .map(|li| LayerWeights {
17959 input_norm: vec![1.0; hidden_size],
17960 post_norm: vec![1.0; hidden_size],
17961 attn_out_norm: None,
17962 ffn_out_norm: None,
17963 layer_scale: None,
17964 ffn: FfnKind::Dense(DenseFfn {
17965 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17966 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17967 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17968 act: Act::Silu,
17969 down_t: None,
17970 segs: Vec::new(),
17971 }),
17972 attn: AttnKind::Full {
17973 bias: None,
17974 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17975 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17976 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17977 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17978 q_norm: None,
17979 k_norm: None,
17980 output_gate: false,
17981 softplus_gate: None,
17982 },
17983 })
17984 .collect();
17985
17986 Pipeline::new(
17987 Tokenizer::byte_level(),
17988 PipelineWeights {
17989 embed_tokens: qt(vocab_size, hidden_size, 100),
17990 layers: layer_weights,
17991 lm_head: qt(vocab_size, hidden_size, 200),
17992 final_norm: vec![1.0; hidden_size],
17993 },
17994 hidden_size,
17995 intermediate_size,
17996 num_heads,
17997 num_kv_heads,
17998 head_dim,
17999 num_layers,
18000 num_layers, false, vocab_size,
18003 1e-6,
18004 10_000.0,
18005 NormStyle::Qwen,
18006 4096,
18007 SamplerConfig {
18008 seed: Some(42),
18009 ..Default::default()
18010 },
18011 )
18012}
18013
18014#[inline]
18019fn mask_bit(row: &[u8], j: usize) -> bool {
18020 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18021}
18022
18023fn mask_gain() -> f32 {
18034 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18035 *G.get_or_init(|| {
18036 std::env::var("CMF_FFN_MASK_GAIN")
18037 .ok()
18038 .and_then(|v| v.parse().ok())
18039 .unwrap_or(1.0)
18040 })
18041}
18042
18043fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18044 let fill = meanfill().and_then(|(i, v)| {
18047 let li = crate::gpu::cur_layer();
18048 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18049 });
18050 for r in 0..rows {
18051 let base = r * inter;
18052 for (bi, &byte) in row.iter().enumerate() {
18053 if byte == 0xFF {
18054 continue;
18055 }
18056 let j0 = bi * 8;
18057 for bit in 0..8 {
18058 let j = j0 + bit;
18059 if j < inter && byte & (1 << bit) == 0 {
18060 g[base + j] = fill.map_or(0.0, |f| f[j]);
18061 }
18062 }
18063 }
18064 }
18065 let gain = mask_gain();
18066 if gain != 1.0 {
18067 for v in g[..rows * inter].iter_mut() {
18068 *v *= gain;
18069 }
18070 }
18071}
18072
18073#[inline]
18075fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18076 row.is_none_or(|r| mask_bit(r, i))
18077}
18078
18079fn all_bits_on(row: &[u8], n: usize) -> bool {
18082 (0..n).all(|i| mask_bit(row, i))
18083}
18084
18085fn tube_topk() -> usize {
18093 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18094 *K.get_or_init(|| {
18095 std::env::var("CMF_TUBE_TOPK")
18096 .ok()
18097 .and_then(|v| v.parse().ok())
18098 .unwrap_or(0)
18099 })
18100}
18101
18102fn tube_score_oracle() -> bool {
18103 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18104 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18105}
18106
18107fn tube_ffn_routed(
18114 d: &DenseFfn,
18115 xs: &[f32],
18116 b: usize,
18117 pool: Option<&Pool>,
18118 mask_row: Option<&[u8]>,
18119 k: usize,
18120) -> Vec<f32> {
18121 let hidden = d.down_proj.rows();
18122 let core = d.gate_proj.rows();
18123 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18124 let mut out = match (b, core_full, mask_row) {
18125 (1, true, _) => dense_ffn(d, xs, pool),
18126 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18127 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18128 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18129 };
18130 let cand: Vec<usize> = (0..d.segs.len())
18131 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18132 .collect();
18133 if cand.is_empty() {
18134 return out;
18135 }
18136 let oracle = tube_score_oracle();
18140 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18141 let mut scores = vec![0f32; b * cand.len()];
18142 for (ci, &i) in cand.iter().enumerate() {
18143 let seg = &d.segs[i];
18144 let w = seg.width;
18145 let mut g = vec![0.0f32; b * w];
18146 if b == 1 {
18147 seg.gate.matvec(xs, &mut g, pool);
18148 } else {
18149 seg.gate.matmat(xs, b, &mut g, pool);
18150 }
18151 for v in g.iter_mut() {
18152 *v = Act::Silu.combine(*v, 1.0);
18153 }
18154 if !oracle {
18155 for t in 0..b {
18156 scores[t * cand.len() + ci] =
18157 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18158 }
18159 }
18160 if oracle || b > 1 {
18161 let mut u = vec![0.0f32; b * w];
18162 if b == 1 {
18163 seg.up.matvec(xs, &mut u, pool);
18164 } else {
18165 seg.up.matmat(xs, b, &mut u, pool);
18166 }
18167 for (a, &v) in g.iter_mut().zip(u.iter()) {
18168 *a *= v;
18169 }
18170 if oracle {
18171 for t in 0..b {
18172 scores[t * cand.len() + ci] =
18173 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18174 }
18175 }
18176 }
18177 acts.push(g);
18178 }
18179 let keep = k.min(cand.len());
18181 let mut scratch: Vec<f32> = Vec::new();
18182 for t in 0..b {
18183 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18184 .map(|ci| (scores[t * cand.len() + ci], ci))
18185 .collect();
18186 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18187 let mut alive = vec![false; cand.len()];
18188 for &(_, ci) in sc.iter().take(keep) {
18189 alive[ci] = true;
18190 }
18191 if b > 1 {
18192 for (ci, a) in acts.iter_mut().enumerate() {
18193 if !alive[ci] {
18194 let w = d.segs[cand[ci]].width;
18195 a[t * w..(t + 1) * w].fill(0.0);
18196 }
18197 }
18198 } else {
18199 for (ci, &i) in cand.iter().enumerate() {
18203 if !alive[ci] {
18204 continue;
18205 }
18206 let seg = &d.segs[i];
18207 let w = seg.width;
18208 let g = &mut acts[ci];
18209 if !tube_score_oracle() {
18210 scratch.clear();
18211 scratch.resize(w, 0.0);
18212 seg.up.matvec(xs, &mut scratch, pool);
18213 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18214 *a *= v;
18215 }
18216 }
18217 let mut acc = vec![0.0f32; hidden];
18218 seg.down.matvec(g, &mut acc, pool);
18219 for (o, a) in out.iter_mut().zip(&acc) {
18220 *o += *a;
18221 }
18222 }
18223 }
18224 }
18225 if b > 1 {
18226 for (ci, &i) in cand.iter().enumerate() {
18227 let seg = &d.segs[i];
18228 let mut acc = vec![0.0f32; b * hidden];
18229 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18230 for (o, a) in out.iter_mut().zip(&acc) {
18231 *o += *a;
18232 }
18233 }
18234 }
18235 out
18236}
18237
18238fn tube_ffn(
18244 d: &DenseFfn,
18245 xs: &[f32],
18246 b: usize,
18247 pool: Option<&Pool>,
18248 mask_row: Option<&[u8]>,
18249) -> Vec<f32> {
18250 if tube_topk() > 0 {
18251 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18252 }
18253 let hidden = d.down_proj.rows();
18254 let core = d.gate_proj.rows();
18255 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18256 let mut out = match (b, core_full, mask_row) {
18257 (1, true, _) => dense_ffn(d, xs, pool),
18258 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18259 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18260 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18261 };
18262 TUBE_SCRATCH.with(|sc| {
18263 let mut sc = sc.borrow_mut();
18264 let [g, u, acc] = &mut *sc;
18265 for seg in &d.segs {
18266 if !tube_bit(mask_row, seg.start) {
18267 continue;
18268 }
18269 let w = seg.width;
18270 g.resize(b * w, 0.0);
18271 if b == 1
18272 && d.act == Act::Silu
18273 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18274 {
18275 } else {
18277 u.resize(b * w, 0.0);
18278 if b == 1 {
18279 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18280 } else {
18281 seg.gate.matmat(xs, b, g, pool);
18282 seg.up.matmat(xs, b, u, pool);
18283 }
18284 for i in 0..b * w {
18285 g[i] = d.act.combine(g[i], u[i]);
18286 }
18287 }
18288 acc.resize(b * hidden, 0.0);
18289 acc.fill(0.0);
18290 if b == 1 {
18291 seg.down.matvec(g, acc, pool);
18292 } else {
18293 seg.down.matmat(g, b, acc, pool);
18294 }
18295 for (o, a) in out.iter_mut().zip(acc.iter()) {
18296 *o += *a;
18297 }
18298 }
18299 out
18300 })
18301}
18302
18303thread_local! {
18304 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18308 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18309}
18310
18311static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18314 std::sync::atomic::AtomicU64::new(0),
18315 std::sync::atomic::AtomicU64::new(0),
18316];
18317
18318fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18322 #[cfg(feature = "gpu")]
18323 {
18324 use std::sync::atomic::Ordering::Relaxed;
18325 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18326 (
18327 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18328 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18329 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18330 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18331 )
18332 }
18333 #[cfg(not(feature = "gpu"))]
18334 (0.0, 0.0, 0.0, 0)
18335}
18336
18337fn wgpu_mirror_counters() -> [u64; 6] {
18340 #[cfg(feature = "gpu")]
18341 {
18342 crate::gpu_wgpu::MIRROR_EVENTS
18343 .each_ref()
18344 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18345 }
18346 #[cfg(not(feature = "gpu"))]
18347 [0; 6]
18348}
18349
18350fn prefill_prof_on() -> bool {
18351 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18352 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18353}
18354
18355pub(crate) fn accumulate_dense_ffn_activation_mass(
18364 d: &DenseFfn,
18365 xs: &[f32],
18366 b: usize,
18367 pool: Option<&Pool>,
18368 mass: &mut [f64],
18369 squared_mass: &mut [f64],
18370) -> Result<(), String> {
18371 let inter = d.gate_proj.rows();
18372 let hidden = d.gate_proj.cols();
18373 if mass.len() != inter || squared_mass.len() != inter {
18374 return Err(format!(
18375 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18376 mass.len(),
18377 squared_mass.len()
18378 ));
18379 }
18380 if d.up_proj.rows() != inter
18381 || d.up_proj.cols() != hidden
18382 || d.down_proj.cols() != inter
18383 || d.down_proj.rows() != hidden
18384 {
18385 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18386 }
18387 if !d.segs.is_empty() {
18388 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18389 }
18390 if xs.len() != b.saturating_mul(hidden) {
18391 return Err(format!(
18392 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18393 xs.len()
18394 ));
18395 }
18396 if b == 0 {
18397 return Ok(());
18398 }
18399 let mut gate = vec![0.0f32; b * inter];
18400 let mut up = vec![0.0f32; b * inter];
18401 d.gate_proj.matmat(xs, b, &mut gate, pool);
18402 d.up_proj.matmat(xs, b, &mut up, pool);
18403 for row in 0..b {
18404 for i in 0..inter {
18405 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18406 let a = a as f64;
18407 mass[i] += a.abs();
18408 squared_mass[i] += a * a;
18409 }
18410 }
18411 Ok(())
18412}
18413
18414fn dense_ffn_batch(
18415 d: &DenseFfn,
18416 xs: &[f32],
18417 b: usize,
18418 pool: Option<&Pool>,
18419 mask_row: Option<&[u8]>,
18420) -> Vec<f32> {
18421 let inter = d.gate_proj.rows();
18422 let hidden = d.down_proj.rows();
18423 let fused_act = d.act.wgpu_graph_act();
18435 if mask_row.is_none()
18436 && fused_act.is_some()
18437 && b >= 32
18438 && crate::gpu::enabled_here()
18439 && !crate::gpu::mm_killed()
18440 && refit_dir().is_none()
18445 && !ffn_probe_active()
18450 && !crate::gptq_capture::capturing()
18454 {
18455 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18456 d.gate_proj.mapped_q4t(),
18457 d.up_proj.mapped_q4t(),
18458 d.down_proj.mapped_q4t(),
18459 ) {
18460 let mut out = vec![0.0f32; b * hidden];
18461 let act = fused_act.expect("checked above");
18462 if crate::gpu::q4_ffn_act(
18463 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18464 ) {
18465 return out;
18466 }
18467 }
18468 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18473 d.gate_proj.mapped_q4tp(),
18474 d.up_proj.mapped_q4tp(),
18475 d.down_proj.mapped_q4tp(),
18476 ) {
18477 let mut out = vec![0.0f32; b * hidden];
18478 let act = fused_act.expect("checked above");
18479 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18480 {
18481 return out;
18482 }
18483 }
18484 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18490 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18491 d.gate_proj.mapped_device_gemm(),
18492 d.up_proj.mapped_device_gemm(),
18493 d.down_proj.mapped_device_gemm(),
18494 ) {
18495 let mut out = vec![0.0f32; b * hidden];
18496 let act = fused_act.expect("checked above");
18497 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18498 return out;
18499 }
18500 }
18501 }
18502 let mut g = vec![0.0f32; b * inter];
18503 d.gate_proj.matmat(xs, b, &mut g, pool);
18504 let mut u = vec![0.0f32; b * inter];
18505 d.up_proj.matmat(xs, b, &mut u, pool);
18506 if gate_topk() > 0 && d.act == Act::Silu {
18507 for t in 0..b {
18508 let row = &mut g[t * inter..(t + 1) * inter];
18509 for v in row.iter_mut() {
18510 *v = Act::Silu.combine(*v, 1.0);
18511 }
18512 keep_top_k(row, gate_topk());
18513 }
18514 for i in 0..b * inter {
18515 g[i] *= u[i];
18516 }
18517 } else {
18518 for i in 0..b * inter {
18519 g[i] = d.act.combine(g[i], u[i]);
18520 }
18521 }
18522 if let Some(row) = mask_row {
18523 zero_masked_cols(&mut g, b, inter, row);
18524 }
18525 if oracle_topk() > 0 {
18526 for t in 0..b {
18527 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18528 }
18529 }
18530 let mut out = vec![0.0f32; b * hidden];
18531 d.down_proj.matmat(&g, b, &mut out, pool);
18532 if refit_dir().is_some() {
18533 let li = crate::gpu::cur_layer();
18534 if li >= 0 {
18535 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18536 }
18537 }
18538 FFN_PROBE.with(|pr| {
18542 if let Some(acc) = pr.borrow_mut().as_mut() {
18543 let li = crate::gpu::cur_layer();
18544 if li < 0 {
18545 return;
18546 }
18547 let Some(row) = acc.get_mut(li as usize) else {
18548 return;
18549 };
18550 let sq = probe_sq();
18551 for t in 0..b {
18552 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18553 *a += if sq {
18554 (v as f64) * (v as f64)
18555 } else {
18556 (v as f64).abs()
18557 };
18558 }
18559 }
18560 }
18561 });
18562 out
18563}
18564
18565fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18570 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18571 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18572 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18573 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18574 if (!on && !dump) || b == 0 {
18575 return;
18576 }
18577 let hidden = xs.len() / b;
18578 if on {
18579 let mut acc = m.act_sq.borrow_mut();
18580 if acc.len() < hidden {
18581 acc.resize(hidden, 0.0);
18582 }
18583 for t in 0..b {
18584 let row = &xs[t * hidden..(t + 1) * hidden];
18585 for (a, &v) in acc.iter_mut().zip(row) {
18586 *a += (v as f64) * (v as f64);
18587 }
18588 }
18589 }
18590 if dump {
18591 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18594 .ok()
18595 .and_then(|v| v.parse().ok())
18596 .unwrap_or(4096);
18597 let mut rows = m.act_rows.borrow_mut();
18598 if rows.len() < cap * hidden {
18599 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18600 rows.extend_from_slice(&xs[..take * hidden]);
18601 }
18602 }
18603}
18604
18605#[derive(Clone, Copy)]
18608struct SendVecs(*mut Vec<f32>);
18609unsafe impl Send for SendVecs {}
18610unsafe impl Sync for SendVecs {}
18611impl SendVecs {
18612 #[inline]
18613 fn at(self, i: usize) -> *mut Vec<f32> {
18614 unsafe { self.0.add(i) }
18615 }
18616}
18617
18618fn moe_ffn_batch(
18619 m: &MoeFfn,
18620 xs: &[f32],
18621 b: usize,
18622 hidden: usize,
18623 pool: Option<&Pool>,
18624 allowed: Option<&[bool]>,
18625) -> Vec<f32> {
18626 accumulate_act(m, xs, b);
18627 let ne = m.experts.len();
18628 let mut logits = vec![0.0f32; b * ne];
18629 match &m.resonance {
18630 Some(r) => {
18631 let hdim = xs.len() / b.max(1);
18632 for bi in 0..b {
18633 r.scores(
18634 &xs[bi * hdim..(bi + 1) * hdim],
18635 &mut logits[bi * ne..(bi + 1) * ne],
18636 );
18637 }
18638 }
18639 None => m.router.matmat(xs, b, &mut logits, pool),
18640 }
18641
18642 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18645 {
18646 let mut st = m.stats.borrow_mut();
18647 if st.len() < ne {
18648 st.resize(ne, 0);
18649 }
18650 for bi in 0..b {
18651 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18652 for &e in &idx {
18653 st[e] += 1;
18654 assign[e].push((bi, p[e] / wsum));
18655 }
18656 }
18657 }
18658
18659 let mut out = vec![0.0f32; b * hidden];
18660 let cols = m.experts[0].gate_proj.cols();
18661 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18662 let sb = list.len();
18663 let mut sub = vec![0.0f32; sb * cols];
18664 for (k, &(bi, _)) in list.iter().enumerate() {
18665 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18666 }
18667 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18668 for (k, &(bi, w)) in list.iter().enumerate() {
18669 for i in 0..hidden {
18670 out[bi * hidden + i] += w * eo[k * hidden + i];
18671 }
18672 }
18673 };
18674 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18680 if pool.is_some() && active.len() >= 8 {
18681 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18682 {
18683 let panel_ptr = SendVecs(panels.as_mut_ptr());
18684 let experts = &m.experts;
18687 let (active_r, assign_r) = (&active, &assign);
18688 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18689 let run = |start: usize, end: usize| {
18690 let _cpu_scope = inherit_cpu();
18691 #[cfg(target_os = "macos")]
18699 let _metal_cpu = crate::gpu::enter_cpu_scope();
18700 for ai in start..end {
18701 let e = active_r[ai];
18702 let list = &assign_r[e];
18703 let sb = list.len();
18704 let mut sub = vec![0.0f32; sb * cols];
18705 for (k, &(bi, _)) in list.iter().enumerate() {
18706 sub[k * cols..(k + 1) * cols]
18707 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18708 }
18709 unsafe {
18711 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18712 }
18713 }
18714 };
18715 match pool {
18716 Some(p) => p.run_rows(active.len(), &run),
18717 None => run(0, active.len()),
18718 }
18719 }
18720 for (ai, &e) in active.iter().enumerate() {
18721 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18722 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18723 for i in 0..hidden {
18724 out[bi * hidden + i] += w * eo[i];
18725 }
18726 }
18727 }
18728 } else {
18729 for &e in &active {
18730 run_expert(&m.experts[e], &assign[e], &mut out);
18731 }
18732 }
18733 if let Some((se, gate)) = &m.shared {
18734 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18735 let mut gl = vec![0.0f32; b];
18736 gate.matmat(xs, b, &mut gl, pool);
18737 (0..b)
18738 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18739 .collect()
18740 } else {
18741 (0..b).map(|bi| (bi, 1.0)).collect()
18742 };
18743 run_expert(se, &all, &mut out);
18744 }
18745 out
18746}
18747
18748fn moe_ffn_rows_exact(
18760 m: &MoeFfn,
18761 xs: &[f32],
18762 b: usize,
18763 hidden: usize,
18764 pool: Option<&Pool>,
18765) -> Vec<f32> {
18766 let mut out = vec![0.0f32; b * hidden];
18767 let per_row = |out: &mut [f32]| {
18768 for r in 0..b {
18769 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18770 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18771 }
18772 };
18773 let covered = !crate::gpu::enabled_here()
18774 && moe_batch_enabled()
18775 && m.shared.is_none()
18776 && m.resonance.is_none()
18777 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18778 && m.experts.iter().all(|d| d.act == Act::Silu);
18779 if !covered {
18780 per_row(&mut out);
18781 return out;
18782 }
18783 let ne = m.experts.len();
18784 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18786 for r in 0..b {
18787 let x = &xs[r * hidden..(r + 1) * hidden];
18788 accumulate_act(m, x, 1);
18789 let mut logits = vec![0.0f32; ne];
18790 m.router.matvec(x, &mut logits, pool);
18791 let (idx, p, wsum) = moe_route(&logits, m, None);
18792 {
18793 let mut st = m.stats.borrow_mut();
18794 if st.len() < ne {
18795 st.resize(ne, 0);
18796 }
18797 for &e in &idx {
18798 st[e] += 1;
18799 }
18800 }
18801 let w: Vec<f32> = idx
18802 .iter()
18803 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18804 .collect();
18805 routes.push((idx, w));
18806 }
18807 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18808 per_row(&mut out);
18809 return out;
18810 }
18811 let mut experts: Vec<usize> = Vec::new();
18813 let mut groups: Vec<Vec<usize>> = Vec::new();
18814 for (r, (idx, _)) in routes.iter().enumerate() {
18815 for &e in idx {
18816 match experts.iter().position(|&x| x == e) {
18817 Some(g) => groups[g].push(r),
18818 None => {
18819 experts.push(e);
18820 groups.push(vec![r]);
18821 }
18822 }
18823 }
18824 }
18825 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18826 let inter = m.experts[experts[0]].gate_proj.rows();
18827 let pairs: Vec<(&QTensor, &QTensor)> = experts
18828 .iter()
18829 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18830 .collect();
18831 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18832 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18833 per_row(&mut out);
18834 return out;
18835 }
18836 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18837 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18838 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18839 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18840 per_row(&mut out);
18841 return out;
18842 }
18843 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18845 let mut p = 0usize;
18846 for (g, &e) in experts.iter().enumerate() {
18847 for &r in &groups[g] {
18848 slot.insert((r, e), p);
18849 p += 1;
18850 }
18851 }
18852 for (r, (idx, w)) in routes.iter().enumerate() {
18853 let terms: Vec<(&[f32], f32)> = idx
18854 .iter()
18855 .zip(w)
18856 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18857 .collect();
18858 let row = &mut out[r * hidden..(r + 1) * hidden];
18859 for (i, dst) in row.iter_mut().enumerate() {
18860 let mut acc = 0f32;
18862 for (d, we) in &terms {
18863 acc += we * d[i];
18864 }
18865 *dst = acc;
18866 }
18867 }
18868 out
18869}
18870
18871pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18875 match pool {
18876 Some(p) if v.len() >= 1 << 15 => {
18877 let base = v.as_mut_ptr() as usize;
18878 p.run_rows(v.len(), &|s, e| {
18879 let sl =
18882 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18883 for x in sl {
18884 *x = c * (*x / c).tanh();
18885 }
18886 });
18887 }
18888 _ => {
18889 for x in v.iter_mut() {
18890 *x = c * (*x / c).tanh();
18891 }
18892 }
18893 }
18894}
18895
18896thread_local! {
18897 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18901 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18902}
18903
18904fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18906 if gate_topk() > 0
18909 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
18910 {
18911 return out;
18912 }
18913 let prism_body = d.gate_proj.has_prism_contract()
18929 || d.up_proj.has_prism_contract()
18930 || d.down_proj.has_prism_contract();
18931 if !prism_body
18932 && crate::gpu::enabled_here()
18933 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
18934 {
18935 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
18936 crate::gpu::ProbeArm::Gpu
18937 } else {
18938 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
18939 };
18940 match arm {
18941 crate::gpu::ProbeArm::Gpu => {
18942 let t0 = std::time::Instant::now();
18943 if let Some(out) = dense_ffn_gpu(d, x, pool) {
18944 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18945 return out;
18946 }
18947 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
18951 }
18952 crate::gpu::ProbeArm::CpuTimed => {
18953 let t0 = std::time::Instant::now();
18954 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18955 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18956 return out;
18957 }
18958 crate::gpu::ProbeArm::Cpu => {
18959 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18960 }
18961 }
18962 }
18963 dense_ffn_cpu(d, x, pool)
18964}
18965
18966fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18968 let inter = d.gate_proj.rows();
18969 FFN_SCRATCH.with(|s| {
18970 let mut s = s.borrow_mut();
18971 let [g, u, ..] = &mut *s;
18972 g.resize(inter, 0.0);
18973 if gate_topk() > 0 {
18976 u.resize(inter, 0.0);
18980 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18981 for i in 0..inter {
18982 g[i] = Act::Silu.combine(g[i], 1.0);
18983 }
18984 keep_top_k(g, gate_topk());
18985 for i in 0..inter {
18986 g[i] *= u[i];
18987 }
18988 } else if d.act == Act::Silu && {
18989 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18990 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
18991 } {
18992 } else {
18994 u.resize(inter, 0.0);
18995 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18997 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18998 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
18999 for i in 0..inter {
19000 g[i] = d.act.combine(g[i], u[i]);
19001 }
19002 }
19003 FFN_PROBE.with(|pr| {
19011 if let Some(acc) = pr.borrow_mut().as_mut() {
19012 let li = crate::gpu::cur_layer();
19013 if li >= 0 {
19014 if let Some(row) = acc.get_mut(li as usize) {
19015 match probe_topk() {
19016 0 if probe_sq() => {
19017 for (a, &v) in row.iter_mut().zip(g.iter()) {
19018 *a += (v as f64) * (v as f64);
19019 }
19020 }
19021 0 if probe_signed() => {
19022 for (a, &v) in row.iter_mut().zip(g.iter()) {
19023 *a += v as f64;
19024 }
19025 }
19026 0 => {
19027 for (a, &v) in row.iter_mut().zip(g.iter()) {
19028 *a += (v as f64).abs();
19029 }
19030 }
19031 k => {
19032 let n = g.len();
19033 let k = k.min(n);
19034 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19035 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19036 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19037 });
19038 let thr = *kth;
19039 for (a, &v) in row.iter_mut().zip(g.iter()) {
19040 if v.abs() >= thr {
19041 *a += 1.0;
19042 }
19043 }
19044 }
19045 }
19046 }
19047 }
19048 }
19049 });
19050 if oracle_topk() > 0 {
19051 keep_top_k(g, oracle_topk());
19052 }
19053 {
19054 let li = crate::gpu::cur_layer();
19055 if li >= 0 {
19056 adump_row(li as usize, g);
19057 }
19058 }
19059 let mut out = attention::take_buf(d.down_proj.rows());
19060 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19061 d.down_proj.matvec(g, &mut out, pool);
19062 out
19063 })
19064}
19065
19066pub struct RefitAcc {
19079 pub support: Vec<u32>,
19080 pub gss: Vec<f32>,
19081 pub ya: Vec<f32>,
19082 pub hidden: usize,
19083 pub tokens: u64,
19084 pub buf_g: Vec<f32>,
19090 pub buf_o: Vec<f32>,
19091 pub buf_t: usize,
19092}
19093
19094type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19098
19099static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19100 std::sync::OnceLock::new();
19101
19102fn ffn_probe_active() -> bool {
19105 FFN_PROBE.with(|p| p.borrow().is_some())
19106}
19107
19108fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19109 REFIT
19110 .get_or_init(|| {
19111 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19112 (
19113 d,
19114 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19115 )
19116 })
19117 })
19118 .as_ref()
19119}
19120
19121fn refit_accumulate(
19123 li: usize,
19124 g: &[f32],
19125 b: usize,
19126 inter: usize,
19127 out: &[f32],
19128 hidden: usize,
19129 pool: Option<&Pool>,
19130) {
19131 let Some((dir, map)) = refit_dir() else {
19132 return;
19133 };
19134 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19135 let (from, to) = *SPAN.get_or_init(|| {
19136 let g = |k: &str, d: usize| {
19137 std::env::var(k)
19138 .ok()
19139 .and_then(|v| v.parse().ok())
19140 .unwrap_or(d)
19141 };
19142 (
19143 g("CMF_FFN_REFIT_FROM", 0),
19144 g("CMF_FFN_REFIT_TO", usize::MAX),
19145 )
19146 });
19147 if li < from || li > to {
19148 return;
19149 }
19150 let mut guard = map.lock().unwrap();
19151 let (map, shared) = &mut *guard;
19152 let acc = match map.entry(li) {
19153 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19154 std::collections::hash_map::Entry::Vacant(e) => {
19155 let path = format!("{dir}/support.{li}.u32");
19156 let Ok(bytes) = std::fs::read(&path) else {
19157 eprintln!("refit: no {path} — layer {li} skipped");
19158 return;
19159 };
19160 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19161 let support: Vec<u32> = bytes[4..4 + n * 4]
19162 .chunks_exact(4)
19163 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19164 .collect();
19165 eprintln!(
19166 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19167 (n * n + hidden * n) as f64 * 4.0 / 1e6
19168 );
19169 e.insert(RefitAcc {
19170 gss: vec![0.0; n * n],
19171 ya: vec![0.0; hidden * n],
19172 buf_g: Vec::new(),
19173 buf_o: Vec::new(),
19174 buf_t: 0,
19175 support,
19176 hidden,
19177 tokens: 0,
19178 })
19179 }
19180 };
19181 let ns = acc.support.len();
19182 let cap = refit_batch();
19184 if acc.buf_g.is_empty() {
19185 acc.buf_g = vec![0.0; ns * cap];
19186 acc.buf_o = vec![0.0; hidden * cap];
19187 }
19188 let take = b.min(cap - acc.buf_t);
19189 for t in 0..take {
19190 let col = acc.buf_t + t;
19191 for (j, &n) in acc.support.iter().enumerate() {
19192 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19193 }
19194 for h in 0..hidden {
19195 acc.buf_o[h * cap + col] = out[t * hidden + h];
19196 }
19197 }
19198 acc.buf_t += take;
19199 acc.tokens += take as u64;
19200 if acc.buf_t < cap {
19201 return;
19202 }
19203 let bt = acc.buf_t;
19204 acc.buf_t = 0;
19205 let RefitAcc {
19215 gss,
19216 ya,
19217 buf_g,
19218 buf_o,
19219 ..
19220 } = acc;
19221 let need = (ns * ns).max(hidden * ns);
19222 if shared.len() < need {
19223 shared.resize(need, 0.0);
19224 }
19225 let scratch = &mut shared[..];
19226 let _ = bt;
19227 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19228 add_into(gss, &scratch[..ns * ns], pool);
19229 if crate::gpu::gemm_nt_f32_transient(
19230 buf_o,
19231 buf_g,
19232 &mut scratch[..hidden * ns],
19233 hidden,
19234 cap,
19235 ns,
19236 ) {
19237 add_into(ya, &scratch[..hidden * ns], pool);
19238 } else {
19239 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19240 }
19241 } else {
19242 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19243 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19244 }
19245 }
19249
19250fn refit_batch() -> usize {
19252 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19253 *B.get_or_init(|| {
19254 std::env::var("CMF_FFN_REFIT_BATCH")
19255 .ok()
19256 .and_then(|v| v.parse().ok())
19257 .unwrap_or(4096)
19258 })
19259}
19260
19261fn accum_outer_t(
19264 c: &mut [f32],
19265 m: usize,
19266 n: usize,
19267 b: usize,
19268 left: &[f32],
19269 right: &[f32],
19270 pool: Option<&Pool>,
19271) {
19272 let ptr = SendMut(c.as_mut_ptr());
19273 let body = |i: usize| {
19274 let ptr = &ptr;
19275 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19276 for t in 0..b {
19277 let a = left[i * b + t];
19278 if a == 0.0 {
19279 continue;
19280 }
19281 for (j, o) in row.iter_mut().enumerate() {
19282 *o += a * right[j * b + t];
19283 }
19284 }
19285 };
19286 match pool {
19287 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19288 for i in s..e {
19289 body(i);
19290 }
19291 }),
19292 _ => {
19293 for i in 0..m {
19294 body(i);
19295 }
19296 }
19297 }
19298}
19299
19300fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19303 let n = dst.len().min(src.len());
19304 match pool {
19305 Some(p) if n >= 1 << 16 => {
19306 let ptr = SendMut(dst.as_mut_ptr());
19307 let f = |s: usize, e: usize| {
19308 let ptr = &ptr;
19309 for blk in s..e {
19310 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19311 for i in a..b {
19312 unsafe { *ptr.0.add(i) += src[i] };
19313 }
19314 }
19315 };
19316 p.run_rows(n.div_ceil(4096), &f);
19317 }
19318 _ => {
19319 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19320 *d += *v;
19321 }
19322 }
19323 }
19324}
19325
19326fn accum_outer(
19331 c: &mut [f32],
19332 m: usize,
19333 n: usize,
19334 b: usize,
19335 left: &[f32],
19336 right: &[f32],
19337 pool: Option<&Pool>,
19338) {
19339 const TILE: usize = 32;
19340 let tiles = m.div_ceil(TILE);
19341 let cp = SendMut(c.as_mut_ptr());
19342 let body = |ti: usize| {
19343 let cp = &cp;
19344 let i0 = ti * TILE;
19345 let i1 = (i0 + TILE).min(m);
19346 for t in 0..b {
19347 let r = &right[t * n..t * n + n];
19348 for i in i0..i1 {
19349 let a = left[i * b + t];
19350 if a == 0.0 {
19351 continue;
19352 }
19353 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19355 for (o, v) in row.iter_mut().zip(r) {
19356 *o += a * *v;
19357 }
19358 }
19359 }
19360 };
19361 match pool {
19362 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19363 for ti in s..e {
19364 body(ti);
19365 }
19366 }),
19367 _ => {
19368 for ti in 0..tiles {
19369 body(ti);
19370 }
19371 }
19372 }
19373}
19374
19375pub fn refit_flush() -> usize {
19377 let Some((dir, map)) = refit_dir() else {
19378 return 0;
19379 };
19380 let guard = map.lock().unwrap();
19381 let mut n = 0;
19382 for (li, acc) in guard.0.iter() {
19383 let w = |name: &str, v: &[f32]| {
19386 let path = format!("{dir}/{name}.{li}.f32");
19387 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19388 match std::fs::write(&path, &bytes) {
19389 Ok(()) => {}
19390 Err(e) => eprintln!(
19391 "refit: FAILED to write {path} ({} MB): {e}",
19392 bytes.len() / 1_000_000
19393 ),
19394 }
19395 };
19396 w("gss", &acc.gss);
19397 w("ya", &acc.ya);
19398 println!(
19399 "refit L{li}: {} support, {} tokens, hidden {}",
19400 acc.support.len(),
19401 acc.tokens,
19402 acc.hidden
19403 );
19404 n += 1;
19405 }
19406 n
19407}
19408
19409fn adump_row(li: usize, g: &[f32]) {
19414 use std::io::Write as _;
19415 static FILES: std::sync::OnceLock<
19416 Option<(
19417 String,
19418 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19419 )>,
19420 > = std::sync::OnceLock::new();
19421 let Some((prefix, map)) = FILES
19422 .get_or_init(|| {
19423 std::env::var("CMF_FFN_ADUMP")
19424 .ok()
19425 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19426 })
19427 .as_ref()
19428 else {
19429 return;
19430 };
19431 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19434 let (from, to) = *SPAN.get_or_init(|| {
19435 let g = |k: &str, d: usize| {
19436 std::env::var(k)
19437 .ok()
19438 .and_then(|v| v.parse().ok())
19439 .unwrap_or(d)
19440 };
19441 (
19442 g("CMF_FFN_ADUMP_FROM", 0),
19443 g("CMF_FFN_ADUMP_TO", usize::MAX),
19444 )
19445 });
19446 if li < from || li > to {
19447 return;
19448 }
19449 let mut map = map.lock().unwrap();
19450 let f = map.entry(li).or_insert_with(|| {
19451 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19452 });
19453 let mut bytes = Vec::with_capacity(g.len() * 2);
19454 for v in g {
19455 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19456 }
19457 let _ = f.write_all(&bytes);
19458}
19459
19460fn oracle_topk() -> usize {
19466 if exact_mtp_calibration_active() {
19467 return 0;
19468 }
19469 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19470 *K.get_or_init(|| {
19471 std::env::var("CMF_FFN_ORACLE_TOPK")
19472 .ok()
19473 .and_then(|v| v.parse().ok())
19474 .unwrap_or(0)
19475 })
19476}
19477
19478fn gate_topk() -> usize {
19484 if exact_mtp_calibration_active() {
19485 return 0;
19486 }
19487 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19488 *K.get_or_init(|| {
19489 std::env::var("CMF_FFN_GATE_TOPK")
19490 .ok()
19491 .and_then(|v| v.parse().ok())
19492 .unwrap_or(0)
19493 })
19494}
19495
19496fn gate_block() -> usize {
19503 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19504 *B.get_or_init(|| {
19505 std::env::var("CMF_FFN_GATE_BLOCK")
19506 .ok()
19507 .and_then(|v| v.parse().ok())
19508 .unwrap_or(1)
19509 })
19510}
19511
19512fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19514 let n = g.len();
19515 let nb = n.div_ceil(block);
19516 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19517 if kb >= nb {
19518 return;
19519 }
19520 let mut score: Vec<f32> = (0..nb)
19521 .map(|b| {
19522 g[b * block..((b + 1) * block).min(n)]
19523 .iter()
19524 .map(|v| v * v)
19525 .sum::<f32>()
19526 })
19527 .collect();
19528 let mut ord = score.clone();
19529 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19530 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19531 });
19532 let thr = *kth;
19533 for b in 0..nb {
19534 if score[b] < thr {
19535 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19536 }
19537 }
19538 score.clear();
19539}
19540
19541fn keep_top_k(g: &mut [f32], k: usize) {
19543 if gate_block() > 1 {
19544 return keep_top_blocks(g, k, gate_block());
19545 }
19546 let n = g.len();
19547 if k == 0 || k >= n {
19548 return;
19549 }
19550 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19551 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19552 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19553 });
19554 let thr = *kth;
19555 for v in g.iter_mut() {
19556 if v.abs() < thr {
19557 *v = 0.0;
19558 }
19559 }
19560}
19561
19562fn probe_sq() -> bool {
19566 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19567 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19568}
19569
19570fn probe_signed() -> bool {
19574 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19575 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19576}
19577
19578fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19586 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19587 M.get_or_init(|| {
19588 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19589 let b = std::fs::read(&p).ok()?;
19590 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19591 let vals: Vec<f32> = b[8..]
19592 .chunks_exact(4)
19593 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19594 .collect();
19595 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19596 Some((inter, vals))
19597 })
19598 .as_ref()
19599}
19600
19601fn probe_topk() -> usize {
19604 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19605 *K.get_or_init(|| {
19606 std::env::var("CMF_FFN_PROBE_TOPK")
19607 .ok()
19608 .and_then(|v| v.parse().ok())
19609 .unwrap_or(0)
19610 })
19611}
19612
19613thread_local! {
19614 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19617 const { std::cell::RefCell::new(None) };
19618}
19619
19620fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19633 if d.gate_proj.has_prism_contract()
19638 || d.up_proj.has_prism_contract()
19639 || d.down_proj.has_prism_contract()
19640 {
19641 return None;
19642 }
19643 let dt = d.down_t.as_ref()?;
19644 let inter = d.gate_proj.rows();
19645 let hidden = dt.cols();
19646 if k == 0 || k >= inter || d.act != Act::Silu {
19647 return None;
19648 }
19649 DYN_SCRATCH.with(|sc| {
19650 let mut sc = sc.borrow_mut();
19651 let DynScratch {
19652 g,
19653 mag,
19654 live,
19655 parts,
19656 } = &mut *sc;
19657 g.resize(inter, 0.0);
19658 d.gate_proj.matvec(x, g, pool);
19659 for v in g.iter_mut() {
19660 *v = inference::silu(*v);
19661 }
19662 mag.clear();
19665 mag.extend(g.iter().map(|v| v.abs()));
19666 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19667 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19668 });
19669 let thr = *kth;
19670 live.clear();
19671 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19672 let mut out = vec![0.0f32; hidden];
19673 match pool {
19674 Some(p) if live.len() >= 64 => {
19675 let nw = p.n_workers() + 1;
19676 parts.clear();
19677 parts.resize(nw * hidden, 0.0);
19678 let ptr = SendMut(parts.as_mut_ptr());
19679 let n = live.len();
19680 let live_ref: &[u32] = live;
19681 let g_ref: &[f32] = g;
19682 p.run(&|w, workers| {
19683 let chunk = n.div_ceil(workers);
19684 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19685 if s >= e {
19686 return;
19687 }
19688 WORKER_SCRATCH.with(|ws| {
19689 let mut ws = ws.borrow_mut();
19690 let [scratch, acc] = &mut *ws;
19691 scratch.resize(hidden.max(x.len()), 0.0);
19692 acc.clear();
19693 acc.resize(hidden, 0.0);
19694 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19695 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19698 d.up_proj.prefetch_row(nx as usize);
19699 dt.prefetch_row(nx as usize);
19700 }
19701 let idx = nrm as usize;
19702 let up = d.up_proj.row_dot(idx, x, scratch);
19703 let a = g_ref[idx] * up;
19704 if a != 0.0 {
19705 dt.add_row_scaled(idx, a, acc, scratch);
19706 }
19707 }
19708 for (j, v) in acc.iter().enumerate() {
19709 unsafe { *ptr.at(w * hidden + j) = *v };
19710 }
19711 });
19712 });
19713 for w in 0..nw {
19714 for (j, o) in out.iter_mut().enumerate() {
19715 *o += parts[w * hidden + j];
19716 }
19717 }
19718 }
19719 _ => {
19720 WORKER_SCRATCH.with(|ws| {
19721 let mut ws = ws.borrow_mut();
19722 let [scratch, _acc] = &mut *ws;
19723 scratch.resize(hidden.max(x.len()), 0.0);
19724 for &nrm in live.iter() {
19725 let idx = nrm as usize;
19726 let up = d.up_proj.row_dot(idx, x, scratch);
19727 let a = g[idx] * up;
19728 if a != 0.0 {
19729 dt.add_row_scaled(idx, a, &mut out, scratch);
19730 }
19731 }
19732 });
19733 }
19734 }
19735 Some(out)
19736 })
19737}
19738
19739struct DynScratch {
19742 g: Vec<f32>,
19743 mag: Vec<f32>,
19744 live: Vec<u32>,
19745 parts: Vec<f32>,
19746}
19747
19748thread_local! {
19749 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19750 std::cell::RefCell::new(DynScratch {
19751 g: Vec::new(),
19752 mag: Vec::new(),
19753 live: Vec::new(),
19754 parts: Vec::new(),
19755 })
19756 };
19757 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19759 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19760}
19761
19762fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19767 let inter = d.gate_proj.rows();
19768 FFN_SCRATCH.with(|s| {
19769 let mut s = s.borrow_mut();
19770 let [g, u, ..] = &mut *s;
19771 g.resize(inter, 0.0);
19772 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19773 } else {
19775 u.resize(inter, 0.0);
19776 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19777 for i in 0..inter {
19778 g[i] = d.act.combine(g[i], u[i]);
19779 }
19780 }
19781 zero_masked_cols(g, 1, inter, mask_row);
19782 let mut out = attention::take_buf(d.down_proj.rows());
19783 d.down_proj.matvec(g, &mut out, pool);
19784 out
19785 })
19786}
19787
19788fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19794 if d.gate_proj.has_prism_contract()
19795 || d.up_proj.has_prism_contract()
19796 || d.down_proj.has_prism_contract()
19797 {
19798 return None;
19799 }
19800 d.act.moe_job_graph_contract()?;
19803 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19806 return None;
19807 }
19808 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19809 let mut model_ref = None;
19810 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19811 let model = model_ref?;
19812 let hidden = jobs[0].down.1;
19813 let mut out = attention::take_buf(hidden);
19814 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19815 Some(out)
19816 } else {
19817 let mut out = out;
19818 attention::recycle_buf(&mut out);
19819 None
19820 }
19821}
19822
19823#[allow(clippy::type_complexity)]
19828#[allow(clippy::type_complexity)]
19829pub(crate) fn moe_parts(
19830 t: &QTensor,
19831) -> Option<(
19832 &std::sync::Arc<cortiq_core::CmfModel>,
19833 usize,
19834 usize,
19835 usize,
19836 &[f32],
19837 &[f32],
19838 bool,
19839 bool,
19840 bool,
19841)> {
19842 match t {
19843 QTensor::Mapped {
19844 model,
19845 idx,
19846 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19847 rows,
19848 cols,
19849 row_scale,
19850 col_field,
19851 ..
19852 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19853 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19854 )),
19855 QTensor::Mapped {
19857 model,
19858 idx,
19859 dtype: cortiq_core::TensorDtype::Q1,
19860 rows,
19861 cols,
19862 ..
19863 } => Some((
19864 model,
19865 *idx,
19866 *rows,
19867 *cols,
19868 &[][..],
19869 &[][..],
19870 true,
19871 false,
19872 false,
19873 )),
19874 QTensor::Mapped {
19876 model,
19877 idx,
19878 dtype: cortiq_core::TensorDtype::Q4Tiled,
19879 rows,
19880 cols,
19881 ..
19882 } => Some((
19883 model,
19884 *idx,
19885 *rows,
19886 *cols,
19887 &[][..],
19888 &[][..],
19889 false,
19890 true,
19891 false,
19892 )),
19893 QTensor::Mapped {
19895 model,
19896 idx,
19897 dtype: cortiq_core::TensorDtype::Q4TiledP,
19898 rows,
19899 cols,
19900 ..
19901 } => Some((
19902 model,
19903 *idx,
19904 *rows,
19905 *cols,
19906 &[][..],
19907 &[][..],
19908 false,
19909 true,
19910 false,
19911 )),
19912 QTensor::Mapped {
19916 model,
19917 idx,
19918 dtype: cortiq_core::TensorDtype::Q2TiledP,
19919 rows,
19920 cols,
19921 ..
19922 } => Some((
19923 model,
19924 *idx,
19925 *rows,
19926 *cols,
19927 &[][..],
19928 &[][..],
19929 false,
19930 true,
19931 true,
19932 )),
19933 _ => None,
19934 }
19935}
19936
19937#[cfg(target_os = "macos")]
19944#[allow(clippy::type_complexity)]
19945fn chunk_moe_parts(
19946 m: &MoeFfn,
19947 hidden: usize,
19948) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
19949 if m.shared.is_some()
19950 || m.per_expert_scale.is_some()
19951 || m.resonance.is_some()
19952 || m.route_tau.is_some()
19953 || m.router_input_norm
19954 || m.experts.is_empty()
19955 || m.top_k == 0
19956 || std::env::var("CMF_RMS_TRACE").is_ok()
19957 || std::env::var("CMF_ACT_DUMP").is_ok()
19958 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
19959 {
19960 return None;
19961 }
19962 let (rf, rr, rc) = m.router.f32_parts()?;
19963 if rr != m.experts.len() || rc != hidden {
19964 return None;
19965 }
19966 let inter = m.experts[0].gate_proj.rows();
19967 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
19970 let experts = m
19971 .experts
19972 .iter()
19973 .map(|e| {
19974 if e.act != Act::Silu
19975 || !e.segs.is_empty()
19976 || e.gate_proj.rows() != inter
19977 || e.up_proj.rows() != inter
19978 || e.down_proj.cols() != inter
19979 || e.gate_proj.cols() != hidden
19980 || e.down_proj.rows() != hidden
19981 {
19982 return None;
19983 }
19984 if q8 {
19985 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19986 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19987 }
19988 Some((
19989 e.gate_proj.mapped_q4tp()?.1,
19990 e.up_proj.mapped_q4tp()?.1,
19991 e.down_proj.mapped_q4tp()?.1,
19992 ))
19993 })
19994 .collect::<Option<Vec<_>>>()?;
19995 Some((rf, experts, q8))
19996}
19997
19998#[cfg(target_os = "macos")]
20011fn metal_flint_moe_graph_parts(
20012 m: &MoeFfn,
20013 hidden: usize,
20014 scratch_key: usize,
20015) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20016 use std::sync::Arc;
20017
20018 const HIDDEN: usize = 2_048;
20019 const ROUTED_INTER: usize = 512;
20020 const SHARED_INTER: usize = 1_024;
20021 const EXPERTS: usize = 256;
20022 const TOP_K: usize = 6;
20023
20024 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20025 return None;
20026 }
20027 let routed0 = m.experts.first()?;
20028 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20029 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20030 if router_rows != EXPERTS || router_cols != HIDDEN {
20031 return None;
20032 }
20033 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20034 return None;
20035 }
20036 let same_q2 = |t: &QTensor| -> Option<usize> {
20037 let (owner, idx) = t.mapped_q2tp()?;
20038 Arc::ptr_eq(&owner, &model).then_some(idx)
20039 };
20040 let same_q4 = |t: &QTensor| -> Option<usize> {
20041 let (owner, idx) = t.mapped_q4tp()?;
20042 Arc::ptr_eq(&owner, &model).then_some(idx)
20043 };
20044 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20045 if e.act != Act::Silu
20046 || !e.segs.is_empty()
20047 || e.gate_proj.rows() != ROUTED_INTER
20048 || e.gate_proj.cols() != HIDDEN
20049 || e.up_proj.rows() != ROUTED_INTER
20050 || e.up_proj.cols() != HIDDEN
20051 || e.down_proj.rows() != HIDDEN
20052 || e.down_proj.cols() != ROUTED_INTER
20053 {
20054 return None;
20055 }
20056 Some((
20057 same_q2(&e.gate_proj)?,
20058 same_q2(&e.up_proj)?,
20059 same_q4(&e.down_proj)?,
20060 ))
20061 };
20062 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20063 if e.act != Act::Silu
20064 || !e.segs.is_empty()
20065 || e.gate_proj.rows() != SHARED_INTER
20066 || e.gate_proj.cols() != HIDDEN
20067 || e.up_proj.rows() != SHARED_INTER
20068 || e.up_proj.cols() != HIDDEN
20069 || e.down_proj.rows() != HIDDEN
20070 || e.down_proj.cols() != SHARED_INTER
20071 {
20072 return None;
20073 }
20074 Some((
20075 same_q4(&e.gate_proj)?,
20076 same_q4(&e.up_proj)?,
20077 same_q4(&e.down_proj)?,
20078 ))
20079 };
20080 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20081 if experts.len() != EXPERTS {
20082 return None;
20083 }
20084 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20085 if shared_gate.is_some() {
20086 return None;
20087 }
20088 Some(crate::gpu::GpuFlintMoe {
20089 router,
20090 bias: m.expert_bias.as_deref(),
20091 experts,
20092 scratch_key,
20093 shared: shared(shared_ffn)?,
20094 top_k: m.top_k,
20095 norm_topk: m.norm_topk_prob,
20096 route_scale: m.routed_scaling,
20097 sigmoid: m.router_sigmoid,
20098 sigmoid_denom_eps: m.sigmoid_denom_eps,
20099 })
20100}
20101
20102#[cfg(target_os = "macos")]
20103fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20104 if m.router_input_norm
20105 || m.route_tau.is_some()
20106 || m.mask.is_some()
20107 || m.per_expert_scale.is_some()
20108 || m.experts.is_empty()
20109 || m.top_k == 0
20110 || m.resonance.is_some()
20111 {
20112 return None;
20113 }
20114 let (sh, sg) = match &m.shared {
20119 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20120 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20121 None => return None,
20122 };
20123 let (rf, rr, rc) = m.router.f32_parts()?;
20124 if rr != m.experts.len() || rc != hidden {
20125 return None;
20126 }
20127 let shared_gated = sg.is_some();
20128 let sf = match sg {
20129 Some(sg) => {
20130 let (sf, sr, sc) = sg.f32_parts()?;
20131 if sr * sc != hidden {
20132 return None;
20133 }
20134 sf
20135 }
20136 None => &rf[..hidden],
20139 };
20140 if let Some(b) = &m.expert_bias {
20141 if b.len() != m.experts.len() {
20142 return None;
20143 }
20144 }
20145 let inter = m.experts[0].gate_proj.rows();
20146 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20149 let q8 = crate::gpu_metal::moe_q8::enabled()
20153 && m.shared.is_none()
20154 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20155 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20156 if e.act != Act::Silu
20157 || e.gate_proj.rows() != inter
20158 || e.gate_proj.cols() != hidden
20159 || e.up_proj.rows() != inter
20160 || e.up_proj.cols() != hidden
20161 || e.down_proj.rows() != hidden
20162 || e.down_proj.cols() != inter
20163 {
20164 return None;
20165 }
20166 if q8 {
20167 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20168 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20169 }
20170 let pick = |t: &QTensor| -> Option<usize> {
20171 if gu_q2 {
20172 t.mapped_q2tp().map(|(_, i)| i)
20173 } else {
20174 t.mapped_q4tp().map(|(_, i)| i)
20175 }
20176 };
20177 Some((
20178 pick(&e.gate_proj)?,
20179 pick(&e.up_proj)?,
20180 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20181 ))
20182 };
20183 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20184 let shared = match sh {
20185 Some(sh) => Some(trio(sh)?),
20186 None => None,
20187 };
20188 Some(crate::gpu::GpuMoe {
20189 router: rf,
20190 sgate: sf,
20191 experts,
20192 shared,
20193 n_exp: m.experts.len(),
20194 top_k: m.top_k,
20195 inter,
20196 norm_topk: m.norm_topk_prob,
20197 route_scale: m.routed_scaling,
20198 gu_q2,
20199 q8,
20200 sigmoid: m.router_sigmoid,
20201 sigmoid_denom_eps: m.sigmoid_denom_eps,
20202 bias: m.expert_bias.as_deref(),
20203 shared_gated,
20204 })
20205}
20206
20207fn forced_ids_at(step: usize) -> Option<u32> {
20213 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20214 IDS.get_or_init(|| {
20215 std::env::var("CMF_FORCE_IDS")
20216 .ok()
20217 .and_then(|p| std::fs::read_to_string(p).ok())
20218 .map(|s| {
20219 s.split_whitespace()
20220 .filter_map(|t| t.parse().ok())
20221 .collect()
20222 })
20223 .unwrap_or_default()
20224 })
20225 .get(step)
20226 .copied()
20227}
20228
20229pub(crate) fn moe_push_job_parts<'a>(
20233 gate: &'a QTensor,
20234 up: &'a QTensor,
20235 down: &'a QTensor,
20236 x: &[f32],
20237 w: f32,
20238 swiglu_limit: f32,
20239 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20240 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20241) -> Option<()> {
20242 use crate::qtensor::prescale;
20243 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20244 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20245 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20246 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20247 return None; }
20249 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20252 return None;
20253 }
20254 if !gq2 && dq2 {
20255 return None;
20256 }
20257 model_ref.get_or_insert_with(|| gm.clone());
20258 let dt = |cf: &[f32]| {
20259 if cf.is_empty() {
20260 cortiq_core::TensorDtype::Q8Row
20261 } else {
20262 cortiq_core::TensorDtype::Q8_2f
20263 }
20264 };
20265 jobs.push(crate::gpu::MoeJob {
20266 gate: (gi, gr, gc, grs),
20267 up: (ui, ur, uc, urs),
20268 down: (di, dr, dc, drs),
20269 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20270 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20271 down_col: dcf,
20272 w,
20273 q1: gq1,
20274 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20275 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20276 gu_q2: gq2,
20277 swiglu_limit,
20278 act: 0,
20279 });
20280 Some(())
20281}
20282
20283fn moe_push_job<'a>(
20285 d: &'a DenseFfn,
20286 x: &[f32],
20287 w: f32,
20288 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20289 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20290) -> Option<()> {
20291 use crate::qtensor::prescale;
20292 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20297 let act = graph_act.code();
20298 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20299 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20300 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20301 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20302 return None; }
20304 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20305 return None;
20306 }
20307 if !gq2 && dq2 {
20308 return None;
20309 }
20310 model_ref.get_or_insert_with(|| gm.clone());
20311 let gdt = if gcf.is_empty() {
20312 cortiq_core::TensorDtype::Q8Row
20313 } else {
20314 cortiq_core::TensorDtype::Q8_2f
20315 };
20316 let udt = if ucf.is_empty() {
20317 cortiq_core::TensorDtype::Q8Row
20318 } else {
20319 cortiq_core::TensorDtype::Q8_2f
20320 };
20321 jobs.push(crate::gpu::MoeJob {
20322 gate: (gi, gr, gc, grs),
20323 up: (ui, ur, uc, urs),
20324 down: (di, dr, dc, drs),
20325 xs_gate: prescale(x, gcf, gdt).into_owned(),
20326 xs_up: prescale(x, ucf, udt).into_owned(),
20327 down_col: dcf,
20328 w,
20329 q1: gq1,
20330 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20331 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20332 gu_q2: gq2,
20333 swiglu_limit,
20334 act,
20335 });
20336 Some(())
20337}
20338
20339fn sparse_ffn_quant(
20346 d: &DenseFfn,
20347 x: &[f32],
20348 active: &[u16],
20349 hidden: usize,
20350 pool: Option<&Pool>,
20351) -> Vec<f32> {
20352 let n = active.len();
20353 let inter = d.gate_proj.rows();
20354 let mut act = vec![0.0f32; n];
20355 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20358 let compute = |ai: usize| -> f32 {
20359 let idx = active[ai] as usize;
20360 if idx >= inter {
20361 return 0.0; }
20363 let mut s = if need_scratch {
20364 vec![0.0f32; hidden]
20365 } else {
20366 Vec::new()
20367 };
20368 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20369 let up = d.up_proj.row_dot(idx, x, &mut s);
20370 d.act.combine(gate, up)
20371 };
20372 match pool {
20373 Some(p) if n >= 256 => {
20374 let ptr = SendMut(act.as_mut_ptr());
20375 p.run(&|widx, nw| {
20376 let chunk = n.div_ceil(nw);
20377 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20378 for ai in s..e {
20379 unsafe { *ptr.at(ai) = compute(ai) };
20380 }
20381 });
20382 }
20383 _ => {
20384 for (ai, a) in act.iter_mut().enumerate() {
20385 *a = compute(ai);
20386 }
20387 }
20388 }
20389 let mut out = vec![0.0f32; hidden];
20391 for (ai, &idx) in active.iter().enumerate() {
20392 let w = act[ai];
20393 if w.abs() >= 1e-12 && (idx as usize) < inter {
20394 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20395 }
20396 }
20397 out
20398}
20399
20400#[doc(hidden)]
20402pub fn sparse_ffn_quant_for_test(
20403 d: &DenseFfn,
20404 x: &[f32],
20405 active: &[u16],
20406 hidden: usize,
20407) -> Vec<f32> {
20408 sparse_ffn_quant(d, x, active, hidden, None)
20409}
20410
20411fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20415 let deq = |t: &QTensor| -> Vec<f32> {
20416 let (rows, cols) = (t.rows(), t.cols());
20417 let mut out = vec![0.0f32; rows * cols];
20418 for r in 0..rows {
20419 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20420 }
20421 out
20422 };
20423 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20424}
20425
20426struct SendMut(*mut f32);
20428unsafe impl Send for SendMut {}
20429unsafe impl Sync for SendMut {}
20430impl SendMut {
20431 #[inline]
20432 #[allow(clippy::mut_from_ref)]
20435 unsafe fn at(&self, i: usize) -> &mut f32 {
20436 unsafe { &mut *self.0.add(i) }
20437 }
20438}
20439
20440pub(crate) fn moe_route(
20452 logits: &[f32],
20453 m: &MoeFfn,
20454 allowed: Option<&[bool]>,
20455) -> (Vec<usize>, Vec<f32>, f32) {
20456 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20457}
20458
20459pub(crate) fn moe_route_with_eps(
20469 logits: &[f32],
20470 m: &MoeFfn,
20471 allowed: Option<&[bool]>,
20472 sigmoid_denom_eps: f32,
20473) -> (Vec<usize>, Vec<f32>, f32) {
20474 let ne = logits.len();
20475 let admit = |e: usize| {
20481 m.mask.as_ref().is_none_or(|mk| mk[e])
20482 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20483 };
20484 if m.resonance.is_some() && m.top_k == 1 {
20497 let mut best: Option<usize> = None;
20498 for e in (0..ne).filter(|&e| admit(e)) {
20499 let l = logits[e];
20500 if l == f32::NEG_INFINITY || l.is_nan() {
20501 continue;
20502 }
20503 if best.is_none_or(|b| l > logits[b]) {
20504 best = Some(e);
20505 }
20506 }
20507 if let Some(b) = best {
20508 let mut p = vec![0.0f32; ne];
20509 p[b] = 1.0;
20510 return (vec![b], p, 1.0 / m.routed_scaling);
20511 }
20512 }
20513 let p: Vec<f32> = if m.router_sigmoid {
20519 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20520 } else {
20521 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20522 if mx == f32::NEG_INFINITY {
20523 vec![1.0 / ne.max(1) as f32; ne]
20524 } else {
20525 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20526 let s: f32 = e.iter().sum();
20527 for v in &mut e {
20528 *v /= s;
20529 }
20530 e
20531 }
20532 };
20533 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20534 match &m.expert_bias {
20536 Some(b) => idx.sort_unstable_by(|&x, &y| {
20537 (p[y] + b[y])
20538 .partial_cmp(&(p[x] + b[x]))
20539 .unwrap()
20540 .then(x.cmp(&y))
20541 }),
20542 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20543 }
20544 idx.truncate(m.top_k);
20545 if let Some(tau) = m.route_tau {
20549 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20550 if total > 0.0 {
20551 let mut acc = 0.0f32;
20552 let mut keep = idx.len();
20553 for (i, &e) in idx.iter().enumerate() {
20554 acc += p[e];
20555 if acc >= tau * total {
20556 keep = i + 1;
20557 break;
20558 }
20559 }
20560 idx.truncate(keep);
20561 }
20562 }
20563 let wsum: f32 = if m.norm_topk_prob {
20564 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20565 (if m.router_sigmoid {
20569 s + sigmoid_denom_eps
20570 } else {
20571 s
20572 }) / m.routed_scaling
20573 } else {
20574 1.0 / m.routed_scaling
20575 };
20576 (idx, p, wsum)
20577}
20578
20579fn moe_trace(idx: &[usize]) {
20581 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20582}
20583
20584pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20587 use std::io::Write;
20588 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20589 std::sync::OnceLock::new();
20590 let Some(f) = F.get_or_init(|| {
20591 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20592 Some(std::sync::Mutex::new(
20593 std::fs::OpenOptions::new()
20594 .create(true)
20595 .append(true)
20596 .open(p)
20597 .ok()?,
20598 ))
20599 }) else {
20600 return;
20601 };
20602 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20603 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20604}
20605
20606pub(crate) fn moe_ffn(
20609 m: &MoeFfn,
20610 x: &[f32],
20611 pool: Option<&Pool>,
20612 allowed: Option<&[bool]>,
20613) -> Vec<f32> {
20614 let r = moe_ffn_route(m, x, pool, allowed);
20615 moe_ffn_experts(m, x, &r, pool)
20616}
20617
20618pub(crate) struct MoeRoute {
20622 pub idx: Vec<usize>,
20623 pub p: Vec<f32>,
20624 pub wsum: f32,
20625 pub logits: Vec<f32>,
20626}
20627
20628pub(crate) fn moe_ffn_route(
20634 m: &MoeFfn,
20635 x: &[f32],
20636 pool: Option<&Pool>,
20637 allowed: Option<&[bool]>,
20638) -> MoeRoute {
20639 accumulate_act(m, x, 1);
20640 let ne = m.experts.len();
20641 let mut logits = vec![0.0f32; ne];
20642 match &m.resonance {
20643 Some(r) => r.scores(x, &mut logits),
20644 None => m.router.matvec(x, &mut logits, pool),
20645 }
20646 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20647 {
20648 let mut st = m.stats.borrow_mut();
20649 if st.len() < ne {
20650 st.resize(ne, 0);
20651 }
20652 for &e in &idx {
20653 st[e] += 1;
20654 }
20655 }
20656 moe_trace(&idx);
20662 MoeRoute {
20663 idx,
20664 p,
20665 wsum,
20666 logits,
20667 }
20668}
20669
20670#[cfg(target_os = "macos")]
20679fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20680 const HIDDEN: usize = 2_048;
20681 const ROUTED_INTER: usize = 512;
20682 const SHARED_INTER: usize = 1_024;
20683 const EXPERTS: usize = 256;
20684 const TOP_K: usize = 6;
20685
20686 let Some(routed) = m.experts.first() else {
20687 return false;
20688 };
20689 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20690 return false;
20691 };
20692 let Some((shared, shared_gate)) = &m.shared else {
20693 return false;
20694 };
20695 if shared_gate.is_some()
20696 || model.header.arch.arch_name != "flint"
20697 || model.header.arch.prism_hadamard.is_some()
20698 || model.header.arch.hidden_size != HIDDEN
20699 || m.experts.len() != EXPERTS
20700 || m.top_k != TOP_K
20701 || !m.router_sigmoid
20702 || !m.norm_topk_prob
20703 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20704 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20705 || m.route_tau.is_some()
20706 || m.mask.is_some()
20707 || m.per_expert_scale.is_some()
20708 || m.router_input_norm
20709 || m.resonance.is_some()
20710 || !m.grown.is_empty()
20711 {
20712 return false;
20713 }
20714
20715 let routed_q2 = routed.act == Act::Silu
20716 && routed.segs.is_empty()
20717 && routed.gate_proj.rows() == ROUTED_INTER
20718 && routed.gate_proj.cols() == HIDDEN
20719 && routed.up_proj.rows() == ROUTED_INTER
20720 && routed.up_proj.cols() == HIDDEN
20721 && routed.down_proj.rows() == HIDDEN
20722 && routed.down_proj.cols() == ROUTED_INTER
20723 && routed.up_proj.mapped_q2tp().is_some()
20724 && routed.down_proj.mapped_q4tp().is_some();
20725 let shared_q4 = shared.act == Act::Silu
20726 && shared.segs.is_empty()
20727 && shared.gate_proj.rows() == SHARED_INTER
20728 && shared.gate_proj.cols() == HIDDEN
20729 && shared.up_proj.rows() == SHARED_INTER
20730 && shared.up_proj.cols() == HIDDEN
20731 && shared.down_proj.rows() == HIDDEN
20732 && shared.down_proj.cols() == SHARED_INTER
20733 && shared.gate_proj.mapped_q4tp().is_some()
20734 && shared.up_proj.mapped_q4tp().is_some()
20735 && shared.down_proj.mapped_q4tp().is_some();
20736 routed_q2 && shared_q4
20737}
20738
20739pub(crate) fn moe_ffn_experts(
20742 m: &MoeFfn,
20743 x: &[f32],
20744 r: &MoeRoute,
20745 pool: Option<&Pool>,
20746) -> Vec<f32> {
20747 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20748 if crate::gpu::enabled_here() {
20753 #[cfg(target_os = "macos")]
20754 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20755 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20761 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20762 });
20763 }
20764 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20765 crate::gpu::ProbeArm::Gpu => {
20766 let t0 = std::time::Instant::now();
20767 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20768 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20769 return out;
20770 }
20771 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20772 }
20773 crate::gpu::ProbeArm::CpuTimed => {
20774 let t0 = std::time::Instant::now();
20775 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20776 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20777 return out;
20778 }
20779 crate::gpu::ProbeArm::Cpu => {
20780 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20781 }
20782 }
20783 }
20784 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20785}
20786
20787fn moe_ffn_minicpm_tail(
20791 slot: &mut crate::minicpm_tail::MiniCpmTail,
20792 li: usize,
20793 total_layers: usize,
20794 m: &MoeFfn,
20795 x: &[f32],
20796 pool: Option<&Pool>,
20797) -> Vec<f32> {
20798 let route = moe_ffn_route(m, x, pool, None);
20799 match slot.forward(li, m, x, &route, pool, total_layers) {
20800 Some(out) => out,
20801 None => moe_ffn_experts(m, x, &route, pool),
20806 }
20807}
20808
20809fn moe_ffn_banked(
20813 slot: &mut crate::mimo_moe::Slot,
20814 li: usize,
20815 m: &MoeFfn,
20816 x: &[f32],
20817 pool: Option<&Pool>,
20818) -> Vec<f32> {
20819 let t0 = std::time::Instant::now();
20820 let r = moe_ffn_route(m, x, pool, None);
20821 slot.note_route(t0.elapsed().as_nanos() as u64);
20822 match slot.forward(li, m, x, &r, pool) {
20823 Some(out) => out,
20824 None => crate::qtensor::float_activations_scope(|| {
20825 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20826 }),
20827 }
20828}
20829
20830fn moe_ffn_banked_rows(
20832 slot: &mut crate::mimo_moe::Slot,
20833 li: usize,
20834 m: &MoeFfn,
20835 xs: &[f32],
20836 b: usize,
20837 hidden: usize,
20838 pool: Option<&Pool>,
20839) -> Vec<f32> {
20840 let t0 = std::time::Instant::now();
20841 let routes: Vec<_> = xs
20842 .chunks_exact(hidden)
20843 .map(|x| moe_ffn_route(m, x, pool, None))
20844 .collect();
20845 slot.note_route(t0.elapsed().as_nanos() as u64);
20846 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20847 return out;
20848 }
20849 let mut out = Vec::with_capacity(b * hidden);
20850 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20851 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20852 crate::qtensor::float_activations_scope(|| {
20854 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20855 })
20856 });
20857 out.extend(row);
20858 }
20859 out
20860}
20861
20862fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20867 use std::sync::atomic::{AtomicBool, Ordering};
20868 if built {
20869 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20870 if total_layers > 0 && layers_run < total_layers {
20871 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20872 } else {
20873 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20874 }
20875 } else {
20876 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20877 }
20878 static SAID: AtomicBool = AtomicBool::new(false);
20879 if !SAID.swap(true, Ordering::Relaxed) {
20880 if built {
20881 tracing::info!("wgpu whole-token graph: ACTIVE");
20882 } else {
20883 tracing::warn!("wgpu whole-token graph refused — per-op path");
20884 }
20885 }
20886}
20887
20888pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20892pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20893pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20897pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20899
20900pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20904pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20905pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20906 std::sync::atomic::AtomicU64::new(0);
20907pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20908pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20909pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
20913 std::sync::atomic::AtomicU64::new(0);
20914pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20915pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
20916 std::sync::atomic::AtomicU64::new(0);
20917pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
20918 std::sync::atomic::AtomicU64::new(0);
20919
20920fn moe_batch_enabled() -> bool {
20923 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20924 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
20925}
20926
20927fn moe_ffn_cpu_batched(
20933 m: &MoeFfn,
20934 x: &[f32],
20935 idx: &[usize],
20936 p: &[f32],
20937 wsum: f32,
20938 pool: Option<&Pool>,
20939) -> Option<Vec<f32>> {
20940 if idx.is_empty() || !moe_batch_enabled() {
20941 return None;
20942 }
20943 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
20947 return None;
20948 }
20949 let n = idx.len() + usize::from(m.shared.is_some());
20950 let mut pairs = Vec::with_capacity(n);
20951 let mut downs = Vec::with_capacity(n);
20952 let mut ws = Vec::with_capacity(n);
20953 for &e in idx {
20954 let d = &m.experts[e];
20955 if d.act != Act::Silu {
20956 return None;
20957 }
20958 pairs.push((&d.gate_proj, &d.up_proj));
20959 downs.push(&d.down_proj);
20960 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
20961 }
20962 if let Some((se, gate)) = &m.shared {
20965 if se.act != Act::Silu {
20966 return None;
20967 }
20968 let g = gate.as_ref().map_or(1.0, |gate| {
20969 let mut gl = [0.0f32; 1];
20970 gate.matvec(x, &mut gl, pool);
20971 1.0 / (1.0 + (-gl[0]).exp())
20972 });
20973 pairs.push((&se.gate_proj, &se.up_proj));
20974 downs.push(&se.down_proj);
20975 ws.push(g);
20976 }
20977 let inter = pairs[0].0.rows();
20978 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
20979 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
20980 return None;
20981 }
20982 let mut out = attention::take_buf(x.len());
20983 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
20984 attention::recycle_buf(&mut out);
20985 return None;
20986 }
20987 Some(out)
20988}
20989
20990pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20998pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20999pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21000pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21001pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21002
21003pub(crate) fn moe_cold_experts_cpu(
21004 experts: &[(&DenseFfn, f32)],
21005 x: &[f32],
21006 pool: Option<&Pool>,
21007) -> Vec<f32> {
21008 let mut out = attention::take_buf(x.len());
21009 if experts.is_empty() {
21010 return out;
21011 }
21012 let pairs: Vec<_> = experts
21013 .iter()
21014 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21015 .collect();
21016 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21017 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21018 let inter = experts[0].0.gate_proj.rows();
21019 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21020 let silu_only = experts
21028 .iter()
21029 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21030 if silu_only
21031 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21032 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21033 {
21034 return out;
21035 }
21036 out.fill(0.0);
21037 for &(expert, weight) in experts {
21038 let mut one = dense_ffn(expert, x, pool);
21039 for (o, v) in out.iter_mut().zip(&one) {
21040 *o += weight * v;
21041 }
21042 attention::recycle_buf(&mut one);
21043 }
21044 out
21045}
21046
21047pub(crate) fn moe_cold_experts_rows_cpu(
21051 jobs: &[Vec<(&DenseFfn, f32)>],
21052 xs: &[f32],
21053 hidden: usize,
21054 pool: Option<&Pool>,
21055) -> Vec<f32> {
21056 let mut out = vec![0.0; xs.len()];
21057 let mut experts: Vec<&DenseFfn> = Vec::new();
21058 let mut groups: Vec<Vec<usize>> = Vec::new();
21059 let mut terms = vec![Vec::new(); jobs.len()];
21060 for (r, row) in jobs.iter().enumerate() {
21061 for &(e, w) in row {
21062 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21063 Some(g) => g,
21064 None => {
21065 experts.push(e);
21066 groups.push(Vec::new());
21067 groups.len() - 1
21068 }
21069 };
21070 terms[r].push((g, groups[g].len(), w));
21071 groups[g].push(r);
21072 }
21073 }
21074 if experts.is_empty() {
21075 return out;
21076 }
21077 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21078 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21079 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21080 let count: usize = lens.iter().sum();
21081 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21082 let mut ds = vec![vec![0.0; hidden]; count];
21083 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21084 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21085 {
21086 let mut offset = 0;
21087 let offsets: Vec<_> = lens
21088 .iter()
21089 .map(|&n| {
21090 let start = offset;
21091 offset += n;
21092 start
21093 })
21094 .collect();
21095 for (r, terms) in terms.iter().enumerate() {
21096 for &(g, slot, w) in terms {
21097 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21098 .iter_mut()
21099 .zip(&ds[offsets[g] + slot])
21100 {
21101 *o += w * v;
21102 }
21103 }
21104 }
21105 } else {
21106 for (r, jobs) in jobs.iter().enumerate() {
21107 if !jobs.is_empty() {
21108 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21109 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21110 attention::recycle_buf(&mut row);
21111 }
21112 }
21113 }
21114 out
21115}
21116
21117fn moe_ffn_cpu(
21119 m: &MoeFfn,
21120 x: &[f32],
21121 idx: &[usize],
21122 p: &[f32],
21123 wsum: f32,
21124 pool: Option<&Pool>,
21125) -> Vec<f32> {
21126 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21127 return out;
21128 }
21129 let mut out = attention::take_buf(x.len());
21130 for &e in idx {
21131 let mut eo = dense_ffn(&m.experts[e], x, pool);
21132 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21133 for i in 0..out.len() {
21134 out[i] += w * eo[i];
21135 }
21136 attention::recycle_buf(&mut eo);
21137 }
21138 if let Some((se, gate)) = &m.shared {
21139 let mut so = dense_ffn(se, x, pool);
21140 let g = gate.as_ref().map_or(1.0, |gate| {
21141 let mut gl = [0.0f32; 1];
21142 gate.matvec(x, &mut gl, pool);
21143 1.0 / (1.0 + (-gl[0]).exp())
21144 });
21145 for i in 0..out.len() {
21146 out[i] += g * so[i];
21147 }
21148 attention::recycle_buf(&mut so);
21149 }
21150 out
21151}
21152
21153#[allow(clippy::too_many_arguments)]
21161pub(crate) fn mla_attention(
21162 w: &MlaWeights,
21163 normed: &[f32],
21164 cache: &mut crate::kv_cache::LayerKvCache,
21165 position: usize,
21166 inv_freq: &[f32],
21167 rope_scale: f32,
21168 eps: f64,
21169 pool: Option<&Pool>,
21170) -> Vec<f32> {
21171 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21172 let hd = dr + dn;
21173 let mut q = vec![0.0f32; nh * hd];
21174 match (&w.q_a, &w.q_a_norm) {
21175 (Some(qa), Some(qn)) => {
21176 let mut t = vec![0.0f32; qa.rows()];
21177 qa.matvec(normed, &mut t, pool);
21178 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21179 w.q_proj.matvec(&tn, &mut q, pool);
21180 }
21181 _ => w.q_proj.matvec(normed, &mut q, pool),
21182 }
21183 let mut ca = vec![0.0f32; lora + dr];
21184 w.kv_a.matvec(normed, &mut ca, pool);
21185 let (c_lat, k_rope) = ca.split_at_mut(lora);
21186 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21187 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21188 w.kv_b.matvec(&latn, &mut kvb, pool);
21189 if !w.nope {
21190 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21191 }
21192 for h in 0..nh {
21193 if !w.nope {
21194 attention::rope_rotate_scaled(
21195 &mut q[h * hd..h * hd + dr],
21196 position,
21197 inv_freq,
21198 rope_scale,
21199 );
21200 }
21201 }
21202 let mut k = vec![0.0f32; nh * hd];
21203 let mut v = vec![0.0f32; nh * hd];
21204 for h in 0..nh {
21205 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21206 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21207 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21208 }
21209 cache.append(&k, &v, &vec![true; nh]);
21210 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21211 attention::recycle_buf(&mut imp);
21212 let mut ov = vec![0.0f32; nh * dv];
21213 for h in 0..nh {
21214 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21215 }
21216 let mut out = vec![0.0f32; w.o_proj.rows()];
21217 w.o_proj.matvec(&ov, &mut out, pool);
21218 out
21219}
21220
21221fn dense_moe_ffn(
21228 dm: &DenseMoeFfn,
21229 x_normed: &[f32],
21230 h_raw: &[f32],
21231 eps: f64,
21232 norm_style: NormStyle,
21233 pool: Option<&Pool>,
21234) -> Vec<f32> {
21235 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21236 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21237 let m = &dm.moe;
21238 let ne = m.experts.len();
21239 let mut logits = vec![0.0f32; ne];
21240 if m.router_input_norm {
21241 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21242 let inv = 1.0 / (ss + eps as f32).sqrt();
21243 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21244 m.router.matvec(&xr, &mut logits, pool);
21245 } else {
21246 m.router.matvec(h_raw, &mut logits, pool);
21247 }
21248 let (idx, p, wsum) = moe_route(&logits, m, None);
21249 {
21250 let mut st = m.stats.borrow_mut();
21251 if st.len() < ne {
21252 st.resize(ne, 0);
21253 }
21254 for &e in &idx {
21255 st[e] += 1;
21256 }
21257 }
21258 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21259 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21260 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21261 for (di, mi) in d.iter_mut().zip(&mo) {
21262 *di += mi;
21263 }
21264 d
21265}
21266
21267fn moe_gpu_refused(why: &'static str) {
21274 use std::sync::atomic::{AtomicBool, Ordering};
21275 static SAID: AtomicBool = AtomicBool::new(false);
21276 if !SAID.swap(true, Ordering::Relaxed) {
21277 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21278 }
21279}
21280
21281fn moe_ffn_gpu(
21282 m: &MoeFfn,
21283 x: &[f32],
21284 idx: &[usize],
21285 p: &[f32],
21286 wsum: f32,
21287 pool: Option<&Pool>,
21288) -> Option<Vec<f32>> {
21289 use crate::gpu::MoeJob;
21290
21291 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21292 let mut model_ref = None;
21293 for &e in idx {
21294 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21295 moe_gpu_refused("push_job(expert)");
21296 return None;
21297 }
21298 }
21299 if let Some((se, gate)) = &m.shared {
21300 let g = gate.as_ref().map_or(1.0, |gate| {
21301 let mut gl = [0.0f32; 1];
21302 gate.matvec(x, &mut gl, pool);
21303 1.0 / (1.0 + (-gl[0]).exp())
21304 });
21305 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21306 moe_gpu_refused("push_job(shared)");
21307 return None;
21308 }
21309 }
21310 let Some(model) = model_ref else {
21311 moe_gpu_refused("no model_ref");
21312 return None;
21313 };
21314 let hidden = jobs[0].down.1;
21315 let mut out = vec![0.0f32; hidden];
21316 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21317 Some(out)
21318 } else {
21319 moe_gpu_refused("gpu::moe_block");
21320 None
21321 }
21322}
21323
21324fn ffn_forward(
21326 ffn: &FfnKind,
21327 x: &[f32],
21328 pool: Option<&Pool>,
21329 experts_allowed: Option<&[bool]>,
21330) -> Vec<f32> {
21331 match ffn {
21332 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21333 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21334 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21335 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21339 }
21340}
21341
21342fn ffn_forward_pair(
21346 ffn: &FfnKind,
21347 x1: &[f32],
21348 x2: &[f32],
21349 pool: Option<&Pool>,
21350 experts_allowed: Option<&[bool]>,
21351) -> (Vec<f32>, Vec<f32>) {
21352 let d = match ffn {
21353 FfnKind::Dense(d) if !d.segs.is_empty() => {
21356 return (
21357 tube_ffn(d, x1, 1, pool, None),
21358 tube_ffn(d, x2, 1, pool, None),
21359 );
21360 }
21361 FfnKind::Dense(d) => d,
21362 FfnKind::Moe(m) => {
21363 return (
21364 moe_ffn(m, x1, pool, experts_allowed),
21365 moe_ffn(m, x2, pool, experts_allowed),
21366 );
21367 }
21368 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21369 };
21370 let inter = d.gate_proj.rows();
21371 FFN_SCRATCH.with(|s| {
21372 let mut s = s.borrow_mut();
21373 let [g1, g2, u1, u2] = &mut *s;
21374 g1.resize(inter, 0.0);
21375 g2.resize(inter, 0.0);
21376 u1.resize(inter, 0.0);
21377 u2.resize(inter, 0.0);
21378 QTensor::matvec2_many(
21381 [&d.gate_proj, &d.up_proj],
21382 x1,
21383 x2,
21384 [g1.as_mut_slice(), u1.as_mut_slice()],
21385 [g2.as_mut_slice(), u2.as_mut_slice()],
21386 pool,
21387 );
21388 for i in 0..inter {
21389 g1[i] = d.act.combine(g1[i], u1[i]);
21390 g2[i] = d.act.combine(g2[i], u2[i]);
21391 }
21392 let mut o1 = attention::take_buf(d.down_proj.rows());
21393 let mut o2 = attention::take_buf(d.down_proj.rows());
21394 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21395 (o1, o2)
21396 })
21397}
21398
21399#[cfg(test)]
21400mod tests {
21401 use std::ffi::OsString;
21402 use std::sync::Mutex;
21403
21404 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21407
21408 struct ScopedTestEnv {
21409 previous: Vec<(&'static str, Option<OsString>)>,
21410 }
21411
21412 impl ScopedTestEnv {
21413 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21414 let previous = vars
21415 .iter()
21416 .map(|(name, _)| (*name, std::env::var_os(name)))
21417 .collect();
21418 for &(name, value) in vars {
21419 unsafe {
21420 match value {
21421 Some(value) => std::env::set_var(name, value),
21422 None => std::env::remove_var(name),
21423 }
21424 }
21425 }
21426 Self { previous }
21427 }
21428 }
21429
21430 impl Drop for ScopedTestEnv {
21431 fn drop(&mut self) {
21432 for (name, value) in self.previous.drain(..).rev() {
21433 unsafe {
21434 match value {
21435 Some(value) => std::env::set_var(name, value),
21436 None => std::env::remove_var(name),
21437 }
21438 }
21439 }
21440 }
21441 }
21442
21443 #[test]
21444 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21445 use super::pad_mtp_draft_logits;
21446
21447 let mut logits = vec![-3.0, -1.0, -2.0];
21448 pad_mtp_draft_logits(&mut logits, 6);
21449 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21450 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21451 assert_eq!(
21452 logits
21453 .iter()
21454 .enumerate()
21455 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21456 .map(|(i, _)| i),
21457 Some(1),
21458 "an unevaluated row cannot win an all-negative shortlist"
21459 );
21460 }
21461
21462 #[test]
21463 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21464 use super::{Pipeline, SpecMon, SpecTrial};
21465
21466 let mut mon = SpecMon::default();
21467 let start = std::time::Instant::now();
21468 mon.round(1.0, 1);
21469 let first = Pipeline::spec_trial_round(
21470 SpecTrial::Spec {
21471 t0: start,
21472 gen0: 0,
21473 rounds: 0,
21474 },
21475 &mut mon,
21476 1,
21477 );
21478 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21479
21480 mon.round(1.0, 1);
21481 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21482 assert!(
21483 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21484 "two verifier-tip-only rounds must stop the losing MTP trial"
21485 );
21486
21487 let mut viable = SpecMon::default();
21490 viable.round(1.0, 1);
21491 viable.round(1.0, 2);
21492 let continued = Pipeline::spec_trial_round(
21493 SpecTrial::Spec {
21494 t0: start,
21495 gen0: 0,
21496 rounds: 1,
21497 },
21498 &mut viable,
21499 3,
21500 );
21501 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21502 }
21503
21504 #[test]
21505 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21506 let mut p = spark_test_pipeline(None);
21507 let head_rows = 248_070;
21508 p.draft_full_streak = 7;
21509 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21510
21511 p.reset_fresh_mtp_draft_state();
21512 assert_eq!(
21513 p.draft_head_rows(head_rows),
21514 super::Pipeline::draft_vocab_rows(head_rows),
21515 "a fresh request returns to its configured shortlist"
21516 );
21517
21518 p.note_draft_id(u32::MAX);
21521 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21522 }
21523
21524 #[test]
21525 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21526 use super::graph_spec_wanted_by_policy;
21527
21528 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21532 assert!(!graph_spec_wanted_by_policy(
21533 Some("anything-but-1"),
21534 true,
21535 false,
21536 false,
21537 true,
21538 ));
21539 assert!(graph_spec_wanted_by_policy(
21540 Some("1"),
21541 false,
21542 true,
21543 false,
21544 true,
21545 ));
21546 assert!(!graph_spec_wanted_by_policy(
21547 Some("0"),
21548 true,
21549 false,
21550 false,
21551 true,
21552 ));
21553
21554 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21557 assert!(graph_spec_wanted_by_policy(
21558 Some("legacy-force"),
21559 false,
21560 true,
21561 true,
21562 false,
21563 ));
21564 }
21565
21566 #[test]
21567 fn encoder_hidden_states_are_normalized_and_reusable() {
21568 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21569 let ids = [1, 7, 11, 19];
21570
21571 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21572 let second = p
21573 .encode_hidden_states(&ids)
21574 .expect("reusable encoder forward");
21575 assert_eq!(first.len(), ids.len() * 8);
21576 assert!(first.iter().all(|v| v.is_finite()));
21577 assert_eq!(
21578 first, second,
21579 "encoder forward must start from a fresh state"
21580 );
21581
21582 assert!(p.forward_ids(&ids, None).is_ok());
21585 }
21586
21587 #[test]
21592 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21593 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21594 let dense_card = ChunkStackFacts {
21595 plain_dense: true,
21596 discrete: true,
21597 gpu_on: true,
21598 ..Default::default()
21599 };
21600 assert!(dense_card.dense_on_discrete());
21601 assert_eq!(
21603 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21604 DISCRETE_DENSE_PREFILL_CHUNK
21605 );
21606 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21607 for (label, facts) in [
21608 (
21609 "GDN hybrid / MoE / DeepSeek stack",
21610 ChunkStackFacts {
21611 plain_dense: false,
21612 ..dense_card
21613 },
21614 ),
21615 (
21616 "integrated GPU",
21617 ChunkStackFacts {
21618 discrete: false,
21619 ..dense_card
21620 },
21621 ),
21622 (
21623 "CPU only",
21624 ChunkStackFacts {
21625 gpu_on: false,
21626 discrete: false,
21627 ..dense_card
21628 },
21629 ),
21630 (
21631 "capacity split",
21632 ChunkStackFacts {
21633 capacity_split: true,
21634 ..dense_card
21635 },
21636 ),
21637 (
21638 "multi-GPU plan",
21639 ChunkStackFacts {
21640 multi_gpu: true,
21641 ..dense_card
21642 },
21643 ),
21644 (
21645 "O(1) layers",
21646 ChunkStackFacts {
21647 o1: true,
21648 ..dense_card
21649 },
21650 ),
21651 ] {
21652 assert!(!facts.dense_on_discrete(), "{label}");
21653 assert_eq!(
21654 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21655 48,
21656 "{label} keeps the historical x86 chunk"
21657 );
21658 }
21659 for dense in [false, true] {
21661 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21662 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21663 }
21664 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21666 for dense in [false, true] {
21667 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21668 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21669 }
21670 }
21671 }
21672
21673 #[test]
21674 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21675 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21676 let full = |host_rows, device_rows| ReuseLayer {
21677 full: true,
21678 host_rows,
21679 device_rows,
21680 device_state: false,
21681 };
21682 assert_eq!(
21685 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21686 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21687 );
21688 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21690 assert_eq!(
21692 kv_reuse_plan(339, &[full(339, Some(345))]),
21693 ReusePlan::Ready
21694 );
21695 assert_eq!(
21697 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21698 ReusePlan::Pull(vec![(0, 300, 339)])
21699 );
21700 assert_eq!(
21702 kv_reuse_plan(339, &[full(300, Some(320))]),
21703 ReusePlan::Fresh
21704 );
21705 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21706 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21707 let conv = |device_state| ReuseLayer {
21710 full: false,
21711 host_rows: 0,
21712 device_rows: None,
21713 device_state,
21714 };
21715 assert_eq!(
21716 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21717 ReusePlan::Fresh
21718 );
21719 assert_eq!(
21720 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21721 ReusePlan::Ready
21722 );
21723 }
21724
21725 #[test]
21726 fn nll_graph_policy_scopes_only_the_fused_head() {
21727 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21728 ("vulkan graph", true, true, false, true, false),
21730 ("native Metal graph", true, true, true, true, true),
21732 ("masked", false, true, false, false, false),
21734 ("graph disabled", true, false, true, false, false),
21735 ] {
21736 let (graph_quality, graph_head_required) =
21737 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21738 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21739 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21740 }
21741 }
21742
21743 #[test]
21744 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21745 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21746 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21747 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21748 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21749 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21750 }
21751
21752 #[test]
21753 fn cancel_flag_stops_generation() {
21754 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21755 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21758 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21759 assert_eq!(r.finish_reason, "cancelled");
21760 assert!(
21761 r.token_ids.is_empty(),
21762 "no tokens after cancel: {:?}",
21763 r.token_ids
21764 );
21765 assert_eq!(p.kv_cache.seq_len(), 0);
21766 assert!(p.kv_history.is_empty());
21767 assert!(!p.graph_want_logits);
21768 assert!(p.graph_logits.is_none());
21769 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21771 assert_ne!(r2.finish_reason, "cancelled");
21772 }
21773 use super::*;
21774
21775 #[test]
21776 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21777 let arch = |arch_name: &str, hidden_act: &str| {
21781 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21782 "arch_name": arch_name,
21783 "hidden_size": 8,
21784 "intermediate_size": 16,
21785 "num_layers": 1,
21786 "num_attention_heads": 1,
21787 "num_kv_heads": 1,
21788 "head_dim": 8,
21789 "vocab_size": 16,
21790 "layer_types": ["FullAttention"],
21791 "rms_norm_eps": 1e-6,
21792 "max_position_embeddings": 16,
21793 "hidden_act": hidden_act
21794 }))
21795 .expect("minimal architecture")
21796 };
21797 let minimax = arch("minimax_m3_vl", "swigluoai");
21798 assert_eq!(
21799 Act::from_arch_full(&minimax),
21800 Act::SwiGluOai {
21801 alpha: 1.702,
21802 limit: 7.0,
21803 }
21804 );
21805
21806 let unrelated = arch("qwen2", "swigluoai");
21809 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21810
21811 let non_oai = arch("minimax_m3_vl", "gelu");
21815 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21816 }
21817
21818 #[test]
21819 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21820 let act = Act::swiglu_oai(1.702, 7.0);
21821 let got = act.combine(10.0, -10.0);
21822 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21823 assert!((got - expected).abs() < 1e-6);
21824 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21825 assert_eq!(
21826 act.wgpu_graph_act(),
21827 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21828 "the WGPU-only arm accepts only the frozen M3 constants"
21829 );
21830 assert_eq!(
21831 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21832 None,
21833 "nearby OAI constants must not borrow MiniMax's kernel"
21834 );
21835 assert_eq!(
21836 act.moe_job_graph_contract(),
21837 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21838 "the direct MoE path carries the frozen clamp with the marker"
21839 );
21840 assert_eq!(
21841 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21842 None,
21843 "a nearby clamp must not borrow MiniMax's device contract"
21844 );
21845
21846 let gate_only = act.apply(-10.0);
21849 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21850 assert!((gate_only - expected_gate).abs() < 1e-10);
21851 }
21852
21853 #[test]
21854 fn explicit_sliding_schedule_selects_local_rope_profile() {
21855 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21860 p.swa = Some((1024, usize::MAX));
21861 p.sliding_layers = Some(vec![true, true, true, false]);
21862 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21863 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21864 p.rope_scale = 1.277_258_9;
21865 p.rope_scale_local = 1.0;
21866
21867 for li in 0..3 {
21868 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21869 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21870 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21871 }
21872 assert_eq!(p.layer_window(3), None);
21873 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21874 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21875 }
21876
21877 #[test]
21885 fn dynamic_ffn_equals_the_zeroing_arm() {
21886 let (hidden, inter) = (8usize, 32usize);
21887 let synth = |n: usize, salt: usize| -> Vec<f32> {
21888 (0..n)
21889 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21890 .collect()
21891 };
21892 let down = synth(hidden * inter, 3);
21893 let mut down_t = vec![0.0f32; inter * hidden];
21894 for r in 0..hidden {
21895 for c in 0..inter {
21896 down_t[c * hidden + r] = down[r * inter + c];
21897 }
21898 }
21899 let d = DenseFfn {
21900 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21901 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21902 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21903 act: Act::Silu,
21904 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21905 segs: Vec::new(),
21906 };
21907 let x = synth(hidden, 11);
21908 let k = 12usize;
21909 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
21910 let mut g = vec![0.0f32; inter];
21912 d.gate_proj.matvec(&x, &mut g, None);
21913 let mut u = vec![0.0f32; inter];
21914 d.up_proj.matvec(&x, &mut u, None);
21915 for v in g.iter_mut() {
21916 *v = inference::silu(*v);
21917 }
21918 keep_top_k(&mut g, k);
21919 for i in 0..inter {
21920 g[i] *= u[i];
21921 }
21922 let mut want = vec![0.0f32; hidden];
21923 d.down_proj.matvec(&g, &mut want, None);
21924 for (a, b) in want.iter().zip(&got) {
21925 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
21926 }
21927 }
21928
21929 #[test]
21935 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
21936 let (hidden, core, tube) = (8usize, 12usize, 8usize);
21937 let inter = core + tube;
21938 let synth = |n: usize, salt: usize| -> Vec<f32> {
21939 (0..n)
21940 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
21941 .collect()
21942 };
21943 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
21944 let d_all = synth(hidden * inter, 3);
21945 let dense = DenseFfn {
21947 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
21948 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
21949 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
21950 act: Act::Silu,
21951 down_t: None,
21952 segs: Vec::new(),
21953 };
21954 let rows =
21955 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
21956 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
21957 let mut o = Vec::with_capacity(hidden * (b - a));
21958 for r in 0..hidden {
21959 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
21960 }
21961 o
21962 };
21963 let tubed = DenseFfn {
21964 down_t: None,
21965 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
21966 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
21967 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
21968 act: Act::Silu,
21969 segs: vec![FfnSeg {
21970 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
21971 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
21972 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
21973 start: core,
21974 width: tube,
21975 }],
21976 };
21977 let x = synth(hidden, 7);
21978 let want = dense_ffn(&dense, &x, None);
21979 let got = tube_ffn(&tubed, &x, 1, None, None);
21980 for (a, b) in want.iter().zip(&got) {
21981 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
21982 }
21983 let mut bits = vec![0u8; inter.div_ceil(8)];
21985 for n in 0..core {
21986 bits[n / 8] |= 1 << (n % 8);
21987 }
21988 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21989 let masked = dense_ffn_masked(&dense, &x, None, &bits);
21990 for (a, b) in masked.iter().zip(&closed) {
21991 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
21992 }
21993 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21995 for (a, b) in closed.iter().zip(&batch) {
21996 assert_eq!(a, b, "batch arm disagrees with decode arm");
21997 }
21998 }
21999
22000 #[test]
22002 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22003 let (hidden, inter) = (16usize, 40usize);
22004 let synth = |n: usize, salt: usize| -> Vec<f32> {
22005 (0..n)
22006 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22007 .collect()
22008 };
22009 let d = DenseFfn {
22010 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22011 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22012 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22013 act: Act::Silu,
22014 down_t: None,
22015 segs: Vec::new(),
22016 };
22017 let x = synth(hidden, 9);
22018 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22020
22021 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22022
22023 let mut g = vec![0.0f32; inter];
22025 d.gate_proj.matvec(&x, &mut g, None);
22026 let mut u = vec![0.0f32; inter];
22027 d.up_proj.matvec(&x, &mut u, None);
22028 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22029 for i in 0..inter {
22030 g[i] = if act_set.contains(&(i as u16)) {
22031 inference::silu(g[i]) * u[i]
22032 } else {
22033 0.0
22034 };
22035 }
22036 let mut reference = vec![0.0f32; hidden];
22037 d.down_proj.matvec(&g, &mut reference, None);
22038
22039 let max_d = sparse
22040 .iter()
22041 .zip(&reference)
22042 .map(|(a, b)| (a - b).abs())
22043 .fold(0.0f32, f32::max);
22044 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22045 }
22046
22047 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22050 let (h, inter, heads, kv, hd) = (
22051 p.hidden_size,
22052 inter,
22053 p.num_heads,
22054 p.num_kv_heads,
22055 p.head_dim,
22056 );
22057 let synth = |n: usize, salt: usize| -> Vec<f32> {
22058 (0..n)
22059 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22060 .collect()
22061 };
22062 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22063 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22064 };
22065 p.mtp = Some(MtpModule {
22066 enorm: vec![1.0; h],
22067 hnorm: vec![1.0; h],
22068 eh_proj: qt(h, 2 * h, 301),
22069 layer: LayerWeights {
22070 input_norm: vec![1.0; h],
22071 post_norm: vec![1.0; h],
22072 attn_out_norm: None,
22073 ffn_out_norm: None,
22074 layer_scale: None,
22075 ffn: FfnKind::Dense(DenseFfn {
22076 gate_proj: qt(inter, h, 315),
22077 up_proj: qt(inter, h, 316),
22078 down_proj: qt(h, inter, 317),
22079 act: Act::Silu,
22080 down_t: None,
22081 segs: Vec::new(),
22082 }),
22083 attn: AttnKind::Full {
22084 bias: None,
22085 wq: qt(heads * hd, h, 311),
22086 wk: qt(kv * hd, h, 312),
22087 wv: qt(kv * hd, h, 313),
22088 wo: qt(h, heads * hd, 314),
22089 q_norm: None,
22090 k_norm: None,
22091 output_gate: false,
22092 softplus_gate: None,
22093 },
22094 },
22095 final_norm: vec![1.0; h],
22096 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22097 });
22098 }
22099
22100 fn attach_test_mtp(p: &mut Pipeline) {
22102 attach_test_mtp_width(p, p.intermediate_size);
22103 }
22104
22105 #[test]
22106 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22107 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22108 attach_test_mtp_width(&mut p, 11);
22111 let ids = [3, 9, 4, 12, 7, 2];
22112 let trunk = p.probe_ffn_mass(&ids);
22113 assert_eq!(trunk[0].len(), p.intermediate_size);
22114
22115 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22116 assert_eq!(got.len(), 1);
22117 assert_eq!(got[0].mass.len(), 11);
22118 assert_eq!(got[0].squared_mass.len(), 11);
22119 assert_eq!(got[0].observed_positions, ids.len() - 1);
22120 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22121 assert!(got[0]
22122 .mass
22123 .iter()
22124 .zip(&got[0].squared_mass)
22125 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22126 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22127 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22128
22129 let h = p.hidden_size;
22133 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22134 panic!("synthetic MTP must be dense")
22135 };
22136 dense.segs.push(FfnSeg {
22137 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22138 up: QTensor::from_f32(vec![0.0; h], 1, h),
22139 down: QTensor::from_f32(vec![0.0; h], h, 1),
22140 start: dense.gate_proj.rows(),
22141 width: 1,
22142 });
22143 let err = p.probe_mtp_ffn(&ids);
22144 assert!(err
22145 .unwrap_err()
22146 .contains("tube-segmented draft FFNs are not supported"));
22147 assert_eq!(p.kv_cache.seq_len(), 0);
22148 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22149 }
22150
22151 #[test]
22152 fn speculative_equals_vanilla_greedy() {
22153 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22154 let _env = ScopedTestEnv::set(&[
22157 ("CMF_GPU", Some("0")),
22158 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22159 ("CMF_MTP_PAIR", Some("1")),
22160 ]);
22161 let run = |spec: bool| {
22162 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22163 p.sampler_config.temperature = 0.0;
22164 attach_test_mtp(&mut p);
22165 p.speculative = spec;
22166 let r = p.generate("abcdef", 12, None, None).unwrap();
22167 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22168 };
22169 let (vanilla, d0, _) = run(false);
22170 let (spec, d1, a1) = run(true);
22171 assert_eq!(d0, 0, "vanilla path must not draft");
22172 assert!(d1 > 0, "speculative path must draft");
22173 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22174 assert_eq!(
22175 vanilla, spec,
22176 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22177 );
22178 }
22179
22180 #[test]
22181 fn speculative_accepts_constant_oracle() {
22182 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22183 let _env = ScopedTestEnv::set(&[
22184 ("CMF_GPU", Some("0")),
22185 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22186 ("CMF_MTP_PAIR", Some("1")),
22187 ]);
22188 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22189 p.sampler_config.temperature = 0.0;
22190 p.sampler_config.repetition_penalty = 1.0;
22191 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22194 attach_test_mtp(&mut p);
22195 p.speculative = true;
22196 let r = p.generate("abcd", 10, None, None).unwrap();
22197 assert!(r.mtp_drafted > 0);
22198 assert_eq!(
22199 r.mtp_accepted, r.mtp_drafted,
22200 "constant logits → every draft accepted"
22201 );
22202 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22205 }
22206
22207 #[test]
22208 fn host_fused_mtp_pair_is_opt_in() {
22209 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22210 let _env = ScopedTestEnv::set(&[
22211 ("CMF_GPU", Some("0")),
22212 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22213 ("CMF_MTP_PAIR", None),
22214 ]);
22215 let run = |spec: bool| {
22216 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22217 p.sampler_config.temperature = 0.0;
22218 attach_test_mtp(&mut p);
22219 p.speculative = spec;
22220 let r = p.generate("abcdef", 12, None, None).unwrap();
22221 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22222 };
22223 let (plain, d0, _) = run(false);
22224 let (guarded, d1, a1) = run(true);
22225 assert_eq!(d0, 0);
22226 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22227 assert_eq!(a1, 0);
22228 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22229 }
22230
22231 #[test]
22232 fn empty_prompt_is_an_error_not_a_panic() {
22233 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22234 let r = p.generate("", 4, None, None);
22235 assert!(r.is_err(), "empty prompt must be a clean error");
22236 }
22237
22238 #[test]
22239 fn every_token_enters_kv_exactly_once() {
22240 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22241 p.sampler_config.temperature = 0.0;
22243 let r = p.generate("abc", 2, None, None).unwrap();
22244 assert_eq!(r.prompt_tokens, 3);
22245 assert_eq!(
22249 p.kv_cache.seq_len(),
22250 3 + r.tokens_generated - 1,
22251 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22252 );
22253 }
22254
22255 #[test]
22256 fn generation_is_reproducible_with_seed() {
22257 let run = || {
22258 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22259 p.generate("hello", 8, None, None).unwrap().token_ids
22260 };
22261 assert_eq!(run(), run());
22262 }
22263
22264 #[test]
22265 fn resetting_sampler_restarts_the_seeded_stream() {
22266 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22267 let config = SamplerConfig {
22268 seed: Some(1234),
22269 ..SamplerConfig::default()
22270 };
22271 p.set_sampler_config(config.clone());
22272 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22273 p.set_sampler_config(config);
22274 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22275 assert_eq!(first, second);
22276 }
22277
22278 #[test]
22279 fn eviction_bounds_the_cache() {
22280 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22281 p.kv_cache.max_seq_len = 6;
22282 p.sampler_config.temperature = 0.0;
22283 let _ = p.generate("abcd", 12, None, None).unwrap();
22284 assert!(
22285 p.kv_cache.seq_len() <= 6 + 1,
22286 "cache must stay bounded by max_seq_len (got {})",
22287 p.kv_cache.seq_len()
22288 );
22289 }
22290
22291 #[test]
22292 fn confidence_matches_tokens_and_is_a_probability() {
22293 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22294 p.sampler_config.temperature = 0.0;
22295 p.sampler_config.repetition_penalty = 1.0;
22296 let r = p.generate("abcd", 10, None, None).unwrap();
22297 assert_eq!(
22298 r.token_confidence.len(),
22299 r.token_ids.len(),
22300 "one confidence per emitted token"
22301 );
22302 for &c in &r.token_confidence {
22303 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22304 }
22305 let logits = [1.0f32, 3.0, 0.5, 3.0];
22307 let p0 = top1_prob_t(&logits, 1, 1.0);
22308 let p1 = top1_prob_t(&logits, 3, 1.0);
22309 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22310 assert!(p0 > 0.0 && p0 < 1.0);
22311 let sharp = top1_prob_t(&logits, 1, 1.0);
22313 let soft = top1_prob_t(&logits, 1, 2.0);
22314 assert!(soft < sharp, "higher temperature lowers peak confidence");
22315 }
22316
22317 #[test]
22318 fn trace_is_opt_in_and_parallels_the_output() {
22319 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22321 p.sampler_config.temperature = 0.0;
22322 p.sampler_config.repetition_penalty = 1.0;
22323 let r = p.generate("abcd", 10, None, None).unwrap();
22324 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22325
22326 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22328 p.sampler_config.temperature = 0.0;
22329 p.sampler_config.repetition_penalty = 1.0;
22330 p.set_trace(true);
22331 let r = p.generate("abcd", 10, None, None).unwrap();
22332 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22333 for (i, tr) in r.traces.iter().enumerate() {
22334 assert_eq!(tr.t, i, "trace index is sequential");
22335 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22336 assert_eq!(
22337 tr.confidence, r.token_confidence[i],
22338 "trace confidence matches the confidence channel"
22339 );
22340 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22342 }
22343 }
22344
22345 #[test]
22346 fn explain_prefill_logits_match_greedy_first_token() {
22347 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22351 p.sampler_config.temperature = 0.0;
22352 p.sampler_config.repetition_penalty = 1.0;
22353 let ids = p.tokenizer.encode("abcd");
22354 let logits = p.prefill_next_logits(&ids, None);
22355 let argmax = logits
22356 .iter()
22357 .enumerate()
22358 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22359 .unwrap()
22360 .0 as u32;
22361 let r = p.generate("abcd", 1, None, None).unwrap();
22362 assert_eq!(
22363 argmax, r.token_ids[0],
22364 "explain preview must match greedy emit"
22365 );
22366 }
22367
22368 #[test]
22369 fn laguna_shared_expert_is_unconditionally_added() {
22370 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22371 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22372 let zero_dense = || DenseFfn {
22373 gate_proj: matrix(vec![0.0; 4]),
22374 up_proj: matrix(vec![0.0; 4]),
22375 down_proj: matrix(vec![0.0; 4]),
22376 act: Act::Silu,
22377 down_t: None,
22378 segs: Vec::new(),
22379 };
22380 let shared = DenseFfn {
22381 gate_proj: identity(),
22382 up_proj: identity(),
22383 down_proj: identity(),
22384 act: Act::Silu,
22385 down_t: None,
22386 segs: Vec::new(),
22387 };
22388 let x = [1.0, 2.0];
22389 let expected = dense_ffn(&shared, &x, None);
22390 let moe = MoeFfn {
22391 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22392 experts: vec![zero_dense()],
22393 top_k: 1,
22394 norm_topk_prob: true,
22395 router_sigmoid: true,
22396 sigmoid_denom_eps: 1e-6,
22397 expert_bias: None,
22398 routed_scaling: 1.0,
22399 route_tau: None,
22400 shared: Some((shared, None)),
22401 stats: std::cell::RefCell::new(Vec::new()),
22402 act_sq: std::cell::RefCell::new(Vec::new()),
22403 act_rows: std::cell::RefCell::new(Vec::new()),
22404 mask: None,
22405 per_expert_scale: None,
22406 router_input_norm: false,
22407 resonance: None,
22408 grown: Vec::new(),
22409 };
22410 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22411 for (actual, expected) in actual.iter().zip(expected) {
22412 assert!((actual - expected).abs() < 1e-6);
22413 }
22414 }
22415
22416 fn mimo_test_pipeline() -> Pipeline {
22425 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22426 let kvh = [1usize, 2, 2, 1];
22427 let synth = |n: usize, salt: usize| -> Vec<f32> {
22428 (0..n)
22429 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22430 .collect()
22431 };
22432 let qt = |rows: usize, cols: usize, salt: usize| {
22433 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22434 };
22435 let dense = |inter: usize, salt: usize| DenseFfn {
22436 gate_proj: qt(inter, hs, salt),
22437 up_proj: qt(inter, hs, salt + 1),
22438 down_proj: qt(hs, inter, salt + 2),
22439 act: Act::Silu,
22440 down_t: None,
22441 segs: Vec::new(),
22442 };
22443 let layers: Vec<LayerWeights> = (0..4)
22444 .map(|li| LayerWeights {
22445 input_norm: vec![1.0; hs],
22446 post_norm: vec![1.0; hs],
22447 attn_out_norm: None,
22448 ffn_out_norm: None,
22449 layer_scale: None,
22450 ffn: if li == 0 {
22451 FfnKind::Dense(dense(inter, 50))
22452 } else {
22453 FfnKind::Moe(MoeFfn {
22454 router: qt(4, hs, 60 + li),
22455 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22456 top_k: 2,
22457 norm_topk_prob: true,
22458 router_sigmoid: true,
22459 sigmoid_denom_eps: 1e-6,
22460 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22461 routed_scaling: 1.0,
22462 route_tau: None,
22463 shared: None,
22464 stats: std::cell::RefCell::new(Vec::new()),
22465 act_sq: std::cell::RefCell::new(Vec::new()),
22466 act_rows: std::cell::RefCell::new(Vec::new()),
22467 mask: None,
22468 per_expert_scale: None,
22469 router_input_norm: false,
22470 resonance: None,
22471 grown: Vec::new(),
22472 })
22473 },
22474 attn: AttnKind::Full {
22475 wq: qt(nh * hd, hs, li * 10 + 1),
22476 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22477 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22478 wo: qt(hs, nh * vd, li * 10 + 4),
22479 q_norm: None,
22480 k_norm: None,
22481 output_gate: false,
22482 softplus_gate: None,
22483 bias: None,
22484 },
22485 })
22486 .collect();
22487 let mut p = Pipeline::new(
22488 Tokenizer::byte_level(),
22489 PipelineWeights {
22490 embed_tokens: qt(vocab, hs, 100),
22491 layers,
22492 lm_head: qt(vocab, hs, 200),
22493 final_norm: vec![1.0; hs],
22494 },
22495 hs,
22496 inter,
22497 nh,
22498 1, hd,
22500 4,
22501 4,
22502 false,
22503 vocab,
22504 1e-6,
22505 1e7,
22506 NormStyle::Qwen,
22507 4096,
22508 SamplerConfig {
22509 seed: Some(7),
22510 ..Default::default()
22511 },
22512 );
22513 p.layer_dump = None;
22515 p.set_rotary(4, 1e7);
22516 p.sliding_layers = Some(vec![false, true, true, false]);
22517 p.swa = Some((3, usize::MAX));
22518 p.rotary_dim_local = Some(4);
22519 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22520 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22521 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22522 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22523 p
22524 }
22525
22526 #[test]
22527 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22528 let mut p = mimo_test_pipeline();
22529 p.speculative = false;
22530 p.ignore_eos = true;
22531 p.sampler_config.temperature = 0.0;
22532 p.sampler_config.repetition_penalty = 1.0;
22533 let a = vec![3, 5, 7, 9, 11, 13];
22534 let b = vec![4, 8, 12, 16, 20, 24];
22535 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22536 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22537 let actual = p
22541 .generate_from_embeds(&a, &rows, 8, None, None)
22542 .unwrap()
22543 .token_ids;
22544 assert_eq!(actual, expected);
22545 assert!(p.kv_history.is_empty());
22546 let mut extended = a.clone();
22547 extended.push(17);
22548 let after_media = p
22549 .generate_from_ids(&extended, 8, None, None)
22550 .unwrap()
22551 .token_ids;
22552 p.reset_session();
22553 let fresh = p
22554 .generate_from_ids(&extended, 8, None, None)
22555 .unwrap()
22556 .token_ids;
22557 assert_eq!(after_media, fresh);
22558 assert!(
22559 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22560 .is_err()
22561 );
22562 p.reset_session();
22566 p.generate_from_ids(&a, 1, None, None).unwrap();
22567 let mut media_ids = p.kv_history.clone();
22568 assert!(!media_ids.is_empty());
22569 media_ids.extend_from_slice(&[19, 21, 23]);
22570 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22571 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22572 let mut oracle = mimo_test_pipeline();
22573 oracle.speculative = false;
22574 oracle.ignore_eos = true;
22575 oracle.sampler_config.temperature = 0.0;
22576 oracle.sampler_config.repetition_penalty = 1.0;
22577 let expected = oracle
22578 .generate_from_ids(&source_ids, 8, None, None)
22579 .unwrap()
22580 .token_ids;
22581 assert_eq!(
22582 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22583 .unwrap()
22584 .token_ids,
22585 expected
22586 );
22587 assert!(p.kv_history.is_empty());
22588 let mut bad = rows;
22589 bad[0] = f32::NAN;
22590 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22591 }
22592
22593 fn f32_bits(v: &[f32]) -> Vec<u32> {
22594 v.iter().map(|x| x.to_bits()).collect()
22595 }
22596
22597 #[test]
22604 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22605 let mut p = mimo_test_pipeline();
22606 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22607 assert_eq!(kv, vec![1, 2, 2, 1]);
22608 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22609 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22610 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22611 let hs = p.hidden_size;
22612 let mut decode = Vec::new();
22613 for (pos, &id) in ids.iter().enumerate() {
22614 let e = p.embed_single(id);
22615 let h = p.forward_layers(&e, pos, None);
22616 decode.push(p.logits_from_hidden(&h));
22617 }
22618 for l in &p.kv_cache.layers {
22619 assert_eq!(l.seq_len, 12);
22620 assert_eq!(l.head_values(0).len(), 12 * 8);
22622 }
22623 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22624
22625 p.clear_sequence_state();
22626 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22627 for pos in 0..ids.len() {
22628 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22629 assert_eq!(
22630 f32_bits(&decode[pos]),
22631 f32_bits(&lg),
22632 "whole prompt, pos {pos}"
22633 );
22634 }
22635
22636 p.clear_sequence_state();
22637 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22638 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22639 for pos in 0..ids.len() {
22640 let row = if pos < 5 {
22641 &a[pos * hs..(pos + 1) * hs]
22642 } else {
22643 &b[(pos - 5) * hs..(pos - 4) * hs]
22644 };
22645 let lg = p.logits_from_hidden(row);
22646 assert_eq!(
22647 f32_bits(&decode[pos]),
22648 f32_bits(&lg),
22649 "two chunks, pos {pos}"
22650 );
22651 }
22652
22653 let last = |p: &mut Pipeline| {
22656 p.clear_sequence_state();
22657 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22658 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22659 };
22660 let base = last(&mut p);
22661 let mut no_sinks = mimo_test_pipeline();
22662 for l in &mut no_sinks.kv_cache.layers {
22663 l.sinks = None;
22664 }
22665 assert_ne!(
22666 f32_bits(&last(&mut no_sinks)),
22667 f32_bits(&base),
22668 "sinks are live"
22669 );
22670 let mut wide = mimo_test_pipeline();
22671 wide.swa = Some((64, usize::MAX));
22672 assert_ne!(
22673 f32_bits(&last(&mut wide)),
22674 f32_bits(&base),
22675 "window is live"
22676 );
22677
22678 p.clear_sequence_state();
22680 p.ignore_eos = true;
22681 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22682 assert_eq!(r.token_ids.len(), 4);
22683 }
22684
22685 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22688 let (hs, nh) = (16usize, 4usize);
22689 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22690 p.layer_dump = None;
22691 p.swa = Some((6, 4));
22692 p.proj_gate_sigmoid = true;
22693 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22694 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22695 let g: Vec<f32> = (0..nh * hs)
22696 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22697 .collect();
22698 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22699 }
22700 }
22701 p.swa_trim = trim;
22702 p
22703 }
22704
22705 #[test]
22711 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22712 let mut a = spark_test_pipeline(None);
22713 let mut b = spark_test_pipeline(Some((2, 4)));
22714 assert_eq!(
22715 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22716 vec![Some(6), Some(6), Some(6), None]
22717 );
22718 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22719 let mut pos = 0usize;
22720 for &n in [5usize, 13, 7, 16].iter().cycle() {
22721 if pos >= ids.len() {
22722 break;
22723 }
22724 let end = (pos + n).min(ids.len());
22725 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22726 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22727 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22728 pos = end;
22729 }
22730 for _ in 0..23 {
22731 let e = a.embed_single(((pos * 7) % 64) as u32);
22732 let ha = a.forward_layers(&e, pos, None);
22733 let hb = b.forward_layers(&e, pos, None);
22734 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22735 for li in 0..3 {
22736 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22737 }
22738 pos += 1;
22739 }
22740 for round in 0..9 {
22741 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22742 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22743 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22744 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22745 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22746 if round % 2 == 1 {
22747 for p in [&mut a, &mut b] {
22749 for l in &mut p.kv_cache.layers {
22750 l.truncate_last(2);
22751 }
22752 }
22753 } else {
22754 pos += 2;
22755 }
22756 }
22757 let e = a.embed_single(9);
22758 let (ha, hb) = (
22759 a.forward_layers(&e, pos, None),
22760 b.forward_layers(&e, pos, None),
22761 );
22762 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22763 for li in 0..3 {
22764 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22765 assert!(lb.base() > 0, "layer {li} trimmed");
22766 assert_eq!(la.base(), 0);
22767 assert_eq!(lb.pos_len(), la.seq_len);
22768 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22769 }
22770 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22771 assert_eq!(
22772 (gb.base(), gb.seq_len),
22773 (0, ga.seq_len),
22774 "the global layer keeps all"
22775 );
22776 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22777 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22778 }
22779
22780 #[test]
22785 fn swa_trim_wire_handoff_continues_bitwise() {
22786 let mut a = spark_test_pipeline(None);
22787 let mut b = spark_test_pipeline(Some((2, 4)));
22788 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22789 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22790 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22791 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22792 let mut pos = ids.len();
22793 for _ in 0..5 {
22794 let e = a.embed_single(((pos * 5) % 64) as u32);
22795 assert_eq!(
22796 f32_bits(&a.forward_layers(&e, pos, None)),
22797 f32_bits(&b.forward_layers(&e, pos, None))
22798 );
22799 pos += 1;
22800 }
22801 let mut c = spark_test_pipeline(Some((2, 4)));
22802 for li in 0..4 {
22803 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22804 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22805 }
22806 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22807 assert_eq!(c.kv_cache.seq_len(), pos);
22808 for step in 0..17 {
22809 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22810 let ha = a.forward_layers(&e, pos, None);
22811 let hc = c.forward_layers(&e, pos, None);
22812 assert_eq!(
22813 f32_bits(&ha),
22814 f32_bits(&hc),
22815 "after the hand-off, step {step}"
22816 );
22817 pos += 1;
22818 }
22819 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22820 }
22821
22822 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
22827 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
22828 let synth = |len: usize, salt: usize| -> Vec<f32> {
22829 (0..len)
22830 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22831 .collect()
22832 };
22833 let qt = |rows: usize, cols: usize, salt: usize| {
22834 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22835 };
22836 let layers = widths
22837 .iter()
22838 .copied()
22839 .enumerate()
22840 .map(|(k, inter)| {
22841 let s = 500 + k * 40;
22842 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22843 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22844 MtpModule {
22845 enorm: vec![1.0; hs],
22846 hnorm: vec![1.0; hs],
22847 eh_proj: qt(hs, 2 * hs, s),
22848 layer: LayerWeights {
22849 input_norm: vec![1.0; hs],
22850 post_norm: vec![1.0; hs],
22851 attn_out_norm: None,
22852 ffn_out_norm: None,
22853 layer_scale: None,
22854 attn: AttnKind::Full {
22855 wq: qt(nh * hd, hs, s + 1),
22856 wk: qt(nkv * hd, hs, s + 2),
22857 wv: qt(nkv * vd, hs, s + 3),
22858 wo: qt(hs, nh * vd, s + 4),
22859 q_norm: None,
22860 k_norm: None,
22861 output_gate: false,
22862 softplus_gate: None,
22863 bias: None,
22864 },
22865 ffn: FfnKind::Dense(DenseFfn {
22866 gate_proj: qt(inter, hs, s + 5),
22867 up_proj: qt(inter, hs, s + 6),
22868 down_proj: qt(hs, inter, s + 7),
22869 act: Act::Silu,
22870 down_t: None,
22871 segs: Vec::new(),
22872 }),
22873 },
22874 final_norm: vec![1.0; hs],
22875 kv,
22876 }
22877 })
22878 .collect();
22879 mimo_mtp::MimoMtp::from_layers(layers)
22880 }
22881
22882 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22883 mimo_test_mtp_widths(&vec![24; n], gain)
22884 }
22885
22886 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22887 p.clear_sequence_state();
22888 p.speculative = spec;
22889 p.ignore_eos = true;
22890 p.sampler_config.temperature = 0.0;
22891 p.generate_from_ids(ids, n, None, None).unwrap()
22892 }
22893
22894 #[test]
22895 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
22896 let mut p = mimo_test_pipeline();
22897 let widths = [7usize, 13, 29];
22898 assert_ne!(widths[0], p.intermediate_size);
22899 assert_ne!(widths[1], p.intermediate_size);
22900 assert_ne!(widths[2], p.intermediate_size);
22901 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
22902 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
22903
22904 let trunk = p.probe_ffn_mass(&ids);
22905 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
22906 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
22907 assert_eq!(got.len(), widths.len());
22908 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
22909 assert_eq!(stat.mass.len(), width, "draft layer {k}");
22910 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
22911 assert_eq!(stat.observed_positions, ids.len() - k - 1);
22912 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
22913 assert!(stat
22914 .mass
22915 .iter()
22916 .zip(&stat.squared_mass)
22917 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22918 }
22919 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
22920 let st = p.mimo_mtp.as_ref().unwrap();
22921 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
22922
22923 let h = p.hidden_size;
22926 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
22927 panic!("synthetic MiMo MTP must be dense")
22928 };
22929 dense.segs.push(FfnSeg {
22930 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22931 up: QTensor::from_f32(vec![0.0; h], 1, h),
22932 down: QTensor::from_f32(vec![0.0; h], h, 1),
22933 start: dense.gate_proj.rows(),
22934 width: 1,
22935 });
22936 assert!(p
22937 .probe_mtp_ffn(&ids)
22938 .unwrap_err()
22939 .contains("tube-segmented draft FFNs are not supported"));
22940 assert_eq!(p.kv_cache.seq_len(), 0);
22941 assert!(p
22942 .mimo_mtp
22943 .as_ref()
22944 .unwrap()
22945 .layers
22946 .iter()
22947 .all(|m| m.kv.seq_len == 0));
22948 }
22949
22950 #[test]
22951 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
22952 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
22953 p.clear_sequence_state();
22954 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
22955 let _exact = ExactMtpCalibrationGuard::enter();
22956 let got = crate::gpu::cpu_scope(|| {
22957 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, rows)
22958 })
22959 .expect("teacher-forced MiMo probe");
22960 st.reset();
22961 p.mimo_mtp = Some(st);
22962 p.reset_mtp_probe_state();
22963 got
22964 }
22965
22966 for post_norm_hidden in [false, true] {
22967 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
22968 let make = || {
22969 let mut p = mimo_test_pipeline();
22970 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
22971 st.post_norm_hidden = post_norm_hidden;
22972 p.mimo_mtp = Some(st);
22973 p
22974 };
22975 let mut full = make();
22976 let expected = collect(&mut full, &ids, usize::MAX);
22977 for rows in [1usize, 7, 128] {
22978 let mut chunked = make();
22979 let got = collect(&mut chunked, &ids, rows);
22980 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
22981 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
22982 assert_eq!(
22983 got.observed_positions, expected.observed_positions,
22984 "post={post_norm_hidden}, rows={rows}, layer={layer}"
22985 );
22986 assert_eq!(
22987 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
22988 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
22989 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
22990 );
22991 assert_eq!(
22992 got.squared_mass
22993 .iter()
22994 .map(|v| v.to_bits())
22995 .collect::<Vec<_>>(),
22996 expected
22997 .squared_mass
22998 .iter()
22999 .map(|v| v.to_bits())
23000 .collect::<Vec<_>>(),
23001 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23002 );
23003 }
23004 }
23005 }
23006 }
23007
23008 #[test]
23014 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23015 for post in [false, true] {
23018 let mut p = mimo_test_pipeline();
23019 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23021 let mut st0 = mimo_test_mtp(3, 1.0);
23022 st0.post_norm_hidden = post;
23023 p.mimo_mtp = Some(st0);
23024 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23025 let hs = p.hidden_size;
23026 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23027 p.mimo_note_rows(&hb, 0);
23028 let mut st = p.mimo_mtp.take().unwrap();
23029 let k = 3;
23032 let mut inc = Vec::new();
23033 for t in 0..ids.len() - k - 1 {
23034 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23035 }
23036 let s = ids.len();
23039 let mut reference = vec![vec![0u32; k]; s - k - 1];
23040 let mut fresh = mimo_test_mtp(3, 1.0);
23041 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23042 let n = s - layer - 1;
23043 let mut cats = vec![0.0f32; n * 2 * hs];
23044 for j in 0..n {
23045 let e = p.embed_single(ids[j + layer + 1]);
23046 let raw = &hb[j * hs..(j + 1) * hs];
23047 let g = if post {
23048 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23049 } else {
23050 raw.to_vec()
23051 };
23052 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23053 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23054 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23055 }
23056 let mut x = vec![0.0f32; n * hs];
23057 m.eh_proj.matmat(&cats, n, &mut x, None);
23058 p.mimo_mtp_block(m, &mut x, n, 0);
23059 for (t, row) in reference.iter_mut().enumerate() {
23060 let y = inference::rms_norm(
23061 &x[t * hs..(t + 1) * hs],
23062 &m.final_norm,
23063 p.rms_eps,
23064 p.norm_style,
23065 );
23066 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23067 }
23068 }
23069 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23070 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23072 assert!(distinct.len() > 3, "{inc:?}");
23073 let last_t = ids.len() - k - 2;
23075 for m in &st.layers {
23076 assert_eq!(m.kv.seq_len, last_t + 1);
23077 }
23078 }
23079 }
23080
23081 #[test]
23088 fn mimo_speculative_greedy_equals_plain_greedy() {
23089 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23090 let _env = ScopedTestEnv::set(&[
23091 ("CMF_GPU", Some("0")),
23092 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23093 ]);
23094 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23095 let n = 24;
23096 let mut p = mimo_test_pipeline();
23097 let plain = mimo_greedy(&mut p, &ids, n, false);
23098 assert_eq!(plain.mtp_drafted, 0);
23099 assert_eq!(plain.token_ids.len(), n);
23100 let plain_kv = p.kv_cache.layers[0].seq_len;
23101
23102 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23104 let spec = mimo_greedy(&mut p, &ids, n, true);
23105 assert!(spec.mtp_drafted > 0, "the round must draft");
23106 assert_eq!(spec.token_ids, plain.token_ids);
23107 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23108
23109 let mut truth: Vec<u32> = ids.clone();
23112 truth.extend(&plain.token_ids);
23113 let mut noisy = truth.clone();
23114 for (i, t) in noisy.iter_mut().enumerate() {
23115 if i % 5 == 0 {
23116 *t = (*t + 1) % 64;
23117 }
23118 }
23119 let mut st = mimo_test_mtp(3, 1.0);
23120 st.draft_override = Some(noisy);
23121 p.mimo_mtp = Some(st);
23122 let spec = mimo_greedy(&mut p, &ids, n, true);
23123 assert_eq!(spec.token_ids, plain.token_ids);
23124 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23125 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23126 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23127 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23128 assert!(
23129 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23130 "{:?}",
23131 stats.accept_hist
23132 );
23133 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23134
23135 let mut st = mimo_test_mtp(3, 1.0);
23138 st.draft_override = Some(truth);
23139 p.mimo_mtp = Some(st);
23140 let spec = mimo_greedy(&mut p, &ids, n, true);
23141 assert_eq!(spec.token_ids, plain.token_ids);
23142 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23143 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23144
23145 let off = mimo_greedy(&mut p, &ids, n, false);
23147 assert_eq!(off.token_ids, plain.token_ids);
23148 assert_eq!(off.mtp_drafted, 0);
23149 }
23150
23151 #[test]
23160 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23161 let p = mimo_test_pipeline();
23162 assert_eq!(
23163 p.graph_attn_decline_reason(),
23164 Some("per-layer KV head counts")
23165 );
23166 assert_eq!(p.wgpu_graph_attn_decline(), None);
23167 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23168 assert_eq!(
23169 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23170 (1, 4, 4, None, false)
23171 );
23172 assert_eq!(g0.invf, p.inv_freq.as_slice());
23173 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23174 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23175 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23176 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23177 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23178 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23179 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23180
23181 let emb = p.embed_single(3);
23184 let mut lg = Vec::new();
23185 assert!(
23186 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23187 .is_none()
23188 );
23189 let mut hid = emb.clone();
23190 assert_eq!(
23191 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23192 crate::gpu::BatchGraphOutcome::Declined
23193 );
23194 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23195 assert!(p.try_multi_burst(3, 0, 4).is_none());
23196 assert!(
23197 p.graph_declines().is_empty(),
23198 "no attention decline logged: {:?}",
23199 p.graph_declines()
23200 );
23201 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23206 assert_eq!(plain().graph_attn_decline_reason(), None);
23207 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23208 assert!(
23209 plain().graph_attn_geom(0).is_none(),
23210 "uniform models keep the historical arms"
23211 );
23212 let mut q = plain();
23213 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23214 assert_eq!(
23215 q.graph_attn_decline_reason(),
23216 Some("learned attention sinks")
23217 );
23218 assert_eq!(
23219 q.graph_attn_geom(1).unwrap().sink,
23220 Some(&[0.25f32, -0.25][..])
23221 );
23222 let mut q = plain();
23223 q.set_attn_geometry(None, Some(2)).unwrap();
23224 assert_eq!(
23225 q.graph_attn_decline_reason(),
23226 Some("V heads narrower than Q/K heads")
23227 );
23228 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23229 let mut q = plain();
23230 q.sliding_layers = Some(vec![true, false]);
23231 q.swa = Some((4, usize::MAX));
23232 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23233 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23234 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23235
23236 let mut q = mimo_test_pipeline();
23240 q.rope_scale = 2.0;
23241 q.rope_scale_local = 1.25;
23242 assert_eq!(q.wgpu_graph_attn_decline(), None);
23243 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23244 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23245
23246 let mut q = plain();
23249 q.rope_scale = 2.0;
23250 assert_eq!(
23251 q.wgpu_graph_attn_decline(),
23252 Some("scaled RoPE without per-layer geometry")
23253 );
23254 }
23255
23256 #[cfg(target_os = "macos")]
23262 #[test]
23263 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23264 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23268 p.swa = Some((16, usize::MAX));
23269 p.sliding_layers = Some(vec![true, true]);
23270 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23271 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23272
23273 assert_eq!(
23274 p.graph_attn_decline_reason(),
23275 Some("learned attention sinks")
23276 );
23277 let swa_graph = p.metal_graph_swa();
23278 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23279 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23280 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23281 assert!(
23282 native_sink_attend,
23283 "the q1 sink admission must follow the SWA door"
23284 );
23285 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23288 }
23289
23290 #[test]
23291 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23292 let mut p = mimo_test_pipeline();
23293 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23294 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23295 for _ in 0..if li == 0 { 2 } else { 12 } {
23296 layer.append(&row, &row, &[]);
23297 }
23298 }
23299 p.mimo_verify_rewind(9).unwrap();
23300 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23301 for layer in &p.kv_cache.layers[1..] {
23302 assert_eq!(layer.seq_len, 9);
23303 }
23304 }
23305
23306 #[test]
23310 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23311 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23312 let _ = std::fs::remove_dir_all(&dir);
23313 let mut p = mimo_test_pipeline();
23314 let hs = p.hidden_size;
23315 let ids = [5u32, 9, 11, 2, 40];
23316 p.layer_dump = Some(dir.join("decode"));
23317 for (pos, &id) in ids.iter().enumerate() {
23318 let e = p.embed_single(id);
23319 let _ = p.forward_layers(&e, pos, None);
23320 }
23321 p.clear_sequence_state();
23322 p.layer_dump = Some(dir.join("prefill"));
23323 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23324 for pos in 0..ids.len() {
23325 for li in 0..p.num_layers {
23326 let name = format!("p{pos:06}_l{li:02}.f32");
23327 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23328 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23329 assert_eq!(a.len(), hs * 4, "{name}");
23330 assert_eq!(a, b, "{name}");
23331 }
23332 }
23333 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23334 let vals: Vec<f32> = last
23335 .chunks(4)
23336 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23337 .collect();
23338 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23339 let _ = std::fs::remove_dir_all(&dir);
23340 }
23341
23342 #[test]
23343 fn attn_geometry_and_sinks_are_validated() {
23344 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23345 assert!(
23346 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23347 "one entry per layer"
23348 );
23349 assert!(
23350 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23351 "3 does not divide 4"
23352 );
23353 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23354 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23355 assert!(
23356 p.set_attn_geometry(None, Some(5)).is_err(),
23357 "V wider than the head"
23358 );
23359 p.set_attn_geometry(None, Some(4)).unwrap();
23360 assert_eq!(
23361 p.v_head_dim, None,
23362 "v_head_dim == head_dim is the uniform case"
23363 );
23364 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23365 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23366 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23367 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23368 assert!(
23369 p.kv_cache.layers[1].sinks.is_some(),
23370 "a reshape keeps the layer's sinks"
23371 );
23372 assert_eq!(p.layer_geom(1).0, 4);
23373 assert!(
23374 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23375 "one sink per Q head"
23376 );
23377 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23378 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23379 }
23380
23381 #[test]
23384 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23385 let cfg = || {
23386 Some(crate::nystrom::O1Cfg {
23387 layers: crate::nystrom::O1Layers::All,
23388 m: 4,
23389 w: 8,
23390 sink: 2,
23391 rect: crate::nystrom::O1Rect::Aggregate,
23392 })
23393 };
23394 let mut p = mimo_test_pipeline();
23395 p.set_o1(cfg());
23396 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23397 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23398 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23399 q.sliding_layers = Some(vec![false, false, true]);
23400 q.swa = Some((4, usize::MAX));
23401 q.set_o1(cfg());
23402 assert_eq!(q.o1_flags, vec![true, false, false]);
23403 }
23404
23405 #[test]
23406 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23407 const B: usize = 19;
23408 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23409 p.set_o1(Some(crate::nystrom::O1Cfg {
23410 layers: crate::nystrom::O1Layers::All,
23411 m: 4,
23412 w: 8,
23413 sink: 2,
23414 rect: crate::nystrom::O1Rect::Aggregate,
23415 }));
23416 p.o1_begin_with_prefix(Some(B));
23417 let ids: Vec<u32> = (0..B as u32).collect();
23418 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23419
23420 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23421 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23422 let next = p.embed_single(B as u32);
23423 let _ = p.forward_layers(&next, B, None);
23424 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23425 }
23426
23427 #[test]
23428 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23429 const B: usize = 19;
23430 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23431 let gdn_cfg = crate::linear_core::GdnCfg {
23435 num_v_heads: 2,
23436 num_k_heads: 1,
23437 key_head_dim: 2,
23438 value_head_dim: 4,
23439 conv_kernel: 3,
23440 hidden_size: 8,
23441 rms_eps: 1e-6,
23442 output_gate_sigmoid: false,
23443 };
23444 let synth = |n: usize, salt: usize| -> Vec<f32> {
23445 (0..n)
23446 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23447 .collect()
23448 };
23449 let qt = |rows: usize, cols: usize, salt: usize| {
23450 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23451 };
23452 let c_dim = gdn_cfg.conv_dim();
23453 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23454 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23455 in_proj_qkv: qt(c_dim, 8, 1),
23456 in_proj_z: qt(vd, 8, 2),
23457 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23458 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23459 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23460 a_log: vec![0.2, 0.5],
23461 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23462 norm: vec![1.0; gdn_cfg.value_head_dim],
23463 out_proj: qt(8, vd, 7),
23464 });
23465 p.gdn_cfg = Some(gdn_cfg);
23466 p.set_o1(Some(crate::nystrom::O1Cfg {
23467 layers: crate::nystrom::O1Layers::All,
23468 m: 4,
23469 w: 8,
23470 sink: 2,
23471 rect: crate::nystrom::O1Rect::Aggregate,
23472 }));
23473 p.o1_begin_with_prefix(Some(B));
23474 for pos in 0..B - 2 {
23475 let emb = p.embed_single(pos as u32);
23476 let _ = p.forward_layers(&emb, pos, None);
23477 }
23478 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23479
23480 let e1 = p.embed_single((B - 2) as u32);
23481 let e2 = p.embed_single((B - 1) as u32);
23482 let _ = p.forward_pair(&e1, &e2, B - 2);
23483
23484 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23485 assert!(
23486 p.kv_cache
23487 .layers
23488 .iter()
23489 .enumerate()
23490 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23491 );
23492 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23493 assert_ne!(
23494 p.kv_cache.layers[0].linear_state, lane1_state,
23495 "real pair must commit GDN lane 2 before returning"
23496 );
23497 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23498 let next = p.embed_single(B as u32);
23499 let _ = p.forward_layers(&next, B, None);
23500 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23501 }
23502
23503 #[test]
23504 fn o1_error_observation_stays_terminal_until_reset() {
23505 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23506 p.set_o1(Some(crate::nystrom::O1Cfg {
23507 layers: crate::nystrom::O1Layers::All,
23508 m: 4,
23509 w: 8,
23510 sink: 2,
23511 rect: crate::nystrom::O1Rect::Aggregate,
23512 }));
23513 p.o1_begin();
23514 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23515
23516 assert!(p.o1_seal_checked().is_err());
23517 assert!(
23518 p.o1_seal_checked().is_err(),
23519 "retry must see the sticky error"
23520 );
23521 let k = vec![0.2f32; 4];
23522 let v = vec![0.3f32; 4];
23523 p.kv_cache.layers[0].append(&k, &v, &[]);
23524 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23525
23526 p.reset_session();
23527 p.o1_begin();
23528 p.kv_cache.layers[0].append(&k, &v, &[]);
23529 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23530 }
23531
23532 #[test]
23533 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23534 let ids = vec![1u32, 2, 3, 4, 5, 6];
23535 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23536 p.graph_logits = Some(vec![123.0]);
23537 p.graph_want_logits = true;
23538 p.graph_failed
23539 .store(true, std::sync::atomic::Ordering::Relaxed);
23540 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23541 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23542 assert!(err.contains("before NLL"));
23543 assert!(p.graph_logits.is_none());
23544 assert!(!p.graph_want_logits);
23545 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23546 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23547
23548 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23549 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23550 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23551 assert_eq!(actual.1, expected.1);
23552 assert!((actual.0 - expected.0).abs() < 1e-9);
23553 }
23554
23555 #[test]
23556 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23557 let ids = vec![1u32, 2, 3, 4, 5, 6];
23558 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23559 p.nll_test_fail_at = Some(1);
23560 let err = p
23561 .nll_ids_from(&ids, 0)
23562 .expect_err("one-shot forward failure");
23563 assert!(err.contains("forward") || err.contains("score row"));
23564 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23565 assert!(!p.graph_want_logits);
23566 assert!(p.graph_logits.is_none());
23567 assert!(p.kv_history.is_empty());
23568
23569 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23570 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23571 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23572 assert_eq!(actual.1, expected.1);
23573 assert!((actual.0 - expected.0).abs() < 1e-9);
23574 }
23575
23576 #[test]
23577 fn nll_serial_failure_before_first_row_is_reported() {
23578 let ids = vec![1u32, 2, 3, 4];
23579 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23580 p.nll_test_force_serial = true;
23581 p.nll_test_fail_at = Some(0);
23582 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23583 assert!(err.contains("serial forward"));
23584 assert!(p.kv_history.is_empty());
23585 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23586 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23587 }
23588
23589 #[test]
23590 fn ffn_probe_failure_discards_recorder_and_state() {
23591 let ids = vec![1u32, 2, 3, 4];
23592 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23593 p.nll_test_fail_at = Some(0);
23594 let err = p
23595 .probe_ffn_mass_batch(&ids)
23596 .expect_err("probe forward failure");
23597 assert!(err.contains("NLL"));
23598 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23599 assert!(p.kv_history.is_empty());
23600 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23601 }
23602
23603 #[test]
23604 fn nll_test_controls_are_pipeline_scoped() {
23605 let ids = vec![1u32, 2, 3, 4];
23606 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23607 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23608 failing.nll_test_force_serial = true;
23609 failing.nll_test_fail_at = Some(0);
23610
23611 assert!(!failing.can_prefill_batched());
23612 assert!(unaffected.can_prefill_batched());
23613 let expected = unaffected
23614 .nll_ids_from(&ids, 0)
23615 .expect("unaffected pipeline remains usable");
23616 let err = failing
23617 .nll_ids_from(&ids, 0)
23618 .expect_err("failure injection belongs to failing pipeline");
23619 assert!(err.contains("serial forward"));
23620 assert!(failing.nll_test_fail_at.is_none());
23621 assert!(unaffected.can_prefill_batched());
23622 let actual = unaffected
23623 .nll_ids_from(&ids, 0)
23624 .expect("unaffected pipeline remains reusable");
23625 assert_eq!(actual.1, expected.1);
23626 assert!((actual.0 - expected.0).abs() < 1e-9);
23627 }
23628
23629 #[test]
23630 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23631 let ids = vec![1u32, 2, 3, 4, 5, 6];
23632 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23633 p.graph_logits = Some(vec![123.0]);
23634 p.graph_want_logits = true;
23635 p.graph_failed
23636 .store(true, std::sync::atomic::Ordering::Relaxed);
23637 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23638
23639 let err = p
23640 .forward_ids(&ids, None)
23641 .expect_err("a failed forward must not become a valid head result");
23642 assert!(err.contains("forward_ids setup"));
23643 assert!(p.graph_logits.is_none());
23644 assert!(!p.graph_want_logits);
23645 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23646 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23647 assert_eq!(p.kv_cache.seq_len(), 0);
23648
23649 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23650 .forward_ids(&ids, None)
23651 .expect("fresh forward_ids");
23652 let actual = p
23653 .forward_ids(&ids, None)
23654 .expect("pipeline remains reusable after a failed forward");
23655 assert_eq!(actual.len(), expected.len());
23656 assert!(
23657 actual
23658 .iter()
23659 .zip(expected)
23660 .all(|(a, b)| (a - b).abs() < 1e-9)
23661 );
23662 assert_eq!(p.kv_cache.seq_len(), ids.len());
23663 }
23664
23665 #[test]
23666 fn sigmoid_router_floor_is_explicit_per_architecture() {
23667 let zero = || DenseFfn {
23673 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23674 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23675 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23676 act: Act::Silu,
23677 down_t: None,
23678 segs: Vec::new(),
23679 };
23680 let m = MoeFfn {
23681 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23682 experts: vec![zero(), zero()],
23683 top_k: 1,
23684 norm_topk_prob: true,
23685 router_sigmoid: true,
23686 sigmoid_denom_eps: 1e-20,
23687 expert_bias: None,
23688 routed_scaling: 2.5,
23689 route_tau: None,
23690 shared: None,
23691 stats: std::cell::RefCell::new(Vec::new()),
23692 act_sq: std::cell::RefCell::new(Vec::new()),
23693 act_rows: std::cell::RefCell::new(Vec::new()),
23694 mask: None,
23695 per_expert_scale: None,
23696 router_input_norm: false,
23697 resonance: None,
23698 grown: Vec::new(),
23699 };
23700 let logits = [-20.0f32, -20.0];
23701 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23702 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23703 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23704 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23705 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23706 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23707 assert!(legacy_wsum > minimax_wsum * 100.0);
23708 }
23709
23710 #[test]
23711 fn resonance_scores_match_formula_and_stable_tie() {
23712 let r = Resonance {
23713 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23715 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23716 k: 1,
23717 bias: vec![1.5, 0.5, 0.0],
23718 shell: Vec::new(),
23719 };
23720 let x = [1.0f32, 1.0];
23721 let mut got = vec![0.0; 3];
23722 r.scores(&x, &mut got);
23723 assert!((got[0] - 0.5).abs() < 1e-6);
23727 assert!((got[1] - 0.5).abs() < 1e-6);
23728 assert!(got[2].abs() < 1e-6);
23729 let best = got
23730 .iter()
23731 .enumerate()
23732 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23733 .map(|(i, _)| i);
23734 assert_eq!(best, Some(0));
23735 assert!(got.iter().all(|v| v.is_finite()));
23736 }
23737
23738 #[test]
23743 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23744 let plain = Resonance {
23750 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23751 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23752 k: 1,
23753 bias: vec![1.5, 0.5, 0.0, 0.0],
23754 shell: Vec::new(),
23755 };
23756 let shelled = Resonance {
23757 mu: plain.mu.clone(),
23758 u: plain.u.clone(),
23759 k: 1,
23760 bias: plain.bias.clone(),
23761 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23762 };
23763 assert!(!plain.has_shell());
23764 assert!(shelled.has_shell());
23765 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23766 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23767 set_growth_shell(Some(true));
23768 assert!(growth_shell_enabled());
23769 let x = [1.0f32, 1.0];
23772 plain.scores(&x, &mut a);
23773 shelled.scores(&x, &mut b);
23774 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23775 assert!(a[2] == 0.0 && a[3] == 0.0);
23776 let xo = [3.0f32, 0.0];
23779 plain.scores(&xo, &mut a);
23780 shelled.scores(&xo, &mut b);
23781 assert_eq!(a[2], -6.0);
23782 assert_eq!(a[3], -6.0);
23783 assert_eq!(
23784 bits(&a[..3]),
23785 bits(&b[..3]),
23786 "trunk rows + the boundary row"
23787 );
23788 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23789 assert_eq!(shelled.effective_shell(4), shelled.shell);
23790 set_growth_shell(Some(false));
23793 assert!(!growth_shell_enabled());
23794 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
23795 shelled.scores(&xo, &mut b);
23796 assert_eq!(bits(&a), bits(&b));
23797 set_growth_shell(None);
23798 let short = Resonance {
23801 shell: vec![f32::INFINITY, f32::INFINITY],
23802 ..shelled
23803 };
23804 set_growth_shell(Some(true));
23805 short.scores(&xo, &mut b);
23806 assert_eq!(bits(&a), bits(&b));
23807 set_growth_shell(None);
23808 }
23809
23810 #[test]
23814 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
23815 let zero = || DenseFfn {
23816 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23817 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23818 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23819 act: Act::Silu,
23820 down_t: None,
23821 segs: Vec::new(),
23822 };
23823 let moe = |sigmoid: bool| MoeFfn {
23824 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
23825 experts: vec![zero(), zero(), zero(), zero()],
23826 top_k: 1,
23827 norm_topk_prob: true,
23828 router_sigmoid: sigmoid,
23829 sigmoid_denom_eps: 1e-6,
23830 expert_bias: None,
23831 routed_scaling: 1.0,
23832 route_tau: None,
23833 shared: None,
23834 stats: std::cell::RefCell::new(Vec::new()),
23835 act_sq: std::cell::RefCell::new(Vec::new()),
23836 act_rows: std::cell::RefCell::new(Vec::new()),
23837 mask: None,
23838 per_expert_scale: None,
23839 router_input_norm: false,
23840 resonance: None,
23841 grown: Vec::new(),
23842 };
23843 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
23844 for sigmoid in [false, true] {
23845 let m = moe(sigmoid);
23846 let (idx, p, wsum) = moe_route(&logits, &m, None);
23847 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
23848 assert_eq!(p[1], 0.0);
23849 assert_eq!(p[3], 0.0);
23850 assert!(p[2] > p[0] && p[0] > 0.0);
23851 assert!(p.iter().all(|v| v.is_finite()));
23852 let w = p[2] / wsum;
23853 if sigmoid {
23854 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
23856 } else {
23857 assert_eq!(
23858 w, 1.0,
23859 "softmax: the renormalized top-1 weight is exactly 1"
23860 );
23861 }
23862 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
23865 assert_eq!(idx, vec![2]);
23866 }
23867 let m = moe(false);
23869 let (idx, _, _) = moe_route(
23870 &[
23871 f32::NEG_INFINITY,
23872 f32::NEG_INFINITY,
23873 f32::NEG_INFINITY,
23874 -9.0,
23875 ],
23876 &m,
23877 None,
23878 );
23879 assert_eq!(idx, vec![3]);
23880 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
23883 assert_eq!(idx, vec![0]);
23884 assert!(p.iter().all(|&v| v == 0.25));
23885 assert!(wsum.is_finite() && wsum > 0.0);
23886 }
23887
23888 #[cfg(feature = "gpu")]
23894 #[test]
23895 fn minimax_device_route_matches_host_sigmoid_contract() {
23896 match crate::gpu_wgpu::selected_and_up() {
23897 None => {
23898 crate::gpu_wgpu::skip_or_fail(module_path!());
23899 return;
23900 }
23901 Some(false) => {
23909 eprintln!(
23910 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23911 module_path!()
23912 );
23913 return;
23914 }
23915 Some(true) => {}
23916 }
23917 let n = 128usize;
23918 let top_k = 4usize;
23919 let logits: Vec<f32> = (0..n)
23920 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23921 .collect();
23922 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23923 let m = MoeFfn {
23924 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23925 experts: Vec::new(),
23926 top_k,
23927 norm_topk_prob: true,
23928 router_sigmoid: true,
23929 sigmoid_denom_eps: 1e-20,
23930 expert_bias: Some(bias.clone()),
23931 routed_scaling: 2.0,
23932 route_tau: None,
23933 shared: None,
23934 stats: std::cell::RefCell::new(Vec::new()),
23935 act_sq: std::cell::RefCell::new(Vec::new()),
23936 act_rows: std::cell::RefCell::new(Vec::new()),
23937 mask: None,
23938 per_expert_scale: None,
23939 router_input_norm: false,
23940 resonance: None,
23941 grown: Vec::new(),
23942 };
23943 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23944 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
23945
23946 for shared in [Some(n as u32), None] {
23947 let mut gi = Vec::new();
23948 let mut gw = Vec::new();
23949 assert!(
23950 crate::gpu_wgpu::minimax_moe_route_for_test(
23951 &logits,
23952 Some(&bias),
23953 top_k,
23954 2.0,
23955 shared,
23956 &mut gi,
23957 &mut gw,
23958 ),
23959 "the device did not come up — the test checked nothing"
23960 );
23961 let routed = if shared.is_some() { top_k } else { gi.len() };
23962 assert_eq!(
23963 &gi[..routed],
23964 &ci[..],
23965 "different experts or order (shared={shared:?})"
23966 );
23967 let d = cw
23968 .iter()
23969 .zip(&gw[..routed])
23970 .map(|(a, b)| (a - b).abs())
23971 .fold(0.0f32, f32::max);
23972 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
23973 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
23974 if let Some(s) = shared {
23975 assert_eq!(gi[top_k], s as usize, "shared pin slot");
23976 assert_eq!(gw[top_k], 1.0, "shared weight");
23977 }
23978 }
23979 }
23980
23981 #[cfg(feature = "gpu")]
23986 #[test]
23987 fn minimax_device_cold_weights_match_host_contract() {
23988 match crate::gpu_wgpu::selected_and_up() {
23989 None => {
23990 crate::gpu_wgpu::skip_or_fail(module_path!());
23991 return;
23992 }
23993 Some(false) => {
24001 eprintln!(
24002 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24003 module_path!()
24004 );
24005 return;
24006 }
24007 Some(true) => {}
24008 }
24009 let n = 128usize;
24010 let top_k = 4usize;
24011 let logits: Vec<f32> = (0..n)
24012 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24013 .collect();
24014 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24015 let m = MoeFfn {
24016 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24017 experts: Vec::new(),
24018 top_k,
24019 norm_topk_prob: true,
24020 router_sigmoid: true,
24021 sigmoid_denom_eps: 1e-20,
24022 expert_bias: Some(bias.clone()),
24023 routed_scaling: 2.0,
24024 route_tau: None,
24025 shared: None,
24026 stats: std::cell::RefCell::new(Vec::new()),
24027 act_sq: std::cell::RefCell::new(Vec::new()),
24028 act_rows: std::cell::RefCell::new(Vec::new()),
24029 mask: None,
24030 per_expert_scale: None,
24031 router_input_norm: false,
24032 resonance: None,
24033 grown: Vec::new(),
24034 };
24035 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24036 assert_eq!(ci.len(), top_k);
24037
24038 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24040 let mut cold = Vec::new();
24041 let mut idx = Vec::new();
24042 let mut dw = Vec::new();
24043 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24044 &logits,
24045 Some(&bias),
24046 top_k,
24047 2.0,
24048 &resident,
24049 n as u32,
24050 &mut cold,
24051 &mut idx,
24052 &mut dw,
24053 ));
24054 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24055 for (r, &e) in ci.iter().enumerate() {
24060 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24061 let host = p[e] / wsum;
24062 let d = (dw[r] - host).abs();
24063 assert!(
24064 d <= 1e-6 * host.abs().max(1.0),
24065 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24066 dw[r]
24067 );
24068 }
24069 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24070 assert_eq!(dw[top_k], 1.0, "shared weight");
24071
24072 let mut remap = resident.clone();
24074 remap[ci[0]] = u32::MAX;
24075 remap[ci[2]] = u32::MAX;
24076 let mut cold = Vec::new();
24077 let mut idx2 = Vec::new();
24078 let mut dw2 = Vec::new();
24079 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24080 &logits,
24081 Some(&bias),
24082 top_k,
24083 2.0,
24084 &remap,
24085 n as u32,
24086 &mut cold,
24087 &mut idx2,
24088 &mut dw2,
24089 ));
24090 let mut resident_weight_checked = 0usize;
24098 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24099 if dw2[r] == 0.0 {
24100 continue; }
24102 let e = slot as usize;
24103 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24104 let host = p[e] / wsum;
24105 let d = (dw2[r] - host).abs();
24106 assert!(
24107 d <= 1e-6 * host.abs().max(1.0),
24108 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24109 );
24110 resident_weight_checked += 1;
24111 }
24112 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24113 println!("partial bank: checked {resident_weight_checked} resident weights");
24114 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24115 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24116 ids.sort_unstable();
24117 let mut want = vec![ci[0], ci[2]];
24118 want.sort_unstable();
24119 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24120 for &(e, w) in &cold {
24121 let host = p[e] / wsum;
24122 let d = (w - host).abs();
24123 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24124 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24125 }
24126 }
24127
24128 #[test]
24133 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24134 let zero = || DenseFfn {
24135 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24136 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24137 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24138 act: Act::Silu,
24139 down_t: None,
24140 segs: Vec::new(),
24141 };
24142 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24143 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24144 experts: vec![zero(), zero(), zero()],
24145 top_k: 1,
24146 norm_topk_prob: norm_topk,
24147 router_sigmoid: false,
24148 sigmoid_denom_eps: 1e-6,
24149 expert_bias: None,
24150 routed_scaling: 1.0,
24151 route_tau: None,
24152 shared: None,
24153 stats: std::cell::RefCell::new(Vec::new()),
24154 act_sq: std::cell::RefCell::new(Vec::new()),
24155 act_rows: std::cell::RefCell::new(Vec::new()),
24156 mask: None,
24157 per_expert_scale: None,
24158 router_input_norm: false,
24159 resonance: resonant.then(|| Resonance {
24160 mu: vec![0.0; 6],
24161 u: Vec::new(),
24162 k: 0,
24163 bias: vec![0.0; 3],
24164 shell: Vec::new(),
24165 }),
24166 grown: Vec::new(),
24167 };
24168 let lo = -0.1f32;
24171 let hi = f32::from_bits(lo.to_bits() - 1);
24172 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24173 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24174 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24176 assert_eq!(
24177 idx,
24178 vec![0],
24179 "softmax tie → lower index (the gated contract)"
24180 );
24181 for norm in [true, false] {
24184 let m = moe(true, norm);
24185 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24186 assert_eq!(idx, vec![1], "norm_topk {norm}");
24187 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24188 assert_eq!(p[1] / wsum, 1.0);
24189 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24192 assert_eq!(idx, vec![0]);
24193 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24195 assert_eq!(idx, vec![2]);
24196 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24197 assert_eq!(idx, vec![0]);
24198 assert_eq!(p[0] / wsum, 1.0);
24199 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24202 assert_eq!(idx, vec![0]);
24203 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24204 }
24205 }
24206}