1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18use crate::kv_cache::KvCache;
19use crate::linear_core::{
20 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
21 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
22 vmf_phase_pair,
23};
24use crate::pool::Pool;
25use crate::qtensor::QTensor;
26use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
27use crate::tokenizer::Tokenizer;
28use cortiq_core::mask::TaskMask;
29use cortiq_core::types::NormStyle;
30
31pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
32 std::sync::atomic::AtomicBool::new(false);
33
34thread_local! {
39 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
40}
41
42struct ExactMtpCalibrationGuard;
43
44impl ExactMtpCalibrationGuard {
45 fn enter() -> Self {
46 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
47 Self
48 }
49}
50
51impl Drop for ExactMtpCalibrationGuard {
52 fn drop(&mut self) {
53 EXACT_MTP_CALIBRATION.with(|depth| {
54 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
55 depth.set(depth.get().saturating_sub(1));
56 });
57 }
58}
59
60fn exact_mtp_calibration_active() -> bool {
61 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
62}
63
64fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
68 logits.resize(vocab_size, f32::NEG_INFINITY);
69}
70
71struct ForwardScratch {
75 n1: Vec<f32>,
76 n2: Vec<f32>,
77 p1: Vec<f32>,
78 p2: Vec<f32>,
79}
80
81impl ForwardScratch {
82 fn new(hidden: usize) -> Self {
83 Self {
84 n1: vec![0.0; hidden],
85 n2: vec![0.0; hidden],
86 p1: vec![0.0; hidden],
87 p2: vec![0.0; hidden],
88 }
89 }
90}
91
92pub struct Pipeline {
94 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
99 pub tokenizer: std::sync::Arc<Tokenizer>,
102 pub kv_cache: KvCache,
103 pub sampler_config: SamplerConfig,
104 pub weights: PipelineWeights,
105 pub hidden_size: usize,
106 pub intermediate_size: usize,
107 pub num_heads: usize,
108 pub num_kv_heads: usize,
109 pub head_dim: usize,
110 pub num_layers: usize,
112 pub physical_layers: usize,
114 pub loop_final_norm: bool,
116 pub vocab_size: usize,
117 pub rms_eps: f64,
118 pub rope_base: f32,
119 pub norm_style: NormStyle,
120 pub rotary_dim: usize,
122 pub attention_heads_per_layer: Option<Vec<usize>>,
124 pub kv_heads_per_layer: Option<Vec<usize>>,
129 pub v_head_dim: Option<usize>,
134 pub layer_dump: Option<std::path::PathBuf>,
147 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
150 pub(crate) mimo_moe: crate::mimo_moe::Slot,
153 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
158 pub vmf_cfg: Option<VmfPhaseCfg>,
160 pub gdn_cfg: Option<GdnCfg>,
162 pub logit_multiplier: Option<f32>,
164 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
169 graph_failed: std::sync::atomic::AtomicBool,
174 pub kv_history: Vec<u32>,
179 pub kv_history_device: bool,
182 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
184 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
187 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
190 pub dsv4: Option<
194 Box<(
195 crate::dsv4::Dsv4Globals,
196 Vec<crate::dsv4::Dsv4Layer>,
197 crate::dsv4::Dsv4Cfg,
198 crate::dsv4::Dsv4State,
199 )>,
200 >,
201 pub dsv41: Option<
205 Box<(
206 crate::dsv41::Dsv41Globals,
207 Vec<crate::dsv41::Dsv41Layer>,
208 crate::dsv41::Dsv41Cfg,
209 crate::dsv41::Dsv41State,
210 )>,
211 >,
212 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
214 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
216 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
221 clover_stats: clover::CloverStats,
222 pub qwen4_exp: Option<
223 Box<(
224 crate::qwen4_exp::Globals,
225 Vec<crate::qwen4_exp::Layer>,
226 crate::qwen4_exp::Cfg,
227 crate::qwen4_exp::State,
228 )>,
229 >,
230 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
234 pub dspark: Option<crate::dsv4::DsparkState>,
236 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
239 pub dspark_hist: Vec<usize>,
241 pub dspark_real: Vec<u32>,
245 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
249 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
251 pub dspark_draft_ns: u128,
255 m3_winners: Vec<Vec<u32>>,
257 m3_counts: Vec<u32>,
259 m3_n_exp: usize,
260 m3_tokens: u64,
261 m3_prewarmed: bool,
262 pub short_conv_cfg: Option<ShortConvCfg>,
265 pub mtp: Option<MtpModule>,
267 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
271 verify_exact_moe: bool,
274 pub speculative: bool,
276 pub ignore_eos: bool,
282 pub draft_full_streak: u32,
288 pub spec_k_adapt: Option<usize>,
296 pub spec_acc_ewma: f32,
298 rng: SplitMix64,
299 sampler_scratch: SamplerScratch,
300 spec_forced: Option<u32>,
306 spec_q: Vec<Vec<f32>>,
307 spec_p: Vec<f32>,
308 spec_res: Vec<f32>,
309 spec_qs: Vec<sampler::Sparse>,
311 spec_ps: sampler::Sparse,
312 spec_ress: sampler::Sparse,
313 mtp_graph_mode: Option<bool>,
320 #[cfg(target_os = "macos")]
323 metal_verify: Option<MetalVerifyPending>,
324 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
328 ws: ForwardScratch,
332 pool: Option<std::sync::Arc<Pool>>,
334 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
338 pub(crate) dyn_force_f32: bool,
340 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
345 pub(crate) dyn_active: Option<usize>,
351 pub(crate) dyn_blend_loaded: bool,
355 pub(crate) dyn_phi_layer: Option<usize>,
358 dyn_phi_ema: Vec<f32>,
360 dyn_phi_seen: usize,
361 pub dyn_router: Option<crate::swarm::DynRouter>,
364 o1_cfg: Option<crate::nystrom::O1Cfg>,
367 o1_epoch: u64,
370 o1_flags: Vec<bool>,
372 trace: bool,
375 calib_temp: f32,
378 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
380 graph_kv_id: u64,
381 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
384 graph_want_logits: bool,
385 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
389 graph_head_required: bool,
390 graph_logits: Option<Vec<f32>>,
393 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
396 graph_refused: std::sync::atomic::AtomicBool,
402 pub embed_multiplier: f32,
404 pub attn_scale: f32,
407 pub swa: Option<(usize, usize)>,
410 pub swa_trim: Option<(usize, usize)>,
417 pub sliding_layers: Option<Vec<bool>>,
420 pub no_rope_layers: Option<Vec<bool>>,
425 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
431 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
434 pub kv_prefix: KvPrefix,
438 pub last_prefill_tokens: usize,
441 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
444 pub rotary_dim_local: Option<usize>,
445 pub rope_scale: f32,
446 pub rope_scale_local: f32,
447 pub global_attn: Option<(usize, usize)>,
450 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
453 pub attn_v_norm: bool,
455 pub qk_norm_after_rope: bool,
457 pub proj_gate_sigmoid: bool,
460 pub final_softcap: Option<f32>,
462 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
466 pub attn_softcap: f32,
468 confidence_on: bool,
472 #[cfg(test)]
475 nll_test_fail_at: Option<usize>,
476 #[cfg(test)]
479 nll_test_force_serial: bool,
480}
481
482#[cfg(target_os = "macos")]
483impl Drop for Pipeline {
484 fn drop(&mut self) {
485 let _ = crate::gpu_metal::wait_replay();
487 crate::gpu::kv_mirror_drop(self.graph_kv_id);
488 }
489}
490
491#[cfg(not(target_os = "macos"))]
492impl Drop for Pipeline {
493 fn drop(&mut self) {
494 crate::gpu::graph_kv_reset(self.graph_kv_id);
498 }
499}
500
501pub struct PipelineWeights {
506 pub embed_tokens: QTensor,
508 pub layers: Vec<LayerWeights>,
510 pub lm_head: QTensor,
512 pub final_norm: Vec<f32>,
514}
515
516pub struct LayerWeights {
518 pub input_norm: Vec<f32>,
519 pub post_norm: Vec<f32>,
522 pub attn_out_norm: Option<Vec<f32>>,
525 pub layer_scale: Option<f32>,
527 pub ffn_out_norm: Option<Vec<f32>>,
530 pub ffn: FfnKind,
531 pub attn: AttnKind,
532}
533
534#[derive(Clone, Copy, PartialEq, Debug, Default)]
538pub enum Act {
539 #[default]
540 Silu,
541 GeluTanh,
542 Gelu,
544 Situ {
547 beta: f32,
548 linear_beta: f32,
549 },
550 SwiGluOai {
555 alpha: f32,
556 limit: f32,
557 },
558}
559
560impl Act {
561 pub fn from_arch(name: &str) -> Self {
562 if name == "gelu_tanh" {
563 Self::GeluTanh
564 } else if name == "gelu" {
565 Self::Gelu
566 } else {
567 Self::Silu
568 }
569 }
570
571 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
573 match arch.hidden_act.as_str() {
574 "situ" => Self::Situ {
575 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
576 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
577 },
578 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
583 alpha: 1.702,
584 limit: 7.0,
585 },
586 other => Self::from_arch(other),
587 }
588 }
589
590 #[inline]
595 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
596 Self::SwiGluOai { alpha, limit }
597 }
598
599 #[inline]
600 pub fn apply(self, x: f32) -> f32 {
601 match self {
602 Self::Silu => inference::silu(x),
603 Self::GeluTanh => inference::gelu_tanh(x),
604 Self::Gelu => inference::gelu_erf(x),
605 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
606 Self::SwiGluOai { alpha, limit } => {
607 let gate = x.min(limit);
608 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
609 }
610 }
611 }
612
613 #[inline]
616 pub fn combine(self, g: f32, u: f32) -> f32 {
617 match self {
618 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
619 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
620 }
621 Self::SwiGluOai { alpha, limit } => {
622 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
623 }
624 _ => self.apply(g) * u,
625 }
626 }
627
628 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
632 match self {
633 Self::Silu => Some(crate::gpu::GraphAct::Silu),
634 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
635 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
636 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
637 }
638 }
639
640 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
646 self.moe_job_graph_contract().map(|(act, _)| act)
647 }
648
649 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
655 match self {
656 Self::SwiGluOai { alpha, limit }
657 if alpha.to_bits() == 1.702f32.to_bits()
658 && limit.to_bits() == 7.0f32.to_bits() =>
659 {
660 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
661 }
662 _ => self.graph_act().map(|act| (act, 0.0)),
663 }
664 }
665}
666
667pub struct DenseFfn {
669 pub gate_proj: QTensor,
670 pub up_proj: QTensor,
671 pub down_proj: QTensor,
672 pub act: Act,
674 pub down_t: Option<QTensor>,
680 pub segs: Vec<FfnSeg>,
687}
688
689pub struct FfnSeg {
694 pub gate: QTensor,
695 pub up: QTensor,
696 pub down: QTensor,
697 pub start: usize,
698 pub width: usize,
699}
700
701pub enum FfnKind {
704 Dense(DenseFfn),
705 Moe(MoeFfn),
709 DenseMoe(Box<DenseMoeFfn>),
716}
717
718pub struct DenseMoeFfn {
720 pub dense: DenseFfn,
721 pub moe: MoeFfn,
722 pub post_norm_1: Vec<f32>,
724 pub pre_norm_2: Vec<f32>,
727 pub post_norm_2: Vec<f32>,
729}
730
731pub struct MoeFfn {
732 pub router: QTensor,
734 pub experts: Vec<DenseFfn>,
735 pub top_k: usize,
736 pub norm_topk_prob: bool,
737 pub router_sigmoid: bool,
740 pub sigmoid_denom_eps: f32,
746 pub expert_bias: Option<Vec<f32>>,
750 pub routed_scaling: f32,
753 pub route_tau: Option<f32>,
759 pub shared: Option<(DenseFfn, Option<QTensor>)>,
762 pub stats: std::cell::RefCell<Vec<u64>>,
766 pub act_sq: std::cell::RefCell<Vec<f64>>,
773 pub act_rows: std::cell::RefCell<Vec<f32>>,
779 pub mask: Option<Vec<bool>>,
784 pub per_expert_scale: Option<Vec<f32>>,
787 pub router_input_norm: bool,
791 pub resonance: Option<Resonance>,
795 pub grown: Vec<GrownExpert>,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
805pub struct GrownExpert {
806 pub record: String,
808 pub record_index: usize,
810 pub layer: usize,
811 pub expert: usize,
815}
816
817pub struct Resonance {
819 pub mu: Vec<f32>,
821 pub u: Vec<f32>,
823 pub k: usize,
824 pub bias: Vec<f32>,
826 pub shell: Vec<f32>,
832}
833
834static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
837
838pub fn growth_shell_enabled() -> bool {
843 use std::sync::atomic::Ordering;
844 match GROWTH_SHELL.load(Ordering::Relaxed) {
845 1 => true,
846 2 => false,
847 _ => {
848 let off = std::env::var("CMF_GROWTH_SHELL")
849 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
850 .unwrap_or(false);
851 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
852 !off
853 }
854 }
855}
856
857pub fn set_growth_shell(on: Option<bool>) {
862 GROWTH_SHELL.store(
863 match on {
864 Some(true) => 1,
865 Some(false) => 2,
866 None => 0,
867 },
868 std::sync::atomic::Ordering::Relaxed,
869 );
870}
871
872impl Resonance {
873 pub fn has_shell(&self) -> bool {
875 self.shell.iter().any(|s| s.is_finite())
876 }
877
878 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
881 let mut out = vec![f32::INFINITY; ne];
882 if growth_shell_enabled() {
883 for (o, s) in out.iter_mut().zip(&self.shell) {
884 *o = *s;
885 }
886 }
887 out
888 }
889
890 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
895 let h = x.len();
896 let ne = out.len();
897 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
898 for e in 0..ne {
899 let mu = &self.mu[e * h..(e + 1) * h];
900 let mut d2 = 0.0f32;
901 for j in 0..h {
902 let d = x[j] - mu[j];
903 d2 += d * d;
904 }
905 let mut proj = 0.0f32;
906 for i in 0..self.k {
907 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
908 let mut p = 0.0f32;
909 for j in 0..h {
910 p += (x[j] - mu[j]) * u[j];
911 }
912 proj += p * p;
913 }
914 let err = d2 - proj;
915 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
916 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
917 out[e] = f32::NEG_INFINITY;
918 }
919 }
920 }
921}
922
923pub enum AttnKind {
926 Full {
928 wq: QTensor,
929 wk: QTensor,
930 wv: QTensor,
931 wo: QTensor,
932 q_norm: Option<Vec<f32>>,
933 k_norm: Option<Vec<f32>>,
934 output_gate: bool,
935 softplus_gate: Option<(QTensor, bool)>,
939 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
941 },
942 Linear(VmfPhaseWeights),
944 LinearGdn(GdnWeights),
946 ShortConv(ShortConvWeights),
949 Mla(Box<MlaWeights>),
957 Kda(Box<crate::linear_core::KdaWeights>),
961 Bounded(Box<crate::bounded::BoundedWeights>),
966}
967
968pub struct MlaWeights {
970 pub q_proj: QTensor,
974 pub q_a: Option<QTensor>,
977 pub q_a_norm: Option<Vec<f32>>,
978 pub kv_a: QTensor,
980 pub kv_a_norm: Vec<f32>,
982 pub kv_b: QTensor,
984 pub o_proj: QTensor,
986 pub nh: usize,
987 pub qk_rope: usize,
988 pub qk_nope: usize,
989 pub v_dim: usize,
990 pub lora: usize,
991 pub scale: f32,
993 pub nope: bool,
995}
996
997pub struct MtpModule {
1002 pub enorm: Vec<f32>,
1003 pub hnorm: Vec<f32>,
1004 pub eh_proj: QTensor,
1006 pub layer: LayerWeights,
1007 pub final_norm: Vec<f32>,
1008 pub kv: crate::kv_cache::LayerKvCache,
1009}
1010
1011#[derive(Clone, Debug, PartialEq)]
1023pub struct MtpFfnProbe {
1024 pub mass: Vec<f64>,
1025 pub squared_mass: Vec<f64>,
1026 pub observed_positions: usize,
1027}
1028
1029#[cfg(target_os = "macos")]
1036enum MetalRowsItem<'a> {
1037 Gdn {
1038 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1039 first: usize,
1040 },
1041 Attn {
1042 l: crate::gpu_metal::AttnGpuLayer<'a>,
1043 li: usize,
1044 q_norm: Option<&'a [f32]>,
1045 k_norm: Option<&'a [f32]>,
1046 output_gate: bool,
1047 },
1048}
1049
1050#[cfg(target_os = "macos")]
1051struct MetalVerifyPending {
1052 graph: crate::gpu_metal::VerifyGraph,
1053 gdn_layers: Vec<usize>,
1054 attn_layers: Vec<(usize, usize)>,
1055}
1056
1057#[cfg(target_os = "macos")]
1061struct MetalWarmPending {
1062 graph: crate::gpu_metal::VerifyGraph,
1063 cpu_stored: usize,
1064 b: usize,
1065}
1066
1067#[cfg(target_os = "macos")]
1068enum MetalRowsRun {
1069 Declined,
1071 Failed,
1074 Completed(MetalVerifyPending),
1075}
1076
1077#[cfg(target_os = "macos")]
1078enum MetalPrefillOutcome {
1079 Declined,
1080 Failed,
1081 Completed(Vec<f32>),
1082}
1083
1084#[cfg(target_os = "macos")]
1085enum MetalBatchNllOutcome {
1086 Declined,
1087 Failed(String),
1088 Completed(f64, usize),
1089}
1090
1091#[derive(Clone, Copy)]
1097enum SpecTrial {
1098 Spec {
1099 t0: std::time::Instant,
1100 gen0: usize,
1101 rounds: usize,
1102 },
1103 Plain {
1104 t0: std::time::Instant,
1105 gen0: usize,
1106 },
1107 Decided {
1108 spec: bool,
1109 recheck_at: usize,
1110 },
1111}
1112
1113pub(crate) fn spec_time_level() -> u8 {
1117 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1118 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1119 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1120 Err(_) => 0,
1121 })
1122}
1123
1124fn graph_spec_wanted_by_policy(
1132 spec_env: Option<&str>,
1133 spec_default_ok: bool,
1134 penalized: bool,
1135 metal_wgpu: bool,
1136 native_metal: bool,
1137) -> bool {
1138 match spec_env {
1139 Some("0") => false,
1140 Some("1") => true,
1145 Some(_) => !native_metal,
1146 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1149 }
1150}
1151
1152struct SpecStampLog {
1158 t_last: std::time::Instant,
1159 items: Vec<(&'static str, f32)>,
1160}
1161
1162static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1163
1164pub(crate) fn spec_stamp(name: &'static str) {
1165 if spec_time_level() == 0 {
1166 return;
1167 }
1168 if let Ok(mut g) = SPEC_STAMPS.lock() {
1169 if let Some(log) = g.as_mut() {
1170 let now = std::time::Instant::now();
1171 log.items
1172 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1173 log.t_last = now;
1174 }
1175 }
1176}
1177
1178fn spec_stamps_begin() {
1179 if spec_time_level() == 0 {
1180 return;
1181 }
1182 if let Ok(mut g) = SPEC_STAMPS.lock() {
1183 *g = Some(SpecStampLog {
1184 t_last: std::time::Instant::now(),
1185 items: Vec::with_capacity(64),
1186 });
1187 }
1188}
1189
1190fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1191 SPEC_STAMPS
1192 .lock()
1193 .ok()
1194 .and_then(|mut g| g.take())
1195 .map(|l| l.items)
1196 .unwrap_or_default()
1197}
1198
1199fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1202 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1203 for &(n, ms) in items {
1204 match agg.iter_mut().find(|e| e.0 == n) {
1205 Some(e) => {
1206 e.1 += ms;
1207 e.2 += 1;
1208 }
1209 None => agg.push((n, ms, 1)),
1210 }
1211 }
1212 let mut s = String::with_capacity(agg.len() * 16);
1213 for (n, ms, k) in agg {
1214 if k > 1 {
1215 s.push_str(&format!("{n} {ms:.1}/{k} "));
1216 } else {
1217 s.push_str(&format!("{n} {ms:.1} "));
1218 }
1219 }
1220 s
1221}
1222
1223#[derive(Default, Clone, Copy)]
1245struct SpecMon {
1246 round_ms: f64,
1247 tokens: f64,
1248 plain_ms: f64,
1249 n: u32,
1250 fails: u32,
1251 zero_accept_rounds: u8,
1256 metal: bool,
1257}
1258
1259const SPEC_PROXY_TOKENS: f64 = 3.5;
1262const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1265
1266impl SpecMon {
1267 fn round(&mut self, dt_ms: f64, produced: usize) {
1268 if produced <= 1 {
1269 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1270 } else {
1271 self.zero_accept_rounds = 0;
1272 }
1273 self.n += 1;
1274 if self.n == 1 {
1275 return; }
1277 let a = if self.n == 2 { 1.0 } else { 0.3 };
1278 self.round_ms += a * (dt_ms - self.round_ms);
1279 self.tokens += a * (produced as f64 - self.tokens);
1280 }
1281 fn pays(&self) -> bool {
1282 if self.plain_ms > 0.0 {
1283 self.tokens * self.plain_ms > self.round_ms * 1.03
1284 } else {
1285 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1286 }
1287 }
1288 fn repeated_zero_acceptance(&self) -> bool {
1289 self.zero_accept_rounds >= 2
1290 }
1291 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1293 let n = generated.saturating_sub(gen0);
1294 if n >= 8 {
1295 return true;
1296 }
1297 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1298 }
1299}
1300
1301pub const KV_PREFIX_TAIL: usize = 128;
1304
1305#[derive(Debug, Clone, Default)]
1311pub struct KvPrefix {
1312 len: usize,
1313 hash: u64,
1314 tail: Vec<u32>,
1315 device: bool,
1320}
1321
1322impl KvPrefix {
1323 #[inline]
1324 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1325 for &id in ids {
1326 h ^= id as u64;
1327 h = h.wrapping_mul(0x100000001b3);
1328 h ^= h >> 29;
1329 }
1330 h
1331 }
1332
1333 pub fn clear(&mut self) {
1334 self.len = 0;
1335 self.hash = 0xcbf29ce484222325;
1336 self.tail.clear();
1337 self.device = false;
1338 }
1339
1340 pub fn on_device(&self) -> bool {
1342 self.device
1343 }
1344
1345 pub fn set_on_device(&mut self, device: bool) {
1347 self.device = device;
1348 }
1349
1350 pub fn len(&self) -> usize {
1352 self.len
1353 }
1354
1355 pub fn is_empty(&self) -> bool {
1356 self.len == 0
1357 }
1358
1359 pub fn tail_len(&self) -> usize {
1361 self.tail.len()
1362 }
1363
1364 pub fn set(&mut self, ids: &[u32]) {
1366 self.clear();
1367 self.extend(ids);
1368 }
1369
1370 pub fn extend(&mut self, more: &[u32]) {
1372 if self.len == 0 && self.hash == 0 {
1373 self.hash = 0xcbf29ce484222325;
1374 }
1375 self.hash = Self::fold(self.hash, more);
1376 self.len += more.len();
1377 if more.len() >= KV_PREFIX_TAIL {
1378 self.tail.clear();
1379 self.tail
1380 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1381 } else {
1382 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1383 self.tail.drain(..drop);
1384 self.tail.extend_from_slice(more);
1385 }
1386 }
1387
1388 pub fn extension(&self, ids: &[u32]) -> usize {
1392 if self.len == 0 || ids.len() <= self.len {
1393 return 0;
1394 }
1395 let t = self.tail.len();
1396 if ids[self.len - t..self.len] != self.tail[..] {
1397 return 0;
1398 }
1399 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1400 return 0;
1401 }
1402 self.len
1403 }
1404}
1405
1406pub struct GenerateResult {
1408 pub text: String,
1409 pub token_ids: Vec<u32>,
1410 pub prompt_tokens: usize,
1411 pub tokens_generated: usize,
1412 pub finish_reason: String,
1413 pub mtp_drafted: usize,
1415 pub mtp_accepted: usize,
1416 pub token_confidence: Vec<f32>,
1421 pub traces: Vec<TokenTrace>,
1424}
1425
1426#[derive(Clone, Debug)]
1431pub struct TokenTrace {
1432 pub t: usize,
1434 pub token_id: u32,
1436 pub confidence: f32,
1438 pub active_skill: Option<String>,
1440 pub recon: Option<f32>,
1444 pub switched: bool,
1447}
1448
1449#[cfg_attr(not(test), allow(dead_code))]
1454fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1455 let t = if temp > 1e-3 { temp } else { 1.0 };
1456 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1457 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1458 if sum > 0.0 {
1459 (((logits[id as usize] - max) / t).exp()) / sum
1460 } else {
1461 0.0
1462 }
1463}
1464
1465fn prefill_batched() -> bool {
1468 std::env::var("CMF_PREFILL")
1469 .map(|v| v != "seq")
1470 .unwrap_or(true)
1471}
1472
1473#[inline]
1477fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1478 let graph_quality = unmasked && prefer_graph;
1479 let fused_head_quality = graph_quality && native_metal;
1480 (graph_quality, fused_head_quality)
1481}
1482
1483#[derive(Clone, Copy)]
1487enum PrefillIn<'a> {
1488 Ids(&'a [u32]),
1489 Hidden(&'a [f32]),
1490}
1491
1492impl Pipeline {
1499 pub(crate) fn can_prefill_batched(&self) -> bool {
1500 #[cfg(test)]
1501 let force_serial = self.nll_test_force_serial;
1502 #[cfg(not(test))]
1503 let force_serial = false;
1504 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1505 }
1506
1507 fn automatic_gpu_prefix(&self) -> Option<usize> {
1510 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1511 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1512 }
1513
1514 pub fn prefill_chunk(&self) -> usize {
1518 let env = env_prefill_chunk();
1519 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1520 return prefill_chunk_rule(env, ChunkHost::here(), false);
1521 }
1522 prefill_chunk_rule(
1523 None,
1524 ChunkHost::Other,
1525 self.chunk_stack_facts().dense_on_discrete(),
1526 )
1527 }
1528
1529 fn per_token_stack(&self) -> bool {
1533 self.g3n.is_some() || self.g4e.is_some()
1534 }
1535
1536 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1537 let plain_dense = !self.weights.layers.is_empty()
1538 && !self.per_token_stack()
1539 && self.dsv4.is_none()
1540 && self.dsv41.is_none()
1541 && self.qwen4_exp.is_none()
1542 && self.weights.layers.iter().all(|lw| {
1543 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1544 });
1545 let gpu_on = crate::gpu::enabled();
1546 ChunkStackFacts {
1547 plain_dense,
1548 discrete: gpu_on && crate::gpu::discrete(),
1549 gpu_on,
1550 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1553 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1554 multi_gpu: self.gpu_plan.is_some(),
1555 o1: self.o1_active(),
1556 }
1557 }
1558}
1559
1560pub fn prefill_chunk() -> usize {
1569 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1570}
1571
1572fn env_prefill_chunk() -> Option<usize> {
1573 std::env::var("CMF_PREFILL_CHUNK")
1574 .ok()
1575 .and_then(|v| v.parse::<usize>().ok())
1576}
1577
1578#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1580enum ChunkHost {
1581 Macos,
1582 Aarch64,
1584 Other,
1586}
1587
1588impl ChunkHost {
1589 fn here() -> Self {
1590 if cfg!(target_os = "macos") {
1591 ChunkHost::Macos
1592 } else if cfg!(target_arch = "aarch64") {
1593 ChunkHost::Aarch64
1594 } else {
1595 ChunkHost::Other
1596 }
1597 }
1598}
1599
1600const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1607
1608fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1614 if let Some(n) = env {
1615 return n.max(1);
1616 }
1617 match host {
1618 ChunkHost::Macos => 512,
1619 ChunkHost::Aarch64 => 256,
1622 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1623 ChunkHost::Other => 48,
1624 }
1625}
1626
1627#[derive(Clone, Copy, Debug, Default)]
1629struct ChunkStackFacts {
1630 plain_dense: bool,
1633 discrete: bool,
1635 gpu_on: bool,
1637 capacity_split: bool,
1639 multi_gpu: bool,
1641 o1: bool,
1643}
1644
1645impl ChunkStackFacts {
1646 fn dense_on_discrete(self) -> bool {
1647 self.plain_dense
1648 && self.discrete
1649 && self.gpu_on
1650 && !self.capacity_split
1651 && !self.multi_gpu
1652 && !self.o1
1653 }
1654}
1655
1656#[inline]
1662fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1663 if end <= start || start >= input_len {
1664 return 0;
1665 }
1666 let rows = (end.min(input_len) - start).min(input_len - start);
1667 if end < input_len {
1668 rows
1669 } else {
1670 rows.saturating_sub(1)
1671 }
1672}
1673
1674pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1676
1677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1679pub(crate) struct ReuseLayer {
1680 pub full: bool,
1683 pub host_rows: usize,
1686 pub device_rows: Option<usize>,
1688 pub device_state: bool,
1690}
1691
1692#[derive(Debug, Clone, PartialEq, Eq)]
1694pub(crate) enum ReusePlan {
1695 Ready,
1697 Pull(Vec<(usize, usize, usize)>),
1700 Fresh,
1702}
1703
1704pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1714 let mut pulls = Vec::new();
1715 for (li, l) in layers.iter().enumerate() {
1716 if !l.full {
1717 if l.device_state {
1718 return ReusePlan::Fresh;
1719 }
1720 continue;
1721 }
1722 if l.host_rows == reuse_from {
1723 continue;
1724 }
1725 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1726 pulls.push((li, l.host_rows, reuse_from));
1727 continue;
1728 }
1729 return ReusePlan::Fresh;
1730 }
1731 if pulls.is_empty() {
1732 ReusePlan::Ready
1733 } else {
1734 ReusePlan::Pull(pulls)
1735 }
1736}
1737
1738impl Pipeline {
1739 fn reset_fresh_mtp_draft_state(&mut self) {
1742 self.draft_full_streak = 0;
1743 }
1744
1745 fn clear_sequence_state(&mut self) {
1753 #[cfg(target_os = "macos")]
1756 let _ = crate::gpu_metal::wait_replay();
1757 self.kv_cache.clear();
1758 self.clear_history();
1761 self.graph_logits = None;
1762 if let Some(b) = &mut self.dsv41 {
1763 b.3.clear();
1764 }
1765 crate::gpu::graph_kv_reset(self.graph_kv_id);
1766 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1771 }
1772
1773 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1780 if self.graph_prefill_preferred() {
1781 return true;
1782 }
1783 let kv_id = self.graph_kv_id;
1784 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1785 .map(|li| {
1786 let full = matches!(
1787 self.weights.layers[self.phys_layer(li)].attn,
1788 AttnKind::Full { .. }
1789 );
1790 ReuseLayer {
1791 full,
1792 host_rows: self.kv_cache.layers[li].pos_len(),
1795 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1796 device_state: crate::gpu::graph_state_resident(kv_id, li),
1797 }
1798 })
1799 .collect();
1800 if layers
1804 .iter()
1805 .all(|l| l.device_rows.is_none() && !l.device_state)
1806 {
1807 return true;
1808 }
1809 let plan = kv_reuse_plan(reuse_from, &layers);
1810 let (what, rows, n) = match &plan {
1811 ReusePlan::Ready => ("host ready", 0, 0),
1812 ReusePlan::Fresh => ("fresh", 0, 0),
1813 ReusePlan::Pull(p) => (
1814 "pull",
1815 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1816 p.len(),
1817 ),
1818 };
1819 let t0 = std::time::Instant::now();
1820 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1821 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1822 eprintln!(
1823 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1824 if ok { "" } else { " (failed → fresh)" },
1825 t0.elapsed().as_secs_f64() * 1e3
1826 );
1827 }
1828 ok
1829 }
1830
1831 fn apply_kv_reuse_plan(
1832 &mut self,
1833 reuse_from: usize,
1834 plan: ReusePlan,
1835 layers: &[ReuseLayer],
1836 ) -> bool {
1837 let kv_id = self.graph_kv_id;
1838 match plan {
1839 ReusePlan::Fresh => return false,
1840 ReusePlan::Ready => {}
1841 ReusePlan::Pull(pulls) => {
1842 let (nkv, hd) = {
1847 let c = &self.kv_cache.layers[pulls[0].0];
1848 (c.num_kv_heads, c.head_dim)
1849 };
1850 let uniform = pulls.iter().all(|&(li, _, _)| {
1851 let c = &self.kv_cache.layers[li];
1852 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1853 });
1854 let batched = if uniform {
1855 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1856 } else {
1857 None
1858 };
1859 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1860 Some(rows) => rows,
1861 None => {
1862 let mut rows = Vec::with_capacity(pulls.len());
1863 for &(li, from, to) in &pulls {
1864 let (lnkv, lhd) = {
1865 let c = &self.kv_cache.layers[li];
1866 (c.num_kv_heads, c.head_dim)
1867 };
1868 let Some((k, v, first_valid)) =
1869 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1870 else {
1871 return false;
1872 };
1873 let need_from = match self.layer_window(li) {
1877 Some(w) => from.max((to + 1).saturating_sub(w)),
1878 None => from,
1879 };
1880 if first_valid > need_from {
1881 return false;
1882 }
1883 rows.push((k, v));
1884 }
1885 rows
1886 }
1887 };
1888 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1889 let cache = &mut self.kv_cache.layers[li];
1890 let row = cache.num_kv_heads * cache.head_dim;
1891 for p in 0..to - from {
1892 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1893 }
1894 if cache.pos_len() != to {
1895 return false;
1896 }
1897 }
1898 }
1899 }
1900 for (li, l) in layers.iter().enumerate() {
1904 if l.full
1905 && l.device_rows.is_some_and(|d| d > reuse_from)
1906 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1907 {
1908 return false;
1909 }
1910 }
1911 true
1912 }
1913
1914 fn finish_generation(
1920 &mut self,
1921 mtp: &mut Option<MtpModule>,
1922 router: &mut Option<crate::swarm::DynRouter>,
1923 clear_sequence: bool,
1924 ) {
1925 if router.is_some() {
1929 let _ = self.set_active_skill(None);
1930 }
1931 #[cfg(target_os = "macos")]
1938 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1939 if clear_sequence {
1940 self.clear_sequence_state();
1941 if let Some(m) = mtp.as_mut() {
1942 m.kv.clear();
1948 }
1949 if let Some(m) = self.mtp.as_mut() {
1950 m.kv.clear();
1954 }
1955 }
1956 self.graph_want_logits = false;
1957 self.graph_head_required = false;
1958 self.graph_logits = None;
1959 self.graph_failed
1960 .store(false, std::sync::atomic::Ordering::Relaxed);
1961 self.cancel
1962 .store(false, std::sync::atomic::Ordering::Relaxed);
1963 self.dyn_router = router.take().or(self.dyn_router.take());
1964 self.mtp = mtp.take().or(self.mtp.take());
1965 self.mtp_graph_mode = None;
1966 self.spec_forced = None;
1967 }
1968
1969 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1973 if self
1974 .graph_failed
1975 .swap(false, std::sync::atomic::Ordering::Relaxed)
1976 {
1977 self.cancel
1978 .store(false, std::sync::atomic::Ordering::Relaxed);
1979 self.clear_sequence_state();
1980 self.graph_logits = None;
1981 self.graph_want_logits = false;
1982 self.graph_head_required = false;
1983 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1984 }
1985 Ok(())
1986 }
1987
1988 #[cfg(target_os = "macos")]
1989 fn fail_metal_graph(&mut self, reason: &str) {
1990 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1991 self.clear_sequence_state();
1992 self.graph_logits = None;
1993 self.graph_failed
1994 .store(true, std::sync::atomic::Ordering::Relaxed);
1995 self.cancel
1996 .store(true, std::sync::atomic::Ordering::Relaxed);
1997 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1998 }
1999
2000 fn nll_begin(&mut self) -> Result<(), String> {
2005 if self
2006 .graph_failed
2007 .swap(false, std::sync::atomic::Ordering::Relaxed)
2008 {
2009 self.cancel
2010 .store(false, std::sync::atomic::Ordering::Relaxed);
2011 self.clear_sequence_state();
2012 self.graph_logits = None;
2013 self.graph_want_logits = false;
2014 self.graph_head_required = false;
2015 return Err("GPU graph failed before NLL scoring".to_string());
2016 }
2017 self.clear_sequence_state();
2018 self.graph_logits = None;
2019 self.graph_want_logits = false;
2020 self.graph_head_required = false;
2021 Ok(())
2022 }
2023
2024 fn nll_end(&mut self) {
2028 self.clear_sequence_state();
2029 self.graph_logits = None;
2030 self.graph_want_logits = false;
2031 self.graph_head_required = false;
2032 self.graph_failed
2033 .store(false, std::sync::atomic::Ordering::Relaxed);
2034 }
2035
2036 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2039 #[cfg(test)]
2040 if self.nll_test_fail_at == Some(pos) {
2041 self.nll_test_fail_at = None;
2042 self.graph_failed
2043 .store(true, std::sync::atomic::Ordering::Relaxed);
2044 self.cancel
2045 .store(true, std::sync::atomic::Ordering::Relaxed);
2046 }
2047 if self
2048 .graph_failed
2049 .swap(false, std::sync::atomic::Ordering::Relaxed)
2050 {
2051 self.cancel
2052 .store(false, std::sync::atomic::Ordering::Relaxed);
2053 self.clear_sequence_state();
2054 self.graph_logits = None;
2055 self.graph_want_logits = false;
2056 return Err(format!(
2057 "GPU graph failed during NLL {phase} at position {pos}"
2058 ));
2059 }
2060 Ok(())
2061 }
2062
2063 #[inline]
2067 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2068 virtual_idx % self.physical_layers
2069 }
2070
2071 #[inline]
2074 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2075 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2076 }
2077
2078 #[allow(clippy::too_many_arguments)]
2080
2081 #[cfg(target_os = "macos")]
2100 fn graph_prefill_preferred(&self) -> bool {
2101 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2102 if !crate::gpu::enabled_here()
2103 || !graph_force
2104 || std::env::var("CMF_GPU_BLOCK")
2105 .map(|v| v == "0")
2106 .unwrap_or(false)
2107 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2110 {
2111 return false;
2112 }
2113 self.weights
2114 .layers
2115 .iter()
2116 .any(|lw| {
2117 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2118 })
2119 }
2120
2121 #[cfg(not(target_os = "macos"))]
2128 fn batch_prefix_prefill(&self) -> bool {
2129 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2130 Ok("0") => return false,
2131 Ok("1") => true,
2132 _ => false,
2133 };
2134 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2135 && crate::gpu::enabled_here()
2136 && !self.graph_refused()
2137 && (forced || self.graph_attn_decline_reason().is_some())
2138 && self.wgpu_graph_attn_decline().is_none()
2139 && self.attn_softcap == 0.0
2140 && self
2141 .weights
2142 .layers
2143 .iter()
2144 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2145 && self.automatic_gpu_prefix().is_some()
2146 }
2147
2148 fn g4_batch_prefill(&self) -> bool {
2154 #[cfg(target_os = "macos")]
2155 {
2156 false
2157 }
2158 #[cfg(not(target_os = "macos"))]
2159 {
2160 !self.weights.layers.is_empty()
2164 && !self.per_token_stack()
2165 && (self.global_attn.is_some() || self.attn_v_norm)
2166 && crate::gpu::g4_batch_on()
2167 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2168 && crate::gpu::enabled_here()
2169 && crate::gpu::discrete()
2170 && !self.graph_refused()
2171 && !self.o1_active()
2172 && self.attn_softcap == 0.0
2173 && self.wgpu_graph_attn_decline().is_none()
2174 && self.weights.layers.iter().all(|lw| {
2175 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2176 })
2177 && self.automatic_gpu_prefix().is_none()
2178 }
2179 }
2180
2181 #[cfg(not(target_os = "macos"))]
2188 fn g4_batch_ingest(
2189 &mut self,
2190 ids: &[u32],
2191 pos0: usize,
2192 keep_all: bool,
2193 ) -> Result<(usize, Vec<f32>), String> {
2194 let hs = self.hidden_size;
2195 let chunk = self.generation_batch_k().max(1);
2196 let end_all = pos0 + ids.len();
2197 let mut pos = pos0;
2198 let mut out = Vec::new();
2199 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2200 let end = (pos + chunk).min(end_all);
2201 let bk = end - pos;
2202 let mut hiddens = vec![0f32; bk * hs];
2203 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2204 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2205 }
2206 let positions: Vec<usize> = (pos..end).collect();
2207 let t_chunk = std::time::Instant::now();
2208 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2209 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2210 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2211 eprintln!(
2212 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2213 bk as f64 / (ms / 1000.0)
2214 );
2215 }
2216 match outcome {
2217 crate::gpu::BatchGraphOutcome::Completed => {
2218 if keep_all {
2219 out.extend_from_slice(&hiddens);
2220 } else {
2221 out = hiddens[(bk - 1) * hs..].to_vec();
2222 }
2223 pos = end;
2224 }
2225 crate::gpu::BatchGraphOutcome::Failed => {
2226 return Err("Gemma-4 batched prefill failed after admission".into());
2227 }
2228 crate::gpu::BatchGraphOutcome::Declined => {
2229 #[cfg(feature = "gpu")]
2230 if pos > pos0 {
2231 self.pull_lagging_host_kv(0, self.num_layers, pos);
2232 }
2233 break;
2234 }
2235 }
2236 }
2237 Ok((pos - pos0, out))
2238 }
2239
2240 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2245 #[cfg(not(target_os = "macos"))]
2246 {
2247 if ids.is_empty() || !self.g4_batch_prefill() {
2248 return None;
2249 }
2250 self.clear_sequence_state();
2251 match self.g4_batch_ingest(ids, 0, true) {
2252 Ok((n, rows)) if n == ids.len() => Some(rows),
2253 _ => {
2254 self.clear_sequence_state();
2255 None
2256 }
2257 }
2258 }
2259 #[cfg(target_os = "macos")]
2260 {
2261 let _ = ids;
2262 None
2263 }
2264 }
2265
2266 #[cfg(not(target_os = "macos"))]
2267 fn graph_prefill_preferred(&self) -> bool {
2268 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2276 if !graph_on || !crate::gpu::enabled_here() {
2277 return false;
2278 }
2279 if self.embryo_resident_eligible() {
2283 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2288 }
2289 if self.o1_active() {
2303 return false;
2304 }
2305 if self.wgpu_graph_attn_decline().is_some() {
2309 return false;
2310 }
2311 if self
2312 .weights
2313 .layers
2314 .iter()
2315 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2316 {
2317 return true;
2318 }
2319 self.weights
2330 .layers
2331 .iter()
2332 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2333 && self.automatic_gpu_prefix().is_none()
2334 }
2335
2336 #[cfg(target_os = "macos")]
2337 fn q1_graph_gpu(
2338 &mut self,
2339 start: usize,
2340 upto: Option<usize>,
2341 position: usize,
2342 h: &mut [f32],
2343 ) -> usize {
2344 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2346 const MOE_WORKING_SET_REASON: &str =
2347 "model weights exceed Metal working set for native MoE graph";
2348 if self
2352 .graph_declines
2353 .borrow()
2354 .iter()
2355 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2356 {
2357 return start;
2358 }
2359 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2360 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2362 || !crate::gpu::enabled_here()
2363 || !graph_force
2364 || std::env::var("CMF_GPU_BLOCK")
2365 .map(|v| v == "0")
2366 .unwrap_or(false)
2367 {
2368 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2369 eprintln!(
2370 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2371 self.attn_softcap > 0.0,
2372 crate::gpu::enabled_here(),
2373 graph_force,
2374 );
2375 }
2376 if self.graph_head_required {
2377 self.fail_metal_graph("native graph front gate refused");
2378 }
2379 return start;
2380 }
2381 let gemma4 = self.metal_gemma4();
2391 let swa_graph = self.metal_graph_swa() || gemma4;
2392 let graph_attn_reason = self.graph_attn_decline_reason();
2399 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2400 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2401 if (self.swa.is_some() && !swa_graph)
2402 || (self.global_attn.is_some() && !gemma4)
2403 || self.attention_heads_per_layer.is_some()
2404 || (self.attn_v_norm && !gemma4)
2405 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2408 || self.weights.layers.iter().any(|lw| {
2409 ((lw.attn_out_norm.is_some()
2410 || lw.ffn_out_norm.is_some()
2411 || lw.layer_scale.is_some())
2412 && !gemma4)
2413 || matches!(&lw.ffn, FfnKind::Dense(d)
2414 if !(matches!(d.act, Act::Silu | Act::Gelu)
2415 || (gemma4 && d.act == Act::GeluTanh)))
2416 })
2417 {
2418 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2421 self.note_graph_decline("metal block graph", reason);
2422 }
2423 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2424 eprintln!(
2425 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2426 self.swa.is_some(),
2427 self.global_attn.is_some(),
2428 self.attention_heads_per_layer.is_some(),
2429 self.attn_v_norm,
2430 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2431 );
2432 }
2433 if self.graph_head_required {
2434 self.fail_metal_graph("native graph architecture gate refused");
2435 }
2436 return start;
2437 }
2438 let limit = upto
2441 .map(|u| u + 1)
2442 .unwrap_or(self.num_layers)
2443 .min(self.num_layers);
2444
2445 enum Item<'a> {
2446 Gdn {
2447 run: Vec<GdnGpuLayer<'a>>,
2448 first: usize,
2449 },
2450 Attn {
2451 l: AttnGpuLayer<'a>,
2452 li: usize,
2453 q_norm: Option<&'a [f32]>,
2454 k_norm: Option<&'a [f32]>,
2455 output_gate: bool,
2456 proj_gate: Option<(&'a QTensor, bool)>,
2460 head_gate_w: Option<&'a [f32]>,
2463 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2464 full_gpu: bool,
2467 },
2468 }
2469
2470 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2477 let attend_contract = attend_mode != "0"
2478 && attend_mode != "off"
2479 && self.head_dim % 4 == 0
2480 && self.head_dim <= 256
2481 && (self.rotary_dim == 0
2485 || (self.rotary_dim >= 2
2486 && self.rotary_dim <= self.head_dim
2487 && (self.rotary_dim / 2) % 32 == 0))
2488 && self.num_kv_heads > 0
2489 && self.num_heads % self.num_kv_heads == 0;
2490
2491 let mut plan: Vec<Item> = Vec::new();
2492 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2493 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2495 let mut scan = start;
2496 while scan < limit {
2497 let lw = &self.weights.layers[self.phys_layer(scan)];
2498 let ffn = match &lw.ffn {
2499 FfnKind::Dense(d) if d.segs.is_empty() => {
2500 let (Some(g), Some(u), Some(dn)) = (
2501 d.gate_proj.metal_graph_parts(),
2502 d.up_proj.metal_graph_parts(),
2503 d.down_proj.metal_graph_parts(),
2504 ) else {
2505 if block_diag {
2506 eprintln!(
2507 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2508 );
2509 }
2510 break;
2511 };
2512 MetalFfn::Dense {
2513 gate: g,
2514 up: u,
2515 down: dn,
2516 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2519 }
2520 }
2521 FfnKind::Moe(m) => {
2522 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2528 {
2529 MetalFfn::FlintMoe(flint)
2530 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2531 MetalFfn::Moe(moe)
2532 } else {
2533 if block_diag {
2534 eprintln!(
2535 "block-graph: L{scan} MoE outside the graph contract — run ends"
2536 );
2537 }
2538 break;
2539 };
2540 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2541 model_ref.get_or_insert_with(|| model.clone());
2542 }
2543 ffn
2544 }
2545 _ => {
2546 if block_diag {
2547 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2548 }
2549 break;
2550 }
2551 };
2552 match &lw.attn {
2553 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2554 let parts = (
2555 w.in_proj_qkv.metal_graph_parts(),
2556 w.in_proj_z.metal_graph_parts(),
2557 w.in_proj_a.f32_parts(),
2558 w.in_proj_b.f32_parts(),
2559 w.out_proj.metal_graph_parts(),
2560 );
2561 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2562 if block_diag {
2563 eprintln!(
2564 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2565 w.in_proj_qkv.metal_graph_parts().is_some(),
2566 w.in_proj_z.metal_graph_parts().is_some(),
2567 w.in_proj_a.f32_parts().is_some(),
2568 w.in_proj_b.f32_parts().is_some(),
2569 w.out_proj.metal_graph_parts().is_some(),
2570 );
2571 }
2572 break;
2573 };
2574 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2575 model_ref.get_or_insert_with(|| model.clone());
2576 }
2577 let gl = GdnGpuLayer {
2578 attn_norm: &lw.input_norm,
2579 post_norm: &lw.post_norm,
2580 qkv,
2581 z,
2582 a,
2583 b,
2584 out,
2585 ffn,
2586 conv1d: &w.conv1d,
2587 a_log: &w.a_log,
2588 dt_bias: &w.dt_bias,
2589 gnorm: &w.norm,
2590 };
2591 match plan.last_mut() {
2592 Some(Item::Gdn { run, .. }) => run.push(gl),
2593 _ => plan.push(Item::Gdn {
2594 run: vec![gl],
2595 first: scan,
2596 }),
2597 }
2598 }
2599 AttnKind::Full {
2600 wq,
2601 wk,
2602 wv,
2603 wo,
2604 q_norm,
2605 k_norm,
2606 output_gate,
2607 softplus_gate,
2608 bias,
2609 } if (!self.kv_cache.layers[scan].o1_sealed()
2610 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2615 && softplus_gate
2619 .as_ref()
2620 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2621 {
2622 let parts = (
2623 wq.metal_graph_parts(),
2624 wk.metal_graph_parts(),
2625 wv.metal_graph_parts(),
2626 wo.metal_graph_parts(),
2627 );
2628 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2629 break;
2630 };
2631 if let QTensor::Mapped { model, .. } = wq {
2632 model_ref.get_or_insert_with(|| model.clone());
2633 }
2634 let cache = &self.kv_cache.layers[scan];
2635 let o1_metal = cache.o1.is_some()
2639 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2640 && cache.o1_views().is_some();
2641 let head_gate_w = softplus_gate
2645 .as_ref()
2646 .and_then(|(g, _)| g.f32_parts())
2647 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2648 .map(|(d, _, _)| d);
2649 let (nkv_g, hd_g) = if gemma4 {
2652 let (a, b, _) = self.layer_geom(scan);
2653 (a, b)
2654 } else {
2655 (self.num_kv_heads, self.head_dim)
2656 };
2657 let full_gpu = attend_contract
2658 && softplus_gate.is_none() == head_gate_w.is_none()
2659 && cache.mode == crate::kv_cache::KvMode::F32
2660 && (cache.o1.is_none() || o1_metal)
2661 && bias.is_none()
2662 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2663 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2664 && pk.1 == nkv_g * hd_g
2665 && pv.1 == nkv_g * hd_g
2666 && po.2 == self.num_heads * hd_g;
2667 plan.push(Item::Attn {
2668 l: AttnGpuLayer {
2669 attn_norm: &lw.input_norm,
2670 post_norm: &lw.post_norm,
2671 wq: pq,
2672 wk: pk,
2673 wv: pv,
2674 wo: po,
2675 ffn,
2676 attn_out_norm: lw.attn_out_norm.as_deref(),
2677 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2678 layer_scale: lw.layer_scale,
2679 },
2680 li: scan,
2681 q_norm: q_norm.as_deref(),
2682 k_norm: k_norm.as_deref(),
2683 output_gate: *output_gate,
2684 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2685 head_gate_w,
2686 bias: bias
2687 .as_ref()
2688 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2689 full_gpu,
2690 });
2691 }
2692 _ => break,
2693 }
2694 scan += 1;
2695 }
2696 let Some(model) = model_ref else {
2697 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2698 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2699 }
2700 if self.graph_head_required {
2701 self.fail_metal_graph("native graph has no mapped model reference");
2702 }
2703 return start;
2704 };
2705 if plan.is_empty() {
2706 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2707 eprintln!("q1-graph: empty plan at layer {start}");
2708 }
2709 if self.graph_head_required {
2710 self.fail_metal_graph("native graph plan is empty");
2711 }
2712 return start;
2713 }
2714 let has_moe = plan.iter().any(|it| match it {
2715 Item::Gdn { run, .. } => run
2716 .iter()
2717 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2718 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2719 });
2720 let has_flint_moe = plan.iter().any(|it| match it {
2721 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2722 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2723 });
2724 let metal_working_set = crate::gpu_metal::working_set_bytes();
2725 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2726 let model_bytes = model.primary_bytes().len() as u64;
2727 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2733 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2734 let pager_cap = if !has_moe {
2743 None
2744 } else if has_flint_moe {
2745 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2749 } else {
2750 crate::gpu_metal::moe_pager_cap_bytes()
2751 };
2752 let pager_eligible =
2753 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2754 let mut pager_span: Option<(usize, usize)> = None;
2755 if pager_eligible {
2756 let cap = pager_cap.expect("checked above");
2757 let tail_span = (self.graph_want_logits
2761 && upto.is_none()
2762 && scan == self.num_layers
2763 && std::env::var("CMF_GPU_LMHEAD")
2764 .map(|v| v != "0")
2765 .unwrap_or(true))
2766 .then(|| self.weights.lm_head.metal_graph_parts())
2767 .flatten()
2768 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2769 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2770 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2771 Some(match span {
2772 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2773 None => (lo, hi),
2774 })
2775 };
2776 let page_span = |span: (usize, usize), include_tail: bool| {
2777 let (mut lo, mut hi) = span;
2778 if include_tail {
2779 if let Some((tail_lo, tail_hi)) = tail_span {
2780 lo = lo.min(tail_lo);
2781 hi = hi.max(tail_hi);
2782 }
2783 }
2784 crate::gpu_metal::page_round_span(lo, hi)
2785 };
2786 let mut selected = 0usize;
2787 let mut selected_span: Option<(usize, usize)> = None;
2788 let mut gdn_prefix: Option<(usize, usize)> = None;
2796 'items: for (i, item) in plan.iter().enumerate() {
2797 match item {
2798 Item::Attn { li, .. } => {
2799 let Some(span) = add_layer_span(selected_span, *li) else {
2800 break;
2801 };
2802 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2803 break;
2804 };
2805 if page_hi - page_lo > cap {
2806 break;
2807 }
2808 selected = i + 1;
2809 selected_span = Some((page_lo, page_hi));
2810 }
2811 Item::Gdn { first, run } => {
2812 let mut whole = None;
2819 for take in 1..=run.len() {
2820 whole = add_layer_span(whole, *first + take - 1);
2821 if whole.is_none() {
2822 break;
2823 }
2824 }
2825 let whole_fits_empty_page = whole
2826 .and_then(|span| page_span(span, false))
2827 .is_some_and(|(lo, hi)| hi - lo <= cap);
2828 if whole_fits_empty_page {
2829 let mut span = selected_span;
2830 for take in 1..=run.len() {
2831 let Some(next) = add_layer_span(span, *first + take - 1) else {
2832 break 'items;
2833 };
2834 span = Some(next);
2835 }
2836 let Some(span) = span else {
2837 break;
2838 };
2839 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2840 else {
2841 break;
2842 };
2843 if page_hi - page_lo > cap {
2844 break;
2845 }
2846 selected = i + 1;
2847 selected_span = Some((page_lo, page_hi));
2848 continue;
2849 }
2850 if selected != 0 {
2851 break;
2852 }
2853 let mut span = None;
2854 let mut best: Option<(usize, (usize, usize))> = None;
2855 for take in 1..=run.len() {
2856 let Some(next) = add_layer_span(span, *first + take - 1) else {
2857 break;
2858 };
2859 let Some((page_lo, page_hi)) = page_span(next, false) else {
2862 break;
2863 };
2864 if page_hi - page_lo > cap {
2865 break;
2866 }
2867 span = Some((page_lo, page_hi));
2868 best = Some((take, (page_lo, page_hi)));
2869 }
2870 let Some((take, page)) = best else {
2871 break;
2872 };
2873 selected = i + 1;
2874 selected_span = Some(page);
2875 if take != run.len() {
2876 gdn_prefix = Some((i, take));
2877 break 'items;
2878 }
2879 }
2880 }
2881 }
2882 if selected != 0 {
2883 if let Some((item, take)) = gdn_prefix {
2884 match &mut plan[item] {
2885 Item::Gdn { run, .. } => run.truncate(take),
2886 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2887 }
2888 }
2889 let (page_lo, page_hi) = selected_span.expect("selected page span");
2890 tracing::info!(
2891 page_start = page_lo,
2892 page_end = page_hi,
2893 page_mb = (page_hi - page_lo) / (1024 * 1024),
2894 cap_mb = cap / (1024 * 1024),
2895 graph_items = selected,
2896 "native Metal MoE graph uses transient bounded weight page"
2897 );
2898 plan.truncate(selected);
2899 pager_span = Some((page_lo, page_hi));
2900 } else {
2901 tracing::warn!(
2902 cap_mb = cap / (1024 * 1024),
2903 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2904 );
2905 }
2906 }
2907 if has_moe && needs_bounded_pager && pager_span.is_none() {
2916 tracing::info!(
2917 model_bytes,
2918 metal_working_set,
2919 "native Metal MoE graph declined before allocation"
2920 );
2921 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2922 if self.graph_head_required {
2923 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2924 }
2925 return start;
2926 }
2927 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2928 let dev_attend = attend_contract
2929 && (self.head_dim <= 128
2930 || has_moe
2931 || (self.head_dim <= 256 && has_gdn)
2937 || (self.head_dim <= 256 && swa_graph)
2941 || attend_mode == "force"
2942 || attend_mode == "256");
2943 if !dev_attend {
2944 for it in &mut plan {
2945 if let Item::Attn { li, full_gpu, .. } = it {
2946 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2949 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2950 if !keep_o1 {
2951 *full_gpu = false;
2952 }
2953 }
2954 }
2955 }
2956 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2957 use std::sync::atomic::{AtomicBool, Ordering};
2958 static SAID: AtomicBool = AtomicBool::new(false);
2959 if !SAID.swap(true, Ordering::Relaxed) {
2960 let fg = plan
2961 .iter()
2962 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2963 .count();
2964 let att = plan
2965 .iter()
2966 .filter(|it| matches!(it, Item::Attn { .. }))
2967 .count();
2968 eprintln!(
2969 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2970 plan.len(),
2971 self.head_dim,
2972 self.rotary_dim,
2973 self.num_kv_heads,
2974 self.num_heads,
2975 );
2976 }
2977 }
2978 let dims = GraphDims {
2979 hidden: self.hidden_size,
2980 eps: self.rms_eps as f32,
2981 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2982 };
2983 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2987 crate::gpu_metal::MoePagerPageScope::new(
2988 _mt0,
2989 position,
2990 start,
2991 page_lo,
2992 page_hi,
2993 pager_cap.unwrap_or(0),
2997 )
2998 });
2999 let graph_new = match pager_span {
3000 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3001 None => TokenGraph::new(&model, dims, h),
3002 };
3003 let Some(mut graph) = graph_new else {
3004 if self.graph_head_required {
3005 self.fail_metal_graph("native TokenGraph allocation refused");
3006 }
3007 return start;
3008 };
3009 if swa_graph && self.head_dim > 128 {
3010 graph.set_attend_blk_from(64);
3016 }
3017 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3018 nv: cfg.num_v_heads,
3019 nk: cfg.num_k_heads,
3020 dk: cfg.key_head_dim,
3021 dv: cfg.value_head_dim,
3022 kk: cfg.conv_kernel,
3023 hidden: self.hidden_size,
3024 inter: self.intermediate_size,
3025 c_dim: cfg.conv_dim(),
3026 eps: cfg.rms_eps as f32,
3027 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3028 });
3029 let mut valid = 0usize;
3033 let mut end = start;
3034 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3036 static ONCE: std::sync::Once = std::sync::Once::new();
3037 ONCE.call_once(|| {
3038 for it in &plan {
3039 match it {
3040 Item::Gdn { first, run } => {
3041 eprintln!("plan: Gdn first={first} len={}", run.len())
3042 }
3043 Item::Attn { li, full_gpu, .. } => {
3044 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3045 }
3046 }
3047 }
3048 });
3049 }
3050 for item in &plan {
3051 let ok = match item {
3052 Item::Gdn { run, .. } => gcfg
3053 .as_ref()
3054 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3055 .unwrap_or(false),
3056 Item::Attn { l, .. } => graph.attn_ok(l),
3057 };
3058 if !ok {
3059 if block_diag {
3060 eprintln!(
3061 "block-graph: plan item {} ({}) failed graph preflight",
3062 valid,
3063 match item {
3064 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3065 Item::Attn { li, .. } => format!("Attn L{li}"),
3066 }
3067 );
3068 }
3069 break;
3070 }
3071 valid += 1;
3072 end += match item {
3073 Item::Gdn { run, .. } => run.len(),
3074 Item::Attn { .. } => 1,
3075 };
3076 }
3077 plan.truncate(valid);
3078 if plan.is_empty() {
3079 if self.graph_head_required {
3080 self.fail_metal_graph("native graph preflight produced no valid items");
3081 }
3082 return start;
3083 }
3084
3085 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3086 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3087 return start;
3088 }
3089
3090 let one_pass = |t: (usize, usize, usize)| {
3099 use cortiq_core::TensorDtype as D;
3100 matches!(
3101 model.tensors[t.0].dtype,
3102 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3103 )
3104 };
3105 let dense_fast = plan.iter().all(|it| match it {
3106 Item::Attn {
3107 l, li, full_gpu, ..
3108 } => {
3109 *full_gpu
3110 && self.kv_cache.layers[*li].o1.is_none()
3111 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3112 && match l.ffn {
3113 MetalFfn::Dense { gate, up, down, .. } => {
3114 one_pass(gate) && one_pass(up) && one_pass(down)
3115 }
3116 _ => false,
3117 }
3118 }
3119 Item::Gdn { .. } => false,
3120 });
3121 let moe_plan = !dense_fast
3125 && plan.iter().all(|it| match it {
3126 Item::Attn {
3127 l, li, full_gpu, ..
3128 } => {
3129 *full_gpu
3130 && self.kv_cache.layers[*li].o1.is_none()
3131 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3132 }
3133 Item::Gdn { .. } => false,
3134 });
3135 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3136 if block_diag {
3137 use std::sync::atomic::{AtomicBool, Ordering};
3138 static SAID_AB: AtomicBool = AtomicBool::new(false);
3139 if !SAID_AB.swap(true, Ordering::Relaxed) {
3140 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3141 }
3142 }
3143 let _mv_fast = match ab {
3144 Some((bits, _)) => {
3145 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3146 crate::gpu_metal::MvFastGuard::set_raw(bits)
3147 }
3148 None if moe_plan => {
3149 graph.set_dense_concurrent(
3150 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3151 );
3152 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3153 }
3154 None => {
3155 graph.set_dense_concurrent(dense_fast);
3156 let q8r4 = if swa_graph {
3159 crate::gpu_metal::DENSE_Q8R4
3160 } else {
3161 0
3162 };
3163 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3164 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3165 } else {
3166 0
3167 })
3168 }
3169 };
3170
3171 let pool = self.pool.clone();
3174 let (nh, nkv, hd, hs, eps) = (
3175 self.num_heads,
3176 self.num_kv_heads,
3177 self.head_dim,
3178 self.hidden_size,
3179 self.rms_eps,
3180 );
3181 let norm_style = self.norm_style;
3182 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3183 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3184 let kv_id = self.graph_kv_id;
3185 let mut pending: Vec<(usize, usize)> = Vec::new();
3188 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3191 for item in &plan {
3192 let _xt0 = std::time::Instant::now();
3193 let _xkind: u32 = match item {
3194 Item::Gdn { .. } => 2,
3195 Item::Attn { .. } => 3,
3196 };
3197 if self.loop_final_norm {
3199 let item_start = match item {
3200 Item::Gdn { first, .. } => *first,
3201 Item::Attn { li, .. } => *li,
3202 };
3203 if item_start > start && self.is_loop_end(item_start - 1) {
3204 graph.encode_loop_norm(&self.weights.final_norm);
3205 }
3206 }
3207 match item {
3208 Item::Gdn { run, first } => {
3209 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3210 if l.linear_state.len() != want {
3211 l.linear_state = vec![0f32; want];
3212 }
3213 }
3214 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3215 .iter()
3216 .map(|l| l.linear_state.as_slice())
3217 .collect();
3218 let _ig = std::time::Instant::now();
3219 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3220 tracing::error!("q1 graph: GDN run refused after validation");
3222 return start;
3223 }
3224 graph.commit_kind = 2;
3227 graph.commit();
3228 crate::gpu::stageprof(0, _ig.elapsed());
3229 pending.push((*first, run.len()));
3230 }
3231 Item::Attn {
3232 l,
3233 li,
3234 q_norm,
3235 k_norm,
3236 output_gate,
3237 proj_gate,
3238 head_gate_w,
3239 bias,
3240 full_gpu,
3241 } => {
3242 let _ia = std::time::Instant::now();
3243 let inv_freq_l = self.layer_inv_freq(*li);
3249 let rd_l = self.layer_geom(*li).2;
3250 let window_l = self.layer_window(*li);
3251 let head_gate_w = *head_gate_w;
3252 let (nkv, hd) = if gemma4 {
3255 let (a, b, _) = self.layer_geom(*li);
3256 (a, b)
3257 } else {
3258 (nkv, hd)
3259 };
3260 if *full_gpu {
3262 let cache = &self.kv_cache.layers[*li];
3263 let o1p = if cache.o1.is_some() {
3264 match cache.o1_views() {
3265 Some(views) => Some(crate::gpu::O1AttnParams {
3266 views,
3267 epoch: self.o1_epoch,
3268 }),
3269 None => None,
3271 }
3272 } else {
3273 None
3274 };
3275 let o1_layer = cache.o1.is_some();
3276 if o1_layer && o1p.is_none() {
3277 }
3279 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3280 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3281 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3282 debug_assert!(
3285 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3286 );
3287 let p = crate::gpu::AttnDeviceParams {
3288 kv_id,
3289 layer: *li,
3290 nh,
3291 nkv,
3292 hd,
3293 rd: rd_l,
3294 position,
3295 scale: self.attn_scale,
3296 eps: eps as f32,
3297 gemma,
3298 late_qk_norm: self.qk_norm_after_rope,
3299 output_gate: *output_gate,
3300 q_norm: *q_norm,
3301 k_norm: *k_norm,
3302 inv_freq: &inv_freq_l,
3303 rope_scale: self.layer_rope_scale(*li),
3304 cpu_k,
3305 cpu_v,
3306 cpu_stored,
3307 cpu_gen: cache.generation(),
3308 o1: o1p,
3309 window: window_l,
3310 softmax_sink: cache.sinks.as_deref(),
3311 head_gate: head_gate_w,
3312 v_norm: self.attn_v_norm,
3313 };
3314 let o1_bad = o1_layer && p.o1.is_none();
3315 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3316 {
3317 if p.o1.is_none() {
3319 dev_attn.push((*li, nkv, hd));
3320 }
3321 graph.commit_kind = 3;
3322 graph.commit();
3323 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3327 continue;
3328 }
3329 }
3331 graph.encode_attn_prefix(l);
3332 if let Err(err) = graph.sync_checked() {
3333 self.fail_metal_graph(&err);
3334 return start;
3335 }
3336 if !pending.is_empty() {
3337 let idxs: Vec<usize> =
3338 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3339 let mut outs: Vec<&mut [f32]> = self
3340 .kv_cache
3341 .layers
3342 .iter_mut()
3343 .enumerate()
3344 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3345 .map(|(_, s)| s.linear_state.as_mut_slice())
3346 .collect();
3347 graph.read_states(&mut outs);
3348 }
3349 let mut q_raw = attention::take_buf(l.wq.1);
3350 let mut k = attention::take_buf(l.wk.1);
3351 let mut v = attention::take_buf(l.wv.1);
3352 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3353 let mut gate_raw = proj_gate.map(|(gp, _)| {
3356 let mut normed = attention::take_buf(hs);
3357 graph.read_normed(&mut normed);
3358 let mut raw = attention::take_buf(gp.rows());
3359 gp.matvec(&normed, &mut raw, pool.as_deref());
3360 attention::recycle_buf(&mut normed);
3361 raw
3362 });
3363 let cfg = QwenAttnCfg {
3364 num_heads: nh,
3365 num_kv_heads: nkv,
3366 head_dim: hd,
3367 hidden_size: hs,
3368 position,
3369 inv_freq: &inv_freq_l,
3370 rotary_dim: rd_l,
3371 scale: self.attn_scale,
3372 softcap: self.attn_softcap,
3373 window: window_l,
3374 v_norm: self.attn_v_norm,
3377 qk_norm_after_rope: self.qk_norm_after_rope,
3378 gate_sigmoid: self.proj_gate_sigmoid,
3379 q_norm: *q_norm,
3380 k_norm: *k_norm,
3381 output_gate: *output_gate,
3382 softplus_gate: None,
3383 rope_scale: self.layer_rope_scale(*li),
3384 bias: *bias,
3385 rms_eps: eps,
3386 norm_style,
3387 pool: pool.as_deref(),
3388 v_head_dim: hd,
3389 };
3390 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3393 || std::env::var("CMF_ATTN_DUMP").is_ok();
3394 let _ = full_gpu;
3395 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3396 let mut ao = attention::qwen_attention_core(
3397 q_raw,
3398 k,
3399 v,
3400 &mut self.kv_cache.layers[*li],
3401 &cfg,
3402 );
3403 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3407 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3408 let (cq, _cg, _ck, _cv) =
3409 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3410 let cache = &self.kv_cache.layers[*li];
3411 let n = cache.head_keys(0).len() / hd;
3412 let mut bytes: Vec<u8> = Vec::new();
3413 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3414 bytes.extend_from_slice(&v.to_le_bytes());
3415 }
3416 for v in &cq {
3417 bytes.extend_from_slice(&v.to_le_bytes());
3418 }
3419 for g in 0..nkv {
3420 for v in cache.head_keys(g) {
3421 bytes.extend_from_slice(&v.to_le_bytes());
3422 }
3423 }
3424 for g in 0..nkv {
3425 for v in cache.head_values(g) {
3426 bytes.extend_from_slice(&v.to_le_bytes());
3427 }
3428 }
3429 let _ =
3430 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3431 }
3432 }
3433 if let Some((qr0, k0, v0)) =
3434 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3435 {
3436 let (cq, _cg, ck, cv) =
3437 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3438 let mut h_now = vec![0f32; hs];
3439 graph.read_h(&mut h_now);
3440 let cache = &self.kv_cache.layers[*li];
3441 let n_after = cache.head_keys(0).len() / hd;
3442 let stored = n_after.saturating_sub(1);
3446 let cpu_k: Vec<&[f32]> = (0..nkv)
3447 .map(|g| &cache.head_keys(g)[..stored * hd])
3448 .collect();
3449 let cpu_v: Vec<&[f32]> = (0..nkv)
3450 .map(|g| &cache.head_values(g)[..stored * hd])
3451 .collect();
3452 let p = crate::gpu::AttnDeviceParams {
3453 kv_id,
3454 layer: *li,
3455 nh,
3456 nkv,
3457 hd,
3458 rd: rd_l,
3462 position,
3463 scale: self.attn_scale,
3464 eps: eps as f32,
3465 gemma,
3466 late_qk_norm: self.qk_norm_after_rope,
3467 output_gate: *output_gate,
3468 q_norm: *q_norm,
3469 k_norm: *k_norm,
3470 inv_freq: &inv_freq_l,
3471 rope_scale: self.layer_rope_scale(*li),
3472 cpu_k,
3473 cpu_v,
3474 cpu_stored: stored,
3475 cpu_gen: cache.generation(),
3476 o1: None,
3477 window: window_l,
3478 softmax_sink: cache.sinks.as_deref(),
3479 head_gate: None,
3480 v_norm: self.attn_v_norm,
3481 };
3482 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3483 let md = |a: &[f32], b: &[f32]| {
3484 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3485 };
3486 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3487 eprintln!(
3488 "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}",
3489 nn(&cq),
3490 md(&cq, &dq),
3491 nn(&ck),
3492 md(&ck, &dk),
3493 nn(&cv),
3494 md(&cv, &dv),
3495 nn(&ao),
3496 md(&ao, &dao)
3497 );
3498 } else {
3499 eprintln!("attn-oracle L{li}: device probe declined");
3500 }
3501 }
3502 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3503 attention::apply_projected_gate(
3506 &mut ao,
3507 raw,
3508 per_head,
3509 hd,
3510 self.proj_gate_sigmoid,
3511 );
3512 }
3513 if let Some(mut raw) = gate_raw.take() {
3514 attention::recycle_buf(&mut raw);
3515 }
3516 graph.encode_attn_suffix(l, &ao);
3517 graph.commit();
3520 attention::recycle_buf(&mut ao);
3521 }
3522 }
3523
3524 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3525 }
3526 let mut lm_rows = None;
3531 if self.graph_want_logits
3532 && upto.is_none()
3533 && end == self.num_layers
3534 && std::env::var("CMF_GPU_LMHEAD")
3535 .map(|v| v != "0")
3536 .unwrap_or(true)
3537 {
3538 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3539 if graph.lm_head_ok(lm) {
3540 graph.encode_lm_head(&self.weights.final_norm, lm);
3541 lm_rows = Some(lm.1);
3542 }
3543 }
3544 }
3545 if self.graph_head_required && lm_rows.is_none() {
3546 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3547 self.fail_metal_graph("fused graph head was requested but not encodable");
3548 return start;
3549 }
3550 let _sy0 = std::time::Instant::now();
3551 if let Err(err) = graph.sync_checked() {
3552 if let Some(profile) = pager_profile_page.as_mut() {
3553 profile.finish(start, graph.pager_page_stats(), "sync-error");
3554 }
3555 self.fail_metal_graph(&err);
3556 return start;
3557 }
3558 let _rs0 = std::time::Instant::now();
3559 if !pending.is_empty() {
3560 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3561 let mut outs: Vec<&mut [f32]> = self
3562 .kv_cache
3563 .layers
3564 .iter_mut()
3565 .enumerate()
3566 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3567 .map(|(_, s)| s.linear_state.as_mut_slice())
3568 .collect();
3569 graph.read_states(&mut outs);
3570 }
3571 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3572 use std::sync::atomic::{AtomicU64, Ordering};
3573 static SY: AtomicU64 = AtomicU64::new(0);
3574 static RS: AtomicU64 = AtomicU64::new(0);
3575 static N: AtomicU64 = AtomicU64::new(0);
3576 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3577 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3578 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3579 if n % 100 == 0 {
3580 eprintln!(
3581 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3582 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3583 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3584 );
3585 }
3586 }
3587 if let Some(rows) = lm_rows {
3588 crate::gpu::hostprof_encode_done(_mt0);
3589 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3590 graph.read_logits(&mut lg);
3591 crate::gpu::hostprof_total(_mt0);
3592 lg.resize(self.vocab_size, 0.0);
3593 if let Some(c) = self.final_softcap {
3594 softcap_in_place(&mut lg, c, self.pool.as_deref());
3595 }
3596 self.graph_logits = Some(lg);
3597 }
3598 graph.read_h(h);
3599 if self.graph_head_required && self.graph_logits.is_none() {
3600 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3601 self.fail_metal_graph("fused graph head completed without logits readback");
3602 return start;
3603 }
3604 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3605 METAL_GRAPH_LAYERS.fetch_add(
3606 end.saturating_sub(start) as u64,
3607 std::sync::atomic::Ordering::Relaxed,
3608 );
3609 if self.graph_head_required {
3610 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3611 }
3612 for (li, nkv, hd) in dev_attn {
3616 let mut krow = attention::take_buf(nkv * hd);
3617 let mut vrow = attention::take_buf(nkv * hd);
3618 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3619 let cache = &mut self.kv_cache.layers[li];
3620 cache.append(&krow, &vrow, &[]);
3621 let n = cache.seq_len;
3622 let mut imp = attention::take_buf(n);
3623 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3624 cache.accumulate_imp(&imp);
3625 attention::recycle_buf(&mut imp);
3626 }
3627 attention::recycle_buf(&mut krow);
3628 attention::recycle_buf(&mut vrow);
3629 }
3630 if let Some(profile) = pager_profile_page.as_mut() {
3631 profile.finish(end, graph.pager_page_stats(), "page-complete");
3632 }
3633 if let Some((_, arm)) = ab {
3634 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3635 }
3636 end
3637 }
3638
3639 pub fn new(
3640 tokenizer: Tokenizer,
3641 weights: PipelineWeights,
3642 hidden_size: usize,
3643 intermediate_size: usize,
3644 num_heads: usize,
3645 num_kv_heads: usize,
3646 head_dim: usize,
3647 num_layers: usize,
3648 physical_layers: usize,
3649 loop_final_norm: bool,
3650 vocab_size: usize,
3651 rms_eps: f64,
3652 rope_base: f32,
3653 norm_style: NormStyle,
3654 max_seq_len: usize,
3655 sampler_config: SamplerConfig,
3656 ) -> Self {
3657 let rng = match sampler_config.seed {
3658 Some(s) => SplitMix64::new(s),
3659 None => SplitMix64::from_entropy(),
3660 };
3661 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3662 let pool = Pool::from_env();
3663 if let Some(p) = &pool {
3664 tracing::info!("worker pool: {} threads", p.n_workers());
3665 if let Some(model) = weights
3667 .lm_head
3668 .model_arc()
3669 .or_else(|| weights.embed_tokens.model_arc())
3670 {
3671 let regions: Vec<&[u8]> =
3672 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3673 p.bind_numa(®ions);
3674 }
3675 }
3676 Self {
3677 gpu_plan: None,
3678 tokenizer: std::sync::Arc::new(tokenizer),
3679 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3680 sampler_config,
3681 weights,
3682 hidden_size,
3683 intermediate_size,
3684 num_heads,
3685 num_kv_heads,
3686 head_dim,
3687 num_layers,
3688 physical_layers,
3689 loop_final_norm,
3690 vocab_size,
3691 rms_eps,
3692 rope_base,
3693 norm_style,
3694 rotary_dim: head_dim,
3695 attention_heads_per_layer: None,
3696 kv_heads_per_layer: None,
3697 v_head_dim: None,
3698 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3699 .filter(|v| !v.is_empty())
3700 .map(std::path::PathBuf::from),
3701 graph_declines: std::cell::RefCell::new(Vec::new()),
3702 mimo_moe: Default::default(),
3703 minicpm_tail: Default::default(),
3704 vmf_cfg: None,
3705 gdn_cfg: None,
3706 kda_cfg: None,
3707 g3n: None,
3708 g4e: None,
3709 dsv4: None,
3710 dsv41: None,
3711 dsv41_vision: None,
3712 dsv41_prefill: None,
3713 qwen4_exp: None,
3714 clover: None,
3715 clover_stats: clover::CloverStats::default(),
3716 dsv4_mtp: Vec::new(),
3717 dspark: None,
3718 dspark_pending: Vec::new(),
3719 dspark_hist: Vec::new(),
3720 dspark_real: Vec::new(),
3721 dspark_trunk_picks: Vec::new(),
3722 dspark_exp: Vec::new(),
3723 dspark_draft_ns: 0,
3724 m3_winners: Vec::new(),
3725 m3_counts: Vec::new(),
3726 m3_n_exp: 0,
3727 m3_tokens: 0,
3728 m3_prewarmed: false,
3729 logit_multiplier: None,
3730 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3731 graph_failed: std::sync::atomic::AtomicBool::new(false),
3732 kv_history: Vec::new(),
3733 kv_history_device: false,
3734 short_conv_cfg: None,
3735 mtp: None,
3736 mimo_mtp: None,
3737 verify_exact_moe: false,
3738 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3739 ignore_eos: false,
3740 draft_full_streak: 0,
3741 spec_k_adapt: None,
3742 spec_acc_ewma: 0.7,
3743 rng,
3744 sampler_scratch: SamplerScratch::default(),
3745 spec_forced: None,
3746 spec_q: Vec::new(),
3747 spec_p: Vec::new(),
3748 spec_res: Vec::new(),
3749 spec_qs: Vec::new(),
3750 spec_ps: Vec::new(),
3751 spec_ress: Vec::new(),
3752 mtp_graph_mode: None,
3753 #[cfg(target_os = "macos")]
3754 metal_verify: None,
3755 inv_freq,
3756 ws: ForwardScratch::new(hidden_size),
3757 pool,
3758 model: None,
3759 dyn_force_f32: false,
3760 dyn_skill_layers: Vec::new(),
3761 dyn_active: None,
3762 dyn_blend_loaded: false,
3763 dyn_phi_layer: None,
3764 dyn_phi_ema: Vec::new(),
3765 dyn_phi_seen: 0,
3766 dyn_router: None,
3767 o1_cfg: None,
3768 o1_epoch: 0,
3769 o1_flags: Vec::new(),
3770 trace: false,
3771 calib_temp: 1.0,
3772 confidence_on: true,
3773 embed_multiplier: 1.0,
3774 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3775 swa: None,
3776 swa_trim: None,
3777 sliding_layers: None,
3778 no_rope_layers: None,
3779 anchor_core: None,
3780 bounded_rope: None,
3781 kv_prefix: KvPrefix::default(),
3782 last_prefill_tokens: 0,
3783 inv_freq_local: None,
3784 rotary_dim_local: None,
3785 rope_scale: 1.0,
3786 rope_scale_local: 1.0,
3787 global_attn: None,
3788 inv_freq_global: None,
3789 attn_v_norm: false,
3790 qk_norm_after_rope: false,
3791 proj_gate_sigmoid: false,
3792 final_softcap: None,
3793 head_clusters: None,
3794 attn_softcap: 0.0,
3795 graph_want_logits: false,
3796 graph_head_required: false,
3797 graph_logits: None,
3798 embryo_graph: None,
3799 graph_refused: std::sync::atomic::AtomicBool::new(false),
3800 graph_kv_id: {
3801 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3802 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3803 },
3804 #[cfg(test)]
3805 nll_test_fail_at: None,
3806 #[cfg(test)]
3807 nll_test_force_serial: false,
3808 }
3809 }
3810
3811 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3819 if let Err(e) = self.try_set_o1(cfg) {
3820 tracing::error!("{e}");
3821 }
3822 }
3823
3824 pub fn bounded_native(&self) -> bool {
3828 self.anchor_core.is_some()
3829 }
3830
3831 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3834 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3835 }
3836
3837 pub fn o1_refusal(&self) -> Option<String> {
3839 self.anchor_core.as_ref().map(|ac| {
3840 format!(
3841 "--o1 / CMF_O1 refused: the anchor is native bounded \
3842 (anchor_core kind={} window={} sink={}); the file's operator \
3843 is executed as-is and no post-hoc Nyström overlay applies",
3844 ac.kind, ac.window, ac.sink
3845 )
3846 })
3847 }
3848
3849 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3851 if let Some(c) = &cfg {
3852 if let Some(why) = self.o1_refusal() {
3853 self.o1_flags = Vec::new();
3854 self.o1_cfg = None;
3855 return Err(why);
3856 }
3857 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3858 self.o1_flags.clear();
3859 self.o1_cfg = None;
3860 return Err(format!(
3861 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3862 c.w, c.sink
3863 ));
3864 }
3865 }
3866 self.o1_flags = match &cfg {
3867 Some(c) => {
3868 let mut flags = c.layer_flags(self.num_layers);
3869 for (li, f) in flags.iter_mut().enumerate() {
3870 if *f
3876 && (!matches!(
3877 self.weights.layers[self.phys_layer(li)].attn,
3878 AttnKind::Full { .. }
3879 ) || self.layer_window(li).is_some()
3880 || self.kv_cache.layers[li].sinks.is_some()
3881 || self.layer_v_dim(li) != self.layer_geom(li).1)
3882 {
3883 *f = false;
3884 }
3885 }
3886 flags
3887 }
3888 None => Vec::new(),
3889 };
3890 if let Some(c) = &cfg {
3891 let n = self.o1_flags.iter().filter(|&&f| f).count();
3892 tracing::info!(
3893 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3894 self.num_layers,
3895 c.m,
3896 c.w,
3897 c.sink,
3898 c.rect
3899 );
3900 }
3901 self.o1_cfg = cfg;
3902 Ok(())
3903 }
3904
3905 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3911 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3912 return Err(format!(
3913 "anchor_core kind '{}' is not executable by this runtime",
3914 cfg.kind
3915 ));
3916 }
3917 if cfg.window == 0 {
3918 return Err("anchor_core.window must be >= 1".into());
3919 }
3920 let mut n = 0usize;
3921 for li in 0..self.num_layers {
3922 let pl = self.phys_layer(li);
3923 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3924 if w.window != cfg.window || w.sink != cfg.sink {
3925 return Err(format!(
3926 "layer {li}: bounded weights (window {} sink {}) disagree with \
3927 anchor_core (window {} sink {})",
3928 w.window, w.sink, cfg.window, cfg.sink
3929 ));
3930 }
3931 self.kv_cache.layers[li].install_bounded(cfg.window);
3932 n += 1;
3933 }
3934 }
3935 if n == 0 {
3936 return Err("anchor_core is present but no layer executes it".into());
3937 }
3938 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3939 self.bounded_rope = Some(std::sync::Arc::new(rope));
3940 self.anchor_core = Some(cfg.clone());
3941 self.embryo_graph = None;
3942 tracing::info!(
3943 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3944 cfg.kind,
3945 cfg.window,
3946 cfg.sink,
3947 self.kv_cache
3948 .layers
3949 .iter()
3950 .map(|l| l.bounded_state_bytes())
3951 .max()
3952 .unwrap_or(0)
3953 );
3954 Ok(())
3955 }
3956
3957 pub fn install_wire_identity(&mut self, identity: u64) {
3961 for li in 0..self.kv_cache.layers.len() {
3962 let pl = self.phys_layer(li);
3963 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3964 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3965 Some(AttnKind::Linear(_))
3966 | Some(AttnKind::LinearGdn(_))
3967 | Some(AttnKind::ShortConv(_))
3968 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3969 _ => crate::kv_cache::WireKind::Full,
3970 };
3971 let l = &mut self.kv_cache.layers[li];
3972 l.wire_kind = kind;
3973 l.wire_identity = identity;
3974 l.wire_layer = li as u32;
3977 }
3978 }
3979
3980 pub fn clear_history(&mut self) {
3982 self.kv_history.clear();
3983 self.kv_history_device = false;
3984 self.kv_prefix.clear();
3985 }
3986
3987 pub fn graph_refused(&self) -> bool {
3991 self.graph_refused
3992 .load(std::sync::atomic::Ordering::Relaxed)
3993 }
3994
3995 pub fn mark_graph_refused(&self) {
3997 if !self
3998 .graph_refused
3999 .swap(true, std::sync::atomic::Ordering::Relaxed)
4000 {
4001 tracing::info!(
4002 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4003 self.graph_kv_id
4004 );
4005 }
4006 }
4007
4008 pub fn device_sequence_position(&self) -> Option<usize> {
4012 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4013 }
4014
4015 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4020 let dev = self.device_sequence_position();
4021 if recorded_on_device {
4022 dev == Some(n) && self.embryo_resident_wanted()
4023 } else {
4024 dev.is_none()
4025 }
4026 }
4027
4028 pub(crate) fn invalidate_for_weight_change(&mut self) {
4036 self.clear_sequence_state();
4037 self.embryo_graph = None;
4038 }
4039
4040 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4046 let (n, on_device) = if self.bounded_native() {
4047 (
4048 self.kv_prefix.extension(input_ids),
4049 self.kv_prefix.on_device(),
4050 )
4051 } else {
4052 let h = &self.kv_history;
4053 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4054 (h.len(), self.kv_history_device)
4055 } else {
4056 (0, false)
4057 }
4058 };
4059 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4062 tracing::warn!(
4063 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4064 the device now holds {:?} — re-prefilling from zero",
4065 if on_device { "resident device" } else { "host" },
4066 self.device_sequence_position()
4067 );
4068 return 0;
4069 }
4070 n
4071 }
4072
4073 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4076 self.cached_prefix_len(input_ids)
4077 }
4078
4079 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4085 let on_device = self.device_sequence_position().is_some();
4086 if self.bounded_native() {
4087 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4088 let prev_device = self.kv_prefix.on_device();
4089 self.kv_history.clear();
4090 self.kv_history_device = false;
4091 if keep && prev_device == on_device {
4092 self.kv_prefix.extend(&consumed[reused..]);
4093 } else {
4094 self.kv_prefix.set(consumed);
4095 }
4096 self.kv_prefix.set_on_device(on_device);
4097 } else {
4098 self.kv_history = consumed.to_vec();
4099 self.kv_history_device = on_device;
4100 }
4101 }
4102
4103 pub fn o1_active(&self) -> bool {
4105 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4106 }
4107
4108 pub fn generation_batch_k(&self) -> usize {
4126 if let Some(k) = std::env::var("CMF_BATCH_K")
4127 .ok()
4128 .and_then(|v| v.parse::<usize>().ok())
4129 {
4130 return k;
4131 }
4132 if self.g4_batch_prefill() {
4135 return 128;
4136 }
4137 #[cfg(not(target_os = "macos"))]
4138 if self.graph_prefill_preferred() && !self.o1_active() {
4139 let moe = self
4142 .weights
4143 .layers
4144 .iter()
4145 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4146 let q82 = crate::gpu::moe_q82_graph_on()
4152 && self.weights.layers.iter().any(|lw| {
4153 matches!(&lw.ffn, FfnKind::Moe(m)
4154 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4155 });
4156 return if q82 {
4157 128
4158 } else if moe {
4159 32
4160 } else {
4161 128
4162 };
4163 }
4164 0
4165 }
4166
4167 pub fn generation_graph_prefill(&self) -> bool {
4168 let graph = self.graph_prefill_preferred();
4169 #[cfg(not(target_os = "macos"))]
4180 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4181 return false;
4182 }
4183 graph
4184 }
4185
4186 pub fn o1_device_stats(&self) -> (usize, u64) {
4191 crate::gpu::o1_device_stats(self.graph_kv_id)
4192 }
4193
4194 pub fn o1_begin(&mut self) {
4199 self.o1_begin_with_prefix(None);
4200 }
4201
4202 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4206 if let Some(c) = &self.o1_cfg {
4207 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4208 let boundary = requested_prefix.map(|p| {
4209 p.max(
4210 crate::nystrom::o1_deferred_boundary(w, sink)
4211 .expect("o1 config boundary validated in set_o1"),
4212 )
4213 });
4214 for (li, &f) in self.o1_flags.iter().enumerate() {
4215 if f {
4216 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4217 }
4218 }
4219 }
4220 }
4221
4222 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4224 self.o1_cfg.as_ref().and_then(|c| {
4225 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4226 .map(|floor| requested_prefix.max(floor))
4227 })
4228 }
4229
4230 fn o1_note_transition(&mut self) {
4231 let mut transitioned = false;
4235 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4236 if flagged {
4237 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4238 }
4239 }
4240 if transitioned {
4241 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4242 }
4243 }
4244
4245 fn o1_pending(&self) -> bool {
4246 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4247 f && self.kv_cache.layers[li].seq_len > 0
4248 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4249 })
4250 }
4251
4252 fn o1_fail(&mut self, err: String) {
4253 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4254 self.clear_sequence_state();
4255 self.graph_failed
4256 .store(true, std::sync::atomic::Ordering::Relaxed);
4257 self.cancel
4258 .store(true, std::sync::atomic::Ordering::Relaxed);
4259 }
4260
4261 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4266 if self.o1_cfg.is_none() {
4267 return Ok(false);
4268 }
4269 let mut participating = false;
4270 for li in 0..self.num_layers {
4271 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4272 continue;
4273 }
4274 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4275 return Err(err);
4276 }
4277 if self.kv_cache.layers[li].seq_len == 0 {
4278 continue;
4279 }
4280 participating = true;
4281 let num_heads = self.layer_num_heads(li);
4282 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4283 }
4284 self.o1_note_transition();
4285 for li in 0..self.num_layers {
4286 if self.o1_flags.get(li).copied().unwrap_or(false) {
4287 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4288 return Err(err);
4289 }
4290 }
4291 }
4292 Ok(participating
4293 && (0..self.num_layers).all(|li| {
4294 !self.o1_flags.get(li).copied().unwrap_or(false)
4295 || self.kv_cache.layers[li].seq_len == 0
4296 || self.kv_cache.layers[li].o1_sealed()
4297 }))
4298 }
4299
4300 fn o1_progress(&mut self) {
4303 if !self.o1_active() {
4304 return;
4305 }
4306 for li in 0..self.num_layers {
4307 if self.o1_flags.get(li).copied().unwrap_or(false) {
4308 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4309 self.o1_fail(err);
4310 return;
4311 }
4312 }
4313 }
4314 self.o1_note_transition();
4318 if !self.o1_pending() {
4319 return;
4320 }
4321 if let Err(err) = self.o1_seal_checked() {
4322 self.o1_fail(err);
4323 }
4324 }
4325
4326 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4331 if self
4332 .graph_failed
4333 .swap(false, std::sync::atomic::Ordering::Relaxed)
4334 {
4335 self.cancel
4336 .store(false, std::sync::atomic::Ordering::Relaxed);
4337 self.clear_sequence_state();
4338 return Err(format!("{phase}: deferred O(1) transition failed"));
4339 }
4340 Ok(())
4341 }
4342
4343 pub fn o1_seal(&mut self) {
4347 if let Err(err) = self.o1_seal_checked() {
4348 self.o1_fail(err);
4349 }
4350 }
4351
4352 pub fn set_trace(&mut self, on: bool) {
4354 self.trace = on;
4355 }
4356
4357 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4360 self.rng = match config.seed {
4361 Some(seed) => SplitMix64::new(seed),
4362 None => SplitMix64::from_entropy(),
4363 };
4364 self.sampler_config = config;
4365 }
4366
4367 pub fn set_confidence(&mut self, on: bool) {
4372 self.confidence_on = on;
4373 }
4374
4375 pub fn set_calib_temp(&mut self, t: f32) {
4378 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4379 }
4380
4381 pub fn calib_temp(&self) -> f32 {
4383 self.calib_temp
4384 }
4385
4386 pub fn rope_inv_freq(&self) -> &[f32] {
4390 self.inv_freq.as_slice()
4391 }
4392
4393 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4396 self.rotary_dim = rotary_dim.min(self.head_dim);
4397 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4398 self.embryo_graph = None;
4402 }
4403
4404 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4405 QwenAttnCfg {
4406 num_heads: self.num_heads,
4407 num_kv_heads: self.num_kv_heads,
4408 head_dim: self.head_dim,
4409 hidden_size: self.hidden_size,
4410 position,
4411 inv_freq: &self.inv_freq,
4412 rotary_dim: self.rotary_dim,
4413 scale: self.attn_scale,
4414 softcap: self.attn_softcap,
4415 window: None,
4416 v_norm: false,
4417 qk_norm_after_rope: self.qk_norm_after_rope,
4418 gate_sigmoid: self.proj_gate_sigmoid,
4419 q_norm: None,
4420 k_norm: None,
4421 output_gate: false,
4422 softplus_gate: None,
4423 rope_scale: self.rope_scale,
4424 bias: None,
4425 rms_eps: self.rms_eps,
4426 norm_style: self.norm_style,
4427 pool: self.pool.as_deref(),
4428 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4429 }
4430 }
4431
4432 pub fn generate(
4434 &mut self,
4435 prompt: &str,
4436 max_tokens: usize,
4437 task_mask: Option<&TaskMask>,
4438 on_token: Option<TokenCallback>,
4439 ) -> Result<GenerateResult, String> {
4440 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4441 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4442 }
4443
4444 pub fn generate_from_vl(
4447 &mut self,
4448 input: &crate::dsv41_vision::PreparedVlInputs,
4449 max_tokens: usize,
4450 task_mask: Option<&TaskMask>,
4451 on_token: Option<TokenCallback>,
4452 ) -> Result<GenerateResult, String> {
4453 let Some(dsv41) = &self.dsv41 else {
4454 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4455 };
4456 if input.token_ids.is_empty() {
4457 return Err("empty V4.1 multimodal prompt".into());
4458 }
4459 if input.token_types.len() != input.token_ids.len() {
4460 return Err(format!(
4461 "V4.1 token type count {} != token count {}",
4462 input.token_types.len(),
4463 input.token_ids.len()
4464 ));
4465 }
4466 let dim = dsv41.2.dim;
4467 let mut embeddings = vec![None; input.token_ids.len()];
4468 let mut participates = vec![true; input.token_ids.len()];
4469 if !input.images.is_empty() {
4470 let vision = self
4471 .dsv41_vision
4472 .as_ref()
4473 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4474 for image in &input.images {
4475 let end = image.start.saturating_add(image.types.len());
4476 if end > input.token_ids.len() {
4477 return Err(format!(
4478 "V4.1 image span {}..{} exceeds prompt length {}",
4479 image.start,
4480 end,
4481 input.token_ids.len()
4482 ));
4483 }
4484 let mut span = vec![0.0f32; image.types.len() * dim];
4485 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4486 for (offset, &kind) in image.types.iter().enumerate() {
4487 let pos = image.start + offset;
4488 if input.token_types[pos] != kind {
4489 return Err(format!(
4490 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4491 input.token_types[pos]
4492 ));
4493 }
4494 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4495 participates[pos] = false;
4496 }
4497 }
4498 }
4499 for (pos, &kind) in input.token_types.iter().enumerate() {
4500 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4501 return Err(format!("V4.1 text position {pos} has an image embedding"));
4502 }
4503 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4504 return Err(format!("V4.1 image position {pos} has no image embedding"));
4505 }
4506 }
4507 self.dsv41_prefill = Some((embeddings, participates));
4508 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4509 self.dsv41_prefill = None;
4510 result
4511 }
4512
4513 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4515 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4516 }
4517
4518 pub fn generate_from_ids(
4526 &mut self,
4527 input_ids: &[u32],
4528 max_tokens: usize,
4529 task_mask: Option<&TaskMask>,
4530 on_token: Option<TokenCallback>,
4531 ) -> Result<GenerateResult, String> {
4532 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4533 }
4534
4535 pub fn generate_from_embeds(
4541 &mut self,
4542 input_ids: &[u32],
4543 prompt_rows: &[f32],
4544 max_tokens: usize,
4545 task_mask: Option<&TaskMask>,
4546 on_token: Option<TokenCallback>,
4547 ) -> Result<GenerateResult, String> {
4548 if input_ids.is_empty()
4549 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4550 {
4551 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4552 }
4553 if prompt_rows.iter().any(|x| !x.is_finite()) {
4554 return Err("embedded prompt contains non-finite values".into());
4555 }
4556 if !self.can_prefill_batched()
4557 || self.dyn_router.is_some()
4558 || self.o1_active()
4559 || self.mtp.is_some()
4560 || self.gpu_plan.is_some()
4561 {
4562 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4563 }
4564 self.generate_with_prompt_rows(
4565 input_ids,
4566 Some(prompt_rows),
4567 max_tokens,
4568 task_mask,
4569 on_token,
4570 )
4571 }
4572
4573 fn generate_with_prompt_rows(
4574 &mut self,
4575 input_ids: &[u32],
4576 prompt_rows: Option<&[f32]>,
4577 max_tokens: usize,
4578 task_mask: Option<&TaskMask>,
4579 mut on_token: Option<TokenCallback>,
4580 ) -> Result<GenerateResult, String> {
4581 #[cfg(target_os = "macos")]
4582 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4583 if std::env::var("CMF_TRACE_H").is_ok() {
4584 eprintln!("input_ids: {input_ids:?}");
4585 }
4586 if input_ids.is_empty() {
4587 return Err("empty prompt: nothing to generate from".to_string());
4588 }
4589 self.graph_failed
4593 .store(false, std::sync::atomic::Ordering::Relaxed);
4594 let task_mask = self.drop_open_mask(task_mask);
4599 if self.clover.is_some() {
4602 if prompt_rows.is_some() || task_mask.is_some() {
4603 return Err("block-diffusion generation takes token prompts without task masks".into());
4604 }
4605 return self.clover_generate(input_ids, max_tokens, on_token);
4606 }
4607
4608 let mut reuse_from = {
4616 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4617 if on
4618 && prompt_rows.is_none()
4619 && task_mask.is_none()
4620 && self.mtp.is_none()
4621 && !(self.mimo_mtp.is_some() && self.speculative)
4622 && self.o1_cfg.is_none()
4623 && self.dsv41.is_none()
4624 {
4625 self.cached_prefix_len(input_ids)
4626 } else {
4627 0
4628 }
4629 };
4630 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4633 reuse_from = 0;
4634 }
4635 self.last_prefill_tokens = input_ids.len() - reuse_from;
4636 let bounded_native = self.bounded_native();
4637 if reuse_from == 0 {
4638 self.reset_fresh_mtp_draft_state();
4643 self.clear_sequence_state();
4644 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4645 eprintln!(
4646 "kv-reuse: {} of {} prompt positions already cached",
4647 reuse_from,
4648 input_ids.len()
4649 );
4650 }
4651 crate::gpu::graph_race_begin_generation();
4652 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4656 std::env::var("CMF_O1_PREFILL")
4657 .ok()
4658 .and_then(|v| v.parse::<usize>().ok())
4659 .filter(|&p| p > 0)
4660 } else {
4661 None
4662 };
4663 if task_mask.is_none() {
4664 self.o1_begin_with_prefix(o1_prefill);
4665 }
4666
4667 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4673 #[cfg(target_os = "macos")]
4711 let metal_graph = crate::gpu::q1_force()
4712 && crate::gpu::enabled_here()
4713 && std::env::var("CMF_GPU_BLOCK")
4714 .map(|v| v != "0")
4715 .unwrap_or(true);
4716 #[cfg(target_os = "macos")]
4722 let native_metal_backend = crate::gpu::q1_force();
4723 #[cfg(not(target_os = "macos"))]
4724 let metal_graph = false;
4725 #[cfg(not(target_os = "macos"))]
4726 let native_metal_backend = false;
4727 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4728 let spec_cheap_round =
4732 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4733 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4734 || match spec_sample_env.as_deref() {
4735 Some("1") => true,
4736 Some(_) => false,
4737 None => metal_graph && spec_cheap_round,
4738 };
4739 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4755 for lw in &self.weights.layers {
4756 if let FfnKind::Dense(d) = &lw.ffn {
4757 dense_n += 1;
4758 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4759 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4760 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4761 {
4762 dense_q4tp += 1;
4763 }
4764 }
4765 }
4766 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4767 let penalized = !metal_graph
4779 && (self.sampler_config.repetition_penalty != 1.0
4780 || self.sampler_config.presence_penalty != 0.0
4781 || !self.sampler_config.suppress_tokens.is_empty());
4782 #[cfg(feature = "gpu")]
4787 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4788 #[cfg(not(feature = "gpu"))]
4789 let metal_wgpu = false;
4790 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4791 let spec_wanted = graph_spec_wanted_by_policy(
4792 spec_env.as_deref(),
4793 spec_default_ok,
4794 penalized,
4795 metal_wgpu,
4796 native_metal_backend,
4797 );
4798 if spec_wanted && metal_wgpu {
4799 tracing::warn!(
4800 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4801 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4802 );
4803 }
4804 let graph_spec = self.speculative
4808 && (graph_on || metal_graph)
4809 && self.mtp.is_some()
4810 && task_mask.is_none()
4811 && !self.o1_active()
4812 && spec_sampling_ok
4813 && spec_wanted;
4814 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4827 let pair_pays = host_pair_opt_in
4828 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4829 #[cfg(target_os = "macos")]
4833 if metal_graph {
4834 static SAID: std::sync::Once = std::sync::Once::new();
4835 SAID.call_once(|| {
4836 let spec = if graph_spec {
4837 let k = std::env::var("CMF_GRAPH_SPEC_K")
4838 .ok()
4839 .and_then(|v| v.parse::<usize>().ok())
4840 .filter(|&v| (1..=8).contains(&v))
4841 .unwrap_or(7);
4842 let arm = if self.sampler_config.temperature < 1e-6 {
4843 "greedy"
4844 } else {
4845 "sampling"
4846 };
4847 format!(
4848 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4849 Self::draft_vocab_rows(usize::MAX)
4850 )
4851 } else if !self.speculative {
4852 "spec off (CMF_MTP=0)".to_string()
4853 } else if self.mtp.is_none() {
4854 "spec off (no MTP head)".to_string()
4855 } else if !spec_sampling_ok {
4856 if spec_cheap_round {
4857 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4858 } else {
4859 "spec off (sampling without a top-k: the dense chain \
4860 costs more than it saves)"
4861 .to_string()
4862 }
4863 } else if !spec_wanted {
4864 if spec_env.is_none() {
4865 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
4866 } else {
4867 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
4868 }
4869 } else if task_mask.is_some() {
4870 "spec off (task mask)".to_string()
4871 } else if !graph_spec && !pair_pays {
4872 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
4873 .to_string()
4874 } else {
4875 "spec off (O(1) attention)".to_string()
4876 };
4877 let on = |var: &str| {
4878 if std::env::var(var).as_deref() == Ok("0") {
4879 "off"
4880 } else {
4881 "on"
4882 }
4883 };
4884 tracing::info!(
4885 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4886 MTP graph {}, attend {}, probe {}",
4887 if crate::gpu_metal::state4_on() {
4888 "on"
4889 } else {
4890 "off"
4891 },
4892 if crate::gpu_metal::async_replay_on() {
4893 "on"
4894 } else {
4895 "off"
4896 },
4897 on("CMF_METAL_PREFILL"),
4898 on("CMF_MTP_GRAPH"),
4899 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4900 if crate::gpu::probe_enabled() {
4901 "bypassed (q1 force)"
4902 } else {
4903 "off"
4904 },
4905 );
4906 });
4907 }
4908 let spec_active = self.speculative
4909 && self.mtp.is_some()
4910 && task_mask.is_none()
4911 && !self.o1_active()
4912 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4913 let mut mtp = if spec_active { self.mtp.take() } else { None };
4916 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4917 eprintln!(
4918 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4919 mtp.is_some(),
4920 self.speculative,
4921 self.sampler_config.temperature < 1e-6,
4922 );
4923 }
4924 if let Some(m) = &mut mtp {
4925 m.kv.clear();
4926 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4928 self.mtp_graph_mode = None;
4929 }
4930 let mimo_spec = self.speculative
4934 && self.mimo_mtp.is_some()
4935 && task_mask.is_none()
4936 && !self.o1_active()
4937 && self.dyn_router.is_none()
4938 && self.sampler_config.temperature < 1e-6
4939 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4940 if let Some(st) = self.mimo_mtp.as_mut() {
4941 st.reset();
4942 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4943 Self::mimo_mtp_hist_cap(st, input_ids.len());
4944 }
4945 }
4946 let mut router = if mtp.is_none() {
4950 self.dyn_router.take()
4951 } else {
4952 None
4953 };
4954 let mut reuse_from = reuse_from;
4955 if let Some(r) = &mut router {
4956 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4959 let _ = self.set_active_skill(None);
4962 reuse_from = 0;
4963 self.last_prefill_tokens = input_ids.len();
4964 }
4965 }
4966
4967 let mut all_ids = input_ids.to_vec();
4968 let mut generated = 0usize;
4969 let mut finish_reason = "max_tokens".to_string();
4970 let mut drafted = 0usize;
4971 let mut accepted = 0usize;
4972 let mut dsv4_spec_bad = 0usize;
4979 let mut dsv4_spec_retry_at = 0usize;
4980 let mut confidence: Vec<f32> = Vec::new();
4981 let trace_on = self.trace;
4982 let calib_temp = self.calib_temp;
4983 let mut traces: Vec<TokenTrace> = Vec::new();
4984
4985 let mut hidden = vec![0.0f32; self.hidden_size];
4991 let mut pos = reuse_from;
4992 let fuse_lm = mtp.is_none()
5001 && router.is_none()
5002 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5003 self.graph_logits = None;
5004 self.graph_want_logits = false;
5005 let _tpf = std::time::Instant::now();
5006 let batch_k = self.generation_batch_k();
5007 if let Some(rows) = prompt_rows {
5008 let hs = self.hidden_size;
5009 let chunk = self.prefill_chunk().max(1);
5010 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5011 let end = (pos + chunk).min(input_ids.len());
5012 let hb = match self.prefill_input_rows(
5013 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5014 pos,
5015 task_mask,
5016 ) {
5017 Ok(hb) => hb,
5018 Err(err) => {
5019 self.finish_generation(&mut mtp, &mut router, true);
5020 return Err(err);
5021 }
5022 };
5023 if mimo_spec {
5024 self.mimo_note_rows(&hb, pos);
5025 }
5026 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5027 pos = end;
5028 }
5029 }
5030 while self.qwen4_exp.is_some()
5041 && mtp.is_none()
5042 && pos < input_ids.len()
5043 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5044 {
5045 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5048 let want_logits = end == input_ids.len();
5049 let mut lg = Vec::new();
5050 if let Some(b) = &mut self.qwen4_exp {
5051 crate::qwen4_exp::forward_tokens(
5052 &b.0,
5053 &b.1,
5054 &b.2,
5055 &mut b.3,
5056 &input_ids[pos..end],
5057 pos,
5058 &self.inv_freq,
5059 self.pool.as_deref(),
5060 &mut lg,
5061 want_logits,
5062 );
5063 }
5064 if want_logits {
5065 self.graph_logits = Some(lg);
5066 }
5067 pos = end;
5068 hidden.fill(0.0);
5069 }
5070 while self.dsv4.is_some()
5071 && mtp.is_none()
5072 && pos < input_ids.len()
5073 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5074 {
5075 let end = (pos + prefill_chunk()).min(input_ids.len());
5076 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5077 let mut lg = Vec::new();
5078 if let Some(b) = &mut self.dsv4 {
5079 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5080 crate::dsv4::forward_chunk(
5081 g,
5082 layers,
5083 &cfg,
5084 st,
5085 &ids,
5086 pos,
5087 &self.inv_freq,
5088 self.pool.as_deref(),
5089 &mut lg,
5090 end == input_ids.len(),
5091 );
5092 }
5093 if end == input_ids.len() {
5094 self.graph_logits = Some(lg);
5095 }
5096 pos = end;
5097 hidden = vec![0.0; self.hidden_size];
5098 }
5099 let dsv41_prefill = self.dsv41_prefill.take();
5100 while self.dsv41.is_some()
5101 && mtp.is_none()
5102 && pos < input_ids.len()
5103 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5104 {
5105 let end = (pos + prefill_chunk()).min(input_ids.len());
5106 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5107 let mut lg = Vec::new();
5108 if let Some(b) = &mut self.dsv41 {
5109 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5110 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5111 crate::dsv41::forward_chunk_masked_with_embeddings(
5112 g,
5113 layers,
5114 cfg,
5115 st,
5116 &ids,
5117 pos,
5118 &embeddings[pos..end],
5119 &participates[pos..end],
5120 self.pool.as_deref(),
5121 &mut lg,
5122 );
5123 } else {
5124 crate::dsv41::forward_chunk(
5125 g,
5126 layers,
5127 cfg,
5128 st,
5129 &ids,
5130 pos,
5131 self.pool.as_deref(),
5132 &mut lg,
5133 );
5134 }
5135 }
5136 if end == input_ids.len() {
5137 self.graph_logits = Some(lg);
5138 }
5139 pos = end;
5140 hidden = vec![0.0; self.hidden_size];
5141 }
5142 let dyn_prefill = router.is_some();
5147 let o1_prefill_limit = o1_prefill
5155 .and_then(|requested| self.o1_effective_boundary(requested))
5156 .map(|boundary| boundary.min(input_ids.len()));
5157 let mut o1_sealed = false;
5158 if let Some(limit) = o1_prefill_limit {
5159 if self.can_prefill_batched() && limit > 2 {
5162 let chunk = self.prefill_chunk();
5163 let hs = self.hidden_size;
5164 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5165 let end = (pos + chunk).min(limit);
5166 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5167 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5168 pos = end;
5169 }
5170 } else {
5171 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5172 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5173 pos += 1;
5174 }
5175 }
5176 if pos >= limit {
5177 o1_sealed = match self.o1_seal_checked() {
5178 Ok(sealed) => sealed,
5179 Err(err) => {
5180 self.finish_generation(&mut mtp, &mut router, true);
5181 return Err(err);
5182 }
5183 };
5184 tracing::info!(
5185 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5186 o1_prefill.unwrap_or(0),
5187 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5188 .unwrap_or(limit),
5189 limit,
5190 input_ids.len()
5191 );
5192 }
5193 }
5194 let graph_prefill = self.graph_prefill_preferred();
5200 #[cfg(target_os = "macos")]
5208 if task_mask.is_none()
5209 && !dyn_prefill
5210 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5211 && crate::gpu::enabled_here()
5212 && self.gdn_cfg.is_some()
5213 && !self.per_token_stack()
5214 && input_ids.len() > 8
5215 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5216 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5217 {
5218 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5219 .ok()
5220 .and_then(|v| v.parse().ok())
5221 .filter(|&v| (16..=512).contains(&v))
5222 .unwrap_or(256);
5223 let hs = self.hidden_size;
5224 let _tp = std::time::Instant::now();
5225 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5226 let end = (pos + chunk).min(input_ids.len());
5227 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5228 MetalPrefillOutcome::Completed(hb) => hb,
5229 MetalPrefillOutcome::Declined => break,
5230 MetalPrefillOutcome::Failed => {
5231 self.finish_generation(&mut mtp, &mut router, true);
5232 return Err("ordinary Metal prefill failed after admission".into());
5233 }
5234 };
5235 if let Some(m) = &mut mtp {
5236 let n_pairs = if end < input_ids.len() {
5237 end - pos
5238 } else {
5239 end - pos - 1
5240 };
5241 if n_pairs > 0 {
5242 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5243 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5244 .collect();
5245 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5246 for (j, (h, t)) in pairs.iter().enumerate() {
5247 let h = h.to_vec();
5248 let _ = self.mtp_step(m, &h, *t, pos + j);
5249 }
5250 }
5251 }
5252 }
5253 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5254 pos = end;
5255 }
5256 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5257 eprintln!(
5258 "metal-prefill: {} of {} tokens in {:.1} ms",
5259 pos,
5260 input_ids.len(),
5261 _tp.elapsed().as_secs_f64() * 1e3
5262 );
5263 }
5264 }
5265 #[cfg(not(target_os = "macos"))]
5270 if task_mask.is_none()
5271 && !dyn_prefill
5272 && mtp.is_none()
5273 && o1_prefill.is_none()
5274 && batch_k > 0
5275 && pos + 1 < input_ids.len()
5276 && self.g4_batch_prefill()
5277 {
5278 let t_g4 = std::time::Instant::now();
5279 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5280 Ok((n, last)) => {
5281 if n > 0 {
5282 hidden.copy_from_slice(&last);
5283 }
5284 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5285 eprintln!(
5286 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5287 input_ids.len() - pos,
5288 t_g4.elapsed().as_secs_f64() * 1e3
5289 );
5290 }
5291 pos += n;
5292 }
5293 Err(e) => {
5294 self.finish_generation(&mut mtp, &mut router, true);
5295 return Err(e);
5296 }
5297 }
5298 }
5299 self.mimo_moe_prepare();
5300 #[cfg(not(target_os = "macos"))]
5306 if task_mask.is_none()
5307 && !dyn_prefill
5308 && !graph_prefill
5309 && mtp.is_none()
5310 && o1_prefill.is_none()
5311 && !self.o1_active()
5312 && input_ids.len() > 2
5313 && self.batch_prefix_prefill()
5314 {
5315 let chunk = self.prefill_chunk().max(1);
5316 let hs = self.hidden_size;
5317 let t_bp = std::time::Instant::now();
5318 let pos0 = pos;
5319 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5320 let end = (pos + chunk).min(input_ids.len());
5321 let bk = end - pos;
5322 let mut hiddens = vec![0f32; bk * hs];
5323 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5324 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5325 }
5326 let positions: Vec<usize> = (pos..end).collect();
5327 let mut run = 0usize;
5328 let outcome = self.try_batch_graph_wgpu_prefix(
5329 &mut hiddens,
5330 &positions,
5331 bk,
5332 None,
5333 Some(&mut run),
5334 );
5335 match outcome {
5336 crate::gpu::BatchGraphOutcome::Completed => {
5337 let hb = if run < self.num_layers {
5338 self.prefill_batch_span(
5339 PrefillIn::Hidden(&hiddens),
5340 pos,
5341 None,
5342 run,
5343 self.num_layers,
5344 )
5345 } else {
5346 hiddens
5347 };
5348 if mimo_spec {
5349 self.mimo_note_rows(&hb, pos);
5350 }
5351 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5352 pos = end;
5353 }
5354 crate::gpu::BatchGraphOutcome::Failed => {
5355 self.finish_generation(&mut mtp, &mut router, true);
5356 return Err("batched prefix prefill failed after admission".into());
5357 }
5358 crate::gpu::BatchGraphOutcome::Declined => {
5359 #[cfg(feature = "gpu")]
5362 if pos > pos0 {
5363 self.pull_lagging_host_kv(0, self.num_layers, pos);
5364 }
5365 break;
5366 }
5367 }
5368 }
5369 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5370 eprintln!(
5371 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5372 pos - pos0,
5373 input_ids.len(),
5374 t_bp.elapsed().as_secs_f64() * 1e3
5375 );
5376 }
5377 }
5378 if task_mask.is_none()
5379 && !dyn_prefill
5380 && !graph_prefill
5381 && self.can_prefill_batched()
5382 && !self.per_token_stack()
5383 && o1_prefill.is_none()
5384 && input_ids.len() > 2
5385 {
5386 let chunk = self.prefill_chunk();
5392 let hs = self.hidden_size;
5393 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5394 let end = (pos + chunk).min(input_ids.len());
5395 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5396 if mimo_spec {
5397 self.mimo_note_rows(&hb, pos);
5398 }
5399 if let Some(m) = &mut mtp {
5400 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5401 .ok()
5402 .and_then(|v| v.parse().ok())
5403 .unwrap_or(0);
5404 for p in pos..end {
5405 if p + 1 < input_ids.len() {
5406 if probe >= 1 && p + 2 < input_ids.len() {
5407 let (d1, mut hx) = self.mtp_step_h(
5411 m,
5412 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5413 input_ids[p + 1],
5414 p,
5415 );
5416 let mut ok = d1 == input_ids[p + 2];
5417 Self::chain_probe_note(0, ok);
5418 let mut d_prev = d1;
5419 let mut extra = 0usize;
5420 for j in 1..probe {
5421 if p + 2 + j >= input_ids.len() {
5422 break;
5423 }
5424 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5425 extra += 1;
5426 ok = ok && dj == input_ids[p + 2 + j];
5427 Self::chain_probe_note(j, ok);
5428 d_prev = dj;
5429 hx = hj;
5430 }
5431 m.kv.truncate_last(extra);
5432 } else {
5433 let _ = self.mtp_step(
5434 m,
5435 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5436 input_ids[p + 1],
5437 p,
5438 );
5439 }
5440 }
5441 }
5442 }
5443 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5444 pos = end;
5445 }
5446 }
5447 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5448 if task_mask.is_none()
5449 && !dyn_prefill
5450 && !graph_prefill
5451 && !pair_off
5452 && self.pair_supported()
5453 && o1_prefill.is_none()
5454 {
5455 while pos + 1 < input_ids.len()
5456 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5457 {
5458 let e1 = self.embed_single(input_ids[pos]);
5459 let e2 = self.embed_single(input_ids[pos + 1]);
5460 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5461 if mimo_spec {
5462 self.mimo_note_rows(&h1, pos);
5463 self.mimo_note_rows(&h2, pos + 1);
5464 }
5465 self.commit_linear_scratch();
5467 if let Some(m) = &mut mtp {
5468 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5469 if pos + 2 < input_ids.len() {
5470 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5471 .ok()
5472 .and_then(|v| v.parse().ok())
5473 .unwrap_or(0);
5474 if probe >= 1 && pos + 3 < input_ids.len() {
5475 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5479 let mut ok = d1 == input_ids[pos + 3];
5480 Self::chain_probe_note(0, ok);
5481 let mut d_prev = d1;
5482 let mut extra = 0usize;
5483 for j in 1..probe {
5484 if pos + 3 + j >= input_ids.len() {
5485 break;
5486 }
5487 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5488 extra += 1;
5489 ok = ok && dj == input_ids[pos + 3 + j];
5490 Self::chain_probe_note(j, ok);
5491 d_prev = dj;
5492 hx = hj;
5493 }
5494 m.kv.truncate_last(extra);
5495 } else {
5496 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5497 }
5498 }
5499 }
5500 hidden = h2;
5501 pos += 2;
5502 }
5503 }
5504 let o1_batch_ready = o1_sealed
5517 && o1_prefill.is_some()
5518 && mtp.is_none()
5519 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5520 && (0..self.num_layers).all(|li| {
5521 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5522 cache.o1.is_none() || cache.o1_views().is_some()
5523 });
5524 let mtp_batch_prefill = mtp.is_some()
5529 && graph_prefill
5530 && task_mask.is_none()
5531 && !dyn_prefill
5532 && !self.o1_active()
5533 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5534 if batch_k > 0
5535 && (graph_prefill || o1_batch_ready)
5536 && task_mask.is_none()
5537 && (!self.o1_active() || o1_batch_ready)
5538 && (mtp.is_none() || mtp_batch_prefill)
5539 && !dyn_prefill
5540 && pos + 1 < input_ids.len()
5541 {
5542 let hs = self.hidden_size;
5543 let chunk = batch_k;
5544 while pos < input_ids.len() {
5545 let end = (pos + chunk).min(input_ids.len());
5546 let bk = end - pos;
5547 let mut hiddens = vec![0f32; bk * hs];
5548 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5549 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5550 }
5551 let positions: Vec<usize> = (pos..end).collect();
5552 let t_chunk = std::time::Instant::now();
5553 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5554 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5555 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5556 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5557 eprintln!(
5558 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5559 if o1_batch_ready {
5560 "o1"
5561 } else if mtp_batch_prefill {
5562 "ordinary_mtp"
5563 } else {
5564 "ordinary"
5565 },
5566 bk as f64 / (ms / 1000.0)
5567 );
5568 }
5569 {
5570 use std::sync::atomic::{AtomicBool, Ordering};
5571 static SAID: AtomicBool = AtomicBool::new(false);
5572 if !SAID.swap(true, Ordering::Relaxed) {
5573 if ok_b {
5574 tracing::info!(
5575 "batched prefill: ACTIVE mode={} (k={bk})",
5576 if o1_batch_ready {
5577 "o1"
5578 } else if mtp_batch_prefill {
5579 "ordinary_mtp"
5580 } else {
5581 "ordinary"
5582 }
5583 );
5584 } else {
5585 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5586 }
5587 }
5588 }
5589 if ok_b {
5590 if mimo_spec {
5591 self.mimo_note_rows(&hiddens, pos);
5592 }
5593 if mtp_batch_prefill {
5594 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5595 if n_pairs > 0 {
5596 let rows: Vec<Vec<f32>> = (0..n_pairs)
5602 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5603 .collect();
5604 let pairs: Vec<(&[f32], u32)> = rows
5605 .iter()
5606 .enumerate()
5607 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5608 .collect();
5609 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5610 eprintln!(
5611 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5612 pos,
5613 n_pairs,
5614 pos + n_pairs - 1,
5615 );
5616 }
5617 let warm_error = if let Some(m) = mtp.as_mut() {
5618 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5619 } else {
5620 None
5621 };
5622 if let Some(err) = warm_error {
5623 self.finish_generation(&mut mtp, &mut router, true);
5628 return Err(err.to_string());
5629 }
5630 }
5631 }
5632 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5633 pos = end;
5634 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5635 self.finish_generation(&mut mtp, &mut router, true);
5640 return Err(if o1_batch_ready {
5641 "sealed O(1) batch graph failed after admission".to_string()
5642 } else {
5643 "ordinary recurrent batch graph failed after admission".to_string()
5644 });
5645 } else {
5646 break; }
5648 }
5649 }
5650 if graph_prefill
5654 && task_mask.is_none()
5655 && mtp.is_none()
5656 && !dyn_prefill
5657 && pos == 0
5658 && input_ids.len() > 1
5659 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5660 {
5661 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5662 self.graph_logits = Some(lg);
5663 hidden = vec![0.0; self.hidden_size];
5664 pos = input_ids.len();
5665 }
5666 }
5667 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5668 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5669 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5670 if mimo_spec {
5671 self.mimo_note_rows(&hidden, pos);
5672 }
5673 if let Some(m) = &mut mtp {
5674 if pos + 1 < input_ids.len() {
5675 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5681 .ok()
5682 .and_then(|v| v.parse().ok())
5683 .unwrap_or(0);
5684 if probe >= 1 && pos + 2 < input_ids.len() {
5685 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5686 let mut ok = d1 == input_ids[pos + 2];
5687 Self::chain_probe_note(0, ok);
5688 let mut d_prev = d1;
5689 let mut extra = 0usize;
5690 for j in 1..probe {
5691 if pos + 2 + j >= input_ids.len() {
5692 break;
5693 }
5694 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5695 extra += 1;
5696 ok = ok && dj == input_ids[pos + 2 + j];
5697 Self::chain_probe_note(j, ok);
5698 d_prev = dj;
5699 hx = hj;
5700 }
5701 m.kv.truncate_last(extra);
5704 } else {
5705 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5706 }
5707 }
5708 }
5709 pos += 1;
5710 }
5711 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5712 eprintln!(
5713 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5714 input_ids.len(),
5715 _tpf.elapsed().as_secs_f64() * 1000.0
5716 );
5717 }
5718 if self
5719 .graph_failed
5720 .swap(false, std::sync::atomic::Ordering::Relaxed)
5721 {
5722 self.finish_generation(&mut mtp, &mut router, true);
5727 return Err("GPU token graph failed during prefill".to_string());
5728 }
5729 if self
5732 .cancel
5733 .swap(false, std::sync::atomic::Ordering::Relaxed)
5734 {
5735 self.finish_generation(&mut mtp, &mut router, true);
5739 return Ok(GenerateResult {
5740 text: String::new(),
5741 token_ids: Vec::new(),
5742 prompt_tokens: input_ids.len(),
5743 tokens_generated: 0,
5744 finish_reason: "cancelled".to_string(),
5745 mtp_drafted: 0,
5746 mtp_accepted: 0,
5747 token_confidence: Vec::new(),
5748 traces: Vec::new(),
5749 });
5750 }
5751
5752 if !o1_sealed {
5755 match self.o1_seal_checked() {
5756 Ok(_) => {}
5757 Err(err) => {
5758 self.finish_generation(&mut mtp, &mut router, true);
5759 return Err(err);
5760 }
5761 }
5762 }
5763
5764 macro_rules! commit {
5766 ($id:expr) => {{
5767 all_ids.push($id);
5768 generated += 1;
5769 self.note_draft_id($id);
5770 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5771 finish_reason = "stop".to_string();
5772 false
5773 } else {
5774 let token_text = self.tokenizer.decode_token($id);
5775 let mut go = true;
5776 if let Some(ref mut cb) = on_token {
5777 if !cb(&token_text) {
5778 finish_reason = "cancelled".to_string();
5779 go = false;
5780 }
5781 }
5782 go
5783 }
5784 }};
5785 }
5786
5787 let mut spec_trial = SpecTrial::Spec {
5798 t0: std::time::Instant::now(),
5799 gen0: generated,
5800 rounds: 0,
5801 };
5802 let mut spec_mon = SpecMon {
5808 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5809 ..SpecMon::default()
5810 };
5811 let mut spec_watchdog_off = false;
5812 let mut spec_walls: Vec<f32> = Vec::new();
5815 let mut spec_round_end: Option<std::time::Instant> = None;
5818 if mimo_spec {
5819 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5820 if let Some(mut st) = self.mimo_mtp.take() {
5821 self.mimo_mtp_probe(&mut st, input_ids, &path);
5822 self.mimo_mtp = Some(st);
5823 }
5824 }
5825 }
5826 let mut next_pos = input_ids.len();
5828 'decode: while generated < max_tokens {
5829 if self
5830 .graph_failed
5831 .swap(false, std::sync::atomic::Ordering::Relaxed)
5832 {
5833 self.finish_generation(&mut mtp, &mut router, true);
5838 return Err("GPU token graph failed during decode".to_string());
5839 }
5840 if self
5841 .cancel
5842 .swap(false, std::sync::atomic::Ordering::Relaxed)
5843 {
5844 finish_reason = "cancelled".to_string();
5845 break 'decode;
5846 }
5847 if mimo_spec && next_pos > 0 {
5852 self.mimo_note_rows(&hidden, next_pos - 1);
5855 }
5856 let forced = self.spec_forced.take();
5857 let mut logits = match (forced, self.graph_logits.take()) {
5858 (Some(_), _) => Vec::new(),
5859 (None, Some(lg)) => lg,
5860 (None, None) => {
5861 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5862 #[cfg(feature = "gpu")]
5867 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5868 lg
5869 } else {
5870 inference::rms_norm_into(
5871 &hidden,
5872 &self.weights.final_norm,
5873 self.rms_eps,
5874 self.norm_style,
5875 &mut self.ws.n1,
5876 );
5877 self.lm_head_forward(&self.ws.n1)
5878 };
5879 #[cfg(not(feature = "gpu"))]
5880 let logits_out = {
5881 inference::rms_norm_into(
5882 &hidden,
5883 &self.weights.final_norm,
5884 self.rms_eps,
5885 self.norm_style,
5886 &mut self.ws.n1,
5887 );
5888 self.lm_head_forward(&self.ws.n1)
5889 };
5890 logits_out
5891 }
5892 };
5893 if generated
5896 == std::env::var("CMF_LOGIT_DUMP_STEP")
5897 .ok()
5898 .and_then(|v| v.parse().ok())
5899 .unwrap_or(0)
5900 {
5901 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5902 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5903 for v in hidden.iter().chain(logits.iter()) {
5904 bytes.extend_from_slice(&v.to_le_bytes());
5905 }
5906 if let Err(e) = std::fs::write(&path, &bytes) {
5907 eprintln!("logit dump: failed to write {path}: {e}");
5908 self.finish_generation(&mut mtp, &mut router, true);
5909 return Err(format!("logit dump write failed: {e}"));
5910 }
5911 }
5912 }
5913 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5917 if !logits.is_empty() {
5918 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5919 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5920 if let Err(e) =
5921 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5922 {
5923 eprintln!("logit dump: failed to write {}: {e}", path.display());
5924 }
5925 }
5926 }
5927 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5928 Some(c) => c,
5929 None => {
5930 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5931 sampler::sample_with_scratch_pool(
5932 &logits,
5933 &self.sampler_config,
5934 self.sampler_config.penalty_past(&all_ids, bounded_native),
5935 &mut self.rng,
5936 &mut self.sampler_scratch,
5937 self.pool.as_deref(),
5938 )
5939 }
5940 };
5941 if self.confidence_on {
5942 confidence.push(if logits.is_empty() {
5943 0.0
5944 } else {
5945 sampler::top1_prob_pool(
5946 self.pool.as_deref(),
5947 &mut self.sampler_scratch,
5948 &logits,
5949 t_next,
5950 calib_temp,
5951 )
5952 });
5953 }
5954 if !logits.is_empty() {
5955 attention::recycle_buf(&mut logits);
5956 }
5957 if trace_on {
5958 let skill = router.as_ref().and_then(|r| r.active_id());
5962 traces.push(TokenTrace {
5963 t: generated,
5964 token_id: t_next,
5965 confidence: confidence.last().copied().unwrap_or(0.0),
5966 active_skill: skill,
5967 recon: None,
5968 switched: false,
5969 });
5970 }
5971 if !commit!(t_next) {
5972 break 'decode;
5973 }
5974 if generated >= max_tokens {
5975 break 'decode;
5976 }
5977
5978 self.swa_trim_tails();
5981 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5982 static SAID: std::sync::Once = std::sync::Once::new();
5988 SAID.call_once(|| {
5989 tracing::warn!(
5990 "KV cache full at {} positions — evicting half; quality \
5991 will degrade. Raise CMF_MAX_SEQ.",
5992 self.kv_cache.max_seq_len,
5993 );
5994 });
5995 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5996 self.kv_cache.evict(keep);
5997 }
5998
5999 if graph_spec {
6002 match spec_trial {
6003 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6004 spec_mon.plain_ms =
6005 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6006 let keep = spec_mon.pays();
6007 tracing::info!(
6008 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6009 spec_mon.tokens,
6010 spec_mon.round_ms,
6011 spec_mon.plain_ms,
6012 if keep { "speculating" } else { "plain" }
6013 );
6014 spec_mon.fails = 0;
6015 spec_trial = SpecTrial::Decided {
6016 spec: keep,
6017 recheck_at: if keep { usize::MAX } else { generated + 128 },
6018 };
6019 }
6020 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6021 spec_mon.n = 0;
6022 spec_mon.zero_accept_rounds = 0;
6023 spec_trial = SpecTrial::Spec {
6024 t0: std::time::Instant::now(),
6025 gen0: generated,
6026 rounds: 0,
6027 };
6028 }
6029 _ => {}
6030 }
6031 spec_watchdog_off = matches!(
6032 spec_trial,
6033 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6034 );
6035 }
6036 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6038 let budget = max_tokens - generated - 1;
6039 if let Some(mut st) = self.mimo_mtp.take() {
6040 let k = st.depth.min(budget);
6041 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6042 self.mimo_mtp = Some(st);
6043 let r = match r {
6044 Ok(r) => r,
6045 Err(err) => {
6046 self.finish_generation(&mut mtp, &mut router, true);
6047 return Err(err);
6048 }
6049 };
6050 if let Some(r) = r {
6051 drafted += r.drafted;
6052 accepted += r.accepted.len();
6053 let mut stopped = false;
6054 for &id in &r.accepted {
6055 if self.confidence_on {
6056 confidence.push(0.0);
6057 }
6058 if !commit!(id) {
6059 stopped = true;
6060 break;
6061 }
6062 }
6063 if stopped {
6064 break 'decode;
6065 }
6066 next_pos += r.accepted.len() + 1;
6067 hidden = r.hidden;
6068 self.graph_logits = Some(r.logits);
6071 continue 'decode;
6072 }
6073 }
6074 }
6075 #[cfg(feature = "gpu")]
6078 if self.speculative
6079 && self.qwen4_exp.is_some()
6080 && task_mask.is_none()
6081 && self.sampler_config.temperature < 1e-6
6082 && generated + 1 < max_tokens
6083 && next_pos > 0
6084 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6085 {
6086 let r = match &mut self.qwen4_exp {
6087 Some(b) => crate::qwen4_exp::spec_round(
6088 &b.0,
6089 &b.1,
6090 &b.2,
6091 &mut b.3,
6092 next_pos,
6093 &all_ids,
6094 &self.inv_freq,
6095 self.pool.as_deref(),
6096 ),
6097 None => None,
6098 };
6099 if let Some(r) = r {
6100 drafted += r.drafted;
6101 accepted += r.accepted.len();
6102 let mut stopped = false;
6103 for &id in &r.accepted {
6104 if self.confidence_on {
6105 confidence.push(0.0);
6106 }
6107 if !commit!(id) {
6108 stopped = true;
6109 break;
6110 }
6111 }
6112 if stopped {
6113 break 'decode;
6114 }
6115 next_pos += r.accepted.len() + 1;
6116 hidden.fill(0.0);
6117 self.graph_logits = Some(r.logits);
6118 continue 'decode;
6119 }
6120 }
6121 match &mut mtp {
6122 #[cfg(feature = "gpu")]
6124 Some(m)
6125 if graph_spec
6126 && !spec_watchdog_off
6127 && generated + 1 < max_tokens
6128 && next_pos > 0 =>
6129 {
6130 let t_round = std::time::Instant::now();
6131 if spec_time_level() >= 2 {
6132 if let Some(t) = spec_round_end.take() {
6133 eprintln!(
6134 "spec-gap {:.2} ms (host between rounds)",
6135 t.elapsed().as_secs_f64() * 1e3
6136 );
6137 }
6138 }
6139 spec_stamps_begin();
6140 #[cfg(target_os = "macos")]
6145 let allocs0 =
6146 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6147 #[cfg(not(target_os = "macos"))]
6148 let allocs0 = 0u64;
6149 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6150 m,
6151 &hidden,
6152 t_next,
6153 next_pos,
6154 &mut drafted,
6155 &mut accepted,
6156 &mut all_ids,
6157 max_tokens - generated,
6158 ) {
6159 next_pos = n_pos;
6160 hidden = new_h;
6161 let level = spec_time_level();
6162 if level > 0 {
6163 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6164 let stamps = spec_stamps_take();
6165 let median = if spec_walls.len() >= 3 {
6168 let mut s = spec_walls.clone();
6169 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6170 Some(s[s.len() / 2])
6171 } else {
6172 None
6173 };
6174 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6175 #[cfg(target_os = "macos")]
6176 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6177 .load(std::sync::atomic::Ordering::Relaxed)
6178 - allocs0;
6179 #[cfg(not(target_os = "macos"))]
6180 let allocs = allocs0;
6181 eprintln!(
6182 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6183 extra.len() + 1,
6184 if allocs > 0 {
6185 format!(" [{allocs} new device buffers]")
6186 } else {
6187 String::new()
6188 },
6189 match (outlier, median) {
6190 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6191 _ => String::new(),
6192 }
6193 );
6194 if level >= 2 || outlier {
6195 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6196 eprintln!(
6197 "spec-stamps: {}| untracked {:.1}",
6198 spec_stamps_format(&stamps),
6199 wall - sum
6200 );
6201 }
6202 if spec_mon.n >= 1 {
6203 spec_walls.push(wall);
6204 }
6205 }
6206 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6210 spec_trial = Self::spec_trial_round(
6213 spec_trial,
6214 &mut spec_mon,
6215 generated + extra.len() + 1,
6216 );
6217 let mut stopped = false;
6218 for &id in &extra {
6219 if self.confidence_on {
6220 confidence.push(0.0);
6221 }
6222 if trace_on {
6235 traces.push(TokenTrace {
6236 t: generated,
6237 token_id: id,
6238 confidence: confidence.last().copied().unwrap_or(0.0),
6239 active_skill: None,
6240 recon: None,
6241 switched: false,
6242 });
6243 }
6244 if !commit!(id) {
6245 stopped = true;
6246 break;
6247 }
6248 }
6249 if stopped {
6250 break 'decode;
6251 }
6252 if spec_time_level() >= 2 {
6253 spec_round_end = Some(std::time::Instant::now());
6254 }
6255 continue 'decode;
6256 }
6257 if self
6258 .graph_failed
6259 .swap(false, std::sync::atomic::Ordering::Relaxed)
6260 {
6261 self.finish_generation(&mut mtp, &mut router, true);
6267 return Err("GPU MTP graph failed during speculative decode".to_string());
6268 }
6269 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6280 spec_mon.tokens = 0.0;
6281 spec_mon.fails = 3;
6282 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6283 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6284 next_pos += 1;
6285 continue 'decode;
6286 }
6287 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6289 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6290 drafted += 1;
6291 let emb1 = self.embed_single(t_next);
6292 let emb2 = self.embed_single(draft);
6293 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6294
6295 inference::rms_norm_into(
6296 &h1,
6297 &self.weights.final_norm,
6298 self.rms_eps,
6299 self.norm_style,
6300 &mut self.ws.n1,
6301 );
6302 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6303 let t_after = sampler::sample_with_scratch_pool(
6304 &logits1,
6305 &self.sampler_config,
6306 self.sampler_config.penalty_past(&all_ids, bounded_native),
6307 &mut self.rng,
6308 &mut self.sampler_scratch,
6309 self.pool.as_deref(),
6310 );
6311 if self.confidence_on {
6312 confidence.push(sampler::top1_prob_pool(
6313 self.pool.as_deref(),
6314 &mut self.sampler_scratch,
6315 &logits1,
6316 t_after,
6317 calib_temp,
6318 ));
6319 }
6320 attention::recycle_buf(&mut logits1);
6321 if trace_on {
6322 traces.push(TokenTrace {
6325 t: generated,
6326 token_id: t_after,
6327 confidence: confidence.last().copied().unwrap_or(0.0),
6328 active_skill: None,
6329 recon: None,
6330 switched: false,
6331 });
6332 }
6333 let stop = !commit!(t_after);
6334
6335 if t_after == draft {
6336 accepted += 1;
6337 self.commit_linear_scratch();
6338 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6339 hidden = h2;
6340 next_pos += 2;
6341 } else {
6342 for layer in &mut self.kv_cache.layers {
6344 layer.truncate_last(1);
6345 }
6346 if !stop {
6347 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6348 hidden = self.forward_layers(
6349 &self.embed_single(t_after),
6350 next_pos + 1,
6351 None,
6352 );
6353 }
6354 next_pos += 2;
6355 }
6356 if stop {
6357 break 'decode;
6358 }
6359 }
6360 _ => {
6362 #[cfg(feature = "gpu")]
6367 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6368 static SAID: std::sync::Once = std::sync::Once::new();
6369 SAID.call_once(|| {
6370 eprintln!(
6371 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6372 !self.dsv4_mtp.is_empty(),
6373 task_mask.is_none(),
6374 router.is_none(),
6375 !trace_on,
6376 self.sampler_config.temperature < 1e-6,
6377 self.sampler_config.repetition_penalty == 1.0,
6378 );
6379 });
6380 }
6381 #[cfg(feature = "gpu")]
6382 if Self::dsv4_spec_on()
6383 && self.dsv4.is_some()
6384 && !self.dsv4_mtp.is_empty()
6385 && task_mask.is_none()
6386 && router.is_none()
6387 && !trace_on
6388 && self.sampler_config.temperature < 1e-6
6389 && self.sampler_config.repetition_penalty == 1.0
6390 && generated + 1 < max_tokens
6391 && all_ids.len() >= 2
6392 && generated >= dsv4_spec_retry_at
6393 {
6394 let tip_token = all_ids[all_ids.len() - 2];
6395 let drafted0 = drafted;
6396 let round = self.dsv4_spec_step(
6397 tip_token,
6398 t_next,
6399 next_pos,
6400 max_tokens.saturating_sub(generated),
6401 &mut drafted,
6402 &mut accepted,
6403 );
6404 if drafted > drafted0 {
6405 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6406 if useful {
6407 dsv4_spec_bad = 0;
6408 } else {
6409 dsv4_spec_bad += 1;
6410 if dsv4_spec_bad >= 2 {
6411 dsv4_spec_bad = 0;
6412 dsv4_spec_retry_at = generated.saturating_add(32);
6413 tracing::info!(
6414 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6415 );
6416 }
6417 }
6418 }
6419 if let Some((extra, n_pos)) = round {
6420 next_pos = n_pos;
6421 let mut stopped = false;
6422 for &id in &extra {
6423 if self.confidence_on {
6424 confidence.push(0.0);
6425 }
6426 if !commit!(id) {
6427 stopped = true;
6428 break;
6429 }
6430 }
6431 if stopped {
6432 break 'decode;
6433 }
6434 continue 'decode;
6435 }
6436 }
6437 self.graph_want_logits = fuse_lm;
6438 let mut t_fwd = t_next;
6444 let pure_greedy = self.sampler_config.temperature < 1e-6
6445 && self.sampler_config.repetition_penalty == 1.0
6446 && self.sampler_config.presence_penalty == 0.0
6447 && self.sampler_config.suppress_tokens.is_empty();
6448 let burst_k = std::env::var("CMF_MULTISTEP")
6453 .ok()
6454 .and_then(|v| v.parse::<usize>().ok())
6455 .unwrap_or(0);
6456 if pure_greedy
6457 && burst_k >= 1
6458 && fuse_lm
6459 && task_mask.is_none()
6460 && router.is_none()
6461 && !trace_on
6462 && !self.confidence_on
6463 && std::env::var_os("CMF_FORCE_IDS").is_none()
6468 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6469 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6470 {
6471 let mut stopped = false;
6472 loop {
6473 if self
6478 .cancel
6479 .swap(false, std::sync::atomic::Ordering::Relaxed)
6480 {
6481 finish_reason = "cancelled".to_string();
6482 stopped = true;
6483 break;
6484 }
6485 let room = max_tokens.saturating_sub(generated);
6486 if room <= 2 {
6487 break;
6488 }
6489 let k = burst_k.min(room - 1);
6490 if k < 1 {
6491 break;
6492 }
6493 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6494 if self
6495 .graph_failed
6496 .swap(false, std::sync::atomic::Ordering::Relaxed)
6497 {
6498 self.finish_generation(&mut mtp, &mut router, true);
6499 return Err(
6500 "GPU token graph failed during greedy burst".to_string()
6501 );
6502 }
6503 break;
6504 };
6505 next_pos += k;
6506 for &id in &ids {
6507 if !commit!(id) {
6508 stopped = true;
6509 break;
6510 }
6511 }
6512 if stopped {
6513 break;
6514 }
6515 t_fwd = *ids.last().unwrap();
6516 }
6517 if stopped {
6518 break 'decode;
6519 }
6520 }
6521 #[cfg(target_os = "macos")]
6531 if graph_spec
6532 && spec_watchdog_off
6533 && next_pos > 0
6534 && self.mtp_graph_mode == Some(true)
6535 && crate::gpu::q1_force()
6536 {
6537 if let Some(m) = mtp.as_mut() {
6538 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6539 }
6540 }
6541 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6542 next_pos += 1;
6543 if let Some(r) = &mut router {
6546 let phi = self.dyn_phi_ema.clone();
6547 let decision = r.step(&phi, generated);
6548 if let Some(new_active) = decision {
6549 let _ = self.set_active_skill(new_active);
6550 }
6551 if trace_on {
6554 if let Some(last) = traces.last_mut() {
6555 let e = r.last_best_e();
6556 last.recon = e.is_finite().then_some(e);
6557 last.switched = decision.is_some();
6558 }
6559 }
6560 }
6561 }
6562 }
6563 }
6564
6565 let cancelled = finish_reason == "cancelled";
6566 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6570 if mimo_spec {
6571 if let Some(st) = self.mimo_mtp.as_ref() {
6572 let line = st.stats.line();
6573 tracing::info!("{line}");
6574 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6575 eprintln!("{line}");
6576 }
6577 }
6578 }
6579 self.finish_generation(&mut mtp, &mut router, cancelled);
6580
6581 let output_ids = &all_ids[input_ids.len()..];
6582 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6586 let consumed = std::mem::take(&mut all_ids);
6591 if dyn_switched {
6592 self.clear_sequence_state();
6593 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6594 self.clear_history();
6595 } else {
6596 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6597 }
6598 all_ids = consumed;
6599 let output_ids = &all_ids[input_ids.len()..];
6600 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6602 Ok(GenerateResult {
6603 text: self.tokenizer.decode(output_ids),
6604 token_ids: output_ids.to_vec(),
6605 prompt_tokens: input_ids.len(),
6606 tokens_generated: generated,
6607 finish_reason,
6608 mtp_drafted: drafted,
6609 mtp_accepted: accepted,
6610 token_confidence: confidence,
6611 traces,
6612 })
6613 }
6614
6615 fn mtp_step(
6619 &mut self,
6620 m: &mut MtpModule,
6621 hidden: &[f32],
6622 next_token: u32,
6623 position: usize,
6624 ) -> u32 {
6625 self.mtp_step_h(m, hidden, next_token, position).0
6626 }
6627
6628 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6632 use std::sync::Mutex;
6633 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6634 let mut t = T.lock().unwrap();
6635 if t.len() <= depth {
6636 t.resize(depth + 1, (0, 0));
6637 }
6638 t[depth].0 += 1;
6639 t[depth].1 += prefix_ok as u64;
6640 if depth == 0 && t[0].0 % 128 == 0 {
6641 let line: Vec<String> = t
6642 .iter()
6643 .enumerate()
6644 .map(|(d, (n, k))| {
6645 format!(
6646 "d{}={:.0}%({n})",
6647 d + 1,
6648 100.0 * *k as f64 / (*n).max(1) as f64
6649 )
6650 })
6651 .collect();
6652 eprintln!("mtp-chain: {}", line.join(" "));
6653 }
6654 }
6655
6656 fn mtp_step_hl(
6664 &mut self,
6665 m: &mut MtpModule,
6666 hidden: &[f32],
6667 next_token: u32,
6668 position: usize,
6669 ) -> (Vec<f32>, Vec<f32>) {
6670 #[cfg(target_os = "macos")]
6675 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6676 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6677 self.mtp_graph_mode = Some(true);
6678 return r;
6679 }
6680 if self.mtp_graph_mode == Some(true) {
6681 tracing::error!("mtp Metal graph failed after admission");
6682 self.clear_sequence_state();
6683 self.graph_failed
6684 .store(true, std::sync::atomic::Ordering::Relaxed);
6685 self.cancel
6686 .store(true, std::sync::atomic::Ordering::Relaxed);
6687 return (Vec::new(), Vec::new());
6688 }
6689 self.mtp_graph_mode = Some(false);
6690 }
6691 #[cfg(feature = "gpu")]
6692 if self.mtp_graph_mode != Some(false) {
6693 if !self.mtp_graph_ok(m) {
6694 if self.mtp_graph_mode == Some(true) {
6695 tracing::error!("mtp graph became unavailable after admission");
6700 self.clear_sequence_state();
6701 self.graph_failed
6702 .store(true, std::sync::atomic::Ordering::Relaxed);
6703 self.cancel
6704 .store(true, std::sync::atomic::Ordering::Relaxed);
6705 return (Vec::new(), Vec::new());
6706 }
6707 self.mtp_graph_mode = Some(false);
6708 } else {
6709 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6710 self.mtp_graph_mode = Some(true);
6711 return r;
6712 }
6713 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6714 return (Vec::new(), Vec::new());
6721 }
6722 tracing::error!("mtp graph failed or declined after admission");
6726 self.clear_sequence_state();
6727 self.graph_failed
6728 .store(true, std::sync::atomic::Ordering::Relaxed);
6729 self.cancel
6730 .store(true, std::sync::atomic::Ordering::Relaxed);
6731 return (Vec::new(), Vec::new());
6732 }
6733 }
6734 let e = self.embed_single(next_token);
6738 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6739 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6740 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6741 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6742 let mut x = vec![0.0f32; self.hidden_size];
6743 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6744
6745 let lw = &m.layer;
6747 inference::rms_norm_into(
6748 &x,
6749 &lw.input_norm,
6750 self.rms_eps,
6751 self.norm_style,
6752 &mut self.ws.n1,
6753 );
6754 let attn = match &lw.attn {
6755 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6757 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6758 AttnKind::Full {
6759 wq,
6760 wk,
6761 wv,
6762 wo,
6763 q_norm,
6764 k_norm,
6765 output_gate,
6766 softplus_gate,
6767 bias,
6768 } => {
6769 let mut cfg = self.attn_cfg(position);
6770 cfg.q_norm = q_norm.as_deref();
6771 cfg.k_norm = k_norm.as_deref();
6772 cfg.output_gate = *output_gate;
6773 cfg.softplus_gate = softplus_gate
6774 .as_ref()
6775 .map(|(gate, per_head)| (gate, *per_head));
6776 cfg.bias = bias
6777 .as_ref()
6778 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6779 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6780 }
6781 AttnKind::Linear(_)
6782 | AttnKind::LinearGdn(_)
6783 | AttnKind::ShortConv(_)
6784 | AttnKind::Bounded(_) => {
6785 unreachable!("MTP block is full attention")
6786 }
6787 };
6788 for (i, &a) in attn.iter().enumerate() {
6789 x[i] += a;
6790 }
6791 inference::rms_norm_into(
6792 &x,
6793 &lw.post_norm,
6794 self.rms_eps,
6795 self.norm_style,
6796 &mut self.ws.p1,
6797 );
6798 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6799 for (i, &f) in ffn.iter().enumerate() {
6800 x[i] += f;
6801 }
6802
6803 inference::rms_norm_into(
6804 &x,
6805 &m.final_norm,
6806 self.rms_eps,
6807 self.norm_style,
6808 &mut self.ws.n1,
6809 );
6810 let lg = self.lm_head_forward(&self.ws.n1);
6811 (lg, x)
6812 }
6813
6814 fn mtp_step_h(
6816 &mut self,
6817 m: &mut MtpModule,
6818 hidden: &[f32],
6819 next_token: u32,
6820 position: usize,
6821 ) -> (u32, Vec<f32>) {
6822 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6823 let draft = sampler::argmax(&lg);
6824 attention::recycle_buf(&mut lg);
6825 (draft, x)
6826 }
6827
6828 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6834 match trial {
6835 SpecTrial::Spec { t0, gen0, rounds } => {
6836 let rounds = rounds + 1;
6837 if mon.repeated_zero_acceptance() {
6845 tracing::info!(
6846 "speculation trial: no accepted drafts in two rounds — timing plain"
6847 );
6848 return SpecTrial::Plain {
6849 t0: std::time::Instant::now(),
6850 gen0: generated,
6851 };
6852 }
6853 if rounds >= 5 {
6854 if mon.plain_ms > 0.0 {
6855 let keep = mon.pays();
6856 mon.fails = 0;
6857 tracing::info!(
6858 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6859 mon.tokens,
6860 mon.round_ms,
6861 mon.plain_ms,
6862 if keep { "speculating" } else { "plain" }
6863 );
6864 SpecTrial::Decided {
6865 spec: keep,
6866 recheck_at: if keep { usize::MAX } else { generated + 128 },
6867 }
6868 } else if mon.pays() {
6869 mon.fails = 0;
6874 tracing::info!(
6875 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6876 mon.tokens,
6877 mon.round_ms,
6878 );
6879 SpecTrial::Decided {
6880 spec: true,
6881 recheck_at: usize::MAX,
6882 }
6883 } else {
6884 SpecTrial::Plain {
6885 t0: std::time::Instant::now(),
6886 gen0: generated,
6887 }
6888 }
6889 } else {
6890 SpecTrial::Spec { t0, gen0, rounds }
6891 }
6892 }
6893 SpecTrial::Decided { spec: true, .. } => {
6894 if mon.pays() {
6895 mon.fails = 0;
6896 trial
6897 } else {
6898 mon.fails += 1;
6899 if mon.fails >= 4 {
6900 if mon.plain_ms <= 0.0 {
6901 tracing::info!(
6905 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6906 mon.tokens,
6907 mon.round_ms,
6908 );
6909 return SpecTrial::Plain {
6910 t0: std::time::Instant::now(),
6911 gen0: generated,
6912 };
6913 }
6914 tracing::info!(
6915 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6916 mon.tokens,
6917 mon.round_ms,
6918 mon.plain_ms
6919 );
6920 SpecTrial::Decided {
6921 spec: false,
6922 recheck_at: generated + 128,
6923 }
6924 } else {
6925 trial
6926 }
6927 }
6928 }
6929 other => other,
6930 }
6931 }
6932
6933 fn mtp_kv_id(&self) -> u64 {
6936 self.graph_kv_id | (1u64 << 40)
6937 }
6938
6939 const MTP_LAYER_BASE: usize = 0;
6944
6945 #[cfg(feature = "gpu")]
6952 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6953 self.mtp_graph_mode != Some(true)
6954 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6955 }
6956
6957 #[cfg(feature = "gpu")]
6963 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6964 let mut ok = true;
6965 let mut expected = false;
6966 for li in 0..self.num_layers {
6967 if matches!(
6968 self.weights.layers[self.phys_layer(li)].attn,
6969 AttnKind::Full { .. }
6970 ) {
6971 expected = true;
6972 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6973 }
6974 }
6975 !expected || ok
6976 }
6977
6978 fn graph_gdn_layer_count(&self) -> usize {
6982 (0..self.num_layers)
6983 .filter(|&li| {
6984 matches!(
6985 &self.weights.layers[self.phys_layer(li)].attn,
6986 AttnKind::LinearGdn(_)
6987 )
6988 })
6989 .count()
6990 }
6991
6992 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6995 let e = self.embed_single(next_token);
6996 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6997 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6998 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6999 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7000 let mut x = vec![0.0f32; self.hidden_size];
7001 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7002 x
7003 }
7004
7005 #[cfg(feature = "gpu")]
7008 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7009 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7010 return false;
7011 }
7012 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7013 || !crate::gpu::enabled_here()
7014 || self.attn_softcap > 0.0
7015 || self.attention_heads_per_layer.is_some()
7016 || self.v_head_dim.is_some()
7019 {
7020 return false;
7021 }
7022 matches!(
7023 &m.layer.attn,
7024 AttnKind::Full {
7025 softplus_gate: None,
7026 ..
7027 }
7028 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7029 }
7030
7031 #[cfg(feature = "gpu")]
7035 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7036 if !self.mtp_block_graph_ok(m) {
7037 return false;
7038 }
7039 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7040 return false;
7041 };
7042 let FfnKind::Dense(d) = &m.layer.ffn else {
7043 return false;
7044 };
7045 d.segs.is_empty()
7046 && wq.graph_weight().is_some()
7047 && wk.graph_weight().is_some()
7048 && wv.graph_weight().is_some()
7049 && wo.graph_weight().is_some()
7050 && d.gate_proj.graph_weight().is_some()
7051 && d.up_proj.graph_weight().is_some()
7052 && d.down_proj.graph_weight().is_some()
7053 && self.weights.lm_head.graph_weight().is_some()
7054 }
7055
7056 #[cfg(feature = "gpu")]
7062 fn mtp_step_graph(
7063 &mut self,
7064 m: &mut MtpModule,
7065 hidden: &[f32],
7066 next_token: u32,
7067 position: usize,
7068 ) -> Option<(Vec<f32>, Vec<f32>)> {
7069 if !self.mtp_graph_ok(m) {
7070 return None;
7071 }
7072 let lw = &m.layer;
7073 let AttnKind::Full {
7074 wq,
7075 wk,
7076 wv,
7077 wo,
7078 q_norm,
7079 k_norm,
7080 output_gate,
7081 softplus_gate,
7082 bias,
7083 } = &lw.attn
7084 else {
7085 return None;
7086 };
7087 if softplus_gate.is_some() {
7088 return None;
7089 }
7090 let FfnKind::Dense(d) = &lw.ffn else {
7091 return None;
7092 };
7093 if !d.segs.is_empty() {
7094 return None; }
7096 let mut x = self.mtp_block_input(m, hidden, next_token);
7099 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7100 let (_, i, kind, rs) = t.graph_weight()?;
7101 Some(crate::gpu::GraphW {
7102 idx: i,
7103 kind,
7104 row_scale: rs,
7105 data: &[],
7106 prism: crate::gpu::GraphPrismOp::None,
7107 affine: false,
7108 })
7109 }
7110 let (model, _, _, _) = wq.graph_weight()?;
7111 let model = model.clone();
7112 let (lm_gw, lm_rows) = {
7113 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7114 let rows = if kind == 6 {
7118 self.draft_head_rows(self.weights.lm_head.rows())
7119 } else {
7120 self.weights.lm_head.rows()
7121 };
7122 (
7123 crate::gpu::GraphW {
7124 idx: i,
7125 kind,
7126 row_scale: rs,
7127 data: &[],
7128 prism: crate::gpu::GraphPrismOp::None,
7129 affine: false,
7130 },
7131 rows,
7132 )
7133 };
7134 let layer = crate::gpu::GraphLayer {
7135 input_norm: &lw.input_norm,
7136 attn: crate::gpu::GraphAttn::Full {
7137 wq: gw(wq)?,
7138 wk: gw(wk)?,
7139 wv: gw(wv)?,
7140 wo: gw(wo)?,
7141 q_norm: q_norm.as_deref(),
7142 k_norm: k_norm.as_deref(),
7143 late_qk_norm: self.qk_norm_after_rope,
7144 bias: bias
7145 .as_ref()
7146 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7147 output_gate: *output_gate,
7148 cpu_k: m.kv.k_heads(),
7149 cpu_v: m.kv.v_heads(),
7150 cpu_base: m.kv.base(),
7151 geom: None,
7152 head_gate: None,
7153 },
7154 post_norm: &lw.post_norm,
7155 attn_out_norm: None,
7156 ffn_out_norm: None,
7157 layer_scale: None,
7158 ffn: crate::gpu::GraphFfn::Dense {
7159 gate: gw(&d.gate_proj)?,
7160 up: gw(&d.up_proj)?,
7161 down: gw(&d.down_proj)?,
7162 act: d.act.graph_act()?,
7163 },
7164 };
7165 let nh = self.num_heads;
7166 let (nkv, hd, rd) = self.layer_geom(0);
7167 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7168 let mut logits = Vec::new();
7169 let ok = crate::gpu::forward_token_graph(
7170 &model,
7171 self.mtp_kv_id(),
7172 std::slice::from_ref(&layer),
7173 &[None],
7174 self.o1_epoch,
7175 &self.inv_freq,
7176 &mut x,
7177 nh,
7178 nkv,
7179 hd,
7180 self.attn_scale,
7181 rd,
7182 self.hidden_size,
7183 d.gate_proj.rows(),
7187 position,
7188 self.kv_cache.max_seq_len,
7189 gemma,
7190 self.rms_eps as f32,
7191 Some((&lm_gw, lm_rows)),
7192 &m.final_norm,
7193 &mut logits,
7194 &[],
7195 1,
7196 None,
7197 None,
7198 None,
7199 Self::MTP_LAYER_BASE,
7200 true,
7201 );
7202 match ok {
7203 crate::gpu::TokenGraphOutcome::Completed => {}
7204 crate::gpu::TokenGraphOutcome::Declined => return None,
7205 crate::gpu::TokenGraphOutcome::Failed => {
7206 self.clear_sequence_state();
7210 self.graph_failed
7211 .store(true, std::sync::atomic::Ordering::Relaxed);
7212 self.cancel
7213 .store(true, std::sync::atomic::Ordering::Relaxed);
7214 return None;
7215 }
7216 }
7217 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7221 Some((logits, x))
7222 }
7223
7224 #[cfg(feature = "gpu")]
7232 fn mtp_warm_graph(
7233 &mut self,
7234 m: &mut MtpModule,
7235 pairs: &[(&[f32], u32)],
7236 first_pos: usize,
7237 ) -> crate::gpu::BatchGraphOutcome {
7238 if pairs.is_empty() {
7239 return crate::gpu::BatchGraphOutcome::Completed;
7240 }
7241 if !self.mtp_block_graph_ok(m) {
7242 return crate::gpu::BatchGraphOutcome::Declined;
7243 }
7244 let hs = self.hidden_size;
7245 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7248 for (h, t) in pairs {
7249 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7250 }
7251 let lw = &m.layer;
7252 let AttnKind::Full {
7253 wq,
7254 wk,
7255 wv,
7256 wo,
7257 q_norm,
7258 k_norm,
7259 output_gate,
7260 bias,
7261 ..
7262 } = &lw.attn
7263 else {
7264 return crate::gpu::BatchGraphOutcome::Declined;
7265 };
7266 let FfnKind::Dense(d) = &lw.ffn else {
7267 return crate::gpu::BatchGraphOutcome::Declined;
7268 };
7269 if !d.segs.is_empty() {
7270 return crate::gpu::BatchGraphOutcome::Declined; }
7272 let (
7275 AttnKind::Full {
7276 softplus_gate: None,
7277 ..
7278 },
7279 Some(gact),
7280 ) = (&lw.attn, d.act.graph_act())
7281 else {
7282 return crate::gpu::BatchGraphOutcome::Declined;
7283 };
7284 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7285 let (_, i, kind, rs) = t.graph_weight()?;
7286 Some(crate::gpu::GraphW {
7287 idx: i,
7288 kind,
7289 row_scale: rs,
7290 data: &[],
7291 prism: crate::gpu::GraphPrismOp::None,
7292 affine: false,
7293 })
7294 }
7295 let Some((model, _, _, _)) = wq.graph_weight() else {
7296 return crate::gpu::BatchGraphOutcome::Declined;
7297 };
7298 let model = model.clone();
7299 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7300 gw(wq),
7301 gw(wk),
7302 gw(wv),
7303 gw(wo),
7304 gw(&d.gate_proj),
7305 gw(&d.up_proj),
7306 gw(&d.down_proj),
7307 ) else {
7308 return crate::gpu::BatchGraphOutcome::Declined;
7309 };
7310 let layer = crate::gpu::GraphLayer {
7311 input_norm: &lw.input_norm,
7312 attn: crate::gpu::GraphAttn::Full {
7313 wq: gwq,
7314 wk: gwk,
7315 wv: gwv,
7316 wo: gwo,
7317 q_norm: q_norm.as_deref(),
7318 k_norm: k_norm.as_deref(),
7319 late_qk_norm: self.qk_norm_after_rope,
7320 bias: bias
7321 .as_ref()
7322 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7323 output_gate: *output_gate,
7324 cpu_k: m.kv.k_heads(),
7325 cpu_v: m.kv.v_heads(),
7326 cpu_base: m.kv.base(),
7327 geom: None,
7328 head_gate: None,
7329 },
7330 post_norm: &lw.post_norm,
7331 attn_out_norm: None,
7332 ffn_out_norm: None,
7333 layer_scale: None,
7334 ffn: crate::gpu::GraphFfn::Dense {
7335 gate: gg,
7336 up: gu,
7337 down: gd,
7338 act: gact,
7339 },
7340 };
7341 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7342 let nh = self.num_heads;
7343 let (nkv, hd, rd) = self.layer_geom(0);
7344 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7345 crate::gpu::forward_batch_graph(
7346 &model,
7347 self.mtp_kv_id(),
7348 std::slice::from_ref(&layer),
7349 &self.inv_freq,
7350 &mut hiddens,
7351 nh,
7352 nkv,
7353 hd,
7354 rd,
7355 hs,
7356 d.gate_proj.rows(),
7359 &positions,
7360 self.kv_cache.max_seq_len,
7361 gemma,
7362 self.rms_eps as f32,
7363 self.attn_scale,
7364 pairs.len(),
7365 &[],
7366 0,
7367 None,
7368 None,
7369 )
7370 }
7371
7372 #[cfg(feature = "gpu")]
7379 fn mtp_warm_graph_fallback(
7380 &mut self,
7381 m: &mut MtpModule,
7382 pairs: &[(&[f32], u32)],
7383 first_pos: usize,
7384 ) -> bool {
7385 if pairs.is_empty() {
7386 return true;
7387 }
7388 let graphable = self.mtp_block_graph_ok(m);
7389 if !graphable {
7390 if self.mtp_graph_mode == Some(true) {
7394 return false;
7395 }
7396 self.mtp_graph_mode = Some(false);
7397 for (j, (h, t)) in pairs.iter().enumerate() {
7398 self.mtp_warm(m, h, *t, first_pos + j);
7399 }
7400 return true;
7401 }
7402
7403 for (j, (h, t)) in pairs.iter().enumerate() {
7408 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7409 return false;
7410 }
7411 }
7412 self.mtp_graph_mode = Some(true);
7413 true
7414 }
7415
7416 #[cfg(feature = "gpu")]
7421 fn mtp_warm_prefill_pairs(
7422 &mut self,
7423 m: &mut MtpModule,
7424 pairs: &[(&[f32], u32)],
7425 first_pos: usize,
7426 ) -> Result<(), &'static str> {
7427 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7432 if self.mtp_graph_mode == Some(true) {
7433 return Err("MTP token graph became unavailable after admission");
7434 }
7435 self.mtp_graph_mode = Some(false);
7436 for (j, (h, t)) in pairs.iter().enumerate() {
7437 self.mtp_warm(m, h, *t, first_pos + j);
7438 }
7439 return Ok(());
7440 }
7441 match self.mtp_warm_graph(m, pairs, first_pos) {
7442 crate::gpu::BatchGraphOutcome::Completed => {
7443 if !pairs.is_empty() {
7444 self.mtp_graph_mode = Some(true);
7445 }
7446 Ok(())
7447 }
7448 crate::gpu::BatchGraphOutcome::Declined => {
7449 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7450 Ok(())
7451 } else {
7452 Err("MTP warm-up fallback failed after device admission")
7453 }
7454 }
7455 crate::gpu::BatchGraphOutcome::Failed => {
7456 Err("MTP warm batch graph failed after admission")
7457 }
7458 }
7459 }
7460
7461 #[cfg(not(feature = "gpu"))]
7462 fn mtp_warm_prefill_pairs(
7463 &mut self,
7464 m: &mut MtpModule,
7465 pairs: &[(&[f32], u32)],
7466 first_pos: usize,
7467 ) -> Result<(), &'static str> {
7468 for (j, (h, t)) in pairs.iter().enumerate() {
7469 self.mtp_warm(m, h, *t, first_pos + j);
7470 }
7471 Ok(())
7472 }
7473
7474 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7478 let e = self.embed_single(next_token);
7479 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7480 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7481 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7482 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7483 let mut x = vec![0.0f32; self.hidden_size];
7484 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7485 inference::rms_norm_into(
7486 &x,
7487 &m.layer.input_norm,
7488 self.rms_eps,
7489 self.norm_style,
7490 &mut self.ws.n1,
7491 );
7492 let attn = match &m.layer.attn {
7493 AttnKind::Full {
7494 wq,
7495 wk,
7496 wv,
7497 wo,
7498 q_norm,
7499 k_norm,
7500 output_gate,
7501 softplus_gate,
7502 bias,
7503 } => {
7504 let mut cfg = self.attn_cfg(position);
7505 cfg.q_norm = q_norm.as_deref();
7506 cfg.k_norm = k_norm.as_deref();
7507 cfg.output_gate = *output_gate;
7508 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7509 cfg.bias = bias
7510 .as_ref()
7511 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7512 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7513 }
7514 _ => return,
7515 };
7516 let _ = attn;
7517 }
7518
7519 #[cfg(feature = "gpu")]
7526 #[allow(clippy::too_many_arguments)]
7527 fn graph_spec_step(
7528 &mut self,
7529 m: &mut MtpModule,
7530 hidden: &[f32],
7531 t_next: u32,
7532 next_pos: usize,
7533 drafted: &mut usize,
7534 accepted: &mut usize,
7535 all_ids: &mut Vec<u32>,
7539 room: usize,
7544 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7545 #[cfg(target_os = "macos")]
7556 let metal_native = crate::gpu::q1_force();
7557 #[cfg(not(target_os = "macos"))]
7558 let metal_native = false;
7559 #[cfg(feature = "gpu")]
7560 let k_default = if metal_native {
7561 7
7564 } else if crate::gpu_wgpu::verify_i8_on() {
7565 5
7566 } else {
7567 4
7568 };
7569 #[cfg(not(feature = "gpu"))]
7570 let k_default = 4;
7571 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7572 .ok()
7573 .and_then(|v| v.parse().ok())
7574 .filter(|&v| (1..=8).contains(&v));
7575 let (k_start, k_max) = if metal_native {
7580 (7, 7)
7581 } else {
7582 (3, k_default.max(5))
7583 };
7584 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7585 let k_spec = k_full.min(room).max(1);
7586 let k_capped = k_spec < k_full;
7589 if next_pos == 0 {
7590 return None;
7591 }
7592 let t_round = std::time::Instant::now();
7593 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7609 let sub0 = subs();
7610 let cfg = self.sampler_config.clone();
7615 let penalized = !(cfg.repetition_penalty == 1.0
7616 && cfg.presence_penalty == 0.0
7617 && cfg.suppress_tokens.is_empty());
7618 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7623 let sampling = cfg.temperature >= 1e-6;
7624 let sparse = sampling && sampler::sparse_ok(&cfg);
7630 let base_len = all_ids.len();
7631 if sampling && !sparse && self.spec_q.len() < k_spec {
7632 self.spec_q.resize_with(k_spec, Vec::new);
7633 }
7634 if sparse && self.spec_qs.len() < k_spec {
7635 self.spec_qs.resize_with(k_spec, Vec::new);
7636 }
7637 let mut drafts = Vec::with_capacity(k_spec);
7642 let mut hx = hidden.to_vec();
7643 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7646 spec_stamp("pro");
7647 #[cfg(target_os = "macos")]
7653 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7654 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7655 Ok(ids) => {
7656 self.mtp_graph_mode = Some(true);
7657 drafts = ids;
7658 }
7659 Err(true) => {
7660 tracing::error!("mtp Metal draft chain failed after commit");
7661 self.clear_sequence_state();
7662 self.graph_failed
7663 .store(true, std::sync::atomic::Ordering::Relaxed);
7664 self.cancel
7665 .store(true, std::sync::atomic::Ordering::Relaxed);
7666 return None;
7667 }
7668 Err(false) => {}
7669 }
7670 }
7671 for j in drafts.len()..k_spec {
7672 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7673 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7674 if spec_dbg {
7675 let saved = self.mtp_graph_mode;
7676 self.mtp_graph_mode = Some(false);
7677 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7678 self.mtp_graph_mode = saved;
7679 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7680 return None;
7681 }
7682 m.kv.truncate_last(1);
7683 dbg_ref = Some(r);
7684 }
7685 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7686 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7687 return None;
7688 }
7689 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7690 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7691 let dl = lg
7692 .iter()
7693 .zip(&lg_cpu)
7694 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7695 let dh = hj
7696 .iter()
7697 .zip(&h_cpu)
7698 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7699 eprintln!(
7700 "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 {}",
7701 next_pos - 1 + j,
7702 sampler::argmax(&lg_cpu),
7703 sampler::argmax(&lg),
7704 n(&h_cpu),
7705 n(&hj),
7706 m.kv.seq_len
7707 );
7708 }
7709 let dj = if sparse {
7710 let mut q = std::mem::take(&mut self.spec_qs[j]);
7711 let ok = sampler::sparse_distribution_into(
7712 &lg,
7713 &cfg,
7714 all_ids,
7715 &mut self.sampler_scratch,
7716 self.pool.as_deref(),
7717 &mut q,
7718 );
7719 let d = if ok {
7720 sampler::draw_sparse(&q, &mut self.rng)
7721 } else {
7722 let t = sampler::argmax(&lg);
7724 q.clear();
7725 q.push((t, 1.0));
7726 t
7727 };
7728 self.spec_qs[j] = q;
7729 all_ids.push(d);
7730 d
7731 } else if sampling {
7732 let mut q = std::mem::take(&mut self.spec_q[j]);
7733 sampler::distribution_into(
7734 &lg,
7735 &cfg,
7736 all_ids,
7737 &mut self.sampler_scratch,
7738 self.pool.as_deref(),
7739 &mut q,
7740 );
7741 let d = sampler::draw(&q, &mut self.rng);
7742 self.spec_q[j] = q;
7743 all_ids.push(d); d
7745 } else if greedy_pen {
7746 let d = sampler::argmax_penalized(
7747 &lg,
7748 &cfg,
7749 all_ids,
7750 &mut self.sampler_scratch,
7751 self.pool.as_deref(),
7752 );
7753 all_ids.push(d);
7754 d
7755 } else {
7756 sampler::argmax(&lg)
7757 };
7758 attention::recycle_buf(&mut lg);
7759 drafts.push(dj);
7760 hx = hj;
7761 spec_stamp("d.pick");
7762 }
7763 all_ids.truncate(base_len);
7764 *drafted += k_spec;
7765 let t_draft = t_round.elapsed();
7766 let sub_draft = subs();
7767 let b = k_spec + 1;
7770 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7771 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7772 let e = self.embed_single(t);
7773 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7774 }
7775 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7776 spec_stamp("v.emb");
7777 let (lm_gw, lm_rows) = {
7778 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7779 (
7780 crate::gpu::GraphW {
7781 idx: i,
7782 kind,
7783 row_scale: rs,
7784 data: &[],
7785 prism: crate::gpu::GraphPrismOp::None,
7786 affine: false,
7787 },
7788 self.weights.lm_head.rows(),
7789 )
7790 };
7791 let mut logits = Vec::new();
7792 let final_norm = self.weights.final_norm.clone();
7793 #[cfg(target_os = "macos")]
7802 let greedy_dev = metal_native
7803 && !sampling
7804 && !greedy_pen
7805 && !self.confidence_on
7806 && self.final_softcap.is_none()
7807 && self.vocab_size == lm_rows
7813 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7814 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7815 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7816 #[cfg(not(target_os = "macos"))]
7817 let greedy_dev = false;
7818 let mut dev_ids: Vec<u32> = Vec::new();
7819 #[cfg(target_os = "macos")]
7820 let verify_outcome = if metal_native {
7821 let lm = self.weights.lm_head.q1_parts()?;
7822 let n_score = self.vocab_size.min(lm_rows);
7823 self.try_batch_graph_metal(
7824 &mut hiddens,
7825 &positions,
7826 b,
7827 Some((lm, &final_norm, &mut logits)),
7828 if greedy_dev {
7829 Some((n_score, &mut dev_ids))
7830 } else {
7831 None
7832 },
7833 )
7834 } else {
7835 self.try_batch_graph_wgpu(
7836 &mut hiddens,
7837 &positions,
7838 b,
7839 Some(crate::gpu::SpecTail {
7840 lm: lm_gw,
7841 lm_rows,
7842 final_norm: &final_norm,
7843 logits_out: &mut logits,
7844 pick: None,
7845 }),
7846 )
7847 };
7848 #[cfg(not(target_os = "macos"))]
7849 let verify_outcome = self.try_batch_graph_wgpu(
7850 &mut hiddens,
7851 &positions,
7852 b,
7853 Some(crate::gpu::SpecTail {
7854 lm: lm_gw,
7855 lm_rows,
7856 final_norm: &final_norm,
7857 logits_out: &mut logits,
7858 pick: None,
7859 }),
7860 );
7861 match verify_outcome {
7862 crate::gpu::BatchGraphOutcome::Completed => {}
7863 crate::gpu::BatchGraphOutcome::Declined => {
7864 m.kv.truncate_last(k_spec);
7868 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7869 self.clear_sequence_state();
7870 self.graph_failed
7871 .store(true, std::sync::atomic::Ordering::Relaxed);
7872 self.cancel
7873 .store(true, std::sync::atomic::Ordering::Relaxed);
7874 tracing::error!("MTP graph mirror rewind failed after verify decline");
7875 }
7876 return None;
7877 }
7878 crate::gpu::BatchGraphOutcome::Failed => {
7879 self.clear_sequence_state();
7883 self.graph_failed
7884 .store(true, std::sync::atomic::Ordering::Relaxed);
7885 self.cancel
7886 .store(true, std::sync::atomic::Ordering::Relaxed);
7887 tracing::error!("MTP verify batch graph failed after admission");
7888 return None;
7889 }
7890 }
7891 #[cfg(target_os = "macos")]
7897 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7898 let snap: Vec<Vec<f32>> = self
7899 .kv_cache
7900 .layers
7901 .iter()
7902 .map(|l| l.linear_state.clone())
7903 .collect();
7904 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7905 let toks: Vec<u32> = std::iter::once(t_next)
7906 .chain(drafts.iter().copied())
7907 .collect();
7908 let want_save = self.graph_want_logits;
7909 self.graph_want_logits = false;
7910 for (i, &t) in toks.iter().enumerate() {
7911 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7912 let _ = self.graph_logits.take();
7913 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7917 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7918 }
7919 let ref_lg = self.logits_from_hidden(&hi);
7920 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7921 let ra = sampler::argmax(&ref_lg);
7922 let va = sampler::argmax(row);
7923 let mut md = 0f32;
7924 let mut rms = 0f64;
7925 for j in 0..lm_rows.min(ref_lg.len()) {
7926 let d = (ref_lg[j] - row[j]).abs();
7927 md = md.max(d);
7928 rms += (d as f64) * (d as f64);
7929 }
7930 let mut hd = 0f32;
7931 for j in 0..self.hidden_size {
7932 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7933 }
7934 eprintln!(
7935 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7936 next_pos + i,
7937 if ra == va { "OK" } else { "MISMATCH" },
7938 (rms / lm_rows as f64).sqrt()
7939 );
7940 }
7941 self.graph_want_logits = want_save;
7942 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7945 if l.linear_state.len() == st.len() {
7946 l.linear_state.copy_from_slice(&st);
7947 } else {
7948 l.linear_state = st;
7949 }
7950 }
7951 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7952 let extra = l.seq_len.saturating_sub(n0);
7953 if extra > 0 {
7954 l.truncate_last(extra);
7955 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7956 }
7957 }
7958 }
7959 let t_verify = t_round.elapsed();
7960 let sub_verify = subs();
7961 let mut a = 0usize;
7966 let mut forced: Option<u32> = None;
7967 let ids: Vec<u32> = if sparse {
7968 let mut p = std::mem::take(&mut self.spec_ps);
7969 let mut res = std::mem::take(&mut self.spec_ress);
7970 while a < k_spec {
7971 let ok = sampler::sparse_distribution_into(
7972 &logits[a * lm_rows..(a + 1) * lm_rows],
7973 &cfg,
7974 all_ids,
7975 &mut self.sampler_scratch,
7976 self.pool.as_deref(),
7977 &mut p,
7978 );
7979 if !ok {
7980 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7981 p.clear();
7982 p.push((t, 1.0));
7983 }
7984 match sampler::spec_accept_or_correct_sparse(
7985 &p,
7986 &self.spec_qs[a],
7987 drafts[a],
7988 &mut self.rng,
7989 &mut res,
7990 ) {
7991 None => {
7992 all_ids.push(drafts[a]);
7993 a += 1;
7994 }
7995 Some(c) => {
7996 forced = Some(c);
7997 break;
7998 }
7999 }
8000 }
8001 all_ids.truncate(base_len);
8002 self.spec_ps = p;
8003 self.spec_ress = res;
8004 drafts.clone()
8005 } else if sampling {
8006 let mut p = std::mem::take(&mut self.spec_p);
8007 let mut res = std::mem::take(&mut self.spec_res);
8008 while a < k_spec {
8009 sampler::distribution_into(
8010 &logits[a * lm_rows..(a + 1) * lm_rows],
8011 &cfg,
8012 all_ids,
8013 &mut self.sampler_scratch,
8014 self.pool.as_deref(),
8015 &mut p,
8016 );
8017 match sampler::spec_accept_or_correct(
8018 &p,
8019 &self.spec_q[a],
8020 drafts[a],
8021 &mut self.rng,
8022 &mut res,
8023 self.pool.as_deref(),
8024 ) {
8025 None => {
8026 all_ids.push(drafts[a]);
8027 a += 1;
8028 }
8029 Some(c) => {
8030 forced = Some(c);
8031 break;
8032 }
8033 }
8034 }
8035 all_ids.truncate(base_len);
8036 self.spec_p = p;
8037 self.spec_res = res;
8038 drafts.clone()
8040 } else if greedy_pen {
8041 let mut ids: Vec<u32> = Vec::with_capacity(b);
8045 for i in 0..b {
8046 let t = sampler::argmax_penalized(
8047 &logits[i * lm_rows..(i + 1) * lm_rows],
8048 &cfg,
8049 all_ids,
8050 &mut self.sampler_scratch,
8051 self.pool.as_deref(),
8052 );
8053 ids.push(t);
8054 if i < k_spec && t == drafts[i] {
8055 all_ids.push(t);
8056 } else {
8057 break;
8058 }
8059 }
8060 all_ids.truncate(base_len);
8061 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8062 a += 1;
8063 }
8064 ids
8067 } else if greedy_dev && dev_ids.len() == b {
8068 let ids = std::mem::take(&mut dev_ids);
8069 while a < k_spec && ids[a] == drafts[a] {
8070 a += 1;
8071 }
8072 ids
8073 } else {
8074 if logits.len() < b * lm_rows {
8075 self.clear_sequence_state();
8078 self.graph_failed
8079 .store(true, std::sync::atomic::Ordering::Relaxed);
8080 self.cancel
8081 .store(true, std::sync::atomic::Ordering::Relaxed);
8082 tracing::error!("Metal verify returned neither logits nor argmax ids");
8083 return None;
8084 }
8085 let ids: Vec<u32> = (0..b)
8086 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8087 .collect();
8088 while a < k_spec && ids[a] == drafts[a] {
8089 a += 1;
8090 }
8091 ids
8092 };
8093 spec_stamp("acc");
8094 if spec_dbg {
8095 eprintln!(
8096 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8097 drafts, ids
8098 );
8099 }
8100 #[cfg(target_os = "macos")]
8104 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8105 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8106 {
8107 let snap: Vec<Vec<f32>> = self
8108 .kv_cache
8109 .layers
8110 .iter()
8111 .map(|l| l.linear_state.clone())
8112 .collect();
8113 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8114 let toks: Vec<u32> = std::iter::once(t_next)
8115 .chain(drafts.iter().copied())
8116 .collect();
8117 let want_save = self.graph_want_logits;
8118 self.graph_want_logits = false;
8119 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8120 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8121 let _ = self.graph_logits.take();
8122 }
8123 self.graph_want_logits = want_save;
8124 let plain_states: Vec<Vec<f32>> = self
8125 .kv_cache
8126 .layers
8127 .iter()
8128 .map(|l| l.linear_state.clone())
8129 .collect();
8130 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8131 let mut rows = Vec::new();
8132 for (li, (l, n0)) in self
8133 .kv_cache
8134 .layers
8135 .iter_mut()
8136 .zip(attn_lens.iter())
8137 .enumerate()
8138 {
8139 let extra = l.seq_len.saturating_sub(*n0);
8140 if extra > 0 {
8141 let mut kk = Vec::new();
8142 let mut vv = Vec::new();
8143 for g in 0..nkv {
8144 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8145 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8146 }
8147 rows.push((li, kk, vv));
8148 l.truncate_last(extra);
8149 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8150 }
8151 }
8152 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8153 if l.linear_state.len() == st.len() {
8154 l.linear_state.copy_from_slice(&st);
8155 } else {
8156 l.linear_state = st;
8157 }
8158 }
8159 Some((plain_states, rows))
8160 } else {
8161 None
8162 };
8163 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8164 #[cfg(target_os = "macos")]
8173 let mut warm_pending: Option<MetalWarmPending> = None;
8174 #[cfg(target_os = "macos")]
8175 if metal_native {
8176 m.kv.truncate_last(k_spec.saturating_sub(1));
8177 if self.mtp_graph_mode == Some(true) {
8178 crate::gpu_metal::kv_mirror_set_stored(
8181 self.mtp_kv_id(),
8182 Self::MTP_LAYER_BASE,
8183 m.kv.seq_len,
8184 );
8185 if !warm_off && a > 0 {
8186 let pairs: Vec<(&[f32], u32)> = (0..a)
8187 .map(|j| {
8188 (
8189 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8190 ids[j],
8191 )
8192 })
8193 .collect();
8194 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8195 }
8196 }
8197 spec_stamp("c.wsub");
8198 }
8199 #[cfg(target_os = "macos")]
8201 if metal_native {
8202 if !self.metal_verify_commit(a) {
8205 self.clear_sequence_state();
8206 self.graph_failed
8207 .store(true, std::sync::atomic::Ordering::Relaxed);
8208 self.cancel
8209 .store(true, std::sync::atomic::Ordering::Relaxed);
8210 tracing::error!("Metal verify state/KV handoff failed after admission");
8211 return None;
8212 }
8213 if let Some((plain_states, rows)) = commit_ref {
8214 crate::gpu_metal::queue_fence();
8215 let _ = crate::gpu_metal::wait_replay();
8218 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8219 let mut worst_s = 0f32;
8220 let mut worst_li = 0usize;
8221 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8222 if l.linear_state.len() != ps.len() || ps.is_empty() {
8223 continue;
8224 }
8225 let d = l
8226 .linear_state
8227 .iter()
8228 .zip(ps)
8229 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8230 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8231 let rel = d / n.max(1e-6);
8232 if rel > worst_s {
8233 worst_s = rel;
8234 worst_li = li;
8235 }
8236 }
8237 let mut worst_k = 0f32;
8238 for (li, kk, vv) in &rows {
8239 let l = &self.kv_cache.layers[*li];
8240 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8241 let mut ck = Vec::new();
8242 let mut cv = Vec::new();
8243 for g in 0..nkv {
8244 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8245 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8246 }
8247 if ck.len() == kk.len() {
8248 let dk = ck
8249 .iter()
8250 .zip(kk)
8251 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8252 let dv = cv
8253 .iter()
8254 .zip(vv)
8255 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8256 worst_k = worst_k.max(dk).max(dv);
8257 } else {
8258 eprintln!(
8259 "commit-check L{li}: kv row count mismatch {} vs {}",
8260 ck.len(),
8261 kk.len()
8262 );
8263 }
8264 }
8265 eprintln!(
8266 "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}"
8267 );
8268 }
8269 }
8270 if !metal_native && a + 1 < b {
8271 let expected_gdn_layers = self.graph_gdn_layer_count();
8272 if expected_gdn_layers > 0
8273 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8274 {
8275 self.clear_sequence_state();
8276 self.graph_failed
8277 .store(true, std::sync::atomic::Ordering::Relaxed);
8278 self.cancel
8279 .store(true, std::sync::atomic::Ordering::Relaxed);
8280 tracing::error!("GDN speculative restore failed after verify");
8281 return None;
8282 }
8283 }
8284 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8285 self.clear_sequence_state();
8290 self.graph_failed
8291 .store(true, std::sync::atomic::Ordering::Relaxed);
8292 self.cancel
8293 .store(true, std::sync::atomic::Ordering::Relaxed);
8294 tracing::error!("trunk graph KV rewind failed after speculative verify");
8295 return None;
8296 }
8297 *accepted += a;
8298 if !metal_native {
8309 m.kv.truncate_last(k_spec.saturating_sub(1));
8311 }
8312 spec_stamp("c.trunc");
8313 if !metal_native
8314 && self.mtp_graph_mode == Some(true)
8315 && !self.rewind_mtp_graph_mirror(next_pos)
8316 {
8317 self.clear_sequence_state();
8321 self.graph_failed
8322 .store(true, std::sync::atomic::Ordering::Relaxed);
8323 self.cancel
8324 .store(true, std::sync::atomic::Ordering::Relaxed);
8325 tracing::error!("MTP graph mirror rewind failed after verify commit");
8326 return None;
8327 }
8328 if !warm_off && a > 0 {
8329 let mut warmed = false;
8332 #[cfg(target_os = "macos")]
8333 if metal_native && self.mtp_graph_mode == Some(true) {
8334 warmed = match warm_pending.take() {
8338 Some(p) => self.mtp_warm_batch_finish(m, p),
8339 None => false,
8340 };
8341 if !warmed {
8342 warmed = true;
8343 for j in 0..a {
8344 let row =
8345 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8346 if self
8347 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8348 .is_none()
8349 {
8350 warmed = false;
8351 break;
8352 }
8353 }
8354 }
8355 }
8356 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8357 let rows: Vec<Vec<f32>> = (0..a)
8358 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8359 .collect();
8360 let pairs: Vec<(&[f32], u32)> = rows
8361 .iter()
8362 .zip(ids.iter())
8363 .map(|(r, &t)| (r.as_slice(), t))
8364 .collect();
8365 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8366 Ok(()) => warmed = true,
8367 Err(err) => {
8368 tracing::error!("{err}");
8374 self.clear_sequence_state();
8375 self.graph_failed
8376 .store(true, std::sync::atomic::Ordering::Relaxed);
8377 self.cancel
8378 .store(true, std::sync::atomic::Ordering::Relaxed);
8379 return None;
8380 }
8381 }
8382 }
8383 if !warmed {
8384 for j in 0..a {
8385 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8386 let row = row.to_vec();
8387 self.mtp_warm(m, &row, ids[j], next_pos + j);
8388 }
8389 }
8390 }
8391 spec_stamp("c.warm");
8395 if let Some(c) = forced {
8396 self.spec_forced = Some(c);
8397 self.graph_logits = None;
8398 } else if greedy_dev && logits.is_empty() {
8399 self.spec_forced = Some(ids[a]);
8402 self.graph_logits = None;
8403 } else {
8404 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8405 row.resize(self.vocab_size, 0.0);
8406 if let Some(c) = self.final_softcap {
8407 for l in row.iter_mut() {
8408 *l = c * (*l / c).tanh();
8409 }
8410 }
8411 self.graph_logits = Some(row);
8412 }
8413 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8414 spec_stamp("c.row");
8415 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8421 let end = subs();
8422 eprintln!(
8423 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8424 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8425 t_draft.as_secs_f64() * 1e3,
8426 sub_draft - sub0,
8427 (t_verify - t_draft).as_secs_f64() * 1e3,
8428 sub_verify - sub_draft,
8429 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8430 end - sub_verify,
8431 self.draft_full_streak,
8432 );
8433 }
8434 if k_env.is_none() && !metal_native && !k_capped {
8439 let f = a as f32 / k_spec.max(1) as f32;
8443 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8444 let mut k_next = k_spec;
8445 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8446 k_next = k_spec + 1;
8447 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8448 k_next = k_spec - 1;
8449 }
8450 if k_next != k_spec {
8451 self.spec_acc_ewma = 0.6;
8452 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8453 eprintln!("spec-k: {k_spec} → {k_next}");
8454 }
8455 }
8456 self.spec_k_adapt = Some(k_next);
8457 }
8458 spec_stamp("end");
8459 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8460 }
8461
8462 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8471 if !self.pair_supported() {
8472 return (0.0, 0.0);
8473 }
8474 if self.gdn_cfg.is_some()
8482 && self.graph_prefill_preferred()
8483 && self.automatic_gpu_prefix().is_some()
8484 {
8485 tracing::info!(
8486 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8487 );
8488 return (0.0, 0.0);
8489 }
8490 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8497 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8498 let emb1 = self.embed_single(1);
8499 let emb2 = self.embed_single(2);
8500 let pos = self.kv_cache.seq_len();
8501
8502 let t0 = std::time::Instant::now();
8503 for _ in 0..iters {
8504 let _ = self.forward_layers(&emb1, pos, None);
8505 let _ = self.forward_layers(&emb2, pos + 1, None);
8506 for l in &mut self.kv_cache.layers {
8507 l.truncate_last(2);
8508 }
8509 }
8510 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8511
8512 let t1 = std::time::Instant::now();
8513 for _ in 0..iters {
8514 let _ = self.forward_pair(&emb1, &emb2, pos);
8515 for l in &mut self.kv_cache.layers {
8516 l.truncate_last(2);
8517 }
8518 }
8519 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8520 match graph_env {
8521 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8522 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8523 }
8524 (singles_ms, pair_ms)
8525 }
8526
8527 fn pair_supported(&self) -> bool {
8535 !self.weights.layers.is_empty()
8542 && !self.per_token_stack()
8543 && !self
8544 .weights
8545 .layers
8546 .iter()
8547 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8548 }
8549
8550 fn forward_pair(
8551 &mut self,
8552 emb1: &[f32],
8553 emb2: &[f32],
8554 position: usize,
8555 ) -> (Vec<f32>, Vec<f32>) {
8556 self.mimo_moe_prepare();
8559 let mut h1 = emb1.to_vec();
8560 let mut h2 = emb2.to_vec();
8561 let (_nkv, _hd, hs, _rd, eps) = (
8562 self.num_kv_heads,
8563 self.head_dim,
8564 self.hidden_size,
8565 self.rotary_dim,
8566 self.rms_eps,
8567 );
8568 let pool = self.pool.clone();
8569 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8576
8577 for li in 0..self.num_layers {
8578 let _capacity_tail = automatic_gpu_prefix
8579 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8580 .map(|_| crate::gpu::enter_cpu_scope());
8581 crate::gpu::set_layer(li as i64);
8584 let lw = &self.weights.layers[self.phys_layer(li)];
8585 inference::rms_norm_into(
8588 &h1,
8589 &lw.input_norm,
8590 self.rms_eps,
8591 self.norm_style,
8592 &mut self.ws.n1,
8593 );
8594 inference::rms_norm_into(
8595 &h2,
8596 &lw.input_norm,
8597 self.rms_eps,
8598 self.norm_style,
8599 &mut self.ws.n2,
8600 );
8601
8602 let (a1, a2) = match &lw.attn {
8603 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8604 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8605 AttnKind::Bounded(w) => {
8606 let rope = self
8609 .bounded_rope
8610 .clone()
8611 .expect("bounded layer without an installed rotation table");
8612 let cfg = crate::bounded::BoundedAttnCfg {
8613 num_heads: self.num_heads,
8614 num_kv_heads: self.num_kv_heads,
8615 head_dim: self.head_dim,
8616 hidden_size: hs,
8617 scale: self.attn_scale,
8618 rope: &rope,
8619 pool: pool.as_deref(),
8620 };
8621 let a1 = crate::bounded::bounded_attention(
8622 &self.ws.n1,
8623 w,
8624 &mut self.kv_cache.layers[li],
8625 &cfg,
8626 );
8627 let a2 = crate::bounded::bounded_attention(
8628 &self.ws.n2,
8629 w,
8630 &mut self.kv_cache.layers[li],
8631 &cfg,
8632 );
8633 (a1, a2)
8634 }
8635 AttnKind::Linear(w) => {
8636 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8637 let layer = &mut self.kv_cache.layers[li];
8638 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8639 vmf_phase_pair(
8640 &self.ws.n1,
8641 &self.ws.n2,
8642 w,
8643 &cfg,
8644 state,
8645 scratch,
8646 self.pool.as_deref(),
8647 )
8648 }
8649 AttnKind::LinearGdn(w) => {
8650 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8651 let layer = &mut self.kv_cache.layers[li];
8652 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8653 gdn_pair(
8654 &self.ws.n1,
8655 &self.ws.n2,
8656 w,
8657 &cfg,
8658 state,
8659 scratch,
8660 self.pool.as_deref(),
8661 )
8662 }
8663 AttnKind::ShortConv(w) => {
8664 let cfg = self
8665 .short_conv_cfg
8666 .expect("short-conv layer without short_conv_cfg");
8667 let layer = &mut self.kv_cache.layers[li];
8668 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8669 short_conv_pair(
8670 &self.ws.n1,
8671 &self.ws.n2,
8672 w,
8673 &cfg,
8674 state,
8675 scratch,
8676 self.pool.as_deref(),
8677 )
8678 }
8679 AttnKind::Full {
8680 wq,
8681 wk,
8682 wv,
8683 wo,
8684 q_norm,
8685 k_norm,
8686 output_gate,
8687 softplus_gate,
8688 bias,
8689 } => {
8690 let inv_freq_l = self.layer_inv_freq(li);
8691 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8692 let cfg = QwenAttnCfg {
8693 num_heads: self.layer_num_heads(li),
8694 num_kv_heads: nkv_l,
8695 head_dim: hd_l,
8696 hidden_size: hs,
8697 position,
8698 inv_freq: &inv_freq_l,
8699 rotary_dim: rd_l,
8700 scale: self.attn_scale,
8701 softcap: self.attn_softcap,
8702 window: self.layer_window(li),
8703 v_norm: self.attn_v_norm,
8704 qk_norm_after_rope: self.qk_norm_after_rope,
8705 gate_sigmoid: self.proj_gate_sigmoid,
8706 q_norm: q_norm.as_deref(),
8707 k_norm: k_norm.as_deref(),
8708 output_gate: *output_gate,
8709 softplus_gate: softplus_gate
8710 .as_ref()
8711 .map(|(gate, per_head)| (gate, *per_head)),
8712 rope_scale: self.layer_rope_scale(li),
8713 bias: bias
8714 .as_ref()
8715 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8716 rms_eps: eps,
8717 norm_style: self.norm_style,
8718 pool: pool.as_deref(),
8719 v_head_dim: self.layer_v_dim(li),
8720 };
8721 attention::qwen_attention_pair(
8722 &self.ws.n1,
8723 &self.ws.n2,
8724 wq,
8725 wk,
8726 wv,
8727 wo,
8728 &mut self.kv_cache.layers[li],
8729 &cfg,
8730 )
8731 }
8732 };
8733 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8734 Some(w) => (
8735 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8736 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8737 ),
8738 None => (a1, a2),
8739 };
8740 for i in 0..self.hidden_size {
8741 h1[i] += a1[i];
8742 h2[i] += a2[i];
8743 }
8744 let (mut a1, mut a2) = (a1, a2);
8745 attention::recycle_buf(&mut a1);
8746 attention::recycle_buf(&mut a2);
8747
8748 let lw = &self.weights.layers[self.phys_layer(li)];
8749 inference::rms_norm_into(
8750 &h1,
8751 &lw.post_norm,
8752 self.rms_eps,
8753 self.norm_style,
8754 &mut self.ws.p1,
8755 );
8756 inference::rms_norm_into(
8757 &h2,
8758 &lw.post_norm,
8759 self.rms_eps,
8760 self.norm_style,
8761 &mut self.ws.p2,
8762 );
8763 let (f1, f2) = match &lw.ffn {
8764 FfnKind::DenseMoe(dm) => (
8767 dense_moe_ffn(
8768 dm,
8769 &self.ws.p1,
8770 &h1,
8771 self.rms_eps,
8772 self.norm_style,
8773 self.pool.as_deref(),
8774 ),
8775 dense_moe_ffn(
8776 dm,
8777 &self.ws.p2,
8778 &h2,
8779 self.rms_eps,
8780 self.norm_style,
8781 self.pool.as_deref(),
8782 ),
8783 ),
8784 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8785 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8786 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8787 ),
8788 _ => ffn_forward_pair(
8789 &lw.ffn,
8790 &self.ws.p1,
8791 &self.ws.p2,
8792 self.pool.as_deref(),
8793 None,
8794 ),
8795 };
8796 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8797 Some(w) => (
8798 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8799 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8800 ),
8801 None => (f1, f2),
8802 };
8803 for i in 0..self.hidden_size {
8804 h1[i] += f1[i];
8805 h2[i] += f2[i];
8806 }
8807 let (mut f1, mut f2) = (f1, f2);
8808 attention::recycle_buf(&mut f1);
8809 attention::recycle_buf(&mut f2);
8810 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8811 for i in 0..self.hidden_size {
8812 h1[i] *= sc;
8813 h2[i] *= sc;
8814 }
8815 }
8816 if self.is_loop_end(li) && li + 1 < self.num_layers {
8818 h1 = inference::rms_norm(
8819 &h1,
8820 &self.weights.final_norm,
8821 self.rms_eps,
8822 self.norm_style,
8823 );
8824 h2 = inference::rms_norm(
8825 &h2,
8826 &self.weights.final_norm,
8827 self.rms_eps,
8828 self.norm_style,
8829 );
8830 }
8831 }
8832 crate::gpu::set_layer(-1); if self.o1_active() {
8839 self.commit_linear_scratch();
8840 }
8841 self.o1_progress();
8842 self.swa_trim_tails();
8843 (h1, h2)
8844 }
8845
8846 fn commit_linear_scratch(&mut self) {
8848 for layer in &mut self.kv_cache.layers {
8849 if !layer.linear_scratch.is_empty() {
8850 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8851 layer.linear_scratch.clear();
8852 }
8853 }
8854 }
8855
8856 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8864 #[cfg(target_os = "macos")]
8865 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8866 if ids.is_empty() {
8867 return Err("empty id sequence".to_string());
8868 }
8869
8870 self.nll_begin()?;
8871 let result = (|| {
8872 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8873 self.nll_check_graph("encoder prefill", 0)?;
8874 let expected = ids.len() * self.hidden_size;
8875 if hidden.len() != expected {
8876 return Err(format!(
8877 "encoder prefill returned {} values for {} rows of hidden size {}",
8878 hidden.len(),
8879 ids.len(),
8880 self.hidden_size
8881 ));
8882 }
8883
8884 let mut out = vec![0.0f32; expected];
8885 for (src, dst) in hidden
8886 .chunks_exact(self.hidden_size)
8887 .zip(out.chunks_exact_mut(self.hidden_size))
8888 {
8889 inference::rms_norm_into(
8890 src,
8891 &self.weights.final_norm,
8892 self.rms_eps,
8893 self.norm_style,
8894 dst,
8895 );
8896 }
8897 Ok(out)
8898 })();
8899 self.nll_end();
8900 result
8901 }
8902
8903 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8908 #[cfg(target_os = "macos")]
8909 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8910 if ids.is_empty() {
8911 return Err("empty id sequence".to_string());
8912 }
8913 self.nll_begin()?;
8914 let result = (|| {
8915 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8916 self.nll_check_graph("hidden prefill", 0)?;
8917 if hidden.len() != ids.len() * self.hidden_size {
8918 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
8919 }
8920 Ok(hidden)
8921 })();
8922 self.nll_end();
8923 result
8924 }
8925
8926 pub fn forward_ids(
8929 &mut self,
8930 ids: &[u32],
8931 task_mask: Option<&TaskMask>,
8932 ) -> Result<Vec<f32>, String> {
8933 #[cfg(target_os = "macos")]
8934 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8935 if ids.is_empty() {
8936 return Err("empty id sequence".to_string());
8937 }
8938 self.clear_sequence_state();
8939 self.check_forward_graph("forward_ids setup", 0)?;
8940 if task_mask.is_none() {
8941 self.o1_begin();
8942 }
8943 let mut hidden = vec![0.0f32; self.hidden_size];
8944 let mut pos = 0usize;
8945 if let Some(b) = &mut self.dsv41 {
8946 let pool = self.pool.clone();
8947 let mut logits = Vec::new();
8948 crate::dsv41::forward_chunk(
8949 &b.0,
8950 &b.1,
8951 &b.2,
8952 &mut b.3,
8953 ids,
8954 0,
8955 pool.as_deref(),
8956 &mut logits,
8957 );
8958 if let Err(err) = self.o1_seal_checked() {
8959 self.clear_sequence_state();
8960 return Err(err);
8961 }
8962 return Ok(logits);
8963 }
8964 #[cfg(not(target_os = "macos"))]
8975 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8976 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8977 Ok(r) => r,
8978 Err(e) => {
8979 self.clear_sequence_state();
8980 return Err(e);
8981 }
8982 };
8983 if n > 0 {
8984 hidden.copy_from_slice(&last);
8985 pos = n;
8986 }
8987 }
8988 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8989 let chunk = self.prefill_chunk();
8993 let hs = self.hidden_size;
8994 while pos < ids.len() {
8995 let end = (pos + chunk).min(ids.len());
8996 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8997 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8998 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8999 pos = end;
9000 }
9001 }
9002 if task_mask.is_none()
9011 && !self.graph_prefill_preferred()
9012 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9013 && self.pair_supported()
9014 {
9015 while pos + 1 < ids.len() {
9016 let e1 = self.embed_single(ids[pos]);
9017 let e2 = self.embed_single(ids[pos + 1]);
9018 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9019 self.check_forward_graph("forward_ids pair", pos + 1)?;
9020 self.commit_linear_scratch();
9021 hidden = h2;
9022 pos += 2;
9023 }
9024 }
9025 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9028 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9029 self.graph_logits = Some(lg);
9030 hidden = vec![0.0; self.hidden_size];
9031 pos = ids.len();
9032 }
9033 }
9034 while pos < ids.len() {
9035 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9036 self.check_forward_graph("forward_ids", pos)?;
9037 pos += 1;
9038 }
9039 if let Some(logits) = self.graph_logits.take() {
9040 if let Err(err) = self.o1_seal_checked() {
9044 self.clear_sequence_state();
9045 return Err(err);
9046 }
9047 return Ok(logits);
9048 }
9049 if let Err(err) = self.o1_seal_checked() {
9053 self.clear_sequence_state();
9054 return Err(err);
9055 }
9056 let normed = inference::rms_norm(
9057 &hidden,
9058 &self.weights.final_norm,
9059 self.rms_eps,
9060 self.norm_style,
9061 );
9062 Ok(self.lm_head_forward(&normed))
9063 }
9064
9065 #[doc(hidden)]
9069 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9070 #[cfg(target_os = "macos")]
9071 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9072 if ids.is_empty() {
9073 return Err("empty id sequence".to_string());
9074 }
9075 self.clear_sequence_state();
9076 self.dsv41
9077 .as_ref()
9078 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9079 self.o1_begin();
9080 let rows = {
9081 let pool = self.pool.clone();
9082 let b = self
9083 .dsv41
9084 .as_mut()
9085 .expect("dsv41 checked above; state cannot change during forward");
9086 let mut rows = Vec::with_capacity(ids.len());
9087 for (position, &id) in ids.iter().enumerate() {
9088 let mut logits = Vec::new();
9089 crate::dsv41::forward_token(
9090 &b.0,
9091 &b.1,
9092 &b.2,
9093 &mut b.3,
9094 id,
9095 position,
9096 pool.as_deref(),
9097 &mut logits,
9098 );
9099 rows.push(logits);
9100 }
9101 rows
9102 };
9103 self.o1_seal();
9104 Ok(rows)
9105 }
9106
9107 pub fn dsv41_moe_capture(
9111 &mut self,
9112 ids: &[u32],
9113 lo: usize,
9114 hi: usize,
9115 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9116 #[cfg(target_os = "macos")]
9117 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9118 if ids.is_empty() {
9119 return Err("empty id sequence".to_string());
9120 }
9121 self.dsv41
9122 .as_ref()
9123 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9124 self.clear_sequence_state();
9125 let res = {
9126 let pool = self.pool.clone();
9127 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9128 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9129 };
9130 self.clear_sequence_state();
9131 res
9132 }
9133
9134 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9141 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9142 Ok((nll / cnt.max(1) as f64).exp())
9143 }
9144
9145 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9152 if !self.can_prefill_batched() {
9153 return self.nll_ids_from(ids, 0).map(|_| ());
9154 }
9155 self.nll_begin()?;
9156 let result: Result<(), String> = (|| {
9157 let chunk = chunk.max(1);
9158 let mut pos = 0usize;
9159 while pos < ids.len() {
9160 let end = (pos + chunk).min(ids.len());
9161 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9162 self.nll_check_graph("calibration prefill", pos)?;
9163 pos = end;
9164 }
9165 Ok(())
9166 })();
9167 self.nll_end();
9168 result
9169 }
9170
9171 pub fn calib_window_span(
9180 &mut self,
9181 ids: &[u32],
9182 input: Option<&[f32]>,
9183 from: usize,
9184 mid: usize,
9185 upto: usize,
9186 ) -> Result<Vec<f32>, String> {
9187 if !self.can_prefill_batched() {
9188 return Err("calib_window_span: this architecture has no batched prefill".into());
9189 }
9190 self.nll_begin()?;
9191 let result: Result<Vec<f32>, String> = (|| {
9192 let h = if mid > from {
9193 let inp = match input {
9194 Some(h) => PrefillIn::Hidden(h),
9195 None => PrefillIn::Ids(ids),
9196 };
9197 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9198 self.nll_check_graph("calibration span", 0)?;
9199 h
9200 } else {
9201 input.map(<[f32]>::to_vec).unwrap_or_default()
9202 };
9203 if upto > mid {
9204 let inp = if mid > from || input.is_some() {
9205 PrefillIn::Hidden(&h)
9206 } else {
9207 PrefillIn::Ids(ids)
9208 };
9209 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9210 self.nll_check_graph("calibration span", 0)?;
9211 }
9212 Ok(h)
9213 })();
9214 self.nll_end();
9215 result
9216 }
9217
9218 #[cfg(feature = "gpu")]
9222 pub fn qwen4_capture(
9223 &mut self,
9224 ids: &[u32],
9225 lo: usize,
9226 hi: usize,
9227 ) -> Result<Vec<Vec<f32>>, String> {
9228 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9229 crate::qwen4_exp::capture_window(
9230 &b.0,
9231 &b.1,
9232 &b.2,
9233 &mut b.3,
9234 ids,
9235 lo,
9236 hi,
9237 &self.inv_freq,
9238 self.pool.as_deref(),
9239 )
9240 }
9241
9242 #[cfg(feature = "gpu")]
9248 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9249 use crate::gpu_wgpu::clover as cap;
9250 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9251 let ne = (lo..hi)
9252 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9253 FfnKind::Moe(m) => Some(m.experts.len()),
9254 _ => None,
9255 })
9256 .ok_or("no MoE layer in the capture range")?;
9257 let chunk = self.prefill_chunk().max(1);
9258 self.clear_sequence_state();
9259 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9260 return Err("capture unavailable (no wgpu device)".into());
9261 }
9262 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9263 let res = (|| {
9264 for (ci, part) in ids.chunks(chunk).enumerate() {
9265 let mut hiddens = Vec::with_capacity(part.len() * hs);
9266 for &id in part {
9267 hiddens.extend_from_slice(&self.embed_single(id));
9268 }
9269 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9270 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9271 crate::gpu::BatchGraphOutcome::Completed => {}
9272 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9273 }
9274 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9275 if written < hi - lo {
9276 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9277 }
9278 for (o, r) in out.iter_mut().zip(rows) {
9279 o.extend_from_slice(&r);
9280 }
9281 }
9282 Ok(())
9283 })();
9284 cap::moe_capture_end();
9285 self.clear_sequence_state();
9286 res.map(|_| out)
9287 }
9288
9289 pub fn moe_expert_dirs(
9292 &self,
9293 li: usize,
9294 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9295 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9296 return None;
9297 };
9298 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9299 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9300 let shared = match &m.shared {
9301 Some((se, _)) => Some(dirs(se)?),
9302 None => None,
9303 };
9304 Some((routed, shared))
9305 }
9306
9307 pub fn qwen4_expert_dirs(
9309 &self,
9310 li: usize,
9311 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9312 let b = self.qwen4_exp.as_ref()?;
9313 crate::qwen4_exp::expert_dirs(&b.1, li)
9314 }
9315
9316 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9331 self.reset_mtp_probe_state();
9332 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9336 let result = if ids.len() < 2 {
9337 Err("MTP FFN probe needs at least two token ids".into())
9338 } else if ids
9339 .iter()
9340 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9341 {
9342 Err("MTP FFN probe received a token id outside the embedding table".into())
9343 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9344 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9347 } else if self.mimo_mtp.is_some() {
9348 let mut st = self
9349 .mimo_mtp
9350 .take()
9351 .expect("checked MiMo MTP before detaching it");
9352 let result = crate::gpu::cpu_scope(|| {
9353 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids)
9354 });
9355 st.reset();
9358 self.mimo_mtp = Some(st);
9359 result
9360 } else if self.mtp.is_some() {
9361 let mut mtp = self
9362 .mtp
9363 .take()
9364 .expect("checked canonical MTP before detaching it");
9365 let result = crate::gpu::cpu_scope(|| self.mtp_ffn_probe_teacher_forced(&mut mtp, ids));
9366 mtp.kv.clear();
9367 self.mtp = Some(mtp);
9368 result
9369 } else {
9370 Err("MTP FFN probe: this pipeline has no draft stack".into())
9371 };
9372 self.reset_mtp_probe_state();
9375 result
9376 }
9377
9378 fn reset_mtp_probe_state(&mut self) {
9380 self.clear_sequence_state();
9381 if let Some(mtp) = self.mtp.as_mut() {
9382 mtp.kv.clear();
9383 }
9384 if let Some(mimo) = self.mimo_mtp.as_mut() {
9385 mimo.reset();
9386 }
9387 self.mtp_graph_mode = None;
9388 }
9389
9390 fn mtp_ffn_probe_teacher_forced(
9396 &mut self,
9397 mtp: &mut MtpModule,
9398 ids: &[u32],
9399 ) -> Result<Vec<MtpFfnProbe>, String> {
9400 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9401 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9402 };
9403 let mut mass = vec![0.0f64; dense.gate_proj.rows()];
9404 let mut squared_mass = vec![0.0f64; dense.gate_proj.rows()];
9405 for position in 0..ids.len() - 1 {
9406 let hidden = self.forward_layers(&self.embed_single(ids[position]), position, None);
9407 if hidden.len() != self.hidden_size {
9408 return Err(format!(
9409 "MTP FFN probe: trunk hidden width {} != {}",
9410 hidden.len(),
9411 self.hidden_size
9412 ));
9413 }
9414 self.mtp_probe_step_ffn(
9415 mtp,
9416 &hidden,
9417 ids[position + 1],
9418 position,
9419 &mut mass,
9420 &mut squared_mass,
9421 )?;
9422 }
9423 Ok(vec![MtpFfnProbe {
9424 mass,
9425 squared_mass,
9426 observed_positions: ids.len() - 1,
9427 }])
9428 }
9429
9430 fn mtp_probe_step_ffn(
9435 &mut self,
9436 mtp: &mut MtpModule,
9437 hidden: &[f32],
9438 next_token: u32,
9439 position: usize,
9440 mass: &mut [f64],
9441 squared_mass: &mut [f64],
9442 ) -> Result<(), String> {
9443 let e = self.embed_single(next_token);
9444 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9445 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9446 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9447 inference::rms_norm_into(
9448 hidden,
9449 &mtp.hnorm,
9450 self.rms_eps,
9451 self.norm_style,
9452 cat_h,
9453 );
9454 let mut x = vec![0.0f32; self.hidden_size];
9455 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9456
9457 let lw = &mtp.layer;
9458 inference::rms_norm_into(
9459 &x,
9460 &lw.input_norm,
9461 self.rms_eps,
9462 self.norm_style,
9463 &mut self.ws.n1,
9464 );
9465 let attn = match &lw.attn {
9466 AttnKind::Full {
9467 wq,
9468 wk,
9469 wv,
9470 wo,
9471 q_norm,
9472 k_norm,
9473 output_gate,
9474 softplus_gate,
9475 bias,
9476 } => {
9477 let mut cfg = self.attn_cfg(position);
9478 cfg.q_norm = q_norm.as_deref();
9479 cfg.k_norm = k_norm.as_deref();
9480 cfg.output_gate = *output_gate;
9481 cfg.softplus_gate = softplus_gate
9482 .as_ref()
9483 .map(|(gate, per_head)| (gate, *per_head));
9484 cfg.bias = bias
9485 .as_ref()
9486 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9487 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9488 }
9489 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9490 };
9491 for (dst, src) in x.iter_mut().zip(attn) {
9492 *dst += src;
9493 }
9494 inference::rms_norm_into(
9495 &x,
9496 &lw.post_norm,
9497 self.rms_eps,
9498 self.norm_style,
9499 &mut self.ws.p1,
9500 );
9501 let FfnKind::Dense(dense) = &lw.ffn else {
9502 return Err("MTP FFN probe: draft FFN is not dense".into());
9503 };
9504 accumulate_dense_ffn_activation_mass(
9505 dense,
9506 &self.ws.p1,
9507 1,
9508 self.pool.as_deref(),
9509 mass,
9510 squared_mass,
9511 )?;
9512 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9515 for (dst, src) in x.iter_mut().zip(ffn) {
9516 *dst += src;
9517 }
9518 Ok(())
9519 }
9520
9521 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9526 self.clear_sequence_state();
9527 FFN_PROBE.with(|p| {
9528 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9529 });
9530 crate::gpu::cpu_scope(|| {
9531 for (pos, &id) in ids.iter().enumerate() {
9532 let emb = self.embed_single(id);
9533 let _ = self.forward_layers(&emb, pos, None);
9534 }
9535 });
9536 self.clear_sequence_state();
9537 FFN_PROBE
9538 .with(|p| p.borrow_mut().take())
9539 .unwrap_or_default()
9540 }
9541
9542 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9546 if let Err(err) = self.nll_begin() {
9547 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9551 self.nll_end();
9552 return Err(err);
9553 }
9554 FFN_PROBE.with(|p| {
9555 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9556 });
9557 let result: Result<(), String> = (|| {
9558 for chunk in ids.chunks(256) {
9559 if chunk.len() < 2 {
9560 continue;
9561 }
9562 self.nll_ids_masked(chunk, 0, None)?;
9563 }
9564 Ok(())
9565 })();
9566 self.nll_end();
9567 let probe = FFN_PROBE
9568 .with(|p| p.borrow_mut().take())
9569 .unwrap_or_default();
9570 match result {
9571 Ok(()) => Ok(probe),
9572 Err(err) => {
9573 drop(probe);
9574 Err(err)
9575 }
9576 }
9577 }
9578
9579 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9583 self.nll_begin()?;
9584 let result: Result<f64, String> = (|| {
9585 let mut nll = 0f64;
9586 let mut cnt = 0usize;
9587 let mut hidden = vec![0f32; self.hidden_size];
9588 for (pos, &id) in ids.iter().enumerate() {
9589 if pos > 0 {
9590 inference::rms_norm_into(
9591 &hidden,
9592 &self.weights.final_norm,
9593 self.rms_eps,
9594 self.norm_style,
9595 &mut self.ws.n1,
9596 );
9597 let mut logits = self.lm_head_forward(&self.ws.n1);
9598 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9599 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9600 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9601 nll -= p.max(1e-300).ln();
9602 cnt += 1;
9603 attention::recycle_buf(&mut logits);
9604 }
9605 let emb = self.embed_single(id);
9606 hidden = self.forward_layers(&emb, pos, Some(mask));
9607 self.nll_check_graph("masked serial forward", pos)?;
9608 let _ = self.graph_logits.take();
9612 }
9613 Ok((nll / cnt.max(1) as f64).exp())
9614 })();
9615 self.nll_end();
9616 result
9617 }
9618
9619 pub fn nll_ids_masked(
9638 &mut self,
9639 ids: &[u32],
9640 start: usize,
9641 task_mask: Option<&TaskMask>,
9642 ) -> Result<(f64, usize), String> {
9643 let task_mask = self.drop_open_mask(task_mask);
9644 self.nll_ids_inner(ids, start, task_mask)
9645 }
9646
9647 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9648 self.nll_ids_inner(ids, start, None)
9649 }
9650
9651 fn nll_ids_inner(
9652 &mut self,
9653 ids: &[u32],
9654 start: usize,
9655 task_mask: Option<&TaskMask>,
9656 ) -> Result<(f64, usize), String> {
9657 self.nll_begin()?;
9658 let result: Result<(f64, usize), String> = (|| {
9659 let mut nll = 0f64;
9660 let mut cnt = 0usize;
9661 let (graph_quality, fused_head_quality) = nll_graph_policy(
9674 task_mask.is_none(),
9675 self.graph_prefill_preferred(),
9676 crate::gpu::q1_force(),
9677 );
9678 self.graph_head_required = fused_head_quality;
9679 self.graph_want_logits = fused_head_quality;
9680 #[cfg(target_os = "macos")]
9681 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9682 match self.nll_batch_metal(ids, start) {
9683 MetalBatchNllOutcome::Completed(nll, count) => {
9684 return Ok((nll, count));
9685 }
9686 MetalBatchNllOutcome::Declined => {}
9687 MetalBatchNllOutcome::Failed(err) => return Err(err),
9688 }
9689 }
9690 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9695 if self.can_prefill_batched() && !graph_quality && !force_serial {
9696 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9705 .ok()
9706 .and_then(|v| v.parse().ok())
9707 .filter(|&c: &usize| c > 0)
9708 .unwrap_or(128);
9709 const LM_SUB: usize = 32;
9710 let n = ids.len().saturating_sub(1);
9711 let hs = self.hidden_size;
9712 let rows = self.weights.lm_head.rows();
9713 let mut pos = 0usize;
9714 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9715 while pos < n {
9716 let end = (pos + chunk_len).min(n);
9717 let bsz = end - pos;
9718 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9719 self.nll_check_graph("batched prefill", pos)?;
9720 if state_trace && end % 256 == 0 {
9721 self.trace_recurrent_state(end);
9722 }
9723 let mut k0 = 0usize;
9724 while k0 < bsz {
9725 let k1 = (k0 + LM_SUB).min(bsz);
9726 let sb = k1 - k0;
9727 if pos + k1 <= start {
9730 k0 = k1;
9731 continue;
9732 }
9733 let mut normed = vec![0.0f32; sb * hs];
9734 for k in 0..sb {
9735 let r = inference::rms_norm(
9736 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9737 &self.weights.final_norm,
9738 self.rms_eps,
9739 self.norm_style,
9740 );
9741 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9742 }
9743 let mut logits = vec![0.0f32; sb * rows];
9744 self.weights
9745 .lm_head
9746 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9747 for k in 0..sb {
9748 if pos + k0 + k < start {
9749 continue;
9750 }
9751 self.nll_check_graph("batched score row", pos + k0 + k)?;
9752 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9753 if let Some(mu) = self.logit_multiplier {
9754 for v in lg.iter_mut() {
9755 *v *= mu;
9756 }
9757 }
9758 if let Some(c) = self.final_softcap {
9762 softcap_in_place(lg, c, self.pool.as_deref());
9763 }
9764 if let Some(cm) = self.head_clusters.clone() {
9767 self.hierarchical_head_logprobs(
9768 &normed[k * hs..(k + 1) * hs],
9769 &cm,
9770 lg,
9771 );
9772 }
9773 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9774 let target = ids[pos + k0 + k + 1] as usize;
9775 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9776 let lse: f64 = lg
9777 .iter()
9778 .map(|&v| ((v - max) as f64).exp())
9779 .sum::<f64>()
9780 .ln()
9781 + max as f64;
9782 nll += lse - lg[target] as f64;
9783 cnt += 1;
9784 if std::env::var("CMF_PPL_TRACE").is_ok() {
9785 let top = lg
9786 .iter()
9787 .enumerate()
9788 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9789 .map(|(i, _)| i)
9790 .unwrap_or(0);
9791 eprintln!(
9792 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9793 pos + k0 + k,
9794 target,
9795 lse - lg[target] as f64,
9796 top,
9797 lg[target],
9798 lg[top]
9799 );
9800 }
9801 }
9802 k0 = k1;
9803 }
9804 pos = end;
9805 }
9806 return Ok((nll, cnt));
9807 }
9808 for pos in 0..ids.len().saturating_sub(1) {
9809 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9810 self.nll_check_graph("serial forward", pos)?;
9811 let out_of_band = self.graph_logits.take();
9819 if self.graph_head_required && out_of_band.is_none() {
9820 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9821 return Err(format!(
9822 "fused Metal graph head did not complete at NLL position {pos}"
9823 ));
9824 }
9825 if pos < start {
9826 continue;
9827 }
9828 let logits = match out_of_band {
9829 Some(lg) => lg,
9830 None => {
9831 let normed = inference::rms_norm(
9832 &hidden,
9833 &self.weights.final_norm,
9834 self.rms_eps,
9835 self.norm_style,
9836 );
9837 self.lm_head_forward(&normed)
9841 }
9842 };
9843 let target = ids[pos + 1] as usize;
9844 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9845 let lse: f64 = logits
9846 .iter()
9847 .map(|&v| ((v - max) as f64).exp())
9848 .sum::<f64>()
9849 .ln()
9850 + max as f64;
9851 let tok_nll = lse - logits[target] as f64;
9852 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9853 let top = logits
9854 .iter()
9855 .enumerate()
9856 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9857 .map(|(i, _)| i)
9858 .unwrap_or(0);
9859 eprintln!(
9860 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9861 logits[target], logits[top]
9862 );
9863 }
9864 nll += tok_nll;
9865 cnt += 1;
9866 }
9867 Ok((nll, cnt))
9868 })();
9869 self.nll_end();
9870 result
9871 }
9872
9873 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9878 let normed = inference::rms_norm(
9879 hidden,
9880 &self.weights.final_norm,
9881 self.rms_eps,
9882 self.norm_style,
9883 );
9884 let mut logits = self.lm_head_forward(&normed);
9887 let target = target as usize;
9888 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9889 let lse: f64 = logits
9890 .iter()
9891 .map(|&v| ((v - max) as f64).exp())
9892 .sum::<f64>()
9893 .ln()
9894 + max as f64;
9895 let tok_nll = lse - logits[target] as f64;
9896 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9897 let top = logits
9898 .iter()
9899 .enumerate()
9900 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9901 .map(|(i, _)| i)
9902 .unwrap_or(0);
9903 eprintln!(
9904 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9905 logits[target], logits[top]
9906 );
9907 }
9908 attention::recycle_buf(&mut logits);
9909 tok_nll
9910 }
9911
9912 fn trace_recurrent_state(&self, pos: usize) {
9920 let stats = |v: &[f32]| -> (f64, f64) {
9921 if v.is_empty() {
9922 return (0.0, 0.0);
9923 }
9924 let (mut ss, mut mx) = (0f64, 0f64);
9925 for &x in v {
9926 ss += (x as f64) * (x as f64);
9927 mx = mx.max((x as f64).abs());
9928 }
9929 ((ss / v.len() as f64).sqrt(), mx)
9930 };
9931 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9932 let lw = &self.weights.layers[self.phys_layer(li)];
9933 let (kind, s_len) = match &lw.attn {
9934 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9935 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9936 AttnKind::Bounded(_) => ("bounded", 0),
9937 AttnKind::Full { .. } => ("full", 0),
9938 _ => ("other", 0),
9939 };
9940 let (rms, max) = stats(&l.linear_state);
9941 let s_part = if kind == "vmf" {
9942 &l.linear_state[..s_len.min(l.linear_state.len())]
9943 } else if kind == "gdn" {
9944 let ring = l
9945 .linear_state
9946 .len()
9947 .saturating_sub(s_len)
9948 .min(l.linear_state.len());
9949 &l.linear_state[ring..]
9950 } else {
9951 &l.linear_state[..0]
9952 };
9953 let (s_rms, s_max) = stats(s_part);
9954 let (ring_rms, ring_len) = match &l.bounded {
9955 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9956 None => (0.0, 0),
9957 };
9958 eprintln!(
9959 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9960 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9961 l.linear_state.len(),
9962 l.seq_len
9963 );
9964 }
9965 }
9966
9967 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9985 self.nll_begin()?;
9990 let requested_prefix = (prefill > 0).then_some(prefill);
9991 self.o1_begin_with_prefix(requested_prefix);
9992 let n = ids.len().saturating_sub(1);
9993 let requested_start = prefill.min(n);
9994 let exact_end = if self.o1_active() {
9999 match requested_prefix {
10000 Some(requested) => self.o1_effective_boundary(requested),
10001 None => self
10002 .o1_cfg
10003 .as_ref()
10004 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10005 }
10006 .unwrap_or(requested_start)
10007 .min(n)
10008 } else {
10009 requested_start
10010 };
10011 let mut nll = 0f64;
10012 let mut cnt = 0usize;
10013
10014 let mut pos = 0usize;
10018 if self.can_prefill_batched() {
10019 const CHUNK: usize = 128;
10020 while pos < exact_end {
10021 let end = (pos + CHUNK).min(exact_end);
10022 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10023 if self
10024 .graph_failed
10025 .swap(false, std::sync::atomic::Ordering::Relaxed)
10026 {
10027 self.cancel
10028 .store(false, std::sync::atomic::Ordering::Relaxed);
10029 self.nll_end();
10030 return Err("GPU graph failed during O(1) NLL prefix".into());
10031 }
10032 for row in 0..end - pos {
10033 let score_pos = pos + row;
10034 if score_pos >= requested_start && score_pos < n {
10035 nll += self.nll_from_hidden(
10036 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10037 ids[score_pos + 1],
10038 score_pos,
10039 );
10040 cnt += 1;
10041 }
10042 }
10043 pos = end;
10044 }
10045 } else {
10046 while pos < exact_end {
10047 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10048 if self
10049 .graph_failed
10050 .swap(false, std::sync::atomic::Ordering::Relaxed)
10051 {
10052 self.cancel
10053 .store(false, std::sync::atomic::Ordering::Relaxed);
10054 self.nll_end();
10055 return Err("GPU graph failed during O(1) NLL prefix".into());
10056 }
10057 if pos >= requested_start && pos < n {
10058 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10059 cnt += 1;
10060 }
10061 pos += 1;
10062 }
10063 }
10064 self.o1_seal_checked().map_err(|err| {
10065 self.nll_end();
10066 err
10067 })?;
10068
10069 let batch_k = std::env::var("CMF_BATCH_K")
10078 .ok()
10079 .and_then(|v| v.parse::<usize>().ok())
10080 .unwrap_or(0);
10081 let batch_admitted = batch_k > 0
10082 && self.can_prefill_batched()
10083 && self.o1_active()
10084 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10085 && (0..self.num_layers).all(|li| {
10086 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10087 cache.o1.is_none() || cache.o1_views().is_some()
10088 });
10089 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10090 eprintln!(
10091 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10092 batch_admitted,
10093 batch_k,
10094 n.saturating_sub(exact_end),
10095 );
10096 }
10097 let mut batch_completed = false;
10098 if batch_admitted && exact_end < n {
10099 let hs = self.hidden_size;
10100 let mut batch_pos = exact_end;
10101 while batch_pos < n {
10102 let end = (batch_pos + batch_k).min(n);
10103 let bk = end - batch_pos;
10104 let mut hiddens = vec![0.0f32; bk * hs];
10105 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10106 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10107 }
10108 let positions: Vec<usize> = (batch_pos..end).collect();
10109 let t_batch = std::time::Instant::now();
10110 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10111 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10112 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10113 eprintln!(
10114 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10115 batch_pos,
10116 end.saturating_sub(1),
10117 bk as f64 / (ms / 1000.0),
10118 );
10119 }
10120 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10121 self.nll_end();
10122 return Err(err);
10123 }
10124 match outcome {
10125 crate::gpu::BatchGraphOutcome::Completed => {
10126 batch_completed = true;
10127 for row in 0..bk {
10128 nll += self.nll_from_hidden(
10129 &hiddens[row * hs..(row + 1) * hs],
10130 ids[batch_pos + row + 1],
10131 batch_pos + row,
10132 );
10133 cnt += 1;
10134 }
10135 batch_pos = end;
10136 }
10137 crate::gpu::BatchGraphOutcome::Declined => {
10138 if batch_completed {
10139 self.nll_end();
10140 return Err(format!(
10141 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10142 ));
10143 }
10144 break;
10145 }
10146 crate::gpu::BatchGraphOutcome::Failed => {
10147 self.nll_end();
10148 return Err(format!(
10149 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10150 ));
10151 }
10152 }
10153 }
10154 if batch_completed && cnt == n.saturating_sub(requested_start) {
10155 self.nll_end();
10156 return Ok((nll, cnt));
10157 }
10158 }
10159
10160 for pos in exact_end..n {
10165 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10166 if self
10167 .graph_failed
10168 .swap(false, std::sync::atomic::Ordering::Relaxed)
10169 {
10170 self.cancel
10171 .store(false, std::sync::atomic::Ordering::Relaxed);
10172 self.nll_end();
10173 return Err(format!(
10174 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10175 ));
10176 }
10177 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10178 cnt += 1;
10179 }
10180 self.nll_end();
10181 Ok((nll, cnt))
10182 }
10183
10184 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10192 self.clear_sequence_state();
10193 let n = ids.len().saturating_sub(1);
10194 let mut correct = Vec::with_capacity(n);
10195 let mut pmax = Vec::with_capacity(n);
10196 for pos in 0..n {
10197 let emb = self.embed_single(ids[pos]);
10198 let hidden = self.forward_layers(&emb, pos, None);
10199 let logits = if let Some(logits) = self.graph_logits.take() {
10200 logits
10201 } else {
10202 let normed = inference::rms_norm(
10203 &hidden,
10204 &self.weights.final_norm,
10205 self.rms_eps,
10206 self.norm_style,
10207 );
10208 self.lm_head_forward(&normed)
10212 };
10213 let target = ids[pos + 1] as usize;
10214 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10215 for (i, &v) in logits.iter().enumerate() {
10216 if v > mval {
10217 mval = v;
10218 amax = i;
10219 }
10220 }
10221 correct.push(amax == target);
10222 let row: Vec<f32> = temps
10223 .iter()
10224 .map(|&t| {
10225 let tt = t.max(1e-3);
10226 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10227 1.0 / s.max(1e-12) })
10229 .collect();
10230 pmax.push(row);
10231 }
10232 self.clear_sequence_state();
10233 (correct, pmax)
10234 }
10235
10236 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10243 if self.dyn_router.is_none() {
10244 return Ok((self.ppl_ids(ids)?, 0));
10245 }
10246 self.nll_begin()?;
10247 let saved_active = self.dyn_active;
10248 let mut router = self
10249 .dyn_router
10250 .take()
10251 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10252 router.reset();
10253 self.dyn_phi_seen = 0;
10254 let _ = self.set_active_skill(None);
10255
10256 let result: Result<(f64, usize), String> = (|| {
10257 let mut nll = 0f64;
10258 let mut cnt = 0usize;
10259 for pos in 0..ids.len().saturating_sub(1) {
10260 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10261 self.nll_check_graph("dynamic serial forward", pos)?;
10262 let out_of_band = self.graph_logits.take();
10263 let mut logits = match out_of_band {
10264 Some(lg) => lg,
10265 None => {
10266 let normed = inference::rms_norm(
10267 &hidden,
10268 &self.weights.final_norm,
10269 self.rms_eps,
10270 self.norm_style,
10271 );
10272 self.lm_head_forward(&normed)
10276 }
10277 };
10278 let target = ids[pos + 1] as usize;
10279 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10280 let lse: f64 = logits
10281 .iter()
10282 .map(|&v| ((v - max) as f64).exp())
10283 .sum::<f64>()
10284 .ln()
10285 + max as f64;
10286 let tok_nll = lse - logits[target] as f64;
10287 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10288 let top = logits
10289 .iter()
10290 .enumerate()
10291 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10292 .map(|(i, _)| i)
10293 .unwrap_or(0);
10294 eprintln!(
10295 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10296 logits[target], logits[top]
10297 );
10298 }
10299 nll += tok_nll;
10300 cnt += 1;
10301 attention::recycle_buf(&mut logits);
10302 let phi = self.dyn_phi_ema.clone();
10304 if let Some(new_active) = router.step(&phi, pos) {
10305 let _ = self.set_active_skill(new_active);
10306 }
10307 }
10308 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10309 })();
10310
10311 let _ = self.set_active_skill(saved_active);
10314 self.dyn_router = Some(router);
10315 self.nll_end();
10316 result
10317 }
10318
10319 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10321 self.clear_sequence_state();
10322 let mut acc = vec![0f32; self.hidden_size];
10323 for (pos, &id) in ids.iter().enumerate() {
10324 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10325 for (a, v) in acc.iter_mut().zip(&h) {
10326 *a += v;
10327 }
10328 }
10329 let n = ids.len().max(1) as f32;
10330 for a in acc.iter_mut() {
10331 *a /= n;
10332 }
10333 self.clear_sequence_state();
10334 acc
10335 }
10336
10337 pub fn probe_phi_span(
10351 &mut self,
10352 ids: &[u32],
10353 layer: usize,
10354 span: std::ops::Range<usize>,
10355 ) -> Vec<f32> {
10356 #[cfg(target_os = "macos")]
10357 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10358 let end = span.end.min(ids.len());
10359 let start = span.start.min(end);
10360 let reset = |p: &mut Self| p.clear_sequence_state();
10361 reset(self);
10362 let mut acc = vec![0f32; self.hidden_size];
10363 for (pos, &id) in ids[..end].iter().enumerate() {
10364 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10365 if pos >= start {
10366 for (a, v) in acc.iter_mut().zip(&h) {
10367 *a += v;
10368 }
10369 }
10370 }
10371 let n = end - start;
10372 if n > 0 {
10373 let n = n as f32;
10374 for a in acc.iter_mut() {
10375 *a /= n;
10376 }
10377 }
10378 reset(self);
10379 acc
10380 }
10381
10382 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10389 #[cfg(target_os = "macos")]
10390 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10391 self.graph_logits = None;
10392 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10393 if let Some(logits) = self.graph_logits.take() {
10394 return logits;
10395 }
10396 inference::rms_norm_into(
10397 &hidden,
10398 &self.weights.final_norm,
10399 self.rms_eps,
10400 self.norm_style,
10401 &mut self.ws.n1,
10402 );
10403 self.lm_head_forward(&self.ws.n1)
10404 }
10405
10406 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10412 self.prefill_batch_masked(ids, start_pos, None)
10413 }
10414
10415 fn prefill_batch_masked(
10421 &mut self,
10422 ids: &[u32],
10423 start_pos: usize,
10424 task_mask: Option<&TaskMask>,
10425 ) -> Vec<f32> {
10426 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10427 }
10428
10429 fn prefill_rows(
10437 &mut self,
10438 ids: &[u32],
10439 pos: usize,
10440 task_mask: Option<&TaskMask>,
10441 ) -> Result<Vec<f32>, String> {
10442 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10443 }
10444
10445 fn prefill_input_rows(
10446 &mut self,
10447 input: PrefillIn<'_>,
10448 pos: usize,
10449 task_mask: Option<&TaskMask>,
10450 ) -> Result<Vec<f32>, String> {
10451 self.mimo_moe_prepare();
10452 let hs = self.hidden_size;
10453 let bk = match input {
10454 PrefillIn::Ids(ids) => ids.len(),
10455 PrefillIn::Hidden(rows) => rows.len() / hs,
10456 };
10457 #[cfg(not(target_os = "macos"))]
10458 if task_mask.is_none()
10459 && !self.o1_active()
10460 && bk > 1
10461 && (self.batch_prefix_prefill()
10462 || (self.verify_exact_moe
10463 && crate::gpu::enabled_here()
10464 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10465 {
10466 let mut hiddens = match input {
10467 PrefillIn::Hidden(rows) => rows.to_vec(),
10468 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10469 };
10470 let positions: Vec<usize> = (pos..pos + bk).collect();
10471 let mut run = 0usize;
10472 match self.try_batch_graph_wgpu_prefix(
10473 &mut hiddens,
10474 &positions,
10475 bk,
10476 None,
10477 Some(&mut run),
10478 ) {
10479 crate::gpu::BatchGraphOutcome::Completed => {
10480 let out = if run < self.num_layers {
10481 self.prefill_batch_span(
10482 PrefillIn::Hidden(&hiddens),
10483 pos,
10484 None,
10485 run,
10486 self.num_layers,
10487 )
10488 } else {
10489 hiddens
10490 };
10491 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10492 Err("MiMo attention graph failed after admission".into())
10493 } else {
10494 Ok(out)
10495 };
10496 }
10497 crate::gpu::BatchGraphOutcome::Failed => {
10498 return Err("batched prefix prefill failed after admission".into());
10499 }
10500 crate::gpu::BatchGraphOutcome::Declined => {
10501 #[cfg(feature = "gpu")]
10503 self.pull_lagging_host_kv(0, self.num_layers, pos);
10504 }
10505 }
10506 }
10507 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10508 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10509 Err("batch tail graph failed after admission".into())
10510 } else {
10511 Ok(out)
10512 }
10513 }
10514
10515 fn prefill_batch_span(
10521 &mut self,
10522 input: PrefillIn<'_>,
10523 start_pos: usize,
10524 task_mask: Option<&TaskMask>,
10525 from: usize,
10526 upto_excl: usize,
10527 ) -> Vec<f32> {
10528 let hs = self.hidden_size;
10529 let b = match input {
10530 PrefillIn::Ids(ids) => ids.len(),
10531 PrefillIn::Hidden(hb) => hb.len() / hs,
10532 };
10533 let upto_excl = upto_excl.min(self.num_layers);
10534 let mut h: Vec<f32>;
10538 let mut h_ready;
10539 match input {
10540 PrefillIn::Ids(_) => {
10541 h = vec![0.0; b * hs];
10542 h_ready = false;
10543 }
10544 PrefillIn::Hidden(hb) => {
10545 h = hb.to_vec();
10546 h_ready = true;
10547 }
10548 }
10549 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10550 if let PrefillIn::Ids(ids) = input {
10551 for (bi, &id) in ids.iter().enumerate() {
10552 let e = me.embed_single(id);
10553 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10554 }
10555 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10556 if let Ok(t) = tp.parse::<usize>() {
10557 if t >= start_pos && t < start_pos + ids.len() {
10558 let bi = t - start_pos;
10559 let row = &h[bi * hs..(bi + 1) * hs];
10560 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10561 eprintln!(
10562 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10563 ids[bi],
10564 row[0],
10565 row[1],
10566 ids.len(),
10567 &ids[..ids.len().min(8)]
10568 );
10569 }
10570 }
10571 }
10572 }
10573 };
10574 let (_nkv, _hd, _rd, eps) = (
10575 self.num_kv_heads,
10576 self.head_dim,
10577 self.rotary_dim,
10578 self.rms_eps,
10579 );
10580 let pool = self.pool.clone();
10581 let norm_style = self.norm_style;
10582 self.mimo_moe_prepare();
10583 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10584
10585 #[cfg(target_os = "macos")]
10586 let mut chunk_skip_until = 0usize;
10587 for li in from..upto_excl {
10588 let _capacity_tail = automatic_gpu_prefix
10589 .filter(|&prefix| {
10590 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10591 })
10592 .map(|_| crate::gpu::enter_cpu_scope());
10593 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10598 let _fast_gemm = self
10599 .proj_gate_sigmoid
10600 .then(crate::gpu::enter_prefill_fast_gemm);
10601 #[cfg(target_os = "macos")]
10607 if task_mask.is_none() {
10608 if li < chunk_skip_until {
10609 continue;
10610 }
10611 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10617 fill_h(&mut h, self);
10618 h_ready = true;
10619 }
10620 let ids_for_embed = match input {
10621 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10622 PrefillIn::Hidden(_) => None,
10623 };
10624 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10625 if end > li {
10626 h_ready = true;
10627 chunk_skip_until = end;
10628 if self.is_loop_end(end - 1) && end < self.num_layers {
10631 for bi in 0..b {
10632 let normed = inference::rms_norm(
10633 &h[bi * hs..(bi + 1) * hs],
10634 &self.weights.final_norm,
10635 eps,
10636 norm_style,
10637 );
10638 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10639 }
10640 }
10641 continue;
10642 }
10643 }
10644 if !h_ready {
10645 fill_h(&mut h, self);
10646 h_ready = true;
10647 }
10648 if task_mask.is_none() && self.verify_exact_moe {
10649 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10650 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10651 crate::gpu::BatchGraphOutcome::Completed => continue,
10652 crate::gpu::BatchGraphOutcome::Failed => return h,
10653 crate::gpu::BatchGraphOutcome::Declined => {}
10654 }
10655 }
10656 #[cfg(feature = "gpu")]
10657 self.pull_lagging_host_kv(li, li + 1, start_pos);
10658 let lw = &self.weights.layers[self.phys_layer(li)];
10659 let t_attn = std::time::Instant::now();
10660 match &lw.attn {
10662 AttnKind::Kda(w) => {
10663 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10665 let mut normed = vec![0.0f32; b * hs];
10666 for bi in 0..b {
10667 inference::rms_norm_into(
10668 &h[bi * hs..(bi + 1) * hs],
10669 &lw.input_norm,
10670 eps,
10671 norm_style,
10672 &mut normed[bi * hs..(bi + 1) * hs],
10673 );
10674 }
10675 let attn = crate::linear_core::kda_forward_batch(
10676 &normed,
10677 b,
10678 w,
10679 &cfg,
10680 &mut self.kv_cache.layers[li].linear_state,
10681 pool.as_deref(),
10682 );
10683 for (dst, &a) in h.iter_mut().zip(&attn) {
10684 *dst += a;
10685 }
10686 }
10687 AttnKind::LinearGdn(w) => {
10688 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10690 let mut normed = vec![0.0f32; b * hs];
10691 for bi in 0..b {
10692 let r = inference::rms_norm(
10693 &h[bi * hs..(bi + 1) * hs],
10694 &lw.input_norm,
10695 eps,
10696 norm_style,
10697 );
10698 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10699 }
10700 let attn = crate::linear_core::gdn_forward_batch(
10701 &normed,
10702 b,
10703 w,
10704 &cfg,
10705 &mut self.kv_cache.layers[li].linear_state,
10706 pool.as_deref(),
10707 );
10708 for (dst, &a) in h.iter_mut().zip(&attn) {
10709 *dst += a;
10710 }
10711 }
10712 AttnKind::ShortConv(w) => {
10713 let cfg = self
10716 .short_conv_cfg
10717 .expect("short-conv layer without short_conv_cfg");
10718 let mut normed = vec![0.0f32; b * hs];
10719 for bi in 0..b {
10720 inference::rms_norm_into(
10721 &h[bi * hs..(bi + 1) * hs],
10722 &lw.input_norm,
10723 eps,
10724 norm_style,
10725 &mut normed[bi * hs..(bi + 1) * hs],
10726 );
10727 }
10728 let attn = short_conv_forward_batch(
10729 &normed,
10730 b,
10731 w,
10732 &cfg,
10733 &mut self.kv_cache.layers[li].linear_state,
10734 pool.as_deref(),
10735 );
10736 for (dst, &a) in h.iter_mut().zip(&attn) {
10737 *dst += a;
10738 }
10739 }
10740 AttnKind::Mla(w) => {
10741 let inv_freq_l = self.layer_inv_freq(li);
10744 let rs = self.layer_rope_scale(li);
10745 let mut normed = vec![0.0f32; hs];
10746 for bi in 0..b {
10747 inference::rms_norm_into(
10748 &h[bi * hs..(bi + 1) * hs],
10749 &lw.input_norm,
10750 eps,
10751 norm_style,
10752 &mut normed,
10753 );
10754 let ao = mla_attention(
10755 w,
10756 &normed,
10757 &mut self.kv_cache.layers[li],
10758 start_pos + bi,
10759 &inv_freq_l,
10760 rs,
10761 eps,
10762 pool.as_deref(),
10763 );
10764 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10765 *dst += a;
10766 }
10767 }
10768 }
10769 AttnKind::Full {
10770 wq,
10771 wk,
10772 wv,
10773 wo,
10774 q_norm,
10775 k_norm,
10776 output_gate,
10777 softplus_gate,
10778 bias,
10779 } => {
10780 let mut normed = vec![0.0f32; b * hs];
10784 for bi in 0..b {
10785 inference::rms_norm_into(
10786 &h[bi * hs..(bi + 1) * hs],
10787 &lw.input_norm,
10788 eps,
10789 norm_style,
10790 &mut normed[bi * hs..(bi + 1) * hs],
10791 );
10792 }
10793 let inv_freq_l = self.layer_inv_freq(li);
10794 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10795 let cfg = QwenAttnCfg {
10796 num_heads: self.layer_num_heads(li),
10797 num_kv_heads: nkv_l,
10798 head_dim: hd_l,
10799 hidden_size: hs,
10800 position: start_pos,
10801 inv_freq: &inv_freq_l,
10802 rotary_dim: rd_l,
10803 scale: self.attn_scale,
10804 softcap: self.attn_softcap,
10805 window: self.layer_window(li),
10806 v_norm: self.attn_v_norm,
10807 qk_norm_after_rope: self.qk_norm_after_rope,
10808 gate_sigmoid: self.proj_gate_sigmoid,
10809 q_norm: q_norm.as_deref(),
10810 k_norm: k_norm.as_deref(),
10811 output_gate: *output_gate,
10812 softplus_gate: softplus_gate
10813 .as_ref()
10814 .map(|(gate, per_head)| (gate, *per_head)),
10815 rope_scale: self.layer_rope_scale(li),
10816 bias: bias
10817 .as_ref()
10818 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10819 rms_eps: eps,
10820 norm_style,
10821 pool: pool.as_deref(),
10822 v_head_dim: self.layer_v_dim(li),
10823 };
10824 #[cfg(feature = "gpu")]
10825 let _mirror = self
10826 .prefill_mirror_target(li)
10827 .map(crate::gpu::enter_prefill_mirror);
10828 let mut attn = attention::qwen_attention_batch(
10829 &normed,
10830 b,
10831 wq,
10832 wk,
10833 wv,
10834 wo,
10835 &mut self.kv_cache.layers[li],
10836 &cfg,
10837 );
10838 if let Some(w) = &lw.attn_out_norm {
10839 for bi in 0..b {
10840 inference::rms_norm_into(
10841 &attn[bi * hs..(bi + 1) * hs],
10842 w,
10843 eps,
10844 norm_style,
10845 &mut normed[bi * hs..(bi + 1) * hs],
10846 );
10847 }
10848 attn.copy_from_slice(&normed);
10849 }
10850 for (dst, &a) in h.iter_mut().zip(&attn) {
10851 *dst += a;
10852 }
10853 }
10854 AttnKind::Bounded(w) => {
10855 let mut normed = vec![0.0f32; b * hs];
10858 for bi in 0..b {
10859 inference::rms_norm_into(
10860 &h[bi * hs..(bi + 1) * hs],
10861 &lw.input_norm,
10862 eps,
10863 norm_style,
10864 &mut normed[bi * hs..(bi + 1) * hs],
10865 );
10866 }
10867 let rope = self
10868 .bounded_rope
10869 .clone()
10870 .expect("bounded layer without an installed rotation table");
10871 let cfg = crate::bounded::BoundedAttnCfg {
10872 num_heads: self.num_heads,
10873 num_kv_heads: self.num_kv_heads,
10874 head_dim: self.head_dim,
10875 hidden_size: hs,
10876 scale: self.attn_scale,
10877 rope: &rope,
10878 pool: pool.as_deref(),
10879 };
10880 let mut attn = crate::bounded::bounded_attention_batch(
10881 &normed,
10882 b,
10883 w,
10884 &mut self.kv_cache.layers[li],
10885 &cfg,
10886 );
10887 if let Some(wn) = &lw.attn_out_norm {
10888 for bi in 0..b {
10889 inference::rms_norm_into(
10890 &attn[bi * hs..(bi + 1) * hs],
10891 wn,
10892 eps,
10893 norm_style,
10894 &mut normed[bi * hs..(bi + 1) * hs],
10895 );
10896 }
10897 attn.copy_from_slice(&normed);
10898 }
10899 for (dst, &a) in h.iter_mut().zip(&attn) {
10900 *dst += a;
10901 }
10902 attention::recycle_buf(&mut attn);
10903 }
10904 AttnKind::Linear(w) => {
10905 for bi in 0..b {
10906 let normed = inference::rms_norm(
10907 &h[bi * hs..(bi + 1) * hs],
10908 &lw.input_norm,
10909 eps,
10910 norm_style,
10911 );
10912 vmf_phase_forward(
10913 &normed,
10914 w,
10915 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10916 &mut self.kv_cache.layers[li].linear_state,
10917 pool.as_deref(),
10918 )
10919 .iter()
10920 .enumerate()
10921 .for_each(|(i, &a)| h[bi * hs + i] += a);
10922 }
10923 }
10924 }
10925
10926 let lw = &self.weights.layers[self.phys_layer(li)];
10928 let mut post = vec![0.0f32; b * hs];
10929 for bi in 0..b {
10930 let r =
10931 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10932 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10933 }
10934 let mask_row = task_mask
10937 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10938 .and_then(|m| m.ffn_masks.get(li))
10939 .map(|v| v.as_slice());
10940 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10941 let t_ffn = std::time::Instant::now();
10942 let mut ffn = match &lw.ffn {
10943 FfnKind::Dense(d) if !d.segs.is_empty() => {
10944 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10945 }
10946 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10947 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10948 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10949 }
10950 FfnKind::Moe(m) if self.verify_exact_moe => {
10951 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10952 }
10953 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10956 let before = m.stats.borrow().clone();
10957 let out = crate::gpu::cpu_scope(|| {
10958 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10959 });
10960 self.mimo_moe.prime(li, m, &before);
10961 out
10962 }
10963 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10964 FfnKind::DenseMoe(dm) => {
10967 let mut out = vec![0.0f32; b * hs];
10968 for bi in 0..b {
10969 let r = dense_moe_ffn(
10970 dm,
10971 &post[bi * hs..(bi + 1) * hs],
10972 &h[bi * hs..(bi + 1) * hs],
10973 eps,
10974 norm_style,
10975 pool.as_deref(),
10976 );
10977 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10978 }
10979 out
10980 }
10981 };
10982 if prefill_prof_on() {
10983 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10984 PREFILL_SPLIT[1].fetch_add(
10985 t_ffn.elapsed().as_nanos() as u64,
10986 std::sync::atomic::Ordering::Relaxed,
10987 );
10988 }
10989 if let Some(w) = &lw.ffn_out_norm {
10990 for bi in 0..b {
10991 inference::rms_norm_into(
10992 &ffn[bi * hs..(bi + 1) * hs],
10993 w,
10994 eps,
10995 norm_style,
10996 &mut post[bi * hs..(bi + 1) * hs],
10997 );
10998 }
10999 ffn.copy_from_slice(&post);
11000 }
11001 for (dst, &f) in h.iter_mut().zip(&ffn) {
11002 *dst += f;
11003 }
11004 if let Some(sc) = lw.layer_scale {
11005 for v in h.iter_mut() {
11006 *v *= sc;
11007 }
11008 }
11009 if self.layer_dump.is_some() {
11011 for bi in 0..b {
11012 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11013 }
11014 }
11015 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11016 if let Ok(t) = tp.parse::<usize>() {
11017 if t >= start_pos && t < start_pos + b {
11018 let bi = t - start_pos;
11019 let row = &h[bi * hs..(bi + 1) * hs];
11020 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11021 eprintln!(
11022 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11023 row[0], row[1]
11024 );
11025 }
11026 }
11027 }
11028 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11032 let row = &h[(b - 1) * hs..b * hs];
11033 let rms =
11034 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11035 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11036 eprintln!(
11037 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11038 match &self.weights.layers[self.phys_layer(li)].attn {
11039 AttnKind::LinearGdn(_) => "gdn",
11040 AttnKind::Linear(_) => "vmf",
11041 AttnKind::ShortConv(_) => "conv",
11042 _ => "attn",
11043 },
11044 match &lw.ffn {
11045 FfnKind::Moe(_) => "moe",
11046 FfnKind::Dense(_) => "dense",
11047 FfnKind::DenseMoe(_) => "dense+moe",
11048 },
11049 );
11050 }
11051 if self.is_loop_end(li) && li + 1 < self.num_layers {
11053 for bi in 0..b {
11054 let normed = inference::rms_norm(
11055 &h[bi * hs..(bi + 1) * hs],
11056 &self.weights.final_norm,
11057 eps,
11058 norm_style,
11059 );
11060 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11061 }
11062 }
11063 if std::env::var("CMF_TRACE_H").is_ok() {
11064 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11065 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11066 eprintln!(
11067 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11068 lw.layer_scale
11069 );
11070 }
11071 }
11072 crate::gpu::set_layer(-1); if prefill_prof_on() {
11074 let ms = |a: &std::sync::atomic::AtomicU64| {
11075 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11076 };
11077 let sp = &attention::ATTN_SPLIT;
11078 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11079 eprintln!(
11080 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11081 host gate {:.1}], host loop {:.1}, attend {:.1} \
11082 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11083 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11084 ms(&PREFILL_SPLIT[0]),
11085 ms(&sp[0]),
11086 qkv_enq,
11087 qkv_wait,
11088 ms(&sp[6]),
11089 ms(&sp[1]),
11090 ms(&sp[2]),
11091 ms(&sp[4]),
11092 ms(&sp[5]),
11093 ca_wait,
11094 ms(&sp[3]),
11095 ms(&PREFILL_SPLIT[1]),
11096 );
11097 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11098 eprintln!(
11099 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11100 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11101 fallbacks (cumulative)",
11102 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11103 );
11104 }
11105 self.o1_progress();
11110 self.swa_trim_tails();
11112 h
11113 }
11114
11115 fn embed_single(&self, id: u32) -> Vec<f32> {
11117 let mut out = vec![0.0f32; self.hidden_size];
11118 if (id as usize) < self.weights.embed_tokens.rows() {
11119 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11120 }
11121 if self.embed_multiplier != 1.0 {
11122 for v in out.iter_mut() {
11123 *v *= self.embed_multiplier;
11124 }
11125 }
11126 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11130 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11131 v[0] = id as f32;
11132 return v;
11133 }
11134 if let Some(b) = &self.g4e {
11138 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11139 }
11140 if let Some(b) = &self.g3n {
11141 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11142 }
11143 out
11144 }
11145
11146 #[cfg(target_os = "macos")]
11152 fn chunk_run_gpu(
11153 &mut self,
11154 li0: usize,
11155 h: &mut [f32],
11156 b: usize,
11157 pos0: usize,
11158 embed_ids: Option<&[u32]>,
11159 cap: usize,
11160 ) -> usize {
11161 let gemma4 =
11168 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11169 if !crate::gpu::enabled_here()
11170 || std::env::var("CMF_GPU_CHUNK")
11171 .map(|v| v == "0")
11172 .unwrap_or(false)
11173 || b < 32
11174 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11177 || (self.global_attn.is_some() && !gemma4)
11178 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11182 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11185 || self.o1_active()
11188 || (self.attn_v_norm && !gemma4)
11189 || !self.metal_rope_scale_ok()
11190 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11191 {
11192 return li0;
11193 }
11194 let Some(model) = self.model.clone() else {
11195 return li0;
11196 };
11197 let (nh, nkv, hd, hs) = (
11198 self.num_heads,
11199 self.num_kv_heads,
11200 self.head_dim,
11201 self.hidden_size,
11202 );
11203 let loop_end = if self.loop_final_norm {
11207 ((li0 / self.physical_layers) + 1) * self.physical_layers
11208 } else {
11209 self.num_layers
11210 };
11211 let run_end = self.num_layers.min(loop_end).min(cap);
11212 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11217 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11218 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11219 let mut stored_at: Vec<usize> = Vec::new();
11220 for li in li0..run_end {
11221 let lw = &self.weights.layers[self.phys_layer(li)];
11222 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11223 && !gemma4
11224 {
11225 break;
11226 }
11227 let AttnKind::Full {
11228 wq,
11229 wk,
11230 wv,
11231 wo,
11232 q_norm,
11233 k_norm,
11234 output_gate: false,
11235 softplus_gate,
11236 bias,
11237 } = &lw.attn
11238 else {
11239 break;
11240 };
11241 let head_gate = match softplus_gate {
11243 None => None,
11244 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11245 Some((d, r, c)) if r == nh && c == hs => Some(d),
11246 _ => break,
11247 },
11248 Some(_) => break,
11249 };
11250 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11254 FfnKind::Dense(d)
11255 if (matches!(d.act, Act::Silu | Act::Gelu)
11256 || (gemma4 && d.act == Act::GeluTanh))
11257 && d.segs.is_empty() =>
11258 {
11259 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11260 }
11261 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11262 Some((router, experts, q8)) => {
11263 let e0 = &m.experts[0];
11264 let route = Box::new(move |lg: &[f32]| {
11265 let (idx, p, wsum) = moe_route(lg, m, None);
11267 let mut st = m.stats.borrow_mut();
11268 if st.len() < m.experts.len() {
11269 st.resize(m.experts.len(), 0);
11270 }
11271 for &e in &idx {
11272 st[e] += 1;
11273 }
11274 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11275 (idx, w)
11276 });
11277 (
11278 &e0.gate_proj,
11279 &e0.up_proj,
11280 &e0.down_proj,
11281 Act::Silu,
11282 Some(crate::gpu_metal::ChunkMoe {
11283 router,
11284 experts,
11285 q8,
11286 route,
11287 }),
11288 )
11289 }
11290 None => break,
11291 },
11292 _ => break,
11293 };
11294 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11300 t.q8_row_parts()
11301 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11302 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11303 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11304 }
11305 fn cf(t: &QTensor) -> &[f32] {
11306 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11307 }
11308 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11309 if moe.is_none()
11318 && !gemma4
11319 && (col.iter().any(|c| !c.is_empty())
11320 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
11321 {
11322 break;
11323 }
11324 let parts = (
11325 cw(wq),
11326 cw(wk),
11327 cw(wv),
11328 cw(wo),
11329 cw(gate_t),
11330 cw(up_t),
11331 cw(down_t),
11332 );
11333 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11334 else {
11335 break;
11336 };
11337 let layer = &self.kv_cache.layers[li];
11338 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11339 break;
11340 }
11341 debug_assert!(
11343 layer.base() == 0
11344 || self
11345 .layer_window(li)
11346 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11347 );
11348 stored_at.push(layer.head_len(0));
11349 let (nkv, hd) = if gemma4 {
11352 let (a, bb, _) = self.layer_geom(li);
11353 (a, bb)
11354 } else {
11355 (nkv, hd)
11356 };
11357 layers.push(crate::gpu_metal::ChunkLayer {
11358 model: &model,
11359 kv_id: self.graph_kv_id,
11360 layer: li,
11361 wq: pq,
11362 wk: pk,
11363 wv: pv,
11364 wo: po,
11365 gate: pg,
11366 up: pu,
11367 down: pd,
11368 input_norm: &lw.input_norm,
11369 post_norm: &lw.post_norm,
11370 bias: bias
11371 .as_ref()
11372 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11373 q_norm: q_norm.as_deref(),
11374 k_norm: k_norm.as_deref(),
11375 inv_freq: &tables[li - li0],
11376 rope_scale: self.layer_rope_scale(li),
11377 rd: self.layer_geom(li).2,
11378 nh,
11379 nkv,
11380 hd,
11381 hs,
11382 inter: gate_t.rows(),
11383 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11384 late_qk_norm: self.qk_norm_after_rope,
11385 eps: self.rms_eps as f32,
11386 window: self.layer_window(li),
11387 head_gate,
11388 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11389 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11390 v_norm: self.attn_v_norm,
11391 scale: self.attn_scale,
11392 attn_out_norm: lw.attn_out_norm.as_deref(),
11393 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11394 layer_scale: lw.layer_scale,
11395 }),
11396 col,
11397 moe,
11398 });
11399 }
11400 if layers.is_empty() {
11401 return li0;
11402 }
11403 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11405 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11406 .iter()
11407 .zip(&rows_kv)
11408 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11409 .collect();
11410 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11411 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11412 let li = layers[i].layer;
11413 let layer = &self.kv_cache.layers[li];
11414 let nkv = rows_kv[i].0;
11415 io.push(crate::gpu_metal::ChunkIo {
11416 cpu_stored: stored_at[i],
11417 cpu_gen: layer.generation(),
11418 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11419 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11420 out_k: ok,
11421 out_v: ov,
11422 imp: oi,
11423 });
11424 }
11425 let n_run = layers.len();
11426 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11427 let ep = embed_ids.and_then(|ids| {
11430 self.weights
11431 .embed_tokens
11432 .q8_row_parts()
11433 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11434 idx,
11435 rows,
11436 row_scale: rs,
11437 ids,
11438 mult: self.embed_multiplier,
11439 })
11440 });
11441 if embed_ids.is_some() && ep.is_none() {
11442 return li0;
11443 }
11444 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11445 return li0;
11446 }
11447 drop(io);
11448 drop(layers);
11449 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11452 let li = li0 + i;
11453 let row = rows_kv[i].1;
11454 let layer = &mut self.kv_cache.layers[li];
11455 for bi in 0..b {
11456 layer.append(
11457 &ok[bi * row..(bi + 1) * row],
11458 &ov[bi * row..(bi + 1) * row],
11459 &[],
11460 );
11461 }
11462 layer.accumulate_imp(oi);
11463 }
11464 last
11465 }
11466
11467 fn layer_is_local(&self, li: usize) -> bool {
11470 if let Some(layers) = &self.sliding_layers {
11471 return layers.get(li).copied().unwrap_or(false);
11472 }
11473 match self.swa {
11474 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11475 None => false,
11476 }
11477 }
11478
11479 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11482 if self.layer_is_local(li) {
11483 if let Some(f) = &self.inv_freq_local {
11484 return f.clone();
11485 }
11486 } else if let Some(f) = &self.inv_freq_global {
11487 return f.clone();
11488 }
11489 self.inv_freq.clone()
11490 }
11491
11492 fn layer_window(&self, li: usize) -> Option<usize> {
11494 self.swa
11495 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11496 }
11497
11498 fn swa_trim_tails(&mut self) {
11507 let Some((slack, align)) = self.swa_trim else {
11508 return;
11509 };
11510 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11511 return;
11512 }
11513 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11514 if let Some(w) = self.layer_window(li) {
11515 self.kv_cache.layers[li].trim_window(w, slack, align);
11516 }
11517 }
11518 }
11519
11520 fn layer_num_heads(&self, li: usize) -> usize {
11521 self.attention_heads_per_layer
11522 .as_ref()
11523 .and_then(|v| v.get(li).copied())
11524 .unwrap_or(self.num_heads)
11525 }
11526
11527 fn layer_rope_scale(&self, li: usize) -> f32 {
11528 if self.layer_is_local(li) {
11529 self.rope_scale_local
11530 } else {
11531 self.rope_scale
11532 }
11533 }
11534
11535 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11538 if !self.layer_is_local(li) {
11539 if let Some((ghd, gkv)) = self.global_attn {
11540 return (gkv, ghd, ghd);
11541 }
11542 }
11543 let rd = if self.layer_is_local(li) {
11544 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11545 } else {
11546 self.rotary_dim
11547 };
11548 let rd = if self
11550 .no_rope_layers
11551 .as_ref()
11552 .and_then(|v| v.get(li))
11553 .copied()
11554 == Some(true)
11555 {
11556 0
11557 } else {
11558 rd
11559 };
11560 (self.layer_num_kv_heads(li), self.head_dim, rd)
11561 }
11562
11563 fn layer_num_kv_heads(&self, li: usize) -> usize {
11566 self.kv_heads_per_layer
11567 .as_ref()
11568 .and_then(|v| v.get(self.phys_layer(li)).copied())
11569 .unwrap_or(self.num_kv_heads)
11570 }
11571
11572 fn layer_v_dim(&self, li: usize) -> usize {
11574 let (_, hd, _) = self.layer_geom(li);
11575 self.v_head_dim.unwrap_or(hd).min(hd)
11576 }
11577
11578 pub fn set_attn_geometry(
11587 &mut self,
11588 kv_heads_per_layer: Option<Vec<usize>>,
11589 v_head_dim: Option<usize>,
11590 ) -> Result<(), String> {
11591 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11592 if self.global_attn.is_some() {
11593 return Err(
11594 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11595 attention geometry"
11596 .into(),
11597 );
11598 }
11599 if self
11600 .weights
11601 .layers
11602 .iter()
11603 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11604 {
11605 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11606 }
11607 }
11608 if let Some(vd) = v_head_dim {
11609 if vd == 0 || vd > self.head_dim {
11610 return Err(format!(
11611 "v_head_dim {vd} must be in 1..={} (head_dim)",
11612 self.head_dim
11613 ));
11614 }
11615 }
11616 if let Some(v) = &kv_heads_per_layer {
11617 if v.len() != self.physical_layers {
11618 return Err(format!(
11619 "kv_heads_per_layer has {} entries, expected {} layers",
11620 v.len(),
11621 self.physical_layers
11622 ));
11623 }
11624 for (li, &nkv) in v.iter().enumerate() {
11625 let is_attn = matches!(
11626 self.weights.layers.get(li).map(|lw| &lw.attn),
11627 Some(AttnKind::Full { .. }) | None
11628 );
11629 if !is_attn {
11630 continue;
11631 }
11632 let nh = self
11633 .attention_heads_per_layer
11634 .as_ref()
11635 .and_then(|h| h.get(li).copied())
11636 .unwrap_or(self.num_heads);
11637 if nkv == 0 || nh % nkv != 0 {
11638 return Err(format!(
11639 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11640 ));
11641 }
11642 }
11643 }
11644 self.kv_heads_per_layer = kv_heads_per_layer;
11645 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11646 if self.kv_heads_per_layer.is_some() {
11647 for li in 0..self.kv_cache.layers.len() {
11648 let full = matches!(
11649 self.weights
11650 .layers
11651 .get(self.phys_layer(li))
11652 .map(|lw| &lw.attn),
11653 Some(AttnKind::Full { .. })
11654 );
11655 let nkv = self.layer_num_kv_heads(li);
11656 let cache = &self.kv_cache.layers[li];
11657 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11658 let sinks = cache.sinks.clone();
11659 self.kv_cache.layers[li] =
11660 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11661 self.kv_cache.layers[li].sinks = sinks;
11662 }
11663 }
11664 }
11665 Ok(())
11666 }
11667
11668 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11672 let Some(lw) = self.weights.layers.get(phys) else {
11673 return Err(format!("sinks for layer {phys}: no such layer"));
11674 };
11675 if !matches!(lw.attn, AttnKind::Full { .. }) {
11676 return Err(format!(
11677 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11678 ));
11679 }
11680 let nh = self
11681 .attention_heads_per_layer
11682 .as_ref()
11683 .and_then(|h| h.get(phys).copied())
11684 .unwrap_or(self.num_heads);
11685 if sinks.len() != nh {
11686 return Err(format!(
11687 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11688 sinks.len()
11689 ));
11690 }
11691 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11692 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11693 }
11694 for li in 0..self.kv_cache.layers.len() {
11695 if self.phys_layer(li) == phys {
11696 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11697 }
11698 }
11699 Ok(())
11700 }
11701
11702 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11712 if self.kv_heads_per_layer.is_some() {
11713 return Some("per-layer KV head counts");
11714 }
11715 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11716 return Some("V heads narrower than Q/K heads");
11717 }
11718 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11719 return Some("learned attention sinks");
11720 }
11721 if self.swa.is_some() || self.sliding_layers.is_some() {
11722 return Some("sliding-window layers");
11723 }
11724 None
11725 }
11726
11727 #[cfg(target_os = "macos")]
11735 fn metal_rope_scale_ok(&self) -> bool {
11736 !(self.qk_norm_after_rope
11737 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11738 }
11739
11740 #[cfg(target_os = "macos")]
11757 fn metal_graph_swa(&self) -> bool {
11758 (self.swa.is_some() || self.sliding_layers.is_some())
11759 && self.attention_heads_per_layer.is_none()
11760 && !self.attn_v_norm
11761 && self.attn_softcap == 0.0
11762 && self.kv_heads_per_layer.is_none()
11763 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11764 && self.global_attn.is_none()
11765 && self.inv_freq_global.is_none()
11766 && self.metal_rope_scale_ok()
11767 && !(0..self.num_layers).any(|li| {
11768 self.layer_is_local(li)
11769 && self.inv_freq_local.is_none()
11770 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11771 })
11772 }
11773
11774 #[cfg(target_os = "macos")]
11782 fn metal_gemma4(&self) -> bool {
11783 crate::gpu_metal::gemma4::enabled()
11784 && (self.global_attn.is_some() || self.attn_v_norm)
11785 && self.attention_heads_per_layer.is_none()
11786 && self.kv_heads_per_layer.is_none()
11787 && self.v_head_dim.is_none()
11788 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11789 && self.attn_softcap == 0.0
11790 && !self.qk_norm_after_rope
11791 && self.metal_rope_scale_ok()
11792 && self
11793 .weights
11794 .layers
11795 .iter()
11796 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11797 && !(0..self.num_layers).any(|li| {
11798 self.layer_is_local(li)
11799 && self.inv_freq_local.is_none()
11800 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11801 })
11802 }
11803
11804 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11811 let geometry_reason = self.graph_attn_decline_reason();
11815 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11816 if has_scaled_rope && geometry_reason.is_none() {
11817 return Some("scaled RoPE without per-layer geometry");
11818 }
11819 if has_scaled_rope && self.qk_norm_after_rope {
11824 return Some("scaled RoPE with post-RoPE Q/K norm");
11825 }
11826 geometry_reason?;
11827 if self.attention_heads_per_layer.is_some() {
11833 return Some("per-layer Q head counts with per-layer geometry");
11834 }
11835 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11839 if !gemma4
11840 && self.weights.layers.iter().any(|lw| {
11841 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11842 })
11843 {
11844 return Some("sandwich norms / layer scale with per-layer geometry");
11845 }
11846 if self.weights.layers.iter().any(|lw| {
11850 matches!(
11851 &lw.attn,
11852 AttnKind::Full {
11853 output_gate: true,
11854 ..
11855 }
11856 )
11857 }) && self.v_head_dim.is_some()
11858 {
11859 return Some("gated attention with V narrower than K");
11860 }
11861 if (0..self.num_layers).any(|li| {
11862 self.layer_is_local(li)
11863 && self.inv_freq_local.is_none()
11864 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11865 }) {
11866 return Some("local rotary width without a local RoPE table");
11867 }
11868 None
11869 }
11870
11871 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11875 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11878 return None;
11879 }
11880 if self.global_attn.is_some() {
11881 return Some("per-layer head width (Gemma-4 global layers)");
11882 }
11883 if self.attn_v_norm {
11884 return Some("V norm");
11885 }
11886 if self.weights.layers.iter().any(|lw| {
11887 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11888 }) {
11889 return Some("sandwich norms / layer scale");
11890 }
11891 None
11892 }
11893
11894 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11899 self.graph_attn_decline_reason()?;
11900 let (nkv, hd, rd) = self.layer_geom(li);
11901 let invf: &[f32] = if self.layer_is_local(li) {
11902 match &self.inv_freq_local {
11903 Some(f) => f.as_slice(),
11904 None => self.inv_freq.as_slice(),
11905 }
11906 } else {
11907 match &self.inv_freq_global {
11908 Some(f) => f.as_slice(),
11909 None => self.inv_freq.as_slice(),
11910 }
11911 };
11912 Some(crate::gpu::GraphAttnGeom {
11913 nkv,
11914 hd,
11915 dv: self.layer_v_dim(li),
11916 rd,
11917 invf,
11918 rope_scale: self.layer_rope_scale(li),
11919 window: self.layer_window(li),
11920 sink: self.kv_cache.layers[li].sinks.as_deref(),
11921 v_norm: self.attn_v_norm,
11922 })
11923 }
11924
11925 #[cfg(feature = "gpu")]
11937 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11938 if !self.proj_gate_sigmoid
11939 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11940 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11941 || self.graph_refused()
11942 || self.o1_active()
11943 || self.attn_softcap > 0.0
11944 || self.wgpu_graph_attn_decline().is_some()
11945 {
11946 return None;
11947 }
11948 let g = self.graph_attn_geom(li)?;
11949 let (nkv, hd, _) = self.layer_geom(li);
11950 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11952 return None;
11953 }
11954 Some(crate::gpu::PrefillMirror {
11955 kv_id: self.graph_kv_id,
11956 layer: li,
11957 limit: self.kv_cache.max_seq_len,
11958 })
11959 }
11960
11961 #[cfg(feature = "gpu")]
11970 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11971 let kv_id = self.graph_kv_id;
11972 for li in from..upto.min(self.num_layers) {
11973 if !matches!(
11974 self.weights.layers[self.phys_layer(li)].attn,
11975 AttnKind::Full { .. }
11976 ) {
11977 continue;
11978 }
11979 let host = self.kv_cache.layers[li].pos_len();
11981 if host >= position {
11982 continue;
11983 }
11984 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11985 continue;
11986 };
11987 let to = dev.min(position);
11988 if to <= host {
11989 continue;
11990 }
11991 let (nkv, hd) = {
11992 let c = &self.kv_cache.layers[li];
11993 (c.num_kv_heads, c.head_dim)
11994 };
11995 let Some((k, v, first_valid)) =
11996 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11997 else {
11998 continue;
11999 };
12000 let need_from = match self.layer_window(li) {
12003 Some(w) => host.max((position + 1).saturating_sub(w)),
12004 None => host,
12005 };
12006 if first_valid > need_from {
12007 tracing::warn!(
12008 "layer {li}: device KV rows {host}..{to} no longer resident \
12009 (from {first_valid}); host attention will miss them"
12010 );
12011 }
12012 let row = nkv * hd;
12013 let cache = &mut self.kv_cache.layers[li];
12014 for p in 0..to - host {
12015 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12016 }
12017 }
12018 }
12019
12020 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12023 let mut seen = self.graph_declines.borrow_mut();
12024 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12025 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12026 seen.push((site, reason));
12027 }
12028 }
12029
12030 pub fn graph_declines(&self) -> Vec<String> {
12033 self.graph_declines
12034 .borrow()
12035 .iter()
12036 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12037 .collect()
12038 }
12039
12040 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12049 let Some(dir) = &self.layer_dump else {
12050 return;
12051 };
12052 let mut bytes = Vec::with_capacity(row.len() * 4);
12053 for v in row {
12054 bytes.extend_from_slice(&v.to_le_bytes());
12055 }
12056 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12057 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12058 use std::sync::atomic::{AtomicBool, Ordering};
12059 static SAID: AtomicBool = AtomicBool::new(false);
12060 if !SAID.swap(true, Ordering::Relaxed) {
12061 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12062 }
12063 }
12064 }
12065
12066 fn mimo_moe_prepare(&mut self) {
12069 if !self.mimo_moe.is_undecided() {
12070 return;
12071 }
12072 let slot = {
12073 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12074 .filter_map(
12075 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12076 FfnKind::Moe(m) => Some((li, m)),
12077 _ => None,
12078 },
12079 )
12080 .collect();
12081 if layers.is_empty()
12084 || self.physical_layers != self.num_layers
12085 || self.gpu_plan.is_some()
12086 {
12087 crate::mimo_moe::Slot::Off
12088 } else {
12089 let graph_prefix =
12093 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12094 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12095 }
12096 };
12097 self.mimo_moe = slot;
12098 }
12099
12100 #[cfg(test)]
12101 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12102 self.graph_kv_id
12103 }
12104
12105 pub(crate) fn mimo_graph_layer_rows(
12109 &mut self,
12110 li: usize,
12111 h: &mut [f32],
12112 positions: &[usize],
12113 ) -> crate::gpu::BatchGraphOutcome {
12114 use crate::gpu::BatchGraphOutcome as Out;
12115 let b = positions.len();
12116 if !(1..=4).contains(&b)
12117 || h.len() != b * self.hidden_size
12118 || !self.mimo_moe.is_dynamic(li, true)
12119 || !crate::gpu::enabled_here()
12120 || !crate::gpu::wgpu_active()
12121 || self.o1_active()
12122 || self.physical_layers != self.num_layers
12123 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12127 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12128 || self.wgpu_graph_attn_decline().is_some()
12129 {
12130 return Out::Declined;
12131 }
12132 let attn_started = std::time::Instant::now();
12133 let outcome = {
12134 let lw = &self.weights.layers[li];
12135 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12136 return Out::Declined;
12137 }
12138 let FfnKind::Moe(m) = &lw.ffn else {
12139 return Out::Declined;
12140 };
12141 let AttnKind::Full {
12142 wq,
12143 wk,
12144 wv,
12145 wo,
12146 q_norm,
12147 k_norm,
12148 output_gate,
12149 softplus_gate,
12150 bias,
12151 } = &lw.attn
12152 else {
12153 return Out::Declined;
12154 };
12155 if *output_gate || softplus_gate.is_some() {
12156 return Out::Declined;
12157 }
12158 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12159 m.experts
12160 .first()?
12161 .gate_proj
12162 .mapped_q4tp()
12163 .map(|(m, _)| m.clone())
12164 }) else {
12165 return Out::Declined;
12166 };
12167 fn gw<'a>(
12168 t: &'a QTensor,
12169 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12170 ) -> Option<crate::gpu::GraphW<'a>> {
12171 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12172 if m.uid() != owner.uid() || t.has_prism_contract() {
12173 return None;
12174 }
12175 return Some(crate::gpu::GraphW {
12176 idx,
12177 kind,
12178 row_scale: rs,
12179 data: &[],
12180 prism: crate::gpu::GraphPrismOp::None,
12181 affine: false,
12182 });
12183 }
12184 t.as_f32().map(|data| crate::gpu::GraphW {
12185 idx: 0,
12186 kind: 4,
12187 row_scale: &[],
12188 data,
12189 prism: crate::gpu::GraphPrismOp::None,
12190 affine: false,
12191 })
12192 }
12193 let (Some(q), Some(k), Some(v), Some(o)) = (
12194 gw(wq, &model),
12195 gw(wk, &model),
12196 gw(wv, &model),
12197 gw(wo, &model),
12198 ) else {
12199 return Out::Declined;
12200 };
12201 let layer = crate::gpu::GraphLayer {
12202 input_norm: &lw.input_norm,
12203 post_norm: &lw.post_norm,
12204 attn_out_norm: None,
12205 ffn_out_norm: None,
12206 layer_scale: None,
12207 ffn: crate::gpu::GraphFfn::AttentionOnly,
12208 attn: crate::gpu::GraphAttn::Full {
12209 wq: q,
12210 wk: k,
12211 wv: v,
12212 wo: o,
12213 q_norm: q_norm.as_deref(),
12214 k_norm: k_norm.as_deref(),
12215 late_qk_norm: self.qk_norm_after_rope,
12216 bias: bias
12217 .as_ref()
12218 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12219 output_gate: false,
12220 cpu_k: self.kv_cache.layers[li].k_heads(),
12221 cpu_v: self.kv_cache.layers[li].v_heads(),
12222 cpu_base: self.kv_cache.layers[li].base(),
12223 geom: self.graph_attn_geom(li),
12224 head_gate: None,
12225 },
12226 };
12227 let (nkv, hd, rd) = self.layer_geom(li);
12228 crate::gpu::forward_batch_graph_at(
12229 &model,
12230 self.graph_kv_id,
12231 li,
12232 &[layer],
12233 &self.inv_freq,
12234 h,
12235 self.layer_num_heads(li),
12236 nkv,
12237 hd,
12238 rd,
12239 self.hidden_size,
12240 1,
12241 positions,
12242 self.kv_cache.max_seq_len,
12243 self.norm_style == cortiq_core::NormStyle::Gemma,
12244 self.rms_eps as f32,
12245 self.attn_scale,
12246 b,
12247 &[],
12248 self.o1_epoch,
12249 None,
12250 None,
12251 )
12252 };
12253 match outcome {
12254 Out::Completed => {}
12255 Out::Declined => return Out::Declined,
12256 Out::Failed => {
12257 self.graph_failed
12258 .store(true, std::sync::atomic::Ordering::Relaxed);
12259 return Out::Failed;
12260 }
12261 }
12262 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12263 let hs = self.hidden_size;
12264 let lw = &self.weights.layers[li];
12265 let FfnKind::Moe(m) = &lw.ffn else {
12266 unreachable!()
12267 };
12268 let mut post = vec![0.0; h.len()];
12269 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12270 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12271 }
12272 let mut ffn = if b == 1 {
12273 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12274 } else {
12275 moe_ffn_banked_rows(
12276 &mut self.mimo_moe,
12277 li,
12278 m,
12279 &post,
12280 b,
12281 hs,
12282 self.pool.as_deref(),
12283 )
12284 };
12285 for (x, &f) in h.iter_mut().zip(&ffn) {
12286 *x += f;
12287 }
12288 attention::recycle_buf(&mut ffn);
12289 if self.layer_dump.is_some() {
12290 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12291 self.dump_layer_row(pos, li, row);
12292 }
12293 }
12294 crate::mimo_moe::note_attention_graph(b, attn_ns);
12295 Out::Completed
12296 }
12297
12298 fn layer_attn_plain(&self, li: usize) -> bool {
12299 self.kv_heads_per_layer.is_none()
12300 && self.v_head_dim.is_none()
12301 && self.global_attn.is_none()
12302 && self.layer_window(li).is_none()
12303 && self.kv_cache.layers[li].sinks.is_none()
12304 }
12305
12306 fn forward_layers(
12308 &mut self,
12309 hidden: &[f32],
12310 position: usize,
12311 task_mask: Option<&TaskMask>,
12312 ) -> Vec<f32> {
12313 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12314 self.o1_progress();
12315 self.swa_trim_tails();
12316 out
12317 }
12318
12319 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12327 self.embed_single(id)
12328 }
12329
12330 pub fn split_supported(&self) -> Result<(), String> {
12334 if self.dsv4.is_some() {
12335 return Err(
12336 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12337 );
12338 }
12339 if self.dsv41.is_some() {
12340 return Err(
12341 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12342 .into(),
12343 );
12344 }
12345 if self.qwen4_exp.is_some() {
12346 return Err(
12347 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12348 );
12349 }
12350 if self.per_token_stack() {
12351 return Err(
12352 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12353 );
12354 }
12355 Ok(())
12356 }
12357
12358 pub fn forward_span(
12363 &mut self,
12364 hidden: &[f32],
12365 position: usize,
12366 from: usize,
12367 upto: usize,
12368 task_mask: Option<&TaskMask>,
12369 ) -> Result<Vec<f32>, String> {
12370 self.split_supported()?;
12371 if from > upto || upto >= self.num_layers {
12372 return Err(format!(
12373 "forward_span: layer range {from}..={upto} outside 0..{}",
12374 self.num_layers
12375 ));
12376 }
12377 if hidden.len() != self.hidden_size {
12378 return Err(format!(
12379 "forward_span: hidden len {} ≠ hidden_size {}",
12380 hidden.len(),
12381 self.hidden_size
12382 ));
12383 }
12384 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12385 self.o1_progress();
12386 self.swa_trim_tails();
12387 if self
12388 .graph_failed
12389 .swap(false, std::sync::atomic::Ordering::Relaxed)
12390 {
12391 self.cancel
12392 .store(false, std::sync::atomic::Ordering::Relaxed);
12393 self.clear_sequence_state();
12394 return Err("forward_span: deferred O(1) transition failed".into());
12395 }
12396 Ok(out)
12397 }
12398
12399 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12402 let normed = inference::rms_norm(
12403 hidden,
12404 &self.weights.final_norm,
12405 self.rms_eps,
12406 self.norm_style,
12407 );
12408 self.lm_head_forward(&normed)
12409 }
12410
12411 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12413 sampler::sample_with_scratch(
12414 logits,
12415 &self.sampler_config,
12416 past_tokens,
12417 &mut self.rng,
12418 &mut self.sampler_scratch,
12419 )
12420 }
12421
12422 pub fn reset_session(&mut self) {
12424 self.clear_sequence_state();
12425 }
12426
12427 pub fn prefill_span_ids(
12433 &mut self,
12434 ids: &[u32],
12435 start_pos: usize,
12436 upto: usize,
12437 task_mask: Option<&TaskMask>,
12438 ) -> Result<Vec<f32>, String> {
12439 self.split_supported()?;
12440 if upto >= self.num_layers {
12441 return Err(format!(
12442 "prefill_span_ids: upto {upto} outside 0..{}",
12443 self.num_layers
12444 ));
12445 }
12446 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12450 let out =
12451 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12452 self.check_o1_progress_failure("prefill_span_ids")?;
12453 Ok(out)
12454 } else {
12455 let hs = self.hidden_size;
12456 let mut out = Vec::with_capacity(ids.len() * hs);
12457 for (i, &id) in ids.iter().enumerate() {
12458 let emb = self.embed_id(id);
12459 out.extend_from_slice(&self.forward_span(
12460 &emb,
12461 start_pos + i,
12462 0,
12463 upto,
12464 task_mask,
12465 )?);
12466 }
12467 Ok(out)
12468 }
12469 }
12470
12471 pub fn prefill_span_hidden(
12474 &mut self,
12475 hidden: &[f32],
12476 start_pos: usize,
12477 from: usize,
12478 upto: usize,
12479 task_mask: Option<&TaskMask>,
12480 ) -> Result<Vec<f32>, String> {
12481 self.split_supported()?;
12482 let hs = self.hidden_size;
12483 if hidden.is_empty() || hidden.len() % hs != 0 {
12484 return Err(format!(
12485 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12486 hidden.len()
12487 ));
12488 }
12489 if from > upto || upto >= self.num_layers {
12490 return Err(format!(
12491 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12492 self.num_layers
12493 ));
12494 }
12495 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12496 let out = self.prefill_batch_span(
12497 PrefillIn::Hidden(hidden),
12498 start_pos,
12499 task_mask,
12500 from,
12501 upto + 1,
12502 );
12503 self.check_o1_progress_failure("prefill_span_hidden")?;
12504 Ok(out)
12505 } else {
12506 let b = hidden.len() / hs;
12507 let mut out = Vec::with_capacity(hidden.len());
12508 for i in 0..b {
12509 let h = self.forward_span(
12510 &hidden[i * hs..(i + 1) * hs],
12511 start_pos + i,
12512 from,
12513 upto,
12514 task_mask,
12515 )?;
12516 out.extend_from_slice(&h);
12517 }
12518 Ok(out)
12519 }
12520 }
12521
12522 fn try_token_graph_wgpu(
12526 &self,
12527 hidden: &[f32],
12528 position: usize,
12529 logits_out: &mut Vec<f32>,
12530 layers_run: &mut usize,
12531 ) -> Option<Result<Vec<f32>, ()>> {
12532 self.try_token_graph_wgpu_steps(
12533 hidden,
12534 position,
12535 logits_out,
12536 1,
12537 None,
12538 Some(layers_run),
12539 0,
12540 self.num_layers,
12541 )
12542 }
12543
12544 fn try_token_graph_wgpu_span(
12548 &self,
12549 hidden: &[f32],
12550 position: usize,
12551 logits_out: &mut Vec<f32>,
12552 from: usize,
12553 upto_excl: usize,
12554 layers_run: &mut usize,
12555 ) -> Option<Result<Vec<f32>, ()>> {
12556 self.try_token_graph_wgpu_steps(
12557 hidden,
12558 position,
12559 logits_out,
12560 1,
12561 None,
12562 Some(layers_run),
12563 from,
12564 upto_excl,
12565 )
12566 }
12567
12568 #[cfg(feature = "gpu")]
12572 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12573 if !crate::minicpm_tail::minimax_pool_on()
12574 || !crate::gpu::enabled_here()
12575 || self.head_clusters.is_some()
12576 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12577 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12578 {
12579 return None;
12580 }
12581 let (model, i, kind, rs) = self
12582 .weights
12583 .lm_head
12584 .graph_weight()
12585 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12586 let name = &model.tensors[i].name;
12587 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12588 crate::gpu::GraphPrismOp::InverseEmbedding
12589 } else if crate::prism::is_forward_weight(&model, name) {
12590 crate::gpu::GraphPrismOp::Forward
12591 } else {
12592 crate::gpu::GraphPrismOp::None
12593 };
12594 let gw = crate::gpu::GraphW {
12595 idx: i,
12596 kind,
12597 row_scale: rs,
12598 data: &[],
12599 prism,
12600 affine: crate::prism::is_affine_target(&model, name),
12601 };
12602 let rows = self.weights.lm_head.rows();
12603 let mut h = hidden.to_vec();
12604 let mut lg = Vec::new();
12605 let mut layers_run = 0usize;
12606 let outcome = crate::gpu::forward_token_graph(
12607 &model,
12608 self.graph_kv_id,
12609 &[],
12610 &[],
12611 self.o1_epoch,
12612 &self.inv_freq,
12613 &mut h,
12614 self.num_heads,
12615 self.num_kv_heads,
12616 self.head_dim,
12617 self.attn_scale,
12618 self.head_dim,
12619 self.hidden_size,
12620 self.intermediate_size,
12621 0,
12622 self.kv_cache.max_seq_len,
12623 self.norm_style == cortiq_core::NormStyle::Gemma,
12624 self.rms_eps as f32,
12625 Some((&gw, rows)),
12626 &self.weights.final_norm,
12627 &mut lg,
12628 &[],
12629 1,
12630 None,
12631 None,
12632 Some(&mut layers_run),
12633 0,
12634 false,
12635 );
12636 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12637 return None;
12638 }
12639 lg.resize(self.vocab_size, 0.0);
12642 if let Some(m) = self.logit_multiplier {
12643 for l in lg.iter_mut() {
12644 *l *= m;
12645 }
12646 }
12647 if let Some(c) = self.final_softcap {
12648 softcap_in_place(&mut lg, c, self.pool.as_deref());
12649 }
12650 Some(lg)
12651 }
12652
12653 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12656 if winners.is_empty() {
12657 return;
12658 }
12659 if self.m3_n_exp == 0 {
12660 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12661 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12662 }
12663 for &e in winners {
12664 let e = e as usize;
12665 if e < self.m3_n_exp && li < self.num_layers {
12666 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12667 }
12668 }
12669 if li + 1 == self.num_layers {
12670 self.m3_tokens += 1;
12671 if self.m3_tokens % 16 == 0 {
12672 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12673 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12674 let _ = std::fs::write(&path, bytes);
12675 }
12676 }
12677 }
12678 }
12679
12680 fn m3_prewarm(&mut self) {
12685 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12686 return;
12687 };
12688 let Ok(bytes) = std::fs::read(&path) else {
12689 tracing::warn!("m3 prewarm: cannot read profile {path}");
12690 return;
12691 };
12692 let counts: Vec<u32> = bytes
12693 .chunks_exact(4)
12694 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12695 .collect();
12696 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12697 if counts.len() != self.num_layers * n_exp {
12698 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12699 return;
12700 }
12701 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12702 .ok()
12703 .and_then(|v| v.parse().ok())
12704 .unwrap_or(300);
12705 let mut ranked: Vec<(u32, usize, usize)> = counts
12706 .iter()
12707 .enumerate()
12708 .filter(|(_, c)| **c > 0)
12709 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12710 .collect();
12711 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12712 ranked.truncate(limit);
12713 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12714 for (_, li, e) in ranked {
12715 per_layer[li].push(e);
12716 }
12717 let mut admitted = 0usize;
12718 for (li, ids) in per_layer.iter().enumerate() {
12719 if ids.is_empty() {
12720 continue;
12721 }
12722 let lw = &self.weights.layers[self.phys_layer(li)];
12723 let FfnKind::Moe(m) = &lw.ffn else {
12724 continue;
12725 };
12726 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12728 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12729 admitted += ids.len();
12730 }
12731 }
12732 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12733 }
12734
12735 #[cfg(feature = "gpu")]
12746 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12747 if self.o1_active() || self.attn_softcap > 0.0 {
12748 return Ok(false);
12749 }
12750 if !self.m3_prewarmed {
12751 self.m3_prewarmed = true;
12752 self.m3_prewarm();
12753 }
12754 let frame = {
12755 let lw = &self.weights.layers[self.phys_layer(li)];
12756 let FfnKind::Moe(m) = &lw.ffn else {
12757 return Ok(false);
12758 };
12759 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12760 return Ok(false);
12761 }
12762 let t_arm = std::time::Instant::now();
12763 if self.m3_winners.len() != self.num_layers {
12764 self.m3_winners = vec![Vec::new(); self.num_layers];
12765 }
12766 let pred = std::mem::take(&mut self.m3_winners[li]);
12767 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12768 if armed.is_none() {
12769 self.m3_winners[li] = pred; }
12771 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12772 match armed {
12773 Some(frame) => frame,
12774 None => return Ok(false),
12777 }
12778 };
12779 crate::gpu_wgpu::pooled_frame_enter(frame);
12780 let want_logits = self.graph_want_logits;
12784 self.graph_want_logits = false;
12785 let mut lg = Vec::new();
12786 let mut gl = 0usize;
12787 let t_span = std::time::Instant::now();
12788 let span = crate::gpu::gpu_scope(|| {
12789 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12790 });
12791 self.graph_want_logits = want_logits;
12792 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12793 let hh = match span {
12794 Some(Ok(hh)) => hh,
12795 Some(Err(())) => return Err(()),
12796 None => {
12797 return Ok(false);
12800 }
12801 };
12802 if gl != 1 {
12803 return if gl == 0 { Ok(false) } else { Err(()) };
12806 }
12807 *h = hh;
12808 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12809 self.m3_note_route(li, &winners);
12810 if self.m3_winners.len() != self.num_layers {
12811 self.m3_winners = vec![Vec::new(); self.num_layers];
12812 }
12813 self.m3_winners[li] = winners;
12814 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12815 cold.retain(|&(_, w)| {
12818 if w.is_finite() {
12819 true
12820 } else {
12821 tracing::error!("pooled tail: non-finite cold weight from expert route");
12822 false
12823 }
12824 });
12825 if cold.is_empty() {
12826 return Ok(true);
12827 }
12828 let x = crate::gpu_wgpu::pooled_frame_take_input();
12829 if x.len() != self.hidden_size {
12830 return Err(());
12831 }
12832 let lw = &self.weights.layers[self.phys_layer(li)];
12838 let FfnKind::Moe(m) = &lw.ffn else {
12839 return Err(());
12840 };
12841 let t_cold = std::time::Instant::now();
12842 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12843 let mut out = crate::attention::take_buf(self.hidden_size);
12844 let on_device = !force_host
12845 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12846 if !on_device {
12847 crate::attention::recycle_buf(&mut out);
12848 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12849 .iter()
12850 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12851 .collect::<Result<_, ()>>()?;
12852 out = crate::gpu::cpu_scope(|| {
12853 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12854 });
12855 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12856 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12857 }
12858 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12859 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12860 if out.len() != h.len() {
12861 crate::attention::recycle_buf(&mut out);
12862 return Err(());
12863 }
12864 for (dst, src) in h.iter_mut().zip(out.iter()) {
12865 *dst += src;
12866 }
12867 crate::attention::recycle_buf(&mut out);
12868 if on_device {
12869 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12870 }
12871 Ok(true)
12872 }
12873
12874 #[cfg(not(feature = "gpu"))]
12875 fn m3_pooled_layer(
12876 &mut self,
12877 _li: usize,
12878 _position: usize,
12879 _h: &mut Vec<f32>,
12880 ) -> Result<bool, ()> {
12881 Ok(false)
12882 }
12883
12884 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12888 if self.o1_active() || self.attn_softcap > 0.0 {
12889 return None;
12890 }
12891 if let Some(reason) = self.wgpu_graph_attn_decline() {
12894 self.note_graph_decline("wgpu multi-burst", reason);
12895 return None;
12896 }
12897 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12898 if !graph_on || self.graph_refused() {
12899 return None;
12906 }
12907 let emb = self.embed_single(t_next);
12908 let mut lg = Vec::new();
12909 let mut ids = Vec::new();
12910 match self.try_token_graph_wgpu_steps(
12911 &emb,
12912 position,
12913 &mut lg,
12914 k,
12915 Some(&mut ids),
12916 None,
12917 0,
12918 self.num_layers,
12919 ) {
12920 Some(Ok(_)) => {}
12921 Some(Err(())) => {
12922 self.graph_failed
12927 .store(true, std::sync::atomic::Ordering::Relaxed);
12928 return None;
12929 }
12930 None => return None,
12931 }
12932 (ids.len() == k).then_some(ids)
12933 }
12934
12935 fn try_token_graph_wgpu_steps(
12939 &self,
12940 hidden: &[f32],
12941 position: usize,
12942 logits_out: &mut Vec<f32>,
12943 steps: usize,
12944 ids_out: Option<&mut Vec<u32>>,
12945 layers_run: Option<&mut usize>,
12946 from: usize,
12947 upto_excl: usize,
12948 ) -> Option<Result<Vec<f32>, ()>> {
12949 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12952 Some(end) if end < upto_excl => {
12953 if steps != 1 || layers_run.is_none() || from >= end {
12954 return None;
12955 }
12956 end
12957 }
12958 _ => upto_excl,
12959 };
12960 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12963 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12964 return None;
12968 }
12969 if let Some(reason) = self.wgpu_graph_attn_decline() {
12976 self.note_graph_decline("wgpu token graph", reason);
12977 return None;
12978 }
12979 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12984 .map(|li| {
12985 if !o1_gpu {
12986 return None;
12987 }
12988 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12989 })
12990 .collect();
12991 if self.o1_active() && o1_gpu {
12992 let want: usize = (from..upto_excl)
12995 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12996 .count();
12997 let have = o1_views.iter().filter(|v| v.is_some()).count();
12998 if want == 0 || have != want {
12999 use std::sync::atomic::{AtomicUsize, Ordering};
13009 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13010 let code = have * 1000 + want;
13011 if LAST.swap(code, Ordering::Relaxed) != code {
13012 tracing::warn!(
13013 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13014 );
13015 }
13016 return None;
13017 }
13018 }
13019 let nh = self.num_heads;
13020 let (nkv, hd, rd) = self.layer_geom(0);
13021 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13022 let mut layers = Vec::with_capacity(upto_excl - from);
13023 let mut model = None;
13024 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13025 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13026 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13028 let name = &m.tensors[i].name;
13029 let prism = if crate::prism::is_inverse_embedding(m, name) {
13030 crate::gpu::GraphPrismOp::InverseEmbedding
13031 } else if crate::prism::is_forward_weight(m, name) {
13032 crate::gpu::GraphPrismOp::Forward
13033 } else {
13034 crate::gpu::GraphPrismOp::None
13035 };
13036 return Some(crate::gpu::GraphW {
13037 idx: i,
13038 kind,
13039 row_scale: rs,
13040 data: &[],
13041 prism,
13042 affine: crate::prism::is_affine_target(m, name),
13043 });
13044 }
13045 match t.as_f32() {
13047 Some(d) => Some(crate::gpu::GraphW {
13048 idx: 0,
13049 kind: 4,
13050 row_scale: &[],
13051 data: d,
13052 prism: crate::gpu::GraphPrismOp::None,
13053 affine: false,
13054 }),
13055 None => {
13056 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13057 eprintln!("batch graph: weight has no graph/f32 representation");
13058 }
13059 None
13060 }
13061 }
13062 }
13063 for li in from..upto_excl {
13064 let lw = &self.weights.layers[self.phys_layer(li)];
13065 if dbg {
13066 let ak = match &lw.attn {
13067 AttnKind::Mla(_) => "Mla".into(),
13068 AttnKind::Full {
13069 output_gate, bias, ..
13070 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13071 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13072 AttnKind::Kda(_) => "Kda".into(),
13073 AttnKind::Linear(_) => "Linear".into(),
13074 AttnKind::ShortConv(_) => "ShortConv".into(),
13075 AttnKind::Bounded(_) => "Bounded".into(),
13076 };
13077 let fk = match &lw.ffn {
13078 FfnKind::Dense(_) => "Dense",
13079 FfnKind::Moe(_) => "Moe",
13080 FfnKind::DenseMoe(_) => "DenseMoe",
13081 };
13082 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13083 }
13084 let gffn = match &lw.ffn {
13085 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13089 FfnKind::Dense(d) => {
13090 let Some(act) = d.act.wgpu_graph_act() else {
13094 self.note_graph_decline(
13095 "wgpu token graph",
13096 "dense FFN activation without a graph kernel",
13097 );
13098 return None;
13099 };
13100 crate::gpu::GraphFfn::Dense {
13101 gate: gw(&d.gate_proj)?,
13102 up: gw(&d.up_proj)?,
13103 down: gw(&d.down_proj)?,
13104 act,
13105 }
13106 }
13107 FfnKind::Moe(m) => {
13108 if m.route_tau.is_some() || m.mask.is_some() {
13116 return None;
13117 }
13118 let shared = m.shared.as_ref();
13119 let inter = m.experts.first()?.gate_proj.rows();
13125 let detached_shared = match shared {
13133 Some((se, None))
13134 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13135 {
13136 if !matches!(se.act, Act::Silu)
13137 || se.gate_proj.mapped_q4tp().is_none()
13138 || se.up_proj.mapped_q4tp().is_none()
13139 || se.down_proj.mapped_q4tp().is_none()
13140 {
13141 return None;
13142 }
13143 Some(crate::gpu::GraphMoeDetachedShared {
13144 gate: gw(&se.gate_proj)?,
13145 up: gw(&se.up_proj)?,
13146 down: gw(&se.down_proj)?,
13147 width: se.gate_proj.rows(),
13148 })
13149 }
13150 Some((se, Some(_)))
13154 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13155 {
13156 return None;
13157 }
13158 _ => None,
13159 };
13160 let has_shared = shared.is_some() && detached_shared.is_none();
13161 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13162 let sgate = match shared {
13163 Some((_, Some(sg))) if has_shared => gw(sg)?,
13164 _ => gw(&m.router)?,
13168 };
13169 let router = gw(&m.router)?;
13170 if router.prism != crate::gpu::GraphPrismOp::None
13176 || sgate.prism != crate::gpu::GraphPrismOp::None
13177 || router.affine
13178 || sgate.affine
13179 {
13180 tracing::warn!(
13181 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13182 );
13183 return None;
13184 }
13185 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13186 let mut q4tp: Option<bool> = None;
13189 let mut gu_q2: Option<bool> = None;
13192 let mut q82: Option<bool> = None;
13194 let mut moe_act = None;
13198 for e in m
13199 .experts
13200 .iter()
13201 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13202 {
13203 let Some(e_act) = e.act.wgpu_graph_act() else {
13204 return None;
13205 };
13206 match moe_act {
13207 Some(existing) if existing != e_act => return None,
13208 None => moe_act = Some(e_act),
13209 _ => {}
13210 }
13211 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13212 return None;
13213 }
13214 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13219 let Some((em, ei, _, _)) = expert_weight
13220 .graph_weight()
13221 .or_else(|| expert_weight.graph_weight_descriptor())
13222 else {
13223 return None;
13224 };
13225 let name = &em.tensors[ei].name;
13226 if crate::prism::is_forward_weight(em, name)
13227 || crate::prism::is_inverse_embedding(em, name)
13228 || crate::prism::is_affine_target(em, name)
13229 {
13230 tracing::warn!(
13231 "resident MoE declined: expert Prism/affine transform is not implemented"
13232 );
13233 return None;
13234 }
13235 }
13236 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13237 Some((mm, gi)) => (
13238 mm,
13239 gi,
13240 e.up_proj.mapped_q4t()?.1,
13241 e.down_proj.mapped_q4t()?.1,
13242 false,
13243 false,
13244 false,
13245 ),
13246 None => match e.gate_proj.mapped_q2tp() {
13247 Some((mm, gi)) => (
13248 mm,
13249 gi,
13250 e.up_proj.mapped_q2tp()?.1,
13251 e.down_proj.mapped_q4tp()?.1,
13252 true,
13253 true,
13254 false,
13255 ),
13256 None => match e.gate_proj.mapped_q4tp() {
13257 Some((mm, gi)) => (
13258 mm,
13259 gi,
13260 e.up_proj.mapped_q4tp()?.1,
13261 e.down_proj.mapped_q4tp()?.1,
13262 true,
13263 false,
13264 false,
13265 ),
13266 None => {
13269 if !crate::gpu::moe_q82_graph_on() {
13270 return None;
13271 }
13272 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13273 (
13274 mm,
13275 gi,
13276 e.up_proj.mapped_q8_2f()?.1,
13277 e.down_proj.mapped_q8_2f()?.1,
13278 false,
13279 false,
13280 true,
13281 )
13282 }
13283 },
13284 },
13285 };
13286 if *q4tp.get_or_insert(is_p) != is_p
13287 || *gu_q2.get_or_insert(is_q2) != is_q2
13288 || *q82.get_or_insert(is_8) != is_8
13289 {
13290 tracing::warn!(
13296 "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."
13297 );
13298 return None;
13299 }
13300 model.get_or_insert_with(|| mm.clone());
13301 experts.push((gi, ui, di));
13302 }
13303 crate::gpu::GraphFfn::Moe {
13304 router,
13305 shared_gate: sgate,
13306 experts,
13307 n_exp: m.experts.len(),
13308 top_k: std::env::var("CMF_TOPK_PROBE")
13314 .ok()
13315 .and_then(|v| v.parse::<usize>().ok())
13316 .filter(|k| *k > 0 && *k <= m.top_k)
13317 .unwrap_or(m.top_k),
13318 inter,
13319 act: moe_act?,
13320 norm_topk: m.norm_topk_prob,
13321 q4tp: q4tp?,
13322 gu_q2: gu_q2.unwrap_or(false),
13323 q82: q82.unwrap_or(false),
13324 sigmoid: m.router_sigmoid,
13325 sigmoid_denom_eps: m.sigmoid_denom_eps,
13326 bias: m.expert_bias.as_deref(),
13327 has_shared,
13328 detached_shared,
13329 shared_gated,
13330 route_scale: m.routed_scaling,
13331 }
13332 }
13333 };
13334 let attn = match &lw.attn {
13335 AttnKind::Full {
13336 wq,
13337 wk,
13338 wv,
13339 wo,
13340 q_norm,
13341 k_norm,
13342 output_gate,
13343 softplus_gate,
13344 bias,
13345 } => {
13346 if self.attention_heads_per_layer.is_some() {
13347 return None;
13348 }
13349 let head_gate = match softplus_gate {
13353 None => None,
13354 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13355 Some(_) => {
13356 self.note_graph_decline(
13357 "wgpu token graph",
13358 "projected softplus / per-element output gate",
13359 );
13360 return None;
13361 }
13362 };
13363 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13364 model = Some(m.clone());
13365 crate::gpu::GraphAttn::Full {
13366 wq: gw(wq)?,
13367 wk: gw(wk)?,
13368 wv: gw(wv)?,
13369 wo: gw(wo)?,
13370 q_norm: q_norm.as_deref(),
13371 k_norm: k_norm.as_deref(),
13372 late_qk_norm: self.qk_norm_after_rope,
13373 bias: bias
13374 .as_ref()
13375 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13376 output_gate: *output_gate,
13377 cpu_k: self.kv_cache.layers[li].k_heads(),
13378 cpu_v: self.kv_cache.layers[li].v_heads(),
13379 cpu_base: self.kv_cache.layers[li].base(),
13380 geom: self.graph_attn_geom(li),
13381 head_gate,
13382 }
13383 }
13384 AttnKind::LinearGdn(w) => {
13385 let cfg = self.gdn_cfg?;
13386 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13387 model = Some(m.clone());
13388 crate::gpu::GraphAttn::Gdn {
13389 qkv: gw(&w.in_proj_qkv)?,
13390 z: gw(&w.in_proj_z)?,
13391 a: gw(&w.in_proj_a)?,
13392 b: gw(&w.in_proj_b)?,
13393 out: gw(&w.out_proj)?,
13394 conv1d: &w.conv1d,
13395 a_log: &w.a_log,
13396 dt_bias: &w.dt_bias,
13397 norm: &w.norm,
13398 nv: cfg.num_v_heads,
13399 nk: cfg.num_k_heads,
13400 dk: cfg.key_head_dim,
13401 dv: cfg.value_head_dim,
13402 kk: cfg.conv_kernel,
13403 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13404 preview: None,
13405 }
13406 }
13407 AttnKind::ShortConv(w) => {
13408 let cfg = self.short_conv_cfg?;
13409 let (m, _, _, _) = w
13410 .in_proj
13411 .graph_weight()
13412 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13413 model = Some(m.clone());
13414 crate::gpu::GraphAttn::ShortConv {
13415 inp: gw(&w.in_proj)?,
13416 out: gw(&w.out_proj)?,
13417 taps: &w.conv,
13418 kernel: cfg.kernel,
13419 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13420 }
13421 }
13422 _ => return None,
13423 };
13424 layers.push(crate::gpu::GraphLayer {
13425 input_norm: &lw.input_norm,
13426 attn,
13427 post_norm: &lw.post_norm,
13428 attn_out_norm: lw.attn_out_norm.as_deref(),
13429 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13430 layer_scale: lw.layer_scale,
13431 ffn: gffn,
13432 });
13433 }
13434 let model = model?;
13435 let lm_gw = if upto_excl == self.num_layers
13441 && self.graph_want_logits
13442 && std::env::var("CMF_GPU_LMHEAD")
13443 .map(|v| v != "0")
13444 .unwrap_or(true)
13445 {
13446 self.weights
13447 .lm_head
13448 .graph_weight()
13449 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13450 .map(|(m, i, kind, rs)| {
13451 let name = &m.tensors[i].name;
13452 let prism = if crate::prism::is_inverse_embedding(m, name) {
13453 crate::gpu::GraphPrismOp::InverseEmbedding
13454 } else if crate::prism::is_forward_weight(m, name) {
13455 crate::gpu::GraphPrismOp::Forward
13456 } else {
13457 crate::gpu::GraphPrismOp::None
13458 };
13459 (
13460 crate::gpu::GraphW {
13461 idx: i,
13462 kind,
13463 row_scale: rs,
13464 data: &[],
13465 prism,
13466 affine: crate::prism::is_affine_target(m, name),
13467 },
13468 self.weights.lm_head.rows(),
13469 )
13470 })
13471 } else {
13472 None
13473 };
13474 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13475 let emb_gw = if steps > 1 {
13477 self.weights
13478 .embed_tokens
13479 .graph_weight()
13480 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13481 .map(|(m, i, kind, rs)| {
13482 let name = &m.tensors[i].name;
13483 let prism = if crate::prism::is_inverse_embedding(m, name) {
13484 crate::gpu::GraphPrismOp::InverseEmbedding
13485 } else if crate::prism::is_forward_weight(m, name) {
13486 crate::gpu::GraphPrismOp::Forward
13487 } else {
13488 crate::gpu::GraphPrismOp::None
13489 };
13490 (
13491 crate::gpu::GraphW {
13492 idx: i,
13493 kind,
13494 row_scale: rs,
13495 data: &[],
13496 prism,
13497 affine: crate::prism::is_affine_target(m, name),
13498 },
13499 self.weights.embed_tokens.rows(),
13500 self.embed_multiplier,
13501 )
13502 })
13503 } else {
13504 None
13505 };
13506
13507 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13513 (from..upto_excl.min(self.num_layers - 1))
13514 .filter(|&li| (li + 1) % self.physical_layers == 0)
13515 .map(|li| li - from)
13516 .collect()
13517 } else {
13518 Vec::new()
13519 };
13520 let mut h = hidden.to_vec();
13521 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13527 let outcome = crate::gpu::forward_token_graph(
13528 &model,
13529 self.graph_kv_id,
13530 &layers,
13531 &o1_views,
13532 self.o1_epoch,
13533 &self.inv_freq,
13534 &mut h,
13535 nh,
13536 nkv,
13537 hd,
13538 self.attn_scale,
13539 rd,
13540 self.hidden_size,
13541 self.intermediate_size,
13542 position,
13543 self.kv_cache.max_seq_len,
13544 gemma,
13545 self.rms_eps as f32,
13546 lm,
13547 &self.weights.final_norm,
13548 logits_out,
13549 &loop_norm_at,
13550 steps,
13551 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13552 ids_out,
13553 layers_run,
13554 from,
13555 dump_hidden,
13556 );
13557 match outcome {
13558 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13559 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13560 crate::gpu::TokenGraphOutcome::Declined => None,
13561 }
13562 }
13563
13564 #[cfg(target_os = "macos")]
13573 #[allow(clippy::type_complexity)]
13574 fn metal_rows_plan(
13575 &self,
13576 ) -> Option<(
13577 Vec<MetalRowsItem<'_>>,
13578 std::sync::Arc<cortiq_core::CmfModel>,
13579 Option<crate::gpu_metal::GdnGpuCfg>,
13580 )> {
13581 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13582 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13583 if !graph_force
13584 || !crate::gpu::enabled_here()
13585 || std::env::var("CMF_GPU_BLOCK")
13586 .map(|v| v == "0")
13587 .unwrap_or(false)
13588 || self.attn_softcap > 0.0
13589 || self.o1_active()
13590 || self.swa.is_some()
13591 || self.global_attn.is_some()
13592 || self.attention_heads_per_layer.is_some()
13593 || self.graph_attn_decline_reason().is_some()
13595 || self.attn_v_norm
13596 || self.loop_final_norm
13597 || !self.metal_rope_scale_ok()
13598 {
13599 return None;
13600 }
13601 let attend_contract = self.head_dim % 4 == 0
13602 && self.head_dim <= 256
13603 && self.rotary_dim >= 2
13604 && self.rotary_dim <= self.head_dim
13605 && (self.rotary_dim / 2) % 32 == 0
13606 && self.num_kv_heads > 0
13607 && self.num_heads % self.num_kv_heads == 0;
13608 if !attend_contract {
13609 return None;
13610 }
13611 let mut plan: Vec<MetalRowsItem> = Vec::new();
13612 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13613 for li in 0..self.num_layers {
13614 let lw = &self.weights.layers[self.phys_layer(li)];
13615 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13616 return None;
13617 }
13618 let ffn = match &lw.ffn {
13619 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13620 let (Some(g), Some(u), Some(dn)) = (
13621 d.gate_proj.metal_graph_parts(),
13622 d.up_proj.metal_graph_parts(),
13623 d.down_proj.metal_graph_parts(),
13624 ) else {
13625 return None;
13626 };
13627 MetalFfn::Dense {
13628 gate: g,
13629 up: u,
13630 down: dn,
13631 act: 0, }
13633 }
13634 _ => return None,
13635 };
13636 match &lw.attn {
13637 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13638 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13639 w.in_proj_qkv.metal_graph_parts(),
13640 w.in_proj_z.metal_graph_parts(),
13641 w.in_proj_a.f32_parts(),
13642 w.in_proj_b.f32_parts(),
13643 w.out_proj.metal_graph_parts(),
13644 ) else {
13645 return None;
13646 };
13647 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13648 model_ref.get_or_insert_with(|| model.clone());
13649 }
13650 let gl = GdnGpuLayer {
13651 attn_norm: &lw.input_norm,
13652 post_norm: &lw.post_norm,
13653 qkv,
13654 z,
13655 a,
13656 b: bb,
13657 out,
13658 ffn,
13659 conv1d: &w.conv1d,
13660 a_log: &w.a_log,
13661 dt_bias: &w.dt_bias,
13662 gnorm: &w.norm,
13663 };
13664 match plan.last_mut() {
13665 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13666 _ => plan.push(MetalRowsItem::Gdn {
13667 run: vec![gl],
13668 first: li,
13669 }),
13670 }
13671 }
13672 AttnKind::Full {
13673 wq,
13674 wk,
13675 wv,
13676 wo,
13677 q_norm,
13678 k_norm,
13679 output_gate,
13680 softplus_gate: None,
13681 bias: None,
13682 } => {
13683 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13684 wq.metal_graph_parts(),
13685 wk.metal_graph_parts(),
13686 wv.metal_graph_parts(),
13687 wo.metal_graph_parts(),
13688 ) else {
13689 return None;
13690 };
13691 if let QTensor::Mapped { model, .. } = wq {
13692 model_ref.get_or_insert_with(|| model.clone());
13693 }
13694 let cache = &self.kv_cache.layers[li];
13695 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13696 return None;
13697 }
13698 plan.push(MetalRowsItem::Attn {
13699 l: AttnGpuLayer {
13700 attn_norm: &lw.input_norm,
13701 post_norm: &lw.post_norm,
13702 wq: pq,
13703 wk: pk,
13704 wv: pv,
13705 wo: po,
13706 ffn,
13707 attn_out_norm: None,
13708 ffn_out_norm: None,
13709 layer_scale: None,
13710 },
13711 li,
13712 q_norm: q_norm.as_deref(),
13713 k_norm: k_norm.as_deref(),
13714 output_gate: *output_gate,
13715 });
13716 }
13717 _ => return None,
13718 }
13719 }
13720 let model = model_ref?;
13721 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13722 nv: cfg.num_v_heads,
13723 nk: cfg.num_k_heads,
13724 dk: cfg.key_head_dim,
13725 dv: cfg.value_head_dim,
13726 kk: cfg.conv_kernel,
13727 hidden: self.hidden_size,
13728 inter: self.intermediate_size,
13729 c_dim: cfg.conv_dim(),
13730 eps: cfg.rms_eps as f32,
13731 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13732 });
13733 Some((plan, model, gcfg))
13734 }
13735
13736 #[cfg(target_os = "macos")]
13738 #[allow(clippy::too_many_arguments)]
13739 fn metal_attn_params<'a>(
13740 li: usize,
13741 cache: &'a crate::kv_cache::LayerKvCache,
13742 q_norm: Option<&'a [f32]>,
13743 k_norm: Option<&'a [f32]>,
13744 output_gate: bool,
13745 inv_freq: &'a [f32],
13746 rope_scale: f32,
13747 geom: (usize, usize, usize, usize),
13748 pos0: usize,
13749 kv_id: u64,
13750 scale: f32,
13751 eps: f32,
13752 gemma: bool,
13753 late_qk_norm: bool,
13754 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13755 let (nh, nkv, hd, rd) = geom;
13756 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13757 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13758 let cpu_stored = cpu_k[0].len() / hd;
13759 (
13760 crate::gpu_metal::AttnDeviceParams {
13761 kv_id,
13762 layer: li,
13763 nh,
13764 nkv,
13765 hd,
13766 rd,
13767 position: pos0,
13768 scale,
13769 eps,
13770 gemma,
13771 late_qk_norm,
13772 output_gate,
13773 q_norm,
13774 k_norm,
13775 inv_freq,
13776 rope_scale,
13777 cpu_k,
13778 cpu_v,
13779 cpu_stored,
13780 cpu_gen: cache.generation(),
13781 o1: None,
13782 window: None,
13783 softmax_sink: cache.sinks.as_deref(),
13784 head_gate: None,
13785 v_norm: false,
13786 },
13787 cpu_stored,
13788 )
13789 }
13790
13791 #[cfg(target_os = "macos")]
13796 #[allow(clippy::type_complexity)]
13797 fn metal_rows_run(
13798 &mut self,
13799 hiddens: &mut [f32],
13800 pos0: usize,
13801 b: usize,
13802 prefill: bool,
13803 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13804 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13808 ) -> MetalRowsRun {
13809 use crate::gpu_metal::{GraphDims, VerifyGraph};
13810 if !crate::gpu_metal::wait_replay() {
13816 tracing::error!("Metal rows graph: the pending async replay failed");
13817 return MetalRowsRun::Failed;
13818 }
13819 spec_stamp("v.wait");
13820 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13826 if want > 0 {
13827 let phys = self.physical_layers.max(1);
13828 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13829 let is_gdn = self
13830 .weights
13831 .layers
13832 .get(li % phys)
13833 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13834 if is_gdn && l.linear_state.len() != want {
13835 l.linear_state = vec![0f32; want];
13836 }
13837 }
13838 }
13839 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13840 return MetalRowsRun::Declined;
13841 };
13842 spec_stamp("v.plan");
13843 let dims = GraphDims {
13844 hidden: self.hidden_size,
13845 eps: self.rms_eps as f32,
13846 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13847 };
13848 let Some(mut graph) = (if prefill {
13849 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13850 } else {
13851 VerifyGraph::new(&model, dims, hiddens, b)
13852 }) else {
13853 return MetalRowsRun::Declined;
13854 };
13855 let geom = (
13856 self.num_heads,
13857 self.num_kv_heads,
13858 self.head_dim,
13859 self.rotary_dim,
13860 );
13861 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13862 let eps = self.rms_eps as f32;
13863 let kv_id = self.graph_kv_id;
13864 let inv_freq = self.inv_freq.clone();
13865 for item in &plan {
13866 let ok = match item {
13867 MetalRowsItem::Gdn { run, .. } => gcfg
13868 .as_ref()
13869 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13870 .unwrap_or(false),
13871 MetalRowsItem::Attn {
13872 l,
13873 li,
13874 q_norm,
13875 k_norm,
13876 output_gate,
13877 } => {
13878 let (p, _) = Self::metal_attn_params(
13879 *li,
13880 &self.kv_cache.layers[*li],
13881 *q_norm,
13882 *k_norm,
13883 *output_gate,
13884 &inv_freq,
13885 self.layer_rope_scale(*li),
13886 geom,
13887 pos0,
13888 kv_id,
13889 self.attn_scale,
13890 eps,
13891 gemma,
13892 self.qk_norm_after_rope,
13893 );
13894 graph.attn_ok(l, &p)
13895 }
13896 };
13897 if !ok {
13898 use std::sync::atomic::{AtomicBool, Ordering};
13899 static SAID: AtomicBool = AtomicBool::new(false);
13900 if !SAID.swap(true, Ordering::Relaxed) {
13901 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13902 }
13903 return MetalRowsRun::Declined;
13904 }
13905 }
13906 let lm = match &spec {
13907 Some((lm, _, _)) => {
13908 if !graph.lm_head_ok(*lm) {
13909 return MetalRowsRun::Declined;
13910 }
13911 Some(*lm)
13912 }
13913 None => None,
13914 };
13915 let mut gdn_layers = Vec::new();
13916 let mut attn_layers = Vec::new();
13917 for item in &plan {
13918 match item {
13919 MetalRowsItem::Gdn { run, first } => {
13920 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13921 .iter()
13922 .map(|l| l.linear_state.as_slice())
13923 .collect();
13924 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13925 return MetalRowsRun::Declined;
13926 }
13927 gdn_layers.extend(*first..*first + run.len());
13928 }
13929 MetalRowsItem::Attn {
13930 l,
13931 li,
13932 q_norm,
13933 k_norm,
13934 output_gate,
13935 } => {
13936 let (p, cpu_stored) = Self::metal_attn_params(
13937 *li,
13938 &self.kv_cache.layers[*li],
13939 *q_norm,
13940 *k_norm,
13941 *output_gate,
13942 &inv_freq,
13943 self.layer_rope_scale(*li),
13944 geom,
13945 pos0,
13946 kv_id,
13947 self.attn_scale,
13948 eps,
13949 gemma,
13950 self.qk_norm_after_rope,
13951 );
13952 if !graph.encode_attn_b(l, &p) {
13953 return MetalRowsRun::Declined;
13954 }
13955 attn_layers.push((*li, cpu_stored));
13956 }
13957 }
13958 }
13959 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13960 if !graph.encode_lm_head_b(final_norm, lm) {
13961 return MetalRowsRun::Declined;
13962 }
13963 if let Some((n, _)) = argmax_out.as_ref() {
13968 if !graph.encode_argmax_b(*n) {
13969 argmax_out = None;
13970 }
13971 }
13972 }
13973 spec_stamp("v.enc");
13974 if !graph.sync() {
13975 return MetalRowsRun::Failed;
13976 }
13977 spec_stamp("v.gpu");
13978 match (spec, argmax_out) {
13979 (Some(_), Some((_, ids))) => {
13980 ids.resize(b, 0);
13981 if !graph.read_argmax(ids) {
13982 return MetalRowsRun::Failed;
13983 }
13984 spec_stamp("v.am");
13985 }
13986 (Some((lm, _, logits)), None) => {
13987 logits.resize(b * lm.1, 0.0);
13988 if !graph.read_logits(logits) {
13989 return MetalRowsRun::Failed;
13990 }
13991 spec_stamp("v.lg");
13992 }
13993 (None, _) => {}
13994 }
13995 if !graph.read_hidden(hiddens) {
13996 return MetalRowsRun::Failed;
13997 }
13998 spec_stamp("v.hid");
13999 MetalRowsRun::Completed(MetalVerifyPending {
14000 graph,
14001 gdn_layers,
14002 attn_layers,
14003 })
14004 }
14005
14006 #[cfg(target_os = "macos")]
14012 fn try_batch_graph_metal(
14013 &mut self,
14014 hiddens: &mut [f32],
14015 positions: &[usize],
14016 b: usize,
14017 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14018 argmax_out: Option<(usize, &mut Vec<u32>)>,
14019 ) -> crate::gpu::BatchGraphOutcome {
14020 let _t0 = std::time::Instant::now();
14021 if positions.len() != b
14022 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14023 || hiddens.len() != b * self.hidden_size
14024 {
14025 return crate::gpu::BatchGraphOutcome::Declined;
14026 }
14027 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14028 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14029 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14030 MetalRowsRun::Completed(pending) => pending,
14031 };
14032 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14033 eprintln!(
14034 "metal-verify: {:.1} ms | b={b}",
14035 _t0.elapsed().as_secs_f64() * 1e3
14036 );
14037 }
14038 self.metal_verify = Some(pending);
14039 crate::gpu::BatchGraphOutcome::Completed
14040 }
14041
14042 #[cfg(target_os = "macos")]
14047 fn prefill_rows_metal(
14048 &mut self,
14049 ids: &[u32],
14050 start_pos: usize,
14051 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14052 ) -> MetalPrefillOutcome {
14053 let b = ids.len();
14054 if b == 0 || b > 512 {
14055 return MetalPrefillOutcome::Declined;
14056 }
14057 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14058 let with_head = spec.is_some();
14059 let hs = self.hidden_size;
14060 let mut hiddens = vec![0f32; b * hs];
14061 for (j, &id) in ids.iter().enumerate() {
14062 let e = self.embed_single(id);
14063 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14064 }
14065 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14066 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14067 MetalRowsRun::Failed => {
14068 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14069 return MetalPrefillOutcome::Failed;
14070 }
14071 MetalRowsRun::Completed(pending) => pending,
14072 };
14073 let idxs = pending.gdn_layers.clone();
14075 let mut outs: Vec<&mut [f32]> = self
14076 .kv_cache
14077 .layers
14078 .iter_mut()
14079 .enumerate()
14080 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14081 .map(|(_, l)| l.linear_state.as_mut_slice())
14082 .collect();
14083 if !pending.graph.finish_states(&mut outs) {
14084 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14085 return MetalPrefillOutcome::Failed;
14086 }
14087 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14088 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14092 for (li, cpu_stored) in &pending.attn_layers {
14093 let mut kbuf = vec![0f32; b * nkv * hd];
14094 let mut vbuf = vec![0f32; b * nkv * hd];
14095 if !crate::gpu_metal::kv_mirror_read_rows(
14096 self.graph_kv_id,
14097 *li,
14098 nkv,
14099 hd,
14100 *cpu_stored,
14101 b,
14102 &mut kbuf,
14103 &mut vbuf,
14104 ) {
14105 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14106 return MetalPrefillOutcome::Failed;
14107 }
14108 rows.push((*li, *cpu_stored, kbuf, vbuf));
14109 }
14110 for (li, cpu_stored, kbuf, vbuf) in rows {
14111 let cache = &mut self.kv_cache.layers[li];
14112 for r in 0..b {
14113 cache.append(
14114 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14115 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14116 &[],
14117 );
14118 }
14119 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14120 }
14121 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14122 if with_head {
14123 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14124 }
14125 MetalPrefillOutcome::Completed(hiddens)
14126 }
14127
14128 #[cfg(target_os = "macos")]
14129 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14130 self.prefill_rows_metal(ids, start_pos, None)
14131 }
14132
14133 #[cfg(target_os = "macos")]
14138 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14139 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14140 return MetalBatchNllOutcome::Declined;
14141 }
14142 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14143 return MetalBatchNllOutcome::Declined;
14144 };
14145 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14146 .ok()
14147 .and_then(|v| v.parse::<usize>().ok())
14148 .filter(|&v| (1..=512).contains(&v))
14149 .unwrap_or(32);
14150 let final_norm = self.weights.final_norm.clone();
14151 let mut nll = 0.0f64;
14152 let mut count = 0usize;
14153 let mut pos = 0usize;
14154 let mut completed = 0usize;
14155 while pos < ids.len() {
14156 let end = (pos + chunk).min(ids.len());
14157 let mut logits = Vec::new();
14158 let outcome =
14159 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14160 match outcome {
14161 MetalPrefillOutcome::Declined => {
14162 return if completed == 0 {
14163 MetalBatchNllOutcome::Declined
14164 } else {
14165 MetalBatchNllOutcome::Failed(format!(
14166 "ordinary Metal NLL batch declined after {completed} chunks"
14167 ))
14168 };
14169 }
14170 MetalPrefillOutcome::Failed => {
14171 return MetalBatchNllOutcome::Failed(
14172 "ordinary Metal NLL batch failed after admission".to_string(),
14173 );
14174 }
14175 MetalPrefillOutcome::Completed(_) => {}
14176 }
14177 completed += 1;
14178 let vocab = self.vocab_size.min(lm.1);
14179 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14180 return MetalBatchNllOutcome::Failed(
14181 "ordinary Metal NLL head returned an invalid shape".to_string(),
14182 );
14183 }
14184 for row in 0..(end - pos) {
14185 let absolute = pos + row;
14186 if absolute < start || absolute + 1 >= ids.len() {
14187 continue;
14188 }
14189 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14190 if let Some(mu) = self.logit_multiplier {
14191 for v in lg.iter_mut() {
14192 *v *= mu;
14193 }
14194 }
14195 if let Some(c) = self.final_softcap {
14196 softcap_in_place(lg, c, self.pool.as_deref());
14197 }
14198 let target = ids[absolute + 1] as usize;
14199 if target >= vocab {
14200 return MetalBatchNllOutcome::Failed(format!(
14201 "target token {target} exceeds Metal head rows {vocab}"
14202 ));
14203 }
14204 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14205 let lse: f64 = lg
14206 .iter()
14207 .map(|&v| ((v - max) as f64).exp())
14208 .sum::<f64>()
14209 .ln()
14210 + max as f64;
14211 nll += lse - lg[target] as f64;
14212 count += 1;
14213 }
14214 pos = end;
14215 }
14216 MetalBatchNllOutcome::Completed(nll, count)
14217 }
14218
14219 #[cfg(target_os = "macos")]
14223 fn metal_verify_commit(&mut self, a: usize) -> bool {
14224 let Some(mut pending) = self.metal_verify.take() else {
14225 return false;
14226 };
14227 let n = a + 1;
14228 let idxs = pending.gdn_layers.clone();
14230 let mut outs: Vec<&mut [f32]> = self
14231 .kv_cache
14232 .layers
14233 .iter_mut()
14234 .enumerate()
14235 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14236 .map(|(_, l)| l.linear_state.as_mut_slice())
14237 .collect();
14238 if !pending.graph.commit(n, &mut outs) {
14239 return false;
14240 }
14241 spec_stamp("c.replay");
14242 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14243 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14247 for (li, cpu_stored) in &pending.attn_layers {
14248 let mut kbuf = vec![0f32; n * nkv * hd];
14249 let mut vbuf = vec![0f32; n * nkv * hd];
14250 if !crate::gpu_metal::kv_mirror_read_rows(
14251 self.graph_kv_id,
14252 *li,
14253 nkv,
14254 hd,
14255 *cpu_stored,
14256 n,
14257 &mut kbuf,
14258 &mut vbuf,
14259 ) {
14260 return false;
14261 }
14262 rows.push((*li, *cpu_stored, kbuf, vbuf));
14263 }
14264 for (li, cpu_stored, kbuf, vbuf) in rows {
14265 let cache = &mut self.kv_cache.layers[li];
14266 for r in 0..n {
14267 cache.append(
14268 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14269 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14270 &[],
14271 );
14272 }
14273 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14274 }
14275 spec_stamp("c.kv");
14276 true
14277 }
14278
14279 #[cfg(target_os = "macos")]
14286 fn mtp_warm_batch_submit(
14287 &mut self,
14288 m: &mut MtpModule,
14289 pairs: &[(&[f32], u32)],
14290 first_pos: usize,
14291 ) -> Option<MetalWarmPending> {
14292 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14293 let b = pairs.len();
14294 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14295 return None;
14296 }
14297 let AttnKind::Full {
14298 wq,
14299 wk,
14300 wv,
14301 wo,
14302 q_norm,
14303 k_norm,
14304 output_gate,
14305 softplus_gate: None,
14306 bias: None,
14307 } = &m.layer.attn
14308 else {
14309 return None;
14310 };
14311 let FfnKind::Dense(d) = &m.layer.ffn else {
14312 return None;
14313 };
14314 if !d.segs.is_empty() {
14315 return None;
14316 }
14317 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14318 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14319 else {
14320 return None;
14321 };
14322 let (Some(g), Some(u), Some(dn)) = (
14323 d.gate_proj.q1_parts(),
14324 d.up_proj.q1_parts(),
14325 d.down_proj.q1_parts(),
14326 ) else {
14327 return None;
14328 };
14329 let Some(eh) = m.eh_proj.q1_parts() else {
14330 return None;
14331 };
14332 let QTensor::Mapped { model, .. } = wq else {
14333 return None;
14334 };
14335 let model = model.clone();
14336 let hs = self.hidden_size;
14337 let mut cat = vec![0f32; b * 2 * hs];
14339 for (j, (h, tok)) in pairs.iter().enumerate() {
14340 let e = self.embed_single(*tok);
14341 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14342 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14343 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14344 }
14345 let dims = GraphDims {
14346 hidden: hs,
14347 eps: self.rms_eps as f32,
14348 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14349 };
14350 spec_stamp("w.cat");
14351 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14352 return None;
14353 };
14354 spec_stamp("w.new");
14355 let l = AttnGpuLayer {
14356 attn_norm: &m.layer.input_norm,
14357 post_norm: &m.layer.post_norm,
14358 wq: pq,
14359 wk: pk,
14360 wv: pv,
14361 wo: po,
14362 ffn: MetalFfn::Dense {
14363 gate: g,
14364 up: u,
14365 down: dn,
14366 act: (d.act == Act::Gelu) as u8,
14367 },
14368 attn_out_norm: None,
14369 ffn_out_norm: None,
14370 layer_scale: None,
14371 };
14372 let (nh, nkv, hd, rd) = (
14373 self.num_heads,
14374 self.num_kv_heads,
14375 self.head_dim,
14376 self.rotary_dim,
14377 );
14378 let inv_freq = self.inv_freq.clone();
14379 let cpu_stored;
14380 {
14381 let cache = &m.kv;
14382 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14383 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14384 cpu_stored = cpu_k[0].len() / hd;
14385 if cpu_stored > first_pos {
14390 spec_stamp("w.decl");
14391 return None;
14392 }
14393 let p = AttnDeviceParams {
14394 kv_id: self.mtp_kv_id(),
14395 layer: Self::MTP_LAYER_BASE,
14396 nh,
14397 nkv,
14398 hd,
14399 rd,
14400 position: first_pos,
14401 scale: self.attn_scale,
14402 eps: self.rms_eps as f32,
14403 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14404 late_qk_norm: self.qk_norm_after_rope,
14405 output_gate: *output_gate,
14406 q_norm: q_norm.as_deref(),
14407 k_norm: k_norm.as_deref(),
14408 inv_freq: &inv_freq,
14409 rope_scale: self.rope_scale,
14410 cpu_k,
14411 cpu_v,
14412 cpu_stored,
14413 cpu_gen: cache.generation(),
14414 o1: None,
14415 window: None,
14416 softmax_sink: None,
14417 head_gate: None,
14418 v_norm: false,
14419 };
14420 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14421 return None;
14422 }
14423 }
14424 spec_stamp("w.enc");
14425 if !graph.submit() {
14426 return None;
14427 }
14428 spec_stamp("w.sub");
14429 Some(MetalWarmPending {
14430 graph,
14431 cpu_stored,
14432 b,
14433 })
14434 }
14435
14436 #[cfg(target_os = "macos")]
14439 fn mtp_warm_batch_metal(
14440 &mut self,
14441 m: &mut MtpModule,
14442 pairs: &[(&[f32], u32)],
14443 first_pos: usize,
14444 ) -> bool {
14445 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14446 Some(p) => self.mtp_warm_batch_finish(m, p),
14447 None => false,
14448 }
14449 }
14450
14451 #[cfg(target_os = "macos")]
14456 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14457 let MetalWarmPending {
14458 mut graph,
14459 cpu_stored,
14460 b,
14461 } = pending;
14462 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14463 if !graph.sync() {
14464 return false;
14465 }
14466 spec_stamp("w.gpu");
14467 let mut kbuf = vec![0f32; b * nkv * hd];
14468 let mut vbuf = vec![0f32; b * nkv * hd];
14469 if !crate::gpu_metal::kv_mirror_read_rows(
14470 self.mtp_kv_id(),
14471 Self::MTP_LAYER_BASE,
14472 nkv,
14473 hd,
14474 cpu_stored,
14475 b,
14476 &mut kbuf,
14477 &mut vbuf,
14478 ) {
14479 return false;
14480 }
14481 for r in 0..b {
14482 m.kv.append(
14483 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14484 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14485 &[],
14486 );
14487 }
14488 crate::gpu_metal::kv_mirror_set_stored(
14489 self.mtp_kv_id(),
14490 Self::MTP_LAYER_BASE,
14491 cpu_stored + b,
14492 );
14493 spec_stamp("w.kv");
14494 true
14495 }
14496
14497 pub(crate) fn note_draft_id(&mut self, id: u32) {
14504 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14505 if (id as usize) >= cut {
14506 self.draft_full_streak = 16;
14507 } else {
14508 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14509 }
14510 }
14511
14512 fn draft_head_rows(&self, head_rows: usize) -> usize {
14515 if self.draft_full_streak > 0 {
14516 head_rows
14517 } else {
14518 Self::draft_vocab_rows(head_rows)
14519 }
14520 }
14521
14522 fn draft_vocab_rows(head_rows: usize) -> usize {
14525 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14526 let n = *N.get_or_init(|| {
14527 std::env::var("CMF_DRAFT_VOCAB")
14528 .ok()
14529 .and_then(|v| v.parse().ok())
14530 .unwrap_or(65536)
14531 });
14532 if n == 0 { head_rows } else { n.min(head_rows) }
14533 }
14534
14535 #[cfg(target_os = "macos")]
14540 fn mtp_step_metal(
14541 &mut self,
14542 m: &mut MtpModule,
14543 hidden: &[f32],
14544 next_token: u32,
14545 position: usize,
14546 want_logits: bool,
14547 ) -> Option<(Vec<f32>, Vec<f32>)> {
14548 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14549 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14550 || !crate::gpu::q1_force()
14551 || !crate::gpu::enabled_here()
14552 || self.attn_softcap > 0.0
14553 || self.attention_heads_per_layer.is_some()
14554 || m.kv.mode != crate::kv_cache::KvMode::F32
14555 || m.kv.o1.is_some()
14556 {
14557 return None;
14558 }
14559 let AttnKind::Full {
14560 wq,
14561 wk,
14562 wv,
14563 wo,
14564 q_norm,
14565 k_norm,
14566 output_gate,
14567 softplus_gate: None,
14568 bias: None,
14569 } = &m.layer.attn
14570 else {
14571 return None;
14572 };
14573 let FfnKind::Dense(d) = &m.layer.ffn else {
14574 return None;
14575 };
14576 if d.act != Act::Silu || !d.segs.is_empty() {
14577 return None;
14578 }
14579 let (pq, pk, pv, po) = (
14580 wq.q1_parts()?,
14581 wk.q1_parts()?,
14582 wv.q1_parts()?,
14583 wo.q1_parts()?,
14584 );
14585 let (g, u, dn) = (
14586 d.gate_proj.q1_parts()?,
14587 d.up_proj.q1_parts()?,
14588 d.down_proj.q1_parts()?,
14589 );
14590 let QTensor::Mapped { model, .. } = wq else {
14591 return None;
14592 };
14593 let model = model.clone();
14594 let lm = if want_logits {
14595 Some(self.weights.lm_head.q1_parts()?)
14596 } else {
14597 None
14598 };
14599 let dims = GraphDims {
14600 hidden: self.hidden_size,
14601 eps: self.rms_eps as f32,
14602 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14603 };
14604 let hs = self.hidden_size;
14607 let mut x = vec![0f32; hs];
14608 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14615 crate::gpu_metal::DENSE_MV,
14616 );
14617 let mut graph = TokenGraph::new(&model, dims, &x)?;
14618 let mut folded = false;
14619 if let Some(eh) = m.eh_proj.q1_parts() {
14620 let e = self.embed_single(next_token);
14621 let mut cat = vec![0.0f32; 2 * hs];
14622 let (cat_e, cat_h) = cat.split_at_mut(hs);
14623 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14624 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14625 folded = graph.encode_input_proj(eh, &cat);
14626 }
14627 if !folded {
14628 x = self.mtp_block_input(m, hidden, next_token);
14629 graph = TokenGraph::new(&model, dims, &x)?;
14630 }
14631 spec_stamp("d.in");
14632 let l = AttnGpuLayer {
14633 attn_norm: &m.layer.input_norm,
14634 post_norm: &m.layer.post_norm,
14635 wq: pq,
14636 wk: pk,
14637 wv: pv,
14638 wo: po,
14639 ffn: MetalFfn::Dense {
14640 gate: g,
14641 up: u,
14642 down: dn,
14643 act: (d.act == Act::Gelu) as u8,
14644 },
14645 attn_out_norm: None,
14646 ffn_out_norm: None,
14647 layer_scale: None,
14648 };
14649 let (nh, nkv, hd, rd) = (
14650 self.num_heads,
14651 self.num_kv_heads,
14652 self.head_dim,
14653 self.rotary_dim,
14654 );
14655 let inv_freq = self.inv_freq.clone();
14656 {
14657 let cache = &m.kv;
14658 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14659 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14660 let cpu_stored = cpu_k[0].len() / hd;
14661 let p = AttnDeviceParams {
14662 kv_id: self.mtp_kv_id(),
14663 layer: Self::MTP_LAYER_BASE,
14664 nh,
14665 nkv,
14666 hd,
14667 rd,
14668 position,
14669 scale: self.attn_scale,
14670 eps: self.rms_eps as f32,
14671 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14672 late_qk_norm: self.qk_norm_after_rope,
14673 output_gate: *output_gate,
14674 q_norm: q_norm.as_deref(),
14675 k_norm: k_norm.as_deref(),
14676 inv_freq: &inv_freq,
14677 rope_scale: self.rope_scale,
14678 cpu_k,
14679 cpu_v,
14680 cpu_stored,
14681 cpu_gen: cache.generation(),
14682 o1: None,
14683 window: None,
14684 softmax_sink: None,
14685 head_gate: None,
14686 v_norm: false,
14687 };
14688 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14689 return None;
14690 }
14691 }
14692 let draft_rows = if let Some(lm) = lm {
14698 self.draft_head_rows(lm.1)
14699 } else {
14700 0
14701 };
14702 if let Some(lm) = lm {
14703 if !graph.lm_head_ok(lm) {
14704 return None;
14705 }
14706 if draft_rows < lm.1 {
14707 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14708 return None;
14709 }
14710 } else {
14711 graph.encode_lm_head(&m.final_norm, lm);
14712 }
14713 }
14714 spec_stamp("d.enc");
14715 if graph.sync_checked().is_err() {
14716 return None;
14717 }
14718 spec_stamp("d.gpu");
14719 let mut logits = Vec::new();
14720 if let Some(lm) = lm {
14721 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14722 logits = attention::take_buf(n_read);
14723 graph.read_logits(&mut logits);
14724 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14726 }
14727 graph.finish(&mut x);
14728 let mut krow = attention::take_buf(nkv * hd);
14729 let mut vrow = attention::take_buf(nkv * hd);
14730 if crate::gpu_metal::kv_mirror_read_last(
14731 self.mtp_kv_id(),
14732 Self::MTP_LAYER_BASE,
14733 nkv,
14734 hd,
14735 &mut krow,
14736 &mut vrow,
14737 ) {
14738 m.kv.append(&krow, &vrow, &[]);
14739 }
14740 attention::recycle_buf(&mut krow);
14741 attention::recycle_buf(&mut vrow);
14742 spec_stamp("d.rd");
14743 Some((logits, x))
14744 }
14745
14746 fn mtp_chain_on() -> bool {
14759 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14760 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14761 }
14762
14763 #[cfg(target_os = "macos")]
14775 fn mtp_draft_chain_metal(
14776 &mut self,
14777 m: &mut MtpModule,
14778 hidden: &[f32],
14779 t_next: u32,
14780 position: usize,
14781 k: usize,
14782 ) -> Result<Vec<u32>, bool> {
14783 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14784 if k == 0
14785 || k > 64
14786 || !Self::mtp_chain_on()
14787 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14788 || !crate::gpu::q1_force()
14789 || !crate::gpu::enabled_here()
14790 || self.attn_softcap > 0.0
14791 || self.attention_heads_per_layer.is_some()
14792 || m.kv.mode != crate::kv_cache::KvMode::F32
14793 || m.kv.o1.is_some()
14794 || self.dsv4.is_some()
14796 || self.dsv41.is_some()
14797 || self.qwen4_exp.is_some()
14798 || self.per_token_stack()
14799 {
14800 return Err(false);
14801 }
14802 let AttnKind::Full {
14803 wq,
14804 wk,
14805 wv,
14806 wo,
14807 q_norm,
14808 k_norm,
14809 output_gate,
14810 softplus_gate: None,
14811 bias: None,
14812 } = &m.layer.attn
14813 else {
14814 return Err(false);
14815 };
14816 let FfnKind::Dense(d) = &m.layer.ffn else {
14817 return Err(false);
14818 };
14819 if d.act != Act::Silu || !d.segs.is_empty() {
14820 return Err(false);
14821 }
14822 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14823 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14824 else {
14825 return Err(false);
14826 };
14827 let (Some(g), Some(u), Some(dn)) = (
14828 d.gate_proj.q1_parts(),
14829 d.up_proj.q1_parts(),
14830 d.down_proj.q1_parts(),
14831 ) else {
14832 return Err(false);
14833 };
14834 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14835 return Err(false);
14836 };
14837 let QTensor::Mapped { model, .. } = wq else {
14838 return Err(false);
14839 };
14840 let model = model.clone();
14841 let QTensor::Mapped {
14844 model: em,
14845 idx: eidx,
14846 dtype: cortiq_core::TensorDtype::Q4TiledP,
14847 ..
14848 } = &self.weights.embed_tokens
14849 else {
14850 return Err(false);
14851 };
14852 if !std::sync::Arc::ptr_eq(em, &model)
14853 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14854 {
14855 return Err(false);
14856 }
14857 let embed = (
14858 *eidx,
14859 self.weights.embed_tokens.rows(),
14860 self.weights.embed_tokens.cols(),
14861 );
14862 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14863 return Err(false);
14864 }
14865 let dims = GraphDims {
14866 hidden: self.hidden_size,
14867 eps: self.rms_eps as f32,
14868 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14869 };
14870 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14874 crate::gpu_metal::DENSE_MV,
14875 );
14876 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14877 return Err(false);
14878 };
14879 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14880 return Err(false);
14881 }
14882 let l = AttnGpuLayer {
14883 attn_norm: &m.layer.input_norm,
14884 post_norm: &m.layer.post_norm,
14885 wq: pq,
14886 wk: pk,
14887 wv: pv,
14888 wo: po,
14889 ffn: MetalFfn::Dense {
14890 gate: g,
14891 up: u,
14892 down: dn,
14893 act: (d.act == Act::Gelu) as u8,
14894 },
14895 attn_out_norm: None,
14896 ffn_out_norm: None,
14897 layer_scale: None,
14898 };
14899 let (nh, nkv, hd, rd) = (
14900 self.num_heads,
14901 self.num_kv_heads,
14902 self.head_dim,
14903 self.rotary_dim,
14904 );
14905 let inv_freq = self.inv_freq.clone();
14906 let draft_rows = self.draft_head_rows(lm.1);
14907 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14908 if n_arg == 0 {
14909 return Err(false);
14910 }
14911 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14918 let t_chain = std::time::Instant::now();
14919 graph.chain_ids_init(t_next, k);
14920 let cpu_stored;
14921 {
14922 let cache = &m.kv;
14923 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14924 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14925 cpu_stored = cpu_k[0].len() / hd;
14926 for j in 0..k {
14927 if !graph.encode_chain_input(
14928 embed,
14929 j as u32,
14930 &m.enorm,
14931 &m.hnorm,
14932 self.embed_multiplier,
14933 eh,
14934 ) {
14935 return Err(false);
14936 }
14937 let p = AttnDeviceParams {
14941 kv_id: self.mtp_kv_id(),
14942 layer: Self::MTP_LAYER_BASE,
14943 nh,
14944 nkv,
14945 hd,
14946 rd,
14947 position: position + j,
14948 scale: self.attn_scale,
14949 eps: self.rms_eps as f32,
14950 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14951 late_qk_norm: self.qk_norm_after_rope,
14952 output_gate: *output_gate,
14953 q_norm: q_norm.as_deref(),
14954 k_norm: k_norm.as_deref(),
14955 inv_freq: &inv_freq,
14956 rope_scale: self.rope_scale,
14957 cpu_k: cpu_k.clone(),
14958 cpu_v: cpu_v.clone(),
14959 cpu_stored: cpu_stored + j,
14960 cpu_gen: cache.generation(),
14961 o1: None,
14962 window: None,
14963 softmax_sink: None,
14964 head_gate: None,
14965 v_norm: false,
14966 };
14967 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14968 return Err(false);
14969 }
14970 if draft_rows < lm.1 {
14971 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14972 return Err(false);
14973 }
14974 } else {
14975 graph.encode_lm_head(&m.final_norm, lm);
14976 }
14977 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14978 return Err(false);
14979 }
14980 if split {
14981 graph.commit();
14984 }
14985 }
14986 }
14987 let t_enc = t_chain.elapsed();
14988 if graph.sync_checked().is_err() {
14989 return Err(true);
14990 }
14991 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14992 eprintln!(
14993 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14994 t_enc.as_secs_f64() * 1e3,
14995 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14996 if split { ", split" } else { "" }
14997 );
14998 }
14999 let mut ids = vec![0u32; k];
15000 if !graph.chain_ids_read(&mut ids) {
15001 return Err(true);
15002 }
15003 let mut kbuf = vec![0f32; k * nkv * hd];
15004 let mut vbuf = vec![0f32; k * nkv * hd];
15005 if !crate::gpu_metal::kv_mirror_read_rows(
15006 self.mtp_kv_id(),
15007 Self::MTP_LAYER_BASE,
15008 nkv,
15009 hd,
15010 cpu_stored,
15011 k,
15012 &mut kbuf,
15013 &mut vbuf,
15014 ) {
15015 return Err(true);
15016 }
15017 for r in 0..k {
15018 m.kv.append(
15019 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15020 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15021 &[],
15022 );
15023 }
15024 Ok(ids)
15025 }
15026
15027 fn try_batch_graph_wgpu(
15028 &self,
15029 hiddens: &mut [f32],
15030 positions: &[usize],
15031 k: usize,
15032 spec: Option<crate::gpu::SpecTail<'_>>,
15033 ) -> crate::gpu::BatchGraphOutcome {
15034 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15035 }
15036
15037 fn try_batch_graph_wgpu_prefix(
15042 &self,
15043 hiddens: &mut [f32],
15044 positions: &[usize],
15045 k: usize,
15046 spec: Option<crate::gpu::SpecTail<'_>>,
15047 layers_run: Option<&mut usize>,
15048 ) -> crate::gpu::BatchGraphOutcome {
15049 let graph_end = match self.mimo_moe.graph_prefix_end() {
15050 Some(end) if end < self.num_layers => {
15051 if layers_run.is_none() || spec.is_some() || end == 0 {
15052 return crate::gpu::BatchGraphOutcome::Declined;
15053 }
15054 end
15055 }
15056 _ => self.num_layers,
15057 };
15058 let _tb = std::time::Instant::now();
15059 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15060 if self.attn_softcap > 0.0 {
15061 return crate::gpu::BatchGraphOutcome::Declined; }
15063 if let Some(reason) = self
15066 .wgpu_graph_attn_decline()
15067 .or_else(|| self.batch_graph_gemma_decline())
15068 {
15069 self.note_graph_decline("wgpu batch graph", reason);
15070 return crate::gpu::BatchGraphOutcome::Declined;
15071 }
15072 let nh = self.num_heads;
15073 let (nkv, hd, rd) = self.layer_geom(0);
15074 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15075 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15076 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15077 {
15078 let name = &m.tensors[i].name;
15079 let prism = if crate::prism::is_inverse_embedding(m, name) {
15080 crate::gpu::GraphPrismOp::InverseEmbedding
15081 } else if crate::prism::is_forward_weight(m, name) {
15082 crate::gpu::GraphPrismOp::Forward
15083 } else {
15084 crate::gpu::GraphPrismOp::None
15085 };
15086 return Some(crate::gpu::GraphW {
15087 idx: i,
15088 kind,
15089 row_scale: rs,
15090 data: &[],
15091 prism,
15092 affine: crate::prism::is_affine_target(m, name),
15093 });
15094 }
15095 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15096 eprintln!(
15097 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15098 t.rows(),
15099 t.cols()
15100 );
15101 }
15102 t.as_f32().map(|d| crate::gpu::GraphW {
15103 idx: 0,
15104 kind: 4,
15105 row_scale: &[],
15106 data: d,
15107 prism: crate::gpu::GraphPrismOp::None,
15108 affine: false,
15109 })
15110 }
15111 let built: Option<(
15112 Vec<crate::gpu::GraphLayer<'_>>,
15113 std::sync::Arc<cortiq_core::CmfModel>,
15114 )> = (|| {
15115 let mut layers = Vec::with_capacity(graph_end);
15116 let mut model = None;
15117 for li in 0..graph_end {
15118 let lw = &self.weights.layers[self.phys_layer(li)];
15119 let gffn = match &lw.ffn {
15126 FfnKind::Dense(d) if !d.segs.is_empty() => {
15127 if batch_debug {
15128 eprintln!("batch graph: dense segmented FFN at layer {li}");
15129 }
15130 return None;
15131 }
15132 FfnKind::Dense(d) => {
15133 let Some(act) = d.act.wgpu_graph_act() else {
15134 if batch_debug {
15135 eprintln!(
15136 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15137 d.act
15138 );
15139 }
15140 return None;
15141 };
15142 crate::gpu::GraphFfn::Dense {
15143 gate: gw(&d.gate_proj)?,
15144 up: gw(&d.up_proj)?,
15145 down: gw(&d.down_proj)?,
15146 act,
15147 }
15148 }
15149 FfnKind::Moe(m) => {
15150 if m.route_tau.is_some() || m.mask.is_some() {
15157 return None;
15158 }
15159 let shared = m.shared.as_ref();
15163 let has_shared = shared.is_some();
15164 let shared_gated = matches!(shared, Some((_, Some(_))));
15165 let sgate = match shared {
15166 Some((_, Some(sg))) => gw(sg)?,
15167 _ => gw(&m.router)?,
15171 };
15172 let router = gw(&m.router)?;
15173 if router.prism != crate::gpu::GraphPrismOp::None
15179 || router.affine
15180 || sgate.prism != crate::gpu::GraphPrismOp::None
15181 || sgate.affine
15182 {
15183 return None;
15184 }
15185 let inter = m.experts.first()?.gate_proj.rows();
15186 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15187 let mut q4tp: Option<bool> = None;
15188 let mut gu_q2: Option<bool> = None;
15189 let mut q82: Option<bool> = None;
15190 let mut moe_act = None;
15194 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15195 let Some(e_act) = e.act.wgpu_graph_act() else {
15196 return None;
15197 };
15198 match moe_act {
15199 Some(existing) if existing != e_act => return None,
15200 None => moe_act = Some(e_act),
15201 _ => {}
15202 }
15203 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15204 return None;
15205 }
15206 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15210 {
15211 Some((mm, gi)) => (
15212 mm,
15213 gi,
15214 e.up_proj.mapped_q4t()?.1,
15215 e.down_proj.mapped_q4t()?.1,
15216 false,
15217 false,
15218 false,
15219 ),
15220 None => match e.gate_proj.mapped_q2tp() {
15221 Some((mm, gi)) => (
15222 mm,
15223 gi,
15224 e.up_proj.mapped_q2tp()?.1,
15225 e.down_proj.mapped_q4tp()?.1,
15226 true,
15227 true,
15228 false,
15229 ),
15230 None => match e.gate_proj.mapped_q4tp() {
15231 Some((mm, gi)) => (
15232 mm,
15233 gi,
15234 e.up_proj.mapped_q4tp()?.1,
15235 e.down_proj.mapped_q4tp()?.1,
15236 true,
15237 false,
15238 false,
15239 ),
15240 None => {
15241 if !crate::gpu::moe_q82_graph_on() {
15242 return None;
15243 }
15244 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15245 (
15246 mm,
15247 gi,
15248 e.up_proj.mapped_q8_2f()?.1,
15249 e.down_proj.mapped_q8_2f()?.1,
15250 false,
15251 false,
15252 true,
15253 )
15254 }
15255 },
15256 },
15257 };
15258 if *q4tp.get_or_insert(is_p) != is_p
15259 || *gu_q2.get_or_insert(is_q2) != is_q2
15260 || *q82.get_or_insert(is_8) != is_8
15261 {
15262 return None;
15263 }
15264 if [gi, ui, di].into_iter().any(|idx| {
15265 mm.tensors.get(idx).is_some_and(|t| {
15266 crate::prism::is_forward_weight(mm, &t.name)
15267 || crate::prism::is_affine_target(mm, &t.name)
15268 })
15269 }) {
15270 return None;
15271 }
15272 model.get_or_insert_with(|| mm.clone());
15273 experts.push((gi, ui, di));
15274 }
15275 crate::gpu::GraphFfn::Moe {
15276 router,
15277 shared_gate: sgate,
15278 experts,
15279 n_exp: m.experts.len(),
15280 top_k: m.top_k,
15281 inter,
15282 act: moe_act?,
15283 norm_topk: m.norm_topk_prob,
15284 q4tp: q4tp?,
15285 gu_q2: gu_q2.unwrap_or(false),
15286 q82: q82.unwrap_or(false),
15287 sigmoid: m.router_sigmoid,
15288 sigmoid_denom_eps: m.sigmoid_denom_eps,
15289 bias: m.expert_bias.as_deref(),
15290 has_shared,
15291 detached_shared: None,
15292 shared_gated,
15293 route_scale: m.routed_scaling,
15294 }
15295 }
15296 _ => return None,
15297 };
15298 let attn = match &lw.attn {
15299 AttnKind::Full {
15300 wq,
15301 wk,
15302 wv,
15303 wo,
15304 q_norm,
15305 k_norm,
15306 output_gate,
15307 softplus_gate,
15308 bias,
15309 } => {
15310 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15311 if batch_debug {
15312 eprintln!(
15313 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15314 softplus_gate.is_some(),
15315 self.attention_heads_per_layer.is_some()
15316 );
15317 }
15318 return None;
15319 }
15320 let (m, _, _, _) =
15321 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15322 model = Some(m.clone());
15323 crate::gpu::GraphAttn::Full {
15324 wq: gw(wq)?,
15325 wk: gw(wk)?,
15326 wv: gw(wv)?,
15327 wo: gw(wo)?,
15328 q_norm: q_norm.as_deref(),
15329 k_norm: k_norm.as_deref(),
15330 late_qk_norm: self.qk_norm_after_rope,
15331 bias: bias
15332 .as_ref()
15333 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15334 output_gate: *output_gate,
15335 cpu_k: self.kv_cache.layers[li].k_heads(),
15336 cpu_v: self.kv_cache.layers[li].v_heads(),
15337 cpu_base: self.kv_cache.layers[li].base(),
15338 geom: self.graph_attn_geom(li),
15339 head_gate: None,
15342 }
15343 }
15344 AttnKind::LinearGdn(w) => {
15345 let Some(cfg) = self.gdn_cfg else {
15346 if batch_debug {
15347 eprintln!("batch graph: no GDN config at layer {li}");
15348 }
15349 return None;
15350 };
15351 let (m, _, _, _) = w
15352 .in_proj_qkv
15353 .graph_weight()
15354 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15355 model = Some(m.clone());
15356 crate::gpu::GraphAttn::Gdn {
15357 qkv: gw(&w.in_proj_qkv)?,
15358 z: gw(&w.in_proj_z)?,
15359 a: gw(&w.in_proj_a)?,
15360 b: gw(&w.in_proj_b)?,
15361 out: gw(&w.out_proj)?,
15362 conv1d: &w.conv1d,
15363 a_log: &w.a_log,
15364 dt_bias: &w.dt_bias,
15365 norm: &w.norm,
15366 nv: cfg.num_v_heads,
15367 nk: cfg.num_k_heads,
15368 dk: cfg.key_head_dim,
15369 dv: cfg.value_head_dim,
15370 kk: cfg.conv_kernel,
15371 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15372 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15373 }
15374 }
15375 _ => return None,
15376 };
15377 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15380 layers.push(crate::gpu::GraphLayer {
15381 input_norm: &lw.input_norm,
15382 attn,
15383 post_norm: &lw.post_norm,
15384 attn_out_norm: if g4 {
15385 lw.attn_out_norm.as_deref()
15386 } else {
15387 None
15388 },
15389 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15390 layer_scale: if g4 { lw.layer_scale } else { None },
15391 ffn: gffn,
15392 });
15393 }
15394 Some((layers, model?))
15395 })();
15396 let Some((layers, model)) = built else {
15397 {
15398 use std::sync::atomic::{AtomicBool, Ordering};
15399 static SAID: AtomicBool = AtomicBool::new(false);
15400 if !SAID.swap(true, Ordering::Relaxed) {
15401 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15402 }
15403 }
15404 return crate::gpu::BatchGraphOutcome::Declined;
15405 };
15406 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15407 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15408 }
15409 crate::gpu::forward_batch_graph(
15410 &model,
15411 self.graph_kv_id,
15412 &layers,
15413 &self.inv_freq,
15414 hiddens,
15415 nh,
15416 nkv,
15417 hd,
15418 rd,
15419 self.hidden_size,
15420 self.intermediate_size,
15421 positions,
15422 self.kv_cache.max_seq_len,
15423 gemma,
15424 self.rms_eps as f32,
15425 self.attn_scale,
15426 k,
15427 &(0..graph_end)
15428 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15429 .collect::<Vec<_>>(),
15430 self.o1_epoch,
15431 spec,
15432 layers_run,
15433 )
15434 }
15435
15436 fn draft_probe() -> bool {
15440 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15441 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15442 }
15443
15444 #[cfg(feature = "gpu")]
15456 fn dsv4_spec_on() -> bool {
15457 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15458 *ON.get_or_init(|| {
15459 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15463 return v != "0";
15464 }
15465 std::env::var("CMF_DSV4_SPEC")
15472 .map(|v| v != "0")
15473 .unwrap_or_else(|_| {
15474 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15475 })
15476 })
15477 }
15478
15479 #[cfg(feature = "gpu")]
15486 fn dsv4_spec_step(
15487 &mut self,
15488 tip_token: u32,
15489 t_next: u32,
15490 next_pos: usize,
15491 max_extra: usize,
15492 drafted: &mut usize,
15493 accepted_ctr: &mut usize,
15494 ) -> Option<(Vec<u32>, usize)> {
15495 let t_all = std::time::Instant::now();
15496 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15497 thread_local! {
15498 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15499 const { std::cell::Cell::new(None) };
15500 }
15501 LAST.with(|l| {
15502 if let Some(prev) = l.get() {
15503 eprintln!(
15504 "between rounds {:.1} ms",
15505 prev.elapsed().as_secs_f64() * 1e3
15506 );
15507 }
15508 l.set(Some(std::time::Instant::now()));
15509 });
15510 }
15511 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15512 eprintln!("spec_step: enter pos={next_pos}");
15513 }
15514 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15515 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15516 if self.dspark.is_none() {
15518 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15519 if t.is_empty() {
15520 return None;
15521 }
15522 crate::dsv4::dspark_arm(&t, cfg.dim);
15523 self.dspark = Some(crate::dsv4::DsparkState::new(
15524 self.dsv4_mtp.len(),
15525 &cfg,
15526 t.len(),
15527 ));
15528 }
15529 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15530 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15531 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15532 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15533 }
15534 let pack = pack?;
15535 let block = crate::dsv4::dspark_block();
15536 let b_box = self.dsv4.as_mut()?;
15537 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15538 let ds = self.dspark.as_mut()?;
15539 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15542 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15543 if dbg {
15544 eprintln!("spec_step: no capture");
15545 }
15546 return None;
15547 }
15548 ds.have_hidden = true;
15549 let tip_pos = next_pos.checked_sub(1)?;
15550 let draft_started = std::time::Instant::now();
15551 let mut conf = Vec::new();
15552 let props = crate::dsv4::dspark_draft_gpu(
15553 g,
15554 &self.dsv4_mtp,
15555 &cfg,
15556 ds,
15557 pack,
15558 st.kv_id,
15559 tip_token,
15560 tip_pos,
15561 self.pool.as_deref(),
15562 &mut conf,
15563 );
15564 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15565 *drafted += block;
15566 if props.is_empty() || props[0] != t_next {
15567 if dbg {
15568 eprintln!(
15569 "spec_step: draft {} (props0={:?} t_next={t_next})",
15570 if props.is_empty() {
15571 "empty"
15572 } else {
15573 "miss"
15574 },
15575 props.first()
15576 );
15577 }
15578 return None;
15579 }
15580 let mut k_verify = crate::dsv4::dspark_verify_k()
15587 .min(props.len())
15588 .min(max_extra.saturating_add(1));
15589 let conf_min = {
15595 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15596 *M.get_or_init(|| {
15597 std::env::var("CMF_DSPARK_CONF_MIN")
15598 .ok()
15599 .and_then(|v| v.parse().ok())
15600 .unwrap_or(0.0)
15601 })
15602 };
15603 if conf_min > 0.0 && conf.len() >= props.len() {
15604 let mut keep = 1usize;
15605 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15606 keep += 1;
15607 }
15608 k_verify = k_verify.min(keep.max(2));
15609 }
15610 if k_verify < 2 {
15611 return None;
15612 }
15613 let mut fed = Vec::with_capacity(k_verify);
15614 fed.push(t_next);
15615 fed.extend_from_slice(&props[1..k_verify]);
15616 let mut argmax = Vec::new();
15617 let mut logits_all = Vec::new();
15618 let mut walked = Vec::new();
15619 let txn = crate::dsv4::dsv4_verify_chunk(
15620 g,
15621 layers,
15622 &cfg,
15623 st,
15624 &fed,
15625 next_pos,
15626 &self.inv_freq,
15627 self.pool.as_deref(),
15628 &targets,
15629 &mut argmax,
15630 &mut logits_all,
15631 &mut walked,
15632 );
15633 if txn.is_none() && dbg {
15634 eprintln!("spec_step: verify declined");
15635 }
15636 let txn = txn?;
15637 let spec_gpu_end = txn.gpu_end;
15638 let b = fed.len();
15639 let mut accepted = 1usize;
15640 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15641 accepted += 1;
15642 }
15643 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15648 accepted = 1;
15649 }
15650 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15651 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15652 }
15653 let t_fin = std::time::Instant::now();
15654 if !crate::dsv4::dsv4_spec_finish(
15655 g,
15656 layers,
15657 &cfg,
15658 st,
15659 txn,
15660 accepted,
15661 &fed,
15662 &self.inv_freq,
15663 self.pool.as_deref(),
15664 ) {
15665 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15666 return None;
15667 }
15668 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15669 eprintln!(
15670 "finish(k={accepted}): {:.1} ms",
15671 t_fin.elapsed().as_secs_f64() * 1e3
15672 );
15673 }
15674 *accepted_ctr += accepted - 1;
15675 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15680 let dev_caps: Vec<usize> = targets
15685 .iter()
15686 .copied()
15687 .filter(|&t| t < spec_gpu_end)
15688 .collect();
15689 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15690 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15691 return None;
15692 }
15693 for t in 0..accepted {
15694 let tip = t + 1 == accepted;
15695 for (slot, &tl) in targets.iter().enumerate() {
15696 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15697 let lo = (di * b + t) * hc * dim;
15698 crate::dsv4::dspark_capture(
15699 &caps_all[lo..lo + hc * dim],
15700 &cfg,
15701 slot,
15702 &mut ds.main_hidden,
15703 );
15704 } else if tip
15705 && crate::dsv4::dspark_peek_slot(slot, dim, {
15706 let lo = slot * dim;
15707 &mut ds.main_hidden[lo..lo + dim]
15708 })
15709 {
15710 } else {
15715 crate::dsv4::dspark_capture(
15719 &walked[t * hc * dim..(t + 1) * hc * dim],
15720 &cfg,
15721 slot,
15722 &mut ds.main_hidden,
15723 );
15724 }
15725 }
15726 crate::dsv4::dspark_ring_append(
15727 g,
15728 &self.dsv4_mtp,
15729 &cfg,
15730 ds,
15731 next_pos + t,
15732 self.pool.as_deref(),
15733 );
15734 }
15735 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15736 self.graph_logits = Some(row);
15737 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15742 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15743 crate::dsv4::pick_tally_arm();
15744 }
15745 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15746 eprintln!(
15747 "spec_step total {:.1} ms (k={accepted})",
15748 t_all.elapsed().as_secs_f64() * 1e3
15749 );
15750 }
15751 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15752 }
15753
15754 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15755 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15756 return;
15757 }
15758 let trunk_now = crate::dsv4::pick_tally_take();
15760 crate::dsv4::trunk_freq_note(&trunk_now);
15761 if !trunk_now.is_empty() {
15762 self.dspark_trunk_picks.push(trunk_now);
15763 let keep = crate::dsv4::dspark_block();
15764 if self.dspark_trunk_picks.len() > keep {
15765 self.dspark_trunk_picks.remove(0);
15766 }
15767 }
15768 for p in std::mem::take(&mut self.dspark_pending) {
15771 let Some(i) = position.checked_sub(p.0 + 1) else {
15772 continue;
15773 };
15774 let mut p = p;
15775 if i < p.1.len() {
15776 if p.2 && p.1[i] == token_id {
15777 p.3 = i + 1;
15778 } else {
15779 p.2 = false;
15780 }
15781 if i + 1 < p.1.len() {
15782 self.dspark_pending.push(p);
15783 continue;
15784 }
15785 }
15786 self.dspark_hist.push(p.3);
15787 self.dspark_real.push(token_id);
15788 }
15789 let Some(b) = &mut self.dsv4 else { return };
15790 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15791 let n_layers = layers.len();
15792 if self.dspark.is_none() {
15793 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15794 if t.is_empty() {
15795 return;
15796 }
15797 eprintln!(
15798 "DSpark: capture from layers {t:?}, block {}",
15799 crate::dsv4::dspark_block()
15800 );
15801 crate::dsv4::dspark_arm(&t, cfg.dim);
15802 self.dspark = Some(crate::dsv4::DsparkState::new(
15803 self.dsv4_mtp.len(),
15804 &cfg,
15805 t.len(),
15806 ));
15807 }
15808 let ds = self.dspark.as_mut().unwrap();
15809 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15810 return; }
15812 let mut conf = Vec::new();
15813 crate::dsv4::pick_tally_arm();
15814 let draft_started = std::time::Instant::now();
15819 #[cfg(feature = "gpu")]
15820 let gpu_draft = crate::dsv4::dspark_gpu_on();
15821 #[cfg(not(feature = "gpu"))]
15822 let gpu_draft = false;
15823 let props = if gpu_draft {
15824 #[cfg(feature = "gpu")]
15825 {
15826 let kv_id = b.3.kv_id;
15827 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15828 Some(pk) => crate::dsv4::dspark_draft_gpu(
15829 g,
15830 &self.dsv4_mtp,
15831 &cfg,
15832 ds,
15833 pk,
15834 kv_id,
15835 token_id,
15836 position,
15837 self.pool.as_deref(),
15838 &mut conf,
15839 ),
15840 None => Vec::new(),
15841 }
15842 }
15843 #[cfg(not(feature = "gpu"))]
15844 Vec::new()
15845 } else {
15846 crate::gpu::cpu_scope(|| {
15847 crate::dsv4::dspark_draft(
15848 g,
15849 &self.dsv4_mtp,
15850 &cfg,
15851 ds,
15852 token_id,
15853 position,
15854 self.pool.as_deref(),
15855 &mut conf,
15856 )
15857 })
15858 };
15859 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15860 let draft_picks = crate::dsv4::pick_tally_take();
15861 crate::dsv4::dspark_freq_note(&draft_picks);
15862 crate::dsv4::pick_tally_arm();
15865 if !props.is_empty() {
15866 let (tu, tt) = {
15870 let flat: Vec<(usize, Vec<usize>)> = self
15871 .dspark_trunk_picks
15872 .iter()
15873 .flat_map(|v| v.iter().cloned())
15874 .collect();
15875 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15877 std::collections::HashMap::new();
15878 for (li, picks) in flat {
15879 per.entry(li).or_default().extend(picks);
15880 }
15881 let n = per.len().max(1);
15882 let mut u = 0usize;
15883 let mut t = 0usize;
15884 for (_, v) in per {
15885 t += v.len();
15886 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15887 }
15888 (u / n, t / n)
15889 };
15890 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15891 self.dspark_exp.push((tu, tt, du, dt));
15892 self.dspark_pending.push((position, props, true, 0));
15893 }
15894 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15895 let n = self.dspark_hist.len() as f32;
15896 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15897 let block = crate::dsv4::dspark_block();
15898 let mut at = vec![0usize; block + 1];
15899 for &k in &self.dspark_hist {
15900 at[k] += 1;
15901 }
15902 let mut surv = Vec::with_capacity(block);
15904 for i in 1..=block {
15905 let k = at[i..].iter().sum::<usize>() as f32 / n;
15906 surv.push(format!("{k:.2}"));
15907 }
15908 let distinct = self
15909 .dspark_real
15910 .iter()
15911 .collect::<std::collections::HashSet<_>>()
15912 .len();
15913 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15914 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15915 });
15916 let m = self.dspark_exp.len().max(1);
15917 eprintln!(
15918 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
15919 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
15920 self.dspark_hist.len(),
15921 mean + 1.0,
15922 surv.join(" ")
15923 );
15924 eprintln!(
15925 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
15926 trunk experts {}/{} per layer over {block} tokens, \
15927 draft {}/{} per block, draft {:.2} ms/block",
15928 self.dspark_real.len(),
15929 tu / m,
15930 tt / m,
15931 du / m,
15932 dt / m,
15933 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15934 );
15935 }
15936 }
15937
15938 fn forward_layers_upto(
15939 &mut self,
15940 hidden: &[f32],
15941 position: usize,
15942 task_mask: Option<&TaskMask>,
15943 upto: Option<usize>,
15944 ) -> Vec<f32> {
15945 if let Some(plan) = self.gpu_plan.clone() {
15951 if upto.is_none() && plan.len() > 1 {
15952 let mut h = hidden.to_vec();
15953 for &(dev, from, upto_incl) in plan.iter() {
15954 h = crate::gpu::with_device(dev, || {
15955 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15956 });
15957 }
15958 return h;
15959 }
15960 }
15961 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15962 }
15963
15964 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15969 self.set_gpu_plan_at(devices, None)
15970 }
15971
15972 pub fn set_gpu_plan_at(
15976 &mut self,
15977 devices: Option<&[usize]>,
15978 at: Option<usize>,
15979 ) -> Result<(), String> {
15980 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15981 self.gpu_plan = None;
15982 return Ok(());
15983 };
15984 self.split_supported()?;
15985 let n = self.num_layers;
15986 if devs.len() > n {
15987 return Err(format!("{} devices for {n} layers", devs.len()));
15988 }
15989 if let Some(k) = at {
15990 if k == 0 || k >= n {
15991 return Err(format!("split at {k}: the model has {n} layers"));
15992 }
15993 if devs.len() == 2 {
15994 self.gpu_plan = Some(std::sync::Arc::new(vec![
15995 (devs[0], 0, k - 1),
15996 (devs[1], k, n - 1),
15997 ]));
15998 return Ok(());
15999 }
16000 return Err(format!(
16001 "an explicit split point takes exactly 2 devices, got {}",
16002 devs.len()
16003 ));
16004 }
16005 let per = n.div_ceil(devs.len());
16006 let mut plan = Vec::with_capacity(devs.len());
16007 let mut from = 0usize;
16008 for &d in devs {
16009 if from >= n {
16010 break;
16011 }
16012 let upto = (from + per - 1).min(n - 1);
16013 plan.push((d, from, upto));
16014 from = upto + 1;
16015 }
16016 self.gpu_plan = Some(std::sync::Arc::new(plan));
16017 Ok(())
16018 }
16019
16020 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16022 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16023 }
16024
16025 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16031 if let Some(x) = t.as_f32() {
16032 return x.to_vec();
16033 }
16034 let mut out = vec![0.0; t.rows() * t.cols()];
16035 for r in 0..t.rows() {
16036 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16037 }
16038 out
16039 }
16040
16041 fn embryo_resident_eligible(&self) -> bool {
16042 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16045 || self.num_layers != self.physical_layers
16046 || self.loop_final_norm
16047 || self.weights.layers.len() != self.num_layers
16048 || self.head_clusters.is_none()
16049 || self.final_softcap.is_some()
16050 || self.logit_multiplier.is_some()
16051 || self.attn_softcap != 0.0
16052 || self.mtp.is_some()
16053 || self.per_token_stack()
16054 || self.dsv4.is_some()
16055 || self.dsv41.is_some()
16056 || self.qwen4_exp.is_some()
16057 || self.dyn_router.is_some()
16064 || self.dyn_phi_layer.is_some()
16065 || self.dyn_blend_loaded
16066 || self.o1_cfg.is_some()
16067 || self.swa.is_some()
16068 || self.sliding_layers.is_some()
16069 || self.global_attn.is_some()
16070 || self.attention_heads_per_layer.is_some()
16071 || self.attn_v_norm
16072 || self
16073 .kv_cache
16074 .layers
16075 .iter()
16076 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16077 || self.rope_scale != 1.0
16078 || self.rope_scale_local != 1.0
16079 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16080 || self.hidden_size == 0
16081 || self.hidden_size > 1024
16082 || self.intermediate_size > 1024
16083 || self.num_heads == 0
16084 || self.num_kv_heads == 0
16085 || self.num_heads % self.num_kv_heads != 0
16086 || self.num_heads.saturating_mul(self.head_dim) > 1024
16087 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16088 || self.vocab_size == 0
16089 || self.kv_cache.max_seq_len == 0
16090 || self.rotary_dim == 0
16091 || self.rotary_dim > self.head_dim
16092 || self.rotary_dim % 2 != 0
16093 || self.inv_freq.len() < self.rotary_dim / 2
16094 {
16095 return false;
16096 }
16097 if self.weights.lm_head.as_f32().is_none()
16108 || self.weights.embed_tokens.as_f32().is_none()
16109 || self.weights.lm_head.rows() < self.vocab_size
16110 || self.weights.lm_head.cols() != self.hidden_size
16111 || self.weights.embed_tokens.rows() < self.vocab_size
16112 || self.weights.embed_tokens.cols() != self.hidden_size
16113 || self.weights.final_norm.len() != self.hidden_size
16114 {
16115 return false;
16116 }
16117 if let Some(cfg) = self.vmf_cfg {
16118 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16119 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16120 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16121 || cfg.state_len() == 0
16122 {
16123 return false;
16124 }
16125 }
16126 if let Some(g) = self.gdn_cfg {
16127 if g.num_v_heads == 0
16132 || g.num_k_heads == 0
16133 || g.num_v_heads % g.num_k_heads != 0
16134 || g.key_head_dim == 0
16135 || g.key_head_dim > 128
16136 || g.value_head_dim == 0
16137 || g.value_head_dim > 256
16138 || g.value_head_dim % 4 != 0
16139 || g.conv_kernel == 0
16140 || g.num_v_heads > 512
16141 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16142 || g.conv_dim() > 2048
16143 || g.conv_dim() % 4 != 0
16144 || g.hidden_size != self.hidden_size
16145 || g.output_gate_sigmoid
16146 || g.rms_eps != self.rms_eps
16147 || g.state_len() == 0
16148 {
16149 return false;
16150 }
16151 }
16152 let mut full_seen = false;
16153 for lw in &self.weights.layers {
16154 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16155 return false;
16156 }
16157 match &lw.attn {
16158 AttnKind::LinearGdn(w) => {
16159 let Some(g) = self.gdn_cfg else {
16160 return false;
16161 };
16162 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16163 if w.in_proj_qkv.rows() != g.conv_dim()
16164 || w.in_proj_qkv.cols() != self.hidden_size
16165 || w.in_proj_qkv.as_f32().is_none()
16166 || w.in_proj_z.rows() != nv * dv
16167 || w.in_proj_z.cols() != self.hidden_size
16168 || w.in_proj_z.as_f32().is_none()
16169 || w.in_proj_a.rows() != nv
16170 || w.in_proj_a.cols() != self.hidden_size
16171 || w.in_proj_a.as_f32().is_none()
16172 || w.in_proj_b.rows() != nv
16173 || w.in_proj_b.cols() != self.hidden_size
16174 || w.in_proj_b.as_f32().is_none()
16175 || w.conv1d.len() != g.conv_dim() * kk
16176 || w.a_log.len() != nv
16177 || w.dt_bias.len() != nv
16178 || w.norm.len() != dv
16179 || w.out_proj.rows() != self.hidden_size
16180 || w.out_proj.cols() != nv * dv
16181 || w.out_proj.as_f32().is_none()
16182 {
16183 return false;
16184 }
16185 }
16186 AttnKind::Linear(w) => {
16187 let Some(cfg) = self.vmf_cfg else {
16188 return false;
16189 };
16190 if w.thq.rows() != cfg.num_heads * cfg.nphase
16191 || w.thq.cols() != self.hidden_size
16192 || w.thq.as_f32().is_none()
16193 || w.thk.rows() != cfg.num_heads * cfg.nphase
16194 || w.thk.cols() != self.hidden_size
16195 || w.thk.as_f32().is_none()
16196 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16197 || w.v_proj.cols() != self.hidden_size
16198 || w.v_proj.as_f32().is_none()
16199 || w.out_proj.rows() != self.hidden_size
16200 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16201 || w.out_proj.as_f32().is_none()
16202 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16203 {
16204 return false;
16205 }
16206 if let Some((kg, kb)) = &w.k_gate {
16207 if kg.rows() != cfg.num_heads
16208 || kg.cols() != self.hidden_size
16209 || kg.as_f32().is_none()
16210 || kb.len() != cfg.num_heads
16211 {
16212 return false;
16213 }
16214 }
16215 if let Some(conv) = &w.conv {
16216 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16217 return false;
16218 }
16219 }
16220 }
16221 AttnKind::Full {
16222 wq,
16223 wk,
16224 wv,
16225 wo,
16226 q_norm,
16227 k_norm,
16228 output_gate,
16229 softplus_gate,
16230 bias,
16231 } => {
16232 if full_seen
16233 || q_norm.is_some()
16234 || k_norm.is_some()
16235 || *output_gate
16236 || softplus_gate.is_some()
16237 || bias.is_some()
16238 || wq.as_f32().is_none()
16239 || wk.as_f32().is_none()
16240 || wv.as_f32().is_none()
16241 || wo.as_f32().is_none()
16242 || wq.rows() != self.num_heads * self.head_dim
16243 || wk.rows() != self.num_kv_heads * self.head_dim
16244 || wv.rows() != self.num_kv_heads * self.head_dim
16245 || wq.cols() != self.hidden_size
16246 || wk.cols() != self.hidden_size
16247 || wv.cols() != self.hidden_size
16248 || wo.rows() != self.hidden_size
16249 || wo.cols() != self.num_heads * self.head_dim
16250 {
16251 return false;
16252 }
16253 full_seen = true;
16254 }
16255 AttnKind::Bounded(w) => {
16256 let Some(ac) = self.anchor_core.as_ref() else {
16259 return false;
16260 };
16261 if self.bounded_rope.is_none()
16262 || w.window != ac.window
16263 || w.sink != ac.sink
16264 || w.window == 0
16265 || w.window + w.sink > 256
16266 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16267 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16268 || w.wq.as_f32().is_none()
16269 || w.wk.as_f32().is_none()
16270 || w.wv.as_f32().is_none()
16271 || w.wo.as_f32().is_none()
16272 || w.wq.rows() != self.num_heads * self.head_dim
16273 || w.wk.rows() != self.num_kv_heads * self.head_dim
16274 || w.wv.rows() != self.num_kv_heads * self.head_dim
16275 || w.wq.cols() != self.hidden_size
16276 || w.wk.cols() != self.hidden_size
16277 || w.wv.cols() != self.hidden_size
16278 || w.wo.rows() != self.hidden_size
16279 || w.wo.cols() != self.num_heads * self.head_dim
16280 {
16281 return false;
16282 }
16283 }
16284 _ => return false,
16285 }
16286 match &lw.ffn {
16287 FfnKind::Dense(d) => {
16288 if d.act != Act::Silu
16289 || !d.segs.is_empty()
16290 || d.gate_proj.as_f32().is_none()
16291 || d.up_proj.as_f32().is_none()
16292 || d.down_proj.as_f32().is_none()
16293 || d.gate_proj.rows() != self.intermediate_size
16294 || d.gate_proj.cols() != self.hidden_size
16295 || d.up_proj.rows() != self.intermediate_size
16296 || d.up_proj.cols() != self.hidden_size
16297 || d.down_proj.rows() != self.hidden_size
16298 || d.down_proj.cols() != self.intermediate_size
16299 {
16300 return false;
16301 }
16302 }
16303 FfnKind::Moe(m) => {
16304 if m.resonance.is_none()
16305 || m.top_k != 1
16306 || m.router_sigmoid
16307 || !m.norm_topk_prob
16308 || m.expert_bias.is_some()
16309 || m.routed_scaling != 1.0
16310 || m.route_tau.is_some()
16311 || m.shared.is_none()
16312 || m.mask.is_some()
16313 || m.per_expert_scale.is_some()
16314 || m.router_input_norm
16315 || m.experts.is_empty()
16316 || m.experts.len() > 8
16317 {
16318 return false;
16319 }
16320 let r = m.resonance.as_ref().unwrap();
16321 if r.mu.len() != m.experts.len() * self.hidden_size
16322 || r.bias.len() != m.experts.len()
16323 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16324 || r.k > 128
16325 {
16326 return false;
16327 }
16328 let Some((shared, gate)) = &m.shared else {
16329 return false;
16330 };
16331 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16332 return false;
16333 }
16334 if shared.gate_proj.as_f32().is_none()
16335 || shared.up_proj.as_f32().is_none()
16336 || shared.down_proj.as_f32().is_none()
16337 || shared.gate_proj.rows() != self.intermediate_size
16338 || shared.gate_proj.cols() != self.hidden_size
16339 || shared.up_proj.rows() != self.intermediate_size
16340 || shared.up_proj.cols() != self.hidden_size
16341 || shared.down_proj.rows() != self.hidden_size
16342 || shared.down_proj.cols() != self.intermediate_size
16343 {
16344 return false;
16345 }
16346 for e in &m.experts {
16347 if e.act != Act::Silu
16348 || !e.segs.is_empty()
16349 || e.gate_proj.as_f32().is_none()
16350 || e.up_proj.as_f32().is_none()
16351 || e.down_proj.as_f32().is_none()
16352 || e.gate_proj.rows() != self.intermediate_size
16353 || e.gate_proj.cols() != self.hidden_size
16354 || e.up_proj.rows() != self.intermediate_size
16355 || e.up_proj.cols() != self.hidden_size
16356 || e.down_proj.rows() != self.hidden_size
16357 || e.down_proj.cols() != self.intermediate_size
16358 {
16359 return false;
16360 }
16361 }
16362 }
16363 FfnKind::DenseMoe(_) => return false,
16364 }
16365 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16366 return false;
16367 }
16368 }
16369 if full_seen && self.anchor_core.is_some() {
16370 return false;
16371 }
16372 full_seen || self.num_layers > 0
16373 }
16374
16375 fn embryo_resident_wanted(&self) -> bool {
16380 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16381 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16382 && matches!(
16383 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16384 Ok("1") | Ok("parallel")
16385 )
16386 && crate::gpu::enabled_here()
16387 && !self.graph_refused()
16388 && self.embryo_resident_eligible()
16389 }
16390
16391 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16399 if ids.len() < 2
16400 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16401 || !self.embryo_resident_wanted()
16402 {
16403 return None;
16404 }
16405 let model = self.ensure_embryo_graph()?;
16406 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16407 .ok()
16408 .and_then(|v| v.parse::<usize>().ok())
16409 .filter(|&v| v >= 1)
16410 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16411 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16412 let hs = self.hidden_size;
16413 let n = ids.len();
16414 let mut pos = start;
16415 let mut last = None;
16416 let mut rows = Vec::with_capacity(cmax * hs);
16417 while pos < n {
16418 let end = (pos + cmax).min(n);
16419 rows.clear();
16420 for &id in &ids[pos..end] {
16421 rows.extend_from_slice(&self.embed_single(id));
16422 }
16423 let mut lg = Vec::new();
16424 if !crate::gpu::forward_embryo_graph_chunk(
16425 &model,
16426 self.graph_kv_id,
16427 &rows,
16428 pos,
16429 end - pos,
16430 &mut lg,
16431 ) {
16432 if pos == start {
16433 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16434 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16435 }
16436 return None;
16437 }
16438 self.kv_cache.clear();
16442 self.clear_history();
16443 crate::gpu::graph_kv_reset(self.graph_kv_id);
16444 panic!(
16445 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16446 );
16447 }
16448 last = Some(lg);
16449 pos = end;
16450 }
16451 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16452 eprintln!(
16453 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16454 n - start,
16455 (n - start).div_ceil(cmax)
16456 );
16457 }
16458 last
16459 }
16460
16461 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16462 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16463 const UMAX: u32 = u32::MAX;
16464 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16465 const REC: usize = 64;
16466 struct Pack {
16467 data: Vec<f32>,
16468 }
16469 impl Pack {
16470 fn put(&mut self, x: &[f32]) -> u32 {
16471 if x.is_empty() {
16472 return u32::MAX;
16473 }
16474 let off = self.data.len();
16475 self.data.extend_from_slice(x);
16476 off as u32
16477 }
16478 }
16479 let vmf = self.vmf_cfg;
16483 let gdn = self.gdn_cfg;
16484 let mut pack = Pack { data: Vec::new() };
16485 let mut meta = vec![0u32; HEADER];
16486 meta[0] = self.hidden_size as u32;
16487 meta[1] = self.intermediate_size as u32;
16488 meta[2] = self.vocab_size as u32;
16489 meta[3] = self.num_layers as u32;
16490 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16491 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16492 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16493 if let Some(g) = gdn {
16494 meta[24] = g.num_v_heads as u32;
16495 meta[25] = g.num_k_heads as u32;
16496 meta[26] = g.key_head_dim as u32;
16497 meta[27] = g.value_head_dim as u32;
16498 meta[28] = g.conv_kernel as u32;
16499 meta[29] = g.conv_dim() as u32;
16500 }
16501 meta[7] = self.num_heads as u32;
16502 meta[8] = self.num_kv_heads as u32;
16503 meta[9] = self.head_dim as u32;
16504 meta[10] = self.kv_cache.max_seq_len as u32;
16505 let clusters = self.head_clusters.as_ref().unwrap();
16506 let cluster_count = clusters.len() / self.hidden_size;
16507 if clusters.len() % self.hidden_size != 0
16508 || cluster_count == 0
16509 || cluster_count > 1024
16510 || self.vocab_size % cluster_count != 0
16511 || self.weights.lm_head.rows() < self.vocab_size
16512 || self.weights.final_norm.len() != self.hidden_size
16513 {
16514 return None;
16515 }
16516 meta[11] = cluster_count as u32;
16517 meta[12] = (self.vocab_size / cluster_count) as u32;
16518 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16519 meta[16] = self.rotary_dim as u32;
16520 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16521 meta[19] = (self.rms_eps as f32).to_bits();
16522 let max_conv = self
16523 .weights
16524 .layers
16525 .iter()
16526 .filter_map(|lw| match &lw.attn {
16527 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16528 _ => None,
16529 })
16530 .max()
16531 .unwrap_or(1);
16532 let phase_stride = vmf
16538 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16539 .unwrap_or(0);
16540 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16541 let state_stride = phase_stride.max(gdn_stride);
16542 let bounded = self.anchor_core.clone();
16546 let (anchor_window, anchor_sink) = bounded
16547 .as_ref()
16548 .map(|ac| (ac.window, ac.sink))
16549 .unwrap_or((0, 0));
16550 let kv_stride = if bounded.is_some() {
16551 2usize
16552 .saturating_mul(self.num_kv_heads)
16553 .saturating_mul(anchor_window)
16554 .saturating_mul(self.head_dim)
16555 } else {
16556 2usize
16557 .saturating_mul(self.num_kv_heads)
16558 .saturating_mul(self.kv_cache.max_seq_len)
16559 .saturating_mul(self.head_dim)
16560 };
16561 meta[14] = state_stride as u32;
16562 meta[15] = kv_stride as u32;
16563 meta[18] = anchor_window as u32;
16564 meta[20] = anchor_sink as u32;
16565 meta[21] = match &self.bounded_rope {
16566 Some(rope) => {
16567 let off = pack.put(&rope.cos);
16569 let _ = pack.put(&rope.sin);
16570 off
16571 }
16572 None => UMAX,
16573 };
16574 let mut full_seen = false;
16575 let mut bounded_seen = 0usize;
16576 let mut phase_seen = 0usize;
16580 let mut gdn_seen = 0usize;
16581 for (li, lw) in self.weights.layers.iter().enumerate() {
16582 let base = meta.len();
16583 meta.resize(base + REC, UMAX);
16584 meta[base] = match &lw.attn {
16585 AttnKind::Linear(w) if w.phase_delta => 1,
16586 AttnKind::Linear(_) => 0,
16587 AttnKind::Full { .. } => 2,
16588 AttnKind::Bounded(_) => 3,
16589 AttnKind::LinearGdn(_) => 4,
16590 _ => UMAX,
16591 };
16592 meta[base + 1] = pack.put(&lw.input_norm);
16593 meta[base + 2] = pack.put(&lw.post_norm);
16594 meta[base + 25] = match &lw.attn {
16595 AttnKind::Linear(_) => {
16596 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16597 phase_seen += 1;
16598 off
16599 }
16600 AttnKind::LinearGdn(_) => {
16601 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16602 gdn_seen += 1;
16603 off
16604 }
16605 _ => UMAX,
16606 };
16607 match &lw.attn {
16608 AttnKind::LinearGdn(w) => {
16609 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16612 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16613 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16614 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16615 meta[base + 60] = pack.put(&w.conv1d);
16616 meta[base + 61] = pack.put(&w.a_log);
16617 meta[base + 62] = pack.put(&w.dt_bias);
16618 meta[base + 63] = pack.put(&w.norm);
16619 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16620 meta[base + 24] = 0;
16621 }
16622 AttnKind::Linear(w) => {
16623 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16624 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16625 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16626 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16627 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16628 meta[base + 7] = pack.put(&decay);
16629 if let Some((kg, kb)) = &w.k_gate {
16630 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16631 meta[base + 9] = pack.put(kb);
16632 }
16633 if let Some(conv) = &w.conv {
16634 meta[base + 10] = pack.put(conv);
16635 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16636 } else {
16637 meta[base + 24] = 0;
16638 }
16639 }
16640 AttnKind::Full { wq, wk, wv, wo, .. } => {
16641 full_seen = true;
16642 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16643 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16644 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16645 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16646 meta[base + 26] = (li * kv_stride) as u32;
16647 }
16648 AttnKind::Bounded(w) => {
16649 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16650 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16651 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16652 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16653 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16655 meta[base + 27] = pack.put(&w.sink_k);
16656 meta[base + 28] = pack.put(&w.sink_v);
16657 bounded_seen += 1;
16658 }
16659 _ => return None,
16660 }
16661 match &lw.ffn {
16662 FfnKind::Dense(d) => {
16663 meta[base + 15] = 0;
16664 meta[base + 16] = 0;
16665 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16666 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16667 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16668 }
16669 FfnKind::Moe(m) => {
16670 let r = m.resonance.as_ref().unwrap();
16671 let (shared, _) = m.shared.as_ref().unwrap();
16672 meta[base + 15] = 1;
16673 meta[base + 16] = m.experts.len() as u32;
16674 meta[base + 17] = pack.put(&r.mu);
16675 meta[base + 18] = pack.put(&r.u);
16676 meta[base + 19] = pack.put(&r.bias);
16677 meta[base + 20] = r.k as u32;
16678 let mut shell = r.effective_shell(m.experts.len());
16687 shell.push(f32::NEG_INFINITY);
16688 meta[base + 30] = pack.put(&shell);
16689 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16690 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16691 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16692 for (e, ex) in m.experts.iter().enumerate() {
16693 meta[base + 32 + e * 3] =
16694 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16695 meta[base + 33 + e * 3] =
16696 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16697 meta[base + 34 + e * 3] =
16698 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16699 }
16700 }
16701 FfnKind::DenseMoe(_) => return None,
16702 }
16703 }
16704 if !full_seen && self.num_layers == 0 {
16705 return None;
16706 }
16707 let id = {
16708 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16709 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16710 };
16711 let model = crate::gpu::EmbryoGraphModel {
16712 id,
16713 hidden: self.hidden_size,
16714 intermediate: self.intermediate_size,
16715 vocab: self.vocab_size,
16716 layers: self.num_layers,
16717 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16718 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16719 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16720 anchor_q_heads: self.num_heads,
16721 anchor_kv_heads: self.num_kv_heads,
16722 anchor_head_dim: self.head_dim,
16723 rotary_dim: self.rotary_dim,
16724 max_seq: self.kv_cache.max_seq_len,
16725 cluster_count,
16726 cluster_size: self.vocab_size / cluster_count,
16727 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16728 state_stride,
16729 kv_stride,
16730 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16731 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16732 weights: pack.data,
16733 meta,
16734 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16735 clusters: clusters.as_ref().clone(),
16736 final_norm: self.weights.final_norm.clone(),
16737 inv_freq: self.inv_freq.as_ref().clone(),
16738 bounded: bounded.is_some(),
16739 kv_layers: if bounded.is_some() {
16740 bounded_seen
16741 } else {
16742 self.num_layers
16743 },
16744 state_layers: phase_seen + gdn_seen,
16745 anchor_window,
16746 anchor_sink,
16747 phase_layers: phase_seen,
16748 gdn_layers: gdn_seen,
16749 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16750 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16751 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16752 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16753 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16754 };
16755 self.embryo_graph = Some(std::sync::Arc::new(model));
16756 }
16757 self.embryo_graph.clone()
16758 }
16759
16760 fn forward_layers_span(
16761 &mut self,
16762 hidden: &[f32],
16763 position: usize,
16764 task_mask: Option<&TaskMask>,
16765 from: usize,
16766 upto: Option<usize>,
16767 ) -> Vec<f32> {
16768 #[cfg(target_os = "macos")]
16769 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16770 position,
16771 from,
16772 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16773 );
16774 debug_assert!(
16775 from == 0
16776 || (self.dsv4.is_none()
16777 && self.dsv41.is_none()
16778 && self.qwen4_exp.is_none()
16779 && !self.per_token_stack())
16780 );
16781 #[cfg(target_os = "macos")]
16787 if !crate::gpu_metal::wait_replay() {
16788 self.fail_metal_graph("the pending async replay failed before a plain forward");
16789 return vec![0.0; self.hidden_size];
16790 }
16791 if let Some(b) = &mut self.qwen4_exp {
16792 let _ = (task_mask, upto);
16793 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16794 let mut logits = Vec::new();
16795 crate::qwen4_exp::forward_token(
16796 &b.0,
16797 &b.1,
16798 &b.2,
16799 &mut b.3,
16800 token_id,
16801 position,
16802 &self.inv_freq,
16803 self.pool.as_deref(),
16804 &mut logits,
16805 true,
16806 );
16807 self.graph_logits = Some(logits);
16808 return vec![0.0; self.hidden_size];
16809 }
16810 if let Some(b) = &mut self.dsv4 {
16816 let _ = (task_mask, upto);
16817 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16818 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16819 st.pos = position;
16820 let mut logits = Vec::new();
16821 crate::dsv4::forward_token(
16822 g,
16823 layers,
16824 &cfg,
16825 st,
16826 token_id,
16827 &self.inv_freq,
16828 self.pool.as_deref(),
16829 &mut logits,
16830 );
16831 self.graph_logits = Some(logits);
16832 self.dspark_probe(position, token_id);
16833 return vec![0.0; self.hidden_size];
16836 }
16837 if let Some(b) = &mut self.dsv41 {
16839 let _ = (task_mask, upto);
16840 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16841 let mut logits = Vec::new();
16842 crate::dsv41::forward_token(
16843 &b.0,
16844 &b.1,
16845 &b.2,
16846 &mut b.3,
16847 token_id,
16848 position,
16849 self.pool.as_deref(),
16850 &mut logits,
16851 );
16852 self.graph_logits = Some(logits);
16853 return vec![0.0; self.hidden_size];
16854 }
16855 if let Some(b) = &self.g4e {
16861 let _ = (task_mask, upto);
16862 return crate::g4e::g4e_forward(
16863 &b.0,
16864 &b.1,
16865 hidden,
16866 position,
16867 &mut self.kv_cache.layers,
16868 self.num_heads,
16869 self.num_kv_heads,
16870 self.pool.as_deref(),
16871 );
16872 }
16873 if let Some(b) = &self.g3n {
16874 let _ = (task_mask, upto);
16875 return crate::g3n::g3n_forward(
16876 &b.0,
16877 &b.1,
16878 hidden,
16879 position,
16880 &mut self.kv_cache.layers,
16881 self.num_heads,
16882 self.num_kv_heads,
16883 self.head_dim,
16884 self.pool.as_deref(),
16885 );
16886 }
16887 if from == 0
16894 && upto.is_none()
16895 && task_mask.is_none()
16896 && self.anchor_core.is_some()
16897 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16898 {
16899 static ONCE: std::sync::Once = std::sync::Once::new();
16900 ONCE.call_once(|| {
16901 eprintln!(
16902 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16903 unsupported={} eligible={}",
16904 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16905 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16906 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16907 crate::gpu::enabled_here(),
16908 self.graph_refused(),
16909 self.embryo_resident_eligible(),
16910 );
16911 });
16912 }
16913 if from == 0
16914 && upto.is_none()
16915 && task_mask.is_none()
16916 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16920 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16921 && matches!(
16925 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16926 Ok("1") | Ok("parallel")
16927 )
16928 && crate::gpu::enabled_here()
16929 && !self.graph_refused()
16930 && (position == 0 || self.device_sequence_position().is_some())
16936 && self.embryo_resident_eligible()
16937 && let Some(model) = self.ensure_embryo_graph()
16938 {
16939 let mut lg = Vec::new();
16940 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16941 {
16942 self.graph_logits = Some(lg);
16943 return vec![0.0; self.hidden_size];
16944 }
16945 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16946 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16947 }
16948 self.mark_graph_refused();
16954 if position != 0 {
16955 self.kv_cache.clear();
16959 self.clear_history();
16960 crate::gpu::graph_kv_reset(self.graph_kv_id);
16961 panic!(
16962 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16963 );
16964 }
16965 }
16966 let mut h = hidden.to_vec();
16967 self.mimo_moe_prepare();
16970 let _mimo_q8 = self
16971 .mimo_moe
16972 .is_on()
16973 .then(crate::qtensor::enter_full_gpu_q8_scope);
16974 let (nh, _nkv, _hd, hs, _rd, eps) = (
16977 self.num_heads,
16978 self.num_kv_heads,
16979 self.head_dim,
16980 self.hidden_size,
16981 self.rotary_dim,
16982 self.rms_eps,
16983 );
16984 let pool = self.pool.clone();
16985 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
16997 let graph_on = match graph_env.as_deref() {
16998 Some("0") => false,
16999 Some("prefill") => false, Some(_) => true,
17001 None => crate::gpu::wgpu_graph_default(),
17007 };
17008 let graph_trusted =
17009 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17010 let race_eligible =
17011 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17012 let mut tail_start = 0usize;
17013 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17014 let t_graph = std::time::Instant::now();
17015 let mut lg = Vec::new();
17016 let mut gl = 0usize;
17017 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17018 let declined = built.is_none();
17019 let built = match built {
17020 Some(Ok(hh)) => Some(hh),
17021 Some(Err(())) => {
17022 self.clear_sequence_state();
17026 self.graph_failed
17027 .store(true, std::sync::atomic::Ordering::Relaxed);
17028 self.cancel
17029 .store(true, std::sync::atomic::Ordering::Relaxed);
17030 tracing::error!("token graph failed after admission; sequence state cleared");
17031 return vec![0.0; self.hidden_size];
17032 }
17033 None => None,
17034 };
17035 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17040 self.mark_graph_refused();
17041 }
17042 graph_note(built.is_some(), gl, self.num_layers);
17043 if let Some(hh) = built {
17044 let dur = t_graph.elapsed();
17045 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17046 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17047 }
17048 if gl > 0 && gl < self.num_layers {
17049 h = hh;
17055 tail_start = gl;
17056 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17057 if !graph_trusted {
17058 crate::gpu::graph_race_record(true, dur);
17059 }
17060 if !lg.is_empty() {
17061 lg.resize(self.vocab_size, 0.0);
17064 if let Some(c) = self.final_softcap {
17065 softcap_in_place(&mut lg, c, self.pool.as_deref());
17066 }
17067 self.graph_logits = Some(lg);
17068 }
17069 return hh;
17070 }
17071 }
17077 }
17078 let span = from > 0 || upto.is_some();
17102 if span && graph_on && task_mask.is_none() && graph_trusted {
17103 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17104 let mut lg = Vec::new();
17105 let mut gl = 0usize;
17106 let span_res =
17107 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17108 let span_res = match span_res {
17109 Some(Ok(hh)) => Some(hh),
17110 Some(Err(())) => {
17111 self.clear_sequence_state();
17112 self.graph_failed
17113 .store(true, std::sync::atomic::Ordering::Relaxed);
17114 self.cancel
17115 .store(true, std::sync::atomic::Ordering::Relaxed);
17116 tracing::error!(
17117 "span token graph failed after admission; sequence state cleared"
17118 );
17119 return vec![0.0; self.hidden_size];
17120 }
17121 None => None,
17122 };
17123 graph_note(span_res.is_some(), gl, upto_excl - from);
17124 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17125 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17129 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17130 eprintln!(
17131 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17132 upto_excl - from,
17133 span_res.is_some()
17134 );
17135 }
17136 }
17137 if let Some(hh) = span_res {
17138 if gl == upto_excl - from {
17139 if !lg.is_empty() {
17140 lg.resize(self.vocab_size, 0.0);
17141 if let Some(c) = self.final_softcap {
17142 softcap_in_place(&mut lg, c, self.pool.as_deref());
17143 }
17144 self.graph_logits = Some(lg);
17145 }
17146 crate::gpu::set_layer(-1);
17147 return hh;
17148 }
17149 h = hh;
17151 tail_start = from + gl;
17152 }
17153 }
17154 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17159
17160 let host_tail = tail_start > from;
17170 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17171 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17172
17173 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17174 #[cfg(target_os = "macos")]
17175 let mut gpu_skip_until = 0usize;
17176 for li in tail_start.max(from)..self.num_layers {
17177 let _capacity_tail = automatic_gpu_prefix
17178 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17179 .map(|_| crate::gpu::enter_cpu_scope());
17180 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17182 if li > u {
17183 break;
17184 }
17185 }
17186 if let Some(mask) = task_mask {
17187 if !mask.layer_alive(li) {
17188 continue; }
17190 }
17191 if host_tail && task_mask.is_none() {
17196 match self.m3_pooled_layer(li, position, &mut h) {
17197 Ok(true) => {
17198 crate::gpu::set_layer(-1);
17199 continue;
17200 }
17201 Ok(false) => {}
17202 Err(()) => {
17203 self.clear_sequence_state();
17204 self.graph_failed
17205 .store(true, std::sync::atomic::Ordering::Relaxed);
17206 self.cancel
17207 .store(true, std::sync::atomic::Ordering::Relaxed);
17208 tracing::error!(
17209 "pooled tail token graph failed after admission; sequence state cleared"
17210 );
17211 return vec![0.0; self.hidden_size];
17212 }
17213 }
17214 }
17215 #[cfg(target_os = "macos")]
17219 {
17220 if li < gpu_skip_until {
17221 continue;
17222 }
17223 if task_mask.is_none() {
17224 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17225 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17226 return vec![0.0; self.hidden_size];
17230 }
17231 if end > li {
17232 gpu_skip_until = end;
17233 if self.is_loop_end(end - 1) && end < self.num_layers {
17236 h = inference::rms_norm(
17237 &h,
17238 &self.weights.final_norm,
17239 self.rms_eps,
17240 self.norm_style,
17241 );
17242 }
17243 continue;
17244 }
17245 }
17246 }
17247
17248 if task_mask.is_none() {
17249 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17250 crate::gpu::BatchGraphOutcome::Completed => continue,
17251 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17252 crate::gpu::BatchGraphOutcome::Declined => {}
17253 }
17254 }
17255 #[cfg(feature = "gpu")]
17256 self.pull_lagging_host_kv(li, li + 1, position);
17257 let lw = &self.weights.layers[self.phys_layer(li)];
17258 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17259 if tp.parse::<usize>().ok() == Some(position) {
17260 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17261 eprintln!(
17262 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17263 h[0], h[1]
17264 );
17265 }
17266 }
17267 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17270 inference::rms_norm_into(
17271 &h,
17272 &lw.input_norm,
17273 self.rms_eps,
17274 self.norm_style,
17275 &mut self.ws.n1,
17276 );
17277 drop(prof);
17278
17279 let attn_out = match &lw.attn {
17280 AttnKind::Mla(w) => {
17281 let inv_freq_l = self.layer_inv_freq(li);
17282 let rs = self.layer_rope_scale(li);
17283 let eps = self.rms_eps;
17284 let pool = self.pool.clone();
17285 mla_attention(
17286 w,
17287 &self.ws.n1,
17288 &mut self.kv_cache.layers[li],
17289 position,
17290 &inv_freq_l,
17291 rs,
17292 eps,
17293 pool.as_deref(),
17294 )
17295 }
17296 AttnKind::Linear(w) => {
17297 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17298 vmf_phase_forward(
17299 &self.ws.n1,
17300 w,
17301 &cfg,
17302 &mut self.kv_cache.layers[li].linear_state,
17303 self.pool.as_deref(),
17304 )
17305 }
17306 AttnKind::Kda(w) => {
17307 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17308 crate::linear_core::kda_forward(
17309 &self.ws.n1,
17310 w,
17311 &cfg,
17312 &mut self.kv_cache.layers[li].linear_state,
17313 self.pool.as_deref(),
17314 )
17315 }
17316 AttnKind::LinearGdn(w) => {
17317 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17318 gdn_forward(
17319 &self.ws.n1,
17320 w,
17321 &cfg,
17322 &mut self.kv_cache.layers[li].linear_state,
17323 self.pool.as_deref(),
17324 )
17325 }
17326 AttnKind::ShortConv(w) => {
17327 let cfg = self
17328 .short_conv_cfg
17329 .expect("short-conv layer without short_conv_cfg");
17330 short_conv_forward(
17331 &self.ws.n1,
17332 w,
17333 &cfg,
17334 &mut self.kv_cache.layers[li].linear_state,
17335 self.pool.as_deref(),
17336 )
17337 }
17338 AttnKind::Bounded(w) => {
17339 let rope = self
17342 .bounded_rope
17343 .clone()
17344 .expect("bounded layer without an installed rotation table");
17345 let cfg = crate::bounded::BoundedAttnCfg {
17346 num_heads: self.num_heads,
17347 num_kv_heads: self.num_kv_heads,
17348 head_dim: self.head_dim,
17349 hidden_size: hs,
17350 scale: self.attn_scale,
17351 rope: &rope,
17352 pool: pool.as_deref(),
17353 };
17354 crate::bounded::bounded_attention(
17355 &self.ws.n1,
17356 w,
17357 &mut self.kv_cache.layers[li],
17358 &cfg,
17359 )
17360 }
17361 AttnKind::Full {
17362 wq,
17363 wk,
17364 wv,
17365 wo,
17366 q_norm,
17367 k_norm,
17368 output_gate,
17369 softplus_gate,
17370 bias,
17371 } if self.kv_cache.layers[li].o1_sealed() => {
17372 let inv_freq_l = self.layer_inv_freq(li);
17375 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17376 let cfg = QwenAttnCfg {
17377 num_heads: self.layer_num_heads(li),
17378 num_kv_heads: nkv_l,
17379 head_dim: hd_l,
17380 hidden_size: hs,
17381 position,
17382 inv_freq: &inv_freq_l,
17383 rotary_dim: rd_l,
17384 scale: self.attn_scale,
17385 softcap: self.attn_softcap,
17386 window: None,
17387 v_norm: self.attn_v_norm,
17388 qk_norm_after_rope: self.qk_norm_after_rope,
17389 gate_sigmoid: self.proj_gate_sigmoid,
17390 q_norm: q_norm.as_deref(),
17391 k_norm: k_norm.as_deref(),
17392 output_gate: *output_gate,
17393 softplus_gate: softplus_gate
17394 .as_ref()
17395 .map(|(gate, per_head)| (gate, *per_head)),
17396 rope_scale: self.layer_rope_scale(li),
17397 bias: bias
17398 .as_ref()
17399 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17400 rms_eps: eps,
17401 norm_style: self.norm_style,
17402 pool: pool.as_deref(),
17403 v_head_dim: self.layer_v_dim(li),
17404 };
17405 attention::qwen_attention_nystrom(
17406 &self.ws.n1,
17407 wq,
17408 wk,
17409 wv,
17410 wo,
17411 &mut self.kv_cache.layers[li],
17412 &cfg,
17413 )
17414 }
17415 AttnKind::Full {
17416 wq,
17417 wk,
17418 wv,
17419 wo,
17420 q_norm,
17421 k_norm,
17422 output_gate,
17423 softplus_gate,
17424 bias,
17425 } => 'attn: {
17426 let dropin_reason =
17431 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17432 if let Some(reason) = dropin_reason {
17433 self.note_graph_decline("wgpu attn dropin", reason);
17434 }
17435 if graph_on
17436 && dropin_reason.is_none()
17437 && !*output_gate
17438 && softplus_gate.is_none()
17439 && self.attention_heads_per_layer.is_none()
17440 && bias.is_none()
17441 && task_mask.is_none()
17442 {
17443 let inv_freq_l = self.layer_inv_freq(li);
17444 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17445 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17446 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17447 wq.mapped_q1(),
17448 wk.mapped_q1(),
17449 wv.mapped_q1(),
17450 wo.mapped_q1(),
17451 ) {
17452 let gm = gm.clone();
17453 let mut out = vec![0f32; hs];
17454 let cache = &self.kv_cache.layers[li];
17455 if crate::gpu::attn_dropin(
17456 &gm,
17457 self.graph_kv_id,
17458 li,
17459 &self.ws.n1,
17460 qi,
17461 ki,
17462 vi,
17463 oi,
17464 q_norm.as_deref(),
17465 k_norm.as_deref(),
17466 self.qk_norm_after_rope,
17467 &inv_freq_l,
17468 nh,
17469 nkv_l,
17470 hd_l,
17471 rd_l,
17472 hs,
17473 position,
17474 self.kv_cache.max_seq_len,
17475 gemma,
17476 eps as f32,
17477 cache.k_heads(),
17478 cache.v_heads(),
17479 &mut out,
17480 ) {
17481 break 'attn out;
17482 }
17483 }
17484 }
17485 let masked = task_mask
17486 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17487 .unwrap_or(false);
17488 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17489 let plain = self.layer_attn_plain(li);
17492 match (masked, f32_view) {
17493 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17496 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17497 attention::multi_head_attention(
17498 &self.ws.n1,
17499 q,
17500 k,
17501 v,
17502 o,
17503 &mut self.kv_cache.layers[li],
17504 self.num_heads,
17505 self.num_kv_heads,
17506 self.head_dim,
17507 self.hidden_size,
17508 position,
17509 &active_heads,
17510 &self.inv_freq,
17511 )
17512 }
17513 (masked, _) => {
17514 if masked {
17515 tracing::warn!(
17516 "layer {li}: head mask on quantized weights or on a \
17517 window/sink/per-layer-geometry layer not supported \
17518 yet — executing dense"
17519 );
17520 }
17521 let inv_freq_l = self.layer_inv_freq(li);
17522 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17523 let cfg = QwenAttnCfg {
17524 num_heads: self.layer_num_heads(li),
17525 num_kv_heads: nkv_l,
17526 head_dim: hd_l,
17527 hidden_size: hs,
17528 position,
17529 inv_freq: &inv_freq_l,
17530 rotary_dim: rd_l,
17531 scale: self.attn_scale,
17532 softcap: self.attn_softcap,
17533 window: self.layer_window(li),
17534 v_norm: self.attn_v_norm,
17535 qk_norm_after_rope: self.qk_norm_after_rope,
17536 gate_sigmoid: self.proj_gate_sigmoid,
17537 q_norm: q_norm.as_deref(),
17538 k_norm: k_norm.as_deref(),
17539 output_gate: *output_gate,
17540 softplus_gate: softplus_gate
17541 .as_ref()
17542 .map(|(gate, per_head)| (gate, *per_head)),
17543 rope_scale: self.layer_rope_scale(li),
17544 bias: bias
17545 .as_ref()
17546 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17547 rms_eps: eps,
17548 norm_style: self.norm_style,
17549 pool: pool.as_deref(),
17550 v_head_dim: self.layer_v_dim(li),
17551 };
17552 attention::qwen_attention(
17553 &self.ws.n1,
17554 wq,
17555 wk,
17556 wv,
17557 wo,
17558 &mut self.kv_cache.layers[li],
17559 &cfg,
17560 )
17561 }
17562 }
17563 }
17564 };
17565 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17568 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17569 None => attn_out,
17570 };
17571 let lw = &self.weights.layers[self.phys_layer(li)];
17572 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17573 inference::add_rmsnorm_fused_into(
17574 &mut h,
17575 &attn_out,
17576 &lw.post_norm,
17577 self.rms_eps,
17578 self.norm_style,
17579 &mut self.ws.p1,
17580 );
17581 drop(prof);
17582 let mut attn_out = attn_out;
17583 attention::recycle_buf(&mut attn_out);
17584 let post_normed = &self.ws.p1;
17585
17586 let ffn_masked = task_mask
17587 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17588 .unwrap_or(false);
17589 let ffn_out = match (ffn_masked, &lw.ffn) {
17601 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17605 let row = task_mask
17606 .and_then(|tm| tm.ffn_masks.get(li))
17607 .map(|v| v.as_slice());
17608 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17609 }
17610 (true, FfnKind::Dense(d)) => {
17611 let tm = task_mask.unwrap();
17612 let alive = tm.ffn_active_count(li);
17613 let deep = alive * 2 <= self.intermediate_size;
17614 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17615 let active = tm.ffn_active_indices(li);
17616 sparse_ffn_quant(
17617 d,
17618 post_normed,
17619 &active,
17620 self.hidden_size,
17621 self.pool.as_deref(),
17622 )
17623 } else if deep
17624 && let (Some(g), Some(u), Some(dn)) = (
17625 d.gate_proj.as_f32(),
17626 d.up_proj.as_f32(),
17627 d.down_proj.as_f32(),
17628 )
17629 {
17630 let active = tm.ffn_active_indices(li);
17631 inference::sparse_ffn_forward(
17632 post_normed,
17633 g,
17634 u,
17635 dn,
17636 self.hidden_size,
17637 self.intermediate_size,
17638 &active,
17639 self.pool.as_deref(),
17640 )
17641 } else {
17642 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17643 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17644 }
17645 }
17646 (true, FfnKind::Moe(m)) => {
17647 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17651 ffn_forward(
17652 &lw.ffn,
17653 post_normed,
17654 self.pool.as_deref(),
17655 allowed.as_deref(),
17656 )
17657 }
17658 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17659 dm,
17660 post_normed,
17661 &h,
17662 self.rms_eps,
17663 self.norm_style,
17664 self.pool.as_deref(),
17665 ),
17666 (false, _) => match &lw.ffn {
17667 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17668 dm,
17669 post_normed,
17670 &h,
17671 self.rms_eps,
17672 self.norm_style,
17673 self.pool.as_deref(),
17674 ),
17675 FfnKind::Moe(m)
17676 if task_mask.is_none()
17677 && host_tail
17678 && li >= tail_start
17679 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17680 {
17681 moe_ffn_minicpm_tail(
17682 &mut self.minicpm_tail,
17683 li,
17684 self.num_layers,
17685 m,
17686 post_normed,
17687 self.pool.as_deref(),
17688 )
17689 }
17690 FfnKind::Moe(m)
17691 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17692 {
17693 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17694 }
17695 _ => {
17696 let allowed = match (&lw.ffn, task_mask) {
17697 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17698 _ => None,
17699 };
17700 ffn_forward(
17701 &lw.ffn,
17702 post_normed,
17703 self.pool.as_deref(),
17704 allowed.as_deref(),
17705 )
17706 }
17707 },
17708 };
17709 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17710 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17711 None => ffn_out,
17712 };
17713 for (i, &f) in ffn_out.iter().enumerate() {
17714 h[i] += f;
17715 }
17716 let mut ffn_out = ffn_out;
17717 attention::recycle_buf(&mut ffn_out);
17718
17719 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17721 for v in h.iter_mut() {
17722 *v *= sc;
17723 }
17724 }
17725 if self.layer_dump.is_some() {
17727 self.dump_layer_row(position, li, &h);
17728 }
17729
17730 if self.is_loop_end(li) && li + 1 < self.num_layers {
17733 h = inference::rms_norm(
17734 &h,
17735 &self.weights.final_norm,
17736 self.rms_eps,
17737 self.norm_style,
17738 );
17739 }
17740
17741 if self.dyn_phi_layer == Some(li) {
17745 self.update_dyn_phi(&h);
17746 }
17747 }
17748 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17750 crate::gpu::graph_race_record(false, t.elapsed());
17751 }
17752
17753 h
17754 }
17755
17756 fn update_dyn_phi(&mut self, h: &[f32]) {
17759 const A: f32 = 0.2;
17760 if self.dyn_phi_ema.len() != h.len() {
17761 self.dyn_phi_ema = vec![0.0; h.len()];
17762 self.dyn_phi_seen = 0;
17763 }
17764 if self.dyn_phi_seen == 0 {
17765 self.dyn_phi_ema.copy_from_slice(h);
17766 } else {
17767 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17768 *e = (1.0 - A) * *e + A * v;
17769 }
17770 }
17771 self.dyn_phi_seen += 1;
17772 }
17773
17774 pub fn dyn_phi(&self) -> &[f32] {
17776 &self.dyn_phi_ema
17777 }
17778
17779 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17781 self.dyn_phi_layer = layer;
17782 self.dyn_phi_ema.clear();
17783 self.dyn_phi_seen = 0;
17784 }
17785
17786 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17788 let Some(model) = &self.model else {
17789 return Vec::new();
17790 };
17791 model
17792 .header
17793 .skills
17794 .iter()
17795 .enumerate()
17796 .filter_map(|(i, sk)| {
17797 if sk.is_v2() {
17801 return None;
17802 }
17803 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17804 let sel = sk.selection.as_ref()?;
17805 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17806 })
17807 .collect()
17808 }
17809
17810 pub fn active_skill(&self) -> Option<usize> {
17812 self.dyn_active
17813 }
17814
17815 pub fn enable_dynamic_routing(&mut self) -> usize {
17820 use crate::swarm::{DynRouter, RoutableSkill};
17821 let Some(model) = self.model.clone() else {
17822 return 0;
17823 };
17824 if let Some(r) = &model.header.router {
17831 tracing::warn!(
17832 "dynamic routing disabled: this file declares router policy '{}' with \
17833 granularity \"{}\" — the request-level decision applies instead",
17834 r.policy,
17835 r.granularity
17836 );
17837 return 0;
17838 }
17839 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17845 || model.header.skills.iter().any(|s| s.is_v2())
17846 {
17847 tracing::warn!(
17848 "dynamic routing disabled: this file carries format-v2 skill records \
17849 (SKILLS_V2) — they route per request through a router policy only"
17850 );
17851 return 0;
17852 }
17853 if self.dyn_blend_loaded {
17856 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17857 return 0;
17858 }
17859 if let Some(a) = self.dyn_active {
17863 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17864 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17865 return 0;
17866 }
17867 }
17868 let hidden = self.hidden_size;
17869 let mut skills = Vec::new();
17870 for (idx, id, _phi) in self.dynamic_skills() {
17871 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17872 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17873 skills.push(rs);
17874 }
17875 }
17876 }
17877 if skills.is_empty() {
17878 return 0;
17879 }
17880 let phi = skills[0].phi_layer;
17882 if skills.iter().any(|s| s.phi_layer != phi) {
17883 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17884 }
17885 let n = skills.len();
17886 self.set_dyn_phi_layer(Some(phi));
17887 self.dyn_router = Some(DynRouter::new(skills));
17888 n
17889 }
17890
17891 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17893 self.dyn_router
17894 .as_ref()
17895 .map(|r| r.switches.clone())
17896 .unwrap_or_default()
17897 }
17898
17899 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17902 let _mimo_q8 = self
17903 .mimo_moe
17904 .is_on()
17905 .then(crate::qtensor::enter_full_gpu_q8_scope);
17906 let rows = self.weights.lm_head.rows();
17907 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17908 let served = self.mimo_moe.is_on()
17912 && crate::gpu::mimo_q8_short_enabled()
17913 && rows == self.vocab_size
17914 && !self.weights.lm_head.has_prism_contract()
17915 && self
17916 .weights
17917 .lm_head
17918 .graph_weight()
17919 .is_some_and(|(model, idx, kind, _)| {
17920 kind == 7
17921 && crate::gpu::q82_short_rows(
17922 model,
17923 idx,
17924 hidden,
17925 1,
17926 rows,
17927 self.hidden_size,
17928 &mut logits,
17929 )
17930 });
17931 if !served {
17932 self.weights
17933 .lm_head
17934 .matvec(hidden, &mut logits, self.pool.as_deref());
17935 }
17936 logits.resize(self.vocab_size, 0.0);
17937 if let Some(m) = self.logit_multiplier {
17938 for l in logits.iter_mut() {
17939 *l *= m;
17940 }
17941 }
17942 if let Some(c) = self.final_softcap {
17943 softcap_in_place(&mut logits, c, self.pool.as_deref());
17944 }
17945 if let Some(cm) = self.head_clusters.as_ref() {
17946 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17947 }
17948 logits
17949 }
17950
17951 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17954 let h = hidden.len();
17955 let ncl = cm.len() / h.max(1);
17956 if ncl == 0 || logits.len() % ncl != 0 {
17957 return;
17958 }
17959 let cs = logits.len() / ncl;
17960 let mut lc = vec![0.0f32; ncl];
17962 for c in 0..ncl {
17963 let row = &cm[c * h..(c + 1) * h];
17964 let mut s = 0.0f32;
17965 for j in 0..h {
17966 s += row[j] * hidden[j];
17967 }
17968 lc[c] = s;
17969 }
17970 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17971 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17972 for c in 0..ncl {
17973 let blk = &mut logits[c * cs..(c + 1) * cs];
17974 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17975 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17976 let add = lc[c] - lse - bl;
17977 for v in blk.iter_mut() {
17978 *v += add;
17979 }
17980 }
17981 }
17982
17983 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17988 #[cfg(target_os = "macos")]
17989 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17990 self.clear_sequence_state();
17991 crate::gpu::graph_race_begin_generation();
17995 if task_mask.is_none() {
17996 self.o1_begin();
17997 }
17998 let mut hidden = vec![0.0f32; self.hidden_size];
17999 for (pos, &id) in ids.iter().enumerate() {
18000 let emb = self.embed_single(id);
18001 hidden = self.forward_layers(&emb, pos, task_mask);
18002 }
18003 if let Err(err) = self.o1_seal_checked() {
18004 self.o1_fail(err);
18005 }
18006 if let Some(logits) = self.graph_logits.take() {
18009 return logits;
18010 }
18011 inference::rms_norm_into(
18012 &hidden,
18013 &self.weights.final_norm,
18014 self.rms_eps,
18015 self.norm_style,
18016 &mut self.ws.n1,
18017 );
18018 self.lm_head_forward(&self.ws.n1)
18019 }
18020}
18021
18022pub fn create_test_pipeline(
18024 hidden_size: usize,
18025 intermediate_size: usize,
18026 num_heads: usize,
18027 num_kv_heads: usize,
18028 head_dim: usize,
18029 num_layers: usize,
18030 vocab_size: usize,
18031) -> Pipeline {
18032 let synth = |n: usize, salt: usize| -> Vec<f32> {
18035 (0..n)
18036 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18037 .collect()
18038 };
18039 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18040 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18041 };
18042 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18043 .map(|li| LayerWeights {
18044 input_norm: vec![1.0; hidden_size],
18045 post_norm: vec![1.0; hidden_size],
18046 attn_out_norm: None,
18047 ffn_out_norm: None,
18048 layer_scale: None,
18049 ffn: FfnKind::Dense(DenseFfn {
18050 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18051 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18052 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18053 act: Act::Silu,
18054 down_t: None,
18055 segs: Vec::new(),
18056 }),
18057 attn: AttnKind::Full {
18058 bias: None,
18059 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18060 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18061 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18062 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18063 q_norm: None,
18064 k_norm: None,
18065 output_gate: false,
18066 softplus_gate: None,
18067 },
18068 })
18069 .collect();
18070
18071 Pipeline::new(
18072 Tokenizer::byte_level(),
18073 PipelineWeights {
18074 embed_tokens: qt(vocab_size, hidden_size, 100),
18075 layers: layer_weights,
18076 lm_head: qt(vocab_size, hidden_size, 200),
18077 final_norm: vec![1.0; hidden_size],
18078 },
18079 hidden_size,
18080 intermediate_size,
18081 num_heads,
18082 num_kv_heads,
18083 head_dim,
18084 num_layers,
18085 num_layers, false, vocab_size,
18088 1e-6,
18089 10_000.0,
18090 NormStyle::Qwen,
18091 4096,
18092 SamplerConfig {
18093 seed: Some(42),
18094 ..Default::default()
18095 },
18096 )
18097}
18098
18099#[inline]
18104fn mask_bit(row: &[u8], j: usize) -> bool {
18105 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18106}
18107
18108fn mask_gain() -> f32 {
18119 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18120 *G.get_or_init(|| {
18121 std::env::var("CMF_FFN_MASK_GAIN")
18122 .ok()
18123 .and_then(|v| v.parse().ok())
18124 .unwrap_or(1.0)
18125 })
18126}
18127
18128fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18129 let fill = meanfill().and_then(|(i, v)| {
18132 let li = crate::gpu::cur_layer();
18133 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18134 });
18135 for r in 0..rows {
18136 let base = r * inter;
18137 for (bi, &byte) in row.iter().enumerate() {
18138 if byte == 0xFF {
18139 continue;
18140 }
18141 let j0 = bi * 8;
18142 for bit in 0..8 {
18143 let j = j0 + bit;
18144 if j < inter && byte & (1 << bit) == 0 {
18145 g[base + j] = fill.map_or(0.0, |f| f[j]);
18146 }
18147 }
18148 }
18149 }
18150 let gain = mask_gain();
18151 if gain != 1.0 {
18152 for v in g[..rows * inter].iter_mut() {
18153 *v *= gain;
18154 }
18155 }
18156}
18157
18158#[inline]
18160fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18161 row.is_none_or(|r| mask_bit(r, i))
18162}
18163
18164fn all_bits_on(row: &[u8], n: usize) -> bool {
18167 (0..n).all(|i| mask_bit(row, i))
18168}
18169
18170fn tube_topk() -> usize {
18178 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18179 *K.get_or_init(|| {
18180 std::env::var("CMF_TUBE_TOPK")
18181 .ok()
18182 .and_then(|v| v.parse().ok())
18183 .unwrap_or(0)
18184 })
18185}
18186
18187fn tube_score_oracle() -> bool {
18188 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18189 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18190}
18191
18192fn tube_ffn_routed(
18199 d: &DenseFfn,
18200 xs: &[f32],
18201 b: usize,
18202 pool: Option<&Pool>,
18203 mask_row: Option<&[u8]>,
18204 k: usize,
18205) -> Vec<f32> {
18206 let hidden = d.down_proj.rows();
18207 let core = d.gate_proj.rows();
18208 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18209 let mut out = match (b, core_full, mask_row) {
18210 (1, true, _) => dense_ffn(d, xs, pool),
18211 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18212 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18213 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18214 };
18215 let cand: Vec<usize> = (0..d.segs.len())
18216 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18217 .collect();
18218 if cand.is_empty() {
18219 return out;
18220 }
18221 let oracle = tube_score_oracle();
18225 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18226 let mut scores = vec![0f32; b * cand.len()];
18227 for (ci, &i) in cand.iter().enumerate() {
18228 let seg = &d.segs[i];
18229 let w = seg.width;
18230 let mut g = vec![0.0f32; b * w];
18231 if b == 1 {
18232 seg.gate.matvec(xs, &mut g, pool);
18233 } else {
18234 seg.gate.matmat(xs, b, &mut g, pool);
18235 }
18236 for v in g.iter_mut() {
18237 *v = Act::Silu.combine(*v, 1.0);
18238 }
18239 if !oracle {
18240 for t in 0..b {
18241 scores[t * cand.len() + ci] =
18242 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18243 }
18244 }
18245 if oracle || b > 1 {
18246 let mut u = vec![0.0f32; b * w];
18247 if b == 1 {
18248 seg.up.matvec(xs, &mut u, pool);
18249 } else {
18250 seg.up.matmat(xs, b, &mut u, pool);
18251 }
18252 for (a, &v) in g.iter_mut().zip(u.iter()) {
18253 *a *= v;
18254 }
18255 if oracle {
18256 for t in 0..b {
18257 scores[t * cand.len() + ci] =
18258 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18259 }
18260 }
18261 }
18262 acts.push(g);
18263 }
18264 let keep = k.min(cand.len());
18266 let mut scratch: Vec<f32> = Vec::new();
18267 for t in 0..b {
18268 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18269 .map(|ci| (scores[t * cand.len() + ci], ci))
18270 .collect();
18271 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18272 let mut alive = vec![false; cand.len()];
18273 for &(_, ci) in sc.iter().take(keep) {
18274 alive[ci] = true;
18275 }
18276 if b > 1 {
18277 for (ci, a) in acts.iter_mut().enumerate() {
18278 if !alive[ci] {
18279 let w = d.segs[cand[ci]].width;
18280 a[t * w..(t + 1) * w].fill(0.0);
18281 }
18282 }
18283 } else {
18284 for (ci, &i) in cand.iter().enumerate() {
18288 if !alive[ci] {
18289 continue;
18290 }
18291 let seg = &d.segs[i];
18292 let w = seg.width;
18293 let g = &mut acts[ci];
18294 if !tube_score_oracle() {
18295 scratch.clear();
18296 scratch.resize(w, 0.0);
18297 seg.up.matvec(xs, &mut scratch, pool);
18298 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18299 *a *= v;
18300 }
18301 }
18302 let mut acc = vec![0.0f32; hidden];
18303 seg.down.matvec(g, &mut acc, pool);
18304 for (o, a) in out.iter_mut().zip(&acc) {
18305 *o += *a;
18306 }
18307 }
18308 }
18309 }
18310 if b > 1 {
18311 for (ci, &i) in cand.iter().enumerate() {
18312 let seg = &d.segs[i];
18313 let mut acc = vec![0.0f32; b * hidden];
18314 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18315 for (o, a) in out.iter_mut().zip(&acc) {
18316 *o += *a;
18317 }
18318 }
18319 }
18320 out
18321}
18322
18323fn tube_ffn(
18329 d: &DenseFfn,
18330 xs: &[f32],
18331 b: usize,
18332 pool: Option<&Pool>,
18333 mask_row: Option<&[u8]>,
18334) -> Vec<f32> {
18335 if tube_topk() > 0 {
18336 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18337 }
18338 let hidden = d.down_proj.rows();
18339 let core = d.gate_proj.rows();
18340 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18341 let mut out = match (b, core_full, mask_row) {
18342 (1, true, _) => dense_ffn(d, xs, pool),
18343 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18344 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18345 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18346 };
18347 TUBE_SCRATCH.with(|sc| {
18348 let mut sc = sc.borrow_mut();
18349 let [g, u, acc] = &mut *sc;
18350 for seg in &d.segs {
18351 if !tube_bit(mask_row, seg.start) {
18352 continue;
18353 }
18354 let w = seg.width;
18355 g.resize(b * w, 0.0);
18356 if b == 1
18357 && d.act == Act::Silu
18358 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18359 {
18360 } else {
18362 u.resize(b * w, 0.0);
18363 if b == 1 {
18364 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18365 } else {
18366 seg.gate.matmat(xs, b, g, pool);
18367 seg.up.matmat(xs, b, u, pool);
18368 }
18369 for i in 0..b * w {
18370 g[i] = d.act.combine(g[i], u[i]);
18371 }
18372 }
18373 acc.resize(b * hidden, 0.0);
18374 acc.fill(0.0);
18375 if b == 1 {
18376 seg.down.matvec(g, acc, pool);
18377 } else {
18378 seg.down.matmat(g, b, acc, pool);
18379 }
18380 for (o, a) in out.iter_mut().zip(acc.iter()) {
18381 *o += *a;
18382 }
18383 }
18384 out
18385 })
18386}
18387
18388thread_local! {
18389 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18393 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18394}
18395
18396static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18399 std::sync::atomic::AtomicU64::new(0),
18400 std::sync::atomic::AtomicU64::new(0),
18401];
18402
18403fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18407 #[cfg(feature = "gpu")]
18408 {
18409 use std::sync::atomic::Ordering::Relaxed;
18410 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18411 (
18412 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18413 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18414 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18415 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18416 )
18417 }
18418 #[cfg(not(feature = "gpu"))]
18419 (0.0, 0.0, 0.0, 0)
18420}
18421
18422fn wgpu_mirror_counters() -> [u64; 6] {
18425 #[cfg(feature = "gpu")]
18426 {
18427 crate::gpu_wgpu::MIRROR_EVENTS
18428 .each_ref()
18429 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18430 }
18431 #[cfg(not(feature = "gpu"))]
18432 [0; 6]
18433}
18434
18435fn prefill_prof_on() -> bool {
18436 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18437 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18438}
18439
18440pub(crate) fn accumulate_dense_ffn_activation_mass(
18449 d: &DenseFfn,
18450 xs: &[f32],
18451 b: usize,
18452 pool: Option<&Pool>,
18453 mass: &mut [f64],
18454 squared_mass: &mut [f64],
18455) -> Result<(), String> {
18456 let inter = d.gate_proj.rows();
18457 let hidden = d.gate_proj.cols();
18458 if mass.len() != inter || squared_mass.len() != inter {
18459 return Err(format!(
18460 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18461 mass.len(),
18462 squared_mass.len()
18463 ));
18464 }
18465 if d.up_proj.rows() != inter
18466 || d.up_proj.cols() != hidden
18467 || d.down_proj.cols() != inter
18468 || d.down_proj.rows() != hidden
18469 {
18470 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18471 }
18472 if !d.segs.is_empty() {
18473 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18474 }
18475 if xs.len() != b.saturating_mul(hidden) {
18476 return Err(format!(
18477 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18478 xs.len()
18479 ));
18480 }
18481 if b == 0 {
18482 return Ok(());
18483 }
18484 let mut gate = vec![0.0f32; b * inter];
18485 let mut up = vec![0.0f32; b * inter];
18486 d.gate_proj.matmat(xs, b, &mut gate, pool);
18487 d.up_proj.matmat(xs, b, &mut up, pool);
18488 for row in 0..b {
18489 for i in 0..inter {
18490 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18491 let a = a as f64;
18492 mass[i] += a.abs();
18493 squared_mass[i] += a * a;
18494 }
18495 }
18496 Ok(())
18497}
18498
18499fn dense_ffn_batch(
18500 d: &DenseFfn,
18501 xs: &[f32],
18502 b: usize,
18503 pool: Option<&Pool>,
18504 mask_row: Option<&[u8]>,
18505) -> Vec<f32> {
18506 let inter = d.gate_proj.rows();
18507 let hidden = d.down_proj.rows();
18508 let fused_act = d.act.wgpu_graph_act();
18520 if mask_row.is_none()
18521 && fused_act.is_some()
18522 && b >= 32
18523 && crate::gpu::enabled_here()
18524 && !crate::gpu::mm_killed()
18525 && refit_dir().is_none()
18530 && !ffn_probe_active()
18535 && !crate::gptq_capture::capturing()
18539 {
18540 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18541 d.gate_proj.mapped_q4t(),
18542 d.up_proj.mapped_q4t(),
18543 d.down_proj.mapped_q4t(),
18544 ) {
18545 let mut out = vec![0.0f32; b * hidden];
18546 let act = fused_act.expect("checked above");
18547 if crate::gpu::q4_ffn_act(
18548 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18549 ) {
18550 return out;
18551 }
18552 }
18553 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18558 d.gate_proj.mapped_q4tp(),
18559 d.up_proj.mapped_q4tp(),
18560 d.down_proj.mapped_q4tp(),
18561 ) {
18562 let mut out = vec![0.0f32; b * hidden];
18563 let act = fused_act.expect("checked above");
18564 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18565 {
18566 return out;
18567 }
18568 }
18569 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18575 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18576 d.gate_proj.mapped_device_gemm(),
18577 d.up_proj.mapped_device_gemm(),
18578 d.down_proj.mapped_device_gemm(),
18579 ) {
18580 let mut out = vec![0.0f32; b * hidden];
18581 let act = fused_act.expect("checked above");
18582 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18583 return out;
18584 }
18585 }
18586 }
18587 let mut g = vec![0.0f32; b * inter];
18588 d.gate_proj.matmat(xs, b, &mut g, pool);
18589 let mut u = vec![0.0f32; b * inter];
18590 d.up_proj.matmat(xs, b, &mut u, pool);
18591 if gate_topk() > 0 && d.act == Act::Silu {
18592 for t in 0..b {
18593 let row = &mut g[t * inter..(t + 1) * inter];
18594 for v in row.iter_mut() {
18595 *v = Act::Silu.combine(*v, 1.0);
18596 }
18597 keep_top_k(row, gate_topk());
18598 }
18599 for i in 0..b * inter {
18600 g[i] *= u[i];
18601 }
18602 } else {
18603 for i in 0..b * inter {
18604 g[i] = d.act.combine(g[i], u[i]);
18605 }
18606 }
18607 if let Some(row) = mask_row {
18608 zero_masked_cols(&mut g, b, inter, row);
18609 }
18610 if oracle_topk() > 0 {
18611 for t in 0..b {
18612 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18613 }
18614 }
18615 let mut out = vec![0.0f32; b * hidden];
18616 d.down_proj.matmat(&g, b, &mut out, pool);
18617 if refit_dir().is_some() {
18618 let li = crate::gpu::cur_layer();
18619 if li >= 0 {
18620 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18621 }
18622 }
18623 FFN_PROBE.with(|pr| {
18627 if let Some(acc) = pr.borrow_mut().as_mut() {
18628 let li = crate::gpu::cur_layer();
18629 if li < 0 {
18630 return;
18631 }
18632 let Some(row) = acc.get_mut(li as usize) else {
18633 return;
18634 };
18635 let sq = probe_sq();
18636 for t in 0..b {
18637 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18638 *a += if sq {
18639 (v as f64) * (v as f64)
18640 } else {
18641 (v as f64).abs()
18642 };
18643 }
18644 }
18645 }
18646 });
18647 out
18648}
18649
18650fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18655 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18656 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18657 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18658 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18659 if (!on && !dump) || b == 0 {
18660 return;
18661 }
18662 let hidden = xs.len() / b;
18663 if on {
18664 let mut acc = m.act_sq.borrow_mut();
18665 if acc.len() < hidden {
18666 acc.resize(hidden, 0.0);
18667 }
18668 for t in 0..b {
18669 let row = &xs[t * hidden..(t + 1) * hidden];
18670 for (a, &v) in acc.iter_mut().zip(row) {
18671 *a += (v as f64) * (v as f64);
18672 }
18673 }
18674 }
18675 if dump {
18676 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18679 .ok()
18680 .and_then(|v| v.parse().ok())
18681 .unwrap_or(4096);
18682 let mut rows = m.act_rows.borrow_mut();
18683 if rows.len() < cap * hidden {
18684 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18685 rows.extend_from_slice(&xs[..take * hidden]);
18686 }
18687 }
18688}
18689
18690#[derive(Clone, Copy)]
18693struct SendVecs(*mut Vec<f32>);
18694unsafe impl Send for SendVecs {}
18695unsafe impl Sync for SendVecs {}
18696impl SendVecs {
18697 #[inline]
18698 fn at(self, i: usize) -> *mut Vec<f32> {
18699 unsafe { self.0.add(i) }
18700 }
18701}
18702
18703fn moe_ffn_batch(
18704 m: &MoeFfn,
18705 xs: &[f32],
18706 b: usize,
18707 hidden: usize,
18708 pool: Option<&Pool>,
18709 allowed: Option<&[bool]>,
18710) -> Vec<f32> {
18711 accumulate_act(m, xs, b);
18712 let ne = m.experts.len();
18713 let mut logits = vec![0.0f32; b * ne];
18714 match &m.resonance {
18715 Some(r) => {
18716 let hdim = xs.len() / b.max(1);
18717 for bi in 0..b {
18718 r.scores(
18719 &xs[bi * hdim..(bi + 1) * hdim],
18720 &mut logits[bi * ne..(bi + 1) * ne],
18721 );
18722 }
18723 }
18724 None => m.router.matmat(xs, b, &mut logits, pool),
18725 }
18726
18727 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18730 {
18731 let mut st = m.stats.borrow_mut();
18732 if st.len() < ne {
18733 st.resize(ne, 0);
18734 }
18735 for bi in 0..b {
18736 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18737 for &e in &idx {
18738 st[e] += 1;
18739 assign[e].push((bi, p[e] / wsum));
18740 }
18741 }
18742 }
18743
18744 let mut out = vec![0.0f32; b * hidden];
18745 let cols = m.experts[0].gate_proj.cols();
18746 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18747 let sb = list.len();
18748 let mut sub = vec![0.0f32; sb * cols];
18749 for (k, &(bi, _)) in list.iter().enumerate() {
18750 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18751 }
18752 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18753 for (k, &(bi, w)) in list.iter().enumerate() {
18754 for i in 0..hidden {
18755 out[bi * hidden + i] += w * eo[k * hidden + i];
18756 }
18757 }
18758 };
18759 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18765 if pool.is_some() && active.len() >= 8 {
18766 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18767 {
18768 let panel_ptr = SendVecs(panels.as_mut_ptr());
18769 let experts = &m.experts;
18772 let (active_r, assign_r) = (&active, &assign);
18773 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18774 let run = |start: usize, end: usize| {
18775 let _cpu_scope = inherit_cpu();
18776 #[cfg(target_os = "macos")]
18784 let _metal_cpu = crate::gpu::enter_cpu_scope();
18785 for ai in start..end {
18786 let e = active_r[ai];
18787 let list = &assign_r[e];
18788 let sb = list.len();
18789 let mut sub = vec![0.0f32; sb * cols];
18790 for (k, &(bi, _)) in list.iter().enumerate() {
18791 sub[k * cols..(k + 1) * cols]
18792 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18793 }
18794 unsafe {
18796 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18797 }
18798 }
18799 };
18800 match pool {
18801 Some(p) => p.run_rows(active.len(), &run),
18802 None => run(0, active.len()),
18803 }
18804 }
18805 for (ai, &e) in active.iter().enumerate() {
18806 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18807 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18808 for i in 0..hidden {
18809 out[bi * hidden + i] += w * eo[i];
18810 }
18811 }
18812 }
18813 } else {
18814 for &e in &active {
18815 run_expert(&m.experts[e], &assign[e], &mut out);
18816 }
18817 }
18818 if let Some((se, gate)) = &m.shared {
18819 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18820 let mut gl = vec![0.0f32; b];
18821 gate.matmat(xs, b, &mut gl, pool);
18822 (0..b)
18823 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18824 .collect()
18825 } else {
18826 (0..b).map(|bi| (bi, 1.0)).collect()
18827 };
18828 run_expert(se, &all, &mut out);
18829 }
18830 out
18831}
18832
18833fn moe_ffn_rows_exact(
18845 m: &MoeFfn,
18846 xs: &[f32],
18847 b: usize,
18848 hidden: usize,
18849 pool: Option<&Pool>,
18850) -> Vec<f32> {
18851 let mut out = vec![0.0f32; b * hidden];
18852 let per_row = |out: &mut [f32]| {
18853 for r in 0..b {
18854 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18855 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18856 }
18857 };
18858 let covered = !crate::gpu::enabled_here()
18859 && moe_batch_enabled()
18860 && m.shared.is_none()
18861 && m.resonance.is_none()
18862 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18863 && m.experts.iter().all(|d| d.act == Act::Silu);
18864 if !covered {
18865 per_row(&mut out);
18866 return out;
18867 }
18868 let ne = m.experts.len();
18869 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18871 for r in 0..b {
18872 let x = &xs[r * hidden..(r + 1) * hidden];
18873 accumulate_act(m, x, 1);
18874 let mut logits = vec![0.0f32; ne];
18875 m.router.matvec(x, &mut logits, pool);
18876 let (idx, p, wsum) = moe_route(&logits, m, None);
18877 {
18878 let mut st = m.stats.borrow_mut();
18879 if st.len() < ne {
18880 st.resize(ne, 0);
18881 }
18882 for &e in &idx {
18883 st[e] += 1;
18884 }
18885 }
18886 let w: Vec<f32> = idx
18887 .iter()
18888 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18889 .collect();
18890 routes.push((idx, w));
18891 }
18892 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18893 per_row(&mut out);
18894 return out;
18895 }
18896 let mut experts: Vec<usize> = Vec::new();
18898 let mut groups: Vec<Vec<usize>> = Vec::new();
18899 for (r, (idx, _)) in routes.iter().enumerate() {
18900 for &e in idx {
18901 match experts.iter().position(|&x| x == e) {
18902 Some(g) => groups[g].push(r),
18903 None => {
18904 experts.push(e);
18905 groups.push(vec![r]);
18906 }
18907 }
18908 }
18909 }
18910 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18911 let inter = m.experts[experts[0]].gate_proj.rows();
18912 let pairs: Vec<(&QTensor, &QTensor)> = experts
18913 .iter()
18914 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18915 .collect();
18916 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18917 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18918 per_row(&mut out);
18919 return out;
18920 }
18921 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18922 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18923 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18924 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18925 per_row(&mut out);
18926 return out;
18927 }
18928 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18930 let mut p = 0usize;
18931 for (g, &e) in experts.iter().enumerate() {
18932 for &r in &groups[g] {
18933 slot.insert((r, e), p);
18934 p += 1;
18935 }
18936 }
18937 for (r, (idx, w)) in routes.iter().enumerate() {
18938 let terms: Vec<(&[f32], f32)> = idx
18939 .iter()
18940 .zip(w)
18941 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18942 .collect();
18943 let row = &mut out[r * hidden..(r + 1) * hidden];
18944 for (i, dst) in row.iter_mut().enumerate() {
18945 let mut acc = 0f32;
18947 for (d, we) in &terms {
18948 acc += we * d[i];
18949 }
18950 *dst = acc;
18951 }
18952 }
18953 out
18954}
18955
18956pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18960 match pool {
18961 Some(p) if v.len() >= 1 << 15 => {
18962 let base = v.as_mut_ptr() as usize;
18963 p.run_rows(v.len(), &|s, e| {
18964 let sl =
18967 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18968 for x in sl {
18969 *x = c * (*x / c).tanh();
18970 }
18971 });
18972 }
18973 _ => {
18974 for x in v.iter_mut() {
18975 *x = c * (*x / c).tanh();
18976 }
18977 }
18978 }
18979}
18980
18981thread_local! {
18982 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18986 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18987}
18988
18989fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18991 if gate_topk() > 0
18994 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
18995 {
18996 return out;
18997 }
18998 let prism_body = d.gate_proj.has_prism_contract()
19014 || d.up_proj.has_prism_contract()
19015 || d.down_proj.has_prism_contract();
19016 if !prism_body
19017 && crate::gpu::enabled_here()
19018 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19019 {
19020 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19021 crate::gpu::ProbeArm::Gpu
19022 } else {
19023 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19024 };
19025 match arm {
19026 crate::gpu::ProbeArm::Gpu => {
19027 let t0 = std::time::Instant::now();
19028 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19029 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19030 return out;
19031 }
19032 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19036 }
19037 crate::gpu::ProbeArm::CpuTimed => {
19038 let t0 = std::time::Instant::now();
19039 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19040 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19041 return out;
19042 }
19043 crate::gpu::ProbeArm::Cpu => {
19044 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19045 }
19046 }
19047 }
19048 dense_ffn_cpu(d, x, pool)
19049}
19050
19051fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19053 let inter = d.gate_proj.rows();
19054 FFN_SCRATCH.with(|s| {
19055 let mut s = s.borrow_mut();
19056 let [g, u, ..] = &mut *s;
19057 g.resize(inter, 0.0);
19058 if gate_topk() > 0 {
19061 u.resize(inter, 0.0);
19065 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19066 for i in 0..inter {
19067 g[i] = Act::Silu.combine(g[i], 1.0);
19068 }
19069 keep_top_k(g, gate_topk());
19070 for i in 0..inter {
19071 g[i] *= u[i];
19072 }
19073 } else if d.act == Act::Silu && {
19074 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19075 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19076 } {
19077 } else {
19079 u.resize(inter, 0.0);
19080 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19082 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19083 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19084 for i in 0..inter {
19085 g[i] = d.act.combine(g[i], u[i]);
19086 }
19087 }
19088 FFN_PROBE.with(|pr| {
19096 if let Some(acc) = pr.borrow_mut().as_mut() {
19097 let li = crate::gpu::cur_layer();
19098 if li >= 0 {
19099 if let Some(row) = acc.get_mut(li as usize) {
19100 match probe_topk() {
19101 0 if probe_sq() => {
19102 for (a, &v) in row.iter_mut().zip(g.iter()) {
19103 *a += (v as f64) * (v as f64);
19104 }
19105 }
19106 0 if probe_signed() => {
19107 for (a, &v) in row.iter_mut().zip(g.iter()) {
19108 *a += v as f64;
19109 }
19110 }
19111 0 => {
19112 for (a, &v) in row.iter_mut().zip(g.iter()) {
19113 *a += (v as f64).abs();
19114 }
19115 }
19116 k => {
19117 let n = g.len();
19118 let k = k.min(n);
19119 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19120 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19121 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19122 });
19123 let thr = *kth;
19124 for (a, &v) in row.iter_mut().zip(g.iter()) {
19125 if v.abs() >= thr {
19126 *a += 1.0;
19127 }
19128 }
19129 }
19130 }
19131 }
19132 }
19133 }
19134 });
19135 if oracle_topk() > 0 {
19136 keep_top_k(g, oracle_topk());
19137 }
19138 {
19139 let li = crate::gpu::cur_layer();
19140 if li >= 0 {
19141 adump_row(li as usize, g);
19142 }
19143 }
19144 let mut out = attention::take_buf(d.down_proj.rows());
19145 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19146 d.down_proj.matvec(g, &mut out, pool);
19147 out
19148 })
19149}
19150
19151pub struct RefitAcc {
19164 pub support: Vec<u32>,
19165 pub gss: Vec<f32>,
19166 pub ya: Vec<f32>,
19167 pub hidden: usize,
19168 pub tokens: u64,
19169 pub buf_g: Vec<f32>,
19175 pub buf_o: Vec<f32>,
19176 pub buf_t: usize,
19177}
19178
19179type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19183
19184static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19185 std::sync::OnceLock::new();
19186
19187fn ffn_probe_active() -> bool {
19190 FFN_PROBE.with(|p| p.borrow().is_some())
19191}
19192
19193fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19194 REFIT
19195 .get_or_init(|| {
19196 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19197 (
19198 d,
19199 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19200 )
19201 })
19202 })
19203 .as_ref()
19204}
19205
19206fn refit_accumulate(
19208 li: usize,
19209 g: &[f32],
19210 b: usize,
19211 inter: usize,
19212 out: &[f32],
19213 hidden: usize,
19214 pool: Option<&Pool>,
19215) {
19216 let Some((dir, map)) = refit_dir() else {
19217 return;
19218 };
19219 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19220 let (from, to) = *SPAN.get_or_init(|| {
19221 let g = |k: &str, d: usize| {
19222 std::env::var(k)
19223 .ok()
19224 .and_then(|v| v.parse().ok())
19225 .unwrap_or(d)
19226 };
19227 (
19228 g("CMF_FFN_REFIT_FROM", 0),
19229 g("CMF_FFN_REFIT_TO", usize::MAX),
19230 )
19231 });
19232 if li < from || li > to {
19233 return;
19234 }
19235 let mut guard = map.lock().unwrap();
19236 let (map, shared) = &mut *guard;
19237 let acc = match map.entry(li) {
19238 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19239 std::collections::hash_map::Entry::Vacant(e) => {
19240 let path = format!("{dir}/support.{li}.u32");
19241 let Ok(bytes) = std::fs::read(&path) else {
19242 eprintln!("refit: no {path} — layer {li} skipped");
19243 return;
19244 };
19245 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19246 let support: Vec<u32> = bytes[4..4 + n * 4]
19247 .chunks_exact(4)
19248 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19249 .collect();
19250 eprintln!(
19251 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19252 (n * n + hidden * n) as f64 * 4.0 / 1e6
19253 );
19254 e.insert(RefitAcc {
19255 gss: vec![0.0; n * n],
19256 ya: vec![0.0; hidden * n],
19257 buf_g: Vec::new(),
19258 buf_o: Vec::new(),
19259 buf_t: 0,
19260 support,
19261 hidden,
19262 tokens: 0,
19263 })
19264 }
19265 };
19266 let ns = acc.support.len();
19267 let cap = refit_batch();
19269 if acc.buf_g.is_empty() {
19270 acc.buf_g = vec![0.0; ns * cap];
19271 acc.buf_o = vec![0.0; hidden * cap];
19272 }
19273 let take = b.min(cap - acc.buf_t);
19274 for t in 0..take {
19275 let col = acc.buf_t + t;
19276 for (j, &n) in acc.support.iter().enumerate() {
19277 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19278 }
19279 for h in 0..hidden {
19280 acc.buf_o[h * cap + col] = out[t * hidden + h];
19281 }
19282 }
19283 acc.buf_t += take;
19284 acc.tokens += take as u64;
19285 if acc.buf_t < cap {
19286 return;
19287 }
19288 let bt = acc.buf_t;
19289 acc.buf_t = 0;
19290 let RefitAcc {
19300 gss,
19301 ya,
19302 buf_g,
19303 buf_o,
19304 ..
19305 } = acc;
19306 let need = (ns * ns).max(hidden * ns);
19307 if shared.len() < need {
19308 shared.resize(need, 0.0);
19309 }
19310 let scratch = &mut shared[..];
19311 let _ = bt;
19312 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19313 add_into(gss, &scratch[..ns * ns], pool);
19314 if crate::gpu::gemm_nt_f32_transient(
19315 buf_o,
19316 buf_g,
19317 &mut scratch[..hidden * ns],
19318 hidden,
19319 cap,
19320 ns,
19321 ) {
19322 add_into(ya, &scratch[..hidden * ns], pool);
19323 } else {
19324 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19325 }
19326 } else {
19327 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19328 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19329 }
19330 }
19334
19335fn refit_batch() -> usize {
19337 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19338 *B.get_or_init(|| {
19339 std::env::var("CMF_FFN_REFIT_BATCH")
19340 .ok()
19341 .and_then(|v| v.parse().ok())
19342 .unwrap_or(4096)
19343 })
19344}
19345
19346fn accum_outer_t(
19349 c: &mut [f32],
19350 m: usize,
19351 n: usize,
19352 b: usize,
19353 left: &[f32],
19354 right: &[f32],
19355 pool: Option<&Pool>,
19356) {
19357 let ptr = SendMut(c.as_mut_ptr());
19358 let body = |i: usize| {
19359 let ptr = &ptr;
19360 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19361 for t in 0..b {
19362 let a = left[i * b + t];
19363 if a == 0.0 {
19364 continue;
19365 }
19366 for (j, o) in row.iter_mut().enumerate() {
19367 *o += a * right[j * b + t];
19368 }
19369 }
19370 };
19371 match pool {
19372 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19373 for i in s..e {
19374 body(i);
19375 }
19376 }),
19377 _ => {
19378 for i in 0..m {
19379 body(i);
19380 }
19381 }
19382 }
19383}
19384
19385fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19388 let n = dst.len().min(src.len());
19389 match pool {
19390 Some(p) if n >= 1 << 16 => {
19391 let ptr = SendMut(dst.as_mut_ptr());
19392 let f = |s: usize, e: usize| {
19393 let ptr = &ptr;
19394 for blk in s..e {
19395 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19396 for i in a..b {
19397 unsafe { *ptr.0.add(i) += src[i] };
19398 }
19399 }
19400 };
19401 p.run_rows(n.div_ceil(4096), &f);
19402 }
19403 _ => {
19404 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19405 *d += *v;
19406 }
19407 }
19408 }
19409}
19410
19411fn accum_outer(
19416 c: &mut [f32],
19417 m: usize,
19418 n: usize,
19419 b: usize,
19420 left: &[f32],
19421 right: &[f32],
19422 pool: Option<&Pool>,
19423) {
19424 const TILE: usize = 32;
19425 let tiles = m.div_ceil(TILE);
19426 let cp = SendMut(c.as_mut_ptr());
19427 let body = |ti: usize| {
19428 let cp = &cp;
19429 let i0 = ti * TILE;
19430 let i1 = (i0 + TILE).min(m);
19431 for t in 0..b {
19432 let r = &right[t * n..t * n + n];
19433 for i in i0..i1 {
19434 let a = left[i * b + t];
19435 if a == 0.0 {
19436 continue;
19437 }
19438 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19440 for (o, v) in row.iter_mut().zip(r) {
19441 *o += a * *v;
19442 }
19443 }
19444 }
19445 };
19446 match pool {
19447 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19448 for ti in s..e {
19449 body(ti);
19450 }
19451 }),
19452 _ => {
19453 for ti in 0..tiles {
19454 body(ti);
19455 }
19456 }
19457 }
19458}
19459
19460pub fn refit_flush() -> usize {
19462 let Some((dir, map)) = refit_dir() else {
19463 return 0;
19464 };
19465 let guard = map.lock().unwrap();
19466 let mut n = 0;
19467 for (li, acc) in guard.0.iter() {
19468 let w = |name: &str, v: &[f32]| {
19471 let path = format!("{dir}/{name}.{li}.f32");
19472 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19473 match std::fs::write(&path, &bytes) {
19474 Ok(()) => {}
19475 Err(e) => eprintln!(
19476 "refit: FAILED to write {path} ({} MB): {e}",
19477 bytes.len() / 1_000_000
19478 ),
19479 }
19480 };
19481 w("gss", &acc.gss);
19482 w("ya", &acc.ya);
19483 println!(
19484 "refit L{li}: {} support, {} tokens, hidden {}",
19485 acc.support.len(),
19486 acc.tokens,
19487 acc.hidden
19488 );
19489 n += 1;
19490 }
19491 n
19492}
19493
19494fn adump_row(li: usize, g: &[f32]) {
19499 use std::io::Write as _;
19500 static FILES: std::sync::OnceLock<
19501 Option<(
19502 String,
19503 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19504 )>,
19505 > = std::sync::OnceLock::new();
19506 let Some((prefix, map)) = FILES
19507 .get_or_init(|| {
19508 std::env::var("CMF_FFN_ADUMP")
19509 .ok()
19510 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19511 })
19512 .as_ref()
19513 else {
19514 return;
19515 };
19516 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19519 let (from, to) = *SPAN.get_or_init(|| {
19520 let g = |k: &str, d: usize| {
19521 std::env::var(k)
19522 .ok()
19523 .and_then(|v| v.parse().ok())
19524 .unwrap_or(d)
19525 };
19526 (
19527 g("CMF_FFN_ADUMP_FROM", 0),
19528 g("CMF_FFN_ADUMP_TO", usize::MAX),
19529 )
19530 });
19531 if li < from || li > to {
19532 return;
19533 }
19534 let mut map = map.lock().unwrap();
19535 let f = map.entry(li).or_insert_with(|| {
19536 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19537 });
19538 let mut bytes = Vec::with_capacity(g.len() * 2);
19539 for v in g {
19540 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19541 }
19542 let _ = f.write_all(&bytes);
19543}
19544
19545fn oracle_topk() -> usize {
19551 if exact_mtp_calibration_active() {
19552 return 0;
19553 }
19554 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19555 *K.get_or_init(|| {
19556 std::env::var("CMF_FFN_ORACLE_TOPK")
19557 .ok()
19558 .and_then(|v| v.parse().ok())
19559 .unwrap_or(0)
19560 })
19561}
19562
19563fn gate_topk() -> usize {
19569 if exact_mtp_calibration_active() {
19570 return 0;
19571 }
19572 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19573 *K.get_or_init(|| {
19574 std::env::var("CMF_FFN_GATE_TOPK")
19575 .ok()
19576 .and_then(|v| v.parse().ok())
19577 .unwrap_or(0)
19578 })
19579}
19580
19581fn gate_block() -> usize {
19588 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19589 *B.get_or_init(|| {
19590 std::env::var("CMF_FFN_GATE_BLOCK")
19591 .ok()
19592 .and_then(|v| v.parse().ok())
19593 .unwrap_or(1)
19594 })
19595}
19596
19597fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19599 let n = g.len();
19600 let nb = n.div_ceil(block);
19601 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19602 if kb >= nb {
19603 return;
19604 }
19605 let mut score: Vec<f32> = (0..nb)
19606 .map(|b| {
19607 g[b * block..((b + 1) * block).min(n)]
19608 .iter()
19609 .map(|v| v * v)
19610 .sum::<f32>()
19611 })
19612 .collect();
19613 let mut ord = score.clone();
19614 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19615 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19616 });
19617 let thr = *kth;
19618 for b in 0..nb {
19619 if score[b] < thr {
19620 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19621 }
19622 }
19623 score.clear();
19624}
19625
19626fn keep_top_k(g: &mut [f32], k: usize) {
19628 if gate_block() > 1 {
19629 return keep_top_blocks(g, k, gate_block());
19630 }
19631 let n = g.len();
19632 if k == 0 || k >= n {
19633 return;
19634 }
19635 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19636 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19637 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19638 });
19639 let thr = *kth;
19640 for v in g.iter_mut() {
19641 if v.abs() < thr {
19642 *v = 0.0;
19643 }
19644 }
19645}
19646
19647fn probe_sq() -> bool {
19651 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19652 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19653}
19654
19655fn probe_signed() -> bool {
19659 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19660 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19661}
19662
19663fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19671 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19672 M.get_or_init(|| {
19673 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19674 let b = std::fs::read(&p).ok()?;
19675 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19676 let vals: Vec<f32> = b[8..]
19677 .chunks_exact(4)
19678 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19679 .collect();
19680 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19681 Some((inter, vals))
19682 })
19683 .as_ref()
19684}
19685
19686fn probe_topk() -> usize {
19689 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19690 *K.get_or_init(|| {
19691 std::env::var("CMF_FFN_PROBE_TOPK")
19692 .ok()
19693 .and_then(|v| v.parse().ok())
19694 .unwrap_or(0)
19695 })
19696}
19697
19698thread_local! {
19699 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19702 const { std::cell::RefCell::new(None) };
19703}
19704
19705fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19718 if d.gate_proj.has_prism_contract()
19723 || d.up_proj.has_prism_contract()
19724 || d.down_proj.has_prism_contract()
19725 {
19726 return None;
19727 }
19728 let dt = d.down_t.as_ref()?;
19729 let inter = d.gate_proj.rows();
19730 let hidden = dt.cols();
19731 if k == 0 || k >= inter || d.act != Act::Silu {
19732 return None;
19733 }
19734 DYN_SCRATCH.with(|sc| {
19735 let mut sc = sc.borrow_mut();
19736 let DynScratch {
19737 g,
19738 mag,
19739 live,
19740 parts,
19741 } = &mut *sc;
19742 g.resize(inter, 0.0);
19743 d.gate_proj.matvec(x, g, pool);
19744 for v in g.iter_mut() {
19745 *v = inference::silu(*v);
19746 }
19747 mag.clear();
19750 mag.extend(g.iter().map(|v| v.abs()));
19751 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19752 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19753 });
19754 let thr = *kth;
19755 live.clear();
19756 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19757 let mut out = vec![0.0f32; hidden];
19758 match pool {
19759 Some(p) if live.len() >= 64 => {
19760 let nw = p.n_workers() + 1;
19761 parts.clear();
19762 parts.resize(nw * hidden, 0.0);
19763 let ptr = SendMut(parts.as_mut_ptr());
19764 let n = live.len();
19765 let live_ref: &[u32] = live;
19766 let g_ref: &[f32] = g;
19767 p.run(&|w, workers| {
19768 let chunk = n.div_ceil(workers);
19769 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19770 if s >= e {
19771 return;
19772 }
19773 WORKER_SCRATCH.with(|ws| {
19774 let mut ws = ws.borrow_mut();
19775 let [scratch, acc] = &mut *ws;
19776 scratch.resize(hidden.max(x.len()), 0.0);
19777 acc.clear();
19778 acc.resize(hidden, 0.0);
19779 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19780 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19783 d.up_proj.prefetch_row(nx as usize);
19784 dt.prefetch_row(nx as usize);
19785 }
19786 let idx = nrm as usize;
19787 let up = d.up_proj.row_dot(idx, x, scratch);
19788 let a = g_ref[idx] * up;
19789 if a != 0.0 {
19790 dt.add_row_scaled(idx, a, acc, scratch);
19791 }
19792 }
19793 for (j, v) in acc.iter().enumerate() {
19794 unsafe { *ptr.at(w * hidden + j) = *v };
19795 }
19796 });
19797 });
19798 for w in 0..nw {
19799 for (j, o) in out.iter_mut().enumerate() {
19800 *o += parts[w * hidden + j];
19801 }
19802 }
19803 }
19804 _ => {
19805 WORKER_SCRATCH.with(|ws| {
19806 let mut ws = ws.borrow_mut();
19807 let [scratch, _acc] = &mut *ws;
19808 scratch.resize(hidden.max(x.len()), 0.0);
19809 for &nrm in live.iter() {
19810 let idx = nrm as usize;
19811 let up = d.up_proj.row_dot(idx, x, scratch);
19812 let a = g[idx] * up;
19813 if a != 0.0 {
19814 dt.add_row_scaled(idx, a, &mut out, scratch);
19815 }
19816 }
19817 });
19818 }
19819 }
19820 Some(out)
19821 })
19822}
19823
19824struct DynScratch {
19827 g: Vec<f32>,
19828 mag: Vec<f32>,
19829 live: Vec<u32>,
19830 parts: Vec<f32>,
19831}
19832
19833thread_local! {
19834 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19835 std::cell::RefCell::new(DynScratch {
19836 g: Vec::new(),
19837 mag: Vec::new(),
19838 live: Vec::new(),
19839 parts: Vec::new(),
19840 })
19841 };
19842 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19844 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19845}
19846
19847fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19852 let inter = d.gate_proj.rows();
19853 FFN_SCRATCH.with(|s| {
19854 let mut s = s.borrow_mut();
19855 let [g, u, ..] = &mut *s;
19856 g.resize(inter, 0.0);
19857 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19858 } else {
19860 u.resize(inter, 0.0);
19861 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19862 for i in 0..inter {
19863 g[i] = d.act.combine(g[i], u[i]);
19864 }
19865 }
19866 zero_masked_cols(g, 1, inter, mask_row);
19867 let mut out = attention::take_buf(d.down_proj.rows());
19868 d.down_proj.matvec(g, &mut out, pool);
19869 out
19870 })
19871}
19872
19873fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19879 if d.gate_proj.has_prism_contract()
19880 || d.up_proj.has_prism_contract()
19881 || d.down_proj.has_prism_contract()
19882 {
19883 return None;
19884 }
19885 d.act.moe_job_graph_contract()?;
19888 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19891 return None;
19892 }
19893 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19894 let mut model_ref = None;
19895 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19896 let model = model_ref?;
19897 let hidden = jobs[0].down.1;
19898 let mut out = attention::take_buf(hidden);
19899 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19900 Some(out)
19901 } else {
19902 let mut out = out;
19903 attention::recycle_buf(&mut out);
19904 None
19905 }
19906}
19907
19908#[allow(clippy::type_complexity)]
19913#[allow(clippy::type_complexity)]
19914pub(crate) fn moe_parts(
19915 t: &QTensor,
19916) -> Option<(
19917 &std::sync::Arc<cortiq_core::CmfModel>,
19918 usize,
19919 usize,
19920 usize,
19921 &[f32],
19922 &[f32],
19923 bool,
19924 bool,
19925 bool,
19926)> {
19927 match t {
19928 QTensor::Mapped {
19929 model,
19930 idx,
19931 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19932 rows,
19933 cols,
19934 row_scale,
19935 col_field,
19936 ..
19937 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19938 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19939 )),
19940 QTensor::Mapped {
19942 model,
19943 idx,
19944 dtype: cortiq_core::TensorDtype::Q1,
19945 rows,
19946 cols,
19947 ..
19948 } => Some((
19949 model,
19950 *idx,
19951 *rows,
19952 *cols,
19953 &[][..],
19954 &[][..],
19955 true,
19956 false,
19957 false,
19958 )),
19959 QTensor::Mapped {
19961 model,
19962 idx,
19963 dtype: cortiq_core::TensorDtype::Q4Tiled,
19964 rows,
19965 cols,
19966 ..
19967 } => Some((
19968 model,
19969 *idx,
19970 *rows,
19971 *cols,
19972 &[][..],
19973 &[][..],
19974 false,
19975 true,
19976 false,
19977 )),
19978 QTensor::Mapped {
19980 model,
19981 idx,
19982 dtype: cortiq_core::TensorDtype::Q4TiledP,
19983 rows,
19984 cols,
19985 ..
19986 } => Some((
19987 model,
19988 *idx,
19989 *rows,
19990 *cols,
19991 &[][..],
19992 &[][..],
19993 false,
19994 true,
19995 false,
19996 )),
19997 QTensor::Mapped {
20001 model,
20002 idx,
20003 dtype: cortiq_core::TensorDtype::Q2TiledP,
20004 rows,
20005 cols,
20006 ..
20007 } => Some((
20008 model,
20009 *idx,
20010 *rows,
20011 *cols,
20012 &[][..],
20013 &[][..],
20014 false,
20015 true,
20016 true,
20017 )),
20018 _ => None,
20019 }
20020}
20021
20022#[cfg(target_os = "macos")]
20029#[allow(clippy::type_complexity)]
20030fn chunk_moe_parts(
20031 m: &MoeFfn,
20032 hidden: usize,
20033) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20034 if m.shared.is_some()
20035 || m.per_expert_scale.is_some()
20036 || m.resonance.is_some()
20037 || m.route_tau.is_some()
20038 || m.router_input_norm
20039 || m.experts.is_empty()
20040 || m.top_k == 0
20041 || std::env::var("CMF_RMS_TRACE").is_ok()
20042 || std::env::var("CMF_ACT_DUMP").is_ok()
20043 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20044 {
20045 return None;
20046 }
20047 let (rf, rr, rc) = m.router.f32_parts()?;
20048 if rr != m.experts.len() || rc != hidden {
20049 return None;
20050 }
20051 let inter = m.experts[0].gate_proj.rows();
20052 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20055 let experts = m
20056 .experts
20057 .iter()
20058 .map(|e| {
20059 if e.act != Act::Silu
20060 || !e.segs.is_empty()
20061 || e.gate_proj.rows() != inter
20062 || e.up_proj.rows() != inter
20063 || e.down_proj.cols() != inter
20064 || e.gate_proj.cols() != hidden
20065 || e.down_proj.rows() != hidden
20066 {
20067 return None;
20068 }
20069 if q8 {
20070 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20071 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20072 }
20073 Some((
20074 e.gate_proj.mapped_q4tp()?.1,
20075 e.up_proj.mapped_q4tp()?.1,
20076 e.down_proj.mapped_q4tp()?.1,
20077 ))
20078 })
20079 .collect::<Option<Vec<_>>>()?;
20080 Some((rf, experts, q8))
20081}
20082
20083#[cfg(target_os = "macos")]
20096fn metal_flint_moe_graph_parts(
20097 m: &MoeFfn,
20098 hidden: usize,
20099 scratch_key: usize,
20100) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20101 use std::sync::Arc;
20102
20103 const HIDDEN: usize = 2_048;
20104 const ROUTED_INTER: usize = 512;
20105 const SHARED_INTER: usize = 1_024;
20106 const EXPERTS: usize = 256;
20107 const TOP_K: usize = 6;
20108
20109 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20110 return None;
20111 }
20112 let routed0 = m.experts.first()?;
20113 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20114 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20115 if router_rows != EXPERTS || router_cols != HIDDEN {
20116 return None;
20117 }
20118 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20119 return None;
20120 }
20121 let same_q2 = |t: &QTensor| -> Option<usize> {
20122 let (owner, idx) = t.mapped_q2tp()?;
20123 Arc::ptr_eq(&owner, &model).then_some(idx)
20124 };
20125 let same_q4 = |t: &QTensor| -> Option<usize> {
20126 let (owner, idx) = t.mapped_q4tp()?;
20127 Arc::ptr_eq(&owner, &model).then_some(idx)
20128 };
20129 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20130 if e.act != Act::Silu
20131 || !e.segs.is_empty()
20132 || e.gate_proj.rows() != ROUTED_INTER
20133 || e.gate_proj.cols() != HIDDEN
20134 || e.up_proj.rows() != ROUTED_INTER
20135 || e.up_proj.cols() != HIDDEN
20136 || e.down_proj.rows() != HIDDEN
20137 || e.down_proj.cols() != ROUTED_INTER
20138 {
20139 return None;
20140 }
20141 Some((
20142 same_q2(&e.gate_proj)?,
20143 same_q2(&e.up_proj)?,
20144 same_q4(&e.down_proj)?,
20145 ))
20146 };
20147 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20148 if e.act != Act::Silu
20149 || !e.segs.is_empty()
20150 || e.gate_proj.rows() != SHARED_INTER
20151 || e.gate_proj.cols() != HIDDEN
20152 || e.up_proj.rows() != SHARED_INTER
20153 || e.up_proj.cols() != HIDDEN
20154 || e.down_proj.rows() != HIDDEN
20155 || e.down_proj.cols() != SHARED_INTER
20156 {
20157 return None;
20158 }
20159 Some((
20160 same_q4(&e.gate_proj)?,
20161 same_q4(&e.up_proj)?,
20162 same_q4(&e.down_proj)?,
20163 ))
20164 };
20165 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20166 if experts.len() != EXPERTS {
20167 return None;
20168 }
20169 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20170 if shared_gate.is_some() {
20171 return None;
20172 }
20173 Some(crate::gpu::GpuFlintMoe {
20174 router,
20175 bias: m.expert_bias.as_deref(),
20176 experts,
20177 scratch_key,
20178 shared: shared(shared_ffn)?,
20179 top_k: m.top_k,
20180 norm_topk: m.norm_topk_prob,
20181 route_scale: m.routed_scaling,
20182 sigmoid: m.router_sigmoid,
20183 sigmoid_denom_eps: m.sigmoid_denom_eps,
20184 })
20185}
20186
20187#[cfg(target_os = "macos")]
20188fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20189 if m.router_input_norm
20190 || m.route_tau.is_some()
20191 || m.mask.is_some()
20192 || m.per_expert_scale.is_some()
20193 || m.experts.is_empty()
20194 || m.top_k == 0
20195 || m.resonance.is_some()
20196 {
20197 return None;
20198 }
20199 let (sh, sg) = match &m.shared {
20204 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20205 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20206 None => return None,
20207 };
20208 let (rf, rr, rc) = m.router.f32_parts()?;
20209 if rr != m.experts.len() || rc != hidden {
20210 return None;
20211 }
20212 let shared_gated = sg.is_some();
20213 let sf = match sg {
20214 Some(sg) => {
20215 let (sf, sr, sc) = sg.f32_parts()?;
20216 if sr * sc != hidden {
20217 return None;
20218 }
20219 sf
20220 }
20221 None => &rf[..hidden],
20224 };
20225 if let Some(b) = &m.expert_bias {
20226 if b.len() != m.experts.len() {
20227 return None;
20228 }
20229 }
20230 let inter = m.experts[0].gate_proj.rows();
20231 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20234 let q8 = crate::gpu_metal::moe_q8::enabled()
20238 && m.shared.is_none()
20239 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20240 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20241 if e.act != Act::Silu
20242 || e.gate_proj.rows() != inter
20243 || e.gate_proj.cols() != hidden
20244 || e.up_proj.rows() != inter
20245 || e.up_proj.cols() != hidden
20246 || e.down_proj.rows() != hidden
20247 || e.down_proj.cols() != inter
20248 {
20249 return None;
20250 }
20251 if q8 {
20252 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20253 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20254 }
20255 let pick = |t: &QTensor| -> Option<usize> {
20256 if gu_q2 {
20257 t.mapped_q2tp().map(|(_, i)| i)
20258 } else {
20259 t.mapped_q4tp().map(|(_, i)| i)
20260 }
20261 };
20262 Some((
20263 pick(&e.gate_proj)?,
20264 pick(&e.up_proj)?,
20265 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20266 ))
20267 };
20268 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20269 let shared = match sh {
20270 Some(sh) => Some(trio(sh)?),
20271 None => None,
20272 };
20273 Some(crate::gpu::GpuMoe {
20274 router: rf,
20275 sgate: sf,
20276 experts,
20277 shared,
20278 n_exp: m.experts.len(),
20279 top_k: m.top_k,
20280 inter,
20281 norm_topk: m.norm_topk_prob,
20282 route_scale: m.routed_scaling,
20283 gu_q2,
20284 q8,
20285 sigmoid: m.router_sigmoid,
20286 sigmoid_denom_eps: m.sigmoid_denom_eps,
20287 bias: m.expert_bias.as_deref(),
20288 shared_gated,
20289 })
20290}
20291
20292fn forced_ids_at(step: usize) -> Option<u32> {
20298 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20299 IDS.get_or_init(|| {
20300 std::env::var("CMF_FORCE_IDS")
20301 .ok()
20302 .and_then(|p| std::fs::read_to_string(p).ok())
20303 .map(|s| {
20304 s.split_whitespace()
20305 .filter_map(|t| t.parse().ok())
20306 .collect()
20307 })
20308 .unwrap_or_default()
20309 })
20310 .get(step)
20311 .copied()
20312}
20313
20314pub(crate) fn moe_push_job_parts<'a>(
20318 gate: &'a QTensor,
20319 up: &'a QTensor,
20320 down: &'a QTensor,
20321 x: &[f32],
20322 w: f32,
20323 swiglu_limit: f32,
20324 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20325 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20326) -> Option<()> {
20327 use crate::qtensor::prescale;
20328 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20329 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20330 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20331 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20332 return None; }
20334 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20337 return None;
20338 }
20339 if !gq2 && dq2 {
20340 return None;
20341 }
20342 model_ref.get_or_insert_with(|| gm.clone());
20343 let dt = |cf: &[f32]| {
20344 if cf.is_empty() {
20345 cortiq_core::TensorDtype::Q8Row
20346 } else {
20347 cortiq_core::TensorDtype::Q8_2f
20348 }
20349 };
20350 jobs.push(crate::gpu::MoeJob {
20351 gate: (gi, gr, gc, grs),
20352 up: (ui, ur, uc, urs),
20353 down: (di, dr, dc, drs),
20354 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20355 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20356 down_col: dcf,
20357 w,
20358 q1: gq1,
20359 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20360 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20361 gu_q2: gq2,
20362 swiglu_limit,
20363 act: 0,
20364 });
20365 Some(())
20366}
20367
20368fn moe_push_job<'a>(
20370 d: &'a DenseFfn,
20371 x: &[f32],
20372 w: f32,
20373 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20374 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20375) -> Option<()> {
20376 use crate::qtensor::prescale;
20377 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20382 let act = graph_act.code();
20383 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20384 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20385 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20386 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20387 return None; }
20389 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20390 return None;
20391 }
20392 if !gq2 && dq2 {
20393 return None;
20394 }
20395 model_ref.get_or_insert_with(|| gm.clone());
20396 let gdt = if gcf.is_empty() {
20397 cortiq_core::TensorDtype::Q8Row
20398 } else {
20399 cortiq_core::TensorDtype::Q8_2f
20400 };
20401 let udt = if ucf.is_empty() {
20402 cortiq_core::TensorDtype::Q8Row
20403 } else {
20404 cortiq_core::TensorDtype::Q8_2f
20405 };
20406 jobs.push(crate::gpu::MoeJob {
20407 gate: (gi, gr, gc, grs),
20408 up: (ui, ur, uc, urs),
20409 down: (di, dr, dc, drs),
20410 xs_gate: prescale(x, gcf, gdt).into_owned(),
20411 xs_up: prescale(x, ucf, udt).into_owned(),
20412 down_col: dcf,
20413 w,
20414 q1: gq1,
20415 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20416 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20417 gu_q2: gq2,
20418 swiglu_limit,
20419 act,
20420 });
20421 Some(())
20422}
20423
20424fn sparse_ffn_quant(
20431 d: &DenseFfn,
20432 x: &[f32],
20433 active: &[u16],
20434 hidden: usize,
20435 pool: Option<&Pool>,
20436) -> Vec<f32> {
20437 let n = active.len();
20438 let inter = d.gate_proj.rows();
20439 let mut act = vec![0.0f32; n];
20440 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20443 let compute = |ai: usize| -> f32 {
20444 let idx = active[ai] as usize;
20445 if idx >= inter {
20446 return 0.0; }
20448 let mut s = if need_scratch {
20449 vec![0.0f32; hidden]
20450 } else {
20451 Vec::new()
20452 };
20453 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20454 let up = d.up_proj.row_dot(idx, x, &mut s);
20455 d.act.combine(gate, up)
20456 };
20457 match pool {
20458 Some(p) if n >= 256 => {
20459 let ptr = SendMut(act.as_mut_ptr());
20460 p.run(&|widx, nw| {
20461 let chunk = n.div_ceil(nw);
20462 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20463 for ai in s..e {
20464 unsafe { *ptr.at(ai) = compute(ai) };
20465 }
20466 });
20467 }
20468 _ => {
20469 for (ai, a) in act.iter_mut().enumerate() {
20470 *a = compute(ai);
20471 }
20472 }
20473 }
20474 let mut out = vec![0.0f32; hidden];
20476 for (ai, &idx) in active.iter().enumerate() {
20477 let w = act[ai];
20478 if w.abs() >= 1e-12 && (idx as usize) < inter {
20479 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20480 }
20481 }
20482 out
20483}
20484
20485#[doc(hidden)]
20487pub fn sparse_ffn_quant_for_test(
20488 d: &DenseFfn,
20489 x: &[f32],
20490 active: &[u16],
20491 hidden: usize,
20492) -> Vec<f32> {
20493 sparse_ffn_quant(d, x, active, hidden, None)
20494}
20495
20496fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20500 let deq = |t: &QTensor| -> Vec<f32> {
20501 let (rows, cols) = (t.rows(), t.cols());
20502 let mut out = vec![0.0f32; rows * cols];
20503 for r in 0..rows {
20504 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20505 }
20506 out
20507 };
20508 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20509}
20510
20511struct SendMut(*mut f32);
20513unsafe impl Send for SendMut {}
20514unsafe impl Sync for SendMut {}
20515impl SendMut {
20516 #[inline]
20517 #[allow(clippy::mut_from_ref)]
20520 unsafe fn at(&self, i: usize) -> &mut f32 {
20521 unsafe { &mut *self.0.add(i) }
20522 }
20523}
20524
20525pub(crate) fn moe_route(
20537 logits: &[f32],
20538 m: &MoeFfn,
20539 allowed: Option<&[bool]>,
20540) -> (Vec<usize>, Vec<f32>, f32) {
20541 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20542}
20543
20544pub(crate) fn moe_route_with_eps(
20554 logits: &[f32],
20555 m: &MoeFfn,
20556 allowed: Option<&[bool]>,
20557 sigmoid_denom_eps: f32,
20558) -> (Vec<usize>, Vec<f32>, f32) {
20559 let ne = logits.len();
20560 let admit = |e: usize| {
20566 m.mask.as_ref().is_none_or(|mk| mk[e])
20567 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20568 };
20569 if m.resonance.is_some() && m.top_k == 1 {
20582 let mut best: Option<usize> = None;
20583 for e in (0..ne).filter(|&e| admit(e)) {
20584 let l = logits[e];
20585 if l == f32::NEG_INFINITY || l.is_nan() {
20586 continue;
20587 }
20588 if best.is_none_or(|b| l > logits[b]) {
20589 best = Some(e);
20590 }
20591 }
20592 if let Some(b) = best {
20593 let mut p = vec![0.0f32; ne];
20594 p[b] = 1.0;
20595 return (vec![b], p, 1.0 / m.routed_scaling);
20596 }
20597 }
20598 let p: Vec<f32> = if m.router_sigmoid {
20604 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20605 } else {
20606 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20607 if mx == f32::NEG_INFINITY {
20608 vec![1.0 / ne.max(1) as f32; ne]
20609 } else {
20610 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20611 let s: f32 = e.iter().sum();
20612 for v in &mut e {
20613 *v /= s;
20614 }
20615 e
20616 }
20617 };
20618 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20619 match &m.expert_bias {
20621 Some(b) => idx.sort_unstable_by(|&x, &y| {
20622 (p[y] + b[y])
20623 .partial_cmp(&(p[x] + b[x]))
20624 .unwrap()
20625 .then(x.cmp(&y))
20626 }),
20627 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20628 }
20629 idx.truncate(m.top_k);
20630 if let Some(tau) = m.route_tau {
20634 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20635 if total > 0.0 {
20636 let mut acc = 0.0f32;
20637 let mut keep = idx.len();
20638 for (i, &e) in idx.iter().enumerate() {
20639 acc += p[e];
20640 if acc >= tau * total {
20641 keep = i + 1;
20642 break;
20643 }
20644 }
20645 idx.truncate(keep);
20646 }
20647 }
20648 let wsum: f32 = if m.norm_topk_prob {
20649 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20650 (if m.router_sigmoid {
20654 s + sigmoid_denom_eps
20655 } else {
20656 s
20657 }) / m.routed_scaling
20658 } else {
20659 1.0 / m.routed_scaling
20660 };
20661 (idx, p, wsum)
20662}
20663
20664fn moe_trace(idx: &[usize]) {
20666 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20667}
20668
20669pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20672 use std::io::Write;
20673 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20674 std::sync::OnceLock::new();
20675 let Some(f) = F.get_or_init(|| {
20676 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20677 Some(std::sync::Mutex::new(
20678 std::fs::OpenOptions::new()
20679 .create(true)
20680 .append(true)
20681 .open(p)
20682 .ok()?,
20683 ))
20684 }) else {
20685 return;
20686 };
20687 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20688 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20689}
20690
20691pub(crate) fn moe_ffn(
20694 m: &MoeFfn,
20695 x: &[f32],
20696 pool: Option<&Pool>,
20697 allowed: Option<&[bool]>,
20698) -> Vec<f32> {
20699 let r = moe_ffn_route(m, x, pool, allowed);
20700 moe_ffn_experts(m, x, &r, pool)
20701}
20702
20703pub(crate) struct MoeRoute {
20707 pub idx: Vec<usize>,
20708 pub p: Vec<f32>,
20709 pub wsum: f32,
20710 pub logits: Vec<f32>,
20711}
20712
20713pub(crate) fn moe_ffn_route(
20719 m: &MoeFfn,
20720 x: &[f32],
20721 pool: Option<&Pool>,
20722 allowed: Option<&[bool]>,
20723) -> MoeRoute {
20724 accumulate_act(m, x, 1);
20725 let ne = m.experts.len();
20726 let mut logits = vec![0.0f32; ne];
20727 match &m.resonance {
20728 Some(r) => r.scores(x, &mut logits),
20729 None => m.router.matvec(x, &mut logits, pool),
20730 }
20731 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20732 {
20733 let mut st = m.stats.borrow_mut();
20734 if st.len() < ne {
20735 st.resize(ne, 0);
20736 }
20737 for &e in &idx {
20738 st[e] += 1;
20739 }
20740 }
20741 moe_trace(&idx);
20747 MoeRoute {
20748 idx,
20749 p,
20750 wsum,
20751 logits,
20752 }
20753}
20754
20755#[cfg(target_os = "macos")]
20764fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20765 const HIDDEN: usize = 2_048;
20766 const ROUTED_INTER: usize = 512;
20767 const SHARED_INTER: usize = 1_024;
20768 const EXPERTS: usize = 256;
20769 const TOP_K: usize = 6;
20770
20771 let Some(routed) = m.experts.first() else {
20772 return false;
20773 };
20774 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20775 return false;
20776 };
20777 let Some((shared, shared_gate)) = &m.shared else {
20778 return false;
20779 };
20780 if shared_gate.is_some()
20781 || model.header.arch.arch_name != "flint"
20782 || model.header.arch.prism_hadamard.is_some()
20783 || model.header.arch.hidden_size != HIDDEN
20784 || m.experts.len() != EXPERTS
20785 || m.top_k != TOP_K
20786 || !m.router_sigmoid
20787 || !m.norm_topk_prob
20788 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20789 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20790 || m.route_tau.is_some()
20791 || m.mask.is_some()
20792 || m.per_expert_scale.is_some()
20793 || m.router_input_norm
20794 || m.resonance.is_some()
20795 || !m.grown.is_empty()
20796 {
20797 return false;
20798 }
20799
20800 let routed_q2 = routed.act == Act::Silu
20801 && routed.segs.is_empty()
20802 && routed.gate_proj.rows() == ROUTED_INTER
20803 && routed.gate_proj.cols() == HIDDEN
20804 && routed.up_proj.rows() == ROUTED_INTER
20805 && routed.up_proj.cols() == HIDDEN
20806 && routed.down_proj.rows() == HIDDEN
20807 && routed.down_proj.cols() == ROUTED_INTER
20808 && routed.up_proj.mapped_q2tp().is_some()
20809 && routed.down_proj.mapped_q4tp().is_some();
20810 let shared_q4 = shared.act == Act::Silu
20811 && shared.segs.is_empty()
20812 && shared.gate_proj.rows() == SHARED_INTER
20813 && shared.gate_proj.cols() == HIDDEN
20814 && shared.up_proj.rows() == SHARED_INTER
20815 && shared.up_proj.cols() == HIDDEN
20816 && shared.down_proj.rows() == HIDDEN
20817 && shared.down_proj.cols() == SHARED_INTER
20818 && shared.gate_proj.mapped_q4tp().is_some()
20819 && shared.up_proj.mapped_q4tp().is_some()
20820 && shared.down_proj.mapped_q4tp().is_some();
20821 routed_q2 && shared_q4
20822}
20823
20824pub(crate) fn moe_ffn_experts(
20827 m: &MoeFfn,
20828 x: &[f32],
20829 r: &MoeRoute,
20830 pool: Option<&Pool>,
20831) -> Vec<f32> {
20832 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20833 if crate::gpu::enabled_here() {
20838 #[cfg(target_os = "macos")]
20839 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20840 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20846 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20847 });
20848 }
20849 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20850 crate::gpu::ProbeArm::Gpu => {
20851 let t0 = std::time::Instant::now();
20852 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20853 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20854 return out;
20855 }
20856 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20857 }
20858 crate::gpu::ProbeArm::CpuTimed => {
20859 let t0 = std::time::Instant::now();
20860 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20861 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20862 return out;
20863 }
20864 crate::gpu::ProbeArm::Cpu => {
20865 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20866 }
20867 }
20868 }
20869 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20870}
20871
20872fn moe_ffn_minicpm_tail(
20876 slot: &mut crate::minicpm_tail::MiniCpmTail,
20877 li: usize,
20878 total_layers: usize,
20879 m: &MoeFfn,
20880 x: &[f32],
20881 pool: Option<&Pool>,
20882) -> Vec<f32> {
20883 let route = moe_ffn_route(m, x, pool, None);
20884 match slot.forward(li, m, x, &route, pool, total_layers) {
20885 Some(out) => out,
20886 None => moe_ffn_experts(m, x, &route, pool),
20891 }
20892}
20893
20894fn moe_ffn_banked(
20898 slot: &mut crate::mimo_moe::Slot,
20899 li: usize,
20900 m: &MoeFfn,
20901 x: &[f32],
20902 pool: Option<&Pool>,
20903) -> Vec<f32> {
20904 let t0 = std::time::Instant::now();
20905 let r = moe_ffn_route(m, x, pool, None);
20906 slot.note_route(t0.elapsed().as_nanos() as u64);
20907 match slot.forward(li, m, x, &r, pool) {
20908 Some(out) => out,
20909 None => crate::qtensor::float_activations_scope(|| {
20910 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20911 }),
20912 }
20913}
20914
20915fn moe_ffn_banked_rows(
20917 slot: &mut crate::mimo_moe::Slot,
20918 li: usize,
20919 m: &MoeFfn,
20920 xs: &[f32],
20921 b: usize,
20922 hidden: usize,
20923 pool: Option<&Pool>,
20924) -> Vec<f32> {
20925 let t0 = std::time::Instant::now();
20926 let routes: Vec<_> = xs
20927 .chunks_exact(hidden)
20928 .map(|x| moe_ffn_route(m, x, pool, None))
20929 .collect();
20930 slot.note_route(t0.elapsed().as_nanos() as u64);
20931 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20932 return out;
20933 }
20934 let mut out = Vec::with_capacity(b * hidden);
20935 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20936 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20937 crate::qtensor::float_activations_scope(|| {
20939 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20940 })
20941 });
20942 out.extend(row);
20943 }
20944 out
20945}
20946
20947fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20952 use std::sync::atomic::{AtomicBool, Ordering};
20953 if built {
20954 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20955 if total_layers > 0 && layers_run < total_layers {
20956 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20957 } else {
20958 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20959 }
20960 } else {
20961 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20962 }
20963 static SAID: AtomicBool = AtomicBool::new(false);
20964 if !SAID.swap(true, Ordering::Relaxed) {
20965 if built {
20966 tracing::info!("wgpu whole-token graph: ACTIVE");
20967 } else {
20968 tracing::warn!("wgpu whole-token graph refused — per-op path");
20969 }
20970 }
20971}
20972
20973pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20977pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20978pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20982pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20984
20985pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20989pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20990pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20991 std::sync::atomic::AtomicU64::new(0);
20992pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20993pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20994pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
20998 std::sync::atomic::AtomicU64::new(0);
20999pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21000pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21001 std::sync::atomic::AtomicU64::new(0);
21002pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21003 std::sync::atomic::AtomicU64::new(0);
21004
21005fn moe_batch_enabled() -> bool {
21008 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21009 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21010}
21011
21012fn moe_ffn_cpu_batched(
21018 m: &MoeFfn,
21019 x: &[f32],
21020 idx: &[usize],
21021 p: &[f32],
21022 wsum: f32,
21023 pool: Option<&Pool>,
21024) -> Option<Vec<f32>> {
21025 if idx.is_empty() || !moe_batch_enabled() {
21026 return None;
21027 }
21028 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21032 return None;
21033 }
21034 let n = idx.len() + usize::from(m.shared.is_some());
21035 let mut pairs = Vec::with_capacity(n);
21036 let mut downs = Vec::with_capacity(n);
21037 let mut ws = Vec::with_capacity(n);
21038 for &e in idx {
21039 let d = &m.experts[e];
21040 if d.act != Act::Silu {
21041 return None;
21042 }
21043 pairs.push((&d.gate_proj, &d.up_proj));
21044 downs.push(&d.down_proj);
21045 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21046 }
21047 if let Some((se, gate)) = &m.shared {
21050 if se.act != Act::Silu {
21051 return None;
21052 }
21053 let g = gate.as_ref().map_or(1.0, |gate| {
21054 let mut gl = [0.0f32; 1];
21055 gate.matvec(x, &mut gl, pool);
21056 1.0 / (1.0 + (-gl[0]).exp())
21057 });
21058 pairs.push((&se.gate_proj, &se.up_proj));
21059 downs.push(&se.down_proj);
21060 ws.push(g);
21061 }
21062 let inter = pairs[0].0.rows();
21063 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21064 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21065 return None;
21066 }
21067 let mut out = attention::take_buf(x.len());
21068 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21069 attention::recycle_buf(&mut out);
21070 return None;
21071 }
21072 Some(out)
21073}
21074
21075pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21083pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21084pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21085pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21086pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21087
21088pub(crate) fn moe_cold_experts_cpu(
21089 experts: &[(&DenseFfn, f32)],
21090 x: &[f32],
21091 pool: Option<&Pool>,
21092) -> Vec<f32> {
21093 let mut out = attention::take_buf(x.len());
21094 if experts.is_empty() {
21095 return out;
21096 }
21097 let pairs: Vec<_> = experts
21098 .iter()
21099 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21100 .collect();
21101 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21102 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21103 let inter = experts[0].0.gate_proj.rows();
21104 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21105 let silu_only = experts
21113 .iter()
21114 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21115 if silu_only
21116 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21117 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21118 {
21119 return out;
21120 }
21121 out.fill(0.0);
21122 for &(expert, weight) in experts {
21123 let mut one = dense_ffn(expert, x, pool);
21124 for (o, v) in out.iter_mut().zip(&one) {
21125 *o += weight * v;
21126 }
21127 attention::recycle_buf(&mut one);
21128 }
21129 out
21130}
21131
21132pub(crate) fn moe_cold_experts_rows_cpu(
21136 jobs: &[Vec<(&DenseFfn, f32)>],
21137 xs: &[f32],
21138 hidden: usize,
21139 pool: Option<&Pool>,
21140) -> Vec<f32> {
21141 let mut out = vec![0.0; xs.len()];
21142 let mut experts: Vec<&DenseFfn> = Vec::new();
21143 let mut groups: Vec<Vec<usize>> = Vec::new();
21144 let mut terms = vec![Vec::new(); jobs.len()];
21145 for (r, row) in jobs.iter().enumerate() {
21146 for &(e, w) in row {
21147 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21148 Some(g) => g,
21149 None => {
21150 experts.push(e);
21151 groups.push(Vec::new());
21152 groups.len() - 1
21153 }
21154 };
21155 terms[r].push((g, groups[g].len(), w));
21156 groups[g].push(r);
21157 }
21158 }
21159 if experts.is_empty() {
21160 return out;
21161 }
21162 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21163 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21164 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21165 let count: usize = lens.iter().sum();
21166 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21167 let mut ds = vec![vec![0.0; hidden]; count];
21168 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21169 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21170 {
21171 let mut offset = 0;
21172 let offsets: Vec<_> = lens
21173 .iter()
21174 .map(|&n| {
21175 let start = offset;
21176 offset += n;
21177 start
21178 })
21179 .collect();
21180 for (r, terms) in terms.iter().enumerate() {
21181 for &(g, slot, w) in terms {
21182 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21183 .iter_mut()
21184 .zip(&ds[offsets[g] + slot])
21185 {
21186 *o += w * v;
21187 }
21188 }
21189 }
21190 } else {
21191 for (r, jobs) in jobs.iter().enumerate() {
21192 if !jobs.is_empty() {
21193 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21194 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21195 attention::recycle_buf(&mut row);
21196 }
21197 }
21198 }
21199 out
21200}
21201
21202fn moe_ffn_cpu(
21204 m: &MoeFfn,
21205 x: &[f32],
21206 idx: &[usize],
21207 p: &[f32],
21208 wsum: f32,
21209 pool: Option<&Pool>,
21210) -> Vec<f32> {
21211 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21212 return out;
21213 }
21214 let mut out = attention::take_buf(x.len());
21215 for &e in idx {
21216 let mut eo = dense_ffn(&m.experts[e], x, pool);
21217 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21218 for i in 0..out.len() {
21219 out[i] += w * eo[i];
21220 }
21221 attention::recycle_buf(&mut eo);
21222 }
21223 if let Some((se, gate)) = &m.shared {
21224 let mut so = dense_ffn(se, x, pool);
21225 let g = gate.as_ref().map_or(1.0, |gate| {
21226 let mut gl = [0.0f32; 1];
21227 gate.matvec(x, &mut gl, pool);
21228 1.0 / (1.0 + (-gl[0]).exp())
21229 });
21230 for i in 0..out.len() {
21231 out[i] += g * so[i];
21232 }
21233 attention::recycle_buf(&mut so);
21234 }
21235 out
21236}
21237
21238#[allow(clippy::too_many_arguments)]
21246pub(crate) fn mla_attention(
21247 w: &MlaWeights,
21248 normed: &[f32],
21249 cache: &mut crate::kv_cache::LayerKvCache,
21250 position: usize,
21251 inv_freq: &[f32],
21252 rope_scale: f32,
21253 eps: f64,
21254 pool: Option<&Pool>,
21255) -> Vec<f32> {
21256 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21257 let hd = dr + dn;
21258 let mut q = vec![0.0f32; nh * hd];
21259 match (&w.q_a, &w.q_a_norm) {
21260 (Some(qa), Some(qn)) => {
21261 let mut t = vec![0.0f32; qa.rows()];
21262 qa.matvec(normed, &mut t, pool);
21263 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21264 w.q_proj.matvec(&tn, &mut q, pool);
21265 }
21266 _ => w.q_proj.matvec(normed, &mut q, pool),
21267 }
21268 let mut ca = vec![0.0f32; lora + dr];
21269 w.kv_a.matvec(normed, &mut ca, pool);
21270 let (c_lat, k_rope) = ca.split_at_mut(lora);
21271 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21272 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21273 w.kv_b.matvec(&latn, &mut kvb, pool);
21274 if !w.nope {
21275 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21276 }
21277 for h in 0..nh {
21278 if !w.nope {
21279 attention::rope_rotate_scaled(
21280 &mut q[h * hd..h * hd + dr],
21281 position,
21282 inv_freq,
21283 rope_scale,
21284 );
21285 }
21286 }
21287 let mut k = vec![0.0f32; nh * hd];
21288 let mut v = vec![0.0f32; nh * hd];
21289 for h in 0..nh {
21290 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21291 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21292 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21293 }
21294 cache.append(&k, &v, &vec![true; nh]);
21295 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21296 attention::recycle_buf(&mut imp);
21297 let mut ov = vec![0.0f32; nh * dv];
21298 for h in 0..nh {
21299 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21300 }
21301 let mut out = vec![0.0f32; w.o_proj.rows()];
21302 w.o_proj.matvec(&ov, &mut out, pool);
21303 out
21304}
21305
21306fn dense_moe_ffn(
21313 dm: &DenseMoeFfn,
21314 x_normed: &[f32],
21315 h_raw: &[f32],
21316 eps: f64,
21317 norm_style: NormStyle,
21318 pool: Option<&Pool>,
21319) -> Vec<f32> {
21320 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21321 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21322 let m = &dm.moe;
21323 let ne = m.experts.len();
21324 let mut logits = vec![0.0f32; ne];
21325 if m.router_input_norm {
21326 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21327 let inv = 1.0 / (ss + eps as f32).sqrt();
21328 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21329 m.router.matvec(&xr, &mut logits, pool);
21330 } else {
21331 m.router.matvec(h_raw, &mut logits, pool);
21332 }
21333 let (idx, p, wsum) = moe_route(&logits, m, None);
21334 {
21335 let mut st = m.stats.borrow_mut();
21336 if st.len() < ne {
21337 st.resize(ne, 0);
21338 }
21339 for &e in &idx {
21340 st[e] += 1;
21341 }
21342 }
21343 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21344 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21345 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21346 for (di, mi) in d.iter_mut().zip(&mo) {
21347 *di += mi;
21348 }
21349 d
21350}
21351
21352fn moe_gpu_refused(why: &'static str) {
21359 use std::sync::atomic::{AtomicBool, Ordering};
21360 static SAID: AtomicBool = AtomicBool::new(false);
21361 if !SAID.swap(true, Ordering::Relaxed) {
21362 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21363 }
21364}
21365
21366fn moe_ffn_gpu(
21367 m: &MoeFfn,
21368 x: &[f32],
21369 idx: &[usize],
21370 p: &[f32],
21371 wsum: f32,
21372 pool: Option<&Pool>,
21373) -> Option<Vec<f32>> {
21374 use crate::gpu::MoeJob;
21375
21376 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21377 let mut model_ref = None;
21378 for &e in idx {
21379 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21380 moe_gpu_refused("push_job(expert)");
21381 return None;
21382 }
21383 }
21384 if let Some((se, gate)) = &m.shared {
21385 let g = gate.as_ref().map_or(1.0, |gate| {
21386 let mut gl = [0.0f32; 1];
21387 gate.matvec(x, &mut gl, pool);
21388 1.0 / (1.0 + (-gl[0]).exp())
21389 });
21390 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21391 moe_gpu_refused("push_job(shared)");
21392 return None;
21393 }
21394 }
21395 let Some(model) = model_ref else {
21396 moe_gpu_refused("no model_ref");
21397 return None;
21398 };
21399 let hidden = jobs[0].down.1;
21400 let mut out = vec![0.0f32; hidden];
21401 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21402 Some(out)
21403 } else {
21404 moe_gpu_refused("gpu::moe_block");
21405 None
21406 }
21407}
21408
21409fn ffn_forward(
21411 ffn: &FfnKind,
21412 x: &[f32],
21413 pool: Option<&Pool>,
21414 experts_allowed: Option<&[bool]>,
21415) -> Vec<f32> {
21416 match ffn {
21417 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21418 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21419 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21420 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21424 }
21425}
21426
21427fn ffn_forward_pair(
21431 ffn: &FfnKind,
21432 x1: &[f32],
21433 x2: &[f32],
21434 pool: Option<&Pool>,
21435 experts_allowed: Option<&[bool]>,
21436) -> (Vec<f32>, Vec<f32>) {
21437 let d = match ffn {
21438 FfnKind::Dense(d) if !d.segs.is_empty() => {
21441 return (
21442 tube_ffn(d, x1, 1, pool, None),
21443 tube_ffn(d, x2, 1, pool, None),
21444 );
21445 }
21446 FfnKind::Dense(d) => d,
21447 FfnKind::Moe(m) => {
21448 return (
21449 moe_ffn(m, x1, pool, experts_allowed),
21450 moe_ffn(m, x2, pool, experts_allowed),
21451 );
21452 }
21453 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21454 };
21455 let inter = d.gate_proj.rows();
21456 FFN_SCRATCH.with(|s| {
21457 let mut s = s.borrow_mut();
21458 let [g1, g2, u1, u2] = &mut *s;
21459 g1.resize(inter, 0.0);
21460 g2.resize(inter, 0.0);
21461 u1.resize(inter, 0.0);
21462 u2.resize(inter, 0.0);
21463 QTensor::matvec2_many(
21466 [&d.gate_proj, &d.up_proj],
21467 x1,
21468 x2,
21469 [g1.as_mut_slice(), u1.as_mut_slice()],
21470 [g2.as_mut_slice(), u2.as_mut_slice()],
21471 pool,
21472 );
21473 for i in 0..inter {
21474 g1[i] = d.act.combine(g1[i], u1[i]);
21475 g2[i] = d.act.combine(g2[i], u2[i]);
21476 }
21477 let mut o1 = attention::take_buf(d.down_proj.rows());
21478 let mut o2 = attention::take_buf(d.down_proj.rows());
21479 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21480 (o1, o2)
21481 })
21482}
21483
21484#[cfg(test)]
21485mod tests {
21486 use std::ffi::OsString;
21487 use std::sync::Mutex;
21488
21489 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21492
21493 struct ScopedTestEnv {
21494 previous: Vec<(&'static str, Option<OsString>)>,
21495 }
21496
21497 impl ScopedTestEnv {
21498 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21499 let previous = vars
21500 .iter()
21501 .map(|(name, _)| (*name, std::env::var_os(name)))
21502 .collect();
21503 for &(name, value) in vars {
21504 unsafe {
21505 match value {
21506 Some(value) => std::env::set_var(name, value),
21507 None => std::env::remove_var(name),
21508 }
21509 }
21510 }
21511 Self { previous }
21512 }
21513 }
21514
21515 impl Drop for ScopedTestEnv {
21516 fn drop(&mut self) {
21517 for (name, value) in self.previous.drain(..).rev() {
21518 unsafe {
21519 match value {
21520 Some(value) => std::env::set_var(name, value),
21521 None => std::env::remove_var(name),
21522 }
21523 }
21524 }
21525 }
21526 }
21527
21528 #[test]
21529 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21530 use super::pad_mtp_draft_logits;
21531
21532 let mut logits = vec![-3.0, -1.0, -2.0];
21533 pad_mtp_draft_logits(&mut logits, 6);
21534 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21535 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21536 assert_eq!(
21537 logits
21538 .iter()
21539 .enumerate()
21540 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21541 .map(|(i, _)| i),
21542 Some(1),
21543 "an unevaluated row cannot win an all-negative shortlist"
21544 );
21545 }
21546
21547 #[test]
21548 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21549 use super::{Pipeline, SpecMon, SpecTrial};
21550
21551 let mut mon = SpecMon::default();
21552 let start = std::time::Instant::now();
21553 mon.round(1.0, 1);
21554 let first = Pipeline::spec_trial_round(
21555 SpecTrial::Spec {
21556 t0: start,
21557 gen0: 0,
21558 rounds: 0,
21559 },
21560 &mut mon,
21561 1,
21562 );
21563 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21564
21565 mon.round(1.0, 1);
21566 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21567 assert!(
21568 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21569 "two verifier-tip-only rounds must stop the losing MTP trial"
21570 );
21571
21572 let mut viable = SpecMon::default();
21575 viable.round(1.0, 1);
21576 viable.round(1.0, 2);
21577 let continued = Pipeline::spec_trial_round(
21578 SpecTrial::Spec {
21579 t0: start,
21580 gen0: 0,
21581 rounds: 1,
21582 },
21583 &mut viable,
21584 3,
21585 );
21586 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21587 }
21588
21589 #[test]
21590 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21591 let mut p = spark_test_pipeline(None);
21592 let head_rows = 248_070;
21593 p.draft_full_streak = 7;
21594 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21595
21596 p.reset_fresh_mtp_draft_state();
21597 assert_eq!(
21598 p.draft_head_rows(head_rows),
21599 super::Pipeline::draft_vocab_rows(head_rows),
21600 "a fresh request returns to its configured shortlist"
21601 );
21602
21603 p.note_draft_id(u32::MAX);
21606 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21607 }
21608
21609 #[test]
21610 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21611 use super::graph_spec_wanted_by_policy;
21612
21613 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21617 assert!(!graph_spec_wanted_by_policy(
21618 Some("anything-but-1"),
21619 true,
21620 false,
21621 false,
21622 true,
21623 ));
21624 assert!(graph_spec_wanted_by_policy(
21625 Some("1"),
21626 false,
21627 true,
21628 false,
21629 true,
21630 ));
21631 assert!(!graph_spec_wanted_by_policy(
21632 Some("0"),
21633 true,
21634 false,
21635 false,
21636 true,
21637 ));
21638
21639 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21642 assert!(graph_spec_wanted_by_policy(
21643 Some("legacy-force"),
21644 false,
21645 true,
21646 true,
21647 false,
21648 ));
21649 }
21650
21651 #[test]
21652 fn encoder_hidden_states_are_normalized_and_reusable() {
21653 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21654 let ids = [1, 7, 11, 19];
21655
21656 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21657 let second = p
21658 .encode_hidden_states(&ids)
21659 .expect("reusable encoder forward");
21660 assert_eq!(first.len(), ids.len() * 8);
21661 assert!(first.iter().all(|v| v.is_finite()));
21662 assert_eq!(
21663 first, second,
21664 "encoder forward must start from a fresh state"
21665 );
21666
21667 assert!(p.forward_ids(&ids, None).is_ok());
21670 }
21671
21672 #[test]
21677 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21678 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21679 let dense_card = ChunkStackFacts {
21680 plain_dense: true,
21681 discrete: true,
21682 gpu_on: true,
21683 ..Default::default()
21684 };
21685 assert!(dense_card.dense_on_discrete());
21686 assert_eq!(
21688 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21689 DISCRETE_DENSE_PREFILL_CHUNK
21690 );
21691 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21692 for (label, facts) in [
21693 (
21694 "GDN hybrid / MoE / DeepSeek stack",
21695 ChunkStackFacts {
21696 plain_dense: false,
21697 ..dense_card
21698 },
21699 ),
21700 (
21701 "integrated GPU",
21702 ChunkStackFacts {
21703 discrete: false,
21704 ..dense_card
21705 },
21706 ),
21707 (
21708 "CPU only",
21709 ChunkStackFacts {
21710 gpu_on: false,
21711 discrete: false,
21712 ..dense_card
21713 },
21714 ),
21715 (
21716 "capacity split",
21717 ChunkStackFacts {
21718 capacity_split: true,
21719 ..dense_card
21720 },
21721 ),
21722 (
21723 "multi-GPU plan",
21724 ChunkStackFacts {
21725 multi_gpu: true,
21726 ..dense_card
21727 },
21728 ),
21729 (
21730 "O(1) layers",
21731 ChunkStackFacts {
21732 o1: true,
21733 ..dense_card
21734 },
21735 ),
21736 ] {
21737 assert!(!facts.dense_on_discrete(), "{label}");
21738 assert_eq!(
21739 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21740 48,
21741 "{label} keeps the historical x86 chunk"
21742 );
21743 }
21744 for dense in [false, true] {
21746 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21747 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21748 }
21749 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21751 for dense in [false, true] {
21752 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21753 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21754 }
21755 }
21756 }
21757
21758 #[test]
21759 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21760 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21761 let full = |host_rows, device_rows| ReuseLayer {
21762 full: true,
21763 host_rows,
21764 device_rows,
21765 device_state: false,
21766 };
21767 assert_eq!(
21770 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21771 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21772 );
21773 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21775 assert_eq!(
21777 kv_reuse_plan(339, &[full(339, Some(345))]),
21778 ReusePlan::Ready
21779 );
21780 assert_eq!(
21782 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21783 ReusePlan::Pull(vec![(0, 300, 339)])
21784 );
21785 assert_eq!(
21787 kv_reuse_plan(339, &[full(300, Some(320))]),
21788 ReusePlan::Fresh
21789 );
21790 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21791 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21792 let conv = |device_state| ReuseLayer {
21795 full: false,
21796 host_rows: 0,
21797 device_rows: None,
21798 device_state,
21799 };
21800 assert_eq!(
21801 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21802 ReusePlan::Fresh
21803 );
21804 assert_eq!(
21805 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21806 ReusePlan::Ready
21807 );
21808 }
21809
21810 #[test]
21811 fn nll_graph_policy_scopes_only_the_fused_head() {
21812 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21813 ("vulkan graph", true, true, false, true, false),
21815 ("native Metal graph", true, true, true, true, true),
21817 ("masked", false, true, false, false, false),
21819 ("graph disabled", true, false, true, false, false),
21820 ] {
21821 let (graph_quality, graph_head_required) =
21822 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21823 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21824 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21825 }
21826 }
21827
21828 #[test]
21829 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21830 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21831 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21832 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21833 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21834 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21835 }
21836
21837 #[test]
21838 fn cancel_flag_stops_generation() {
21839 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21840 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21843 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21844 assert_eq!(r.finish_reason, "cancelled");
21845 assert!(
21846 r.token_ids.is_empty(),
21847 "no tokens after cancel: {:?}",
21848 r.token_ids
21849 );
21850 assert_eq!(p.kv_cache.seq_len(), 0);
21851 assert!(p.kv_history.is_empty());
21852 assert!(!p.graph_want_logits);
21853 assert!(p.graph_logits.is_none());
21854 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21856 assert_ne!(r2.finish_reason, "cancelled");
21857 }
21858 use super::*;
21859
21860 #[test]
21861 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21862 let arch = |arch_name: &str, hidden_act: &str| {
21866 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21867 "arch_name": arch_name,
21868 "hidden_size": 8,
21869 "intermediate_size": 16,
21870 "num_layers": 1,
21871 "num_attention_heads": 1,
21872 "num_kv_heads": 1,
21873 "head_dim": 8,
21874 "vocab_size": 16,
21875 "layer_types": ["FullAttention"],
21876 "rms_norm_eps": 1e-6,
21877 "max_position_embeddings": 16,
21878 "hidden_act": hidden_act
21879 }))
21880 .expect("minimal architecture")
21881 };
21882 let minimax = arch("minimax_m3_vl", "swigluoai");
21883 assert_eq!(
21884 Act::from_arch_full(&minimax),
21885 Act::SwiGluOai {
21886 alpha: 1.702,
21887 limit: 7.0,
21888 }
21889 );
21890
21891 let unrelated = arch("qwen2", "swigluoai");
21894 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21895
21896 let non_oai = arch("minimax_m3_vl", "gelu");
21900 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21901 }
21902
21903 #[test]
21904 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21905 let act = Act::swiglu_oai(1.702, 7.0);
21906 let got = act.combine(10.0, -10.0);
21907 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21908 assert!((got - expected).abs() < 1e-6);
21909 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21910 assert_eq!(
21911 act.wgpu_graph_act(),
21912 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21913 "the WGPU-only arm accepts only the frozen M3 constants"
21914 );
21915 assert_eq!(
21916 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21917 None,
21918 "nearby OAI constants must not borrow MiniMax's kernel"
21919 );
21920 assert_eq!(
21921 act.moe_job_graph_contract(),
21922 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21923 "the direct MoE path carries the frozen clamp with the marker"
21924 );
21925 assert_eq!(
21926 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21927 None,
21928 "a nearby clamp must not borrow MiniMax's device contract"
21929 );
21930
21931 let gate_only = act.apply(-10.0);
21934 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21935 assert!((gate_only - expected_gate).abs() < 1e-10);
21936 }
21937
21938 #[test]
21939 fn explicit_sliding_schedule_selects_local_rope_profile() {
21940 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21945 p.swa = Some((1024, usize::MAX));
21946 p.sliding_layers = Some(vec![true, true, true, false]);
21947 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21948 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21949 p.rope_scale = 1.277_258_9;
21950 p.rope_scale_local = 1.0;
21951
21952 for li in 0..3 {
21953 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21954 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21955 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21956 }
21957 assert_eq!(p.layer_window(3), None);
21958 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21959 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21960 }
21961
21962 #[test]
21970 fn dynamic_ffn_equals_the_zeroing_arm() {
21971 let (hidden, inter) = (8usize, 32usize);
21972 let synth = |n: usize, salt: usize| -> Vec<f32> {
21973 (0..n)
21974 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21975 .collect()
21976 };
21977 let down = synth(hidden * inter, 3);
21978 let mut down_t = vec![0.0f32; inter * hidden];
21979 for r in 0..hidden {
21980 for c in 0..inter {
21981 down_t[c * hidden + r] = down[r * inter + c];
21982 }
21983 }
21984 let d = DenseFfn {
21985 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21986 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21987 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21988 act: Act::Silu,
21989 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21990 segs: Vec::new(),
21991 };
21992 let x = synth(hidden, 11);
21993 let k = 12usize;
21994 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
21995 let mut g = vec![0.0f32; inter];
21997 d.gate_proj.matvec(&x, &mut g, None);
21998 let mut u = vec![0.0f32; inter];
21999 d.up_proj.matvec(&x, &mut u, None);
22000 for v in g.iter_mut() {
22001 *v = inference::silu(*v);
22002 }
22003 keep_top_k(&mut g, k);
22004 for i in 0..inter {
22005 g[i] *= u[i];
22006 }
22007 let mut want = vec![0.0f32; hidden];
22008 d.down_proj.matvec(&g, &mut want, None);
22009 for (a, b) in want.iter().zip(&got) {
22010 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22011 }
22012 }
22013
22014 #[test]
22020 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22021 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22022 let inter = core + tube;
22023 let synth = |n: usize, salt: usize| -> Vec<f32> {
22024 (0..n)
22025 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22026 .collect()
22027 };
22028 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22029 let d_all = synth(hidden * inter, 3);
22030 let dense = DenseFfn {
22032 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22033 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22034 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22035 act: Act::Silu,
22036 down_t: None,
22037 segs: Vec::new(),
22038 };
22039 let rows =
22040 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22041 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22042 let mut o = Vec::with_capacity(hidden * (b - a));
22043 for r in 0..hidden {
22044 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22045 }
22046 o
22047 };
22048 let tubed = DenseFfn {
22049 down_t: None,
22050 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22051 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22052 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22053 act: Act::Silu,
22054 segs: vec![FfnSeg {
22055 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22056 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22057 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22058 start: core,
22059 width: tube,
22060 }],
22061 };
22062 let x = synth(hidden, 7);
22063 let want = dense_ffn(&dense, &x, None);
22064 let got = tube_ffn(&tubed, &x, 1, None, None);
22065 for (a, b) in want.iter().zip(&got) {
22066 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22067 }
22068 let mut bits = vec![0u8; inter.div_ceil(8)];
22070 for n in 0..core {
22071 bits[n / 8] |= 1 << (n % 8);
22072 }
22073 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22074 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22075 for (a, b) in masked.iter().zip(&closed) {
22076 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22077 }
22078 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22080 for (a, b) in closed.iter().zip(&batch) {
22081 assert_eq!(a, b, "batch arm disagrees with decode arm");
22082 }
22083 }
22084
22085 #[test]
22087 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22088 let (hidden, inter) = (16usize, 40usize);
22089 let synth = |n: usize, salt: usize| -> Vec<f32> {
22090 (0..n)
22091 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22092 .collect()
22093 };
22094 let d = DenseFfn {
22095 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22096 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22097 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22098 act: Act::Silu,
22099 down_t: None,
22100 segs: Vec::new(),
22101 };
22102 let x = synth(hidden, 9);
22103 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22105
22106 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22107
22108 let mut g = vec![0.0f32; inter];
22110 d.gate_proj.matvec(&x, &mut g, None);
22111 let mut u = vec![0.0f32; inter];
22112 d.up_proj.matvec(&x, &mut u, None);
22113 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22114 for i in 0..inter {
22115 g[i] = if act_set.contains(&(i as u16)) {
22116 inference::silu(g[i]) * u[i]
22117 } else {
22118 0.0
22119 };
22120 }
22121 let mut reference = vec![0.0f32; hidden];
22122 d.down_proj.matvec(&g, &mut reference, None);
22123
22124 let max_d = sparse
22125 .iter()
22126 .zip(&reference)
22127 .map(|(a, b)| (a - b).abs())
22128 .fold(0.0f32, f32::max);
22129 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22130 }
22131
22132 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22135 let (h, inter, heads, kv, hd) = (
22136 p.hidden_size,
22137 inter,
22138 p.num_heads,
22139 p.num_kv_heads,
22140 p.head_dim,
22141 );
22142 let synth = |n: usize, salt: usize| -> Vec<f32> {
22143 (0..n)
22144 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22145 .collect()
22146 };
22147 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22148 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22149 };
22150 p.mtp = Some(MtpModule {
22151 enorm: vec![1.0; h],
22152 hnorm: vec![1.0; h],
22153 eh_proj: qt(h, 2 * h, 301),
22154 layer: LayerWeights {
22155 input_norm: vec![1.0; h],
22156 post_norm: vec![1.0; h],
22157 attn_out_norm: None,
22158 ffn_out_norm: None,
22159 layer_scale: None,
22160 ffn: FfnKind::Dense(DenseFfn {
22161 gate_proj: qt(inter, h, 315),
22162 up_proj: qt(inter, h, 316),
22163 down_proj: qt(h, inter, 317),
22164 act: Act::Silu,
22165 down_t: None,
22166 segs: Vec::new(),
22167 }),
22168 attn: AttnKind::Full {
22169 bias: None,
22170 wq: qt(heads * hd, h, 311),
22171 wk: qt(kv * hd, h, 312),
22172 wv: qt(kv * hd, h, 313),
22173 wo: qt(h, heads * hd, 314),
22174 q_norm: None,
22175 k_norm: None,
22176 output_gate: false,
22177 softplus_gate: None,
22178 },
22179 },
22180 final_norm: vec![1.0; h],
22181 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22182 });
22183 }
22184
22185 fn attach_test_mtp(p: &mut Pipeline) {
22187 attach_test_mtp_width(p, p.intermediate_size);
22188 }
22189
22190 #[test]
22191 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22192 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22193 attach_test_mtp_width(&mut p, 11);
22196 let ids = [3, 9, 4, 12, 7, 2];
22197 let trunk = p.probe_ffn_mass(&ids);
22198 assert_eq!(trunk[0].len(), p.intermediate_size);
22199
22200 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22201 assert_eq!(got.len(), 1);
22202 assert_eq!(got[0].mass.len(), 11);
22203 assert_eq!(got[0].squared_mass.len(), 11);
22204 assert_eq!(got[0].observed_positions, ids.len() - 1);
22205 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22206 assert!(got[0]
22207 .mass
22208 .iter()
22209 .zip(&got[0].squared_mass)
22210 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22211 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22212 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22213
22214 let h = p.hidden_size;
22218 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22219 panic!("synthetic MTP must be dense")
22220 };
22221 dense.segs.push(FfnSeg {
22222 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22223 up: QTensor::from_f32(vec![0.0; h], 1, h),
22224 down: QTensor::from_f32(vec![0.0; h], h, 1),
22225 start: dense.gate_proj.rows(),
22226 width: 1,
22227 });
22228 let err = p.probe_mtp_ffn(&ids);
22229 assert!(err
22230 .unwrap_err()
22231 .contains("tube-segmented draft FFNs are not supported"));
22232 assert_eq!(p.kv_cache.seq_len(), 0);
22233 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22234 }
22235
22236 #[test]
22237 fn speculative_equals_vanilla_greedy() {
22238 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22239 let _env = ScopedTestEnv::set(&[
22242 ("CMF_GPU", Some("0")),
22243 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22244 ("CMF_MTP_PAIR", Some("1")),
22245 ]);
22246 let run = |spec: bool| {
22247 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22248 p.sampler_config.temperature = 0.0;
22249 attach_test_mtp(&mut p);
22250 p.speculative = spec;
22251 let r = p.generate("abcdef", 12, None, None).unwrap();
22252 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22253 };
22254 let (vanilla, d0, _) = run(false);
22255 let (spec, d1, a1) = run(true);
22256 assert_eq!(d0, 0, "vanilla path must not draft");
22257 assert!(d1 > 0, "speculative path must draft");
22258 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22259 assert_eq!(
22260 vanilla, spec,
22261 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22262 );
22263 }
22264
22265 #[test]
22266 fn speculative_accepts_constant_oracle() {
22267 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22268 let _env = ScopedTestEnv::set(&[
22269 ("CMF_GPU", Some("0")),
22270 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22271 ("CMF_MTP_PAIR", Some("1")),
22272 ]);
22273 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22274 p.sampler_config.temperature = 0.0;
22275 p.sampler_config.repetition_penalty = 1.0;
22276 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22279 attach_test_mtp(&mut p);
22280 p.speculative = true;
22281 let r = p.generate("abcd", 10, None, None).unwrap();
22282 assert!(r.mtp_drafted > 0);
22283 assert_eq!(
22284 r.mtp_accepted, r.mtp_drafted,
22285 "constant logits → every draft accepted"
22286 );
22287 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22290 }
22291
22292 #[test]
22293 fn host_fused_mtp_pair_is_opt_in() {
22294 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22295 let _env = ScopedTestEnv::set(&[
22296 ("CMF_GPU", Some("0")),
22297 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22298 ("CMF_MTP_PAIR", None),
22299 ]);
22300 let run = |spec: bool| {
22301 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22302 p.sampler_config.temperature = 0.0;
22303 attach_test_mtp(&mut p);
22304 p.speculative = spec;
22305 let r = p.generate("abcdef", 12, None, None).unwrap();
22306 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22307 };
22308 let (plain, d0, _) = run(false);
22309 let (guarded, d1, a1) = run(true);
22310 assert_eq!(d0, 0);
22311 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22312 assert_eq!(a1, 0);
22313 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22314 }
22315
22316 #[test]
22317 fn empty_prompt_is_an_error_not_a_panic() {
22318 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22319 let r = p.generate("", 4, None, None);
22320 assert!(r.is_err(), "empty prompt must be a clean error");
22321 }
22322
22323 #[test]
22324 fn every_token_enters_kv_exactly_once() {
22325 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22326 p.sampler_config.temperature = 0.0;
22328 let r = p.generate("abc", 2, None, None).unwrap();
22329 assert_eq!(r.prompt_tokens, 3);
22330 assert_eq!(
22334 p.kv_cache.seq_len(),
22335 3 + r.tokens_generated - 1,
22336 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22337 );
22338 }
22339
22340 #[test]
22341 fn generation_is_reproducible_with_seed() {
22342 let run = || {
22343 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22344 p.generate("hello", 8, None, None).unwrap().token_ids
22345 };
22346 assert_eq!(run(), run());
22347 }
22348
22349 #[test]
22350 fn resetting_sampler_restarts_the_seeded_stream() {
22351 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22352 let config = SamplerConfig {
22353 seed: Some(1234),
22354 ..SamplerConfig::default()
22355 };
22356 p.set_sampler_config(config.clone());
22357 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22358 p.set_sampler_config(config);
22359 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22360 assert_eq!(first, second);
22361 }
22362
22363 #[test]
22364 fn eviction_bounds_the_cache() {
22365 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22366 p.kv_cache.max_seq_len = 6;
22367 p.sampler_config.temperature = 0.0;
22368 let _ = p.generate("abcd", 12, None, None).unwrap();
22369 assert!(
22370 p.kv_cache.seq_len() <= 6 + 1,
22371 "cache must stay bounded by max_seq_len (got {})",
22372 p.kv_cache.seq_len()
22373 );
22374 }
22375
22376 #[test]
22377 fn confidence_matches_tokens_and_is_a_probability() {
22378 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22379 p.sampler_config.temperature = 0.0;
22380 p.sampler_config.repetition_penalty = 1.0;
22381 let r = p.generate("abcd", 10, None, None).unwrap();
22382 assert_eq!(
22383 r.token_confidence.len(),
22384 r.token_ids.len(),
22385 "one confidence per emitted token"
22386 );
22387 for &c in &r.token_confidence {
22388 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22389 }
22390 let logits = [1.0f32, 3.0, 0.5, 3.0];
22392 let p0 = top1_prob_t(&logits, 1, 1.0);
22393 let p1 = top1_prob_t(&logits, 3, 1.0);
22394 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22395 assert!(p0 > 0.0 && p0 < 1.0);
22396 let sharp = top1_prob_t(&logits, 1, 1.0);
22398 let soft = top1_prob_t(&logits, 1, 2.0);
22399 assert!(soft < sharp, "higher temperature lowers peak confidence");
22400 }
22401
22402 #[test]
22403 fn trace_is_opt_in_and_parallels_the_output() {
22404 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22406 p.sampler_config.temperature = 0.0;
22407 p.sampler_config.repetition_penalty = 1.0;
22408 let r = p.generate("abcd", 10, None, None).unwrap();
22409 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22410
22411 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22413 p.sampler_config.temperature = 0.0;
22414 p.sampler_config.repetition_penalty = 1.0;
22415 p.set_trace(true);
22416 let r = p.generate("abcd", 10, None, None).unwrap();
22417 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22418 for (i, tr) in r.traces.iter().enumerate() {
22419 assert_eq!(tr.t, i, "trace index is sequential");
22420 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22421 assert_eq!(
22422 tr.confidence, r.token_confidence[i],
22423 "trace confidence matches the confidence channel"
22424 );
22425 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22427 }
22428 }
22429
22430 #[test]
22431 fn explain_prefill_logits_match_greedy_first_token() {
22432 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22436 p.sampler_config.temperature = 0.0;
22437 p.sampler_config.repetition_penalty = 1.0;
22438 let ids = p.tokenizer.encode("abcd");
22439 let logits = p.prefill_next_logits(&ids, None);
22440 let argmax = logits
22441 .iter()
22442 .enumerate()
22443 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22444 .unwrap()
22445 .0 as u32;
22446 let r = p.generate("abcd", 1, None, None).unwrap();
22447 assert_eq!(
22448 argmax, r.token_ids[0],
22449 "explain preview must match greedy emit"
22450 );
22451 }
22452
22453 #[test]
22454 fn laguna_shared_expert_is_unconditionally_added() {
22455 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22456 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22457 let zero_dense = || DenseFfn {
22458 gate_proj: matrix(vec![0.0; 4]),
22459 up_proj: matrix(vec![0.0; 4]),
22460 down_proj: matrix(vec![0.0; 4]),
22461 act: Act::Silu,
22462 down_t: None,
22463 segs: Vec::new(),
22464 };
22465 let shared = DenseFfn {
22466 gate_proj: identity(),
22467 up_proj: identity(),
22468 down_proj: identity(),
22469 act: Act::Silu,
22470 down_t: None,
22471 segs: Vec::new(),
22472 };
22473 let x = [1.0, 2.0];
22474 let expected = dense_ffn(&shared, &x, None);
22475 let moe = MoeFfn {
22476 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22477 experts: vec![zero_dense()],
22478 top_k: 1,
22479 norm_topk_prob: true,
22480 router_sigmoid: true,
22481 sigmoid_denom_eps: 1e-6,
22482 expert_bias: None,
22483 routed_scaling: 1.0,
22484 route_tau: None,
22485 shared: Some((shared, None)),
22486 stats: std::cell::RefCell::new(Vec::new()),
22487 act_sq: std::cell::RefCell::new(Vec::new()),
22488 act_rows: std::cell::RefCell::new(Vec::new()),
22489 mask: None,
22490 per_expert_scale: None,
22491 router_input_norm: false,
22492 resonance: None,
22493 grown: Vec::new(),
22494 };
22495 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22496 for (actual, expected) in actual.iter().zip(expected) {
22497 assert!((actual - expected).abs() < 1e-6);
22498 }
22499 }
22500
22501 fn mimo_test_pipeline() -> Pipeline {
22510 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22511 let kvh = [1usize, 2, 2, 1];
22512 let synth = |n: usize, salt: usize| -> Vec<f32> {
22513 (0..n)
22514 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22515 .collect()
22516 };
22517 let qt = |rows: usize, cols: usize, salt: usize| {
22518 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22519 };
22520 let dense = |inter: usize, salt: usize| DenseFfn {
22521 gate_proj: qt(inter, hs, salt),
22522 up_proj: qt(inter, hs, salt + 1),
22523 down_proj: qt(hs, inter, salt + 2),
22524 act: Act::Silu,
22525 down_t: None,
22526 segs: Vec::new(),
22527 };
22528 let layers: Vec<LayerWeights> = (0..4)
22529 .map(|li| LayerWeights {
22530 input_norm: vec![1.0; hs],
22531 post_norm: vec![1.0; hs],
22532 attn_out_norm: None,
22533 ffn_out_norm: None,
22534 layer_scale: None,
22535 ffn: if li == 0 {
22536 FfnKind::Dense(dense(inter, 50))
22537 } else {
22538 FfnKind::Moe(MoeFfn {
22539 router: qt(4, hs, 60 + li),
22540 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22541 top_k: 2,
22542 norm_topk_prob: true,
22543 router_sigmoid: true,
22544 sigmoid_denom_eps: 1e-6,
22545 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22546 routed_scaling: 1.0,
22547 route_tau: None,
22548 shared: None,
22549 stats: std::cell::RefCell::new(Vec::new()),
22550 act_sq: std::cell::RefCell::new(Vec::new()),
22551 act_rows: std::cell::RefCell::new(Vec::new()),
22552 mask: None,
22553 per_expert_scale: None,
22554 router_input_norm: false,
22555 resonance: None,
22556 grown: Vec::new(),
22557 })
22558 },
22559 attn: AttnKind::Full {
22560 wq: qt(nh * hd, hs, li * 10 + 1),
22561 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22562 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22563 wo: qt(hs, nh * vd, li * 10 + 4),
22564 q_norm: None,
22565 k_norm: None,
22566 output_gate: false,
22567 softplus_gate: None,
22568 bias: None,
22569 },
22570 })
22571 .collect();
22572 let mut p = Pipeline::new(
22573 Tokenizer::byte_level(),
22574 PipelineWeights {
22575 embed_tokens: qt(vocab, hs, 100),
22576 layers,
22577 lm_head: qt(vocab, hs, 200),
22578 final_norm: vec![1.0; hs],
22579 },
22580 hs,
22581 inter,
22582 nh,
22583 1, hd,
22585 4,
22586 4,
22587 false,
22588 vocab,
22589 1e-6,
22590 1e7,
22591 NormStyle::Qwen,
22592 4096,
22593 SamplerConfig {
22594 seed: Some(7),
22595 ..Default::default()
22596 },
22597 );
22598 p.layer_dump = None;
22600 p.set_rotary(4, 1e7);
22601 p.sliding_layers = Some(vec![false, true, true, false]);
22602 p.swa = Some((3, usize::MAX));
22603 p.rotary_dim_local = Some(4);
22604 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22605 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22606 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22607 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22608 p
22609 }
22610
22611 #[test]
22612 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22613 let mut p = mimo_test_pipeline();
22614 p.speculative = false;
22615 p.ignore_eos = true;
22616 p.sampler_config.temperature = 0.0;
22617 p.sampler_config.repetition_penalty = 1.0;
22618 let a = vec![3, 5, 7, 9, 11, 13];
22619 let b = vec![4, 8, 12, 16, 20, 24];
22620 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22621 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22622 let actual = p
22626 .generate_from_embeds(&a, &rows, 8, None, None)
22627 .unwrap()
22628 .token_ids;
22629 assert_eq!(actual, expected);
22630 assert!(p.kv_history.is_empty());
22631 let mut extended = a.clone();
22632 extended.push(17);
22633 let after_media = p
22634 .generate_from_ids(&extended, 8, None, None)
22635 .unwrap()
22636 .token_ids;
22637 p.reset_session();
22638 let fresh = p
22639 .generate_from_ids(&extended, 8, None, None)
22640 .unwrap()
22641 .token_ids;
22642 assert_eq!(after_media, fresh);
22643 assert!(
22644 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22645 .is_err()
22646 );
22647 p.reset_session();
22651 p.generate_from_ids(&a, 1, None, None).unwrap();
22652 let mut media_ids = p.kv_history.clone();
22653 assert!(!media_ids.is_empty());
22654 media_ids.extend_from_slice(&[19, 21, 23]);
22655 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22656 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22657 let mut oracle = mimo_test_pipeline();
22658 oracle.speculative = false;
22659 oracle.ignore_eos = true;
22660 oracle.sampler_config.temperature = 0.0;
22661 oracle.sampler_config.repetition_penalty = 1.0;
22662 let expected = oracle
22663 .generate_from_ids(&source_ids, 8, None, None)
22664 .unwrap()
22665 .token_ids;
22666 assert_eq!(
22667 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22668 .unwrap()
22669 .token_ids,
22670 expected
22671 );
22672 assert!(p.kv_history.is_empty());
22673 let mut bad = rows;
22674 bad[0] = f32::NAN;
22675 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22676 }
22677
22678 fn f32_bits(v: &[f32]) -> Vec<u32> {
22679 v.iter().map(|x| x.to_bits()).collect()
22680 }
22681
22682 #[test]
22689 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22690 let mut p = mimo_test_pipeline();
22691 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22692 assert_eq!(kv, vec![1, 2, 2, 1]);
22693 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22694 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22695 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22696 let hs = p.hidden_size;
22697 let mut decode = Vec::new();
22698 for (pos, &id) in ids.iter().enumerate() {
22699 let e = p.embed_single(id);
22700 let h = p.forward_layers(&e, pos, None);
22701 decode.push(p.logits_from_hidden(&h));
22702 }
22703 for l in &p.kv_cache.layers {
22704 assert_eq!(l.seq_len, 12);
22705 assert_eq!(l.head_values(0).len(), 12 * 8);
22707 }
22708 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22709
22710 p.clear_sequence_state();
22711 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22712 for pos in 0..ids.len() {
22713 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22714 assert_eq!(
22715 f32_bits(&decode[pos]),
22716 f32_bits(&lg),
22717 "whole prompt, pos {pos}"
22718 );
22719 }
22720
22721 p.clear_sequence_state();
22722 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22723 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22724 for pos in 0..ids.len() {
22725 let row = if pos < 5 {
22726 &a[pos * hs..(pos + 1) * hs]
22727 } else {
22728 &b[(pos - 5) * hs..(pos - 4) * hs]
22729 };
22730 let lg = p.logits_from_hidden(row);
22731 assert_eq!(
22732 f32_bits(&decode[pos]),
22733 f32_bits(&lg),
22734 "two chunks, pos {pos}"
22735 );
22736 }
22737
22738 let last = |p: &mut Pipeline| {
22741 p.clear_sequence_state();
22742 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22743 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22744 };
22745 let base = last(&mut p);
22746 let mut no_sinks = mimo_test_pipeline();
22747 for l in &mut no_sinks.kv_cache.layers {
22748 l.sinks = None;
22749 }
22750 assert_ne!(
22751 f32_bits(&last(&mut no_sinks)),
22752 f32_bits(&base),
22753 "sinks are live"
22754 );
22755 let mut wide = mimo_test_pipeline();
22756 wide.swa = Some((64, usize::MAX));
22757 assert_ne!(
22758 f32_bits(&last(&mut wide)),
22759 f32_bits(&base),
22760 "window is live"
22761 );
22762
22763 p.clear_sequence_state();
22765 p.ignore_eos = true;
22766 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22767 assert_eq!(r.token_ids.len(), 4);
22768 }
22769
22770 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22773 let (hs, nh) = (16usize, 4usize);
22774 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22775 p.layer_dump = None;
22776 p.swa = Some((6, 4));
22777 p.proj_gate_sigmoid = true;
22778 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22779 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22780 let g: Vec<f32> = (0..nh * hs)
22781 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22782 .collect();
22783 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22784 }
22785 }
22786 p.swa_trim = trim;
22787 p
22788 }
22789
22790 #[test]
22796 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22797 let mut a = spark_test_pipeline(None);
22798 let mut b = spark_test_pipeline(Some((2, 4)));
22799 assert_eq!(
22800 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22801 vec![Some(6), Some(6), Some(6), None]
22802 );
22803 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22804 let mut pos = 0usize;
22805 for &n in [5usize, 13, 7, 16].iter().cycle() {
22806 if pos >= ids.len() {
22807 break;
22808 }
22809 let end = (pos + n).min(ids.len());
22810 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22811 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22812 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22813 pos = end;
22814 }
22815 for _ in 0..23 {
22816 let e = a.embed_single(((pos * 7) % 64) as u32);
22817 let ha = a.forward_layers(&e, pos, None);
22818 let hb = b.forward_layers(&e, pos, None);
22819 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22820 for li in 0..3 {
22821 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22822 }
22823 pos += 1;
22824 }
22825 for round in 0..9 {
22826 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22827 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22828 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22829 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22830 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22831 if round % 2 == 1 {
22832 for p in [&mut a, &mut b] {
22834 for l in &mut p.kv_cache.layers {
22835 l.truncate_last(2);
22836 }
22837 }
22838 } else {
22839 pos += 2;
22840 }
22841 }
22842 let e = a.embed_single(9);
22843 let (ha, hb) = (
22844 a.forward_layers(&e, pos, None),
22845 b.forward_layers(&e, pos, None),
22846 );
22847 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22848 for li in 0..3 {
22849 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22850 assert!(lb.base() > 0, "layer {li} trimmed");
22851 assert_eq!(la.base(), 0);
22852 assert_eq!(lb.pos_len(), la.seq_len);
22853 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22854 }
22855 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22856 assert_eq!(
22857 (gb.base(), gb.seq_len),
22858 (0, ga.seq_len),
22859 "the global layer keeps all"
22860 );
22861 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22862 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22863 }
22864
22865 #[test]
22870 fn swa_trim_wire_handoff_continues_bitwise() {
22871 let mut a = spark_test_pipeline(None);
22872 let mut b = spark_test_pipeline(Some((2, 4)));
22873 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22874 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22875 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22876 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22877 let mut pos = ids.len();
22878 for _ in 0..5 {
22879 let e = a.embed_single(((pos * 5) % 64) as u32);
22880 assert_eq!(
22881 f32_bits(&a.forward_layers(&e, pos, None)),
22882 f32_bits(&b.forward_layers(&e, pos, None))
22883 );
22884 pos += 1;
22885 }
22886 let mut c = spark_test_pipeline(Some((2, 4)));
22887 for li in 0..4 {
22888 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22889 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22890 }
22891 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22892 assert_eq!(c.kv_cache.seq_len(), pos);
22893 for step in 0..17 {
22894 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22895 let ha = a.forward_layers(&e, pos, None);
22896 let hc = c.forward_layers(&e, pos, None);
22897 assert_eq!(
22898 f32_bits(&ha),
22899 f32_bits(&hc),
22900 "after the hand-off, step {step}"
22901 );
22902 pos += 1;
22903 }
22904 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22905 }
22906
22907 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
22912 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
22913 let synth = |len: usize, salt: usize| -> Vec<f32> {
22914 (0..len)
22915 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22916 .collect()
22917 };
22918 let qt = |rows: usize, cols: usize, salt: usize| {
22919 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22920 };
22921 let layers = widths
22922 .iter()
22923 .copied()
22924 .enumerate()
22925 .map(|(k, inter)| {
22926 let s = 500 + k * 40;
22927 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22928 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22929 MtpModule {
22930 enorm: vec![1.0; hs],
22931 hnorm: vec![1.0; hs],
22932 eh_proj: qt(hs, 2 * hs, s),
22933 layer: LayerWeights {
22934 input_norm: vec![1.0; hs],
22935 post_norm: vec![1.0; hs],
22936 attn_out_norm: None,
22937 ffn_out_norm: None,
22938 layer_scale: None,
22939 attn: AttnKind::Full {
22940 wq: qt(nh * hd, hs, s + 1),
22941 wk: qt(nkv * hd, hs, s + 2),
22942 wv: qt(nkv * vd, hs, s + 3),
22943 wo: qt(hs, nh * vd, s + 4),
22944 q_norm: None,
22945 k_norm: None,
22946 output_gate: false,
22947 softplus_gate: None,
22948 bias: None,
22949 },
22950 ffn: FfnKind::Dense(DenseFfn {
22951 gate_proj: qt(inter, hs, s + 5),
22952 up_proj: qt(inter, hs, s + 6),
22953 down_proj: qt(hs, inter, s + 7),
22954 act: Act::Silu,
22955 down_t: None,
22956 segs: Vec::new(),
22957 }),
22958 },
22959 final_norm: vec![1.0; hs],
22960 kv,
22961 }
22962 })
22963 .collect();
22964 mimo_mtp::MimoMtp::from_layers(layers)
22965 }
22966
22967 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22968 mimo_test_mtp_widths(&vec![24; n], gain)
22969 }
22970
22971 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22972 p.clear_sequence_state();
22973 p.speculative = spec;
22974 p.ignore_eos = true;
22975 p.sampler_config.temperature = 0.0;
22976 p.generate_from_ids(ids, n, None, None).unwrap()
22977 }
22978
22979 #[test]
22980 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
22981 let mut p = mimo_test_pipeline();
22982 let widths = [7usize, 13, 29];
22983 assert_ne!(widths[0], p.intermediate_size);
22984 assert_ne!(widths[1], p.intermediate_size);
22985 assert_ne!(widths[2], p.intermediate_size);
22986 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
22987 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
22988
22989 let trunk = p.probe_ffn_mass(&ids);
22990 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
22991 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
22992 assert_eq!(got.len(), widths.len());
22993 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
22994 assert_eq!(stat.mass.len(), width, "draft layer {k}");
22995 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
22996 assert_eq!(stat.observed_positions, ids.len() - k - 1);
22997 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
22998 assert!(stat
22999 .mass
23000 .iter()
23001 .zip(&stat.squared_mass)
23002 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23003 }
23004 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23005 let st = p.mimo_mtp.as_ref().unwrap();
23006 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23007
23008 let h = p.hidden_size;
23011 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23012 panic!("synthetic MiMo MTP must be dense")
23013 };
23014 dense.segs.push(FfnSeg {
23015 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23016 up: QTensor::from_f32(vec![0.0; h], 1, h),
23017 down: QTensor::from_f32(vec![0.0; h], h, 1),
23018 start: dense.gate_proj.rows(),
23019 width: 1,
23020 });
23021 assert!(p
23022 .probe_mtp_ffn(&ids)
23023 .unwrap_err()
23024 .contains("tube-segmented draft FFNs are not supported"));
23025 assert_eq!(p.kv_cache.seq_len(), 0);
23026 assert!(p
23027 .mimo_mtp
23028 .as_ref()
23029 .unwrap()
23030 .layers
23031 .iter()
23032 .all(|m| m.kv.seq_len == 0));
23033 }
23034
23035 #[test]
23036 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23037 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23038 p.clear_sequence_state();
23039 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23040 let _exact = ExactMtpCalibrationGuard::enter();
23041 let got = crate::gpu::cpu_scope(|| {
23042 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, rows)
23043 })
23044 .expect("teacher-forced MiMo probe");
23045 st.reset();
23046 p.mimo_mtp = Some(st);
23047 p.reset_mtp_probe_state();
23048 got
23049 }
23050
23051 for post_norm_hidden in [false, true] {
23052 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23053 let make = || {
23054 let mut p = mimo_test_pipeline();
23055 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23056 st.post_norm_hidden = post_norm_hidden;
23057 p.mimo_mtp = Some(st);
23058 p
23059 };
23060 let mut full = make();
23061 let expected = collect(&mut full, &ids, usize::MAX);
23062 for rows in [1usize, 7, 128] {
23063 let mut chunked = make();
23064 let got = collect(&mut chunked, &ids, rows);
23065 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23066 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23067 assert_eq!(
23068 got.observed_positions, expected.observed_positions,
23069 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23070 );
23071 assert_eq!(
23072 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23073 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23074 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23075 );
23076 assert_eq!(
23077 got.squared_mass
23078 .iter()
23079 .map(|v| v.to_bits())
23080 .collect::<Vec<_>>(),
23081 expected
23082 .squared_mass
23083 .iter()
23084 .map(|v| v.to_bits())
23085 .collect::<Vec<_>>(),
23086 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23087 );
23088 }
23089 }
23090 }
23091 }
23092
23093 #[test]
23099 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23100 for post in [false, true] {
23103 let mut p = mimo_test_pipeline();
23104 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23106 let mut st0 = mimo_test_mtp(3, 1.0);
23107 st0.post_norm_hidden = post;
23108 p.mimo_mtp = Some(st0);
23109 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23110 let hs = p.hidden_size;
23111 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23112 p.mimo_note_rows(&hb, 0);
23113 let mut st = p.mimo_mtp.take().unwrap();
23114 let k = 3;
23117 let mut inc = Vec::new();
23118 for t in 0..ids.len() - k - 1 {
23119 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23120 }
23121 let s = ids.len();
23124 let mut reference = vec![vec![0u32; k]; s - k - 1];
23125 let mut fresh = mimo_test_mtp(3, 1.0);
23126 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23127 let n = s - layer - 1;
23128 let mut cats = vec![0.0f32; n * 2 * hs];
23129 for j in 0..n {
23130 let e = p.embed_single(ids[j + layer + 1]);
23131 let raw = &hb[j * hs..(j + 1) * hs];
23132 let g = if post {
23133 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23134 } else {
23135 raw.to_vec()
23136 };
23137 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23138 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23139 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23140 }
23141 let mut x = vec![0.0f32; n * hs];
23142 m.eh_proj.matmat(&cats, n, &mut x, None);
23143 p.mimo_mtp_block(m, &mut x, n, 0);
23144 for (t, row) in reference.iter_mut().enumerate() {
23145 let y = inference::rms_norm(
23146 &x[t * hs..(t + 1) * hs],
23147 &m.final_norm,
23148 p.rms_eps,
23149 p.norm_style,
23150 );
23151 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23152 }
23153 }
23154 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23155 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23157 assert!(distinct.len() > 3, "{inc:?}");
23158 let last_t = ids.len() - k - 2;
23160 for m in &st.layers {
23161 assert_eq!(m.kv.seq_len, last_t + 1);
23162 }
23163 }
23164 }
23165
23166 #[test]
23173 fn mimo_speculative_greedy_equals_plain_greedy() {
23174 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23175 let _env = ScopedTestEnv::set(&[
23176 ("CMF_GPU", Some("0")),
23177 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23178 ]);
23179 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23180 let n = 24;
23181 let mut p = mimo_test_pipeline();
23182 let plain = mimo_greedy(&mut p, &ids, n, false);
23183 assert_eq!(plain.mtp_drafted, 0);
23184 assert_eq!(plain.token_ids.len(), n);
23185 let plain_kv = p.kv_cache.layers[0].seq_len;
23186
23187 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23189 let spec = mimo_greedy(&mut p, &ids, n, true);
23190 assert!(spec.mtp_drafted > 0, "the round must draft");
23191 assert_eq!(spec.token_ids, plain.token_ids);
23192 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23193
23194 let mut truth: Vec<u32> = ids.clone();
23197 truth.extend(&plain.token_ids);
23198 let mut noisy = truth.clone();
23199 for (i, t) in noisy.iter_mut().enumerate() {
23200 if i % 5 == 0 {
23201 *t = (*t + 1) % 64;
23202 }
23203 }
23204 let mut st = mimo_test_mtp(3, 1.0);
23205 st.draft_override = Some(noisy);
23206 p.mimo_mtp = Some(st);
23207 let spec = mimo_greedy(&mut p, &ids, n, true);
23208 assert_eq!(spec.token_ids, plain.token_ids);
23209 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23210 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23211 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23212 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23213 assert!(
23214 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23215 "{:?}",
23216 stats.accept_hist
23217 );
23218 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23219
23220 let mut st = mimo_test_mtp(3, 1.0);
23223 st.draft_override = Some(truth);
23224 p.mimo_mtp = Some(st);
23225 let spec = mimo_greedy(&mut p, &ids, n, true);
23226 assert_eq!(spec.token_ids, plain.token_ids);
23227 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23228 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23229
23230 let off = mimo_greedy(&mut p, &ids, n, false);
23232 assert_eq!(off.token_ids, plain.token_ids);
23233 assert_eq!(off.mtp_drafted, 0);
23234 }
23235
23236 #[test]
23245 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23246 let p = mimo_test_pipeline();
23247 assert_eq!(
23248 p.graph_attn_decline_reason(),
23249 Some("per-layer KV head counts")
23250 );
23251 assert_eq!(p.wgpu_graph_attn_decline(), None);
23252 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23253 assert_eq!(
23254 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23255 (1, 4, 4, None, false)
23256 );
23257 assert_eq!(g0.invf, p.inv_freq.as_slice());
23258 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23259 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23260 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23261 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23262 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23263 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23264 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23265
23266 let emb = p.embed_single(3);
23269 let mut lg = Vec::new();
23270 assert!(
23271 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23272 .is_none()
23273 );
23274 let mut hid = emb.clone();
23275 assert_eq!(
23276 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23277 crate::gpu::BatchGraphOutcome::Declined
23278 );
23279 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23280 assert!(p.try_multi_burst(3, 0, 4).is_none());
23281 assert!(
23282 p.graph_declines().is_empty(),
23283 "no attention decline logged: {:?}",
23284 p.graph_declines()
23285 );
23286 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23291 assert_eq!(plain().graph_attn_decline_reason(), None);
23292 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23293 assert!(
23294 plain().graph_attn_geom(0).is_none(),
23295 "uniform models keep the historical arms"
23296 );
23297 let mut q = plain();
23298 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23299 assert_eq!(
23300 q.graph_attn_decline_reason(),
23301 Some("learned attention sinks")
23302 );
23303 assert_eq!(
23304 q.graph_attn_geom(1).unwrap().sink,
23305 Some(&[0.25f32, -0.25][..])
23306 );
23307 let mut q = plain();
23308 q.set_attn_geometry(None, Some(2)).unwrap();
23309 assert_eq!(
23310 q.graph_attn_decline_reason(),
23311 Some("V heads narrower than Q/K heads")
23312 );
23313 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23314 let mut q = plain();
23315 q.sliding_layers = Some(vec![true, false]);
23316 q.swa = Some((4, usize::MAX));
23317 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23318 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23319 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23320
23321 let mut q = mimo_test_pipeline();
23325 q.rope_scale = 2.0;
23326 q.rope_scale_local = 1.25;
23327 assert_eq!(q.wgpu_graph_attn_decline(), None);
23328 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23329 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23330
23331 let mut q = plain();
23334 q.rope_scale = 2.0;
23335 assert_eq!(
23336 q.wgpu_graph_attn_decline(),
23337 Some("scaled RoPE without per-layer geometry")
23338 );
23339 }
23340
23341 #[cfg(target_os = "macos")]
23347 #[test]
23348 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23349 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23353 p.swa = Some((16, usize::MAX));
23354 p.sliding_layers = Some(vec![true, true]);
23355 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23356 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23357
23358 assert_eq!(
23359 p.graph_attn_decline_reason(),
23360 Some("learned attention sinks")
23361 );
23362 let swa_graph = p.metal_graph_swa();
23363 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23364 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23365 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23366 assert!(
23367 native_sink_attend,
23368 "the q1 sink admission must follow the SWA door"
23369 );
23370 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23373 }
23374
23375 #[test]
23376 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23377 let mut p = mimo_test_pipeline();
23378 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23379 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23380 for _ in 0..if li == 0 { 2 } else { 12 } {
23381 layer.append(&row, &row, &[]);
23382 }
23383 }
23384 p.mimo_verify_rewind(9).unwrap();
23385 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23386 for layer in &p.kv_cache.layers[1..] {
23387 assert_eq!(layer.seq_len, 9);
23388 }
23389 }
23390
23391 #[test]
23395 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23396 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23397 let _ = std::fs::remove_dir_all(&dir);
23398 let mut p = mimo_test_pipeline();
23399 let hs = p.hidden_size;
23400 let ids = [5u32, 9, 11, 2, 40];
23401 p.layer_dump = Some(dir.join("decode"));
23402 for (pos, &id) in ids.iter().enumerate() {
23403 let e = p.embed_single(id);
23404 let _ = p.forward_layers(&e, pos, None);
23405 }
23406 p.clear_sequence_state();
23407 p.layer_dump = Some(dir.join("prefill"));
23408 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23409 for pos in 0..ids.len() {
23410 for li in 0..p.num_layers {
23411 let name = format!("p{pos:06}_l{li:02}.f32");
23412 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23413 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23414 assert_eq!(a.len(), hs * 4, "{name}");
23415 assert_eq!(a, b, "{name}");
23416 }
23417 }
23418 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23419 let vals: Vec<f32> = last
23420 .chunks(4)
23421 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23422 .collect();
23423 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23424 let _ = std::fs::remove_dir_all(&dir);
23425 }
23426
23427 #[test]
23428 fn attn_geometry_and_sinks_are_validated() {
23429 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23430 assert!(
23431 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23432 "one entry per layer"
23433 );
23434 assert!(
23435 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23436 "3 does not divide 4"
23437 );
23438 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23439 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23440 assert!(
23441 p.set_attn_geometry(None, Some(5)).is_err(),
23442 "V wider than the head"
23443 );
23444 p.set_attn_geometry(None, Some(4)).unwrap();
23445 assert_eq!(
23446 p.v_head_dim, None,
23447 "v_head_dim == head_dim is the uniform case"
23448 );
23449 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23450 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23451 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23452 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23453 assert!(
23454 p.kv_cache.layers[1].sinks.is_some(),
23455 "a reshape keeps the layer's sinks"
23456 );
23457 assert_eq!(p.layer_geom(1).0, 4);
23458 assert!(
23459 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23460 "one sink per Q head"
23461 );
23462 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23463 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23464 }
23465
23466 #[test]
23469 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23470 let cfg = || {
23471 Some(crate::nystrom::O1Cfg {
23472 layers: crate::nystrom::O1Layers::All,
23473 m: 4,
23474 w: 8,
23475 sink: 2,
23476 rect: crate::nystrom::O1Rect::Aggregate,
23477 })
23478 };
23479 let mut p = mimo_test_pipeline();
23480 p.set_o1(cfg());
23481 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23482 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23483 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23484 q.sliding_layers = Some(vec![false, false, true]);
23485 q.swa = Some((4, usize::MAX));
23486 q.set_o1(cfg());
23487 assert_eq!(q.o1_flags, vec![true, false, false]);
23488 }
23489
23490 #[test]
23491 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23492 const B: usize = 19;
23493 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23494 p.set_o1(Some(crate::nystrom::O1Cfg {
23495 layers: crate::nystrom::O1Layers::All,
23496 m: 4,
23497 w: 8,
23498 sink: 2,
23499 rect: crate::nystrom::O1Rect::Aggregate,
23500 }));
23501 p.o1_begin_with_prefix(Some(B));
23502 let ids: Vec<u32> = (0..B as u32).collect();
23503 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23504
23505 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23506 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23507 let next = p.embed_single(B as u32);
23508 let _ = p.forward_layers(&next, B, None);
23509 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23510 }
23511
23512 #[test]
23513 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23514 const B: usize = 19;
23515 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23516 let gdn_cfg = crate::linear_core::GdnCfg {
23520 num_v_heads: 2,
23521 num_k_heads: 1,
23522 key_head_dim: 2,
23523 value_head_dim: 4,
23524 conv_kernel: 3,
23525 hidden_size: 8,
23526 rms_eps: 1e-6,
23527 output_gate_sigmoid: false,
23528 };
23529 let synth = |n: usize, salt: usize| -> Vec<f32> {
23530 (0..n)
23531 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23532 .collect()
23533 };
23534 let qt = |rows: usize, cols: usize, salt: usize| {
23535 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23536 };
23537 let c_dim = gdn_cfg.conv_dim();
23538 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23539 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23540 in_proj_qkv: qt(c_dim, 8, 1),
23541 in_proj_z: qt(vd, 8, 2),
23542 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23543 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23544 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23545 a_log: vec![0.2, 0.5],
23546 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23547 norm: vec![1.0; gdn_cfg.value_head_dim],
23548 out_proj: qt(8, vd, 7),
23549 });
23550 p.gdn_cfg = Some(gdn_cfg);
23551 p.set_o1(Some(crate::nystrom::O1Cfg {
23552 layers: crate::nystrom::O1Layers::All,
23553 m: 4,
23554 w: 8,
23555 sink: 2,
23556 rect: crate::nystrom::O1Rect::Aggregate,
23557 }));
23558 p.o1_begin_with_prefix(Some(B));
23559 for pos in 0..B - 2 {
23560 let emb = p.embed_single(pos as u32);
23561 let _ = p.forward_layers(&emb, pos, None);
23562 }
23563 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23564
23565 let e1 = p.embed_single((B - 2) as u32);
23566 let e2 = p.embed_single((B - 1) as u32);
23567 let _ = p.forward_pair(&e1, &e2, B - 2);
23568
23569 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23570 assert!(
23571 p.kv_cache
23572 .layers
23573 .iter()
23574 .enumerate()
23575 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23576 );
23577 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23578 assert_ne!(
23579 p.kv_cache.layers[0].linear_state, lane1_state,
23580 "real pair must commit GDN lane 2 before returning"
23581 );
23582 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23583 let next = p.embed_single(B as u32);
23584 let _ = p.forward_layers(&next, B, None);
23585 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23586 }
23587
23588 #[test]
23589 fn o1_error_observation_stays_terminal_until_reset() {
23590 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23591 p.set_o1(Some(crate::nystrom::O1Cfg {
23592 layers: crate::nystrom::O1Layers::All,
23593 m: 4,
23594 w: 8,
23595 sink: 2,
23596 rect: crate::nystrom::O1Rect::Aggregate,
23597 }));
23598 p.o1_begin();
23599 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23600
23601 assert!(p.o1_seal_checked().is_err());
23602 assert!(
23603 p.o1_seal_checked().is_err(),
23604 "retry must see the sticky error"
23605 );
23606 let k = vec![0.2f32; 4];
23607 let v = vec![0.3f32; 4];
23608 p.kv_cache.layers[0].append(&k, &v, &[]);
23609 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23610
23611 p.reset_session();
23612 p.o1_begin();
23613 p.kv_cache.layers[0].append(&k, &v, &[]);
23614 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23615 }
23616
23617 #[test]
23618 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23619 let ids = vec![1u32, 2, 3, 4, 5, 6];
23620 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23621 p.graph_logits = Some(vec![123.0]);
23622 p.graph_want_logits = true;
23623 p.graph_failed
23624 .store(true, std::sync::atomic::Ordering::Relaxed);
23625 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23626 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23627 assert!(err.contains("before NLL"));
23628 assert!(p.graph_logits.is_none());
23629 assert!(!p.graph_want_logits);
23630 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23631 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23632
23633 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23634 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23635 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23636 assert_eq!(actual.1, expected.1);
23637 assert!((actual.0 - expected.0).abs() < 1e-9);
23638 }
23639
23640 #[test]
23641 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23642 let ids = vec![1u32, 2, 3, 4, 5, 6];
23643 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23644 p.nll_test_fail_at = Some(1);
23645 let err = p
23646 .nll_ids_from(&ids, 0)
23647 .expect_err("one-shot forward failure");
23648 assert!(err.contains("forward") || err.contains("score row"));
23649 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23650 assert!(!p.graph_want_logits);
23651 assert!(p.graph_logits.is_none());
23652 assert!(p.kv_history.is_empty());
23653
23654 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23655 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23656 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23657 assert_eq!(actual.1, expected.1);
23658 assert!((actual.0 - expected.0).abs() < 1e-9);
23659 }
23660
23661 #[test]
23662 fn nll_serial_failure_before_first_row_is_reported() {
23663 let ids = vec![1u32, 2, 3, 4];
23664 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23665 p.nll_test_force_serial = true;
23666 p.nll_test_fail_at = Some(0);
23667 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23668 assert!(err.contains("serial forward"));
23669 assert!(p.kv_history.is_empty());
23670 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23671 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23672 }
23673
23674 #[test]
23675 fn ffn_probe_failure_discards_recorder_and_state() {
23676 let ids = vec![1u32, 2, 3, 4];
23677 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23678 p.nll_test_fail_at = Some(0);
23679 let err = p
23680 .probe_ffn_mass_batch(&ids)
23681 .expect_err("probe forward failure");
23682 assert!(err.contains("NLL"));
23683 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23684 assert!(p.kv_history.is_empty());
23685 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23686 }
23687
23688 #[test]
23689 fn nll_test_controls_are_pipeline_scoped() {
23690 let ids = vec![1u32, 2, 3, 4];
23691 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23692 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23693 failing.nll_test_force_serial = true;
23694 failing.nll_test_fail_at = Some(0);
23695
23696 assert!(!failing.can_prefill_batched());
23697 assert!(unaffected.can_prefill_batched());
23698 let expected = unaffected
23699 .nll_ids_from(&ids, 0)
23700 .expect("unaffected pipeline remains usable");
23701 let err = failing
23702 .nll_ids_from(&ids, 0)
23703 .expect_err("failure injection belongs to failing pipeline");
23704 assert!(err.contains("serial forward"));
23705 assert!(failing.nll_test_fail_at.is_none());
23706 assert!(unaffected.can_prefill_batched());
23707 let actual = unaffected
23708 .nll_ids_from(&ids, 0)
23709 .expect("unaffected pipeline remains reusable");
23710 assert_eq!(actual.1, expected.1);
23711 assert!((actual.0 - expected.0).abs() < 1e-9);
23712 }
23713
23714 #[test]
23715 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23716 let ids = vec![1u32, 2, 3, 4, 5, 6];
23717 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23718 p.graph_logits = Some(vec![123.0]);
23719 p.graph_want_logits = true;
23720 p.graph_failed
23721 .store(true, std::sync::atomic::Ordering::Relaxed);
23722 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23723
23724 let err = p
23725 .forward_ids(&ids, None)
23726 .expect_err("a failed forward must not become a valid head result");
23727 assert!(err.contains("forward_ids setup"));
23728 assert!(p.graph_logits.is_none());
23729 assert!(!p.graph_want_logits);
23730 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23731 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23732 assert_eq!(p.kv_cache.seq_len(), 0);
23733
23734 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23735 .forward_ids(&ids, None)
23736 .expect("fresh forward_ids");
23737 let actual = p
23738 .forward_ids(&ids, None)
23739 .expect("pipeline remains reusable after a failed forward");
23740 assert_eq!(actual.len(), expected.len());
23741 assert!(
23742 actual
23743 .iter()
23744 .zip(expected)
23745 .all(|(a, b)| (a - b).abs() < 1e-9)
23746 );
23747 assert_eq!(p.kv_cache.seq_len(), ids.len());
23748 }
23749
23750 #[test]
23751 fn sigmoid_router_floor_is_explicit_per_architecture() {
23752 let zero = || DenseFfn {
23758 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23759 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23760 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23761 act: Act::Silu,
23762 down_t: None,
23763 segs: Vec::new(),
23764 };
23765 let m = MoeFfn {
23766 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23767 experts: vec![zero(), zero()],
23768 top_k: 1,
23769 norm_topk_prob: true,
23770 router_sigmoid: true,
23771 sigmoid_denom_eps: 1e-20,
23772 expert_bias: None,
23773 routed_scaling: 2.5,
23774 route_tau: None,
23775 shared: None,
23776 stats: std::cell::RefCell::new(Vec::new()),
23777 act_sq: std::cell::RefCell::new(Vec::new()),
23778 act_rows: std::cell::RefCell::new(Vec::new()),
23779 mask: None,
23780 per_expert_scale: None,
23781 router_input_norm: false,
23782 resonance: None,
23783 grown: Vec::new(),
23784 };
23785 let logits = [-20.0f32, -20.0];
23786 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23787 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23788 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23789 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23790 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23791 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23792 assert!(legacy_wsum > minimax_wsum * 100.0);
23793 }
23794
23795 #[test]
23796 fn resonance_scores_match_formula_and_stable_tie() {
23797 let r = Resonance {
23798 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23800 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23801 k: 1,
23802 bias: vec![1.5, 0.5, 0.0],
23803 shell: Vec::new(),
23804 };
23805 let x = [1.0f32, 1.0];
23806 let mut got = vec![0.0; 3];
23807 r.scores(&x, &mut got);
23808 assert!((got[0] - 0.5).abs() < 1e-6);
23812 assert!((got[1] - 0.5).abs() < 1e-6);
23813 assert!(got[2].abs() < 1e-6);
23814 let best = got
23815 .iter()
23816 .enumerate()
23817 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23818 .map(|(i, _)| i);
23819 assert_eq!(best, Some(0));
23820 assert!(got.iter().all(|v| v.is_finite()));
23821 }
23822
23823 #[test]
23828 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23829 let plain = Resonance {
23835 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23836 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23837 k: 1,
23838 bias: vec![1.5, 0.5, 0.0, 0.0],
23839 shell: Vec::new(),
23840 };
23841 let shelled = Resonance {
23842 mu: plain.mu.clone(),
23843 u: plain.u.clone(),
23844 k: 1,
23845 bias: plain.bias.clone(),
23846 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23847 };
23848 assert!(!plain.has_shell());
23849 assert!(shelled.has_shell());
23850 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23851 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23852 set_growth_shell(Some(true));
23853 assert!(growth_shell_enabled());
23854 let x = [1.0f32, 1.0];
23857 plain.scores(&x, &mut a);
23858 shelled.scores(&x, &mut b);
23859 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23860 assert!(a[2] == 0.0 && a[3] == 0.0);
23861 let xo = [3.0f32, 0.0];
23864 plain.scores(&xo, &mut a);
23865 shelled.scores(&xo, &mut b);
23866 assert_eq!(a[2], -6.0);
23867 assert_eq!(a[3], -6.0);
23868 assert_eq!(
23869 bits(&a[..3]),
23870 bits(&b[..3]),
23871 "trunk rows + the boundary row"
23872 );
23873 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23874 assert_eq!(shelled.effective_shell(4), shelled.shell);
23875 set_growth_shell(Some(false));
23878 assert!(!growth_shell_enabled());
23879 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
23880 shelled.scores(&xo, &mut b);
23881 assert_eq!(bits(&a), bits(&b));
23882 set_growth_shell(None);
23883 let short = Resonance {
23886 shell: vec![f32::INFINITY, f32::INFINITY],
23887 ..shelled
23888 };
23889 set_growth_shell(Some(true));
23890 short.scores(&xo, &mut b);
23891 assert_eq!(bits(&a), bits(&b));
23892 set_growth_shell(None);
23893 }
23894
23895 #[test]
23899 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
23900 let zero = || DenseFfn {
23901 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23902 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23903 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23904 act: Act::Silu,
23905 down_t: None,
23906 segs: Vec::new(),
23907 };
23908 let moe = |sigmoid: bool| MoeFfn {
23909 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
23910 experts: vec![zero(), zero(), zero(), zero()],
23911 top_k: 1,
23912 norm_topk_prob: true,
23913 router_sigmoid: sigmoid,
23914 sigmoid_denom_eps: 1e-6,
23915 expert_bias: None,
23916 routed_scaling: 1.0,
23917 route_tau: None,
23918 shared: None,
23919 stats: std::cell::RefCell::new(Vec::new()),
23920 act_sq: std::cell::RefCell::new(Vec::new()),
23921 act_rows: std::cell::RefCell::new(Vec::new()),
23922 mask: None,
23923 per_expert_scale: None,
23924 router_input_norm: false,
23925 resonance: None,
23926 grown: Vec::new(),
23927 };
23928 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
23929 for sigmoid in [false, true] {
23930 let m = moe(sigmoid);
23931 let (idx, p, wsum) = moe_route(&logits, &m, None);
23932 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
23933 assert_eq!(p[1], 0.0);
23934 assert_eq!(p[3], 0.0);
23935 assert!(p[2] > p[0] && p[0] > 0.0);
23936 assert!(p.iter().all(|v| v.is_finite()));
23937 let w = p[2] / wsum;
23938 if sigmoid {
23939 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
23941 } else {
23942 assert_eq!(
23943 w, 1.0,
23944 "softmax: the renormalized top-1 weight is exactly 1"
23945 );
23946 }
23947 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
23950 assert_eq!(idx, vec![2]);
23951 }
23952 let m = moe(false);
23954 let (idx, _, _) = moe_route(
23955 &[
23956 f32::NEG_INFINITY,
23957 f32::NEG_INFINITY,
23958 f32::NEG_INFINITY,
23959 -9.0,
23960 ],
23961 &m,
23962 None,
23963 );
23964 assert_eq!(idx, vec![3]);
23965 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
23968 assert_eq!(idx, vec![0]);
23969 assert!(p.iter().all(|&v| v == 0.25));
23970 assert!(wsum.is_finite() && wsum > 0.0);
23971 }
23972
23973 #[cfg(feature = "gpu")]
23979 #[test]
23980 fn minimax_device_route_matches_host_sigmoid_contract() {
23981 match crate::gpu_wgpu::selected_and_up() {
23982 None => {
23983 crate::gpu_wgpu::skip_or_fail(module_path!());
23984 return;
23985 }
23986 Some(false) => {
23994 eprintln!(
23995 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23996 module_path!()
23997 );
23998 return;
23999 }
24000 Some(true) => {}
24001 }
24002 let n = 128usize;
24003 let top_k = 4usize;
24004 let logits: Vec<f32> = (0..n)
24005 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24006 .collect();
24007 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24008 let m = MoeFfn {
24009 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24010 experts: Vec::new(),
24011 top_k,
24012 norm_topk_prob: true,
24013 router_sigmoid: true,
24014 sigmoid_denom_eps: 1e-20,
24015 expert_bias: Some(bias.clone()),
24016 routed_scaling: 2.0,
24017 route_tau: None,
24018 shared: None,
24019 stats: std::cell::RefCell::new(Vec::new()),
24020 act_sq: std::cell::RefCell::new(Vec::new()),
24021 act_rows: std::cell::RefCell::new(Vec::new()),
24022 mask: None,
24023 per_expert_scale: None,
24024 router_input_norm: false,
24025 resonance: None,
24026 grown: Vec::new(),
24027 };
24028 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24029 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24030
24031 for shared in [Some(n as u32), None] {
24032 let mut gi = Vec::new();
24033 let mut gw = Vec::new();
24034 assert!(
24035 crate::gpu_wgpu::minimax_moe_route_for_test(
24036 &logits,
24037 Some(&bias),
24038 top_k,
24039 2.0,
24040 shared,
24041 &mut gi,
24042 &mut gw,
24043 ),
24044 "the device did not come up — the test checked nothing"
24045 );
24046 let routed = if shared.is_some() { top_k } else { gi.len() };
24047 assert_eq!(
24048 &gi[..routed],
24049 &ci[..],
24050 "different experts or order (shared={shared:?})"
24051 );
24052 let d = cw
24053 .iter()
24054 .zip(&gw[..routed])
24055 .map(|(a, b)| (a - b).abs())
24056 .fold(0.0f32, f32::max);
24057 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24058 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24059 if let Some(s) = shared {
24060 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24061 assert_eq!(gw[top_k], 1.0, "shared weight");
24062 }
24063 }
24064 }
24065
24066 #[cfg(feature = "gpu")]
24071 #[test]
24072 fn minimax_device_cold_weights_match_host_contract() {
24073 match crate::gpu_wgpu::selected_and_up() {
24074 None => {
24075 crate::gpu_wgpu::skip_or_fail(module_path!());
24076 return;
24077 }
24078 Some(false) => {
24086 eprintln!(
24087 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24088 module_path!()
24089 );
24090 return;
24091 }
24092 Some(true) => {}
24093 }
24094 let n = 128usize;
24095 let top_k = 4usize;
24096 let logits: Vec<f32> = (0..n)
24097 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24098 .collect();
24099 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24100 let m = MoeFfn {
24101 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24102 experts: Vec::new(),
24103 top_k,
24104 norm_topk_prob: true,
24105 router_sigmoid: true,
24106 sigmoid_denom_eps: 1e-20,
24107 expert_bias: Some(bias.clone()),
24108 routed_scaling: 2.0,
24109 route_tau: None,
24110 shared: None,
24111 stats: std::cell::RefCell::new(Vec::new()),
24112 act_sq: std::cell::RefCell::new(Vec::new()),
24113 act_rows: std::cell::RefCell::new(Vec::new()),
24114 mask: None,
24115 per_expert_scale: None,
24116 router_input_norm: false,
24117 resonance: None,
24118 grown: Vec::new(),
24119 };
24120 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24121 assert_eq!(ci.len(), top_k);
24122
24123 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24125 let mut cold = Vec::new();
24126 let mut idx = Vec::new();
24127 let mut dw = Vec::new();
24128 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24129 &logits,
24130 Some(&bias),
24131 top_k,
24132 2.0,
24133 &resident,
24134 n as u32,
24135 &mut cold,
24136 &mut idx,
24137 &mut dw,
24138 ));
24139 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24140 for (r, &e) in ci.iter().enumerate() {
24145 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24146 let host = p[e] / wsum;
24147 let d = (dw[r] - host).abs();
24148 assert!(
24149 d <= 1e-6 * host.abs().max(1.0),
24150 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24151 dw[r]
24152 );
24153 }
24154 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24155 assert_eq!(dw[top_k], 1.0, "shared weight");
24156
24157 let mut remap = resident.clone();
24159 remap[ci[0]] = u32::MAX;
24160 remap[ci[2]] = u32::MAX;
24161 let mut cold = Vec::new();
24162 let mut idx2 = Vec::new();
24163 let mut dw2 = Vec::new();
24164 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24165 &logits,
24166 Some(&bias),
24167 top_k,
24168 2.0,
24169 &remap,
24170 n as u32,
24171 &mut cold,
24172 &mut idx2,
24173 &mut dw2,
24174 ));
24175 let mut resident_weight_checked = 0usize;
24183 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24184 if dw2[r] == 0.0 {
24185 continue; }
24187 let e = slot as usize;
24188 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24189 let host = p[e] / wsum;
24190 let d = (dw2[r] - host).abs();
24191 assert!(
24192 d <= 1e-6 * host.abs().max(1.0),
24193 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24194 );
24195 resident_weight_checked += 1;
24196 }
24197 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24198 println!("partial bank: checked {resident_weight_checked} resident weights");
24199 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24200 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24201 ids.sort_unstable();
24202 let mut want = vec![ci[0], ci[2]];
24203 want.sort_unstable();
24204 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24205 for &(e, w) in &cold {
24206 let host = p[e] / wsum;
24207 let d = (w - host).abs();
24208 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24209 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24210 }
24211 }
24212
24213 #[test]
24218 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24219 let zero = || DenseFfn {
24220 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24221 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24222 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24223 act: Act::Silu,
24224 down_t: None,
24225 segs: Vec::new(),
24226 };
24227 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24228 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24229 experts: vec![zero(), zero(), zero()],
24230 top_k: 1,
24231 norm_topk_prob: norm_topk,
24232 router_sigmoid: false,
24233 sigmoid_denom_eps: 1e-6,
24234 expert_bias: None,
24235 routed_scaling: 1.0,
24236 route_tau: None,
24237 shared: None,
24238 stats: std::cell::RefCell::new(Vec::new()),
24239 act_sq: std::cell::RefCell::new(Vec::new()),
24240 act_rows: std::cell::RefCell::new(Vec::new()),
24241 mask: None,
24242 per_expert_scale: None,
24243 router_input_norm: false,
24244 resonance: resonant.then(|| Resonance {
24245 mu: vec![0.0; 6],
24246 u: Vec::new(),
24247 k: 0,
24248 bias: vec![0.0; 3],
24249 shell: Vec::new(),
24250 }),
24251 grown: Vec::new(),
24252 };
24253 let lo = -0.1f32;
24256 let hi = f32::from_bits(lo.to_bits() - 1);
24257 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24258 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24259 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24261 assert_eq!(
24262 idx,
24263 vec![0],
24264 "softmax tie → lower index (the gated contract)"
24265 );
24266 for norm in [true, false] {
24269 let m = moe(true, norm);
24270 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24271 assert_eq!(idx, vec![1], "norm_topk {norm}");
24272 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24273 assert_eq!(p[1] / wsum, 1.0);
24274 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24277 assert_eq!(idx, vec![0]);
24278 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24280 assert_eq!(idx, vec![2]);
24281 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24282 assert_eq!(idx, vec![0]);
24283 assert_eq!(p[0] / wsum, 1.0);
24284 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24287 assert_eq!(idx, vec![0]);
24288 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24289 }
24290 }
24291}