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 #[cfg(target_os = "macos")]
9660 if task_mask.is_none() && self.is_block_diffusion() {
9661 if let Some(r) = self.clover_metal_nll(ids, start) {
9662 self.nll_end();
9663 return r;
9664 }
9665 }
9666 let result: Result<(f64, usize), String> = (|| {
9667 let mut nll = 0f64;
9668 let mut cnt = 0usize;
9669 let (graph_quality, fused_head_quality) = nll_graph_policy(
9682 task_mask.is_none(),
9683 self.graph_prefill_preferred(),
9684 crate::gpu::q1_force(),
9685 );
9686 self.graph_head_required = fused_head_quality;
9687 self.graph_want_logits = fused_head_quality;
9688 #[cfg(target_os = "macos")]
9689 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9690 match self.nll_batch_metal(ids, start) {
9691 MetalBatchNllOutcome::Completed(nll, count) => {
9692 return Ok((nll, count));
9693 }
9694 MetalBatchNllOutcome::Declined => {}
9695 MetalBatchNllOutcome::Failed(err) => return Err(err),
9696 }
9697 }
9698 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9703 if self.can_prefill_batched() && !graph_quality && !force_serial {
9704 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9713 .ok()
9714 .and_then(|v| v.parse().ok())
9715 .filter(|&c: &usize| c > 0)
9716 .unwrap_or(128);
9717 const LM_SUB: usize = 32;
9718 let n = ids.len().saturating_sub(1);
9719 let hs = self.hidden_size;
9720 let rows = self.weights.lm_head.rows();
9721 let mut pos = 0usize;
9722 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9723 while pos < n {
9724 let end = (pos + chunk_len).min(n);
9725 let bsz = end - pos;
9726 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9727 self.nll_check_graph("batched prefill", pos)?;
9728 if state_trace && end % 256 == 0 {
9729 self.trace_recurrent_state(end);
9730 }
9731 let mut k0 = 0usize;
9732 while k0 < bsz {
9733 let k1 = (k0 + LM_SUB).min(bsz);
9734 let sb = k1 - k0;
9735 if pos + k1 <= start {
9738 k0 = k1;
9739 continue;
9740 }
9741 let mut normed = vec![0.0f32; sb * hs];
9742 for k in 0..sb {
9743 let r = inference::rms_norm(
9744 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9745 &self.weights.final_norm,
9746 self.rms_eps,
9747 self.norm_style,
9748 );
9749 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9750 }
9751 let mut logits = vec![0.0f32; sb * rows];
9752 self.weights
9753 .lm_head
9754 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9755 for k in 0..sb {
9756 if pos + k0 + k < start {
9757 continue;
9758 }
9759 self.nll_check_graph("batched score row", pos + k0 + k)?;
9760 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9761 if let Some(mu) = self.logit_multiplier {
9762 for v in lg.iter_mut() {
9763 *v *= mu;
9764 }
9765 }
9766 if let Some(c) = self.final_softcap {
9770 softcap_in_place(lg, c, self.pool.as_deref());
9771 }
9772 if let Some(cm) = self.head_clusters.clone() {
9775 self.hierarchical_head_logprobs(
9776 &normed[k * hs..(k + 1) * hs],
9777 &cm,
9778 lg,
9779 );
9780 }
9781 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9782 let target = ids[pos + k0 + k + 1] as usize;
9783 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9784 let lse: f64 = lg
9785 .iter()
9786 .map(|&v| ((v - max) as f64).exp())
9787 .sum::<f64>()
9788 .ln()
9789 + max as f64;
9790 nll += lse - lg[target] as f64;
9791 cnt += 1;
9792 if std::env::var("CMF_PPL_TRACE").is_ok() {
9793 let top = lg
9794 .iter()
9795 .enumerate()
9796 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9797 .map(|(i, _)| i)
9798 .unwrap_or(0);
9799 eprintln!(
9800 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9801 pos + k0 + k,
9802 target,
9803 lse - lg[target] as f64,
9804 top,
9805 lg[target],
9806 lg[top]
9807 );
9808 }
9809 }
9810 k0 = k1;
9811 }
9812 pos = end;
9813 }
9814 return Ok((nll, cnt));
9815 }
9816 for pos in 0..ids.len().saturating_sub(1) {
9817 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9818 self.nll_check_graph("serial forward", pos)?;
9819 let out_of_band = self.graph_logits.take();
9827 if self.graph_head_required && out_of_band.is_none() {
9828 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9829 return Err(format!(
9830 "fused Metal graph head did not complete at NLL position {pos}"
9831 ));
9832 }
9833 if pos < start {
9834 continue;
9835 }
9836 let logits = match out_of_band {
9837 Some(lg) => lg,
9838 None => {
9839 let normed = inference::rms_norm(
9840 &hidden,
9841 &self.weights.final_norm,
9842 self.rms_eps,
9843 self.norm_style,
9844 );
9845 self.lm_head_forward(&normed)
9849 }
9850 };
9851 let target = ids[pos + 1] as usize;
9852 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9853 let lse: f64 = logits
9854 .iter()
9855 .map(|&v| ((v - max) as f64).exp())
9856 .sum::<f64>()
9857 .ln()
9858 + max as f64;
9859 let tok_nll = lse - logits[target] as f64;
9860 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9861 let top = logits
9862 .iter()
9863 .enumerate()
9864 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9865 .map(|(i, _)| i)
9866 .unwrap_or(0);
9867 eprintln!(
9868 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9869 logits[target], logits[top]
9870 );
9871 }
9872 nll += tok_nll;
9873 cnt += 1;
9874 }
9875 Ok((nll, cnt))
9876 })();
9877 self.nll_end();
9878 result
9879 }
9880
9881 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9886 let normed = inference::rms_norm(
9887 hidden,
9888 &self.weights.final_norm,
9889 self.rms_eps,
9890 self.norm_style,
9891 );
9892 let mut logits = self.lm_head_forward(&normed);
9895 let target = target as usize;
9896 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9897 let lse: f64 = logits
9898 .iter()
9899 .map(|&v| ((v - max) as f64).exp())
9900 .sum::<f64>()
9901 .ln()
9902 + max as f64;
9903 let tok_nll = lse - logits[target] as f64;
9904 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9905 let top = logits
9906 .iter()
9907 .enumerate()
9908 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9909 .map(|(i, _)| i)
9910 .unwrap_or(0);
9911 eprintln!(
9912 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9913 logits[target], logits[top]
9914 );
9915 }
9916 attention::recycle_buf(&mut logits);
9917 tok_nll
9918 }
9919
9920 fn trace_recurrent_state(&self, pos: usize) {
9928 let stats = |v: &[f32]| -> (f64, f64) {
9929 if v.is_empty() {
9930 return (0.0, 0.0);
9931 }
9932 let (mut ss, mut mx) = (0f64, 0f64);
9933 for &x in v {
9934 ss += (x as f64) * (x as f64);
9935 mx = mx.max((x as f64).abs());
9936 }
9937 ((ss / v.len() as f64).sqrt(), mx)
9938 };
9939 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9940 let lw = &self.weights.layers[self.phys_layer(li)];
9941 let (kind, s_len) = match &lw.attn {
9942 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9943 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9944 AttnKind::Bounded(_) => ("bounded", 0),
9945 AttnKind::Full { .. } => ("full", 0),
9946 _ => ("other", 0),
9947 };
9948 let (rms, max) = stats(&l.linear_state);
9949 let s_part = if kind == "vmf" {
9950 &l.linear_state[..s_len.min(l.linear_state.len())]
9951 } else if kind == "gdn" {
9952 let ring = l
9953 .linear_state
9954 .len()
9955 .saturating_sub(s_len)
9956 .min(l.linear_state.len());
9957 &l.linear_state[ring..]
9958 } else {
9959 &l.linear_state[..0]
9960 };
9961 let (s_rms, s_max) = stats(s_part);
9962 let (ring_rms, ring_len) = match &l.bounded {
9963 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9964 None => (0.0, 0),
9965 };
9966 eprintln!(
9967 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9968 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9969 l.linear_state.len(),
9970 l.seq_len
9971 );
9972 }
9973 }
9974
9975 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9993 self.nll_begin()?;
9998 let requested_prefix = (prefill > 0).then_some(prefill);
9999 self.o1_begin_with_prefix(requested_prefix);
10000 let n = ids.len().saturating_sub(1);
10001 let requested_start = prefill.min(n);
10002 let exact_end = if self.o1_active() {
10007 match requested_prefix {
10008 Some(requested) => self.o1_effective_boundary(requested),
10009 None => self
10010 .o1_cfg
10011 .as_ref()
10012 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10013 }
10014 .unwrap_or(requested_start)
10015 .min(n)
10016 } else {
10017 requested_start
10018 };
10019 let mut nll = 0f64;
10020 let mut cnt = 0usize;
10021
10022 let mut pos = 0usize;
10026 if self.can_prefill_batched() {
10027 const CHUNK: usize = 128;
10028 while pos < exact_end {
10029 let end = (pos + CHUNK).min(exact_end);
10030 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10031 if self
10032 .graph_failed
10033 .swap(false, std::sync::atomic::Ordering::Relaxed)
10034 {
10035 self.cancel
10036 .store(false, std::sync::atomic::Ordering::Relaxed);
10037 self.nll_end();
10038 return Err("GPU graph failed during O(1) NLL prefix".into());
10039 }
10040 for row in 0..end - pos {
10041 let score_pos = pos + row;
10042 if score_pos >= requested_start && score_pos < n {
10043 nll += self.nll_from_hidden(
10044 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10045 ids[score_pos + 1],
10046 score_pos,
10047 );
10048 cnt += 1;
10049 }
10050 }
10051 pos = end;
10052 }
10053 } else {
10054 while pos < exact_end {
10055 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10056 if self
10057 .graph_failed
10058 .swap(false, std::sync::atomic::Ordering::Relaxed)
10059 {
10060 self.cancel
10061 .store(false, std::sync::atomic::Ordering::Relaxed);
10062 self.nll_end();
10063 return Err("GPU graph failed during O(1) NLL prefix".into());
10064 }
10065 if pos >= requested_start && pos < n {
10066 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10067 cnt += 1;
10068 }
10069 pos += 1;
10070 }
10071 }
10072 self.o1_seal_checked().map_err(|err| {
10073 self.nll_end();
10074 err
10075 })?;
10076
10077 let batch_k = std::env::var("CMF_BATCH_K")
10086 .ok()
10087 .and_then(|v| v.parse::<usize>().ok())
10088 .unwrap_or(0);
10089 let batch_admitted = batch_k > 0
10090 && self.can_prefill_batched()
10091 && self.o1_active()
10092 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10093 && (0..self.num_layers).all(|li| {
10094 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10095 cache.o1.is_none() || cache.o1_views().is_some()
10096 });
10097 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10098 eprintln!(
10099 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10100 batch_admitted,
10101 batch_k,
10102 n.saturating_sub(exact_end),
10103 );
10104 }
10105 let mut batch_completed = false;
10106 if batch_admitted && exact_end < n {
10107 let hs = self.hidden_size;
10108 let mut batch_pos = exact_end;
10109 while batch_pos < n {
10110 let end = (batch_pos + batch_k).min(n);
10111 let bk = end - batch_pos;
10112 let mut hiddens = vec![0.0f32; bk * hs];
10113 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10114 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10115 }
10116 let positions: Vec<usize> = (batch_pos..end).collect();
10117 let t_batch = std::time::Instant::now();
10118 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10119 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10120 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10121 eprintln!(
10122 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10123 batch_pos,
10124 end.saturating_sub(1),
10125 bk as f64 / (ms / 1000.0),
10126 );
10127 }
10128 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10129 self.nll_end();
10130 return Err(err);
10131 }
10132 match outcome {
10133 crate::gpu::BatchGraphOutcome::Completed => {
10134 batch_completed = true;
10135 for row in 0..bk {
10136 nll += self.nll_from_hidden(
10137 &hiddens[row * hs..(row + 1) * hs],
10138 ids[batch_pos + row + 1],
10139 batch_pos + row,
10140 );
10141 cnt += 1;
10142 }
10143 batch_pos = end;
10144 }
10145 crate::gpu::BatchGraphOutcome::Declined => {
10146 if batch_completed {
10147 self.nll_end();
10148 return Err(format!(
10149 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10150 ));
10151 }
10152 break;
10153 }
10154 crate::gpu::BatchGraphOutcome::Failed => {
10155 self.nll_end();
10156 return Err(format!(
10157 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10158 ));
10159 }
10160 }
10161 }
10162 if batch_completed && cnt == n.saturating_sub(requested_start) {
10163 self.nll_end();
10164 return Ok((nll, cnt));
10165 }
10166 }
10167
10168 for pos in exact_end..n {
10173 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10174 if self
10175 .graph_failed
10176 .swap(false, std::sync::atomic::Ordering::Relaxed)
10177 {
10178 self.cancel
10179 .store(false, std::sync::atomic::Ordering::Relaxed);
10180 self.nll_end();
10181 return Err(format!(
10182 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10183 ));
10184 }
10185 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10186 cnt += 1;
10187 }
10188 self.nll_end();
10189 Ok((nll, cnt))
10190 }
10191
10192 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10200 self.clear_sequence_state();
10201 let n = ids.len().saturating_sub(1);
10202 let mut correct = Vec::with_capacity(n);
10203 let mut pmax = Vec::with_capacity(n);
10204 for pos in 0..n {
10205 let emb = self.embed_single(ids[pos]);
10206 let hidden = self.forward_layers(&emb, pos, None);
10207 let logits = if let Some(logits) = self.graph_logits.take() {
10208 logits
10209 } else {
10210 let normed = inference::rms_norm(
10211 &hidden,
10212 &self.weights.final_norm,
10213 self.rms_eps,
10214 self.norm_style,
10215 );
10216 self.lm_head_forward(&normed)
10220 };
10221 let target = ids[pos + 1] as usize;
10222 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10223 for (i, &v) in logits.iter().enumerate() {
10224 if v > mval {
10225 mval = v;
10226 amax = i;
10227 }
10228 }
10229 correct.push(amax == target);
10230 let row: Vec<f32> = temps
10231 .iter()
10232 .map(|&t| {
10233 let tt = t.max(1e-3);
10234 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10235 1.0 / s.max(1e-12) })
10237 .collect();
10238 pmax.push(row);
10239 }
10240 self.clear_sequence_state();
10241 (correct, pmax)
10242 }
10243
10244 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10251 if self.dyn_router.is_none() {
10252 return Ok((self.ppl_ids(ids)?, 0));
10253 }
10254 self.nll_begin()?;
10255 let saved_active = self.dyn_active;
10256 let mut router = self
10257 .dyn_router
10258 .take()
10259 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10260 router.reset();
10261 self.dyn_phi_seen = 0;
10262 let _ = self.set_active_skill(None);
10263
10264 let result: Result<(f64, usize), String> = (|| {
10265 let mut nll = 0f64;
10266 let mut cnt = 0usize;
10267 for pos in 0..ids.len().saturating_sub(1) {
10268 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10269 self.nll_check_graph("dynamic serial forward", pos)?;
10270 let out_of_band = self.graph_logits.take();
10271 let mut logits = match out_of_band {
10272 Some(lg) => lg,
10273 None => {
10274 let normed = inference::rms_norm(
10275 &hidden,
10276 &self.weights.final_norm,
10277 self.rms_eps,
10278 self.norm_style,
10279 );
10280 self.lm_head_forward(&normed)
10284 }
10285 };
10286 let target = ids[pos + 1] as usize;
10287 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10288 let lse: f64 = logits
10289 .iter()
10290 .map(|&v| ((v - max) as f64).exp())
10291 .sum::<f64>()
10292 .ln()
10293 + max as f64;
10294 let tok_nll = lse - logits[target] as f64;
10295 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10296 let top = logits
10297 .iter()
10298 .enumerate()
10299 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10300 .map(|(i, _)| i)
10301 .unwrap_or(0);
10302 eprintln!(
10303 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10304 logits[target], logits[top]
10305 );
10306 }
10307 nll += tok_nll;
10308 cnt += 1;
10309 attention::recycle_buf(&mut logits);
10310 let phi = self.dyn_phi_ema.clone();
10312 if let Some(new_active) = router.step(&phi, pos) {
10313 let _ = self.set_active_skill(new_active);
10314 }
10315 }
10316 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10317 })();
10318
10319 let _ = self.set_active_skill(saved_active);
10322 self.dyn_router = Some(router);
10323 self.nll_end();
10324 result
10325 }
10326
10327 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10329 self.clear_sequence_state();
10330 let mut acc = vec![0f32; self.hidden_size];
10331 for (pos, &id) in ids.iter().enumerate() {
10332 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10333 for (a, v) in acc.iter_mut().zip(&h) {
10334 *a += v;
10335 }
10336 }
10337 let n = ids.len().max(1) as f32;
10338 for a in acc.iter_mut() {
10339 *a /= n;
10340 }
10341 self.clear_sequence_state();
10342 acc
10343 }
10344
10345 pub fn probe_phi_span(
10359 &mut self,
10360 ids: &[u32],
10361 layer: usize,
10362 span: std::ops::Range<usize>,
10363 ) -> Vec<f32> {
10364 #[cfg(target_os = "macos")]
10365 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10366 let end = span.end.min(ids.len());
10367 let start = span.start.min(end);
10368 let reset = |p: &mut Self| p.clear_sequence_state();
10369 reset(self);
10370 let mut acc = vec![0f32; self.hidden_size];
10371 for (pos, &id) in ids[..end].iter().enumerate() {
10372 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10373 if pos >= start {
10374 for (a, v) in acc.iter_mut().zip(&h) {
10375 *a += v;
10376 }
10377 }
10378 }
10379 let n = end - start;
10380 if n > 0 {
10381 let n = n as f32;
10382 for a in acc.iter_mut() {
10383 *a /= n;
10384 }
10385 }
10386 reset(self);
10387 acc
10388 }
10389
10390 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10397 #[cfg(target_os = "macos")]
10398 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10399 self.graph_logits = None;
10400 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10401 if let Some(logits) = self.graph_logits.take() {
10402 return logits;
10403 }
10404 inference::rms_norm_into(
10405 &hidden,
10406 &self.weights.final_norm,
10407 self.rms_eps,
10408 self.norm_style,
10409 &mut self.ws.n1,
10410 );
10411 self.lm_head_forward(&self.ws.n1)
10412 }
10413
10414 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10420 self.prefill_batch_masked(ids, start_pos, None)
10421 }
10422
10423 fn prefill_batch_masked(
10429 &mut self,
10430 ids: &[u32],
10431 start_pos: usize,
10432 task_mask: Option<&TaskMask>,
10433 ) -> Vec<f32> {
10434 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10435 }
10436
10437 fn prefill_rows(
10445 &mut self,
10446 ids: &[u32],
10447 pos: usize,
10448 task_mask: Option<&TaskMask>,
10449 ) -> Result<Vec<f32>, String> {
10450 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10451 }
10452
10453 fn prefill_input_rows(
10454 &mut self,
10455 input: PrefillIn<'_>,
10456 pos: usize,
10457 task_mask: Option<&TaskMask>,
10458 ) -> Result<Vec<f32>, String> {
10459 self.mimo_moe_prepare();
10460 let hs = self.hidden_size;
10461 let bk = match input {
10462 PrefillIn::Ids(ids) => ids.len(),
10463 PrefillIn::Hidden(rows) => rows.len() / hs,
10464 };
10465 #[cfg(not(target_os = "macos"))]
10466 if task_mask.is_none()
10467 && !self.o1_active()
10468 && bk > 1
10469 && (self.batch_prefix_prefill()
10470 || (self.verify_exact_moe
10471 && crate::gpu::enabled_here()
10472 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10473 {
10474 let mut hiddens = match input {
10475 PrefillIn::Hidden(rows) => rows.to_vec(),
10476 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10477 };
10478 let positions: Vec<usize> = (pos..pos + bk).collect();
10479 let mut run = 0usize;
10480 match self.try_batch_graph_wgpu_prefix(
10481 &mut hiddens,
10482 &positions,
10483 bk,
10484 None,
10485 Some(&mut run),
10486 ) {
10487 crate::gpu::BatchGraphOutcome::Completed => {
10488 let out = if run < self.num_layers {
10489 self.prefill_batch_span(
10490 PrefillIn::Hidden(&hiddens),
10491 pos,
10492 None,
10493 run,
10494 self.num_layers,
10495 )
10496 } else {
10497 hiddens
10498 };
10499 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10500 Err("MiMo attention graph failed after admission".into())
10501 } else {
10502 Ok(out)
10503 };
10504 }
10505 crate::gpu::BatchGraphOutcome::Failed => {
10506 return Err("batched prefix prefill failed after admission".into());
10507 }
10508 crate::gpu::BatchGraphOutcome::Declined => {
10509 #[cfg(feature = "gpu")]
10511 self.pull_lagging_host_kv(0, self.num_layers, pos);
10512 }
10513 }
10514 }
10515 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10516 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10517 Err("batch tail graph failed after admission".into())
10518 } else {
10519 Ok(out)
10520 }
10521 }
10522
10523 fn prefill_batch_span(
10529 &mut self,
10530 input: PrefillIn<'_>,
10531 start_pos: usize,
10532 task_mask: Option<&TaskMask>,
10533 from: usize,
10534 upto_excl: usize,
10535 ) -> Vec<f32> {
10536 let hs = self.hidden_size;
10537 let b = match input {
10538 PrefillIn::Ids(ids) => ids.len(),
10539 PrefillIn::Hidden(hb) => hb.len() / hs,
10540 };
10541 let upto_excl = upto_excl.min(self.num_layers);
10542 let mut h: Vec<f32>;
10546 let mut h_ready;
10547 match input {
10548 PrefillIn::Ids(_) => {
10549 h = vec![0.0; b * hs];
10550 h_ready = false;
10551 }
10552 PrefillIn::Hidden(hb) => {
10553 h = hb.to_vec();
10554 h_ready = true;
10555 }
10556 }
10557 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10558 if let PrefillIn::Ids(ids) = input {
10559 for (bi, &id) in ids.iter().enumerate() {
10560 let e = me.embed_single(id);
10561 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10562 }
10563 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10564 if let Ok(t) = tp.parse::<usize>() {
10565 if t >= start_pos && t < start_pos + ids.len() {
10566 let bi = t - start_pos;
10567 let row = &h[bi * hs..(bi + 1) * hs];
10568 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10569 eprintln!(
10570 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10571 ids[bi],
10572 row[0],
10573 row[1],
10574 ids.len(),
10575 &ids[..ids.len().min(8)]
10576 );
10577 }
10578 }
10579 }
10580 }
10581 };
10582 let (_nkv, _hd, _rd, eps) = (
10583 self.num_kv_heads,
10584 self.head_dim,
10585 self.rotary_dim,
10586 self.rms_eps,
10587 );
10588 let pool = self.pool.clone();
10589 let norm_style = self.norm_style;
10590 self.mimo_moe_prepare();
10591 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10592
10593 #[cfg(target_os = "macos")]
10594 let mut chunk_skip_until = 0usize;
10595 for li in from..upto_excl {
10596 let _capacity_tail = automatic_gpu_prefix
10597 .filter(|&prefix| {
10598 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10599 })
10600 .map(|_| crate::gpu::enter_cpu_scope());
10601 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10606 let _fast_gemm = self
10607 .proj_gate_sigmoid
10608 .then(crate::gpu::enter_prefill_fast_gemm);
10609 #[cfg(target_os = "macos")]
10615 if task_mask.is_none() {
10616 if li < chunk_skip_until {
10617 continue;
10618 }
10619 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10625 fill_h(&mut h, self);
10626 h_ready = true;
10627 }
10628 let ids_for_embed = match input {
10629 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10630 PrefillIn::Hidden(_) => None,
10631 };
10632 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10633 if end > li {
10634 h_ready = true;
10635 chunk_skip_until = end;
10636 if self.is_loop_end(end - 1) && end < self.num_layers {
10639 for bi in 0..b {
10640 let normed = inference::rms_norm(
10641 &h[bi * hs..(bi + 1) * hs],
10642 &self.weights.final_norm,
10643 eps,
10644 norm_style,
10645 );
10646 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10647 }
10648 }
10649 continue;
10650 }
10651 }
10652 if !h_ready {
10653 fill_h(&mut h, self);
10654 h_ready = true;
10655 }
10656 if task_mask.is_none() && self.verify_exact_moe {
10657 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10658 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10659 crate::gpu::BatchGraphOutcome::Completed => continue,
10660 crate::gpu::BatchGraphOutcome::Failed => return h,
10661 crate::gpu::BatchGraphOutcome::Declined => {}
10662 }
10663 }
10664 #[cfg(feature = "gpu")]
10665 self.pull_lagging_host_kv(li, li + 1, start_pos);
10666 let lw = &self.weights.layers[self.phys_layer(li)];
10667 let t_attn = std::time::Instant::now();
10668 match &lw.attn {
10670 AttnKind::Kda(w) => {
10671 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10673 let mut normed = vec![0.0f32; b * hs];
10674 for bi in 0..b {
10675 inference::rms_norm_into(
10676 &h[bi * hs..(bi + 1) * hs],
10677 &lw.input_norm,
10678 eps,
10679 norm_style,
10680 &mut normed[bi * hs..(bi + 1) * hs],
10681 );
10682 }
10683 let attn = crate::linear_core::kda_forward_batch(
10684 &normed,
10685 b,
10686 w,
10687 &cfg,
10688 &mut self.kv_cache.layers[li].linear_state,
10689 pool.as_deref(),
10690 );
10691 for (dst, &a) in h.iter_mut().zip(&attn) {
10692 *dst += a;
10693 }
10694 }
10695 AttnKind::LinearGdn(w) => {
10696 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10698 let mut normed = vec![0.0f32; b * hs];
10699 for bi in 0..b {
10700 let r = inference::rms_norm(
10701 &h[bi * hs..(bi + 1) * hs],
10702 &lw.input_norm,
10703 eps,
10704 norm_style,
10705 );
10706 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10707 }
10708 let attn = crate::linear_core::gdn_forward_batch(
10709 &normed,
10710 b,
10711 w,
10712 &cfg,
10713 &mut self.kv_cache.layers[li].linear_state,
10714 pool.as_deref(),
10715 );
10716 for (dst, &a) in h.iter_mut().zip(&attn) {
10717 *dst += a;
10718 }
10719 }
10720 AttnKind::ShortConv(w) => {
10721 let cfg = self
10724 .short_conv_cfg
10725 .expect("short-conv layer without short_conv_cfg");
10726 let mut normed = vec![0.0f32; b * hs];
10727 for bi in 0..b {
10728 inference::rms_norm_into(
10729 &h[bi * hs..(bi + 1) * hs],
10730 &lw.input_norm,
10731 eps,
10732 norm_style,
10733 &mut normed[bi * hs..(bi + 1) * hs],
10734 );
10735 }
10736 let attn = short_conv_forward_batch(
10737 &normed,
10738 b,
10739 w,
10740 &cfg,
10741 &mut self.kv_cache.layers[li].linear_state,
10742 pool.as_deref(),
10743 );
10744 for (dst, &a) in h.iter_mut().zip(&attn) {
10745 *dst += a;
10746 }
10747 }
10748 AttnKind::Mla(w) => {
10749 let inv_freq_l = self.layer_inv_freq(li);
10752 let rs = self.layer_rope_scale(li);
10753 let mut normed = vec![0.0f32; hs];
10754 for bi in 0..b {
10755 inference::rms_norm_into(
10756 &h[bi * hs..(bi + 1) * hs],
10757 &lw.input_norm,
10758 eps,
10759 norm_style,
10760 &mut normed,
10761 );
10762 let ao = mla_attention(
10763 w,
10764 &normed,
10765 &mut self.kv_cache.layers[li],
10766 start_pos + bi,
10767 &inv_freq_l,
10768 rs,
10769 eps,
10770 pool.as_deref(),
10771 );
10772 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10773 *dst += a;
10774 }
10775 }
10776 }
10777 AttnKind::Full {
10778 wq,
10779 wk,
10780 wv,
10781 wo,
10782 q_norm,
10783 k_norm,
10784 output_gate,
10785 softplus_gate,
10786 bias,
10787 } => {
10788 let mut normed = vec![0.0f32; b * hs];
10792 for bi in 0..b {
10793 inference::rms_norm_into(
10794 &h[bi * hs..(bi + 1) * hs],
10795 &lw.input_norm,
10796 eps,
10797 norm_style,
10798 &mut normed[bi * hs..(bi + 1) * hs],
10799 );
10800 }
10801 let inv_freq_l = self.layer_inv_freq(li);
10802 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10803 let cfg = QwenAttnCfg {
10804 num_heads: self.layer_num_heads(li),
10805 num_kv_heads: nkv_l,
10806 head_dim: hd_l,
10807 hidden_size: hs,
10808 position: start_pos,
10809 inv_freq: &inv_freq_l,
10810 rotary_dim: rd_l,
10811 scale: self.attn_scale,
10812 softcap: self.attn_softcap,
10813 window: self.layer_window(li),
10814 v_norm: self.attn_v_norm,
10815 qk_norm_after_rope: self.qk_norm_after_rope,
10816 gate_sigmoid: self.proj_gate_sigmoid,
10817 q_norm: q_norm.as_deref(),
10818 k_norm: k_norm.as_deref(),
10819 output_gate: *output_gate,
10820 softplus_gate: softplus_gate
10821 .as_ref()
10822 .map(|(gate, per_head)| (gate, *per_head)),
10823 rope_scale: self.layer_rope_scale(li),
10824 bias: bias
10825 .as_ref()
10826 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10827 rms_eps: eps,
10828 norm_style,
10829 pool: pool.as_deref(),
10830 v_head_dim: self.layer_v_dim(li),
10831 };
10832 #[cfg(feature = "gpu")]
10833 let _mirror = self
10834 .prefill_mirror_target(li)
10835 .map(crate::gpu::enter_prefill_mirror);
10836 let mut attn = attention::qwen_attention_batch(
10837 &normed,
10838 b,
10839 wq,
10840 wk,
10841 wv,
10842 wo,
10843 &mut self.kv_cache.layers[li],
10844 &cfg,
10845 );
10846 if let Some(w) = &lw.attn_out_norm {
10847 for bi in 0..b {
10848 inference::rms_norm_into(
10849 &attn[bi * hs..(bi + 1) * hs],
10850 w,
10851 eps,
10852 norm_style,
10853 &mut normed[bi * hs..(bi + 1) * hs],
10854 );
10855 }
10856 attn.copy_from_slice(&normed);
10857 }
10858 for (dst, &a) in h.iter_mut().zip(&attn) {
10859 *dst += a;
10860 }
10861 }
10862 AttnKind::Bounded(w) => {
10863 let mut normed = vec![0.0f32; b * hs];
10866 for bi in 0..b {
10867 inference::rms_norm_into(
10868 &h[bi * hs..(bi + 1) * hs],
10869 &lw.input_norm,
10870 eps,
10871 norm_style,
10872 &mut normed[bi * hs..(bi + 1) * hs],
10873 );
10874 }
10875 let rope = self
10876 .bounded_rope
10877 .clone()
10878 .expect("bounded layer without an installed rotation table");
10879 let cfg = crate::bounded::BoundedAttnCfg {
10880 num_heads: self.num_heads,
10881 num_kv_heads: self.num_kv_heads,
10882 head_dim: self.head_dim,
10883 hidden_size: hs,
10884 scale: self.attn_scale,
10885 rope: &rope,
10886 pool: pool.as_deref(),
10887 };
10888 let mut attn = crate::bounded::bounded_attention_batch(
10889 &normed,
10890 b,
10891 w,
10892 &mut self.kv_cache.layers[li],
10893 &cfg,
10894 );
10895 if let Some(wn) = &lw.attn_out_norm {
10896 for bi in 0..b {
10897 inference::rms_norm_into(
10898 &attn[bi * hs..(bi + 1) * hs],
10899 wn,
10900 eps,
10901 norm_style,
10902 &mut normed[bi * hs..(bi + 1) * hs],
10903 );
10904 }
10905 attn.copy_from_slice(&normed);
10906 }
10907 for (dst, &a) in h.iter_mut().zip(&attn) {
10908 *dst += a;
10909 }
10910 attention::recycle_buf(&mut attn);
10911 }
10912 AttnKind::Linear(w) => {
10913 for bi in 0..b {
10914 let normed = inference::rms_norm(
10915 &h[bi * hs..(bi + 1) * hs],
10916 &lw.input_norm,
10917 eps,
10918 norm_style,
10919 );
10920 vmf_phase_forward(
10921 &normed,
10922 w,
10923 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10924 &mut self.kv_cache.layers[li].linear_state,
10925 pool.as_deref(),
10926 )
10927 .iter()
10928 .enumerate()
10929 .for_each(|(i, &a)| h[bi * hs + i] += a);
10930 }
10931 }
10932 }
10933
10934 let lw = &self.weights.layers[self.phys_layer(li)];
10936 let mut post = vec![0.0f32; b * hs];
10937 for bi in 0..b {
10938 let r =
10939 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10940 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10941 }
10942 let mask_row = task_mask
10945 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10946 .and_then(|m| m.ffn_masks.get(li))
10947 .map(|v| v.as_slice());
10948 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10949 let t_ffn = std::time::Instant::now();
10950 let mut ffn = match &lw.ffn {
10951 FfnKind::Dense(d) if !d.segs.is_empty() => {
10952 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10953 }
10954 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10955 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10956 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10957 }
10958 FfnKind::Moe(m) if self.verify_exact_moe => {
10959 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10960 }
10961 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10964 let before = m.stats.borrow().clone();
10965 let out = crate::gpu::cpu_scope(|| {
10966 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10967 });
10968 self.mimo_moe.prime(li, m, &before);
10969 out
10970 }
10971 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10972 FfnKind::DenseMoe(dm) => {
10975 let mut out = vec![0.0f32; b * hs];
10976 for bi in 0..b {
10977 let r = dense_moe_ffn(
10978 dm,
10979 &post[bi * hs..(bi + 1) * hs],
10980 &h[bi * hs..(bi + 1) * hs],
10981 eps,
10982 norm_style,
10983 pool.as_deref(),
10984 );
10985 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10986 }
10987 out
10988 }
10989 };
10990 if prefill_prof_on() {
10991 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10992 PREFILL_SPLIT[1].fetch_add(
10993 t_ffn.elapsed().as_nanos() as u64,
10994 std::sync::atomic::Ordering::Relaxed,
10995 );
10996 }
10997 if let Some(w) = &lw.ffn_out_norm {
10998 for bi in 0..b {
10999 inference::rms_norm_into(
11000 &ffn[bi * hs..(bi + 1) * hs],
11001 w,
11002 eps,
11003 norm_style,
11004 &mut post[bi * hs..(bi + 1) * hs],
11005 );
11006 }
11007 ffn.copy_from_slice(&post);
11008 }
11009 for (dst, &f) in h.iter_mut().zip(&ffn) {
11010 *dst += f;
11011 }
11012 if let Some(sc) = lw.layer_scale {
11013 for v in h.iter_mut() {
11014 *v *= sc;
11015 }
11016 }
11017 if self.layer_dump.is_some() {
11019 for bi in 0..b {
11020 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11021 }
11022 }
11023 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11024 if let Ok(t) = tp.parse::<usize>() {
11025 if t >= start_pos && t < start_pos + b {
11026 let bi = t - start_pos;
11027 let row = &h[bi * hs..(bi + 1) * hs];
11028 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11029 eprintln!(
11030 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11031 row[0], row[1]
11032 );
11033 }
11034 }
11035 }
11036 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11040 let row = &h[(b - 1) * hs..b * hs];
11041 let rms =
11042 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11043 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11044 eprintln!(
11045 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11046 match &self.weights.layers[self.phys_layer(li)].attn {
11047 AttnKind::LinearGdn(_) => "gdn",
11048 AttnKind::Linear(_) => "vmf",
11049 AttnKind::ShortConv(_) => "conv",
11050 _ => "attn",
11051 },
11052 match &lw.ffn {
11053 FfnKind::Moe(_) => "moe",
11054 FfnKind::Dense(_) => "dense",
11055 FfnKind::DenseMoe(_) => "dense+moe",
11056 },
11057 );
11058 }
11059 if self.is_loop_end(li) && li + 1 < self.num_layers {
11061 for bi in 0..b {
11062 let normed = inference::rms_norm(
11063 &h[bi * hs..(bi + 1) * hs],
11064 &self.weights.final_norm,
11065 eps,
11066 norm_style,
11067 );
11068 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11069 }
11070 }
11071 if std::env::var("CMF_TRACE_H").is_ok() {
11072 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11073 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11074 eprintln!(
11075 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11076 lw.layer_scale
11077 );
11078 }
11079 }
11080 crate::gpu::set_layer(-1); if prefill_prof_on() {
11082 let ms = |a: &std::sync::atomic::AtomicU64| {
11083 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11084 };
11085 let sp = &attention::ATTN_SPLIT;
11086 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11087 eprintln!(
11088 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11089 host gate {:.1}], host loop {:.1}, attend {:.1} \
11090 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11091 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11092 ms(&PREFILL_SPLIT[0]),
11093 ms(&sp[0]),
11094 qkv_enq,
11095 qkv_wait,
11096 ms(&sp[6]),
11097 ms(&sp[1]),
11098 ms(&sp[2]),
11099 ms(&sp[4]),
11100 ms(&sp[5]),
11101 ca_wait,
11102 ms(&sp[3]),
11103 ms(&PREFILL_SPLIT[1]),
11104 );
11105 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11106 eprintln!(
11107 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11108 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11109 fallbacks (cumulative)",
11110 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11111 );
11112 }
11113 self.o1_progress();
11118 self.swa_trim_tails();
11120 h
11121 }
11122
11123 fn embed_single(&self, id: u32) -> Vec<f32> {
11125 let mut out = vec![0.0f32; self.hidden_size];
11126 if (id as usize) < self.weights.embed_tokens.rows() {
11127 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11128 }
11129 if self.embed_multiplier != 1.0 {
11130 for v in out.iter_mut() {
11131 *v *= self.embed_multiplier;
11132 }
11133 }
11134 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11138 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11139 v[0] = id as f32;
11140 return v;
11141 }
11142 if let Some(b) = &self.g4e {
11146 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11147 }
11148 if let Some(b) = &self.g3n {
11149 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11150 }
11151 out
11152 }
11153
11154 #[cfg(target_os = "macos")]
11160 fn chunk_run_gpu(
11161 &mut self,
11162 li0: usize,
11163 h: &mut [f32],
11164 b: usize,
11165 pos0: usize,
11166 embed_ids: Option<&[u32]>,
11167 cap: usize,
11168 ) -> usize {
11169 let gemma4 =
11176 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11177 if !crate::gpu::enabled_here()
11178 || std::env::var("CMF_GPU_CHUNK")
11179 .map(|v| v == "0")
11180 .unwrap_or(false)
11181 || b < 32
11182 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11185 || (self.global_attn.is_some() && !gemma4)
11186 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11190 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11193 || self.o1_active()
11196 || (self.attn_v_norm && !gemma4)
11197 || !self.metal_rope_scale_ok()
11198 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11199 {
11200 return li0;
11201 }
11202 let Some(model) = self.model.clone() else {
11203 return li0;
11204 };
11205 let (nh, nkv, hd, hs) = (
11206 self.num_heads,
11207 self.num_kv_heads,
11208 self.head_dim,
11209 self.hidden_size,
11210 );
11211 let loop_end = if self.loop_final_norm {
11215 ((li0 / self.physical_layers) + 1) * self.physical_layers
11216 } else {
11217 self.num_layers
11218 };
11219 let run_end = self.num_layers.min(loop_end).min(cap);
11220 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11225 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11226 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11227 let mut stored_at: Vec<usize> = Vec::new();
11228 for li in li0..run_end {
11229 let lw = &self.weights.layers[self.phys_layer(li)];
11230 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11231 && !gemma4
11232 {
11233 break;
11234 }
11235 let AttnKind::Full {
11236 wq,
11237 wk,
11238 wv,
11239 wo,
11240 q_norm,
11241 k_norm,
11242 output_gate: false,
11243 softplus_gate,
11244 bias,
11245 } = &lw.attn
11246 else {
11247 break;
11248 };
11249 let head_gate = match softplus_gate {
11251 None => None,
11252 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11253 Some((d, r, c)) if r == nh && c == hs => Some(d),
11254 _ => break,
11255 },
11256 Some(_) => break,
11257 };
11258 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11262 FfnKind::Dense(d)
11263 if (matches!(d.act, Act::Silu | Act::Gelu)
11264 || (gemma4 && d.act == Act::GeluTanh))
11265 && d.segs.is_empty() =>
11266 {
11267 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11268 }
11269 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11270 Some((router, experts, q8)) => {
11271 let e0 = &m.experts[0];
11272 let route = Box::new(move |lg: &[f32]| {
11273 let (idx, p, wsum) = moe_route(lg, m, None);
11275 let mut st = m.stats.borrow_mut();
11276 if st.len() < m.experts.len() {
11277 st.resize(m.experts.len(), 0);
11278 }
11279 for &e in &idx {
11280 st[e] += 1;
11281 }
11282 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11283 (idx, w)
11284 });
11285 (
11286 &e0.gate_proj,
11287 &e0.up_proj,
11288 &e0.down_proj,
11289 Act::Silu,
11290 Some(crate::gpu_metal::ChunkMoe {
11291 router,
11292 experts,
11293 q8,
11294 route,
11295 }),
11296 )
11297 }
11298 None => break,
11299 },
11300 _ => break,
11301 };
11302 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11308 t.q8_row_parts()
11309 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11310 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11311 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11312 }
11313 fn cf(t: &QTensor) -> &[f32] {
11314 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11315 }
11316 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11317 if moe.is_none()
11326 && !gemma4
11327 && (col.iter().any(|c| !c.is_empty())
11328 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
11329 {
11330 break;
11331 }
11332 let parts = (
11333 cw(wq),
11334 cw(wk),
11335 cw(wv),
11336 cw(wo),
11337 cw(gate_t),
11338 cw(up_t),
11339 cw(down_t),
11340 );
11341 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11342 else {
11343 break;
11344 };
11345 let layer = &self.kv_cache.layers[li];
11346 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11347 break;
11348 }
11349 debug_assert!(
11351 layer.base() == 0
11352 || self
11353 .layer_window(li)
11354 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11355 );
11356 stored_at.push(layer.head_len(0));
11357 let (nkv, hd) = if gemma4 {
11360 let (a, bb, _) = self.layer_geom(li);
11361 (a, bb)
11362 } else {
11363 (nkv, hd)
11364 };
11365 layers.push(crate::gpu_metal::ChunkLayer {
11366 model: &model,
11367 kv_id: self.graph_kv_id,
11368 layer: li,
11369 wq: pq,
11370 wk: pk,
11371 wv: pv,
11372 wo: po,
11373 gate: pg,
11374 up: pu,
11375 down: pd,
11376 input_norm: &lw.input_norm,
11377 post_norm: &lw.post_norm,
11378 bias: bias
11379 .as_ref()
11380 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11381 q_norm: q_norm.as_deref(),
11382 k_norm: k_norm.as_deref(),
11383 inv_freq: &tables[li - li0],
11384 rope_scale: self.layer_rope_scale(li),
11385 rd: self.layer_geom(li).2,
11386 nh,
11387 nkv,
11388 hd,
11389 hs,
11390 inter: gate_t.rows(),
11391 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11392 late_qk_norm: self.qk_norm_after_rope,
11393 eps: self.rms_eps as f32,
11394 window: self.layer_window(li),
11395 head_gate,
11396 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11397 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11398 v_norm: self.attn_v_norm,
11399 scale: self.attn_scale,
11400 attn_out_norm: lw.attn_out_norm.as_deref(),
11401 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11402 layer_scale: lw.layer_scale,
11403 }),
11404 col,
11405 moe,
11406 });
11407 }
11408 if layers.is_empty() {
11409 return li0;
11410 }
11411 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11413 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11414 .iter()
11415 .zip(&rows_kv)
11416 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11417 .collect();
11418 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11419 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11420 let li = layers[i].layer;
11421 let layer = &self.kv_cache.layers[li];
11422 let nkv = rows_kv[i].0;
11423 io.push(crate::gpu_metal::ChunkIo {
11424 cpu_stored: stored_at[i],
11425 cpu_gen: layer.generation(),
11426 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11427 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11428 out_k: ok,
11429 out_v: ov,
11430 imp: oi,
11431 });
11432 }
11433 let n_run = layers.len();
11434 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11435 let ep = embed_ids.and_then(|ids| {
11438 self.weights
11439 .embed_tokens
11440 .q8_row_parts()
11441 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11442 idx,
11443 rows,
11444 row_scale: rs,
11445 ids,
11446 mult: self.embed_multiplier,
11447 })
11448 });
11449 if embed_ids.is_some() && ep.is_none() {
11450 return li0;
11451 }
11452 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11453 return li0;
11454 }
11455 drop(io);
11456 drop(layers);
11457 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11460 let li = li0 + i;
11461 let row = rows_kv[i].1;
11462 let layer = &mut self.kv_cache.layers[li];
11463 for bi in 0..b {
11464 layer.append(
11465 &ok[bi * row..(bi + 1) * row],
11466 &ov[bi * row..(bi + 1) * row],
11467 &[],
11468 );
11469 }
11470 layer.accumulate_imp(oi);
11471 }
11472 last
11473 }
11474
11475 fn layer_is_local(&self, li: usize) -> bool {
11478 if let Some(layers) = &self.sliding_layers {
11479 return layers.get(li).copied().unwrap_or(false);
11480 }
11481 match self.swa {
11482 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11483 None => false,
11484 }
11485 }
11486
11487 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11490 if self.layer_is_local(li) {
11491 if let Some(f) = &self.inv_freq_local {
11492 return f.clone();
11493 }
11494 } else if let Some(f) = &self.inv_freq_global {
11495 return f.clone();
11496 }
11497 self.inv_freq.clone()
11498 }
11499
11500 fn layer_window(&self, li: usize) -> Option<usize> {
11502 self.swa
11503 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11504 }
11505
11506 fn swa_trim_tails(&mut self) {
11515 let Some((slack, align)) = self.swa_trim else {
11516 return;
11517 };
11518 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11519 return;
11520 }
11521 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11522 if let Some(w) = self.layer_window(li) {
11523 self.kv_cache.layers[li].trim_window(w, slack, align);
11524 }
11525 }
11526 }
11527
11528 fn layer_num_heads(&self, li: usize) -> usize {
11529 self.attention_heads_per_layer
11530 .as_ref()
11531 .and_then(|v| v.get(li).copied())
11532 .unwrap_or(self.num_heads)
11533 }
11534
11535 fn layer_rope_scale(&self, li: usize) -> f32 {
11536 if self.layer_is_local(li) {
11537 self.rope_scale_local
11538 } else {
11539 self.rope_scale
11540 }
11541 }
11542
11543 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11546 if !self.layer_is_local(li) {
11547 if let Some((ghd, gkv)) = self.global_attn {
11548 return (gkv, ghd, ghd);
11549 }
11550 }
11551 let rd = if self.layer_is_local(li) {
11552 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11553 } else {
11554 self.rotary_dim
11555 };
11556 let rd = if self
11558 .no_rope_layers
11559 .as_ref()
11560 .and_then(|v| v.get(li))
11561 .copied()
11562 == Some(true)
11563 {
11564 0
11565 } else {
11566 rd
11567 };
11568 (self.layer_num_kv_heads(li), self.head_dim, rd)
11569 }
11570
11571 fn layer_num_kv_heads(&self, li: usize) -> usize {
11574 self.kv_heads_per_layer
11575 .as_ref()
11576 .and_then(|v| v.get(self.phys_layer(li)).copied())
11577 .unwrap_or(self.num_kv_heads)
11578 }
11579
11580 fn layer_v_dim(&self, li: usize) -> usize {
11582 let (_, hd, _) = self.layer_geom(li);
11583 self.v_head_dim.unwrap_or(hd).min(hd)
11584 }
11585
11586 pub fn set_attn_geometry(
11595 &mut self,
11596 kv_heads_per_layer: Option<Vec<usize>>,
11597 v_head_dim: Option<usize>,
11598 ) -> Result<(), String> {
11599 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11600 if self.global_attn.is_some() {
11601 return Err(
11602 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11603 attention geometry"
11604 .into(),
11605 );
11606 }
11607 if self
11608 .weights
11609 .layers
11610 .iter()
11611 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11612 {
11613 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11614 }
11615 }
11616 if let Some(vd) = v_head_dim {
11617 if vd == 0 || vd > self.head_dim {
11618 return Err(format!(
11619 "v_head_dim {vd} must be in 1..={} (head_dim)",
11620 self.head_dim
11621 ));
11622 }
11623 }
11624 if let Some(v) = &kv_heads_per_layer {
11625 if v.len() != self.physical_layers {
11626 return Err(format!(
11627 "kv_heads_per_layer has {} entries, expected {} layers",
11628 v.len(),
11629 self.physical_layers
11630 ));
11631 }
11632 for (li, &nkv) in v.iter().enumerate() {
11633 let is_attn = matches!(
11634 self.weights.layers.get(li).map(|lw| &lw.attn),
11635 Some(AttnKind::Full { .. }) | None
11636 );
11637 if !is_attn {
11638 continue;
11639 }
11640 let nh = self
11641 .attention_heads_per_layer
11642 .as_ref()
11643 .and_then(|h| h.get(li).copied())
11644 .unwrap_or(self.num_heads);
11645 if nkv == 0 || nh % nkv != 0 {
11646 return Err(format!(
11647 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11648 ));
11649 }
11650 }
11651 }
11652 self.kv_heads_per_layer = kv_heads_per_layer;
11653 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11654 if self.kv_heads_per_layer.is_some() {
11655 for li in 0..self.kv_cache.layers.len() {
11656 let full = matches!(
11657 self.weights
11658 .layers
11659 .get(self.phys_layer(li))
11660 .map(|lw| &lw.attn),
11661 Some(AttnKind::Full { .. })
11662 );
11663 let nkv = self.layer_num_kv_heads(li);
11664 let cache = &self.kv_cache.layers[li];
11665 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11666 let sinks = cache.sinks.clone();
11667 self.kv_cache.layers[li] =
11668 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11669 self.kv_cache.layers[li].sinks = sinks;
11670 }
11671 }
11672 }
11673 Ok(())
11674 }
11675
11676 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11680 let Some(lw) = self.weights.layers.get(phys) else {
11681 return Err(format!("sinks for layer {phys}: no such layer"));
11682 };
11683 if !matches!(lw.attn, AttnKind::Full { .. }) {
11684 return Err(format!(
11685 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11686 ));
11687 }
11688 let nh = self
11689 .attention_heads_per_layer
11690 .as_ref()
11691 .and_then(|h| h.get(phys).copied())
11692 .unwrap_or(self.num_heads);
11693 if sinks.len() != nh {
11694 return Err(format!(
11695 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11696 sinks.len()
11697 ));
11698 }
11699 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11700 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11701 }
11702 for li in 0..self.kv_cache.layers.len() {
11703 if self.phys_layer(li) == phys {
11704 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11705 }
11706 }
11707 Ok(())
11708 }
11709
11710 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11720 if self.kv_heads_per_layer.is_some() {
11721 return Some("per-layer KV head counts");
11722 }
11723 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11724 return Some("V heads narrower than Q/K heads");
11725 }
11726 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11727 return Some("learned attention sinks");
11728 }
11729 if self.swa.is_some() || self.sliding_layers.is_some() {
11730 return Some("sliding-window layers");
11731 }
11732 None
11733 }
11734
11735 #[cfg(target_os = "macos")]
11743 fn metal_rope_scale_ok(&self) -> bool {
11744 !(self.qk_norm_after_rope
11745 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11746 }
11747
11748 #[cfg(target_os = "macos")]
11765 fn metal_graph_swa(&self) -> bool {
11766 (self.swa.is_some() || self.sliding_layers.is_some())
11767 && self.attention_heads_per_layer.is_none()
11768 && !self.attn_v_norm
11769 && self.attn_softcap == 0.0
11770 && self.kv_heads_per_layer.is_none()
11771 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11772 && self.global_attn.is_none()
11773 && self.inv_freq_global.is_none()
11774 && self.metal_rope_scale_ok()
11775 && !(0..self.num_layers).any(|li| {
11776 self.layer_is_local(li)
11777 && self.inv_freq_local.is_none()
11778 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11779 })
11780 }
11781
11782 #[cfg(target_os = "macos")]
11790 fn metal_gemma4(&self) -> bool {
11791 crate::gpu_metal::gemma4::enabled()
11792 && (self.global_attn.is_some() || self.attn_v_norm)
11793 && self.attention_heads_per_layer.is_none()
11794 && self.kv_heads_per_layer.is_none()
11795 && self.v_head_dim.is_none()
11796 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11797 && self.attn_softcap == 0.0
11798 && !self.qk_norm_after_rope
11799 && self.metal_rope_scale_ok()
11800 && self
11801 .weights
11802 .layers
11803 .iter()
11804 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11805 && !(0..self.num_layers).any(|li| {
11806 self.layer_is_local(li)
11807 && self.inv_freq_local.is_none()
11808 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11809 })
11810 }
11811
11812 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11819 let geometry_reason = self.graph_attn_decline_reason();
11823 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11824 if has_scaled_rope && geometry_reason.is_none() {
11825 return Some("scaled RoPE without per-layer geometry");
11826 }
11827 if has_scaled_rope && self.qk_norm_after_rope {
11832 return Some("scaled RoPE with post-RoPE Q/K norm");
11833 }
11834 geometry_reason?;
11835 if self.attention_heads_per_layer.is_some() {
11841 return Some("per-layer Q head counts with per-layer geometry");
11842 }
11843 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11847 if !gemma4
11848 && self.weights.layers.iter().any(|lw| {
11849 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11850 })
11851 {
11852 return Some("sandwich norms / layer scale with per-layer geometry");
11853 }
11854 if self.weights.layers.iter().any(|lw| {
11858 matches!(
11859 &lw.attn,
11860 AttnKind::Full {
11861 output_gate: true,
11862 ..
11863 }
11864 )
11865 }) && self.v_head_dim.is_some()
11866 {
11867 return Some("gated attention with V narrower than K");
11868 }
11869 if (0..self.num_layers).any(|li| {
11870 self.layer_is_local(li)
11871 && self.inv_freq_local.is_none()
11872 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11873 }) {
11874 return Some("local rotary width without a local RoPE table");
11875 }
11876 None
11877 }
11878
11879 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11883 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11886 return None;
11887 }
11888 if self.global_attn.is_some() {
11889 return Some("per-layer head width (Gemma-4 global layers)");
11890 }
11891 if self.attn_v_norm {
11892 return Some("V norm");
11893 }
11894 if self.weights.layers.iter().any(|lw| {
11895 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11896 }) {
11897 return Some("sandwich norms / layer scale");
11898 }
11899 None
11900 }
11901
11902 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11907 self.graph_attn_decline_reason()?;
11908 let (nkv, hd, rd) = self.layer_geom(li);
11909 let invf: &[f32] = if self.layer_is_local(li) {
11910 match &self.inv_freq_local {
11911 Some(f) => f.as_slice(),
11912 None => self.inv_freq.as_slice(),
11913 }
11914 } else {
11915 match &self.inv_freq_global {
11916 Some(f) => f.as_slice(),
11917 None => self.inv_freq.as_slice(),
11918 }
11919 };
11920 Some(crate::gpu::GraphAttnGeom {
11921 nkv,
11922 hd,
11923 dv: self.layer_v_dim(li),
11924 rd,
11925 invf,
11926 rope_scale: self.layer_rope_scale(li),
11927 window: self.layer_window(li),
11928 sink: self.kv_cache.layers[li].sinks.as_deref(),
11929 v_norm: self.attn_v_norm,
11930 })
11931 }
11932
11933 #[cfg(feature = "gpu")]
11945 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11946 if !self.proj_gate_sigmoid
11947 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11948 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11949 || self.graph_refused()
11950 || self.o1_active()
11951 || self.attn_softcap > 0.0
11952 || self.wgpu_graph_attn_decline().is_some()
11953 {
11954 return None;
11955 }
11956 let g = self.graph_attn_geom(li)?;
11957 let (nkv, hd, _) = self.layer_geom(li);
11958 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11960 return None;
11961 }
11962 Some(crate::gpu::PrefillMirror {
11963 kv_id: self.graph_kv_id,
11964 layer: li,
11965 limit: self.kv_cache.max_seq_len,
11966 })
11967 }
11968
11969 #[cfg(feature = "gpu")]
11978 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11979 let kv_id = self.graph_kv_id;
11980 for li in from..upto.min(self.num_layers) {
11981 if !matches!(
11982 self.weights.layers[self.phys_layer(li)].attn,
11983 AttnKind::Full { .. }
11984 ) {
11985 continue;
11986 }
11987 let host = self.kv_cache.layers[li].pos_len();
11989 if host >= position {
11990 continue;
11991 }
11992 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11993 continue;
11994 };
11995 let to = dev.min(position);
11996 if to <= host {
11997 continue;
11998 }
11999 let (nkv, hd) = {
12000 let c = &self.kv_cache.layers[li];
12001 (c.num_kv_heads, c.head_dim)
12002 };
12003 let Some((k, v, first_valid)) =
12004 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
12005 else {
12006 continue;
12007 };
12008 let need_from = match self.layer_window(li) {
12011 Some(w) => host.max((position + 1).saturating_sub(w)),
12012 None => host,
12013 };
12014 if first_valid > need_from {
12015 tracing::warn!(
12016 "layer {li}: device KV rows {host}..{to} no longer resident \
12017 (from {first_valid}); host attention will miss them"
12018 );
12019 }
12020 let row = nkv * hd;
12021 let cache = &mut self.kv_cache.layers[li];
12022 for p in 0..to - host {
12023 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12024 }
12025 }
12026 }
12027
12028 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12031 let mut seen = self.graph_declines.borrow_mut();
12032 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12033 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12034 seen.push((site, reason));
12035 }
12036 }
12037
12038 pub fn graph_declines(&self) -> Vec<String> {
12041 self.graph_declines
12042 .borrow()
12043 .iter()
12044 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12045 .collect()
12046 }
12047
12048 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12057 let Some(dir) = &self.layer_dump else {
12058 return;
12059 };
12060 let mut bytes = Vec::with_capacity(row.len() * 4);
12061 for v in row {
12062 bytes.extend_from_slice(&v.to_le_bytes());
12063 }
12064 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12065 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12066 use std::sync::atomic::{AtomicBool, Ordering};
12067 static SAID: AtomicBool = AtomicBool::new(false);
12068 if !SAID.swap(true, Ordering::Relaxed) {
12069 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12070 }
12071 }
12072 }
12073
12074 fn mimo_moe_prepare(&mut self) {
12077 if !self.mimo_moe.is_undecided() {
12078 return;
12079 }
12080 let slot = {
12081 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12082 .filter_map(
12083 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12084 FfnKind::Moe(m) => Some((li, m)),
12085 _ => None,
12086 },
12087 )
12088 .collect();
12089 if layers.is_empty()
12092 || self.physical_layers != self.num_layers
12093 || self.gpu_plan.is_some()
12094 {
12095 crate::mimo_moe::Slot::Off
12096 } else {
12097 let graph_prefix =
12101 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12102 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12103 }
12104 };
12105 self.mimo_moe = slot;
12106 }
12107
12108 #[cfg(test)]
12109 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12110 self.graph_kv_id
12111 }
12112
12113 pub(crate) fn mimo_graph_layer_rows(
12117 &mut self,
12118 li: usize,
12119 h: &mut [f32],
12120 positions: &[usize],
12121 ) -> crate::gpu::BatchGraphOutcome {
12122 use crate::gpu::BatchGraphOutcome as Out;
12123 let b = positions.len();
12124 if !(1..=4).contains(&b)
12125 || h.len() != b * self.hidden_size
12126 || !self.mimo_moe.is_dynamic(li, true)
12127 || !crate::gpu::enabled_here()
12128 || !crate::gpu::wgpu_active()
12129 || self.o1_active()
12130 || self.physical_layers != self.num_layers
12131 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12135 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12136 || self.wgpu_graph_attn_decline().is_some()
12137 {
12138 return Out::Declined;
12139 }
12140 let attn_started = std::time::Instant::now();
12141 let outcome = {
12142 let lw = &self.weights.layers[li];
12143 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12144 return Out::Declined;
12145 }
12146 let FfnKind::Moe(m) = &lw.ffn else {
12147 return Out::Declined;
12148 };
12149 let AttnKind::Full {
12150 wq,
12151 wk,
12152 wv,
12153 wo,
12154 q_norm,
12155 k_norm,
12156 output_gate,
12157 softplus_gate,
12158 bias,
12159 } = &lw.attn
12160 else {
12161 return Out::Declined;
12162 };
12163 if *output_gate || softplus_gate.is_some() {
12164 return Out::Declined;
12165 }
12166 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12167 m.experts
12168 .first()?
12169 .gate_proj
12170 .mapped_q4tp()
12171 .map(|(m, _)| m.clone())
12172 }) else {
12173 return Out::Declined;
12174 };
12175 fn gw<'a>(
12176 t: &'a QTensor,
12177 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12178 ) -> Option<crate::gpu::GraphW<'a>> {
12179 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12180 if m.uid() != owner.uid() || t.has_prism_contract() {
12181 return None;
12182 }
12183 return Some(crate::gpu::GraphW {
12184 idx,
12185 kind,
12186 row_scale: rs,
12187 data: &[],
12188 prism: crate::gpu::GraphPrismOp::None,
12189 affine: false,
12190 });
12191 }
12192 t.as_f32().map(|data| crate::gpu::GraphW {
12193 idx: 0,
12194 kind: 4,
12195 row_scale: &[],
12196 data,
12197 prism: crate::gpu::GraphPrismOp::None,
12198 affine: false,
12199 })
12200 }
12201 let (Some(q), Some(k), Some(v), Some(o)) = (
12202 gw(wq, &model),
12203 gw(wk, &model),
12204 gw(wv, &model),
12205 gw(wo, &model),
12206 ) else {
12207 return Out::Declined;
12208 };
12209 let layer = crate::gpu::GraphLayer {
12210 input_norm: &lw.input_norm,
12211 post_norm: &lw.post_norm,
12212 attn_out_norm: None,
12213 ffn_out_norm: None,
12214 layer_scale: None,
12215 ffn: crate::gpu::GraphFfn::AttentionOnly,
12216 attn: crate::gpu::GraphAttn::Full {
12217 wq: q,
12218 wk: k,
12219 wv: v,
12220 wo: o,
12221 q_norm: q_norm.as_deref(),
12222 k_norm: k_norm.as_deref(),
12223 late_qk_norm: self.qk_norm_after_rope,
12224 bias: bias
12225 .as_ref()
12226 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12227 output_gate: false,
12228 cpu_k: self.kv_cache.layers[li].k_heads(),
12229 cpu_v: self.kv_cache.layers[li].v_heads(),
12230 cpu_base: self.kv_cache.layers[li].base(),
12231 geom: self.graph_attn_geom(li),
12232 head_gate: None,
12233 },
12234 };
12235 let (nkv, hd, rd) = self.layer_geom(li);
12236 crate::gpu::forward_batch_graph_at(
12237 &model,
12238 self.graph_kv_id,
12239 li,
12240 &[layer],
12241 &self.inv_freq,
12242 h,
12243 self.layer_num_heads(li),
12244 nkv,
12245 hd,
12246 rd,
12247 self.hidden_size,
12248 1,
12249 positions,
12250 self.kv_cache.max_seq_len,
12251 self.norm_style == cortiq_core::NormStyle::Gemma,
12252 self.rms_eps as f32,
12253 self.attn_scale,
12254 b,
12255 &[],
12256 self.o1_epoch,
12257 None,
12258 None,
12259 )
12260 };
12261 match outcome {
12262 Out::Completed => {}
12263 Out::Declined => return Out::Declined,
12264 Out::Failed => {
12265 self.graph_failed
12266 .store(true, std::sync::atomic::Ordering::Relaxed);
12267 return Out::Failed;
12268 }
12269 }
12270 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12271 let hs = self.hidden_size;
12272 let lw = &self.weights.layers[li];
12273 let FfnKind::Moe(m) = &lw.ffn else {
12274 unreachable!()
12275 };
12276 let mut post = vec![0.0; h.len()];
12277 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12278 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12279 }
12280 let mut ffn = if b == 1 {
12281 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12282 } else {
12283 moe_ffn_banked_rows(
12284 &mut self.mimo_moe,
12285 li,
12286 m,
12287 &post,
12288 b,
12289 hs,
12290 self.pool.as_deref(),
12291 )
12292 };
12293 for (x, &f) in h.iter_mut().zip(&ffn) {
12294 *x += f;
12295 }
12296 attention::recycle_buf(&mut ffn);
12297 if self.layer_dump.is_some() {
12298 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12299 self.dump_layer_row(pos, li, row);
12300 }
12301 }
12302 crate::mimo_moe::note_attention_graph(b, attn_ns);
12303 Out::Completed
12304 }
12305
12306 fn layer_attn_plain(&self, li: usize) -> bool {
12307 self.kv_heads_per_layer.is_none()
12308 && self.v_head_dim.is_none()
12309 && self.global_attn.is_none()
12310 && self.layer_window(li).is_none()
12311 && self.kv_cache.layers[li].sinks.is_none()
12312 }
12313
12314 fn forward_layers(
12316 &mut self,
12317 hidden: &[f32],
12318 position: usize,
12319 task_mask: Option<&TaskMask>,
12320 ) -> Vec<f32> {
12321 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12322 self.o1_progress();
12323 self.swa_trim_tails();
12324 out
12325 }
12326
12327 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12335 self.embed_single(id)
12336 }
12337
12338 pub fn split_supported(&self) -> Result<(), String> {
12342 if self.dsv4.is_some() {
12343 return Err(
12344 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12345 );
12346 }
12347 if self.dsv41.is_some() {
12348 return Err(
12349 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12350 .into(),
12351 );
12352 }
12353 if self.qwen4_exp.is_some() {
12354 return Err(
12355 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12356 );
12357 }
12358 if self.per_token_stack() {
12359 return Err(
12360 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12361 );
12362 }
12363 Ok(())
12364 }
12365
12366 pub fn forward_span(
12371 &mut self,
12372 hidden: &[f32],
12373 position: usize,
12374 from: usize,
12375 upto: usize,
12376 task_mask: Option<&TaskMask>,
12377 ) -> Result<Vec<f32>, String> {
12378 self.split_supported()?;
12379 if from > upto || upto >= self.num_layers {
12380 return Err(format!(
12381 "forward_span: layer range {from}..={upto} outside 0..{}",
12382 self.num_layers
12383 ));
12384 }
12385 if hidden.len() != self.hidden_size {
12386 return Err(format!(
12387 "forward_span: hidden len {} ≠ hidden_size {}",
12388 hidden.len(),
12389 self.hidden_size
12390 ));
12391 }
12392 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12393 self.o1_progress();
12394 self.swa_trim_tails();
12395 if self
12396 .graph_failed
12397 .swap(false, std::sync::atomic::Ordering::Relaxed)
12398 {
12399 self.cancel
12400 .store(false, std::sync::atomic::Ordering::Relaxed);
12401 self.clear_sequence_state();
12402 return Err("forward_span: deferred O(1) transition failed".into());
12403 }
12404 Ok(out)
12405 }
12406
12407 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12410 let normed = inference::rms_norm(
12411 hidden,
12412 &self.weights.final_norm,
12413 self.rms_eps,
12414 self.norm_style,
12415 );
12416 self.lm_head_forward(&normed)
12417 }
12418
12419 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12421 sampler::sample_with_scratch(
12422 logits,
12423 &self.sampler_config,
12424 past_tokens,
12425 &mut self.rng,
12426 &mut self.sampler_scratch,
12427 )
12428 }
12429
12430 pub fn reset_session(&mut self) {
12432 self.clear_sequence_state();
12433 }
12434
12435 pub fn prefill_span_ids(
12441 &mut self,
12442 ids: &[u32],
12443 start_pos: usize,
12444 upto: usize,
12445 task_mask: Option<&TaskMask>,
12446 ) -> Result<Vec<f32>, String> {
12447 self.split_supported()?;
12448 if upto >= self.num_layers {
12449 return Err(format!(
12450 "prefill_span_ids: upto {upto} outside 0..{}",
12451 self.num_layers
12452 ));
12453 }
12454 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12458 let out =
12459 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12460 self.check_o1_progress_failure("prefill_span_ids")?;
12461 Ok(out)
12462 } else {
12463 let hs = self.hidden_size;
12464 let mut out = Vec::with_capacity(ids.len() * hs);
12465 for (i, &id) in ids.iter().enumerate() {
12466 let emb = self.embed_id(id);
12467 out.extend_from_slice(&self.forward_span(
12468 &emb,
12469 start_pos + i,
12470 0,
12471 upto,
12472 task_mask,
12473 )?);
12474 }
12475 Ok(out)
12476 }
12477 }
12478
12479 pub fn prefill_span_hidden(
12482 &mut self,
12483 hidden: &[f32],
12484 start_pos: usize,
12485 from: usize,
12486 upto: usize,
12487 task_mask: Option<&TaskMask>,
12488 ) -> Result<Vec<f32>, String> {
12489 self.split_supported()?;
12490 let hs = self.hidden_size;
12491 if hidden.is_empty() || hidden.len() % hs != 0 {
12492 return Err(format!(
12493 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12494 hidden.len()
12495 ));
12496 }
12497 if from > upto || upto >= self.num_layers {
12498 return Err(format!(
12499 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12500 self.num_layers
12501 ));
12502 }
12503 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12504 let out = self.prefill_batch_span(
12505 PrefillIn::Hidden(hidden),
12506 start_pos,
12507 task_mask,
12508 from,
12509 upto + 1,
12510 );
12511 self.check_o1_progress_failure("prefill_span_hidden")?;
12512 Ok(out)
12513 } else {
12514 let b = hidden.len() / hs;
12515 let mut out = Vec::with_capacity(hidden.len());
12516 for i in 0..b {
12517 let h = self.forward_span(
12518 &hidden[i * hs..(i + 1) * hs],
12519 start_pos + i,
12520 from,
12521 upto,
12522 task_mask,
12523 )?;
12524 out.extend_from_slice(&h);
12525 }
12526 Ok(out)
12527 }
12528 }
12529
12530 fn try_token_graph_wgpu(
12534 &self,
12535 hidden: &[f32],
12536 position: usize,
12537 logits_out: &mut Vec<f32>,
12538 layers_run: &mut usize,
12539 ) -> Option<Result<Vec<f32>, ()>> {
12540 self.try_token_graph_wgpu_steps(
12541 hidden,
12542 position,
12543 logits_out,
12544 1,
12545 None,
12546 Some(layers_run),
12547 0,
12548 self.num_layers,
12549 )
12550 }
12551
12552 fn try_token_graph_wgpu_span(
12556 &self,
12557 hidden: &[f32],
12558 position: usize,
12559 logits_out: &mut Vec<f32>,
12560 from: usize,
12561 upto_excl: usize,
12562 layers_run: &mut usize,
12563 ) -> Option<Result<Vec<f32>, ()>> {
12564 self.try_token_graph_wgpu_steps(
12565 hidden,
12566 position,
12567 logits_out,
12568 1,
12569 None,
12570 Some(layers_run),
12571 from,
12572 upto_excl,
12573 )
12574 }
12575
12576 #[cfg(feature = "gpu")]
12580 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12581 if !crate::minicpm_tail::minimax_pool_on()
12582 || !crate::gpu::enabled_here()
12583 || self.head_clusters.is_some()
12584 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12585 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12586 {
12587 return None;
12588 }
12589 let (model, i, kind, rs) = self
12590 .weights
12591 .lm_head
12592 .graph_weight()
12593 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12594 let name = &model.tensors[i].name;
12595 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12596 crate::gpu::GraphPrismOp::InverseEmbedding
12597 } else if crate::prism::is_forward_weight(&model, name) {
12598 crate::gpu::GraphPrismOp::Forward
12599 } else {
12600 crate::gpu::GraphPrismOp::None
12601 };
12602 let gw = crate::gpu::GraphW {
12603 idx: i,
12604 kind,
12605 row_scale: rs,
12606 data: &[],
12607 prism,
12608 affine: crate::prism::is_affine_target(&model, name),
12609 };
12610 let rows = self.weights.lm_head.rows();
12611 let mut h = hidden.to_vec();
12612 let mut lg = Vec::new();
12613 let mut layers_run = 0usize;
12614 let outcome = crate::gpu::forward_token_graph(
12615 &model,
12616 self.graph_kv_id,
12617 &[],
12618 &[],
12619 self.o1_epoch,
12620 &self.inv_freq,
12621 &mut h,
12622 self.num_heads,
12623 self.num_kv_heads,
12624 self.head_dim,
12625 self.attn_scale,
12626 self.head_dim,
12627 self.hidden_size,
12628 self.intermediate_size,
12629 0,
12630 self.kv_cache.max_seq_len,
12631 self.norm_style == cortiq_core::NormStyle::Gemma,
12632 self.rms_eps as f32,
12633 Some((&gw, rows)),
12634 &self.weights.final_norm,
12635 &mut lg,
12636 &[],
12637 1,
12638 None,
12639 None,
12640 Some(&mut layers_run),
12641 0,
12642 false,
12643 );
12644 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12645 return None;
12646 }
12647 lg.resize(self.vocab_size, 0.0);
12650 if let Some(m) = self.logit_multiplier {
12651 for l in lg.iter_mut() {
12652 *l *= m;
12653 }
12654 }
12655 if let Some(c) = self.final_softcap {
12656 softcap_in_place(&mut lg, c, self.pool.as_deref());
12657 }
12658 Some(lg)
12659 }
12660
12661 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12664 if winners.is_empty() {
12665 return;
12666 }
12667 if self.m3_n_exp == 0 {
12668 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12669 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12670 }
12671 for &e in winners {
12672 let e = e as usize;
12673 if e < self.m3_n_exp && li < self.num_layers {
12674 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12675 }
12676 }
12677 if li + 1 == self.num_layers {
12678 self.m3_tokens += 1;
12679 if self.m3_tokens % 16 == 0 {
12680 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12681 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12682 let _ = std::fs::write(&path, bytes);
12683 }
12684 }
12685 }
12686 }
12687
12688 fn m3_prewarm(&mut self) {
12693 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12694 return;
12695 };
12696 let Ok(bytes) = std::fs::read(&path) else {
12697 tracing::warn!("m3 prewarm: cannot read profile {path}");
12698 return;
12699 };
12700 let counts: Vec<u32> = bytes
12701 .chunks_exact(4)
12702 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12703 .collect();
12704 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12705 if counts.len() != self.num_layers * n_exp {
12706 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12707 return;
12708 }
12709 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12710 .ok()
12711 .and_then(|v| v.parse().ok())
12712 .unwrap_or(300);
12713 let mut ranked: Vec<(u32, usize, usize)> = counts
12714 .iter()
12715 .enumerate()
12716 .filter(|(_, c)| **c > 0)
12717 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12718 .collect();
12719 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12720 ranked.truncate(limit);
12721 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12722 for (_, li, e) in ranked {
12723 per_layer[li].push(e);
12724 }
12725 let mut admitted = 0usize;
12726 for (li, ids) in per_layer.iter().enumerate() {
12727 if ids.is_empty() {
12728 continue;
12729 }
12730 let lw = &self.weights.layers[self.phys_layer(li)];
12731 let FfnKind::Moe(m) = &lw.ffn else {
12732 continue;
12733 };
12734 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12736 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12737 admitted += ids.len();
12738 }
12739 }
12740 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12741 }
12742
12743 #[cfg(feature = "gpu")]
12754 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12755 if self.o1_active() || self.attn_softcap > 0.0 {
12756 return Ok(false);
12757 }
12758 if !self.m3_prewarmed {
12759 self.m3_prewarmed = true;
12760 self.m3_prewarm();
12761 }
12762 let frame = {
12763 let lw = &self.weights.layers[self.phys_layer(li)];
12764 let FfnKind::Moe(m) = &lw.ffn else {
12765 return Ok(false);
12766 };
12767 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12768 return Ok(false);
12769 }
12770 let t_arm = std::time::Instant::now();
12771 if self.m3_winners.len() != self.num_layers {
12772 self.m3_winners = vec![Vec::new(); self.num_layers];
12773 }
12774 let pred = std::mem::take(&mut self.m3_winners[li]);
12775 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12776 if armed.is_none() {
12777 self.m3_winners[li] = pred; }
12779 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12780 match armed {
12781 Some(frame) => frame,
12782 None => return Ok(false),
12785 }
12786 };
12787 crate::gpu_wgpu::pooled_frame_enter(frame);
12788 let want_logits = self.graph_want_logits;
12792 self.graph_want_logits = false;
12793 let mut lg = Vec::new();
12794 let mut gl = 0usize;
12795 let t_span = std::time::Instant::now();
12796 let span = crate::gpu::gpu_scope(|| {
12797 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12798 });
12799 self.graph_want_logits = want_logits;
12800 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12801 let hh = match span {
12802 Some(Ok(hh)) => hh,
12803 Some(Err(())) => return Err(()),
12804 None => {
12805 return Ok(false);
12808 }
12809 };
12810 if gl != 1 {
12811 return if gl == 0 { Ok(false) } else { Err(()) };
12814 }
12815 *h = hh;
12816 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12817 self.m3_note_route(li, &winners);
12818 if self.m3_winners.len() != self.num_layers {
12819 self.m3_winners = vec![Vec::new(); self.num_layers];
12820 }
12821 self.m3_winners[li] = winners;
12822 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12823 cold.retain(|&(_, w)| {
12826 if w.is_finite() {
12827 true
12828 } else {
12829 tracing::error!("pooled tail: non-finite cold weight from expert route");
12830 false
12831 }
12832 });
12833 if cold.is_empty() {
12834 return Ok(true);
12835 }
12836 let x = crate::gpu_wgpu::pooled_frame_take_input();
12837 if x.len() != self.hidden_size {
12838 return Err(());
12839 }
12840 let lw = &self.weights.layers[self.phys_layer(li)];
12846 let FfnKind::Moe(m) = &lw.ffn else {
12847 return Err(());
12848 };
12849 let t_cold = std::time::Instant::now();
12850 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12851 let mut out = crate::attention::take_buf(self.hidden_size);
12852 let on_device = !force_host
12853 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12854 if !on_device {
12855 crate::attention::recycle_buf(&mut out);
12856 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12857 .iter()
12858 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12859 .collect::<Result<_, ()>>()?;
12860 out = crate::gpu::cpu_scope(|| {
12861 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12862 });
12863 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12864 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12865 }
12866 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12867 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12868 if out.len() != h.len() {
12869 crate::attention::recycle_buf(&mut out);
12870 return Err(());
12871 }
12872 for (dst, src) in h.iter_mut().zip(out.iter()) {
12873 *dst += src;
12874 }
12875 crate::attention::recycle_buf(&mut out);
12876 if on_device {
12877 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12878 }
12879 Ok(true)
12880 }
12881
12882 #[cfg(not(feature = "gpu"))]
12883 fn m3_pooled_layer(
12884 &mut self,
12885 _li: usize,
12886 _position: usize,
12887 _h: &mut Vec<f32>,
12888 ) -> Result<bool, ()> {
12889 Ok(false)
12890 }
12891
12892 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12896 if self.o1_active() || self.attn_softcap > 0.0 {
12897 return None;
12898 }
12899 if let Some(reason) = self.wgpu_graph_attn_decline() {
12902 self.note_graph_decline("wgpu multi-burst", reason);
12903 return None;
12904 }
12905 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12906 if !graph_on || self.graph_refused() {
12907 return None;
12914 }
12915 let emb = self.embed_single(t_next);
12916 let mut lg = Vec::new();
12917 let mut ids = Vec::new();
12918 match self.try_token_graph_wgpu_steps(
12919 &emb,
12920 position,
12921 &mut lg,
12922 k,
12923 Some(&mut ids),
12924 None,
12925 0,
12926 self.num_layers,
12927 ) {
12928 Some(Ok(_)) => {}
12929 Some(Err(())) => {
12930 self.graph_failed
12935 .store(true, std::sync::atomic::Ordering::Relaxed);
12936 return None;
12937 }
12938 None => return None,
12939 }
12940 (ids.len() == k).then_some(ids)
12941 }
12942
12943 fn try_token_graph_wgpu_steps(
12947 &self,
12948 hidden: &[f32],
12949 position: usize,
12950 logits_out: &mut Vec<f32>,
12951 steps: usize,
12952 ids_out: Option<&mut Vec<u32>>,
12953 layers_run: Option<&mut usize>,
12954 from: usize,
12955 upto_excl: usize,
12956 ) -> Option<Result<Vec<f32>, ()>> {
12957 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12960 Some(end) if end < upto_excl => {
12961 if steps != 1 || layers_run.is_none() || from >= end {
12962 return None;
12963 }
12964 end
12965 }
12966 _ => upto_excl,
12967 };
12968 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12971 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12972 return None;
12976 }
12977 if let Some(reason) = self.wgpu_graph_attn_decline() {
12984 self.note_graph_decline("wgpu token graph", reason);
12985 return None;
12986 }
12987 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12992 .map(|li| {
12993 if !o1_gpu {
12994 return None;
12995 }
12996 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12997 })
12998 .collect();
12999 if self.o1_active() && o1_gpu {
13000 let want: usize = (from..upto_excl)
13003 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
13004 .count();
13005 let have = o1_views.iter().filter(|v| v.is_some()).count();
13006 if want == 0 || have != want {
13007 use std::sync::atomic::{AtomicUsize, Ordering};
13017 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13018 let code = have * 1000 + want;
13019 if LAST.swap(code, Ordering::Relaxed) != code {
13020 tracing::warn!(
13021 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13022 );
13023 }
13024 return None;
13025 }
13026 }
13027 let nh = self.num_heads;
13028 let (nkv, hd, rd) = self.layer_geom(0);
13029 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13030 let mut layers = Vec::with_capacity(upto_excl - from);
13031 let mut model = None;
13032 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13033 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13034 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13036 let name = &m.tensors[i].name;
13037 let prism = if crate::prism::is_inverse_embedding(m, name) {
13038 crate::gpu::GraphPrismOp::InverseEmbedding
13039 } else if crate::prism::is_forward_weight(m, name) {
13040 crate::gpu::GraphPrismOp::Forward
13041 } else {
13042 crate::gpu::GraphPrismOp::None
13043 };
13044 return Some(crate::gpu::GraphW {
13045 idx: i,
13046 kind,
13047 row_scale: rs,
13048 data: &[],
13049 prism,
13050 affine: crate::prism::is_affine_target(m, name),
13051 });
13052 }
13053 match t.as_f32() {
13055 Some(d) => Some(crate::gpu::GraphW {
13056 idx: 0,
13057 kind: 4,
13058 row_scale: &[],
13059 data: d,
13060 prism: crate::gpu::GraphPrismOp::None,
13061 affine: false,
13062 }),
13063 None => {
13064 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13065 eprintln!("batch graph: weight has no graph/f32 representation");
13066 }
13067 None
13068 }
13069 }
13070 }
13071 for li in from..upto_excl {
13072 let lw = &self.weights.layers[self.phys_layer(li)];
13073 if dbg {
13074 let ak = match &lw.attn {
13075 AttnKind::Mla(_) => "Mla".into(),
13076 AttnKind::Full {
13077 output_gate, bias, ..
13078 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13079 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13080 AttnKind::Kda(_) => "Kda".into(),
13081 AttnKind::Linear(_) => "Linear".into(),
13082 AttnKind::ShortConv(_) => "ShortConv".into(),
13083 AttnKind::Bounded(_) => "Bounded".into(),
13084 };
13085 let fk = match &lw.ffn {
13086 FfnKind::Dense(_) => "Dense",
13087 FfnKind::Moe(_) => "Moe",
13088 FfnKind::DenseMoe(_) => "DenseMoe",
13089 };
13090 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13091 }
13092 let gffn = match &lw.ffn {
13093 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13097 FfnKind::Dense(d) => {
13098 let Some(act) = d.act.wgpu_graph_act() else {
13102 self.note_graph_decline(
13103 "wgpu token graph",
13104 "dense FFN activation without a graph kernel",
13105 );
13106 return None;
13107 };
13108 crate::gpu::GraphFfn::Dense {
13109 gate: gw(&d.gate_proj)?,
13110 up: gw(&d.up_proj)?,
13111 down: gw(&d.down_proj)?,
13112 act,
13113 }
13114 }
13115 FfnKind::Moe(m) => {
13116 if m.route_tau.is_some() || m.mask.is_some() {
13124 return None;
13125 }
13126 let shared = m.shared.as_ref();
13127 let inter = m.experts.first()?.gate_proj.rows();
13133 let detached_shared = match shared {
13141 Some((se, None))
13142 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13143 {
13144 if !matches!(se.act, Act::Silu)
13145 || se.gate_proj.mapped_q4tp().is_none()
13146 || se.up_proj.mapped_q4tp().is_none()
13147 || se.down_proj.mapped_q4tp().is_none()
13148 {
13149 return None;
13150 }
13151 Some(crate::gpu::GraphMoeDetachedShared {
13152 gate: gw(&se.gate_proj)?,
13153 up: gw(&se.up_proj)?,
13154 down: gw(&se.down_proj)?,
13155 width: se.gate_proj.rows(),
13156 })
13157 }
13158 Some((se, Some(_)))
13162 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13163 {
13164 return None;
13165 }
13166 _ => None,
13167 };
13168 let has_shared = shared.is_some() && detached_shared.is_none();
13169 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13170 let sgate = match shared {
13171 Some((_, Some(sg))) if has_shared => gw(sg)?,
13172 _ => gw(&m.router)?,
13176 };
13177 let router = gw(&m.router)?;
13178 if router.prism != crate::gpu::GraphPrismOp::None
13184 || sgate.prism != crate::gpu::GraphPrismOp::None
13185 || router.affine
13186 || sgate.affine
13187 {
13188 tracing::warn!(
13189 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13190 );
13191 return None;
13192 }
13193 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13194 let mut q4tp: Option<bool> = None;
13197 let mut gu_q2: Option<bool> = None;
13200 let mut q82: Option<bool> = None;
13202 let mut moe_act = None;
13206 for e in m
13207 .experts
13208 .iter()
13209 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13210 {
13211 let Some(e_act) = e.act.wgpu_graph_act() else {
13212 return None;
13213 };
13214 match moe_act {
13215 Some(existing) if existing != e_act => return None,
13216 None => moe_act = Some(e_act),
13217 _ => {}
13218 }
13219 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13220 return None;
13221 }
13222 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13227 let Some((em, ei, _, _)) = expert_weight
13228 .graph_weight()
13229 .or_else(|| expert_weight.graph_weight_descriptor())
13230 else {
13231 return None;
13232 };
13233 let name = &em.tensors[ei].name;
13234 if crate::prism::is_forward_weight(em, name)
13235 || crate::prism::is_inverse_embedding(em, name)
13236 || crate::prism::is_affine_target(em, name)
13237 {
13238 tracing::warn!(
13239 "resident MoE declined: expert Prism/affine transform is not implemented"
13240 );
13241 return None;
13242 }
13243 }
13244 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13245 Some((mm, gi)) => (
13246 mm,
13247 gi,
13248 e.up_proj.mapped_q4t()?.1,
13249 e.down_proj.mapped_q4t()?.1,
13250 false,
13251 false,
13252 false,
13253 ),
13254 None => match e.gate_proj.mapped_q2tp() {
13255 Some((mm, gi)) => (
13256 mm,
13257 gi,
13258 e.up_proj.mapped_q2tp()?.1,
13259 e.down_proj.mapped_q4tp()?.1,
13260 true,
13261 true,
13262 false,
13263 ),
13264 None => match e.gate_proj.mapped_q4tp() {
13265 Some((mm, gi)) => (
13266 mm,
13267 gi,
13268 e.up_proj.mapped_q4tp()?.1,
13269 e.down_proj.mapped_q4tp()?.1,
13270 true,
13271 false,
13272 false,
13273 ),
13274 None => {
13277 if !crate::gpu::moe_q82_graph_on() {
13278 return None;
13279 }
13280 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13281 (
13282 mm,
13283 gi,
13284 e.up_proj.mapped_q8_2f()?.1,
13285 e.down_proj.mapped_q8_2f()?.1,
13286 false,
13287 false,
13288 true,
13289 )
13290 }
13291 },
13292 },
13293 };
13294 if *q4tp.get_or_insert(is_p) != is_p
13295 || *gu_q2.get_or_insert(is_q2) != is_q2
13296 || *q82.get_or_insert(is_8) != is_8
13297 {
13298 tracing::warn!(
13304 "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."
13305 );
13306 return None;
13307 }
13308 model.get_or_insert_with(|| mm.clone());
13309 experts.push((gi, ui, di));
13310 }
13311 crate::gpu::GraphFfn::Moe {
13312 router,
13313 shared_gate: sgate,
13314 experts,
13315 n_exp: m.experts.len(),
13316 top_k: std::env::var("CMF_TOPK_PROBE")
13322 .ok()
13323 .and_then(|v| v.parse::<usize>().ok())
13324 .filter(|k| *k > 0 && *k <= m.top_k)
13325 .unwrap_or(m.top_k),
13326 inter,
13327 act: moe_act?,
13328 norm_topk: m.norm_topk_prob,
13329 q4tp: q4tp?,
13330 gu_q2: gu_q2.unwrap_or(false),
13331 q82: q82.unwrap_or(false),
13332 sigmoid: m.router_sigmoid,
13333 sigmoid_denom_eps: m.sigmoid_denom_eps,
13334 bias: m.expert_bias.as_deref(),
13335 has_shared,
13336 detached_shared,
13337 shared_gated,
13338 route_scale: m.routed_scaling,
13339 }
13340 }
13341 };
13342 let attn = match &lw.attn {
13343 AttnKind::Full {
13344 wq,
13345 wk,
13346 wv,
13347 wo,
13348 q_norm,
13349 k_norm,
13350 output_gate,
13351 softplus_gate,
13352 bias,
13353 } => {
13354 if self.attention_heads_per_layer.is_some() {
13355 return None;
13356 }
13357 let head_gate = match softplus_gate {
13361 None => None,
13362 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13363 Some(_) => {
13364 self.note_graph_decline(
13365 "wgpu token graph",
13366 "projected softplus / per-element output gate",
13367 );
13368 return None;
13369 }
13370 };
13371 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13372 model = Some(m.clone());
13373 crate::gpu::GraphAttn::Full {
13374 wq: gw(wq)?,
13375 wk: gw(wk)?,
13376 wv: gw(wv)?,
13377 wo: gw(wo)?,
13378 q_norm: q_norm.as_deref(),
13379 k_norm: k_norm.as_deref(),
13380 late_qk_norm: self.qk_norm_after_rope,
13381 bias: bias
13382 .as_ref()
13383 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13384 output_gate: *output_gate,
13385 cpu_k: self.kv_cache.layers[li].k_heads(),
13386 cpu_v: self.kv_cache.layers[li].v_heads(),
13387 cpu_base: self.kv_cache.layers[li].base(),
13388 geom: self.graph_attn_geom(li),
13389 head_gate,
13390 }
13391 }
13392 AttnKind::LinearGdn(w) => {
13393 let cfg = self.gdn_cfg?;
13394 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13395 model = Some(m.clone());
13396 crate::gpu::GraphAttn::Gdn {
13397 qkv: gw(&w.in_proj_qkv)?,
13398 z: gw(&w.in_proj_z)?,
13399 a: gw(&w.in_proj_a)?,
13400 b: gw(&w.in_proj_b)?,
13401 out: gw(&w.out_proj)?,
13402 conv1d: &w.conv1d,
13403 a_log: &w.a_log,
13404 dt_bias: &w.dt_bias,
13405 norm: &w.norm,
13406 nv: cfg.num_v_heads,
13407 nk: cfg.num_k_heads,
13408 dk: cfg.key_head_dim,
13409 dv: cfg.value_head_dim,
13410 kk: cfg.conv_kernel,
13411 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13412 preview: None,
13413 }
13414 }
13415 AttnKind::ShortConv(w) => {
13416 let cfg = self.short_conv_cfg?;
13417 let (m, _, _, _) = w
13418 .in_proj
13419 .graph_weight()
13420 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13421 model = Some(m.clone());
13422 crate::gpu::GraphAttn::ShortConv {
13423 inp: gw(&w.in_proj)?,
13424 out: gw(&w.out_proj)?,
13425 taps: &w.conv,
13426 kernel: cfg.kernel,
13427 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13428 }
13429 }
13430 _ => return None,
13431 };
13432 layers.push(crate::gpu::GraphLayer {
13433 input_norm: &lw.input_norm,
13434 attn,
13435 post_norm: &lw.post_norm,
13436 attn_out_norm: lw.attn_out_norm.as_deref(),
13437 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13438 layer_scale: lw.layer_scale,
13439 ffn: gffn,
13440 });
13441 }
13442 let model = model?;
13443 let lm_gw = if upto_excl == self.num_layers
13449 && self.graph_want_logits
13450 && std::env::var("CMF_GPU_LMHEAD")
13451 .map(|v| v != "0")
13452 .unwrap_or(true)
13453 {
13454 self.weights
13455 .lm_head
13456 .graph_weight()
13457 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13458 .map(|(m, i, kind, rs)| {
13459 let name = &m.tensors[i].name;
13460 let prism = if crate::prism::is_inverse_embedding(m, name) {
13461 crate::gpu::GraphPrismOp::InverseEmbedding
13462 } else if crate::prism::is_forward_weight(m, name) {
13463 crate::gpu::GraphPrismOp::Forward
13464 } else {
13465 crate::gpu::GraphPrismOp::None
13466 };
13467 (
13468 crate::gpu::GraphW {
13469 idx: i,
13470 kind,
13471 row_scale: rs,
13472 data: &[],
13473 prism,
13474 affine: crate::prism::is_affine_target(m, name),
13475 },
13476 self.weights.lm_head.rows(),
13477 )
13478 })
13479 } else {
13480 None
13481 };
13482 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13483 let emb_gw = if steps > 1 {
13485 self.weights
13486 .embed_tokens
13487 .graph_weight()
13488 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13489 .map(|(m, i, kind, rs)| {
13490 let name = &m.tensors[i].name;
13491 let prism = if crate::prism::is_inverse_embedding(m, name) {
13492 crate::gpu::GraphPrismOp::InverseEmbedding
13493 } else if crate::prism::is_forward_weight(m, name) {
13494 crate::gpu::GraphPrismOp::Forward
13495 } else {
13496 crate::gpu::GraphPrismOp::None
13497 };
13498 (
13499 crate::gpu::GraphW {
13500 idx: i,
13501 kind,
13502 row_scale: rs,
13503 data: &[],
13504 prism,
13505 affine: crate::prism::is_affine_target(m, name),
13506 },
13507 self.weights.embed_tokens.rows(),
13508 self.embed_multiplier,
13509 )
13510 })
13511 } else {
13512 None
13513 };
13514
13515 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13521 (from..upto_excl.min(self.num_layers - 1))
13522 .filter(|&li| (li + 1) % self.physical_layers == 0)
13523 .map(|li| li - from)
13524 .collect()
13525 } else {
13526 Vec::new()
13527 };
13528 let mut h = hidden.to_vec();
13529 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13535 let outcome = crate::gpu::forward_token_graph(
13536 &model,
13537 self.graph_kv_id,
13538 &layers,
13539 &o1_views,
13540 self.o1_epoch,
13541 &self.inv_freq,
13542 &mut h,
13543 nh,
13544 nkv,
13545 hd,
13546 self.attn_scale,
13547 rd,
13548 self.hidden_size,
13549 self.intermediate_size,
13550 position,
13551 self.kv_cache.max_seq_len,
13552 gemma,
13553 self.rms_eps as f32,
13554 lm,
13555 &self.weights.final_norm,
13556 logits_out,
13557 &loop_norm_at,
13558 steps,
13559 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13560 ids_out,
13561 layers_run,
13562 from,
13563 dump_hidden,
13564 );
13565 match outcome {
13566 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13567 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13568 crate::gpu::TokenGraphOutcome::Declined => None,
13569 }
13570 }
13571
13572 #[cfg(target_os = "macos")]
13581 #[allow(clippy::type_complexity)]
13582 fn metal_rows_plan(
13583 &self,
13584 ) -> Option<(
13585 Vec<MetalRowsItem<'_>>,
13586 std::sync::Arc<cortiq_core::CmfModel>,
13587 Option<crate::gpu_metal::GdnGpuCfg>,
13588 )> {
13589 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13590 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13591 if !graph_force
13592 || !crate::gpu::enabled_here()
13593 || std::env::var("CMF_GPU_BLOCK")
13594 .map(|v| v == "0")
13595 .unwrap_or(false)
13596 || self.attn_softcap > 0.0
13597 || self.o1_active()
13598 || self.swa.is_some()
13599 || self.global_attn.is_some()
13600 || self.attention_heads_per_layer.is_some()
13601 || self.graph_attn_decline_reason().is_some()
13603 || self.attn_v_norm
13604 || self.loop_final_norm
13605 || !self.metal_rope_scale_ok()
13606 {
13607 return None;
13608 }
13609 let attend_contract = self.head_dim % 4 == 0
13610 && self.head_dim <= 256
13611 && self.rotary_dim >= 2
13612 && self.rotary_dim <= self.head_dim
13613 && (self.rotary_dim / 2) % 32 == 0
13614 && self.num_kv_heads > 0
13615 && self.num_heads % self.num_kv_heads == 0;
13616 if !attend_contract {
13617 return None;
13618 }
13619 let mut plan: Vec<MetalRowsItem> = Vec::new();
13620 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13621 for li in 0..self.num_layers {
13622 let lw = &self.weights.layers[self.phys_layer(li)];
13623 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13624 return None;
13625 }
13626 let ffn = match &lw.ffn {
13627 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13628 let (Some(g), Some(u), Some(dn)) = (
13629 d.gate_proj.metal_graph_parts(),
13630 d.up_proj.metal_graph_parts(),
13631 d.down_proj.metal_graph_parts(),
13632 ) else {
13633 return None;
13634 };
13635 MetalFfn::Dense {
13636 gate: g,
13637 up: u,
13638 down: dn,
13639 act: 0, }
13641 }
13642 _ => return None,
13643 };
13644 match &lw.attn {
13645 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13646 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13647 w.in_proj_qkv.metal_graph_parts(),
13648 w.in_proj_z.metal_graph_parts(),
13649 w.in_proj_a.f32_parts(),
13650 w.in_proj_b.f32_parts(),
13651 w.out_proj.metal_graph_parts(),
13652 ) else {
13653 return None;
13654 };
13655 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13656 model_ref.get_or_insert_with(|| model.clone());
13657 }
13658 let gl = GdnGpuLayer {
13659 attn_norm: &lw.input_norm,
13660 post_norm: &lw.post_norm,
13661 qkv,
13662 z,
13663 a,
13664 b: bb,
13665 out,
13666 ffn,
13667 conv1d: &w.conv1d,
13668 a_log: &w.a_log,
13669 dt_bias: &w.dt_bias,
13670 gnorm: &w.norm,
13671 };
13672 match plan.last_mut() {
13673 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13674 _ => plan.push(MetalRowsItem::Gdn {
13675 run: vec![gl],
13676 first: li,
13677 }),
13678 }
13679 }
13680 AttnKind::Full {
13681 wq,
13682 wk,
13683 wv,
13684 wo,
13685 q_norm,
13686 k_norm,
13687 output_gate,
13688 softplus_gate: None,
13689 bias: None,
13690 } => {
13691 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13692 wq.metal_graph_parts(),
13693 wk.metal_graph_parts(),
13694 wv.metal_graph_parts(),
13695 wo.metal_graph_parts(),
13696 ) else {
13697 return None;
13698 };
13699 if let QTensor::Mapped { model, .. } = wq {
13700 model_ref.get_or_insert_with(|| model.clone());
13701 }
13702 let cache = &self.kv_cache.layers[li];
13703 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13704 return None;
13705 }
13706 plan.push(MetalRowsItem::Attn {
13707 l: AttnGpuLayer {
13708 attn_norm: &lw.input_norm,
13709 post_norm: &lw.post_norm,
13710 wq: pq,
13711 wk: pk,
13712 wv: pv,
13713 wo: po,
13714 ffn,
13715 attn_out_norm: None,
13716 ffn_out_norm: None,
13717 layer_scale: None,
13718 },
13719 li,
13720 q_norm: q_norm.as_deref(),
13721 k_norm: k_norm.as_deref(),
13722 output_gate: *output_gate,
13723 });
13724 }
13725 _ => return None,
13726 }
13727 }
13728 let model = model_ref?;
13729 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13730 nv: cfg.num_v_heads,
13731 nk: cfg.num_k_heads,
13732 dk: cfg.key_head_dim,
13733 dv: cfg.value_head_dim,
13734 kk: cfg.conv_kernel,
13735 hidden: self.hidden_size,
13736 inter: self.intermediate_size,
13737 c_dim: cfg.conv_dim(),
13738 eps: cfg.rms_eps as f32,
13739 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13740 });
13741 Some((plan, model, gcfg))
13742 }
13743
13744 #[cfg(target_os = "macos")]
13746 #[allow(clippy::too_many_arguments)]
13747 fn metal_attn_params<'a>(
13748 li: usize,
13749 cache: &'a crate::kv_cache::LayerKvCache,
13750 q_norm: Option<&'a [f32]>,
13751 k_norm: Option<&'a [f32]>,
13752 output_gate: bool,
13753 inv_freq: &'a [f32],
13754 rope_scale: f32,
13755 geom: (usize, usize, usize, usize),
13756 pos0: usize,
13757 kv_id: u64,
13758 scale: f32,
13759 eps: f32,
13760 gemma: bool,
13761 late_qk_norm: bool,
13762 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13763 let (nh, nkv, hd, rd) = geom;
13764 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13765 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13766 let cpu_stored = cpu_k[0].len() / hd;
13767 (
13768 crate::gpu_metal::AttnDeviceParams {
13769 kv_id,
13770 layer: li,
13771 nh,
13772 nkv,
13773 hd,
13774 rd,
13775 position: pos0,
13776 scale,
13777 eps,
13778 gemma,
13779 late_qk_norm,
13780 output_gate,
13781 q_norm,
13782 k_norm,
13783 inv_freq,
13784 rope_scale,
13785 cpu_k,
13786 cpu_v,
13787 cpu_stored,
13788 cpu_gen: cache.generation(),
13789 o1: None,
13790 window: None,
13791 softmax_sink: cache.sinks.as_deref(),
13792 head_gate: None,
13793 v_norm: false,
13794 },
13795 cpu_stored,
13796 )
13797 }
13798
13799 #[cfg(target_os = "macos")]
13804 #[allow(clippy::type_complexity)]
13805 fn metal_rows_run(
13806 &mut self,
13807 hiddens: &mut [f32],
13808 pos0: usize,
13809 b: usize,
13810 prefill: bool,
13811 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13812 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13816 ) -> MetalRowsRun {
13817 use crate::gpu_metal::{GraphDims, VerifyGraph};
13818 if !crate::gpu_metal::wait_replay() {
13824 tracing::error!("Metal rows graph: the pending async replay failed");
13825 return MetalRowsRun::Failed;
13826 }
13827 spec_stamp("v.wait");
13828 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13834 if want > 0 {
13835 let phys = self.physical_layers.max(1);
13836 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13837 let is_gdn = self
13838 .weights
13839 .layers
13840 .get(li % phys)
13841 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13842 if is_gdn && l.linear_state.len() != want {
13843 l.linear_state = vec![0f32; want];
13844 }
13845 }
13846 }
13847 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13848 return MetalRowsRun::Declined;
13849 };
13850 spec_stamp("v.plan");
13851 let dims = GraphDims {
13852 hidden: self.hidden_size,
13853 eps: self.rms_eps as f32,
13854 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13855 };
13856 let Some(mut graph) = (if prefill {
13857 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13858 } else {
13859 VerifyGraph::new(&model, dims, hiddens, b)
13860 }) else {
13861 return MetalRowsRun::Declined;
13862 };
13863 let geom = (
13864 self.num_heads,
13865 self.num_kv_heads,
13866 self.head_dim,
13867 self.rotary_dim,
13868 );
13869 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13870 let eps = self.rms_eps as f32;
13871 let kv_id = self.graph_kv_id;
13872 let inv_freq = self.inv_freq.clone();
13873 for item in &plan {
13874 let ok = match item {
13875 MetalRowsItem::Gdn { run, .. } => gcfg
13876 .as_ref()
13877 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13878 .unwrap_or(false),
13879 MetalRowsItem::Attn {
13880 l,
13881 li,
13882 q_norm,
13883 k_norm,
13884 output_gate,
13885 } => {
13886 let (p, _) = Self::metal_attn_params(
13887 *li,
13888 &self.kv_cache.layers[*li],
13889 *q_norm,
13890 *k_norm,
13891 *output_gate,
13892 &inv_freq,
13893 self.layer_rope_scale(*li),
13894 geom,
13895 pos0,
13896 kv_id,
13897 self.attn_scale,
13898 eps,
13899 gemma,
13900 self.qk_norm_after_rope,
13901 );
13902 graph.attn_ok(l, &p)
13903 }
13904 };
13905 if !ok {
13906 use std::sync::atomic::{AtomicBool, Ordering};
13907 static SAID: AtomicBool = AtomicBool::new(false);
13908 if !SAID.swap(true, Ordering::Relaxed) {
13909 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13910 }
13911 return MetalRowsRun::Declined;
13912 }
13913 }
13914 let lm = match &spec {
13915 Some((lm, _, _)) => {
13916 if !graph.lm_head_ok(*lm) {
13917 return MetalRowsRun::Declined;
13918 }
13919 Some(*lm)
13920 }
13921 None => None,
13922 };
13923 let mut gdn_layers = Vec::new();
13924 let mut attn_layers = Vec::new();
13925 for item in &plan {
13926 match item {
13927 MetalRowsItem::Gdn { run, first } => {
13928 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13929 .iter()
13930 .map(|l| l.linear_state.as_slice())
13931 .collect();
13932 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13933 return MetalRowsRun::Declined;
13934 }
13935 gdn_layers.extend(*first..*first + run.len());
13936 }
13937 MetalRowsItem::Attn {
13938 l,
13939 li,
13940 q_norm,
13941 k_norm,
13942 output_gate,
13943 } => {
13944 let (p, cpu_stored) = Self::metal_attn_params(
13945 *li,
13946 &self.kv_cache.layers[*li],
13947 *q_norm,
13948 *k_norm,
13949 *output_gate,
13950 &inv_freq,
13951 self.layer_rope_scale(*li),
13952 geom,
13953 pos0,
13954 kv_id,
13955 self.attn_scale,
13956 eps,
13957 gemma,
13958 self.qk_norm_after_rope,
13959 );
13960 if !graph.encode_attn_b(l, &p) {
13961 return MetalRowsRun::Declined;
13962 }
13963 attn_layers.push((*li, cpu_stored));
13964 }
13965 }
13966 }
13967 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13968 if !graph.encode_lm_head_b(final_norm, lm) {
13969 return MetalRowsRun::Declined;
13970 }
13971 if let Some((n, _)) = argmax_out.as_ref() {
13976 if !graph.encode_argmax_b(*n) {
13977 argmax_out = None;
13978 }
13979 }
13980 }
13981 spec_stamp("v.enc");
13982 if !graph.sync() {
13983 return MetalRowsRun::Failed;
13984 }
13985 spec_stamp("v.gpu");
13986 match (spec, argmax_out) {
13987 (Some(_), Some((_, ids))) => {
13988 ids.resize(b, 0);
13989 if !graph.read_argmax(ids) {
13990 return MetalRowsRun::Failed;
13991 }
13992 spec_stamp("v.am");
13993 }
13994 (Some((lm, _, logits)), None) => {
13995 logits.resize(b * lm.1, 0.0);
13996 if !graph.read_logits(logits) {
13997 return MetalRowsRun::Failed;
13998 }
13999 spec_stamp("v.lg");
14000 }
14001 (None, _) => {}
14002 }
14003 if !graph.read_hidden(hiddens) {
14004 return MetalRowsRun::Failed;
14005 }
14006 spec_stamp("v.hid");
14007 MetalRowsRun::Completed(MetalVerifyPending {
14008 graph,
14009 gdn_layers,
14010 attn_layers,
14011 })
14012 }
14013
14014 #[cfg(target_os = "macos")]
14020 fn try_batch_graph_metal(
14021 &mut self,
14022 hiddens: &mut [f32],
14023 positions: &[usize],
14024 b: usize,
14025 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14026 argmax_out: Option<(usize, &mut Vec<u32>)>,
14027 ) -> crate::gpu::BatchGraphOutcome {
14028 let _t0 = std::time::Instant::now();
14029 if positions.len() != b
14030 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14031 || hiddens.len() != b * self.hidden_size
14032 {
14033 return crate::gpu::BatchGraphOutcome::Declined;
14034 }
14035 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14036 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14037 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14038 MetalRowsRun::Completed(pending) => pending,
14039 };
14040 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14041 eprintln!(
14042 "metal-verify: {:.1} ms | b={b}",
14043 _t0.elapsed().as_secs_f64() * 1e3
14044 );
14045 }
14046 self.metal_verify = Some(pending);
14047 crate::gpu::BatchGraphOutcome::Completed
14048 }
14049
14050 #[cfg(target_os = "macos")]
14055 fn prefill_rows_metal(
14056 &mut self,
14057 ids: &[u32],
14058 start_pos: usize,
14059 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14060 ) -> MetalPrefillOutcome {
14061 let b = ids.len();
14062 if b == 0 || b > 512 {
14063 return MetalPrefillOutcome::Declined;
14064 }
14065 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14066 let with_head = spec.is_some();
14067 let hs = self.hidden_size;
14068 let mut hiddens = vec![0f32; b * hs];
14069 for (j, &id) in ids.iter().enumerate() {
14070 let e = self.embed_single(id);
14071 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14072 }
14073 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14074 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14075 MetalRowsRun::Failed => {
14076 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14077 return MetalPrefillOutcome::Failed;
14078 }
14079 MetalRowsRun::Completed(pending) => pending,
14080 };
14081 let idxs = pending.gdn_layers.clone();
14083 let mut outs: Vec<&mut [f32]> = self
14084 .kv_cache
14085 .layers
14086 .iter_mut()
14087 .enumerate()
14088 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14089 .map(|(_, l)| l.linear_state.as_mut_slice())
14090 .collect();
14091 if !pending.graph.finish_states(&mut outs) {
14092 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14093 return MetalPrefillOutcome::Failed;
14094 }
14095 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14096 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14100 for (li, cpu_stored) in &pending.attn_layers {
14101 let mut kbuf = vec![0f32; b * nkv * hd];
14102 let mut vbuf = vec![0f32; b * nkv * hd];
14103 if !crate::gpu_metal::kv_mirror_read_rows(
14104 self.graph_kv_id,
14105 *li,
14106 nkv,
14107 hd,
14108 *cpu_stored,
14109 b,
14110 &mut kbuf,
14111 &mut vbuf,
14112 ) {
14113 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14114 return MetalPrefillOutcome::Failed;
14115 }
14116 rows.push((*li, *cpu_stored, kbuf, vbuf));
14117 }
14118 for (li, cpu_stored, kbuf, vbuf) in rows {
14119 let cache = &mut self.kv_cache.layers[li];
14120 for r in 0..b {
14121 cache.append(
14122 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14123 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14124 &[],
14125 );
14126 }
14127 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14128 }
14129 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14130 if with_head {
14131 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14132 }
14133 MetalPrefillOutcome::Completed(hiddens)
14134 }
14135
14136 #[cfg(target_os = "macos")]
14137 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14138 self.prefill_rows_metal(ids, start_pos, None)
14139 }
14140
14141 #[cfg(target_os = "macos")]
14146 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14147 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14148 return MetalBatchNllOutcome::Declined;
14149 }
14150 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14151 return MetalBatchNllOutcome::Declined;
14152 };
14153 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14154 .ok()
14155 .and_then(|v| v.parse::<usize>().ok())
14156 .filter(|&v| (1..=512).contains(&v))
14157 .unwrap_or(32);
14158 let final_norm = self.weights.final_norm.clone();
14159 let mut nll = 0.0f64;
14160 let mut count = 0usize;
14161 let mut pos = 0usize;
14162 let mut completed = 0usize;
14163 while pos < ids.len() {
14164 let end = (pos + chunk).min(ids.len());
14165 let mut logits = Vec::new();
14166 let outcome =
14167 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14168 match outcome {
14169 MetalPrefillOutcome::Declined => {
14170 return if completed == 0 {
14171 MetalBatchNllOutcome::Declined
14172 } else {
14173 MetalBatchNllOutcome::Failed(format!(
14174 "ordinary Metal NLL batch declined after {completed} chunks"
14175 ))
14176 };
14177 }
14178 MetalPrefillOutcome::Failed => {
14179 return MetalBatchNllOutcome::Failed(
14180 "ordinary Metal NLL batch failed after admission".to_string(),
14181 );
14182 }
14183 MetalPrefillOutcome::Completed(_) => {}
14184 }
14185 completed += 1;
14186 let vocab = self.vocab_size.min(lm.1);
14187 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14188 return MetalBatchNllOutcome::Failed(
14189 "ordinary Metal NLL head returned an invalid shape".to_string(),
14190 );
14191 }
14192 for row in 0..(end - pos) {
14193 let absolute = pos + row;
14194 if absolute < start || absolute + 1 >= ids.len() {
14195 continue;
14196 }
14197 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14198 if let Some(mu) = self.logit_multiplier {
14199 for v in lg.iter_mut() {
14200 *v *= mu;
14201 }
14202 }
14203 if let Some(c) = self.final_softcap {
14204 softcap_in_place(lg, c, self.pool.as_deref());
14205 }
14206 let target = ids[absolute + 1] as usize;
14207 if target >= vocab {
14208 return MetalBatchNllOutcome::Failed(format!(
14209 "target token {target} exceeds Metal head rows {vocab}"
14210 ));
14211 }
14212 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14213 let lse: f64 = lg
14214 .iter()
14215 .map(|&v| ((v - max) as f64).exp())
14216 .sum::<f64>()
14217 .ln()
14218 + max as f64;
14219 nll += lse - lg[target] as f64;
14220 count += 1;
14221 }
14222 pos = end;
14223 }
14224 MetalBatchNllOutcome::Completed(nll, count)
14225 }
14226
14227 #[cfg(target_os = "macos")]
14231 fn metal_verify_commit(&mut self, a: usize) -> bool {
14232 let Some(mut pending) = self.metal_verify.take() else {
14233 return false;
14234 };
14235 let n = a + 1;
14236 let idxs = pending.gdn_layers.clone();
14238 let mut outs: Vec<&mut [f32]> = self
14239 .kv_cache
14240 .layers
14241 .iter_mut()
14242 .enumerate()
14243 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14244 .map(|(_, l)| l.linear_state.as_mut_slice())
14245 .collect();
14246 if !pending.graph.commit(n, &mut outs) {
14247 return false;
14248 }
14249 spec_stamp("c.replay");
14250 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14251 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14255 for (li, cpu_stored) in &pending.attn_layers {
14256 let mut kbuf = vec![0f32; n * nkv * hd];
14257 let mut vbuf = vec![0f32; n * nkv * hd];
14258 if !crate::gpu_metal::kv_mirror_read_rows(
14259 self.graph_kv_id,
14260 *li,
14261 nkv,
14262 hd,
14263 *cpu_stored,
14264 n,
14265 &mut kbuf,
14266 &mut vbuf,
14267 ) {
14268 return false;
14269 }
14270 rows.push((*li, *cpu_stored, kbuf, vbuf));
14271 }
14272 for (li, cpu_stored, kbuf, vbuf) in rows {
14273 let cache = &mut self.kv_cache.layers[li];
14274 for r in 0..n {
14275 cache.append(
14276 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14277 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14278 &[],
14279 );
14280 }
14281 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14282 }
14283 spec_stamp("c.kv");
14284 true
14285 }
14286
14287 #[cfg(target_os = "macos")]
14294 fn mtp_warm_batch_submit(
14295 &mut self,
14296 m: &mut MtpModule,
14297 pairs: &[(&[f32], u32)],
14298 first_pos: usize,
14299 ) -> Option<MetalWarmPending> {
14300 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14301 let b = pairs.len();
14302 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14303 return None;
14304 }
14305 let AttnKind::Full {
14306 wq,
14307 wk,
14308 wv,
14309 wo,
14310 q_norm,
14311 k_norm,
14312 output_gate,
14313 softplus_gate: None,
14314 bias: None,
14315 } = &m.layer.attn
14316 else {
14317 return None;
14318 };
14319 let FfnKind::Dense(d) = &m.layer.ffn else {
14320 return None;
14321 };
14322 if !d.segs.is_empty() {
14323 return None;
14324 }
14325 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14326 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14327 else {
14328 return None;
14329 };
14330 let (Some(g), Some(u), Some(dn)) = (
14331 d.gate_proj.q1_parts(),
14332 d.up_proj.q1_parts(),
14333 d.down_proj.q1_parts(),
14334 ) else {
14335 return None;
14336 };
14337 let Some(eh) = m.eh_proj.q1_parts() else {
14338 return None;
14339 };
14340 let QTensor::Mapped { model, .. } = wq else {
14341 return None;
14342 };
14343 let model = model.clone();
14344 let hs = self.hidden_size;
14345 let mut cat = vec![0f32; b * 2 * hs];
14347 for (j, (h, tok)) in pairs.iter().enumerate() {
14348 let e = self.embed_single(*tok);
14349 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14350 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14351 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14352 }
14353 let dims = GraphDims {
14354 hidden: hs,
14355 eps: self.rms_eps as f32,
14356 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14357 };
14358 spec_stamp("w.cat");
14359 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14360 return None;
14361 };
14362 spec_stamp("w.new");
14363 let l = AttnGpuLayer {
14364 attn_norm: &m.layer.input_norm,
14365 post_norm: &m.layer.post_norm,
14366 wq: pq,
14367 wk: pk,
14368 wv: pv,
14369 wo: po,
14370 ffn: MetalFfn::Dense {
14371 gate: g,
14372 up: u,
14373 down: dn,
14374 act: (d.act == Act::Gelu) as u8,
14375 },
14376 attn_out_norm: None,
14377 ffn_out_norm: None,
14378 layer_scale: None,
14379 };
14380 let (nh, nkv, hd, rd) = (
14381 self.num_heads,
14382 self.num_kv_heads,
14383 self.head_dim,
14384 self.rotary_dim,
14385 );
14386 let inv_freq = self.inv_freq.clone();
14387 let cpu_stored;
14388 {
14389 let cache = &m.kv;
14390 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14391 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14392 cpu_stored = cpu_k[0].len() / hd;
14393 if cpu_stored > first_pos {
14398 spec_stamp("w.decl");
14399 return None;
14400 }
14401 let p = AttnDeviceParams {
14402 kv_id: self.mtp_kv_id(),
14403 layer: Self::MTP_LAYER_BASE,
14404 nh,
14405 nkv,
14406 hd,
14407 rd,
14408 position: first_pos,
14409 scale: self.attn_scale,
14410 eps: self.rms_eps as f32,
14411 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14412 late_qk_norm: self.qk_norm_after_rope,
14413 output_gate: *output_gate,
14414 q_norm: q_norm.as_deref(),
14415 k_norm: k_norm.as_deref(),
14416 inv_freq: &inv_freq,
14417 rope_scale: self.rope_scale,
14418 cpu_k,
14419 cpu_v,
14420 cpu_stored,
14421 cpu_gen: cache.generation(),
14422 o1: None,
14423 window: None,
14424 softmax_sink: None,
14425 head_gate: None,
14426 v_norm: false,
14427 };
14428 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14429 return None;
14430 }
14431 }
14432 spec_stamp("w.enc");
14433 if !graph.submit() {
14434 return None;
14435 }
14436 spec_stamp("w.sub");
14437 Some(MetalWarmPending {
14438 graph,
14439 cpu_stored,
14440 b,
14441 })
14442 }
14443
14444 #[cfg(target_os = "macos")]
14447 fn mtp_warm_batch_metal(
14448 &mut self,
14449 m: &mut MtpModule,
14450 pairs: &[(&[f32], u32)],
14451 first_pos: usize,
14452 ) -> bool {
14453 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14454 Some(p) => self.mtp_warm_batch_finish(m, p),
14455 None => false,
14456 }
14457 }
14458
14459 #[cfg(target_os = "macos")]
14464 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14465 let MetalWarmPending {
14466 mut graph,
14467 cpu_stored,
14468 b,
14469 } = pending;
14470 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14471 if !graph.sync() {
14472 return false;
14473 }
14474 spec_stamp("w.gpu");
14475 let mut kbuf = vec![0f32; b * nkv * hd];
14476 let mut vbuf = vec![0f32; b * nkv * hd];
14477 if !crate::gpu_metal::kv_mirror_read_rows(
14478 self.mtp_kv_id(),
14479 Self::MTP_LAYER_BASE,
14480 nkv,
14481 hd,
14482 cpu_stored,
14483 b,
14484 &mut kbuf,
14485 &mut vbuf,
14486 ) {
14487 return false;
14488 }
14489 for r in 0..b {
14490 m.kv.append(
14491 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14492 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14493 &[],
14494 );
14495 }
14496 crate::gpu_metal::kv_mirror_set_stored(
14497 self.mtp_kv_id(),
14498 Self::MTP_LAYER_BASE,
14499 cpu_stored + b,
14500 );
14501 spec_stamp("w.kv");
14502 true
14503 }
14504
14505 pub(crate) fn note_draft_id(&mut self, id: u32) {
14512 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14513 if (id as usize) >= cut {
14514 self.draft_full_streak = 16;
14515 } else {
14516 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14517 }
14518 }
14519
14520 fn draft_head_rows(&self, head_rows: usize) -> usize {
14523 if self.draft_full_streak > 0 {
14524 head_rows
14525 } else {
14526 Self::draft_vocab_rows(head_rows)
14527 }
14528 }
14529
14530 fn draft_vocab_rows(head_rows: usize) -> usize {
14533 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14534 let n = *N.get_or_init(|| {
14535 std::env::var("CMF_DRAFT_VOCAB")
14536 .ok()
14537 .and_then(|v| v.parse().ok())
14538 .unwrap_or(65536)
14539 });
14540 if n == 0 { head_rows } else { n.min(head_rows) }
14541 }
14542
14543 #[cfg(target_os = "macos")]
14548 fn mtp_step_metal(
14549 &mut self,
14550 m: &mut MtpModule,
14551 hidden: &[f32],
14552 next_token: u32,
14553 position: usize,
14554 want_logits: bool,
14555 ) -> Option<(Vec<f32>, Vec<f32>)> {
14556 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14557 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14558 || !crate::gpu::q1_force()
14559 || !crate::gpu::enabled_here()
14560 || self.attn_softcap > 0.0
14561 || self.attention_heads_per_layer.is_some()
14562 || m.kv.mode != crate::kv_cache::KvMode::F32
14563 || m.kv.o1.is_some()
14564 {
14565 return None;
14566 }
14567 let AttnKind::Full {
14568 wq,
14569 wk,
14570 wv,
14571 wo,
14572 q_norm,
14573 k_norm,
14574 output_gate,
14575 softplus_gate: None,
14576 bias: None,
14577 } = &m.layer.attn
14578 else {
14579 return None;
14580 };
14581 let FfnKind::Dense(d) = &m.layer.ffn else {
14582 return None;
14583 };
14584 if d.act != Act::Silu || !d.segs.is_empty() {
14585 return None;
14586 }
14587 let (pq, pk, pv, po) = (
14588 wq.q1_parts()?,
14589 wk.q1_parts()?,
14590 wv.q1_parts()?,
14591 wo.q1_parts()?,
14592 );
14593 let (g, u, dn) = (
14594 d.gate_proj.q1_parts()?,
14595 d.up_proj.q1_parts()?,
14596 d.down_proj.q1_parts()?,
14597 );
14598 let QTensor::Mapped { model, .. } = wq else {
14599 return None;
14600 };
14601 let model = model.clone();
14602 let lm = if want_logits {
14603 Some(self.weights.lm_head.q1_parts()?)
14604 } else {
14605 None
14606 };
14607 let dims = GraphDims {
14608 hidden: self.hidden_size,
14609 eps: self.rms_eps as f32,
14610 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14611 };
14612 let hs = self.hidden_size;
14615 let mut x = vec![0f32; hs];
14616 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14623 crate::gpu_metal::DENSE_MV,
14624 );
14625 let mut graph = TokenGraph::new(&model, dims, &x)?;
14626 let mut folded = false;
14627 if let Some(eh) = m.eh_proj.q1_parts() {
14628 let e = self.embed_single(next_token);
14629 let mut cat = vec![0.0f32; 2 * hs];
14630 let (cat_e, cat_h) = cat.split_at_mut(hs);
14631 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14632 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14633 folded = graph.encode_input_proj(eh, &cat);
14634 }
14635 if !folded {
14636 x = self.mtp_block_input(m, hidden, next_token);
14637 graph = TokenGraph::new(&model, dims, &x)?;
14638 }
14639 spec_stamp("d.in");
14640 let l = AttnGpuLayer {
14641 attn_norm: &m.layer.input_norm,
14642 post_norm: &m.layer.post_norm,
14643 wq: pq,
14644 wk: pk,
14645 wv: pv,
14646 wo: po,
14647 ffn: MetalFfn::Dense {
14648 gate: g,
14649 up: u,
14650 down: dn,
14651 act: (d.act == Act::Gelu) as u8,
14652 },
14653 attn_out_norm: None,
14654 ffn_out_norm: None,
14655 layer_scale: None,
14656 };
14657 let (nh, nkv, hd, rd) = (
14658 self.num_heads,
14659 self.num_kv_heads,
14660 self.head_dim,
14661 self.rotary_dim,
14662 );
14663 let inv_freq = self.inv_freq.clone();
14664 {
14665 let cache = &m.kv;
14666 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14667 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14668 let cpu_stored = cpu_k[0].len() / hd;
14669 let p = AttnDeviceParams {
14670 kv_id: self.mtp_kv_id(),
14671 layer: Self::MTP_LAYER_BASE,
14672 nh,
14673 nkv,
14674 hd,
14675 rd,
14676 position,
14677 scale: self.attn_scale,
14678 eps: self.rms_eps as f32,
14679 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14680 late_qk_norm: self.qk_norm_after_rope,
14681 output_gate: *output_gate,
14682 q_norm: q_norm.as_deref(),
14683 k_norm: k_norm.as_deref(),
14684 inv_freq: &inv_freq,
14685 rope_scale: self.rope_scale,
14686 cpu_k,
14687 cpu_v,
14688 cpu_stored,
14689 cpu_gen: cache.generation(),
14690 o1: None,
14691 window: None,
14692 softmax_sink: None,
14693 head_gate: None,
14694 v_norm: false,
14695 };
14696 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14697 return None;
14698 }
14699 }
14700 let draft_rows = if let Some(lm) = lm {
14706 self.draft_head_rows(lm.1)
14707 } else {
14708 0
14709 };
14710 if let Some(lm) = lm {
14711 if !graph.lm_head_ok(lm) {
14712 return None;
14713 }
14714 if draft_rows < lm.1 {
14715 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14716 return None;
14717 }
14718 } else {
14719 graph.encode_lm_head(&m.final_norm, lm);
14720 }
14721 }
14722 spec_stamp("d.enc");
14723 if graph.sync_checked().is_err() {
14724 return None;
14725 }
14726 spec_stamp("d.gpu");
14727 let mut logits = Vec::new();
14728 if let Some(lm) = lm {
14729 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14730 logits = attention::take_buf(n_read);
14731 graph.read_logits(&mut logits);
14732 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14734 }
14735 graph.finish(&mut x);
14736 let mut krow = attention::take_buf(nkv * hd);
14737 let mut vrow = attention::take_buf(nkv * hd);
14738 if crate::gpu_metal::kv_mirror_read_last(
14739 self.mtp_kv_id(),
14740 Self::MTP_LAYER_BASE,
14741 nkv,
14742 hd,
14743 &mut krow,
14744 &mut vrow,
14745 ) {
14746 m.kv.append(&krow, &vrow, &[]);
14747 }
14748 attention::recycle_buf(&mut krow);
14749 attention::recycle_buf(&mut vrow);
14750 spec_stamp("d.rd");
14751 Some((logits, x))
14752 }
14753
14754 fn mtp_chain_on() -> bool {
14767 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14768 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14769 }
14770
14771 #[cfg(target_os = "macos")]
14783 fn mtp_draft_chain_metal(
14784 &mut self,
14785 m: &mut MtpModule,
14786 hidden: &[f32],
14787 t_next: u32,
14788 position: usize,
14789 k: usize,
14790 ) -> Result<Vec<u32>, bool> {
14791 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14792 if k == 0
14793 || k > 64
14794 || !Self::mtp_chain_on()
14795 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14796 || !crate::gpu::q1_force()
14797 || !crate::gpu::enabled_here()
14798 || self.attn_softcap > 0.0
14799 || self.attention_heads_per_layer.is_some()
14800 || m.kv.mode != crate::kv_cache::KvMode::F32
14801 || m.kv.o1.is_some()
14802 || self.dsv4.is_some()
14804 || self.dsv41.is_some()
14805 || self.qwen4_exp.is_some()
14806 || self.per_token_stack()
14807 {
14808 return Err(false);
14809 }
14810 let AttnKind::Full {
14811 wq,
14812 wk,
14813 wv,
14814 wo,
14815 q_norm,
14816 k_norm,
14817 output_gate,
14818 softplus_gate: None,
14819 bias: None,
14820 } = &m.layer.attn
14821 else {
14822 return Err(false);
14823 };
14824 let FfnKind::Dense(d) = &m.layer.ffn else {
14825 return Err(false);
14826 };
14827 if d.act != Act::Silu || !d.segs.is_empty() {
14828 return Err(false);
14829 }
14830 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14831 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14832 else {
14833 return Err(false);
14834 };
14835 let (Some(g), Some(u), Some(dn)) = (
14836 d.gate_proj.q1_parts(),
14837 d.up_proj.q1_parts(),
14838 d.down_proj.q1_parts(),
14839 ) else {
14840 return Err(false);
14841 };
14842 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14843 return Err(false);
14844 };
14845 let QTensor::Mapped { model, .. } = wq else {
14846 return Err(false);
14847 };
14848 let model = model.clone();
14849 let QTensor::Mapped {
14852 model: em,
14853 idx: eidx,
14854 dtype: cortiq_core::TensorDtype::Q4TiledP,
14855 ..
14856 } = &self.weights.embed_tokens
14857 else {
14858 return Err(false);
14859 };
14860 if !std::sync::Arc::ptr_eq(em, &model)
14861 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14862 {
14863 return Err(false);
14864 }
14865 let embed = (
14866 *eidx,
14867 self.weights.embed_tokens.rows(),
14868 self.weights.embed_tokens.cols(),
14869 );
14870 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14871 return Err(false);
14872 }
14873 let dims = GraphDims {
14874 hidden: self.hidden_size,
14875 eps: self.rms_eps as f32,
14876 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14877 };
14878 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14882 crate::gpu_metal::DENSE_MV,
14883 );
14884 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14885 return Err(false);
14886 };
14887 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14888 return Err(false);
14889 }
14890 let l = AttnGpuLayer {
14891 attn_norm: &m.layer.input_norm,
14892 post_norm: &m.layer.post_norm,
14893 wq: pq,
14894 wk: pk,
14895 wv: pv,
14896 wo: po,
14897 ffn: MetalFfn::Dense {
14898 gate: g,
14899 up: u,
14900 down: dn,
14901 act: (d.act == Act::Gelu) as u8,
14902 },
14903 attn_out_norm: None,
14904 ffn_out_norm: None,
14905 layer_scale: None,
14906 };
14907 let (nh, nkv, hd, rd) = (
14908 self.num_heads,
14909 self.num_kv_heads,
14910 self.head_dim,
14911 self.rotary_dim,
14912 );
14913 let inv_freq = self.inv_freq.clone();
14914 let draft_rows = self.draft_head_rows(lm.1);
14915 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14916 if n_arg == 0 {
14917 return Err(false);
14918 }
14919 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14926 let t_chain = std::time::Instant::now();
14927 graph.chain_ids_init(t_next, k);
14928 let cpu_stored;
14929 {
14930 let cache = &m.kv;
14931 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14932 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14933 cpu_stored = cpu_k[0].len() / hd;
14934 for j in 0..k {
14935 if !graph.encode_chain_input(
14936 embed,
14937 j as u32,
14938 &m.enorm,
14939 &m.hnorm,
14940 self.embed_multiplier,
14941 eh,
14942 ) {
14943 return Err(false);
14944 }
14945 let p = AttnDeviceParams {
14949 kv_id: self.mtp_kv_id(),
14950 layer: Self::MTP_LAYER_BASE,
14951 nh,
14952 nkv,
14953 hd,
14954 rd,
14955 position: position + j,
14956 scale: self.attn_scale,
14957 eps: self.rms_eps as f32,
14958 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14959 late_qk_norm: self.qk_norm_after_rope,
14960 output_gate: *output_gate,
14961 q_norm: q_norm.as_deref(),
14962 k_norm: k_norm.as_deref(),
14963 inv_freq: &inv_freq,
14964 rope_scale: self.rope_scale,
14965 cpu_k: cpu_k.clone(),
14966 cpu_v: cpu_v.clone(),
14967 cpu_stored: cpu_stored + j,
14968 cpu_gen: cache.generation(),
14969 o1: None,
14970 window: None,
14971 softmax_sink: None,
14972 head_gate: None,
14973 v_norm: false,
14974 };
14975 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14976 return Err(false);
14977 }
14978 if draft_rows < lm.1 {
14979 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14980 return Err(false);
14981 }
14982 } else {
14983 graph.encode_lm_head(&m.final_norm, lm);
14984 }
14985 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14986 return Err(false);
14987 }
14988 if split {
14989 graph.commit();
14992 }
14993 }
14994 }
14995 let t_enc = t_chain.elapsed();
14996 if graph.sync_checked().is_err() {
14997 return Err(true);
14998 }
14999 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
15000 eprintln!(
15001 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
15002 t_enc.as_secs_f64() * 1e3,
15003 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
15004 if split { ", split" } else { "" }
15005 );
15006 }
15007 let mut ids = vec![0u32; k];
15008 if !graph.chain_ids_read(&mut ids) {
15009 return Err(true);
15010 }
15011 let mut kbuf = vec![0f32; k * nkv * hd];
15012 let mut vbuf = vec![0f32; k * nkv * hd];
15013 if !crate::gpu_metal::kv_mirror_read_rows(
15014 self.mtp_kv_id(),
15015 Self::MTP_LAYER_BASE,
15016 nkv,
15017 hd,
15018 cpu_stored,
15019 k,
15020 &mut kbuf,
15021 &mut vbuf,
15022 ) {
15023 return Err(true);
15024 }
15025 for r in 0..k {
15026 m.kv.append(
15027 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15028 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15029 &[],
15030 );
15031 }
15032 Ok(ids)
15033 }
15034
15035 fn try_batch_graph_wgpu(
15036 &self,
15037 hiddens: &mut [f32],
15038 positions: &[usize],
15039 k: usize,
15040 spec: Option<crate::gpu::SpecTail<'_>>,
15041 ) -> crate::gpu::BatchGraphOutcome {
15042 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15043 }
15044
15045 fn try_batch_graph_wgpu_prefix(
15050 &self,
15051 hiddens: &mut [f32],
15052 positions: &[usize],
15053 k: usize,
15054 spec: Option<crate::gpu::SpecTail<'_>>,
15055 layers_run: Option<&mut usize>,
15056 ) -> crate::gpu::BatchGraphOutcome {
15057 let graph_end = match self.mimo_moe.graph_prefix_end() {
15058 Some(end) if end < self.num_layers => {
15059 if layers_run.is_none() || spec.is_some() || end == 0 {
15060 return crate::gpu::BatchGraphOutcome::Declined;
15061 }
15062 end
15063 }
15064 _ => self.num_layers,
15065 };
15066 let _tb = std::time::Instant::now();
15067 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15068 if self.attn_softcap > 0.0 {
15069 return crate::gpu::BatchGraphOutcome::Declined; }
15071 if let Some(reason) = self
15074 .wgpu_graph_attn_decline()
15075 .or_else(|| self.batch_graph_gemma_decline())
15076 {
15077 self.note_graph_decline("wgpu batch graph", reason);
15078 return crate::gpu::BatchGraphOutcome::Declined;
15079 }
15080 let nh = self.num_heads;
15081 let (nkv, hd, rd) = self.layer_geom(0);
15082 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15083 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15084 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15085 {
15086 let name = &m.tensors[i].name;
15087 let prism = if crate::prism::is_inverse_embedding(m, name) {
15088 crate::gpu::GraphPrismOp::InverseEmbedding
15089 } else if crate::prism::is_forward_weight(m, name) {
15090 crate::gpu::GraphPrismOp::Forward
15091 } else {
15092 crate::gpu::GraphPrismOp::None
15093 };
15094 return Some(crate::gpu::GraphW {
15095 idx: i,
15096 kind,
15097 row_scale: rs,
15098 data: &[],
15099 prism,
15100 affine: crate::prism::is_affine_target(m, name),
15101 });
15102 }
15103 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15104 eprintln!(
15105 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15106 t.rows(),
15107 t.cols()
15108 );
15109 }
15110 t.as_f32().map(|d| crate::gpu::GraphW {
15111 idx: 0,
15112 kind: 4,
15113 row_scale: &[],
15114 data: d,
15115 prism: crate::gpu::GraphPrismOp::None,
15116 affine: false,
15117 })
15118 }
15119 let built: Option<(
15120 Vec<crate::gpu::GraphLayer<'_>>,
15121 std::sync::Arc<cortiq_core::CmfModel>,
15122 )> = (|| {
15123 let mut layers = Vec::with_capacity(graph_end);
15124 let mut model = None;
15125 for li in 0..graph_end {
15126 let lw = &self.weights.layers[self.phys_layer(li)];
15127 let gffn = match &lw.ffn {
15134 FfnKind::Dense(d) if !d.segs.is_empty() => {
15135 if batch_debug {
15136 eprintln!("batch graph: dense segmented FFN at layer {li}");
15137 }
15138 return None;
15139 }
15140 FfnKind::Dense(d) => {
15141 let Some(act) = d.act.wgpu_graph_act() else {
15142 if batch_debug {
15143 eprintln!(
15144 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15145 d.act
15146 );
15147 }
15148 return None;
15149 };
15150 crate::gpu::GraphFfn::Dense {
15151 gate: gw(&d.gate_proj)?,
15152 up: gw(&d.up_proj)?,
15153 down: gw(&d.down_proj)?,
15154 act,
15155 }
15156 }
15157 FfnKind::Moe(m) => {
15158 if m.route_tau.is_some() || m.mask.is_some() {
15165 return None;
15166 }
15167 let shared = m.shared.as_ref();
15171 let has_shared = shared.is_some();
15172 let shared_gated = matches!(shared, Some((_, Some(_))));
15173 let sgate = match shared {
15174 Some((_, Some(sg))) => gw(sg)?,
15175 _ => gw(&m.router)?,
15179 };
15180 let router = gw(&m.router)?;
15181 if router.prism != crate::gpu::GraphPrismOp::None
15187 || router.affine
15188 || sgate.prism != crate::gpu::GraphPrismOp::None
15189 || sgate.affine
15190 {
15191 return None;
15192 }
15193 let inter = m.experts.first()?.gate_proj.rows();
15194 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15195 let mut q4tp: Option<bool> = None;
15196 let mut gu_q2: Option<bool> = None;
15197 let mut q82: Option<bool> = None;
15198 let mut moe_act = None;
15202 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15203 let Some(e_act) = e.act.wgpu_graph_act() else {
15204 return None;
15205 };
15206 match moe_act {
15207 Some(existing) if existing != e_act => return None,
15208 None => moe_act = Some(e_act),
15209 _ => {}
15210 }
15211 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15212 return None;
15213 }
15214 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15218 {
15219 Some((mm, gi)) => (
15220 mm,
15221 gi,
15222 e.up_proj.mapped_q4t()?.1,
15223 e.down_proj.mapped_q4t()?.1,
15224 false,
15225 false,
15226 false,
15227 ),
15228 None => match e.gate_proj.mapped_q2tp() {
15229 Some((mm, gi)) => (
15230 mm,
15231 gi,
15232 e.up_proj.mapped_q2tp()?.1,
15233 e.down_proj.mapped_q4tp()?.1,
15234 true,
15235 true,
15236 false,
15237 ),
15238 None => match e.gate_proj.mapped_q4tp() {
15239 Some((mm, gi)) => (
15240 mm,
15241 gi,
15242 e.up_proj.mapped_q4tp()?.1,
15243 e.down_proj.mapped_q4tp()?.1,
15244 true,
15245 false,
15246 false,
15247 ),
15248 None => {
15249 if !crate::gpu::moe_q82_graph_on() {
15250 return None;
15251 }
15252 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15253 (
15254 mm,
15255 gi,
15256 e.up_proj.mapped_q8_2f()?.1,
15257 e.down_proj.mapped_q8_2f()?.1,
15258 false,
15259 false,
15260 true,
15261 )
15262 }
15263 },
15264 },
15265 };
15266 if *q4tp.get_or_insert(is_p) != is_p
15267 || *gu_q2.get_or_insert(is_q2) != is_q2
15268 || *q82.get_or_insert(is_8) != is_8
15269 {
15270 return None;
15271 }
15272 if [gi, ui, di].into_iter().any(|idx| {
15273 mm.tensors.get(idx).is_some_and(|t| {
15274 crate::prism::is_forward_weight(mm, &t.name)
15275 || crate::prism::is_affine_target(mm, &t.name)
15276 })
15277 }) {
15278 return None;
15279 }
15280 model.get_or_insert_with(|| mm.clone());
15281 experts.push((gi, ui, di));
15282 }
15283 crate::gpu::GraphFfn::Moe {
15284 router,
15285 shared_gate: sgate,
15286 experts,
15287 n_exp: m.experts.len(),
15288 top_k: m.top_k,
15289 inter,
15290 act: moe_act?,
15291 norm_topk: m.norm_topk_prob,
15292 q4tp: q4tp?,
15293 gu_q2: gu_q2.unwrap_or(false),
15294 q82: q82.unwrap_or(false),
15295 sigmoid: m.router_sigmoid,
15296 sigmoid_denom_eps: m.sigmoid_denom_eps,
15297 bias: m.expert_bias.as_deref(),
15298 has_shared,
15299 detached_shared: None,
15300 shared_gated,
15301 route_scale: m.routed_scaling,
15302 }
15303 }
15304 _ => return None,
15305 };
15306 let attn = match &lw.attn {
15307 AttnKind::Full {
15308 wq,
15309 wk,
15310 wv,
15311 wo,
15312 q_norm,
15313 k_norm,
15314 output_gate,
15315 softplus_gate,
15316 bias,
15317 } => {
15318 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15319 if batch_debug {
15320 eprintln!(
15321 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15322 softplus_gate.is_some(),
15323 self.attention_heads_per_layer.is_some()
15324 );
15325 }
15326 return None;
15327 }
15328 let (m, _, _, _) =
15329 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15330 model = Some(m.clone());
15331 crate::gpu::GraphAttn::Full {
15332 wq: gw(wq)?,
15333 wk: gw(wk)?,
15334 wv: gw(wv)?,
15335 wo: gw(wo)?,
15336 q_norm: q_norm.as_deref(),
15337 k_norm: k_norm.as_deref(),
15338 late_qk_norm: self.qk_norm_after_rope,
15339 bias: bias
15340 .as_ref()
15341 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15342 output_gate: *output_gate,
15343 cpu_k: self.kv_cache.layers[li].k_heads(),
15344 cpu_v: self.kv_cache.layers[li].v_heads(),
15345 cpu_base: self.kv_cache.layers[li].base(),
15346 geom: self.graph_attn_geom(li),
15347 head_gate: None,
15350 }
15351 }
15352 AttnKind::LinearGdn(w) => {
15353 let Some(cfg) = self.gdn_cfg else {
15354 if batch_debug {
15355 eprintln!("batch graph: no GDN config at layer {li}");
15356 }
15357 return None;
15358 };
15359 let (m, _, _, _) = w
15360 .in_proj_qkv
15361 .graph_weight()
15362 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15363 model = Some(m.clone());
15364 crate::gpu::GraphAttn::Gdn {
15365 qkv: gw(&w.in_proj_qkv)?,
15366 z: gw(&w.in_proj_z)?,
15367 a: gw(&w.in_proj_a)?,
15368 b: gw(&w.in_proj_b)?,
15369 out: gw(&w.out_proj)?,
15370 conv1d: &w.conv1d,
15371 a_log: &w.a_log,
15372 dt_bias: &w.dt_bias,
15373 norm: &w.norm,
15374 nv: cfg.num_v_heads,
15375 nk: cfg.num_k_heads,
15376 dk: cfg.key_head_dim,
15377 dv: cfg.value_head_dim,
15378 kk: cfg.conv_kernel,
15379 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15380 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15381 }
15382 }
15383 _ => return None,
15384 };
15385 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15388 layers.push(crate::gpu::GraphLayer {
15389 input_norm: &lw.input_norm,
15390 attn,
15391 post_norm: &lw.post_norm,
15392 attn_out_norm: if g4 {
15393 lw.attn_out_norm.as_deref()
15394 } else {
15395 None
15396 },
15397 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15398 layer_scale: if g4 { lw.layer_scale } else { None },
15399 ffn: gffn,
15400 });
15401 }
15402 Some((layers, model?))
15403 })();
15404 let Some((layers, model)) = built else {
15405 {
15406 use std::sync::atomic::{AtomicBool, Ordering};
15407 static SAID: AtomicBool = AtomicBool::new(false);
15408 if !SAID.swap(true, Ordering::Relaxed) {
15409 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15410 }
15411 }
15412 return crate::gpu::BatchGraphOutcome::Declined;
15413 };
15414 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15415 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15416 }
15417 crate::gpu::forward_batch_graph(
15418 &model,
15419 self.graph_kv_id,
15420 &layers,
15421 &self.inv_freq,
15422 hiddens,
15423 nh,
15424 nkv,
15425 hd,
15426 rd,
15427 self.hidden_size,
15428 self.intermediate_size,
15429 positions,
15430 self.kv_cache.max_seq_len,
15431 gemma,
15432 self.rms_eps as f32,
15433 self.attn_scale,
15434 k,
15435 &(0..graph_end)
15436 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15437 .collect::<Vec<_>>(),
15438 self.o1_epoch,
15439 spec,
15440 layers_run,
15441 )
15442 }
15443
15444 fn draft_probe() -> bool {
15448 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15449 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15450 }
15451
15452 #[cfg(feature = "gpu")]
15464 fn dsv4_spec_on() -> bool {
15465 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15466 *ON.get_or_init(|| {
15467 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15471 return v != "0";
15472 }
15473 std::env::var("CMF_DSV4_SPEC")
15480 .map(|v| v != "0")
15481 .unwrap_or_else(|_| {
15482 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15483 })
15484 })
15485 }
15486
15487 #[cfg(feature = "gpu")]
15494 fn dsv4_spec_step(
15495 &mut self,
15496 tip_token: u32,
15497 t_next: u32,
15498 next_pos: usize,
15499 max_extra: usize,
15500 drafted: &mut usize,
15501 accepted_ctr: &mut usize,
15502 ) -> Option<(Vec<u32>, usize)> {
15503 let t_all = std::time::Instant::now();
15504 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15505 thread_local! {
15506 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15507 const { std::cell::Cell::new(None) };
15508 }
15509 LAST.with(|l| {
15510 if let Some(prev) = l.get() {
15511 eprintln!(
15512 "between rounds {:.1} ms",
15513 prev.elapsed().as_secs_f64() * 1e3
15514 );
15515 }
15516 l.set(Some(std::time::Instant::now()));
15517 });
15518 }
15519 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15520 eprintln!("spec_step: enter pos={next_pos}");
15521 }
15522 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15523 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15524 if self.dspark.is_none() {
15526 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15527 if t.is_empty() {
15528 return None;
15529 }
15530 crate::dsv4::dspark_arm(&t, cfg.dim);
15531 self.dspark = Some(crate::dsv4::DsparkState::new(
15532 self.dsv4_mtp.len(),
15533 &cfg,
15534 t.len(),
15535 ));
15536 }
15537 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15538 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15539 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15540 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15541 }
15542 let pack = pack?;
15543 let block = crate::dsv4::dspark_block();
15544 let b_box = self.dsv4.as_mut()?;
15545 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15546 let ds = self.dspark.as_mut()?;
15547 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15550 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15551 if dbg {
15552 eprintln!("spec_step: no capture");
15553 }
15554 return None;
15555 }
15556 ds.have_hidden = true;
15557 let tip_pos = next_pos.checked_sub(1)?;
15558 let draft_started = std::time::Instant::now();
15559 let mut conf = Vec::new();
15560 let props = crate::dsv4::dspark_draft_gpu(
15561 g,
15562 &self.dsv4_mtp,
15563 &cfg,
15564 ds,
15565 pack,
15566 st.kv_id,
15567 tip_token,
15568 tip_pos,
15569 self.pool.as_deref(),
15570 &mut conf,
15571 );
15572 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15573 *drafted += block;
15574 if props.is_empty() || props[0] != t_next {
15575 if dbg {
15576 eprintln!(
15577 "spec_step: draft {} (props0={:?} t_next={t_next})",
15578 if props.is_empty() {
15579 "empty"
15580 } else {
15581 "miss"
15582 },
15583 props.first()
15584 );
15585 }
15586 return None;
15587 }
15588 let mut k_verify = crate::dsv4::dspark_verify_k()
15595 .min(props.len())
15596 .min(max_extra.saturating_add(1));
15597 let conf_min = {
15603 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15604 *M.get_or_init(|| {
15605 std::env::var("CMF_DSPARK_CONF_MIN")
15606 .ok()
15607 .and_then(|v| v.parse().ok())
15608 .unwrap_or(0.0)
15609 })
15610 };
15611 if conf_min > 0.0 && conf.len() >= props.len() {
15612 let mut keep = 1usize;
15613 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15614 keep += 1;
15615 }
15616 k_verify = k_verify.min(keep.max(2));
15617 }
15618 if k_verify < 2 {
15619 return None;
15620 }
15621 let mut fed = Vec::with_capacity(k_verify);
15622 fed.push(t_next);
15623 fed.extend_from_slice(&props[1..k_verify]);
15624 let mut argmax = Vec::new();
15625 let mut logits_all = Vec::new();
15626 let mut walked = Vec::new();
15627 let txn = crate::dsv4::dsv4_verify_chunk(
15628 g,
15629 layers,
15630 &cfg,
15631 st,
15632 &fed,
15633 next_pos,
15634 &self.inv_freq,
15635 self.pool.as_deref(),
15636 &targets,
15637 &mut argmax,
15638 &mut logits_all,
15639 &mut walked,
15640 );
15641 if txn.is_none() && dbg {
15642 eprintln!("spec_step: verify declined");
15643 }
15644 let txn = txn?;
15645 let spec_gpu_end = txn.gpu_end;
15646 let b = fed.len();
15647 let mut accepted = 1usize;
15648 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15649 accepted += 1;
15650 }
15651 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15656 accepted = 1;
15657 }
15658 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15659 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15660 }
15661 let t_fin = std::time::Instant::now();
15662 if !crate::dsv4::dsv4_spec_finish(
15663 g,
15664 layers,
15665 &cfg,
15666 st,
15667 txn,
15668 accepted,
15669 &fed,
15670 &self.inv_freq,
15671 self.pool.as_deref(),
15672 ) {
15673 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15674 return None;
15675 }
15676 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15677 eprintln!(
15678 "finish(k={accepted}): {:.1} ms",
15679 t_fin.elapsed().as_secs_f64() * 1e3
15680 );
15681 }
15682 *accepted_ctr += accepted - 1;
15683 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15688 let dev_caps: Vec<usize> = targets
15693 .iter()
15694 .copied()
15695 .filter(|&t| t < spec_gpu_end)
15696 .collect();
15697 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15698 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15699 return None;
15700 }
15701 for t in 0..accepted {
15702 let tip = t + 1 == accepted;
15703 for (slot, &tl) in targets.iter().enumerate() {
15704 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15705 let lo = (di * b + t) * hc * dim;
15706 crate::dsv4::dspark_capture(
15707 &caps_all[lo..lo + hc * dim],
15708 &cfg,
15709 slot,
15710 &mut ds.main_hidden,
15711 );
15712 } else if tip
15713 && crate::dsv4::dspark_peek_slot(slot, dim, {
15714 let lo = slot * dim;
15715 &mut ds.main_hidden[lo..lo + dim]
15716 })
15717 {
15718 } else {
15723 crate::dsv4::dspark_capture(
15727 &walked[t * hc * dim..(t + 1) * hc * dim],
15728 &cfg,
15729 slot,
15730 &mut ds.main_hidden,
15731 );
15732 }
15733 }
15734 crate::dsv4::dspark_ring_append(
15735 g,
15736 &self.dsv4_mtp,
15737 &cfg,
15738 ds,
15739 next_pos + t,
15740 self.pool.as_deref(),
15741 );
15742 }
15743 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15744 self.graph_logits = Some(row);
15745 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15750 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15751 crate::dsv4::pick_tally_arm();
15752 }
15753 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15754 eprintln!(
15755 "spec_step total {:.1} ms (k={accepted})",
15756 t_all.elapsed().as_secs_f64() * 1e3
15757 );
15758 }
15759 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15760 }
15761
15762 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15763 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15764 return;
15765 }
15766 let trunk_now = crate::dsv4::pick_tally_take();
15768 crate::dsv4::trunk_freq_note(&trunk_now);
15769 if !trunk_now.is_empty() {
15770 self.dspark_trunk_picks.push(trunk_now);
15771 let keep = crate::dsv4::dspark_block();
15772 if self.dspark_trunk_picks.len() > keep {
15773 self.dspark_trunk_picks.remove(0);
15774 }
15775 }
15776 for p in std::mem::take(&mut self.dspark_pending) {
15779 let Some(i) = position.checked_sub(p.0 + 1) else {
15780 continue;
15781 };
15782 let mut p = p;
15783 if i < p.1.len() {
15784 if p.2 && p.1[i] == token_id {
15785 p.3 = i + 1;
15786 } else {
15787 p.2 = false;
15788 }
15789 if i + 1 < p.1.len() {
15790 self.dspark_pending.push(p);
15791 continue;
15792 }
15793 }
15794 self.dspark_hist.push(p.3);
15795 self.dspark_real.push(token_id);
15796 }
15797 let Some(b) = &mut self.dsv4 else { return };
15798 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15799 let n_layers = layers.len();
15800 if self.dspark.is_none() {
15801 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15802 if t.is_empty() {
15803 return;
15804 }
15805 eprintln!(
15806 "DSpark: capture from layers {t:?}, block {}",
15807 crate::dsv4::dspark_block()
15808 );
15809 crate::dsv4::dspark_arm(&t, cfg.dim);
15810 self.dspark = Some(crate::dsv4::DsparkState::new(
15811 self.dsv4_mtp.len(),
15812 &cfg,
15813 t.len(),
15814 ));
15815 }
15816 let ds = self.dspark.as_mut().unwrap();
15817 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15818 return; }
15820 let mut conf = Vec::new();
15821 crate::dsv4::pick_tally_arm();
15822 let draft_started = std::time::Instant::now();
15827 #[cfg(feature = "gpu")]
15828 let gpu_draft = crate::dsv4::dspark_gpu_on();
15829 #[cfg(not(feature = "gpu"))]
15830 let gpu_draft = false;
15831 let props = if gpu_draft {
15832 #[cfg(feature = "gpu")]
15833 {
15834 let kv_id = b.3.kv_id;
15835 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15836 Some(pk) => crate::dsv4::dspark_draft_gpu(
15837 g,
15838 &self.dsv4_mtp,
15839 &cfg,
15840 ds,
15841 pk,
15842 kv_id,
15843 token_id,
15844 position,
15845 self.pool.as_deref(),
15846 &mut conf,
15847 ),
15848 None => Vec::new(),
15849 }
15850 }
15851 #[cfg(not(feature = "gpu"))]
15852 Vec::new()
15853 } else {
15854 crate::gpu::cpu_scope(|| {
15855 crate::dsv4::dspark_draft(
15856 g,
15857 &self.dsv4_mtp,
15858 &cfg,
15859 ds,
15860 token_id,
15861 position,
15862 self.pool.as_deref(),
15863 &mut conf,
15864 )
15865 })
15866 };
15867 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15868 let draft_picks = crate::dsv4::pick_tally_take();
15869 crate::dsv4::dspark_freq_note(&draft_picks);
15870 crate::dsv4::pick_tally_arm();
15873 if !props.is_empty() {
15874 let (tu, tt) = {
15878 let flat: Vec<(usize, Vec<usize>)> = self
15879 .dspark_trunk_picks
15880 .iter()
15881 .flat_map(|v| v.iter().cloned())
15882 .collect();
15883 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15885 std::collections::HashMap::new();
15886 for (li, picks) in flat {
15887 per.entry(li).or_default().extend(picks);
15888 }
15889 let n = per.len().max(1);
15890 let mut u = 0usize;
15891 let mut t = 0usize;
15892 for (_, v) in per {
15893 t += v.len();
15894 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15895 }
15896 (u / n, t / n)
15897 };
15898 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15899 self.dspark_exp.push((tu, tt, du, dt));
15900 self.dspark_pending.push((position, props, true, 0));
15901 }
15902 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15903 let n = self.dspark_hist.len() as f32;
15904 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15905 let block = crate::dsv4::dspark_block();
15906 let mut at = vec![0usize; block + 1];
15907 for &k in &self.dspark_hist {
15908 at[k] += 1;
15909 }
15910 let mut surv = Vec::with_capacity(block);
15912 for i in 1..=block {
15913 let k = at[i..].iter().sum::<usize>() as f32 / n;
15914 surv.push(format!("{k:.2}"));
15915 }
15916 let distinct = self
15917 .dspark_real
15918 .iter()
15919 .collect::<std::collections::HashSet<_>>()
15920 .len();
15921 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15922 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15923 });
15924 let m = self.dspark_exp.len().max(1);
15925 eprintln!(
15926 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
15927 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
15928 self.dspark_hist.len(),
15929 mean + 1.0,
15930 surv.join(" ")
15931 );
15932 eprintln!(
15933 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
15934 trunk experts {}/{} per layer over {block} tokens, \
15935 draft {}/{} per block, draft {:.2} ms/block",
15936 self.dspark_real.len(),
15937 tu / m,
15938 tt / m,
15939 du / m,
15940 dt / m,
15941 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15942 );
15943 }
15944 }
15945
15946 fn forward_layers_upto(
15947 &mut self,
15948 hidden: &[f32],
15949 position: usize,
15950 task_mask: Option<&TaskMask>,
15951 upto: Option<usize>,
15952 ) -> Vec<f32> {
15953 if let Some(plan) = self.gpu_plan.clone() {
15959 if upto.is_none() && plan.len() > 1 {
15960 let mut h = hidden.to_vec();
15961 for &(dev, from, upto_incl) in plan.iter() {
15962 h = crate::gpu::with_device(dev, || {
15963 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15964 });
15965 }
15966 return h;
15967 }
15968 }
15969 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15970 }
15971
15972 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15977 self.set_gpu_plan_at(devices, None)
15978 }
15979
15980 pub fn set_gpu_plan_at(
15984 &mut self,
15985 devices: Option<&[usize]>,
15986 at: Option<usize>,
15987 ) -> Result<(), String> {
15988 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15989 self.gpu_plan = None;
15990 return Ok(());
15991 };
15992 self.split_supported()?;
15993 let n = self.num_layers;
15994 if devs.len() > n {
15995 return Err(format!("{} devices for {n} layers", devs.len()));
15996 }
15997 if let Some(k) = at {
15998 if k == 0 || k >= n {
15999 return Err(format!("split at {k}: the model has {n} layers"));
16000 }
16001 if devs.len() == 2 {
16002 self.gpu_plan = Some(std::sync::Arc::new(vec![
16003 (devs[0], 0, k - 1),
16004 (devs[1], k, n - 1),
16005 ]));
16006 return Ok(());
16007 }
16008 return Err(format!(
16009 "an explicit split point takes exactly 2 devices, got {}",
16010 devs.len()
16011 ));
16012 }
16013 let per = n.div_ceil(devs.len());
16014 let mut plan = Vec::with_capacity(devs.len());
16015 let mut from = 0usize;
16016 for &d in devs {
16017 if from >= n {
16018 break;
16019 }
16020 let upto = (from + per - 1).min(n - 1);
16021 plan.push((d, from, upto));
16022 from = upto + 1;
16023 }
16024 self.gpu_plan = Some(std::sync::Arc::new(plan));
16025 Ok(())
16026 }
16027
16028 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16030 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16031 }
16032
16033 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16039 if let Some(x) = t.as_f32() {
16040 return x.to_vec();
16041 }
16042 let mut out = vec![0.0; t.rows() * t.cols()];
16043 for r in 0..t.rows() {
16044 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16045 }
16046 out
16047 }
16048
16049 fn embryo_resident_eligible(&self) -> bool {
16050 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16053 || self.num_layers != self.physical_layers
16054 || self.loop_final_norm
16055 || self.weights.layers.len() != self.num_layers
16056 || self.head_clusters.is_none()
16057 || self.final_softcap.is_some()
16058 || self.logit_multiplier.is_some()
16059 || self.attn_softcap != 0.0
16060 || self.mtp.is_some()
16061 || self.per_token_stack()
16062 || self.dsv4.is_some()
16063 || self.dsv41.is_some()
16064 || self.qwen4_exp.is_some()
16065 || self.dyn_router.is_some()
16072 || self.dyn_phi_layer.is_some()
16073 || self.dyn_blend_loaded
16074 || self.o1_cfg.is_some()
16075 || self.swa.is_some()
16076 || self.sliding_layers.is_some()
16077 || self.global_attn.is_some()
16078 || self.attention_heads_per_layer.is_some()
16079 || self.attn_v_norm
16080 || self
16081 .kv_cache
16082 .layers
16083 .iter()
16084 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16085 || self.rope_scale != 1.0
16086 || self.rope_scale_local != 1.0
16087 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16088 || self.hidden_size == 0
16089 || self.hidden_size > 1024
16090 || self.intermediate_size > 1024
16091 || self.num_heads == 0
16092 || self.num_kv_heads == 0
16093 || self.num_heads % self.num_kv_heads != 0
16094 || self.num_heads.saturating_mul(self.head_dim) > 1024
16095 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16096 || self.vocab_size == 0
16097 || self.kv_cache.max_seq_len == 0
16098 || self.rotary_dim == 0
16099 || self.rotary_dim > self.head_dim
16100 || self.rotary_dim % 2 != 0
16101 || self.inv_freq.len() < self.rotary_dim / 2
16102 {
16103 return false;
16104 }
16105 if self.weights.lm_head.as_f32().is_none()
16116 || self.weights.embed_tokens.as_f32().is_none()
16117 || self.weights.lm_head.rows() < self.vocab_size
16118 || self.weights.lm_head.cols() != self.hidden_size
16119 || self.weights.embed_tokens.rows() < self.vocab_size
16120 || self.weights.embed_tokens.cols() != self.hidden_size
16121 || self.weights.final_norm.len() != self.hidden_size
16122 {
16123 return false;
16124 }
16125 if let Some(cfg) = self.vmf_cfg {
16126 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16127 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16128 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16129 || cfg.state_len() == 0
16130 {
16131 return false;
16132 }
16133 }
16134 if let Some(g) = self.gdn_cfg {
16135 if g.num_v_heads == 0
16140 || g.num_k_heads == 0
16141 || g.num_v_heads % g.num_k_heads != 0
16142 || g.key_head_dim == 0
16143 || g.key_head_dim > 128
16144 || g.value_head_dim == 0
16145 || g.value_head_dim > 256
16146 || g.value_head_dim % 4 != 0
16147 || g.conv_kernel == 0
16148 || g.num_v_heads > 512
16149 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16150 || g.conv_dim() > 2048
16151 || g.conv_dim() % 4 != 0
16152 || g.hidden_size != self.hidden_size
16153 || g.output_gate_sigmoid
16154 || g.rms_eps != self.rms_eps
16155 || g.state_len() == 0
16156 {
16157 return false;
16158 }
16159 }
16160 let mut full_seen = false;
16161 for lw in &self.weights.layers {
16162 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16163 return false;
16164 }
16165 match &lw.attn {
16166 AttnKind::LinearGdn(w) => {
16167 let Some(g) = self.gdn_cfg else {
16168 return false;
16169 };
16170 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16171 if w.in_proj_qkv.rows() != g.conv_dim()
16172 || w.in_proj_qkv.cols() != self.hidden_size
16173 || w.in_proj_qkv.as_f32().is_none()
16174 || w.in_proj_z.rows() != nv * dv
16175 || w.in_proj_z.cols() != self.hidden_size
16176 || w.in_proj_z.as_f32().is_none()
16177 || w.in_proj_a.rows() != nv
16178 || w.in_proj_a.cols() != self.hidden_size
16179 || w.in_proj_a.as_f32().is_none()
16180 || w.in_proj_b.rows() != nv
16181 || w.in_proj_b.cols() != self.hidden_size
16182 || w.in_proj_b.as_f32().is_none()
16183 || w.conv1d.len() != g.conv_dim() * kk
16184 || w.a_log.len() != nv
16185 || w.dt_bias.len() != nv
16186 || w.norm.len() != dv
16187 || w.out_proj.rows() != self.hidden_size
16188 || w.out_proj.cols() != nv * dv
16189 || w.out_proj.as_f32().is_none()
16190 {
16191 return false;
16192 }
16193 }
16194 AttnKind::Linear(w) => {
16195 let Some(cfg) = self.vmf_cfg else {
16196 return false;
16197 };
16198 if w.thq.rows() != cfg.num_heads * cfg.nphase
16199 || w.thq.cols() != self.hidden_size
16200 || w.thq.as_f32().is_none()
16201 || w.thk.rows() != cfg.num_heads * cfg.nphase
16202 || w.thk.cols() != self.hidden_size
16203 || w.thk.as_f32().is_none()
16204 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16205 || w.v_proj.cols() != self.hidden_size
16206 || w.v_proj.as_f32().is_none()
16207 || w.out_proj.rows() != self.hidden_size
16208 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16209 || w.out_proj.as_f32().is_none()
16210 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16211 {
16212 return false;
16213 }
16214 if let Some((kg, kb)) = &w.k_gate {
16215 if kg.rows() != cfg.num_heads
16216 || kg.cols() != self.hidden_size
16217 || kg.as_f32().is_none()
16218 || kb.len() != cfg.num_heads
16219 {
16220 return false;
16221 }
16222 }
16223 if let Some(conv) = &w.conv {
16224 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16225 return false;
16226 }
16227 }
16228 }
16229 AttnKind::Full {
16230 wq,
16231 wk,
16232 wv,
16233 wo,
16234 q_norm,
16235 k_norm,
16236 output_gate,
16237 softplus_gate,
16238 bias,
16239 } => {
16240 if full_seen
16241 || q_norm.is_some()
16242 || k_norm.is_some()
16243 || *output_gate
16244 || softplus_gate.is_some()
16245 || bias.is_some()
16246 || wq.as_f32().is_none()
16247 || wk.as_f32().is_none()
16248 || wv.as_f32().is_none()
16249 || wo.as_f32().is_none()
16250 || wq.rows() != self.num_heads * self.head_dim
16251 || wk.rows() != self.num_kv_heads * self.head_dim
16252 || wv.rows() != self.num_kv_heads * self.head_dim
16253 || wq.cols() != self.hidden_size
16254 || wk.cols() != self.hidden_size
16255 || wv.cols() != self.hidden_size
16256 || wo.rows() != self.hidden_size
16257 || wo.cols() != self.num_heads * self.head_dim
16258 {
16259 return false;
16260 }
16261 full_seen = true;
16262 }
16263 AttnKind::Bounded(w) => {
16264 let Some(ac) = self.anchor_core.as_ref() else {
16267 return false;
16268 };
16269 if self.bounded_rope.is_none()
16270 || w.window != ac.window
16271 || w.sink != ac.sink
16272 || w.window == 0
16273 || w.window + w.sink > 256
16274 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16275 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16276 || w.wq.as_f32().is_none()
16277 || w.wk.as_f32().is_none()
16278 || w.wv.as_f32().is_none()
16279 || w.wo.as_f32().is_none()
16280 || w.wq.rows() != self.num_heads * self.head_dim
16281 || w.wk.rows() != self.num_kv_heads * self.head_dim
16282 || w.wv.rows() != self.num_kv_heads * self.head_dim
16283 || w.wq.cols() != self.hidden_size
16284 || w.wk.cols() != self.hidden_size
16285 || w.wv.cols() != self.hidden_size
16286 || w.wo.rows() != self.hidden_size
16287 || w.wo.cols() != self.num_heads * self.head_dim
16288 {
16289 return false;
16290 }
16291 }
16292 _ => return false,
16293 }
16294 match &lw.ffn {
16295 FfnKind::Dense(d) => {
16296 if d.act != Act::Silu
16297 || !d.segs.is_empty()
16298 || d.gate_proj.as_f32().is_none()
16299 || d.up_proj.as_f32().is_none()
16300 || d.down_proj.as_f32().is_none()
16301 || d.gate_proj.rows() != self.intermediate_size
16302 || d.gate_proj.cols() != self.hidden_size
16303 || d.up_proj.rows() != self.intermediate_size
16304 || d.up_proj.cols() != self.hidden_size
16305 || d.down_proj.rows() != self.hidden_size
16306 || d.down_proj.cols() != self.intermediate_size
16307 {
16308 return false;
16309 }
16310 }
16311 FfnKind::Moe(m) => {
16312 if m.resonance.is_none()
16313 || m.top_k != 1
16314 || m.router_sigmoid
16315 || !m.norm_topk_prob
16316 || m.expert_bias.is_some()
16317 || m.routed_scaling != 1.0
16318 || m.route_tau.is_some()
16319 || m.shared.is_none()
16320 || m.mask.is_some()
16321 || m.per_expert_scale.is_some()
16322 || m.router_input_norm
16323 || m.experts.is_empty()
16324 || m.experts.len() > 8
16325 {
16326 return false;
16327 }
16328 let r = m.resonance.as_ref().unwrap();
16329 if r.mu.len() != m.experts.len() * self.hidden_size
16330 || r.bias.len() != m.experts.len()
16331 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16332 || r.k > 128
16333 {
16334 return false;
16335 }
16336 let Some((shared, gate)) = &m.shared else {
16337 return false;
16338 };
16339 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16340 return false;
16341 }
16342 if shared.gate_proj.as_f32().is_none()
16343 || shared.up_proj.as_f32().is_none()
16344 || shared.down_proj.as_f32().is_none()
16345 || shared.gate_proj.rows() != self.intermediate_size
16346 || shared.gate_proj.cols() != self.hidden_size
16347 || shared.up_proj.rows() != self.intermediate_size
16348 || shared.up_proj.cols() != self.hidden_size
16349 || shared.down_proj.rows() != self.hidden_size
16350 || shared.down_proj.cols() != self.intermediate_size
16351 {
16352 return false;
16353 }
16354 for e in &m.experts {
16355 if e.act != Act::Silu
16356 || !e.segs.is_empty()
16357 || e.gate_proj.as_f32().is_none()
16358 || e.up_proj.as_f32().is_none()
16359 || e.down_proj.as_f32().is_none()
16360 || e.gate_proj.rows() != self.intermediate_size
16361 || e.gate_proj.cols() != self.hidden_size
16362 || e.up_proj.rows() != self.intermediate_size
16363 || e.up_proj.cols() != self.hidden_size
16364 || e.down_proj.rows() != self.hidden_size
16365 || e.down_proj.cols() != self.intermediate_size
16366 {
16367 return false;
16368 }
16369 }
16370 }
16371 FfnKind::DenseMoe(_) => return false,
16372 }
16373 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16374 return false;
16375 }
16376 }
16377 if full_seen && self.anchor_core.is_some() {
16378 return false;
16379 }
16380 full_seen || self.num_layers > 0
16381 }
16382
16383 fn embryo_resident_wanted(&self) -> bool {
16388 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16389 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16390 && matches!(
16391 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16392 Ok("1") | Ok("parallel")
16393 )
16394 && crate::gpu::enabled_here()
16395 && !self.graph_refused()
16396 && self.embryo_resident_eligible()
16397 }
16398
16399 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16407 if ids.len() < 2
16408 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16409 || !self.embryo_resident_wanted()
16410 {
16411 return None;
16412 }
16413 let model = self.ensure_embryo_graph()?;
16414 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16415 .ok()
16416 .and_then(|v| v.parse::<usize>().ok())
16417 .filter(|&v| v >= 1)
16418 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16419 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16420 let hs = self.hidden_size;
16421 let n = ids.len();
16422 let mut pos = start;
16423 let mut last = None;
16424 let mut rows = Vec::with_capacity(cmax * hs);
16425 while pos < n {
16426 let end = (pos + cmax).min(n);
16427 rows.clear();
16428 for &id in &ids[pos..end] {
16429 rows.extend_from_slice(&self.embed_single(id));
16430 }
16431 let mut lg = Vec::new();
16432 if !crate::gpu::forward_embryo_graph_chunk(
16433 &model,
16434 self.graph_kv_id,
16435 &rows,
16436 pos,
16437 end - pos,
16438 &mut lg,
16439 ) {
16440 if pos == start {
16441 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16442 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16443 }
16444 return None;
16445 }
16446 self.kv_cache.clear();
16450 self.clear_history();
16451 crate::gpu::graph_kv_reset(self.graph_kv_id);
16452 panic!(
16453 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16454 );
16455 }
16456 last = Some(lg);
16457 pos = end;
16458 }
16459 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16460 eprintln!(
16461 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16462 n - start,
16463 (n - start).div_ceil(cmax)
16464 );
16465 }
16466 last
16467 }
16468
16469 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16470 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16471 const UMAX: u32 = u32::MAX;
16472 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16473 const REC: usize = 64;
16474 struct Pack {
16475 data: Vec<f32>,
16476 }
16477 impl Pack {
16478 fn put(&mut self, x: &[f32]) -> u32 {
16479 if x.is_empty() {
16480 return u32::MAX;
16481 }
16482 let off = self.data.len();
16483 self.data.extend_from_slice(x);
16484 off as u32
16485 }
16486 }
16487 let vmf = self.vmf_cfg;
16491 let gdn = self.gdn_cfg;
16492 let mut pack = Pack { data: Vec::new() };
16493 let mut meta = vec![0u32; HEADER];
16494 meta[0] = self.hidden_size as u32;
16495 meta[1] = self.intermediate_size as u32;
16496 meta[2] = self.vocab_size as u32;
16497 meta[3] = self.num_layers as u32;
16498 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16499 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16500 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16501 if let Some(g) = gdn {
16502 meta[24] = g.num_v_heads as u32;
16503 meta[25] = g.num_k_heads as u32;
16504 meta[26] = g.key_head_dim as u32;
16505 meta[27] = g.value_head_dim as u32;
16506 meta[28] = g.conv_kernel as u32;
16507 meta[29] = g.conv_dim() as u32;
16508 }
16509 meta[7] = self.num_heads as u32;
16510 meta[8] = self.num_kv_heads as u32;
16511 meta[9] = self.head_dim as u32;
16512 meta[10] = self.kv_cache.max_seq_len as u32;
16513 let clusters = self.head_clusters.as_ref().unwrap();
16514 let cluster_count = clusters.len() / self.hidden_size;
16515 if clusters.len() % self.hidden_size != 0
16516 || cluster_count == 0
16517 || cluster_count > 1024
16518 || self.vocab_size % cluster_count != 0
16519 || self.weights.lm_head.rows() < self.vocab_size
16520 || self.weights.final_norm.len() != self.hidden_size
16521 {
16522 return None;
16523 }
16524 meta[11] = cluster_count as u32;
16525 meta[12] = (self.vocab_size / cluster_count) as u32;
16526 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16527 meta[16] = self.rotary_dim as u32;
16528 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16529 meta[19] = (self.rms_eps as f32).to_bits();
16530 let max_conv = self
16531 .weights
16532 .layers
16533 .iter()
16534 .filter_map(|lw| match &lw.attn {
16535 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16536 _ => None,
16537 })
16538 .max()
16539 .unwrap_or(1);
16540 let phase_stride = vmf
16546 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16547 .unwrap_or(0);
16548 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16549 let state_stride = phase_stride.max(gdn_stride);
16550 let bounded = self.anchor_core.clone();
16554 let (anchor_window, anchor_sink) = bounded
16555 .as_ref()
16556 .map(|ac| (ac.window, ac.sink))
16557 .unwrap_or((0, 0));
16558 let kv_stride = if bounded.is_some() {
16559 2usize
16560 .saturating_mul(self.num_kv_heads)
16561 .saturating_mul(anchor_window)
16562 .saturating_mul(self.head_dim)
16563 } else {
16564 2usize
16565 .saturating_mul(self.num_kv_heads)
16566 .saturating_mul(self.kv_cache.max_seq_len)
16567 .saturating_mul(self.head_dim)
16568 };
16569 meta[14] = state_stride as u32;
16570 meta[15] = kv_stride as u32;
16571 meta[18] = anchor_window as u32;
16572 meta[20] = anchor_sink as u32;
16573 meta[21] = match &self.bounded_rope {
16574 Some(rope) => {
16575 let off = pack.put(&rope.cos);
16577 let _ = pack.put(&rope.sin);
16578 off
16579 }
16580 None => UMAX,
16581 };
16582 let mut full_seen = false;
16583 let mut bounded_seen = 0usize;
16584 let mut phase_seen = 0usize;
16588 let mut gdn_seen = 0usize;
16589 for (li, lw) in self.weights.layers.iter().enumerate() {
16590 let base = meta.len();
16591 meta.resize(base + REC, UMAX);
16592 meta[base] = match &lw.attn {
16593 AttnKind::Linear(w) if w.phase_delta => 1,
16594 AttnKind::Linear(_) => 0,
16595 AttnKind::Full { .. } => 2,
16596 AttnKind::Bounded(_) => 3,
16597 AttnKind::LinearGdn(_) => 4,
16598 _ => UMAX,
16599 };
16600 meta[base + 1] = pack.put(&lw.input_norm);
16601 meta[base + 2] = pack.put(&lw.post_norm);
16602 meta[base + 25] = match &lw.attn {
16603 AttnKind::Linear(_) => {
16604 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16605 phase_seen += 1;
16606 off
16607 }
16608 AttnKind::LinearGdn(_) => {
16609 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16610 gdn_seen += 1;
16611 off
16612 }
16613 _ => UMAX,
16614 };
16615 match &lw.attn {
16616 AttnKind::LinearGdn(w) => {
16617 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16620 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16621 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16622 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16623 meta[base + 60] = pack.put(&w.conv1d);
16624 meta[base + 61] = pack.put(&w.a_log);
16625 meta[base + 62] = pack.put(&w.dt_bias);
16626 meta[base + 63] = pack.put(&w.norm);
16627 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16628 meta[base + 24] = 0;
16629 }
16630 AttnKind::Linear(w) => {
16631 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16632 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16633 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16634 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16635 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16636 meta[base + 7] = pack.put(&decay);
16637 if let Some((kg, kb)) = &w.k_gate {
16638 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16639 meta[base + 9] = pack.put(kb);
16640 }
16641 if let Some(conv) = &w.conv {
16642 meta[base + 10] = pack.put(conv);
16643 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16644 } else {
16645 meta[base + 24] = 0;
16646 }
16647 }
16648 AttnKind::Full { wq, wk, wv, wo, .. } => {
16649 full_seen = true;
16650 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16651 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16652 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16653 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16654 meta[base + 26] = (li * kv_stride) as u32;
16655 }
16656 AttnKind::Bounded(w) => {
16657 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16658 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16659 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16660 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16661 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16663 meta[base + 27] = pack.put(&w.sink_k);
16664 meta[base + 28] = pack.put(&w.sink_v);
16665 bounded_seen += 1;
16666 }
16667 _ => return None,
16668 }
16669 match &lw.ffn {
16670 FfnKind::Dense(d) => {
16671 meta[base + 15] = 0;
16672 meta[base + 16] = 0;
16673 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16674 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16675 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16676 }
16677 FfnKind::Moe(m) => {
16678 let r = m.resonance.as_ref().unwrap();
16679 let (shared, _) = m.shared.as_ref().unwrap();
16680 meta[base + 15] = 1;
16681 meta[base + 16] = m.experts.len() as u32;
16682 meta[base + 17] = pack.put(&r.mu);
16683 meta[base + 18] = pack.put(&r.u);
16684 meta[base + 19] = pack.put(&r.bias);
16685 meta[base + 20] = r.k as u32;
16686 let mut shell = r.effective_shell(m.experts.len());
16695 shell.push(f32::NEG_INFINITY);
16696 meta[base + 30] = pack.put(&shell);
16697 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16698 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16699 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16700 for (e, ex) in m.experts.iter().enumerate() {
16701 meta[base + 32 + e * 3] =
16702 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16703 meta[base + 33 + e * 3] =
16704 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16705 meta[base + 34 + e * 3] =
16706 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16707 }
16708 }
16709 FfnKind::DenseMoe(_) => return None,
16710 }
16711 }
16712 if !full_seen && self.num_layers == 0 {
16713 return None;
16714 }
16715 let id = {
16716 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16717 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16718 };
16719 let model = crate::gpu::EmbryoGraphModel {
16720 id,
16721 hidden: self.hidden_size,
16722 intermediate: self.intermediate_size,
16723 vocab: self.vocab_size,
16724 layers: self.num_layers,
16725 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16726 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16727 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16728 anchor_q_heads: self.num_heads,
16729 anchor_kv_heads: self.num_kv_heads,
16730 anchor_head_dim: self.head_dim,
16731 rotary_dim: self.rotary_dim,
16732 max_seq: self.kv_cache.max_seq_len,
16733 cluster_count,
16734 cluster_size: self.vocab_size / cluster_count,
16735 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16736 state_stride,
16737 kv_stride,
16738 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16739 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16740 weights: pack.data,
16741 meta,
16742 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16743 clusters: clusters.as_ref().clone(),
16744 final_norm: self.weights.final_norm.clone(),
16745 inv_freq: self.inv_freq.as_ref().clone(),
16746 bounded: bounded.is_some(),
16747 kv_layers: if bounded.is_some() {
16748 bounded_seen
16749 } else {
16750 self.num_layers
16751 },
16752 state_layers: phase_seen + gdn_seen,
16753 anchor_window,
16754 anchor_sink,
16755 phase_layers: phase_seen,
16756 gdn_layers: gdn_seen,
16757 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16758 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16759 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16760 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16761 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16762 };
16763 self.embryo_graph = Some(std::sync::Arc::new(model));
16764 }
16765 self.embryo_graph.clone()
16766 }
16767
16768 fn forward_layers_span(
16769 &mut self,
16770 hidden: &[f32],
16771 position: usize,
16772 task_mask: Option<&TaskMask>,
16773 from: usize,
16774 upto: Option<usize>,
16775 ) -> Vec<f32> {
16776 #[cfg(target_os = "macos")]
16777 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16778 position,
16779 from,
16780 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16781 );
16782 debug_assert!(
16783 from == 0
16784 || (self.dsv4.is_none()
16785 && self.dsv41.is_none()
16786 && self.qwen4_exp.is_none()
16787 && !self.per_token_stack())
16788 );
16789 #[cfg(target_os = "macos")]
16795 if !crate::gpu_metal::wait_replay() {
16796 self.fail_metal_graph("the pending async replay failed before a plain forward");
16797 return vec![0.0; self.hidden_size];
16798 }
16799 if let Some(b) = &mut self.qwen4_exp {
16800 let _ = (task_mask, upto);
16801 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16802 let mut logits = Vec::new();
16803 crate::qwen4_exp::forward_token(
16804 &b.0,
16805 &b.1,
16806 &b.2,
16807 &mut b.3,
16808 token_id,
16809 position,
16810 &self.inv_freq,
16811 self.pool.as_deref(),
16812 &mut logits,
16813 true,
16814 );
16815 self.graph_logits = Some(logits);
16816 return vec![0.0; self.hidden_size];
16817 }
16818 if let Some(b) = &mut self.dsv4 {
16824 let _ = (task_mask, upto);
16825 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16826 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16827 st.pos = position;
16828 let mut logits = Vec::new();
16829 crate::dsv4::forward_token(
16830 g,
16831 layers,
16832 &cfg,
16833 st,
16834 token_id,
16835 &self.inv_freq,
16836 self.pool.as_deref(),
16837 &mut logits,
16838 );
16839 self.graph_logits = Some(logits);
16840 self.dspark_probe(position, token_id);
16841 return vec![0.0; self.hidden_size];
16844 }
16845 if let Some(b) = &mut self.dsv41 {
16847 let _ = (task_mask, upto);
16848 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16849 let mut logits = Vec::new();
16850 crate::dsv41::forward_token(
16851 &b.0,
16852 &b.1,
16853 &b.2,
16854 &mut b.3,
16855 token_id,
16856 position,
16857 self.pool.as_deref(),
16858 &mut logits,
16859 );
16860 self.graph_logits = Some(logits);
16861 return vec![0.0; self.hidden_size];
16862 }
16863 if let Some(b) = &self.g4e {
16869 let _ = (task_mask, upto);
16870 return crate::g4e::g4e_forward(
16871 &b.0,
16872 &b.1,
16873 hidden,
16874 position,
16875 &mut self.kv_cache.layers,
16876 self.num_heads,
16877 self.num_kv_heads,
16878 self.pool.as_deref(),
16879 );
16880 }
16881 if let Some(b) = &self.g3n {
16882 let _ = (task_mask, upto);
16883 return crate::g3n::g3n_forward(
16884 &b.0,
16885 &b.1,
16886 hidden,
16887 position,
16888 &mut self.kv_cache.layers,
16889 self.num_heads,
16890 self.num_kv_heads,
16891 self.head_dim,
16892 self.pool.as_deref(),
16893 );
16894 }
16895 if from == 0
16902 && upto.is_none()
16903 && task_mask.is_none()
16904 && self.anchor_core.is_some()
16905 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16906 {
16907 static ONCE: std::sync::Once = std::sync::Once::new();
16908 ONCE.call_once(|| {
16909 eprintln!(
16910 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16911 unsupported={} eligible={}",
16912 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16913 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16914 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16915 crate::gpu::enabled_here(),
16916 self.graph_refused(),
16917 self.embryo_resident_eligible(),
16918 );
16919 });
16920 }
16921 if from == 0
16922 && upto.is_none()
16923 && task_mask.is_none()
16924 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16928 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16929 && matches!(
16933 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16934 Ok("1") | Ok("parallel")
16935 )
16936 && crate::gpu::enabled_here()
16937 && !self.graph_refused()
16938 && (position == 0 || self.device_sequence_position().is_some())
16944 && self.embryo_resident_eligible()
16945 && let Some(model) = self.ensure_embryo_graph()
16946 {
16947 let mut lg = Vec::new();
16948 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16949 {
16950 self.graph_logits = Some(lg);
16951 return vec![0.0; self.hidden_size];
16952 }
16953 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16954 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16955 }
16956 self.mark_graph_refused();
16962 if position != 0 {
16963 self.kv_cache.clear();
16967 self.clear_history();
16968 crate::gpu::graph_kv_reset(self.graph_kv_id);
16969 panic!(
16970 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16971 );
16972 }
16973 }
16974 let mut h = hidden.to_vec();
16975 self.mimo_moe_prepare();
16978 let _mimo_q8 = self
16979 .mimo_moe
16980 .is_on()
16981 .then(crate::qtensor::enter_full_gpu_q8_scope);
16982 let (nh, _nkv, _hd, hs, _rd, eps) = (
16985 self.num_heads,
16986 self.num_kv_heads,
16987 self.head_dim,
16988 self.hidden_size,
16989 self.rotary_dim,
16990 self.rms_eps,
16991 );
16992 let pool = self.pool.clone();
16993 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
17005 let graph_on = match graph_env.as_deref() {
17006 Some("0") => false,
17007 Some("prefill") => false, Some(_) => true,
17009 None => crate::gpu::wgpu_graph_default(),
17015 };
17016 let graph_trusted =
17017 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17018 let race_eligible =
17019 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17020 let mut tail_start = 0usize;
17021 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17022 let t_graph = std::time::Instant::now();
17023 let mut lg = Vec::new();
17024 let mut gl = 0usize;
17025 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17026 let declined = built.is_none();
17027 let built = match built {
17028 Some(Ok(hh)) => Some(hh),
17029 Some(Err(())) => {
17030 self.clear_sequence_state();
17034 self.graph_failed
17035 .store(true, std::sync::atomic::Ordering::Relaxed);
17036 self.cancel
17037 .store(true, std::sync::atomic::Ordering::Relaxed);
17038 tracing::error!("token graph failed after admission; sequence state cleared");
17039 return vec![0.0; self.hidden_size];
17040 }
17041 None => None,
17042 };
17043 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17048 self.mark_graph_refused();
17049 }
17050 graph_note(built.is_some(), gl, self.num_layers);
17051 if let Some(hh) = built {
17052 let dur = t_graph.elapsed();
17053 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17054 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17055 }
17056 if gl > 0 && gl < self.num_layers {
17057 h = hh;
17063 tail_start = gl;
17064 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17065 if !graph_trusted {
17066 crate::gpu::graph_race_record(true, dur);
17067 }
17068 if !lg.is_empty() {
17069 lg.resize(self.vocab_size, 0.0);
17072 if let Some(c) = self.final_softcap {
17073 softcap_in_place(&mut lg, c, self.pool.as_deref());
17074 }
17075 self.graph_logits = Some(lg);
17076 }
17077 return hh;
17078 }
17079 }
17085 }
17086 let span = from > 0 || upto.is_some();
17110 if span && graph_on && task_mask.is_none() && graph_trusted {
17111 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17112 let mut lg = Vec::new();
17113 let mut gl = 0usize;
17114 let span_res =
17115 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17116 let span_res = match span_res {
17117 Some(Ok(hh)) => Some(hh),
17118 Some(Err(())) => {
17119 self.clear_sequence_state();
17120 self.graph_failed
17121 .store(true, std::sync::atomic::Ordering::Relaxed);
17122 self.cancel
17123 .store(true, std::sync::atomic::Ordering::Relaxed);
17124 tracing::error!(
17125 "span token graph failed after admission; sequence state cleared"
17126 );
17127 return vec![0.0; self.hidden_size];
17128 }
17129 None => None,
17130 };
17131 graph_note(span_res.is_some(), gl, upto_excl - from);
17132 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17133 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17137 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17138 eprintln!(
17139 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17140 upto_excl - from,
17141 span_res.is_some()
17142 );
17143 }
17144 }
17145 if let Some(hh) = span_res {
17146 if gl == upto_excl - from {
17147 if !lg.is_empty() {
17148 lg.resize(self.vocab_size, 0.0);
17149 if let Some(c) = self.final_softcap {
17150 softcap_in_place(&mut lg, c, self.pool.as_deref());
17151 }
17152 self.graph_logits = Some(lg);
17153 }
17154 crate::gpu::set_layer(-1);
17155 return hh;
17156 }
17157 h = hh;
17159 tail_start = from + gl;
17160 }
17161 }
17162 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17167
17168 let host_tail = tail_start > from;
17178 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17179 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17180
17181 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17182 #[cfg(target_os = "macos")]
17183 let mut gpu_skip_until = 0usize;
17184 for li in tail_start.max(from)..self.num_layers {
17185 let _capacity_tail = automatic_gpu_prefix
17186 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17187 .map(|_| crate::gpu::enter_cpu_scope());
17188 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17190 if li > u {
17191 break;
17192 }
17193 }
17194 if let Some(mask) = task_mask {
17195 if !mask.layer_alive(li) {
17196 continue; }
17198 }
17199 if host_tail && task_mask.is_none() {
17204 match self.m3_pooled_layer(li, position, &mut h) {
17205 Ok(true) => {
17206 crate::gpu::set_layer(-1);
17207 continue;
17208 }
17209 Ok(false) => {}
17210 Err(()) => {
17211 self.clear_sequence_state();
17212 self.graph_failed
17213 .store(true, std::sync::atomic::Ordering::Relaxed);
17214 self.cancel
17215 .store(true, std::sync::atomic::Ordering::Relaxed);
17216 tracing::error!(
17217 "pooled tail token graph failed after admission; sequence state cleared"
17218 );
17219 return vec![0.0; self.hidden_size];
17220 }
17221 }
17222 }
17223 #[cfg(target_os = "macos")]
17227 {
17228 if li < gpu_skip_until {
17229 continue;
17230 }
17231 if task_mask.is_none() {
17232 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17233 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17234 return vec![0.0; self.hidden_size];
17238 }
17239 if end > li {
17240 gpu_skip_until = end;
17241 if self.is_loop_end(end - 1) && end < self.num_layers {
17244 h = inference::rms_norm(
17245 &h,
17246 &self.weights.final_norm,
17247 self.rms_eps,
17248 self.norm_style,
17249 );
17250 }
17251 continue;
17252 }
17253 }
17254 }
17255
17256 if task_mask.is_none() {
17257 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17258 crate::gpu::BatchGraphOutcome::Completed => continue,
17259 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17260 crate::gpu::BatchGraphOutcome::Declined => {}
17261 }
17262 }
17263 #[cfg(feature = "gpu")]
17264 self.pull_lagging_host_kv(li, li + 1, position);
17265 let lw = &self.weights.layers[self.phys_layer(li)];
17266 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17267 if tp.parse::<usize>().ok() == Some(position) {
17268 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17269 eprintln!(
17270 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17271 h[0], h[1]
17272 );
17273 }
17274 }
17275 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17278 inference::rms_norm_into(
17279 &h,
17280 &lw.input_norm,
17281 self.rms_eps,
17282 self.norm_style,
17283 &mut self.ws.n1,
17284 );
17285 drop(prof);
17286
17287 let attn_out = match &lw.attn {
17288 AttnKind::Mla(w) => {
17289 let inv_freq_l = self.layer_inv_freq(li);
17290 let rs = self.layer_rope_scale(li);
17291 let eps = self.rms_eps;
17292 let pool = self.pool.clone();
17293 mla_attention(
17294 w,
17295 &self.ws.n1,
17296 &mut self.kv_cache.layers[li],
17297 position,
17298 &inv_freq_l,
17299 rs,
17300 eps,
17301 pool.as_deref(),
17302 )
17303 }
17304 AttnKind::Linear(w) => {
17305 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17306 vmf_phase_forward(
17307 &self.ws.n1,
17308 w,
17309 &cfg,
17310 &mut self.kv_cache.layers[li].linear_state,
17311 self.pool.as_deref(),
17312 )
17313 }
17314 AttnKind::Kda(w) => {
17315 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17316 crate::linear_core::kda_forward(
17317 &self.ws.n1,
17318 w,
17319 &cfg,
17320 &mut self.kv_cache.layers[li].linear_state,
17321 self.pool.as_deref(),
17322 )
17323 }
17324 AttnKind::LinearGdn(w) => {
17325 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17326 gdn_forward(
17327 &self.ws.n1,
17328 w,
17329 &cfg,
17330 &mut self.kv_cache.layers[li].linear_state,
17331 self.pool.as_deref(),
17332 )
17333 }
17334 AttnKind::ShortConv(w) => {
17335 let cfg = self
17336 .short_conv_cfg
17337 .expect("short-conv layer without short_conv_cfg");
17338 short_conv_forward(
17339 &self.ws.n1,
17340 w,
17341 &cfg,
17342 &mut self.kv_cache.layers[li].linear_state,
17343 self.pool.as_deref(),
17344 )
17345 }
17346 AttnKind::Bounded(w) => {
17347 let rope = self
17350 .bounded_rope
17351 .clone()
17352 .expect("bounded layer without an installed rotation table");
17353 let cfg = crate::bounded::BoundedAttnCfg {
17354 num_heads: self.num_heads,
17355 num_kv_heads: self.num_kv_heads,
17356 head_dim: self.head_dim,
17357 hidden_size: hs,
17358 scale: self.attn_scale,
17359 rope: &rope,
17360 pool: pool.as_deref(),
17361 };
17362 crate::bounded::bounded_attention(
17363 &self.ws.n1,
17364 w,
17365 &mut self.kv_cache.layers[li],
17366 &cfg,
17367 )
17368 }
17369 AttnKind::Full {
17370 wq,
17371 wk,
17372 wv,
17373 wo,
17374 q_norm,
17375 k_norm,
17376 output_gate,
17377 softplus_gate,
17378 bias,
17379 } if self.kv_cache.layers[li].o1_sealed() => {
17380 let inv_freq_l = self.layer_inv_freq(li);
17383 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17384 let cfg = QwenAttnCfg {
17385 num_heads: self.layer_num_heads(li),
17386 num_kv_heads: nkv_l,
17387 head_dim: hd_l,
17388 hidden_size: hs,
17389 position,
17390 inv_freq: &inv_freq_l,
17391 rotary_dim: rd_l,
17392 scale: self.attn_scale,
17393 softcap: self.attn_softcap,
17394 window: None,
17395 v_norm: self.attn_v_norm,
17396 qk_norm_after_rope: self.qk_norm_after_rope,
17397 gate_sigmoid: self.proj_gate_sigmoid,
17398 q_norm: q_norm.as_deref(),
17399 k_norm: k_norm.as_deref(),
17400 output_gate: *output_gate,
17401 softplus_gate: softplus_gate
17402 .as_ref()
17403 .map(|(gate, per_head)| (gate, *per_head)),
17404 rope_scale: self.layer_rope_scale(li),
17405 bias: bias
17406 .as_ref()
17407 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17408 rms_eps: eps,
17409 norm_style: self.norm_style,
17410 pool: pool.as_deref(),
17411 v_head_dim: self.layer_v_dim(li),
17412 };
17413 attention::qwen_attention_nystrom(
17414 &self.ws.n1,
17415 wq,
17416 wk,
17417 wv,
17418 wo,
17419 &mut self.kv_cache.layers[li],
17420 &cfg,
17421 )
17422 }
17423 AttnKind::Full {
17424 wq,
17425 wk,
17426 wv,
17427 wo,
17428 q_norm,
17429 k_norm,
17430 output_gate,
17431 softplus_gate,
17432 bias,
17433 } => 'attn: {
17434 let dropin_reason =
17439 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17440 if let Some(reason) = dropin_reason {
17441 self.note_graph_decline("wgpu attn dropin", reason);
17442 }
17443 if graph_on
17444 && dropin_reason.is_none()
17445 && !*output_gate
17446 && softplus_gate.is_none()
17447 && self.attention_heads_per_layer.is_none()
17448 && bias.is_none()
17449 && task_mask.is_none()
17450 {
17451 let inv_freq_l = self.layer_inv_freq(li);
17452 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17453 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17454 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17455 wq.mapped_q1(),
17456 wk.mapped_q1(),
17457 wv.mapped_q1(),
17458 wo.mapped_q1(),
17459 ) {
17460 let gm = gm.clone();
17461 let mut out = vec![0f32; hs];
17462 let cache = &self.kv_cache.layers[li];
17463 if crate::gpu::attn_dropin(
17464 &gm,
17465 self.graph_kv_id,
17466 li,
17467 &self.ws.n1,
17468 qi,
17469 ki,
17470 vi,
17471 oi,
17472 q_norm.as_deref(),
17473 k_norm.as_deref(),
17474 self.qk_norm_after_rope,
17475 &inv_freq_l,
17476 nh,
17477 nkv_l,
17478 hd_l,
17479 rd_l,
17480 hs,
17481 position,
17482 self.kv_cache.max_seq_len,
17483 gemma,
17484 eps as f32,
17485 cache.k_heads(),
17486 cache.v_heads(),
17487 &mut out,
17488 ) {
17489 break 'attn out;
17490 }
17491 }
17492 }
17493 let masked = task_mask
17494 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17495 .unwrap_or(false);
17496 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17497 let plain = self.layer_attn_plain(li);
17500 match (masked, f32_view) {
17501 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17504 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17505 attention::multi_head_attention(
17506 &self.ws.n1,
17507 q,
17508 k,
17509 v,
17510 o,
17511 &mut self.kv_cache.layers[li],
17512 self.num_heads,
17513 self.num_kv_heads,
17514 self.head_dim,
17515 self.hidden_size,
17516 position,
17517 &active_heads,
17518 &self.inv_freq,
17519 )
17520 }
17521 (masked, _) => {
17522 if masked {
17523 tracing::warn!(
17524 "layer {li}: head mask on quantized weights or on a \
17525 window/sink/per-layer-geometry layer not supported \
17526 yet — executing dense"
17527 );
17528 }
17529 let inv_freq_l = self.layer_inv_freq(li);
17530 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17531 let cfg = QwenAttnCfg {
17532 num_heads: self.layer_num_heads(li),
17533 num_kv_heads: nkv_l,
17534 head_dim: hd_l,
17535 hidden_size: hs,
17536 position,
17537 inv_freq: &inv_freq_l,
17538 rotary_dim: rd_l,
17539 scale: self.attn_scale,
17540 softcap: self.attn_softcap,
17541 window: self.layer_window(li),
17542 v_norm: self.attn_v_norm,
17543 qk_norm_after_rope: self.qk_norm_after_rope,
17544 gate_sigmoid: self.proj_gate_sigmoid,
17545 q_norm: q_norm.as_deref(),
17546 k_norm: k_norm.as_deref(),
17547 output_gate: *output_gate,
17548 softplus_gate: softplus_gate
17549 .as_ref()
17550 .map(|(gate, per_head)| (gate, *per_head)),
17551 rope_scale: self.layer_rope_scale(li),
17552 bias: bias
17553 .as_ref()
17554 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17555 rms_eps: eps,
17556 norm_style: self.norm_style,
17557 pool: pool.as_deref(),
17558 v_head_dim: self.layer_v_dim(li),
17559 };
17560 attention::qwen_attention(
17561 &self.ws.n1,
17562 wq,
17563 wk,
17564 wv,
17565 wo,
17566 &mut self.kv_cache.layers[li],
17567 &cfg,
17568 )
17569 }
17570 }
17571 }
17572 };
17573 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17576 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17577 None => attn_out,
17578 };
17579 let lw = &self.weights.layers[self.phys_layer(li)];
17580 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17581 inference::add_rmsnorm_fused_into(
17582 &mut h,
17583 &attn_out,
17584 &lw.post_norm,
17585 self.rms_eps,
17586 self.norm_style,
17587 &mut self.ws.p1,
17588 );
17589 drop(prof);
17590 let mut attn_out = attn_out;
17591 attention::recycle_buf(&mut attn_out);
17592 let post_normed = &self.ws.p1;
17593
17594 let ffn_masked = task_mask
17595 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17596 .unwrap_or(false);
17597 let ffn_out = match (ffn_masked, &lw.ffn) {
17609 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17613 let row = task_mask
17614 .and_then(|tm| tm.ffn_masks.get(li))
17615 .map(|v| v.as_slice());
17616 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17617 }
17618 (true, FfnKind::Dense(d)) => {
17619 let tm = task_mask.unwrap();
17620 let alive = tm.ffn_active_count(li);
17621 let deep = alive * 2 <= self.intermediate_size;
17622 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17623 let active = tm.ffn_active_indices(li);
17624 sparse_ffn_quant(
17625 d,
17626 post_normed,
17627 &active,
17628 self.hidden_size,
17629 self.pool.as_deref(),
17630 )
17631 } else if deep
17632 && let (Some(g), Some(u), Some(dn)) = (
17633 d.gate_proj.as_f32(),
17634 d.up_proj.as_f32(),
17635 d.down_proj.as_f32(),
17636 )
17637 {
17638 let active = tm.ffn_active_indices(li);
17639 inference::sparse_ffn_forward(
17640 post_normed,
17641 g,
17642 u,
17643 dn,
17644 self.hidden_size,
17645 self.intermediate_size,
17646 &active,
17647 self.pool.as_deref(),
17648 )
17649 } else {
17650 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17651 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17652 }
17653 }
17654 (true, FfnKind::Moe(m)) => {
17655 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17659 ffn_forward(
17660 &lw.ffn,
17661 post_normed,
17662 self.pool.as_deref(),
17663 allowed.as_deref(),
17664 )
17665 }
17666 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17667 dm,
17668 post_normed,
17669 &h,
17670 self.rms_eps,
17671 self.norm_style,
17672 self.pool.as_deref(),
17673 ),
17674 (false, _) => match &lw.ffn {
17675 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17676 dm,
17677 post_normed,
17678 &h,
17679 self.rms_eps,
17680 self.norm_style,
17681 self.pool.as_deref(),
17682 ),
17683 FfnKind::Moe(m)
17684 if task_mask.is_none()
17685 && host_tail
17686 && li >= tail_start
17687 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17688 {
17689 moe_ffn_minicpm_tail(
17690 &mut self.minicpm_tail,
17691 li,
17692 self.num_layers,
17693 m,
17694 post_normed,
17695 self.pool.as_deref(),
17696 )
17697 }
17698 FfnKind::Moe(m)
17699 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17700 {
17701 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17702 }
17703 _ => {
17704 let allowed = match (&lw.ffn, task_mask) {
17705 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17706 _ => None,
17707 };
17708 ffn_forward(
17709 &lw.ffn,
17710 post_normed,
17711 self.pool.as_deref(),
17712 allowed.as_deref(),
17713 )
17714 }
17715 },
17716 };
17717 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17718 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17719 None => ffn_out,
17720 };
17721 for (i, &f) in ffn_out.iter().enumerate() {
17722 h[i] += f;
17723 }
17724 let mut ffn_out = ffn_out;
17725 attention::recycle_buf(&mut ffn_out);
17726
17727 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17729 for v in h.iter_mut() {
17730 *v *= sc;
17731 }
17732 }
17733 if self.layer_dump.is_some() {
17735 self.dump_layer_row(position, li, &h);
17736 }
17737
17738 if self.is_loop_end(li) && li + 1 < self.num_layers {
17741 h = inference::rms_norm(
17742 &h,
17743 &self.weights.final_norm,
17744 self.rms_eps,
17745 self.norm_style,
17746 );
17747 }
17748
17749 if self.dyn_phi_layer == Some(li) {
17753 self.update_dyn_phi(&h);
17754 }
17755 }
17756 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17758 crate::gpu::graph_race_record(false, t.elapsed());
17759 }
17760
17761 h
17762 }
17763
17764 fn update_dyn_phi(&mut self, h: &[f32]) {
17767 const A: f32 = 0.2;
17768 if self.dyn_phi_ema.len() != h.len() {
17769 self.dyn_phi_ema = vec![0.0; h.len()];
17770 self.dyn_phi_seen = 0;
17771 }
17772 if self.dyn_phi_seen == 0 {
17773 self.dyn_phi_ema.copy_from_slice(h);
17774 } else {
17775 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17776 *e = (1.0 - A) * *e + A * v;
17777 }
17778 }
17779 self.dyn_phi_seen += 1;
17780 }
17781
17782 pub fn dyn_phi(&self) -> &[f32] {
17784 &self.dyn_phi_ema
17785 }
17786
17787 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17789 self.dyn_phi_layer = layer;
17790 self.dyn_phi_ema.clear();
17791 self.dyn_phi_seen = 0;
17792 }
17793
17794 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17796 let Some(model) = &self.model else {
17797 return Vec::new();
17798 };
17799 model
17800 .header
17801 .skills
17802 .iter()
17803 .enumerate()
17804 .filter_map(|(i, sk)| {
17805 if sk.is_v2() {
17809 return None;
17810 }
17811 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17812 let sel = sk.selection.as_ref()?;
17813 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17814 })
17815 .collect()
17816 }
17817
17818 pub fn active_skill(&self) -> Option<usize> {
17820 self.dyn_active
17821 }
17822
17823 pub fn enable_dynamic_routing(&mut self) -> usize {
17828 use crate::swarm::{DynRouter, RoutableSkill};
17829 let Some(model) = self.model.clone() else {
17830 return 0;
17831 };
17832 if let Some(r) = &model.header.router {
17839 tracing::warn!(
17840 "dynamic routing disabled: this file declares router policy '{}' with \
17841 granularity \"{}\" — the request-level decision applies instead",
17842 r.policy,
17843 r.granularity
17844 );
17845 return 0;
17846 }
17847 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17853 || model.header.skills.iter().any(|s| s.is_v2())
17854 {
17855 tracing::warn!(
17856 "dynamic routing disabled: this file carries format-v2 skill records \
17857 (SKILLS_V2) — they route per request through a router policy only"
17858 );
17859 return 0;
17860 }
17861 if self.dyn_blend_loaded {
17864 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17865 return 0;
17866 }
17867 if let Some(a) = self.dyn_active {
17871 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17872 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17873 return 0;
17874 }
17875 }
17876 let hidden = self.hidden_size;
17877 let mut skills = Vec::new();
17878 for (idx, id, _phi) in self.dynamic_skills() {
17879 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17880 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17881 skills.push(rs);
17882 }
17883 }
17884 }
17885 if skills.is_empty() {
17886 return 0;
17887 }
17888 let phi = skills[0].phi_layer;
17890 if skills.iter().any(|s| s.phi_layer != phi) {
17891 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17892 }
17893 let n = skills.len();
17894 self.set_dyn_phi_layer(Some(phi));
17895 self.dyn_router = Some(DynRouter::new(skills));
17896 n
17897 }
17898
17899 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17901 self.dyn_router
17902 .as_ref()
17903 .map(|r| r.switches.clone())
17904 .unwrap_or_default()
17905 }
17906
17907 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17910 let _mimo_q8 = self
17911 .mimo_moe
17912 .is_on()
17913 .then(crate::qtensor::enter_full_gpu_q8_scope);
17914 let rows = self.weights.lm_head.rows();
17915 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17916 let served = self.mimo_moe.is_on()
17920 && crate::gpu::mimo_q8_short_enabled()
17921 && rows == self.vocab_size
17922 && !self.weights.lm_head.has_prism_contract()
17923 && self
17924 .weights
17925 .lm_head
17926 .graph_weight()
17927 .is_some_and(|(model, idx, kind, _)| {
17928 kind == 7
17929 && crate::gpu::q82_short_rows(
17930 model,
17931 idx,
17932 hidden,
17933 1,
17934 rows,
17935 self.hidden_size,
17936 &mut logits,
17937 )
17938 });
17939 if !served {
17940 self.weights
17941 .lm_head
17942 .matvec(hidden, &mut logits, self.pool.as_deref());
17943 }
17944 logits.resize(self.vocab_size, 0.0);
17945 if let Some(m) = self.logit_multiplier {
17946 for l in logits.iter_mut() {
17947 *l *= m;
17948 }
17949 }
17950 if let Some(c) = self.final_softcap {
17951 softcap_in_place(&mut logits, c, self.pool.as_deref());
17952 }
17953 if let Some(cm) = self.head_clusters.as_ref() {
17954 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17955 }
17956 logits
17957 }
17958
17959 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17962 let h = hidden.len();
17963 let ncl = cm.len() / h.max(1);
17964 if ncl == 0 || logits.len() % ncl != 0 {
17965 return;
17966 }
17967 let cs = logits.len() / ncl;
17968 let mut lc = vec![0.0f32; ncl];
17970 for c in 0..ncl {
17971 let row = &cm[c * h..(c + 1) * h];
17972 let mut s = 0.0f32;
17973 for j in 0..h {
17974 s += row[j] * hidden[j];
17975 }
17976 lc[c] = s;
17977 }
17978 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17979 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17980 for c in 0..ncl {
17981 let blk = &mut logits[c * cs..(c + 1) * cs];
17982 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17983 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17984 let add = lc[c] - lse - bl;
17985 for v in blk.iter_mut() {
17986 *v += add;
17987 }
17988 }
17989 }
17990
17991 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17996 #[cfg(target_os = "macos")]
17997 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17998 self.clear_sequence_state();
17999 crate::gpu::graph_race_begin_generation();
18003 if task_mask.is_none() {
18004 self.o1_begin();
18005 }
18006 let mut hidden = vec![0.0f32; self.hidden_size];
18007 for (pos, &id) in ids.iter().enumerate() {
18008 let emb = self.embed_single(id);
18009 hidden = self.forward_layers(&emb, pos, task_mask);
18010 }
18011 if let Err(err) = self.o1_seal_checked() {
18012 self.o1_fail(err);
18013 }
18014 if let Some(logits) = self.graph_logits.take() {
18017 return logits;
18018 }
18019 inference::rms_norm_into(
18020 &hidden,
18021 &self.weights.final_norm,
18022 self.rms_eps,
18023 self.norm_style,
18024 &mut self.ws.n1,
18025 );
18026 self.lm_head_forward(&self.ws.n1)
18027 }
18028}
18029
18030pub fn create_test_pipeline(
18032 hidden_size: usize,
18033 intermediate_size: usize,
18034 num_heads: usize,
18035 num_kv_heads: usize,
18036 head_dim: usize,
18037 num_layers: usize,
18038 vocab_size: usize,
18039) -> Pipeline {
18040 let synth = |n: usize, salt: usize| -> Vec<f32> {
18043 (0..n)
18044 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18045 .collect()
18046 };
18047 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18048 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18049 };
18050 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18051 .map(|li| LayerWeights {
18052 input_norm: vec![1.0; hidden_size],
18053 post_norm: vec![1.0; hidden_size],
18054 attn_out_norm: None,
18055 ffn_out_norm: None,
18056 layer_scale: None,
18057 ffn: FfnKind::Dense(DenseFfn {
18058 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18059 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18060 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18061 act: Act::Silu,
18062 down_t: None,
18063 segs: Vec::new(),
18064 }),
18065 attn: AttnKind::Full {
18066 bias: None,
18067 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18068 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18069 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18070 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18071 q_norm: None,
18072 k_norm: None,
18073 output_gate: false,
18074 softplus_gate: None,
18075 },
18076 })
18077 .collect();
18078
18079 Pipeline::new(
18080 Tokenizer::byte_level(),
18081 PipelineWeights {
18082 embed_tokens: qt(vocab_size, hidden_size, 100),
18083 layers: layer_weights,
18084 lm_head: qt(vocab_size, hidden_size, 200),
18085 final_norm: vec![1.0; hidden_size],
18086 },
18087 hidden_size,
18088 intermediate_size,
18089 num_heads,
18090 num_kv_heads,
18091 head_dim,
18092 num_layers,
18093 num_layers, false, vocab_size,
18096 1e-6,
18097 10_000.0,
18098 NormStyle::Qwen,
18099 4096,
18100 SamplerConfig {
18101 seed: Some(42),
18102 ..Default::default()
18103 },
18104 )
18105}
18106
18107#[inline]
18112fn mask_bit(row: &[u8], j: usize) -> bool {
18113 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18114}
18115
18116fn mask_gain() -> f32 {
18127 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18128 *G.get_or_init(|| {
18129 std::env::var("CMF_FFN_MASK_GAIN")
18130 .ok()
18131 .and_then(|v| v.parse().ok())
18132 .unwrap_or(1.0)
18133 })
18134}
18135
18136fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18137 let fill = meanfill().and_then(|(i, v)| {
18140 let li = crate::gpu::cur_layer();
18141 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18142 });
18143 for r in 0..rows {
18144 let base = r * inter;
18145 for (bi, &byte) in row.iter().enumerate() {
18146 if byte == 0xFF {
18147 continue;
18148 }
18149 let j0 = bi * 8;
18150 for bit in 0..8 {
18151 let j = j0 + bit;
18152 if j < inter && byte & (1 << bit) == 0 {
18153 g[base + j] = fill.map_or(0.0, |f| f[j]);
18154 }
18155 }
18156 }
18157 }
18158 let gain = mask_gain();
18159 if gain != 1.0 {
18160 for v in g[..rows * inter].iter_mut() {
18161 *v *= gain;
18162 }
18163 }
18164}
18165
18166#[inline]
18168fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18169 row.is_none_or(|r| mask_bit(r, i))
18170}
18171
18172fn all_bits_on(row: &[u8], n: usize) -> bool {
18175 (0..n).all(|i| mask_bit(row, i))
18176}
18177
18178fn tube_topk() -> usize {
18186 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18187 *K.get_or_init(|| {
18188 std::env::var("CMF_TUBE_TOPK")
18189 .ok()
18190 .and_then(|v| v.parse().ok())
18191 .unwrap_or(0)
18192 })
18193}
18194
18195fn tube_score_oracle() -> bool {
18196 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18197 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18198}
18199
18200fn tube_ffn_routed(
18207 d: &DenseFfn,
18208 xs: &[f32],
18209 b: usize,
18210 pool: Option<&Pool>,
18211 mask_row: Option<&[u8]>,
18212 k: usize,
18213) -> Vec<f32> {
18214 let hidden = d.down_proj.rows();
18215 let core = d.gate_proj.rows();
18216 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18217 let mut out = match (b, core_full, mask_row) {
18218 (1, true, _) => dense_ffn(d, xs, pool),
18219 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18220 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18221 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18222 };
18223 let cand: Vec<usize> = (0..d.segs.len())
18224 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18225 .collect();
18226 if cand.is_empty() {
18227 return out;
18228 }
18229 let oracle = tube_score_oracle();
18233 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18234 let mut scores = vec![0f32; b * cand.len()];
18235 for (ci, &i) in cand.iter().enumerate() {
18236 let seg = &d.segs[i];
18237 let w = seg.width;
18238 let mut g = vec![0.0f32; b * w];
18239 if b == 1 {
18240 seg.gate.matvec(xs, &mut g, pool);
18241 } else {
18242 seg.gate.matmat(xs, b, &mut g, pool);
18243 }
18244 for v in g.iter_mut() {
18245 *v = Act::Silu.combine(*v, 1.0);
18246 }
18247 if !oracle {
18248 for t in 0..b {
18249 scores[t * cand.len() + ci] =
18250 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18251 }
18252 }
18253 if oracle || b > 1 {
18254 let mut u = vec![0.0f32; b * w];
18255 if b == 1 {
18256 seg.up.matvec(xs, &mut u, pool);
18257 } else {
18258 seg.up.matmat(xs, b, &mut u, pool);
18259 }
18260 for (a, &v) in g.iter_mut().zip(u.iter()) {
18261 *a *= v;
18262 }
18263 if oracle {
18264 for t in 0..b {
18265 scores[t * cand.len() + ci] =
18266 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18267 }
18268 }
18269 }
18270 acts.push(g);
18271 }
18272 let keep = k.min(cand.len());
18274 let mut scratch: Vec<f32> = Vec::new();
18275 for t in 0..b {
18276 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18277 .map(|ci| (scores[t * cand.len() + ci], ci))
18278 .collect();
18279 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18280 let mut alive = vec![false; cand.len()];
18281 for &(_, ci) in sc.iter().take(keep) {
18282 alive[ci] = true;
18283 }
18284 if b > 1 {
18285 for (ci, a) in acts.iter_mut().enumerate() {
18286 if !alive[ci] {
18287 let w = d.segs[cand[ci]].width;
18288 a[t * w..(t + 1) * w].fill(0.0);
18289 }
18290 }
18291 } else {
18292 for (ci, &i) in cand.iter().enumerate() {
18296 if !alive[ci] {
18297 continue;
18298 }
18299 let seg = &d.segs[i];
18300 let w = seg.width;
18301 let g = &mut acts[ci];
18302 if !tube_score_oracle() {
18303 scratch.clear();
18304 scratch.resize(w, 0.0);
18305 seg.up.matvec(xs, &mut scratch, pool);
18306 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18307 *a *= v;
18308 }
18309 }
18310 let mut acc = vec![0.0f32; hidden];
18311 seg.down.matvec(g, &mut acc, pool);
18312 for (o, a) in out.iter_mut().zip(&acc) {
18313 *o += *a;
18314 }
18315 }
18316 }
18317 }
18318 if b > 1 {
18319 for (ci, &i) in cand.iter().enumerate() {
18320 let seg = &d.segs[i];
18321 let mut acc = vec![0.0f32; b * hidden];
18322 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18323 for (o, a) in out.iter_mut().zip(&acc) {
18324 *o += *a;
18325 }
18326 }
18327 }
18328 out
18329}
18330
18331fn tube_ffn(
18337 d: &DenseFfn,
18338 xs: &[f32],
18339 b: usize,
18340 pool: Option<&Pool>,
18341 mask_row: Option<&[u8]>,
18342) -> Vec<f32> {
18343 if tube_topk() > 0 {
18344 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18345 }
18346 let hidden = d.down_proj.rows();
18347 let core = d.gate_proj.rows();
18348 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18349 let mut out = match (b, core_full, mask_row) {
18350 (1, true, _) => dense_ffn(d, xs, pool),
18351 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18352 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18353 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18354 };
18355 TUBE_SCRATCH.with(|sc| {
18356 let mut sc = sc.borrow_mut();
18357 let [g, u, acc] = &mut *sc;
18358 for seg in &d.segs {
18359 if !tube_bit(mask_row, seg.start) {
18360 continue;
18361 }
18362 let w = seg.width;
18363 g.resize(b * w, 0.0);
18364 if b == 1
18365 && d.act == Act::Silu
18366 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18367 {
18368 } else {
18370 u.resize(b * w, 0.0);
18371 if b == 1 {
18372 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18373 } else {
18374 seg.gate.matmat(xs, b, g, pool);
18375 seg.up.matmat(xs, b, u, pool);
18376 }
18377 for i in 0..b * w {
18378 g[i] = d.act.combine(g[i], u[i]);
18379 }
18380 }
18381 acc.resize(b * hidden, 0.0);
18382 acc.fill(0.0);
18383 if b == 1 {
18384 seg.down.matvec(g, acc, pool);
18385 } else {
18386 seg.down.matmat(g, b, acc, pool);
18387 }
18388 for (o, a) in out.iter_mut().zip(acc.iter()) {
18389 *o += *a;
18390 }
18391 }
18392 out
18393 })
18394}
18395
18396thread_local! {
18397 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18401 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18402}
18403
18404static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18407 std::sync::atomic::AtomicU64::new(0),
18408 std::sync::atomic::AtomicU64::new(0),
18409];
18410
18411fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18415 #[cfg(feature = "gpu")]
18416 {
18417 use std::sync::atomic::Ordering::Relaxed;
18418 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18419 (
18420 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18421 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18422 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18423 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18424 )
18425 }
18426 #[cfg(not(feature = "gpu"))]
18427 (0.0, 0.0, 0.0, 0)
18428}
18429
18430fn wgpu_mirror_counters() -> [u64; 6] {
18433 #[cfg(feature = "gpu")]
18434 {
18435 crate::gpu_wgpu::MIRROR_EVENTS
18436 .each_ref()
18437 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18438 }
18439 #[cfg(not(feature = "gpu"))]
18440 [0; 6]
18441}
18442
18443fn prefill_prof_on() -> bool {
18444 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18445 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18446}
18447
18448pub(crate) fn accumulate_dense_ffn_activation_mass(
18457 d: &DenseFfn,
18458 xs: &[f32],
18459 b: usize,
18460 pool: Option<&Pool>,
18461 mass: &mut [f64],
18462 squared_mass: &mut [f64],
18463) -> Result<(), String> {
18464 let inter = d.gate_proj.rows();
18465 let hidden = d.gate_proj.cols();
18466 if mass.len() != inter || squared_mass.len() != inter {
18467 return Err(format!(
18468 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18469 mass.len(),
18470 squared_mass.len()
18471 ));
18472 }
18473 if d.up_proj.rows() != inter
18474 || d.up_proj.cols() != hidden
18475 || d.down_proj.cols() != inter
18476 || d.down_proj.rows() != hidden
18477 {
18478 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18479 }
18480 if !d.segs.is_empty() {
18481 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18482 }
18483 if xs.len() != b.saturating_mul(hidden) {
18484 return Err(format!(
18485 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18486 xs.len()
18487 ));
18488 }
18489 if b == 0 {
18490 return Ok(());
18491 }
18492 let mut gate = vec![0.0f32; b * inter];
18493 let mut up = vec![0.0f32; b * inter];
18494 d.gate_proj.matmat(xs, b, &mut gate, pool);
18495 d.up_proj.matmat(xs, b, &mut up, pool);
18496 for row in 0..b {
18497 for i in 0..inter {
18498 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18499 let a = a as f64;
18500 mass[i] += a.abs();
18501 squared_mass[i] += a * a;
18502 }
18503 }
18504 Ok(())
18505}
18506
18507fn dense_ffn_batch(
18508 d: &DenseFfn,
18509 xs: &[f32],
18510 b: usize,
18511 pool: Option<&Pool>,
18512 mask_row: Option<&[u8]>,
18513) -> Vec<f32> {
18514 let inter = d.gate_proj.rows();
18515 let hidden = d.down_proj.rows();
18516 let fused_act = d.act.wgpu_graph_act();
18528 if mask_row.is_none()
18529 && fused_act.is_some()
18530 && b >= 32
18531 && crate::gpu::enabled_here()
18532 && !crate::gpu::mm_killed()
18533 && refit_dir().is_none()
18538 && !ffn_probe_active()
18543 && !crate::gptq_capture::capturing()
18547 {
18548 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18549 d.gate_proj.mapped_q4t(),
18550 d.up_proj.mapped_q4t(),
18551 d.down_proj.mapped_q4t(),
18552 ) {
18553 let mut out = vec![0.0f32; b * hidden];
18554 let act = fused_act.expect("checked above");
18555 if crate::gpu::q4_ffn_act(
18556 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18557 ) {
18558 return out;
18559 }
18560 }
18561 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18566 d.gate_proj.mapped_q4tp(),
18567 d.up_proj.mapped_q4tp(),
18568 d.down_proj.mapped_q4tp(),
18569 ) {
18570 let mut out = vec![0.0f32; b * hidden];
18571 let act = fused_act.expect("checked above");
18572 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18573 {
18574 return out;
18575 }
18576 }
18577 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18583 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18584 d.gate_proj.mapped_device_gemm(),
18585 d.up_proj.mapped_device_gemm(),
18586 d.down_proj.mapped_device_gemm(),
18587 ) {
18588 let mut out = vec![0.0f32; b * hidden];
18589 let act = fused_act.expect("checked above");
18590 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18591 return out;
18592 }
18593 }
18594 }
18595 let mut g = vec![0.0f32; b * inter];
18596 d.gate_proj.matmat(xs, b, &mut g, pool);
18597 let mut u = vec![0.0f32; b * inter];
18598 d.up_proj.matmat(xs, b, &mut u, pool);
18599 if gate_topk() > 0 && d.act == Act::Silu {
18600 for t in 0..b {
18601 let row = &mut g[t * inter..(t + 1) * inter];
18602 for v in row.iter_mut() {
18603 *v = Act::Silu.combine(*v, 1.0);
18604 }
18605 keep_top_k(row, gate_topk());
18606 }
18607 for i in 0..b * inter {
18608 g[i] *= u[i];
18609 }
18610 } else {
18611 for i in 0..b * inter {
18612 g[i] = d.act.combine(g[i], u[i]);
18613 }
18614 }
18615 if let Some(row) = mask_row {
18616 zero_masked_cols(&mut g, b, inter, row);
18617 }
18618 if oracle_topk() > 0 {
18619 for t in 0..b {
18620 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18621 }
18622 }
18623 let mut out = vec![0.0f32; b * hidden];
18624 d.down_proj.matmat(&g, b, &mut out, pool);
18625 if refit_dir().is_some() {
18626 let li = crate::gpu::cur_layer();
18627 if li >= 0 {
18628 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18629 }
18630 }
18631 FFN_PROBE.with(|pr| {
18635 if let Some(acc) = pr.borrow_mut().as_mut() {
18636 let li = crate::gpu::cur_layer();
18637 if li < 0 {
18638 return;
18639 }
18640 let Some(row) = acc.get_mut(li as usize) else {
18641 return;
18642 };
18643 let sq = probe_sq();
18644 for t in 0..b {
18645 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18646 *a += if sq {
18647 (v as f64) * (v as f64)
18648 } else {
18649 (v as f64).abs()
18650 };
18651 }
18652 }
18653 }
18654 });
18655 out
18656}
18657
18658fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18663 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18664 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18665 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18666 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18667 if (!on && !dump) || b == 0 {
18668 return;
18669 }
18670 let hidden = xs.len() / b;
18671 if on {
18672 let mut acc = m.act_sq.borrow_mut();
18673 if acc.len() < hidden {
18674 acc.resize(hidden, 0.0);
18675 }
18676 for t in 0..b {
18677 let row = &xs[t * hidden..(t + 1) * hidden];
18678 for (a, &v) in acc.iter_mut().zip(row) {
18679 *a += (v as f64) * (v as f64);
18680 }
18681 }
18682 }
18683 if dump {
18684 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18687 .ok()
18688 .and_then(|v| v.parse().ok())
18689 .unwrap_or(4096);
18690 let mut rows = m.act_rows.borrow_mut();
18691 if rows.len() < cap * hidden {
18692 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18693 rows.extend_from_slice(&xs[..take * hidden]);
18694 }
18695 }
18696}
18697
18698#[derive(Clone, Copy)]
18701struct SendVecs(*mut Vec<f32>);
18702unsafe impl Send for SendVecs {}
18703unsafe impl Sync for SendVecs {}
18704impl SendVecs {
18705 #[inline]
18706 fn at(self, i: usize) -> *mut Vec<f32> {
18707 unsafe { self.0.add(i) }
18708 }
18709}
18710
18711fn moe_ffn_batch(
18712 m: &MoeFfn,
18713 xs: &[f32],
18714 b: usize,
18715 hidden: usize,
18716 pool: Option<&Pool>,
18717 allowed: Option<&[bool]>,
18718) -> Vec<f32> {
18719 accumulate_act(m, xs, b);
18720 let ne = m.experts.len();
18721 let mut logits = vec![0.0f32; b * ne];
18722 match &m.resonance {
18723 Some(r) => {
18724 let hdim = xs.len() / b.max(1);
18725 for bi in 0..b {
18726 r.scores(
18727 &xs[bi * hdim..(bi + 1) * hdim],
18728 &mut logits[bi * ne..(bi + 1) * ne],
18729 );
18730 }
18731 }
18732 None => m.router.matmat(xs, b, &mut logits, pool),
18733 }
18734
18735 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18738 {
18739 let mut st = m.stats.borrow_mut();
18740 if st.len() < ne {
18741 st.resize(ne, 0);
18742 }
18743 for bi in 0..b {
18744 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18745 for &e in &idx {
18746 st[e] += 1;
18747 assign[e].push((bi, p[e] / wsum));
18748 }
18749 }
18750 }
18751
18752 let mut out = vec![0.0f32; b * hidden];
18753 let cols = m.experts[0].gate_proj.cols();
18754 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18755 let sb = list.len();
18756 let mut sub = vec![0.0f32; sb * cols];
18757 for (k, &(bi, _)) in list.iter().enumerate() {
18758 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18759 }
18760 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18761 for (k, &(bi, w)) in list.iter().enumerate() {
18762 for i in 0..hidden {
18763 out[bi * hidden + i] += w * eo[k * hidden + i];
18764 }
18765 }
18766 };
18767 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18773 if pool.is_some() && active.len() >= 8 {
18774 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18775 {
18776 let panel_ptr = SendVecs(panels.as_mut_ptr());
18777 let experts = &m.experts;
18780 let (active_r, assign_r) = (&active, &assign);
18781 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18782 let run = |start: usize, end: usize| {
18783 let _cpu_scope = inherit_cpu();
18784 #[cfg(target_os = "macos")]
18792 let _metal_cpu = crate::gpu::enter_cpu_scope();
18793 for ai in start..end {
18794 let e = active_r[ai];
18795 let list = &assign_r[e];
18796 let sb = list.len();
18797 let mut sub = vec![0.0f32; sb * cols];
18798 for (k, &(bi, _)) in list.iter().enumerate() {
18799 sub[k * cols..(k + 1) * cols]
18800 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18801 }
18802 unsafe {
18804 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18805 }
18806 }
18807 };
18808 match pool {
18809 Some(p) => p.run_rows(active.len(), &run),
18810 None => run(0, active.len()),
18811 }
18812 }
18813 for (ai, &e) in active.iter().enumerate() {
18814 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18815 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18816 for i in 0..hidden {
18817 out[bi * hidden + i] += w * eo[i];
18818 }
18819 }
18820 }
18821 } else {
18822 for &e in &active {
18823 run_expert(&m.experts[e], &assign[e], &mut out);
18824 }
18825 }
18826 if let Some((se, gate)) = &m.shared {
18827 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18828 let mut gl = vec![0.0f32; b];
18829 gate.matmat(xs, b, &mut gl, pool);
18830 (0..b)
18831 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18832 .collect()
18833 } else {
18834 (0..b).map(|bi| (bi, 1.0)).collect()
18835 };
18836 run_expert(se, &all, &mut out);
18837 }
18838 out
18839}
18840
18841fn moe_ffn_rows_exact(
18853 m: &MoeFfn,
18854 xs: &[f32],
18855 b: usize,
18856 hidden: usize,
18857 pool: Option<&Pool>,
18858) -> Vec<f32> {
18859 let mut out = vec![0.0f32; b * hidden];
18860 let per_row = |out: &mut [f32]| {
18861 for r in 0..b {
18862 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18863 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18864 }
18865 };
18866 let covered = !crate::gpu::enabled_here()
18867 && moe_batch_enabled()
18868 && m.shared.is_none()
18869 && m.resonance.is_none()
18870 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18871 && m.experts.iter().all(|d| d.act == Act::Silu);
18872 if !covered {
18873 per_row(&mut out);
18874 return out;
18875 }
18876 let ne = m.experts.len();
18877 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18879 for r in 0..b {
18880 let x = &xs[r * hidden..(r + 1) * hidden];
18881 accumulate_act(m, x, 1);
18882 let mut logits = vec![0.0f32; ne];
18883 m.router.matvec(x, &mut logits, pool);
18884 let (idx, p, wsum) = moe_route(&logits, m, None);
18885 {
18886 let mut st = m.stats.borrow_mut();
18887 if st.len() < ne {
18888 st.resize(ne, 0);
18889 }
18890 for &e in &idx {
18891 st[e] += 1;
18892 }
18893 }
18894 let w: Vec<f32> = idx
18895 .iter()
18896 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18897 .collect();
18898 routes.push((idx, w));
18899 }
18900 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18901 per_row(&mut out);
18902 return out;
18903 }
18904 let mut experts: Vec<usize> = Vec::new();
18906 let mut groups: Vec<Vec<usize>> = Vec::new();
18907 for (r, (idx, _)) in routes.iter().enumerate() {
18908 for &e in idx {
18909 match experts.iter().position(|&x| x == e) {
18910 Some(g) => groups[g].push(r),
18911 None => {
18912 experts.push(e);
18913 groups.push(vec![r]);
18914 }
18915 }
18916 }
18917 }
18918 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18919 let inter = m.experts[experts[0]].gate_proj.rows();
18920 let pairs: Vec<(&QTensor, &QTensor)> = experts
18921 .iter()
18922 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18923 .collect();
18924 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18925 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18926 per_row(&mut out);
18927 return out;
18928 }
18929 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18930 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18931 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18932 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18933 per_row(&mut out);
18934 return out;
18935 }
18936 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18938 let mut p = 0usize;
18939 for (g, &e) in experts.iter().enumerate() {
18940 for &r in &groups[g] {
18941 slot.insert((r, e), p);
18942 p += 1;
18943 }
18944 }
18945 for (r, (idx, w)) in routes.iter().enumerate() {
18946 let terms: Vec<(&[f32], f32)> = idx
18947 .iter()
18948 .zip(w)
18949 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18950 .collect();
18951 let row = &mut out[r * hidden..(r + 1) * hidden];
18952 for (i, dst) in row.iter_mut().enumerate() {
18953 let mut acc = 0f32;
18955 for (d, we) in &terms {
18956 acc += we * d[i];
18957 }
18958 *dst = acc;
18959 }
18960 }
18961 out
18962}
18963
18964pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18968 match pool {
18969 Some(p) if v.len() >= 1 << 15 => {
18970 let base = v.as_mut_ptr() as usize;
18971 p.run_rows(v.len(), &|s, e| {
18972 let sl =
18975 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18976 for x in sl {
18977 *x = c * (*x / c).tanh();
18978 }
18979 });
18980 }
18981 _ => {
18982 for x in v.iter_mut() {
18983 *x = c * (*x / c).tanh();
18984 }
18985 }
18986 }
18987}
18988
18989thread_local! {
18990 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18994 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18995}
18996
18997fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18999 if gate_topk() > 0
19002 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
19003 {
19004 return out;
19005 }
19006 let prism_body = d.gate_proj.has_prism_contract()
19022 || d.up_proj.has_prism_contract()
19023 || d.down_proj.has_prism_contract();
19024 if !prism_body
19025 && crate::gpu::enabled_here()
19026 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19027 {
19028 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19029 crate::gpu::ProbeArm::Gpu
19030 } else {
19031 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19032 };
19033 match arm {
19034 crate::gpu::ProbeArm::Gpu => {
19035 let t0 = std::time::Instant::now();
19036 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19037 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19038 return out;
19039 }
19040 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19044 }
19045 crate::gpu::ProbeArm::CpuTimed => {
19046 let t0 = std::time::Instant::now();
19047 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19048 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19049 return out;
19050 }
19051 crate::gpu::ProbeArm::Cpu => {
19052 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19053 }
19054 }
19055 }
19056 dense_ffn_cpu(d, x, pool)
19057}
19058
19059fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19061 let inter = d.gate_proj.rows();
19062 FFN_SCRATCH.with(|s| {
19063 let mut s = s.borrow_mut();
19064 let [g, u, ..] = &mut *s;
19065 g.resize(inter, 0.0);
19066 if gate_topk() > 0 {
19069 u.resize(inter, 0.0);
19073 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19074 for i in 0..inter {
19075 g[i] = Act::Silu.combine(g[i], 1.0);
19076 }
19077 keep_top_k(g, gate_topk());
19078 for i in 0..inter {
19079 g[i] *= u[i];
19080 }
19081 } else if d.act == Act::Silu && {
19082 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19083 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19084 } {
19085 } else {
19087 u.resize(inter, 0.0);
19088 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19090 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19091 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19092 for i in 0..inter {
19093 g[i] = d.act.combine(g[i], u[i]);
19094 }
19095 }
19096 FFN_PROBE.with(|pr| {
19104 if let Some(acc) = pr.borrow_mut().as_mut() {
19105 let li = crate::gpu::cur_layer();
19106 if li >= 0 {
19107 if let Some(row) = acc.get_mut(li as usize) {
19108 match probe_topk() {
19109 0 if probe_sq() => {
19110 for (a, &v) in row.iter_mut().zip(g.iter()) {
19111 *a += (v as f64) * (v as f64);
19112 }
19113 }
19114 0 if probe_signed() => {
19115 for (a, &v) in row.iter_mut().zip(g.iter()) {
19116 *a += v as f64;
19117 }
19118 }
19119 0 => {
19120 for (a, &v) in row.iter_mut().zip(g.iter()) {
19121 *a += (v as f64).abs();
19122 }
19123 }
19124 k => {
19125 let n = g.len();
19126 let k = k.min(n);
19127 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19128 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19129 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19130 });
19131 let thr = *kth;
19132 for (a, &v) in row.iter_mut().zip(g.iter()) {
19133 if v.abs() >= thr {
19134 *a += 1.0;
19135 }
19136 }
19137 }
19138 }
19139 }
19140 }
19141 }
19142 });
19143 if oracle_topk() > 0 {
19144 keep_top_k(g, oracle_topk());
19145 }
19146 {
19147 let li = crate::gpu::cur_layer();
19148 if li >= 0 {
19149 adump_row(li as usize, g);
19150 }
19151 }
19152 let mut out = attention::take_buf(d.down_proj.rows());
19153 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19154 d.down_proj.matvec(g, &mut out, pool);
19155 out
19156 })
19157}
19158
19159pub struct RefitAcc {
19172 pub support: Vec<u32>,
19173 pub gss: Vec<f32>,
19174 pub ya: Vec<f32>,
19175 pub hidden: usize,
19176 pub tokens: u64,
19177 pub buf_g: Vec<f32>,
19183 pub buf_o: Vec<f32>,
19184 pub buf_t: usize,
19185}
19186
19187type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19191
19192static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19193 std::sync::OnceLock::new();
19194
19195fn ffn_probe_active() -> bool {
19198 FFN_PROBE.with(|p| p.borrow().is_some())
19199}
19200
19201fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19202 REFIT
19203 .get_or_init(|| {
19204 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19205 (
19206 d,
19207 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19208 )
19209 })
19210 })
19211 .as_ref()
19212}
19213
19214fn refit_accumulate(
19216 li: usize,
19217 g: &[f32],
19218 b: usize,
19219 inter: usize,
19220 out: &[f32],
19221 hidden: usize,
19222 pool: Option<&Pool>,
19223) {
19224 let Some((dir, map)) = refit_dir() else {
19225 return;
19226 };
19227 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19228 let (from, to) = *SPAN.get_or_init(|| {
19229 let g = |k: &str, d: usize| {
19230 std::env::var(k)
19231 .ok()
19232 .and_then(|v| v.parse().ok())
19233 .unwrap_or(d)
19234 };
19235 (
19236 g("CMF_FFN_REFIT_FROM", 0),
19237 g("CMF_FFN_REFIT_TO", usize::MAX),
19238 )
19239 });
19240 if li < from || li > to {
19241 return;
19242 }
19243 let mut guard = map.lock().unwrap();
19244 let (map, shared) = &mut *guard;
19245 let acc = match map.entry(li) {
19246 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19247 std::collections::hash_map::Entry::Vacant(e) => {
19248 let path = format!("{dir}/support.{li}.u32");
19249 let Ok(bytes) = std::fs::read(&path) else {
19250 eprintln!("refit: no {path} — layer {li} skipped");
19251 return;
19252 };
19253 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19254 let support: Vec<u32> = bytes[4..4 + n * 4]
19255 .chunks_exact(4)
19256 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19257 .collect();
19258 eprintln!(
19259 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19260 (n * n + hidden * n) as f64 * 4.0 / 1e6
19261 );
19262 e.insert(RefitAcc {
19263 gss: vec![0.0; n * n],
19264 ya: vec![0.0; hidden * n],
19265 buf_g: Vec::new(),
19266 buf_o: Vec::new(),
19267 buf_t: 0,
19268 support,
19269 hidden,
19270 tokens: 0,
19271 })
19272 }
19273 };
19274 let ns = acc.support.len();
19275 let cap = refit_batch();
19277 if acc.buf_g.is_empty() {
19278 acc.buf_g = vec![0.0; ns * cap];
19279 acc.buf_o = vec![0.0; hidden * cap];
19280 }
19281 let take = b.min(cap - acc.buf_t);
19282 for t in 0..take {
19283 let col = acc.buf_t + t;
19284 for (j, &n) in acc.support.iter().enumerate() {
19285 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19286 }
19287 for h in 0..hidden {
19288 acc.buf_o[h * cap + col] = out[t * hidden + h];
19289 }
19290 }
19291 acc.buf_t += take;
19292 acc.tokens += take as u64;
19293 if acc.buf_t < cap {
19294 return;
19295 }
19296 let bt = acc.buf_t;
19297 acc.buf_t = 0;
19298 let RefitAcc {
19308 gss,
19309 ya,
19310 buf_g,
19311 buf_o,
19312 ..
19313 } = acc;
19314 let need = (ns * ns).max(hidden * ns);
19315 if shared.len() < need {
19316 shared.resize(need, 0.0);
19317 }
19318 let scratch = &mut shared[..];
19319 let _ = bt;
19320 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19321 add_into(gss, &scratch[..ns * ns], pool);
19322 if crate::gpu::gemm_nt_f32_transient(
19323 buf_o,
19324 buf_g,
19325 &mut scratch[..hidden * ns],
19326 hidden,
19327 cap,
19328 ns,
19329 ) {
19330 add_into(ya, &scratch[..hidden * ns], pool);
19331 } else {
19332 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19333 }
19334 } else {
19335 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19336 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19337 }
19338 }
19342
19343fn refit_batch() -> usize {
19345 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19346 *B.get_or_init(|| {
19347 std::env::var("CMF_FFN_REFIT_BATCH")
19348 .ok()
19349 .and_then(|v| v.parse().ok())
19350 .unwrap_or(4096)
19351 })
19352}
19353
19354fn accum_outer_t(
19357 c: &mut [f32],
19358 m: usize,
19359 n: usize,
19360 b: usize,
19361 left: &[f32],
19362 right: &[f32],
19363 pool: Option<&Pool>,
19364) {
19365 let ptr = SendMut(c.as_mut_ptr());
19366 let body = |i: usize| {
19367 let ptr = &ptr;
19368 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19369 for t in 0..b {
19370 let a = left[i * b + t];
19371 if a == 0.0 {
19372 continue;
19373 }
19374 for (j, o) in row.iter_mut().enumerate() {
19375 *o += a * right[j * b + t];
19376 }
19377 }
19378 };
19379 match pool {
19380 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19381 for i in s..e {
19382 body(i);
19383 }
19384 }),
19385 _ => {
19386 for i in 0..m {
19387 body(i);
19388 }
19389 }
19390 }
19391}
19392
19393fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19396 let n = dst.len().min(src.len());
19397 match pool {
19398 Some(p) if n >= 1 << 16 => {
19399 let ptr = SendMut(dst.as_mut_ptr());
19400 let f = |s: usize, e: usize| {
19401 let ptr = &ptr;
19402 for blk in s..e {
19403 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19404 for i in a..b {
19405 unsafe { *ptr.0.add(i) += src[i] };
19406 }
19407 }
19408 };
19409 p.run_rows(n.div_ceil(4096), &f);
19410 }
19411 _ => {
19412 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19413 *d += *v;
19414 }
19415 }
19416 }
19417}
19418
19419fn accum_outer(
19424 c: &mut [f32],
19425 m: usize,
19426 n: usize,
19427 b: usize,
19428 left: &[f32],
19429 right: &[f32],
19430 pool: Option<&Pool>,
19431) {
19432 const TILE: usize = 32;
19433 let tiles = m.div_ceil(TILE);
19434 let cp = SendMut(c.as_mut_ptr());
19435 let body = |ti: usize| {
19436 let cp = &cp;
19437 let i0 = ti * TILE;
19438 let i1 = (i0 + TILE).min(m);
19439 for t in 0..b {
19440 let r = &right[t * n..t * n + n];
19441 for i in i0..i1 {
19442 let a = left[i * b + t];
19443 if a == 0.0 {
19444 continue;
19445 }
19446 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19448 for (o, v) in row.iter_mut().zip(r) {
19449 *o += a * *v;
19450 }
19451 }
19452 }
19453 };
19454 match pool {
19455 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19456 for ti in s..e {
19457 body(ti);
19458 }
19459 }),
19460 _ => {
19461 for ti in 0..tiles {
19462 body(ti);
19463 }
19464 }
19465 }
19466}
19467
19468pub fn refit_flush() -> usize {
19470 let Some((dir, map)) = refit_dir() else {
19471 return 0;
19472 };
19473 let guard = map.lock().unwrap();
19474 let mut n = 0;
19475 for (li, acc) in guard.0.iter() {
19476 let w = |name: &str, v: &[f32]| {
19479 let path = format!("{dir}/{name}.{li}.f32");
19480 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19481 match std::fs::write(&path, &bytes) {
19482 Ok(()) => {}
19483 Err(e) => eprintln!(
19484 "refit: FAILED to write {path} ({} MB): {e}",
19485 bytes.len() / 1_000_000
19486 ),
19487 }
19488 };
19489 w("gss", &acc.gss);
19490 w("ya", &acc.ya);
19491 println!(
19492 "refit L{li}: {} support, {} tokens, hidden {}",
19493 acc.support.len(),
19494 acc.tokens,
19495 acc.hidden
19496 );
19497 n += 1;
19498 }
19499 n
19500}
19501
19502fn adump_row(li: usize, g: &[f32]) {
19507 use std::io::Write as _;
19508 static FILES: std::sync::OnceLock<
19509 Option<(
19510 String,
19511 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19512 )>,
19513 > = std::sync::OnceLock::new();
19514 let Some((prefix, map)) = FILES
19515 .get_or_init(|| {
19516 std::env::var("CMF_FFN_ADUMP")
19517 .ok()
19518 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19519 })
19520 .as_ref()
19521 else {
19522 return;
19523 };
19524 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19527 let (from, to) = *SPAN.get_or_init(|| {
19528 let g = |k: &str, d: usize| {
19529 std::env::var(k)
19530 .ok()
19531 .and_then(|v| v.parse().ok())
19532 .unwrap_or(d)
19533 };
19534 (
19535 g("CMF_FFN_ADUMP_FROM", 0),
19536 g("CMF_FFN_ADUMP_TO", usize::MAX),
19537 )
19538 });
19539 if li < from || li > to {
19540 return;
19541 }
19542 let mut map = map.lock().unwrap();
19543 let f = map.entry(li).or_insert_with(|| {
19544 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19545 });
19546 let mut bytes = Vec::with_capacity(g.len() * 2);
19547 for v in g {
19548 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19549 }
19550 let _ = f.write_all(&bytes);
19551}
19552
19553fn oracle_topk() -> usize {
19559 if exact_mtp_calibration_active() {
19560 return 0;
19561 }
19562 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19563 *K.get_or_init(|| {
19564 std::env::var("CMF_FFN_ORACLE_TOPK")
19565 .ok()
19566 .and_then(|v| v.parse().ok())
19567 .unwrap_or(0)
19568 })
19569}
19570
19571fn gate_topk() -> usize {
19577 if exact_mtp_calibration_active() {
19578 return 0;
19579 }
19580 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19581 *K.get_or_init(|| {
19582 std::env::var("CMF_FFN_GATE_TOPK")
19583 .ok()
19584 .and_then(|v| v.parse().ok())
19585 .unwrap_or(0)
19586 })
19587}
19588
19589fn gate_block() -> usize {
19596 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19597 *B.get_or_init(|| {
19598 std::env::var("CMF_FFN_GATE_BLOCK")
19599 .ok()
19600 .and_then(|v| v.parse().ok())
19601 .unwrap_or(1)
19602 })
19603}
19604
19605fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19607 let n = g.len();
19608 let nb = n.div_ceil(block);
19609 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19610 if kb >= nb {
19611 return;
19612 }
19613 let mut score: Vec<f32> = (0..nb)
19614 .map(|b| {
19615 g[b * block..((b + 1) * block).min(n)]
19616 .iter()
19617 .map(|v| v * v)
19618 .sum::<f32>()
19619 })
19620 .collect();
19621 let mut ord = score.clone();
19622 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19623 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19624 });
19625 let thr = *kth;
19626 for b in 0..nb {
19627 if score[b] < thr {
19628 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19629 }
19630 }
19631 score.clear();
19632}
19633
19634fn keep_top_k(g: &mut [f32], k: usize) {
19636 if gate_block() > 1 {
19637 return keep_top_blocks(g, k, gate_block());
19638 }
19639 let n = g.len();
19640 if k == 0 || k >= n {
19641 return;
19642 }
19643 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19644 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19645 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19646 });
19647 let thr = *kth;
19648 for v in g.iter_mut() {
19649 if v.abs() < thr {
19650 *v = 0.0;
19651 }
19652 }
19653}
19654
19655fn probe_sq() -> bool {
19659 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19660 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19661}
19662
19663fn probe_signed() -> bool {
19667 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19668 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19669}
19670
19671fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19679 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19680 M.get_or_init(|| {
19681 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19682 let b = std::fs::read(&p).ok()?;
19683 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19684 let vals: Vec<f32> = b[8..]
19685 .chunks_exact(4)
19686 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19687 .collect();
19688 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19689 Some((inter, vals))
19690 })
19691 .as_ref()
19692}
19693
19694fn probe_topk() -> usize {
19697 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19698 *K.get_or_init(|| {
19699 std::env::var("CMF_FFN_PROBE_TOPK")
19700 .ok()
19701 .and_then(|v| v.parse().ok())
19702 .unwrap_or(0)
19703 })
19704}
19705
19706thread_local! {
19707 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19710 const { std::cell::RefCell::new(None) };
19711}
19712
19713fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19726 if d.gate_proj.has_prism_contract()
19731 || d.up_proj.has_prism_contract()
19732 || d.down_proj.has_prism_contract()
19733 {
19734 return None;
19735 }
19736 let dt = d.down_t.as_ref()?;
19737 let inter = d.gate_proj.rows();
19738 let hidden = dt.cols();
19739 if k == 0 || k >= inter || d.act != Act::Silu {
19740 return None;
19741 }
19742 DYN_SCRATCH.with(|sc| {
19743 let mut sc = sc.borrow_mut();
19744 let DynScratch {
19745 g,
19746 mag,
19747 live,
19748 parts,
19749 } = &mut *sc;
19750 g.resize(inter, 0.0);
19751 d.gate_proj.matvec(x, g, pool);
19752 for v in g.iter_mut() {
19753 *v = inference::silu(*v);
19754 }
19755 mag.clear();
19758 mag.extend(g.iter().map(|v| v.abs()));
19759 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19760 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19761 });
19762 let thr = *kth;
19763 live.clear();
19764 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19765 let mut out = vec![0.0f32; hidden];
19766 match pool {
19767 Some(p) if live.len() >= 64 => {
19768 let nw = p.n_workers() + 1;
19769 parts.clear();
19770 parts.resize(nw * hidden, 0.0);
19771 let ptr = SendMut(parts.as_mut_ptr());
19772 let n = live.len();
19773 let live_ref: &[u32] = live;
19774 let g_ref: &[f32] = g;
19775 p.run(&|w, workers| {
19776 let chunk = n.div_ceil(workers);
19777 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19778 if s >= e {
19779 return;
19780 }
19781 WORKER_SCRATCH.with(|ws| {
19782 let mut ws = ws.borrow_mut();
19783 let [scratch, acc] = &mut *ws;
19784 scratch.resize(hidden.max(x.len()), 0.0);
19785 acc.clear();
19786 acc.resize(hidden, 0.0);
19787 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19788 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19791 d.up_proj.prefetch_row(nx as usize);
19792 dt.prefetch_row(nx as usize);
19793 }
19794 let idx = nrm as usize;
19795 let up = d.up_proj.row_dot(idx, x, scratch);
19796 let a = g_ref[idx] * up;
19797 if a != 0.0 {
19798 dt.add_row_scaled(idx, a, acc, scratch);
19799 }
19800 }
19801 for (j, v) in acc.iter().enumerate() {
19802 unsafe { *ptr.at(w * hidden + j) = *v };
19803 }
19804 });
19805 });
19806 for w in 0..nw {
19807 for (j, o) in out.iter_mut().enumerate() {
19808 *o += parts[w * hidden + j];
19809 }
19810 }
19811 }
19812 _ => {
19813 WORKER_SCRATCH.with(|ws| {
19814 let mut ws = ws.borrow_mut();
19815 let [scratch, _acc] = &mut *ws;
19816 scratch.resize(hidden.max(x.len()), 0.0);
19817 for &nrm in live.iter() {
19818 let idx = nrm as usize;
19819 let up = d.up_proj.row_dot(idx, x, scratch);
19820 let a = g[idx] * up;
19821 if a != 0.0 {
19822 dt.add_row_scaled(idx, a, &mut out, scratch);
19823 }
19824 }
19825 });
19826 }
19827 }
19828 Some(out)
19829 })
19830}
19831
19832struct DynScratch {
19835 g: Vec<f32>,
19836 mag: Vec<f32>,
19837 live: Vec<u32>,
19838 parts: Vec<f32>,
19839}
19840
19841thread_local! {
19842 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19843 std::cell::RefCell::new(DynScratch {
19844 g: Vec::new(),
19845 mag: Vec::new(),
19846 live: Vec::new(),
19847 parts: Vec::new(),
19848 })
19849 };
19850 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19852 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19853}
19854
19855fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19860 let inter = d.gate_proj.rows();
19861 FFN_SCRATCH.with(|s| {
19862 let mut s = s.borrow_mut();
19863 let [g, u, ..] = &mut *s;
19864 g.resize(inter, 0.0);
19865 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19866 } else {
19868 u.resize(inter, 0.0);
19869 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19870 for i in 0..inter {
19871 g[i] = d.act.combine(g[i], u[i]);
19872 }
19873 }
19874 zero_masked_cols(g, 1, inter, mask_row);
19875 let mut out = attention::take_buf(d.down_proj.rows());
19876 d.down_proj.matvec(g, &mut out, pool);
19877 out
19878 })
19879}
19880
19881fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19887 if d.gate_proj.has_prism_contract()
19888 || d.up_proj.has_prism_contract()
19889 || d.down_proj.has_prism_contract()
19890 {
19891 return None;
19892 }
19893 d.act.moe_job_graph_contract()?;
19896 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19899 return None;
19900 }
19901 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19902 let mut model_ref = None;
19903 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19904 let model = model_ref?;
19905 let hidden = jobs[0].down.1;
19906 let mut out = attention::take_buf(hidden);
19907 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19908 Some(out)
19909 } else {
19910 let mut out = out;
19911 attention::recycle_buf(&mut out);
19912 None
19913 }
19914}
19915
19916#[allow(clippy::type_complexity)]
19921#[allow(clippy::type_complexity)]
19922pub(crate) fn moe_parts(
19923 t: &QTensor,
19924) -> Option<(
19925 &std::sync::Arc<cortiq_core::CmfModel>,
19926 usize,
19927 usize,
19928 usize,
19929 &[f32],
19930 &[f32],
19931 bool,
19932 bool,
19933 bool,
19934)> {
19935 match t {
19936 QTensor::Mapped {
19937 model,
19938 idx,
19939 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19940 rows,
19941 cols,
19942 row_scale,
19943 col_field,
19944 ..
19945 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19946 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19947 )),
19948 QTensor::Mapped {
19950 model,
19951 idx,
19952 dtype: cortiq_core::TensorDtype::Q1,
19953 rows,
19954 cols,
19955 ..
19956 } => Some((
19957 model,
19958 *idx,
19959 *rows,
19960 *cols,
19961 &[][..],
19962 &[][..],
19963 true,
19964 false,
19965 false,
19966 )),
19967 QTensor::Mapped {
19969 model,
19970 idx,
19971 dtype: cortiq_core::TensorDtype::Q4Tiled,
19972 rows,
19973 cols,
19974 ..
19975 } => Some((
19976 model,
19977 *idx,
19978 *rows,
19979 *cols,
19980 &[][..],
19981 &[][..],
19982 false,
19983 true,
19984 false,
19985 )),
19986 QTensor::Mapped {
19988 model,
19989 idx,
19990 dtype: cortiq_core::TensorDtype::Q4TiledP,
19991 rows,
19992 cols,
19993 ..
19994 } => Some((
19995 model,
19996 *idx,
19997 *rows,
19998 *cols,
19999 &[][..],
20000 &[][..],
20001 false,
20002 true,
20003 false,
20004 )),
20005 QTensor::Mapped {
20009 model,
20010 idx,
20011 dtype: cortiq_core::TensorDtype::Q2TiledP,
20012 rows,
20013 cols,
20014 ..
20015 } => Some((
20016 model,
20017 *idx,
20018 *rows,
20019 *cols,
20020 &[][..],
20021 &[][..],
20022 false,
20023 true,
20024 true,
20025 )),
20026 _ => None,
20027 }
20028}
20029
20030#[cfg(target_os = "macos")]
20037#[allow(clippy::type_complexity)]
20038fn chunk_moe_parts(
20039 m: &MoeFfn,
20040 hidden: usize,
20041) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20042 if m.shared.is_some()
20043 || m.per_expert_scale.is_some()
20044 || m.resonance.is_some()
20045 || m.route_tau.is_some()
20046 || m.router_input_norm
20047 || m.experts.is_empty()
20048 || m.top_k == 0
20049 || std::env::var("CMF_RMS_TRACE").is_ok()
20050 || std::env::var("CMF_ACT_DUMP").is_ok()
20051 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20052 {
20053 return None;
20054 }
20055 let (rf, rr, rc) = m.router.f32_parts()?;
20056 if rr != m.experts.len() || rc != hidden {
20057 return None;
20058 }
20059 let inter = m.experts[0].gate_proj.rows();
20060 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20063 let experts = m
20064 .experts
20065 .iter()
20066 .map(|e| {
20067 if e.act != Act::Silu
20068 || !e.segs.is_empty()
20069 || e.gate_proj.rows() != inter
20070 || e.up_proj.rows() != inter
20071 || e.down_proj.cols() != inter
20072 || e.gate_proj.cols() != hidden
20073 || e.down_proj.rows() != hidden
20074 {
20075 return None;
20076 }
20077 if q8 {
20078 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20079 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20080 }
20081 Some((
20082 e.gate_proj.mapped_q4tp()?.1,
20083 e.up_proj.mapped_q4tp()?.1,
20084 e.down_proj.mapped_q4tp()?.1,
20085 ))
20086 })
20087 .collect::<Option<Vec<_>>>()?;
20088 Some((rf, experts, q8))
20089}
20090
20091#[cfg(target_os = "macos")]
20104fn metal_flint_moe_graph_parts(
20105 m: &MoeFfn,
20106 hidden: usize,
20107 scratch_key: usize,
20108) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20109 use std::sync::Arc;
20110
20111 const HIDDEN: usize = 2_048;
20112 const ROUTED_INTER: usize = 512;
20113 const SHARED_INTER: usize = 1_024;
20114 const EXPERTS: usize = 256;
20115 const TOP_K: usize = 6;
20116
20117 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20118 return None;
20119 }
20120 let routed0 = m.experts.first()?;
20121 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20122 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20123 if router_rows != EXPERTS || router_cols != HIDDEN {
20124 return None;
20125 }
20126 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20127 return None;
20128 }
20129 let same_q2 = |t: &QTensor| -> Option<usize> {
20130 let (owner, idx) = t.mapped_q2tp()?;
20131 Arc::ptr_eq(&owner, &model).then_some(idx)
20132 };
20133 let same_q4 = |t: &QTensor| -> Option<usize> {
20134 let (owner, idx) = t.mapped_q4tp()?;
20135 Arc::ptr_eq(&owner, &model).then_some(idx)
20136 };
20137 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20138 if e.act != Act::Silu
20139 || !e.segs.is_empty()
20140 || e.gate_proj.rows() != ROUTED_INTER
20141 || e.gate_proj.cols() != HIDDEN
20142 || e.up_proj.rows() != ROUTED_INTER
20143 || e.up_proj.cols() != HIDDEN
20144 || e.down_proj.rows() != HIDDEN
20145 || e.down_proj.cols() != ROUTED_INTER
20146 {
20147 return None;
20148 }
20149 Some((
20150 same_q2(&e.gate_proj)?,
20151 same_q2(&e.up_proj)?,
20152 same_q4(&e.down_proj)?,
20153 ))
20154 };
20155 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20156 if e.act != Act::Silu
20157 || !e.segs.is_empty()
20158 || e.gate_proj.rows() != SHARED_INTER
20159 || e.gate_proj.cols() != HIDDEN
20160 || e.up_proj.rows() != SHARED_INTER
20161 || e.up_proj.cols() != HIDDEN
20162 || e.down_proj.rows() != HIDDEN
20163 || e.down_proj.cols() != SHARED_INTER
20164 {
20165 return None;
20166 }
20167 Some((
20168 same_q4(&e.gate_proj)?,
20169 same_q4(&e.up_proj)?,
20170 same_q4(&e.down_proj)?,
20171 ))
20172 };
20173 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20174 if experts.len() != EXPERTS {
20175 return None;
20176 }
20177 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20178 if shared_gate.is_some() {
20179 return None;
20180 }
20181 Some(crate::gpu::GpuFlintMoe {
20182 router,
20183 bias: m.expert_bias.as_deref(),
20184 experts,
20185 scratch_key,
20186 shared: shared(shared_ffn)?,
20187 top_k: m.top_k,
20188 norm_topk: m.norm_topk_prob,
20189 route_scale: m.routed_scaling,
20190 sigmoid: m.router_sigmoid,
20191 sigmoid_denom_eps: m.sigmoid_denom_eps,
20192 })
20193}
20194
20195#[cfg(target_os = "macos")]
20196fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20197 if m.router_input_norm
20198 || m.route_tau.is_some()
20199 || m.mask.is_some()
20200 || m.per_expert_scale.is_some()
20201 || m.experts.is_empty()
20202 || m.top_k == 0
20203 || m.resonance.is_some()
20204 {
20205 return None;
20206 }
20207 let (sh, sg) = match &m.shared {
20212 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20213 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20214 None => return None,
20215 };
20216 let (rf, rr, rc) = m.router.f32_parts()?;
20217 if rr != m.experts.len() || rc != hidden {
20218 return None;
20219 }
20220 let shared_gated = sg.is_some();
20221 let sf = match sg {
20222 Some(sg) => {
20223 let (sf, sr, sc) = sg.f32_parts()?;
20224 if sr * sc != hidden {
20225 return None;
20226 }
20227 sf
20228 }
20229 None => &rf[..hidden],
20232 };
20233 if let Some(b) = &m.expert_bias {
20234 if b.len() != m.experts.len() {
20235 return None;
20236 }
20237 }
20238 let inter = m.experts[0].gate_proj.rows();
20239 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20242 let q8 = crate::gpu_metal::moe_q8::enabled()
20246 && m.shared.is_none()
20247 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20248 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20249 if e.act != Act::Silu
20250 || e.gate_proj.rows() != inter
20251 || e.gate_proj.cols() != hidden
20252 || e.up_proj.rows() != inter
20253 || e.up_proj.cols() != hidden
20254 || e.down_proj.rows() != hidden
20255 || e.down_proj.cols() != inter
20256 {
20257 return None;
20258 }
20259 if q8 {
20260 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20261 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20262 }
20263 let pick = |t: &QTensor| -> Option<usize> {
20264 if gu_q2 {
20265 t.mapped_q2tp().map(|(_, i)| i)
20266 } else {
20267 t.mapped_q4tp().map(|(_, i)| i)
20268 }
20269 };
20270 Some((
20271 pick(&e.gate_proj)?,
20272 pick(&e.up_proj)?,
20273 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20274 ))
20275 };
20276 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20277 let shared = match sh {
20278 Some(sh) => Some(trio(sh)?),
20279 None => None,
20280 };
20281 Some(crate::gpu::GpuMoe {
20282 router: rf,
20283 sgate: sf,
20284 experts,
20285 shared,
20286 n_exp: m.experts.len(),
20287 top_k: m.top_k,
20288 inter,
20289 norm_topk: m.norm_topk_prob,
20290 route_scale: m.routed_scaling,
20291 gu_q2,
20292 q8,
20293 sigmoid: m.router_sigmoid,
20294 sigmoid_denom_eps: m.sigmoid_denom_eps,
20295 bias: m.expert_bias.as_deref(),
20296 shared_gated,
20297 })
20298}
20299
20300fn forced_ids_at(step: usize) -> Option<u32> {
20306 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20307 IDS.get_or_init(|| {
20308 std::env::var("CMF_FORCE_IDS")
20309 .ok()
20310 .and_then(|p| std::fs::read_to_string(p).ok())
20311 .map(|s| {
20312 s.split_whitespace()
20313 .filter_map(|t| t.parse().ok())
20314 .collect()
20315 })
20316 .unwrap_or_default()
20317 })
20318 .get(step)
20319 .copied()
20320}
20321
20322pub(crate) fn moe_push_job_parts<'a>(
20326 gate: &'a QTensor,
20327 up: &'a QTensor,
20328 down: &'a QTensor,
20329 x: &[f32],
20330 w: f32,
20331 swiglu_limit: f32,
20332 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20333 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20334) -> Option<()> {
20335 use crate::qtensor::prescale;
20336 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20337 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20338 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20339 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20340 return None; }
20342 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20345 return None;
20346 }
20347 if !gq2 && dq2 {
20348 return None;
20349 }
20350 model_ref.get_or_insert_with(|| gm.clone());
20351 let dt = |cf: &[f32]| {
20352 if cf.is_empty() {
20353 cortiq_core::TensorDtype::Q8Row
20354 } else {
20355 cortiq_core::TensorDtype::Q8_2f
20356 }
20357 };
20358 jobs.push(crate::gpu::MoeJob {
20359 gate: (gi, gr, gc, grs),
20360 up: (ui, ur, uc, urs),
20361 down: (di, dr, dc, drs),
20362 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20363 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20364 down_col: dcf,
20365 w,
20366 q1: gq1,
20367 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20368 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20369 gu_q2: gq2,
20370 swiglu_limit,
20371 act: 0,
20372 });
20373 Some(())
20374}
20375
20376fn moe_push_job<'a>(
20378 d: &'a DenseFfn,
20379 x: &[f32],
20380 w: f32,
20381 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20382 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20383) -> Option<()> {
20384 use crate::qtensor::prescale;
20385 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20390 let act = graph_act.code();
20391 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20392 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20393 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20394 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20395 return None; }
20397 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20398 return None;
20399 }
20400 if !gq2 && dq2 {
20401 return None;
20402 }
20403 model_ref.get_or_insert_with(|| gm.clone());
20404 let gdt = if gcf.is_empty() {
20405 cortiq_core::TensorDtype::Q8Row
20406 } else {
20407 cortiq_core::TensorDtype::Q8_2f
20408 };
20409 let udt = if ucf.is_empty() {
20410 cortiq_core::TensorDtype::Q8Row
20411 } else {
20412 cortiq_core::TensorDtype::Q8_2f
20413 };
20414 jobs.push(crate::gpu::MoeJob {
20415 gate: (gi, gr, gc, grs),
20416 up: (ui, ur, uc, urs),
20417 down: (di, dr, dc, drs),
20418 xs_gate: prescale(x, gcf, gdt).into_owned(),
20419 xs_up: prescale(x, ucf, udt).into_owned(),
20420 down_col: dcf,
20421 w,
20422 q1: gq1,
20423 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20424 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20425 gu_q2: gq2,
20426 swiglu_limit,
20427 act,
20428 });
20429 Some(())
20430}
20431
20432fn sparse_ffn_quant(
20439 d: &DenseFfn,
20440 x: &[f32],
20441 active: &[u16],
20442 hidden: usize,
20443 pool: Option<&Pool>,
20444) -> Vec<f32> {
20445 let n = active.len();
20446 let inter = d.gate_proj.rows();
20447 let mut act = vec![0.0f32; n];
20448 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20451 let compute = |ai: usize| -> f32 {
20452 let idx = active[ai] as usize;
20453 if idx >= inter {
20454 return 0.0; }
20456 let mut s = if need_scratch {
20457 vec![0.0f32; hidden]
20458 } else {
20459 Vec::new()
20460 };
20461 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20462 let up = d.up_proj.row_dot(idx, x, &mut s);
20463 d.act.combine(gate, up)
20464 };
20465 match pool {
20466 Some(p) if n >= 256 => {
20467 let ptr = SendMut(act.as_mut_ptr());
20468 p.run(&|widx, nw| {
20469 let chunk = n.div_ceil(nw);
20470 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20471 for ai in s..e {
20472 unsafe { *ptr.at(ai) = compute(ai) };
20473 }
20474 });
20475 }
20476 _ => {
20477 for (ai, a) in act.iter_mut().enumerate() {
20478 *a = compute(ai);
20479 }
20480 }
20481 }
20482 let mut out = vec![0.0f32; hidden];
20484 for (ai, &idx) in active.iter().enumerate() {
20485 let w = act[ai];
20486 if w.abs() >= 1e-12 && (idx as usize) < inter {
20487 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20488 }
20489 }
20490 out
20491}
20492
20493#[doc(hidden)]
20495pub fn sparse_ffn_quant_for_test(
20496 d: &DenseFfn,
20497 x: &[f32],
20498 active: &[u16],
20499 hidden: usize,
20500) -> Vec<f32> {
20501 sparse_ffn_quant(d, x, active, hidden, None)
20502}
20503
20504fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20508 let deq = |t: &QTensor| -> Vec<f32> {
20509 let (rows, cols) = (t.rows(), t.cols());
20510 let mut out = vec![0.0f32; rows * cols];
20511 for r in 0..rows {
20512 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20513 }
20514 out
20515 };
20516 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20517}
20518
20519struct SendMut(*mut f32);
20521unsafe impl Send for SendMut {}
20522unsafe impl Sync for SendMut {}
20523impl SendMut {
20524 #[inline]
20525 #[allow(clippy::mut_from_ref)]
20528 unsafe fn at(&self, i: usize) -> &mut f32 {
20529 unsafe { &mut *self.0.add(i) }
20530 }
20531}
20532
20533pub(crate) fn moe_route(
20545 logits: &[f32],
20546 m: &MoeFfn,
20547 allowed: Option<&[bool]>,
20548) -> (Vec<usize>, Vec<f32>, f32) {
20549 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20550}
20551
20552pub(crate) fn moe_route_with_eps(
20562 logits: &[f32],
20563 m: &MoeFfn,
20564 allowed: Option<&[bool]>,
20565 sigmoid_denom_eps: f32,
20566) -> (Vec<usize>, Vec<f32>, f32) {
20567 let ne = logits.len();
20568 let admit = |e: usize| {
20574 m.mask.as_ref().is_none_or(|mk| mk[e])
20575 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20576 };
20577 if m.resonance.is_some() && m.top_k == 1 {
20590 let mut best: Option<usize> = None;
20591 for e in (0..ne).filter(|&e| admit(e)) {
20592 let l = logits[e];
20593 if l == f32::NEG_INFINITY || l.is_nan() {
20594 continue;
20595 }
20596 if best.is_none_or(|b| l > logits[b]) {
20597 best = Some(e);
20598 }
20599 }
20600 if let Some(b) = best {
20601 let mut p = vec![0.0f32; ne];
20602 p[b] = 1.0;
20603 return (vec![b], p, 1.0 / m.routed_scaling);
20604 }
20605 }
20606 let p: Vec<f32> = if m.router_sigmoid {
20612 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20613 } else {
20614 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20615 if mx == f32::NEG_INFINITY {
20616 vec![1.0 / ne.max(1) as f32; ne]
20617 } else {
20618 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20619 let s: f32 = e.iter().sum();
20620 for v in &mut e {
20621 *v /= s;
20622 }
20623 e
20624 }
20625 };
20626 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20627 match &m.expert_bias {
20629 Some(b) => idx.sort_unstable_by(|&x, &y| {
20630 (p[y] + b[y])
20631 .partial_cmp(&(p[x] + b[x]))
20632 .unwrap()
20633 .then(x.cmp(&y))
20634 }),
20635 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20636 }
20637 idx.truncate(m.top_k);
20638 if let Some(tau) = m.route_tau {
20642 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20643 if total > 0.0 {
20644 let mut acc = 0.0f32;
20645 let mut keep = idx.len();
20646 for (i, &e) in idx.iter().enumerate() {
20647 acc += p[e];
20648 if acc >= tau * total {
20649 keep = i + 1;
20650 break;
20651 }
20652 }
20653 idx.truncate(keep);
20654 }
20655 }
20656 let wsum: f32 = if m.norm_topk_prob {
20657 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20658 (if m.router_sigmoid {
20662 s + sigmoid_denom_eps
20663 } else {
20664 s
20665 }) / m.routed_scaling
20666 } else {
20667 1.0 / m.routed_scaling
20668 };
20669 (idx, p, wsum)
20670}
20671
20672fn moe_trace(idx: &[usize]) {
20674 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20675}
20676
20677pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20680 use std::io::Write;
20681 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20682 std::sync::OnceLock::new();
20683 let Some(f) = F.get_or_init(|| {
20684 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20685 Some(std::sync::Mutex::new(
20686 std::fs::OpenOptions::new()
20687 .create(true)
20688 .append(true)
20689 .open(p)
20690 .ok()?,
20691 ))
20692 }) else {
20693 return;
20694 };
20695 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20696 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20697}
20698
20699pub(crate) fn moe_ffn(
20702 m: &MoeFfn,
20703 x: &[f32],
20704 pool: Option<&Pool>,
20705 allowed: Option<&[bool]>,
20706) -> Vec<f32> {
20707 let r = moe_ffn_route(m, x, pool, allowed);
20708 moe_ffn_experts(m, x, &r, pool)
20709}
20710
20711pub(crate) struct MoeRoute {
20715 pub idx: Vec<usize>,
20716 pub p: Vec<f32>,
20717 pub wsum: f32,
20718 pub logits: Vec<f32>,
20719}
20720
20721pub(crate) fn moe_ffn_route(
20727 m: &MoeFfn,
20728 x: &[f32],
20729 pool: Option<&Pool>,
20730 allowed: Option<&[bool]>,
20731) -> MoeRoute {
20732 accumulate_act(m, x, 1);
20733 let ne = m.experts.len();
20734 let mut logits = vec![0.0f32; ne];
20735 match &m.resonance {
20736 Some(r) => r.scores(x, &mut logits),
20737 None => m.router.matvec(x, &mut logits, pool),
20738 }
20739 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20740 {
20741 let mut st = m.stats.borrow_mut();
20742 if st.len() < ne {
20743 st.resize(ne, 0);
20744 }
20745 for &e in &idx {
20746 st[e] += 1;
20747 }
20748 }
20749 moe_trace(&idx);
20755 MoeRoute {
20756 idx,
20757 p,
20758 wsum,
20759 logits,
20760 }
20761}
20762
20763#[cfg(target_os = "macos")]
20772fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20773 const HIDDEN: usize = 2_048;
20774 const ROUTED_INTER: usize = 512;
20775 const SHARED_INTER: usize = 1_024;
20776 const EXPERTS: usize = 256;
20777 const TOP_K: usize = 6;
20778
20779 let Some(routed) = m.experts.first() else {
20780 return false;
20781 };
20782 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20783 return false;
20784 };
20785 let Some((shared, shared_gate)) = &m.shared else {
20786 return false;
20787 };
20788 if shared_gate.is_some()
20789 || model.header.arch.arch_name != "flint"
20790 || model.header.arch.prism_hadamard.is_some()
20791 || model.header.arch.hidden_size != HIDDEN
20792 || m.experts.len() != EXPERTS
20793 || m.top_k != TOP_K
20794 || !m.router_sigmoid
20795 || !m.norm_topk_prob
20796 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20797 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20798 || m.route_tau.is_some()
20799 || m.mask.is_some()
20800 || m.per_expert_scale.is_some()
20801 || m.router_input_norm
20802 || m.resonance.is_some()
20803 || !m.grown.is_empty()
20804 {
20805 return false;
20806 }
20807
20808 let routed_q2 = routed.act == Act::Silu
20809 && routed.segs.is_empty()
20810 && routed.gate_proj.rows() == ROUTED_INTER
20811 && routed.gate_proj.cols() == HIDDEN
20812 && routed.up_proj.rows() == ROUTED_INTER
20813 && routed.up_proj.cols() == HIDDEN
20814 && routed.down_proj.rows() == HIDDEN
20815 && routed.down_proj.cols() == ROUTED_INTER
20816 && routed.up_proj.mapped_q2tp().is_some()
20817 && routed.down_proj.mapped_q4tp().is_some();
20818 let shared_q4 = shared.act == Act::Silu
20819 && shared.segs.is_empty()
20820 && shared.gate_proj.rows() == SHARED_INTER
20821 && shared.gate_proj.cols() == HIDDEN
20822 && shared.up_proj.rows() == SHARED_INTER
20823 && shared.up_proj.cols() == HIDDEN
20824 && shared.down_proj.rows() == HIDDEN
20825 && shared.down_proj.cols() == SHARED_INTER
20826 && shared.gate_proj.mapped_q4tp().is_some()
20827 && shared.up_proj.mapped_q4tp().is_some()
20828 && shared.down_proj.mapped_q4tp().is_some();
20829 routed_q2 && shared_q4
20830}
20831
20832pub(crate) fn moe_ffn_experts(
20835 m: &MoeFfn,
20836 x: &[f32],
20837 r: &MoeRoute,
20838 pool: Option<&Pool>,
20839) -> Vec<f32> {
20840 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20841 if crate::gpu::enabled_here() {
20846 #[cfg(target_os = "macos")]
20847 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20848 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20854 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20855 });
20856 }
20857 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20858 crate::gpu::ProbeArm::Gpu => {
20859 let t0 = std::time::Instant::now();
20860 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20861 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20862 return out;
20863 }
20864 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20865 }
20866 crate::gpu::ProbeArm::CpuTimed => {
20867 let t0 = std::time::Instant::now();
20868 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20869 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20870 return out;
20871 }
20872 crate::gpu::ProbeArm::Cpu => {
20873 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20874 }
20875 }
20876 }
20877 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20878}
20879
20880fn moe_ffn_minicpm_tail(
20884 slot: &mut crate::minicpm_tail::MiniCpmTail,
20885 li: usize,
20886 total_layers: usize,
20887 m: &MoeFfn,
20888 x: &[f32],
20889 pool: Option<&Pool>,
20890) -> Vec<f32> {
20891 let route = moe_ffn_route(m, x, pool, None);
20892 match slot.forward(li, m, x, &route, pool, total_layers) {
20893 Some(out) => out,
20894 None => moe_ffn_experts(m, x, &route, pool),
20899 }
20900}
20901
20902fn moe_ffn_banked(
20906 slot: &mut crate::mimo_moe::Slot,
20907 li: usize,
20908 m: &MoeFfn,
20909 x: &[f32],
20910 pool: Option<&Pool>,
20911) -> Vec<f32> {
20912 let t0 = std::time::Instant::now();
20913 let r = moe_ffn_route(m, x, pool, None);
20914 slot.note_route(t0.elapsed().as_nanos() as u64);
20915 match slot.forward(li, m, x, &r, pool) {
20916 Some(out) => out,
20917 None => crate::qtensor::float_activations_scope(|| {
20918 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20919 }),
20920 }
20921}
20922
20923fn moe_ffn_banked_rows(
20925 slot: &mut crate::mimo_moe::Slot,
20926 li: usize,
20927 m: &MoeFfn,
20928 xs: &[f32],
20929 b: usize,
20930 hidden: usize,
20931 pool: Option<&Pool>,
20932) -> Vec<f32> {
20933 let t0 = std::time::Instant::now();
20934 let routes: Vec<_> = xs
20935 .chunks_exact(hidden)
20936 .map(|x| moe_ffn_route(m, x, pool, None))
20937 .collect();
20938 slot.note_route(t0.elapsed().as_nanos() as u64);
20939 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20940 return out;
20941 }
20942 let mut out = Vec::with_capacity(b * hidden);
20943 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20944 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20945 crate::qtensor::float_activations_scope(|| {
20947 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20948 })
20949 });
20950 out.extend(row);
20951 }
20952 out
20953}
20954
20955fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20960 use std::sync::atomic::{AtomicBool, Ordering};
20961 if built {
20962 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20963 if total_layers > 0 && layers_run < total_layers {
20964 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20965 } else {
20966 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20967 }
20968 } else {
20969 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20970 }
20971 static SAID: AtomicBool = AtomicBool::new(false);
20972 if !SAID.swap(true, Ordering::Relaxed) {
20973 if built {
20974 tracing::info!("wgpu whole-token graph: ACTIVE");
20975 } else {
20976 tracing::warn!("wgpu whole-token graph refused — per-op path");
20977 }
20978 }
20979}
20980
20981pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20985pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20986pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20990pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20992
20993pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20997pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20998pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20999 std::sync::atomic::AtomicU64::new(0);
21000pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21001pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21002pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
21006 std::sync::atomic::AtomicU64::new(0);
21007pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21008pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21009 std::sync::atomic::AtomicU64::new(0);
21010pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21011 std::sync::atomic::AtomicU64::new(0);
21012
21013fn moe_batch_enabled() -> bool {
21016 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21017 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21018}
21019
21020fn moe_ffn_cpu_batched(
21026 m: &MoeFfn,
21027 x: &[f32],
21028 idx: &[usize],
21029 p: &[f32],
21030 wsum: f32,
21031 pool: Option<&Pool>,
21032) -> Option<Vec<f32>> {
21033 if idx.is_empty() || !moe_batch_enabled() {
21034 return None;
21035 }
21036 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21040 return None;
21041 }
21042 let n = idx.len() + usize::from(m.shared.is_some());
21043 let mut pairs = Vec::with_capacity(n);
21044 let mut downs = Vec::with_capacity(n);
21045 let mut ws = Vec::with_capacity(n);
21046 for &e in idx {
21047 let d = &m.experts[e];
21048 if d.act != Act::Silu {
21049 return None;
21050 }
21051 pairs.push((&d.gate_proj, &d.up_proj));
21052 downs.push(&d.down_proj);
21053 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21054 }
21055 if let Some((se, gate)) = &m.shared {
21058 if se.act != Act::Silu {
21059 return None;
21060 }
21061 let g = gate.as_ref().map_or(1.0, |gate| {
21062 let mut gl = [0.0f32; 1];
21063 gate.matvec(x, &mut gl, pool);
21064 1.0 / (1.0 + (-gl[0]).exp())
21065 });
21066 pairs.push((&se.gate_proj, &se.up_proj));
21067 downs.push(&se.down_proj);
21068 ws.push(g);
21069 }
21070 let inter = pairs[0].0.rows();
21071 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21072 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21073 return None;
21074 }
21075 let mut out = attention::take_buf(x.len());
21076 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21077 attention::recycle_buf(&mut out);
21078 return None;
21079 }
21080 Some(out)
21081}
21082
21083pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21091pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21092pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21093pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21094pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21095
21096pub(crate) fn moe_cold_experts_cpu(
21097 experts: &[(&DenseFfn, f32)],
21098 x: &[f32],
21099 pool: Option<&Pool>,
21100) -> Vec<f32> {
21101 let mut out = attention::take_buf(x.len());
21102 if experts.is_empty() {
21103 return out;
21104 }
21105 let pairs: Vec<_> = experts
21106 .iter()
21107 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21108 .collect();
21109 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21110 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21111 let inter = experts[0].0.gate_proj.rows();
21112 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21113 let silu_only = experts
21121 .iter()
21122 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21123 if silu_only
21124 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21125 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21126 {
21127 return out;
21128 }
21129 out.fill(0.0);
21130 for &(expert, weight) in experts {
21131 let mut one = dense_ffn(expert, x, pool);
21132 for (o, v) in out.iter_mut().zip(&one) {
21133 *o += weight * v;
21134 }
21135 attention::recycle_buf(&mut one);
21136 }
21137 out
21138}
21139
21140pub(crate) fn moe_cold_experts_rows_cpu(
21144 jobs: &[Vec<(&DenseFfn, f32)>],
21145 xs: &[f32],
21146 hidden: usize,
21147 pool: Option<&Pool>,
21148) -> Vec<f32> {
21149 let mut out = vec![0.0; xs.len()];
21150 let mut experts: Vec<&DenseFfn> = Vec::new();
21151 let mut groups: Vec<Vec<usize>> = Vec::new();
21152 let mut terms = vec![Vec::new(); jobs.len()];
21153 for (r, row) in jobs.iter().enumerate() {
21154 for &(e, w) in row {
21155 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21156 Some(g) => g,
21157 None => {
21158 experts.push(e);
21159 groups.push(Vec::new());
21160 groups.len() - 1
21161 }
21162 };
21163 terms[r].push((g, groups[g].len(), w));
21164 groups[g].push(r);
21165 }
21166 }
21167 if experts.is_empty() {
21168 return out;
21169 }
21170 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21171 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21172 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21173 let count: usize = lens.iter().sum();
21174 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21175 let mut ds = vec![vec![0.0; hidden]; count];
21176 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21177 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21178 {
21179 let mut offset = 0;
21180 let offsets: Vec<_> = lens
21181 .iter()
21182 .map(|&n| {
21183 let start = offset;
21184 offset += n;
21185 start
21186 })
21187 .collect();
21188 for (r, terms) in terms.iter().enumerate() {
21189 for &(g, slot, w) in terms {
21190 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21191 .iter_mut()
21192 .zip(&ds[offsets[g] + slot])
21193 {
21194 *o += w * v;
21195 }
21196 }
21197 }
21198 } else {
21199 for (r, jobs) in jobs.iter().enumerate() {
21200 if !jobs.is_empty() {
21201 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21202 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21203 attention::recycle_buf(&mut row);
21204 }
21205 }
21206 }
21207 out
21208}
21209
21210fn moe_ffn_cpu(
21212 m: &MoeFfn,
21213 x: &[f32],
21214 idx: &[usize],
21215 p: &[f32],
21216 wsum: f32,
21217 pool: Option<&Pool>,
21218) -> Vec<f32> {
21219 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21220 return out;
21221 }
21222 let mut out = attention::take_buf(x.len());
21223 for &e in idx {
21224 let mut eo = dense_ffn(&m.experts[e], x, pool);
21225 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21226 for i in 0..out.len() {
21227 out[i] += w * eo[i];
21228 }
21229 attention::recycle_buf(&mut eo);
21230 }
21231 if let Some((se, gate)) = &m.shared {
21232 let mut so = dense_ffn(se, x, pool);
21233 let g = gate.as_ref().map_or(1.0, |gate| {
21234 let mut gl = [0.0f32; 1];
21235 gate.matvec(x, &mut gl, pool);
21236 1.0 / (1.0 + (-gl[0]).exp())
21237 });
21238 for i in 0..out.len() {
21239 out[i] += g * so[i];
21240 }
21241 attention::recycle_buf(&mut so);
21242 }
21243 out
21244}
21245
21246#[allow(clippy::too_many_arguments)]
21254pub(crate) fn mla_attention(
21255 w: &MlaWeights,
21256 normed: &[f32],
21257 cache: &mut crate::kv_cache::LayerKvCache,
21258 position: usize,
21259 inv_freq: &[f32],
21260 rope_scale: f32,
21261 eps: f64,
21262 pool: Option<&Pool>,
21263) -> Vec<f32> {
21264 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21265 let hd = dr + dn;
21266 let mut q = vec![0.0f32; nh * hd];
21267 match (&w.q_a, &w.q_a_norm) {
21268 (Some(qa), Some(qn)) => {
21269 let mut t = vec![0.0f32; qa.rows()];
21270 qa.matvec(normed, &mut t, pool);
21271 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21272 w.q_proj.matvec(&tn, &mut q, pool);
21273 }
21274 _ => w.q_proj.matvec(normed, &mut q, pool),
21275 }
21276 let mut ca = vec![0.0f32; lora + dr];
21277 w.kv_a.matvec(normed, &mut ca, pool);
21278 let (c_lat, k_rope) = ca.split_at_mut(lora);
21279 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21280 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21281 w.kv_b.matvec(&latn, &mut kvb, pool);
21282 if !w.nope {
21283 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21284 }
21285 for h in 0..nh {
21286 if !w.nope {
21287 attention::rope_rotate_scaled(
21288 &mut q[h * hd..h * hd + dr],
21289 position,
21290 inv_freq,
21291 rope_scale,
21292 );
21293 }
21294 }
21295 let mut k = vec![0.0f32; nh * hd];
21296 let mut v = vec![0.0f32; nh * hd];
21297 for h in 0..nh {
21298 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21299 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21300 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21301 }
21302 cache.append(&k, &v, &vec![true; nh]);
21303 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21304 attention::recycle_buf(&mut imp);
21305 let mut ov = vec![0.0f32; nh * dv];
21306 for h in 0..nh {
21307 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21308 }
21309 let mut out = vec![0.0f32; w.o_proj.rows()];
21310 w.o_proj.matvec(&ov, &mut out, pool);
21311 out
21312}
21313
21314fn dense_moe_ffn(
21321 dm: &DenseMoeFfn,
21322 x_normed: &[f32],
21323 h_raw: &[f32],
21324 eps: f64,
21325 norm_style: NormStyle,
21326 pool: Option<&Pool>,
21327) -> Vec<f32> {
21328 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21329 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21330 let m = &dm.moe;
21331 let ne = m.experts.len();
21332 let mut logits = vec![0.0f32; ne];
21333 if m.router_input_norm {
21334 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21335 let inv = 1.0 / (ss + eps as f32).sqrt();
21336 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21337 m.router.matvec(&xr, &mut logits, pool);
21338 } else {
21339 m.router.matvec(h_raw, &mut logits, pool);
21340 }
21341 let (idx, p, wsum) = moe_route(&logits, m, None);
21342 {
21343 let mut st = m.stats.borrow_mut();
21344 if st.len() < ne {
21345 st.resize(ne, 0);
21346 }
21347 for &e in &idx {
21348 st[e] += 1;
21349 }
21350 }
21351 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21352 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21353 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21354 for (di, mi) in d.iter_mut().zip(&mo) {
21355 *di += mi;
21356 }
21357 d
21358}
21359
21360fn moe_gpu_refused(why: &'static str) {
21367 use std::sync::atomic::{AtomicBool, Ordering};
21368 static SAID: AtomicBool = AtomicBool::new(false);
21369 if !SAID.swap(true, Ordering::Relaxed) {
21370 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21371 }
21372}
21373
21374fn moe_ffn_gpu(
21375 m: &MoeFfn,
21376 x: &[f32],
21377 idx: &[usize],
21378 p: &[f32],
21379 wsum: f32,
21380 pool: Option<&Pool>,
21381) -> Option<Vec<f32>> {
21382 use crate::gpu::MoeJob;
21383
21384 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21385 let mut model_ref = None;
21386 for &e in idx {
21387 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21388 moe_gpu_refused("push_job(expert)");
21389 return None;
21390 }
21391 }
21392 if let Some((se, gate)) = &m.shared {
21393 let g = gate.as_ref().map_or(1.0, |gate| {
21394 let mut gl = [0.0f32; 1];
21395 gate.matvec(x, &mut gl, pool);
21396 1.0 / (1.0 + (-gl[0]).exp())
21397 });
21398 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21399 moe_gpu_refused("push_job(shared)");
21400 return None;
21401 }
21402 }
21403 let Some(model) = model_ref else {
21404 moe_gpu_refused("no model_ref");
21405 return None;
21406 };
21407 let hidden = jobs[0].down.1;
21408 let mut out = vec![0.0f32; hidden];
21409 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21410 Some(out)
21411 } else {
21412 moe_gpu_refused("gpu::moe_block");
21413 None
21414 }
21415}
21416
21417fn ffn_forward(
21419 ffn: &FfnKind,
21420 x: &[f32],
21421 pool: Option<&Pool>,
21422 experts_allowed: Option<&[bool]>,
21423) -> Vec<f32> {
21424 match ffn {
21425 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21426 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21427 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21428 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21432 }
21433}
21434
21435fn ffn_forward_pair(
21439 ffn: &FfnKind,
21440 x1: &[f32],
21441 x2: &[f32],
21442 pool: Option<&Pool>,
21443 experts_allowed: Option<&[bool]>,
21444) -> (Vec<f32>, Vec<f32>) {
21445 let d = match ffn {
21446 FfnKind::Dense(d) if !d.segs.is_empty() => {
21449 return (
21450 tube_ffn(d, x1, 1, pool, None),
21451 tube_ffn(d, x2, 1, pool, None),
21452 );
21453 }
21454 FfnKind::Dense(d) => d,
21455 FfnKind::Moe(m) => {
21456 return (
21457 moe_ffn(m, x1, pool, experts_allowed),
21458 moe_ffn(m, x2, pool, experts_allowed),
21459 );
21460 }
21461 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21462 };
21463 let inter = d.gate_proj.rows();
21464 FFN_SCRATCH.with(|s| {
21465 let mut s = s.borrow_mut();
21466 let [g1, g2, u1, u2] = &mut *s;
21467 g1.resize(inter, 0.0);
21468 g2.resize(inter, 0.0);
21469 u1.resize(inter, 0.0);
21470 u2.resize(inter, 0.0);
21471 QTensor::matvec2_many(
21474 [&d.gate_proj, &d.up_proj],
21475 x1,
21476 x2,
21477 [g1.as_mut_slice(), u1.as_mut_slice()],
21478 [g2.as_mut_slice(), u2.as_mut_slice()],
21479 pool,
21480 );
21481 for i in 0..inter {
21482 g1[i] = d.act.combine(g1[i], u1[i]);
21483 g2[i] = d.act.combine(g2[i], u2[i]);
21484 }
21485 let mut o1 = attention::take_buf(d.down_proj.rows());
21486 let mut o2 = attention::take_buf(d.down_proj.rows());
21487 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21488 (o1, o2)
21489 })
21490}
21491
21492#[cfg(test)]
21493mod tests {
21494 use std::ffi::OsString;
21495 use std::sync::Mutex;
21496
21497 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21500
21501 struct ScopedTestEnv {
21502 previous: Vec<(&'static str, Option<OsString>)>,
21503 }
21504
21505 impl ScopedTestEnv {
21506 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21507 let previous = vars
21508 .iter()
21509 .map(|(name, _)| (*name, std::env::var_os(name)))
21510 .collect();
21511 for &(name, value) in vars {
21512 unsafe {
21513 match value {
21514 Some(value) => std::env::set_var(name, value),
21515 None => std::env::remove_var(name),
21516 }
21517 }
21518 }
21519 Self { previous }
21520 }
21521 }
21522
21523 impl Drop for ScopedTestEnv {
21524 fn drop(&mut self) {
21525 for (name, value) in self.previous.drain(..).rev() {
21526 unsafe {
21527 match value {
21528 Some(value) => std::env::set_var(name, value),
21529 None => std::env::remove_var(name),
21530 }
21531 }
21532 }
21533 }
21534 }
21535
21536 #[test]
21537 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21538 use super::pad_mtp_draft_logits;
21539
21540 let mut logits = vec![-3.0, -1.0, -2.0];
21541 pad_mtp_draft_logits(&mut logits, 6);
21542 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21543 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21544 assert_eq!(
21545 logits
21546 .iter()
21547 .enumerate()
21548 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21549 .map(|(i, _)| i),
21550 Some(1),
21551 "an unevaluated row cannot win an all-negative shortlist"
21552 );
21553 }
21554
21555 #[test]
21556 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21557 use super::{Pipeline, SpecMon, SpecTrial};
21558
21559 let mut mon = SpecMon::default();
21560 let start = std::time::Instant::now();
21561 mon.round(1.0, 1);
21562 let first = Pipeline::spec_trial_round(
21563 SpecTrial::Spec {
21564 t0: start,
21565 gen0: 0,
21566 rounds: 0,
21567 },
21568 &mut mon,
21569 1,
21570 );
21571 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21572
21573 mon.round(1.0, 1);
21574 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21575 assert!(
21576 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21577 "two verifier-tip-only rounds must stop the losing MTP trial"
21578 );
21579
21580 let mut viable = SpecMon::default();
21583 viable.round(1.0, 1);
21584 viable.round(1.0, 2);
21585 let continued = Pipeline::spec_trial_round(
21586 SpecTrial::Spec {
21587 t0: start,
21588 gen0: 0,
21589 rounds: 1,
21590 },
21591 &mut viable,
21592 3,
21593 );
21594 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21595 }
21596
21597 #[test]
21598 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21599 let mut p = spark_test_pipeline(None);
21600 let head_rows = 248_070;
21601 p.draft_full_streak = 7;
21602 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21603
21604 p.reset_fresh_mtp_draft_state();
21605 assert_eq!(
21606 p.draft_head_rows(head_rows),
21607 super::Pipeline::draft_vocab_rows(head_rows),
21608 "a fresh request returns to its configured shortlist"
21609 );
21610
21611 p.note_draft_id(u32::MAX);
21614 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21615 }
21616
21617 #[test]
21618 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21619 use super::graph_spec_wanted_by_policy;
21620
21621 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21625 assert!(!graph_spec_wanted_by_policy(
21626 Some("anything-but-1"),
21627 true,
21628 false,
21629 false,
21630 true,
21631 ));
21632 assert!(graph_spec_wanted_by_policy(
21633 Some("1"),
21634 false,
21635 true,
21636 false,
21637 true,
21638 ));
21639 assert!(!graph_spec_wanted_by_policy(
21640 Some("0"),
21641 true,
21642 false,
21643 false,
21644 true,
21645 ));
21646
21647 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21650 assert!(graph_spec_wanted_by_policy(
21651 Some("legacy-force"),
21652 false,
21653 true,
21654 true,
21655 false,
21656 ));
21657 }
21658
21659 #[test]
21660 fn encoder_hidden_states_are_normalized_and_reusable() {
21661 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21662 let ids = [1, 7, 11, 19];
21663
21664 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21665 let second = p
21666 .encode_hidden_states(&ids)
21667 .expect("reusable encoder forward");
21668 assert_eq!(first.len(), ids.len() * 8);
21669 assert!(first.iter().all(|v| v.is_finite()));
21670 assert_eq!(
21671 first, second,
21672 "encoder forward must start from a fresh state"
21673 );
21674
21675 assert!(p.forward_ids(&ids, None).is_ok());
21678 }
21679
21680 #[test]
21685 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21686 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21687 let dense_card = ChunkStackFacts {
21688 plain_dense: true,
21689 discrete: true,
21690 gpu_on: true,
21691 ..Default::default()
21692 };
21693 assert!(dense_card.dense_on_discrete());
21694 assert_eq!(
21696 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21697 DISCRETE_DENSE_PREFILL_CHUNK
21698 );
21699 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21700 for (label, facts) in [
21701 (
21702 "GDN hybrid / MoE / DeepSeek stack",
21703 ChunkStackFacts {
21704 plain_dense: false,
21705 ..dense_card
21706 },
21707 ),
21708 (
21709 "integrated GPU",
21710 ChunkStackFacts {
21711 discrete: false,
21712 ..dense_card
21713 },
21714 ),
21715 (
21716 "CPU only",
21717 ChunkStackFacts {
21718 gpu_on: false,
21719 discrete: false,
21720 ..dense_card
21721 },
21722 ),
21723 (
21724 "capacity split",
21725 ChunkStackFacts {
21726 capacity_split: true,
21727 ..dense_card
21728 },
21729 ),
21730 (
21731 "multi-GPU plan",
21732 ChunkStackFacts {
21733 multi_gpu: true,
21734 ..dense_card
21735 },
21736 ),
21737 (
21738 "O(1) layers",
21739 ChunkStackFacts {
21740 o1: true,
21741 ..dense_card
21742 },
21743 ),
21744 ] {
21745 assert!(!facts.dense_on_discrete(), "{label}");
21746 assert_eq!(
21747 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21748 48,
21749 "{label} keeps the historical x86 chunk"
21750 );
21751 }
21752 for dense in [false, true] {
21754 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21755 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21756 }
21757 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21759 for dense in [false, true] {
21760 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21761 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21762 }
21763 }
21764 }
21765
21766 #[test]
21767 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21768 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21769 let full = |host_rows, device_rows| ReuseLayer {
21770 full: true,
21771 host_rows,
21772 device_rows,
21773 device_state: false,
21774 };
21775 assert_eq!(
21778 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21779 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21780 );
21781 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21783 assert_eq!(
21785 kv_reuse_plan(339, &[full(339, Some(345))]),
21786 ReusePlan::Ready
21787 );
21788 assert_eq!(
21790 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21791 ReusePlan::Pull(vec![(0, 300, 339)])
21792 );
21793 assert_eq!(
21795 kv_reuse_plan(339, &[full(300, Some(320))]),
21796 ReusePlan::Fresh
21797 );
21798 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21799 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21800 let conv = |device_state| ReuseLayer {
21803 full: false,
21804 host_rows: 0,
21805 device_rows: None,
21806 device_state,
21807 };
21808 assert_eq!(
21809 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21810 ReusePlan::Fresh
21811 );
21812 assert_eq!(
21813 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21814 ReusePlan::Ready
21815 );
21816 }
21817
21818 #[test]
21819 fn nll_graph_policy_scopes_only_the_fused_head() {
21820 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21821 ("vulkan graph", true, true, false, true, false),
21823 ("native Metal graph", true, true, true, true, true),
21825 ("masked", false, true, false, false, false),
21827 ("graph disabled", true, false, true, false, false),
21828 ] {
21829 let (graph_quality, graph_head_required) =
21830 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21831 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21832 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21833 }
21834 }
21835
21836 #[test]
21837 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21838 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21839 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21840 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21841 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21842 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21843 }
21844
21845 #[test]
21846 fn cancel_flag_stops_generation() {
21847 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21848 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21851 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21852 assert_eq!(r.finish_reason, "cancelled");
21853 assert!(
21854 r.token_ids.is_empty(),
21855 "no tokens after cancel: {:?}",
21856 r.token_ids
21857 );
21858 assert_eq!(p.kv_cache.seq_len(), 0);
21859 assert!(p.kv_history.is_empty());
21860 assert!(!p.graph_want_logits);
21861 assert!(p.graph_logits.is_none());
21862 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21864 assert_ne!(r2.finish_reason, "cancelled");
21865 }
21866 use super::*;
21867
21868 #[test]
21869 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21870 let arch = |arch_name: &str, hidden_act: &str| {
21874 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21875 "arch_name": arch_name,
21876 "hidden_size": 8,
21877 "intermediate_size": 16,
21878 "num_layers": 1,
21879 "num_attention_heads": 1,
21880 "num_kv_heads": 1,
21881 "head_dim": 8,
21882 "vocab_size": 16,
21883 "layer_types": ["FullAttention"],
21884 "rms_norm_eps": 1e-6,
21885 "max_position_embeddings": 16,
21886 "hidden_act": hidden_act
21887 }))
21888 .expect("minimal architecture")
21889 };
21890 let minimax = arch("minimax_m3_vl", "swigluoai");
21891 assert_eq!(
21892 Act::from_arch_full(&minimax),
21893 Act::SwiGluOai {
21894 alpha: 1.702,
21895 limit: 7.0,
21896 }
21897 );
21898
21899 let unrelated = arch("qwen2", "swigluoai");
21902 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21903
21904 let non_oai = arch("minimax_m3_vl", "gelu");
21908 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21909 }
21910
21911 #[test]
21912 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21913 let act = Act::swiglu_oai(1.702, 7.0);
21914 let got = act.combine(10.0, -10.0);
21915 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21916 assert!((got - expected).abs() < 1e-6);
21917 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21918 assert_eq!(
21919 act.wgpu_graph_act(),
21920 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21921 "the WGPU-only arm accepts only the frozen M3 constants"
21922 );
21923 assert_eq!(
21924 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21925 None,
21926 "nearby OAI constants must not borrow MiniMax's kernel"
21927 );
21928 assert_eq!(
21929 act.moe_job_graph_contract(),
21930 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21931 "the direct MoE path carries the frozen clamp with the marker"
21932 );
21933 assert_eq!(
21934 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21935 None,
21936 "a nearby clamp must not borrow MiniMax's device contract"
21937 );
21938
21939 let gate_only = act.apply(-10.0);
21942 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21943 assert!((gate_only - expected_gate).abs() < 1e-10);
21944 }
21945
21946 #[test]
21947 fn explicit_sliding_schedule_selects_local_rope_profile() {
21948 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21953 p.swa = Some((1024, usize::MAX));
21954 p.sliding_layers = Some(vec![true, true, true, false]);
21955 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21956 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21957 p.rope_scale = 1.277_258_9;
21958 p.rope_scale_local = 1.0;
21959
21960 for li in 0..3 {
21961 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21962 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21963 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21964 }
21965 assert_eq!(p.layer_window(3), None);
21966 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21967 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21968 }
21969
21970 #[test]
21978 fn dynamic_ffn_equals_the_zeroing_arm() {
21979 let (hidden, inter) = (8usize, 32usize);
21980 let synth = |n: usize, salt: usize| -> Vec<f32> {
21981 (0..n)
21982 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21983 .collect()
21984 };
21985 let down = synth(hidden * inter, 3);
21986 let mut down_t = vec![0.0f32; inter * hidden];
21987 for r in 0..hidden {
21988 for c in 0..inter {
21989 down_t[c * hidden + r] = down[r * inter + c];
21990 }
21991 }
21992 let d = DenseFfn {
21993 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21994 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21995 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21996 act: Act::Silu,
21997 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21998 segs: Vec::new(),
21999 };
22000 let x = synth(hidden, 11);
22001 let k = 12usize;
22002 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
22003 let mut g = vec![0.0f32; inter];
22005 d.gate_proj.matvec(&x, &mut g, None);
22006 let mut u = vec![0.0f32; inter];
22007 d.up_proj.matvec(&x, &mut u, None);
22008 for v in g.iter_mut() {
22009 *v = inference::silu(*v);
22010 }
22011 keep_top_k(&mut g, k);
22012 for i in 0..inter {
22013 g[i] *= u[i];
22014 }
22015 let mut want = vec![0.0f32; hidden];
22016 d.down_proj.matvec(&g, &mut want, None);
22017 for (a, b) in want.iter().zip(&got) {
22018 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22019 }
22020 }
22021
22022 #[test]
22028 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22029 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22030 let inter = core + tube;
22031 let synth = |n: usize, salt: usize| -> Vec<f32> {
22032 (0..n)
22033 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22034 .collect()
22035 };
22036 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22037 let d_all = synth(hidden * inter, 3);
22038 let dense = DenseFfn {
22040 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22041 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22042 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22043 act: Act::Silu,
22044 down_t: None,
22045 segs: Vec::new(),
22046 };
22047 let rows =
22048 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22049 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22050 let mut o = Vec::with_capacity(hidden * (b - a));
22051 for r in 0..hidden {
22052 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22053 }
22054 o
22055 };
22056 let tubed = DenseFfn {
22057 down_t: None,
22058 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22059 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22060 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22061 act: Act::Silu,
22062 segs: vec![FfnSeg {
22063 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22064 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22065 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22066 start: core,
22067 width: tube,
22068 }],
22069 };
22070 let x = synth(hidden, 7);
22071 let want = dense_ffn(&dense, &x, None);
22072 let got = tube_ffn(&tubed, &x, 1, None, None);
22073 for (a, b) in want.iter().zip(&got) {
22074 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22075 }
22076 let mut bits = vec![0u8; inter.div_ceil(8)];
22078 for n in 0..core {
22079 bits[n / 8] |= 1 << (n % 8);
22080 }
22081 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22082 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22083 for (a, b) in masked.iter().zip(&closed) {
22084 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22085 }
22086 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22088 for (a, b) in closed.iter().zip(&batch) {
22089 assert_eq!(a, b, "batch arm disagrees with decode arm");
22090 }
22091 }
22092
22093 #[test]
22095 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22096 let (hidden, inter) = (16usize, 40usize);
22097 let synth = |n: usize, salt: usize| -> Vec<f32> {
22098 (0..n)
22099 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22100 .collect()
22101 };
22102 let d = DenseFfn {
22103 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22104 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22105 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22106 act: Act::Silu,
22107 down_t: None,
22108 segs: Vec::new(),
22109 };
22110 let x = synth(hidden, 9);
22111 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22113
22114 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22115
22116 let mut g = vec![0.0f32; inter];
22118 d.gate_proj.matvec(&x, &mut g, None);
22119 let mut u = vec![0.0f32; inter];
22120 d.up_proj.matvec(&x, &mut u, None);
22121 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22122 for i in 0..inter {
22123 g[i] = if act_set.contains(&(i as u16)) {
22124 inference::silu(g[i]) * u[i]
22125 } else {
22126 0.0
22127 };
22128 }
22129 let mut reference = vec![0.0f32; hidden];
22130 d.down_proj.matvec(&g, &mut reference, None);
22131
22132 let max_d = sparse
22133 .iter()
22134 .zip(&reference)
22135 .map(|(a, b)| (a - b).abs())
22136 .fold(0.0f32, f32::max);
22137 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22138 }
22139
22140 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22143 let (h, inter, heads, kv, hd) = (
22144 p.hidden_size,
22145 inter,
22146 p.num_heads,
22147 p.num_kv_heads,
22148 p.head_dim,
22149 );
22150 let synth = |n: usize, salt: usize| -> Vec<f32> {
22151 (0..n)
22152 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22153 .collect()
22154 };
22155 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22156 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22157 };
22158 p.mtp = Some(MtpModule {
22159 enorm: vec![1.0; h],
22160 hnorm: vec![1.0; h],
22161 eh_proj: qt(h, 2 * h, 301),
22162 layer: LayerWeights {
22163 input_norm: vec![1.0; h],
22164 post_norm: vec![1.0; h],
22165 attn_out_norm: None,
22166 ffn_out_norm: None,
22167 layer_scale: None,
22168 ffn: FfnKind::Dense(DenseFfn {
22169 gate_proj: qt(inter, h, 315),
22170 up_proj: qt(inter, h, 316),
22171 down_proj: qt(h, inter, 317),
22172 act: Act::Silu,
22173 down_t: None,
22174 segs: Vec::new(),
22175 }),
22176 attn: AttnKind::Full {
22177 bias: None,
22178 wq: qt(heads * hd, h, 311),
22179 wk: qt(kv * hd, h, 312),
22180 wv: qt(kv * hd, h, 313),
22181 wo: qt(h, heads * hd, 314),
22182 q_norm: None,
22183 k_norm: None,
22184 output_gate: false,
22185 softplus_gate: None,
22186 },
22187 },
22188 final_norm: vec![1.0; h],
22189 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22190 });
22191 }
22192
22193 fn attach_test_mtp(p: &mut Pipeline) {
22195 attach_test_mtp_width(p, p.intermediate_size);
22196 }
22197
22198 #[test]
22199 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22200 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22201 attach_test_mtp_width(&mut p, 11);
22204 let ids = [3, 9, 4, 12, 7, 2];
22205 let trunk = p.probe_ffn_mass(&ids);
22206 assert_eq!(trunk[0].len(), p.intermediate_size);
22207
22208 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22209 assert_eq!(got.len(), 1);
22210 assert_eq!(got[0].mass.len(), 11);
22211 assert_eq!(got[0].squared_mass.len(), 11);
22212 assert_eq!(got[0].observed_positions, ids.len() - 1);
22213 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22214 assert!(got[0]
22215 .mass
22216 .iter()
22217 .zip(&got[0].squared_mass)
22218 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22219 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22220 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22221
22222 let h = p.hidden_size;
22226 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22227 panic!("synthetic MTP must be dense")
22228 };
22229 dense.segs.push(FfnSeg {
22230 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22231 up: QTensor::from_f32(vec![0.0; h], 1, h),
22232 down: QTensor::from_f32(vec![0.0; h], h, 1),
22233 start: dense.gate_proj.rows(),
22234 width: 1,
22235 });
22236 let err = p.probe_mtp_ffn(&ids);
22237 assert!(err
22238 .unwrap_err()
22239 .contains("tube-segmented draft FFNs are not supported"));
22240 assert_eq!(p.kv_cache.seq_len(), 0);
22241 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22242 }
22243
22244 #[test]
22245 fn speculative_equals_vanilla_greedy() {
22246 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22247 let _env = ScopedTestEnv::set(&[
22250 ("CMF_GPU", Some("0")),
22251 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22252 ("CMF_MTP_PAIR", Some("1")),
22253 ]);
22254 let run = |spec: bool| {
22255 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22256 p.sampler_config.temperature = 0.0;
22257 attach_test_mtp(&mut p);
22258 p.speculative = spec;
22259 let r = p.generate("abcdef", 12, None, None).unwrap();
22260 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22261 };
22262 let (vanilla, d0, _) = run(false);
22263 let (spec, d1, a1) = run(true);
22264 assert_eq!(d0, 0, "vanilla path must not draft");
22265 assert!(d1 > 0, "speculative path must draft");
22266 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22267 assert_eq!(
22268 vanilla, spec,
22269 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22270 );
22271 }
22272
22273 #[test]
22274 fn speculative_accepts_constant_oracle() {
22275 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22276 let _env = ScopedTestEnv::set(&[
22277 ("CMF_GPU", Some("0")),
22278 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22279 ("CMF_MTP_PAIR", Some("1")),
22280 ]);
22281 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22282 p.sampler_config.temperature = 0.0;
22283 p.sampler_config.repetition_penalty = 1.0;
22284 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22287 attach_test_mtp(&mut p);
22288 p.speculative = true;
22289 let r = p.generate("abcd", 10, None, None).unwrap();
22290 assert!(r.mtp_drafted > 0);
22291 assert_eq!(
22292 r.mtp_accepted, r.mtp_drafted,
22293 "constant logits → every draft accepted"
22294 );
22295 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22298 }
22299
22300 #[test]
22301 fn host_fused_mtp_pair_is_opt_in() {
22302 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22303 let _env = ScopedTestEnv::set(&[
22304 ("CMF_GPU", Some("0")),
22305 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22306 ("CMF_MTP_PAIR", None),
22307 ]);
22308 let run = |spec: bool| {
22309 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22310 p.sampler_config.temperature = 0.0;
22311 attach_test_mtp(&mut p);
22312 p.speculative = spec;
22313 let r = p.generate("abcdef", 12, None, None).unwrap();
22314 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22315 };
22316 let (plain, d0, _) = run(false);
22317 let (guarded, d1, a1) = run(true);
22318 assert_eq!(d0, 0);
22319 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22320 assert_eq!(a1, 0);
22321 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22322 }
22323
22324 #[test]
22325 fn empty_prompt_is_an_error_not_a_panic() {
22326 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22327 let r = p.generate("", 4, None, None);
22328 assert!(r.is_err(), "empty prompt must be a clean error");
22329 }
22330
22331 #[test]
22332 fn every_token_enters_kv_exactly_once() {
22333 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22334 p.sampler_config.temperature = 0.0;
22336 let r = p.generate("abc", 2, None, None).unwrap();
22337 assert_eq!(r.prompt_tokens, 3);
22338 assert_eq!(
22342 p.kv_cache.seq_len(),
22343 3 + r.tokens_generated - 1,
22344 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22345 );
22346 }
22347
22348 #[test]
22349 fn generation_is_reproducible_with_seed() {
22350 let run = || {
22351 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22352 p.generate("hello", 8, None, None).unwrap().token_ids
22353 };
22354 assert_eq!(run(), run());
22355 }
22356
22357 #[test]
22358 fn resetting_sampler_restarts_the_seeded_stream() {
22359 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22360 let config = SamplerConfig {
22361 seed: Some(1234),
22362 ..SamplerConfig::default()
22363 };
22364 p.set_sampler_config(config.clone());
22365 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22366 p.set_sampler_config(config);
22367 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22368 assert_eq!(first, second);
22369 }
22370
22371 #[test]
22372 fn eviction_bounds_the_cache() {
22373 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22374 p.kv_cache.max_seq_len = 6;
22375 p.sampler_config.temperature = 0.0;
22376 let _ = p.generate("abcd", 12, None, None).unwrap();
22377 assert!(
22378 p.kv_cache.seq_len() <= 6 + 1,
22379 "cache must stay bounded by max_seq_len (got {})",
22380 p.kv_cache.seq_len()
22381 );
22382 }
22383
22384 #[test]
22385 fn confidence_matches_tokens_and_is_a_probability() {
22386 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22387 p.sampler_config.temperature = 0.0;
22388 p.sampler_config.repetition_penalty = 1.0;
22389 let r = p.generate("abcd", 10, None, None).unwrap();
22390 assert_eq!(
22391 r.token_confidence.len(),
22392 r.token_ids.len(),
22393 "one confidence per emitted token"
22394 );
22395 for &c in &r.token_confidence {
22396 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22397 }
22398 let logits = [1.0f32, 3.0, 0.5, 3.0];
22400 let p0 = top1_prob_t(&logits, 1, 1.0);
22401 let p1 = top1_prob_t(&logits, 3, 1.0);
22402 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22403 assert!(p0 > 0.0 && p0 < 1.0);
22404 let sharp = top1_prob_t(&logits, 1, 1.0);
22406 let soft = top1_prob_t(&logits, 1, 2.0);
22407 assert!(soft < sharp, "higher temperature lowers peak confidence");
22408 }
22409
22410 #[test]
22411 fn trace_is_opt_in_and_parallels_the_output() {
22412 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22414 p.sampler_config.temperature = 0.0;
22415 p.sampler_config.repetition_penalty = 1.0;
22416 let r = p.generate("abcd", 10, None, None).unwrap();
22417 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22418
22419 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22421 p.sampler_config.temperature = 0.0;
22422 p.sampler_config.repetition_penalty = 1.0;
22423 p.set_trace(true);
22424 let r = p.generate("abcd", 10, None, None).unwrap();
22425 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22426 for (i, tr) in r.traces.iter().enumerate() {
22427 assert_eq!(tr.t, i, "trace index is sequential");
22428 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22429 assert_eq!(
22430 tr.confidence, r.token_confidence[i],
22431 "trace confidence matches the confidence channel"
22432 );
22433 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22435 }
22436 }
22437
22438 #[test]
22439 fn explain_prefill_logits_match_greedy_first_token() {
22440 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22444 p.sampler_config.temperature = 0.0;
22445 p.sampler_config.repetition_penalty = 1.0;
22446 let ids = p.tokenizer.encode("abcd");
22447 let logits = p.prefill_next_logits(&ids, None);
22448 let argmax = logits
22449 .iter()
22450 .enumerate()
22451 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22452 .unwrap()
22453 .0 as u32;
22454 let r = p.generate("abcd", 1, None, None).unwrap();
22455 assert_eq!(
22456 argmax, r.token_ids[0],
22457 "explain preview must match greedy emit"
22458 );
22459 }
22460
22461 #[test]
22462 fn laguna_shared_expert_is_unconditionally_added() {
22463 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22464 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22465 let zero_dense = || DenseFfn {
22466 gate_proj: matrix(vec![0.0; 4]),
22467 up_proj: matrix(vec![0.0; 4]),
22468 down_proj: matrix(vec![0.0; 4]),
22469 act: Act::Silu,
22470 down_t: None,
22471 segs: Vec::new(),
22472 };
22473 let shared = DenseFfn {
22474 gate_proj: identity(),
22475 up_proj: identity(),
22476 down_proj: identity(),
22477 act: Act::Silu,
22478 down_t: None,
22479 segs: Vec::new(),
22480 };
22481 let x = [1.0, 2.0];
22482 let expected = dense_ffn(&shared, &x, None);
22483 let moe = MoeFfn {
22484 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22485 experts: vec![zero_dense()],
22486 top_k: 1,
22487 norm_topk_prob: true,
22488 router_sigmoid: true,
22489 sigmoid_denom_eps: 1e-6,
22490 expert_bias: None,
22491 routed_scaling: 1.0,
22492 route_tau: None,
22493 shared: Some((shared, None)),
22494 stats: std::cell::RefCell::new(Vec::new()),
22495 act_sq: std::cell::RefCell::new(Vec::new()),
22496 act_rows: std::cell::RefCell::new(Vec::new()),
22497 mask: None,
22498 per_expert_scale: None,
22499 router_input_norm: false,
22500 resonance: None,
22501 grown: Vec::new(),
22502 };
22503 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22504 for (actual, expected) in actual.iter().zip(expected) {
22505 assert!((actual - expected).abs() < 1e-6);
22506 }
22507 }
22508
22509 fn mimo_test_pipeline() -> Pipeline {
22518 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22519 let kvh = [1usize, 2, 2, 1];
22520 let synth = |n: usize, salt: usize| -> Vec<f32> {
22521 (0..n)
22522 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22523 .collect()
22524 };
22525 let qt = |rows: usize, cols: usize, salt: usize| {
22526 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22527 };
22528 let dense = |inter: usize, salt: usize| DenseFfn {
22529 gate_proj: qt(inter, hs, salt),
22530 up_proj: qt(inter, hs, salt + 1),
22531 down_proj: qt(hs, inter, salt + 2),
22532 act: Act::Silu,
22533 down_t: None,
22534 segs: Vec::new(),
22535 };
22536 let layers: Vec<LayerWeights> = (0..4)
22537 .map(|li| LayerWeights {
22538 input_norm: vec![1.0; hs],
22539 post_norm: vec![1.0; hs],
22540 attn_out_norm: None,
22541 ffn_out_norm: None,
22542 layer_scale: None,
22543 ffn: if li == 0 {
22544 FfnKind::Dense(dense(inter, 50))
22545 } else {
22546 FfnKind::Moe(MoeFfn {
22547 router: qt(4, hs, 60 + li),
22548 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22549 top_k: 2,
22550 norm_topk_prob: true,
22551 router_sigmoid: true,
22552 sigmoid_denom_eps: 1e-6,
22553 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22554 routed_scaling: 1.0,
22555 route_tau: None,
22556 shared: None,
22557 stats: std::cell::RefCell::new(Vec::new()),
22558 act_sq: std::cell::RefCell::new(Vec::new()),
22559 act_rows: std::cell::RefCell::new(Vec::new()),
22560 mask: None,
22561 per_expert_scale: None,
22562 router_input_norm: false,
22563 resonance: None,
22564 grown: Vec::new(),
22565 })
22566 },
22567 attn: AttnKind::Full {
22568 wq: qt(nh * hd, hs, li * 10 + 1),
22569 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22570 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22571 wo: qt(hs, nh * vd, li * 10 + 4),
22572 q_norm: None,
22573 k_norm: None,
22574 output_gate: false,
22575 softplus_gate: None,
22576 bias: None,
22577 },
22578 })
22579 .collect();
22580 let mut p = Pipeline::new(
22581 Tokenizer::byte_level(),
22582 PipelineWeights {
22583 embed_tokens: qt(vocab, hs, 100),
22584 layers,
22585 lm_head: qt(vocab, hs, 200),
22586 final_norm: vec![1.0; hs],
22587 },
22588 hs,
22589 inter,
22590 nh,
22591 1, hd,
22593 4,
22594 4,
22595 false,
22596 vocab,
22597 1e-6,
22598 1e7,
22599 NormStyle::Qwen,
22600 4096,
22601 SamplerConfig {
22602 seed: Some(7),
22603 ..Default::default()
22604 },
22605 );
22606 p.layer_dump = None;
22608 p.set_rotary(4, 1e7);
22609 p.sliding_layers = Some(vec![false, true, true, false]);
22610 p.swa = Some((3, usize::MAX));
22611 p.rotary_dim_local = Some(4);
22612 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22613 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22614 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22615 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22616 p
22617 }
22618
22619 #[test]
22620 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22621 let mut p = mimo_test_pipeline();
22622 p.speculative = false;
22623 p.ignore_eos = true;
22624 p.sampler_config.temperature = 0.0;
22625 p.sampler_config.repetition_penalty = 1.0;
22626 let a = vec![3, 5, 7, 9, 11, 13];
22627 let b = vec![4, 8, 12, 16, 20, 24];
22628 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22629 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22630 let actual = p
22634 .generate_from_embeds(&a, &rows, 8, None, None)
22635 .unwrap()
22636 .token_ids;
22637 assert_eq!(actual, expected);
22638 assert!(p.kv_history.is_empty());
22639 let mut extended = a.clone();
22640 extended.push(17);
22641 let after_media = p
22642 .generate_from_ids(&extended, 8, None, None)
22643 .unwrap()
22644 .token_ids;
22645 p.reset_session();
22646 let fresh = p
22647 .generate_from_ids(&extended, 8, None, None)
22648 .unwrap()
22649 .token_ids;
22650 assert_eq!(after_media, fresh);
22651 assert!(
22652 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22653 .is_err()
22654 );
22655 p.reset_session();
22659 p.generate_from_ids(&a, 1, None, None).unwrap();
22660 let mut media_ids = p.kv_history.clone();
22661 assert!(!media_ids.is_empty());
22662 media_ids.extend_from_slice(&[19, 21, 23]);
22663 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22664 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22665 let mut oracle = mimo_test_pipeline();
22666 oracle.speculative = false;
22667 oracle.ignore_eos = true;
22668 oracle.sampler_config.temperature = 0.0;
22669 oracle.sampler_config.repetition_penalty = 1.0;
22670 let expected = oracle
22671 .generate_from_ids(&source_ids, 8, None, None)
22672 .unwrap()
22673 .token_ids;
22674 assert_eq!(
22675 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22676 .unwrap()
22677 .token_ids,
22678 expected
22679 );
22680 assert!(p.kv_history.is_empty());
22681 let mut bad = rows;
22682 bad[0] = f32::NAN;
22683 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22684 }
22685
22686 fn f32_bits(v: &[f32]) -> Vec<u32> {
22687 v.iter().map(|x| x.to_bits()).collect()
22688 }
22689
22690 #[test]
22697 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22698 let mut p = mimo_test_pipeline();
22699 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22700 assert_eq!(kv, vec![1, 2, 2, 1]);
22701 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22702 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22703 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22704 let hs = p.hidden_size;
22705 let mut decode = Vec::new();
22706 for (pos, &id) in ids.iter().enumerate() {
22707 let e = p.embed_single(id);
22708 let h = p.forward_layers(&e, pos, None);
22709 decode.push(p.logits_from_hidden(&h));
22710 }
22711 for l in &p.kv_cache.layers {
22712 assert_eq!(l.seq_len, 12);
22713 assert_eq!(l.head_values(0).len(), 12 * 8);
22715 }
22716 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22717
22718 p.clear_sequence_state();
22719 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22720 for pos in 0..ids.len() {
22721 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22722 assert_eq!(
22723 f32_bits(&decode[pos]),
22724 f32_bits(&lg),
22725 "whole prompt, pos {pos}"
22726 );
22727 }
22728
22729 p.clear_sequence_state();
22730 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22731 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22732 for pos in 0..ids.len() {
22733 let row = if pos < 5 {
22734 &a[pos * hs..(pos + 1) * hs]
22735 } else {
22736 &b[(pos - 5) * hs..(pos - 4) * hs]
22737 };
22738 let lg = p.logits_from_hidden(row);
22739 assert_eq!(
22740 f32_bits(&decode[pos]),
22741 f32_bits(&lg),
22742 "two chunks, pos {pos}"
22743 );
22744 }
22745
22746 let last = |p: &mut Pipeline| {
22749 p.clear_sequence_state();
22750 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22751 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22752 };
22753 let base = last(&mut p);
22754 let mut no_sinks = mimo_test_pipeline();
22755 for l in &mut no_sinks.kv_cache.layers {
22756 l.sinks = None;
22757 }
22758 assert_ne!(
22759 f32_bits(&last(&mut no_sinks)),
22760 f32_bits(&base),
22761 "sinks are live"
22762 );
22763 let mut wide = mimo_test_pipeline();
22764 wide.swa = Some((64, usize::MAX));
22765 assert_ne!(
22766 f32_bits(&last(&mut wide)),
22767 f32_bits(&base),
22768 "window is live"
22769 );
22770
22771 p.clear_sequence_state();
22773 p.ignore_eos = true;
22774 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22775 assert_eq!(r.token_ids.len(), 4);
22776 }
22777
22778 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22781 let (hs, nh) = (16usize, 4usize);
22782 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22783 p.layer_dump = None;
22784 p.swa = Some((6, 4));
22785 p.proj_gate_sigmoid = true;
22786 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22787 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22788 let g: Vec<f32> = (0..nh * hs)
22789 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22790 .collect();
22791 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22792 }
22793 }
22794 p.swa_trim = trim;
22795 p
22796 }
22797
22798 #[test]
22804 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22805 let mut a = spark_test_pipeline(None);
22806 let mut b = spark_test_pipeline(Some((2, 4)));
22807 assert_eq!(
22808 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22809 vec![Some(6), Some(6), Some(6), None]
22810 );
22811 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22812 let mut pos = 0usize;
22813 for &n in [5usize, 13, 7, 16].iter().cycle() {
22814 if pos >= ids.len() {
22815 break;
22816 }
22817 let end = (pos + n).min(ids.len());
22818 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22819 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22820 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22821 pos = end;
22822 }
22823 for _ in 0..23 {
22824 let e = a.embed_single(((pos * 7) % 64) as u32);
22825 let ha = a.forward_layers(&e, pos, None);
22826 let hb = b.forward_layers(&e, pos, None);
22827 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22828 for li in 0..3 {
22829 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22830 }
22831 pos += 1;
22832 }
22833 for round in 0..9 {
22834 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22835 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22836 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22837 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22838 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22839 if round % 2 == 1 {
22840 for p in [&mut a, &mut b] {
22842 for l in &mut p.kv_cache.layers {
22843 l.truncate_last(2);
22844 }
22845 }
22846 } else {
22847 pos += 2;
22848 }
22849 }
22850 let e = a.embed_single(9);
22851 let (ha, hb) = (
22852 a.forward_layers(&e, pos, None),
22853 b.forward_layers(&e, pos, None),
22854 );
22855 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22856 for li in 0..3 {
22857 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22858 assert!(lb.base() > 0, "layer {li} trimmed");
22859 assert_eq!(la.base(), 0);
22860 assert_eq!(lb.pos_len(), la.seq_len);
22861 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22862 }
22863 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22864 assert_eq!(
22865 (gb.base(), gb.seq_len),
22866 (0, ga.seq_len),
22867 "the global layer keeps all"
22868 );
22869 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22870 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22871 }
22872
22873 #[test]
22878 fn swa_trim_wire_handoff_continues_bitwise() {
22879 let mut a = spark_test_pipeline(None);
22880 let mut b = spark_test_pipeline(Some((2, 4)));
22881 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22882 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22883 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22884 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22885 let mut pos = ids.len();
22886 for _ in 0..5 {
22887 let e = a.embed_single(((pos * 5) % 64) as u32);
22888 assert_eq!(
22889 f32_bits(&a.forward_layers(&e, pos, None)),
22890 f32_bits(&b.forward_layers(&e, pos, None))
22891 );
22892 pos += 1;
22893 }
22894 let mut c = spark_test_pipeline(Some((2, 4)));
22895 for li in 0..4 {
22896 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22897 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22898 }
22899 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22900 assert_eq!(c.kv_cache.seq_len(), pos);
22901 for step in 0..17 {
22902 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22903 let ha = a.forward_layers(&e, pos, None);
22904 let hc = c.forward_layers(&e, pos, None);
22905 assert_eq!(
22906 f32_bits(&ha),
22907 f32_bits(&hc),
22908 "after the hand-off, step {step}"
22909 );
22910 pos += 1;
22911 }
22912 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22913 }
22914
22915 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
22920 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
22921 let synth = |len: usize, salt: usize| -> Vec<f32> {
22922 (0..len)
22923 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22924 .collect()
22925 };
22926 let qt = |rows: usize, cols: usize, salt: usize| {
22927 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22928 };
22929 let layers = widths
22930 .iter()
22931 .copied()
22932 .enumerate()
22933 .map(|(k, inter)| {
22934 let s = 500 + k * 40;
22935 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22936 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22937 MtpModule {
22938 enorm: vec![1.0; hs],
22939 hnorm: vec![1.0; hs],
22940 eh_proj: qt(hs, 2 * hs, s),
22941 layer: LayerWeights {
22942 input_norm: vec![1.0; hs],
22943 post_norm: vec![1.0; hs],
22944 attn_out_norm: None,
22945 ffn_out_norm: None,
22946 layer_scale: None,
22947 attn: AttnKind::Full {
22948 wq: qt(nh * hd, hs, s + 1),
22949 wk: qt(nkv * hd, hs, s + 2),
22950 wv: qt(nkv * vd, hs, s + 3),
22951 wo: qt(hs, nh * vd, s + 4),
22952 q_norm: None,
22953 k_norm: None,
22954 output_gate: false,
22955 softplus_gate: None,
22956 bias: None,
22957 },
22958 ffn: FfnKind::Dense(DenseFfn {
22959 gate_proj: qt(inter, hs, s + 5),
22960 up_proj: qt(inter, hs, s + 6),
22961 down_proj: qt(hs, inter, s + 7),
22962 act: Act::Silu,
22963 down_t: None,
22964 segs: Vec::new(),
22965 }),
22966 },
22967 final_norm: vec![1.0; hs],
22968 kv,
22969 }
22970 })
22971 .collect();
22972 mimo_mtp::MimoMtp::from_layers(layers)
22973 }
22974
22975 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22976 mimo_test_mtp_widths(&vec![24; n], gain)
22977 }
22978
22979 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22980 p.clear_sequence_state();
22981 p.speculative = spec;
22982 p.ignore_eos = true;
22983 p.sampler_config.temperature = 0.0;
22984 p.generate_from_ids(ids, n, None, None).unwrap()
22985 }
22986
22987 #[test]
22988 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
22989 let mut p = mimo_test_pipeline();
22990 let widths = [7usize, 13, 29];
22991 assert_ne!(widths[0], p.intermediate_size);
22992 assert_ne!(widths[1], p.intermediate_size);
22993 assert_ne!(widths[2], p.intermediate_size);
22994 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
22995 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
22996
22997 let trunk = p.probe_ffn_mass(&ids);
22998 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
22999 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
23000 assert_eq!(got.len(), widths.len());
23001 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
23002 assert_eq!(stat.mass.len(), width, "draft layer {k}");
23003 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
23004 assert_eq!(stat.observed_positions, ids.len() - k - 1);
23005 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
23006 assert!(stat
23007 .mass
23008 .iter()
23009 .zip(&stat.squared_mass)
23010 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23011 }
23012 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23013 let st = p.mimo_mtp.as_ref().unwrap();
23014 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23015
23016 let h = p.hidden_size;
23019 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23020 panic!("synthetic MiMo MTP must be dense")
23021 };
23022 dense.segs.push(FfnSeg {
23023 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23024 up: QTensor::from_f32(vec![0.0; h], 1, h),
23025 down: QTensor::from_f32(vec![0.0; h], h, 1),
23026 start: dense.gate_proj.rows(),
23027 width: 1,
23028 });
23029 assert!(p
23030 .probe_mtp_ffn(&ids)
23031 .unwrap_err()
23032 .contains("tube-segmented draft FFNs are not supported"));
23033 assert_eq!(p.kv_cache.seq_len(), 0);
23034 assert!(p
23035 .mimo_mtp
23036 .as_ref()
23037 .unwrap()
23038 .layers
23039 .iter()
23040 .all(|m| m.kv.seq_len == 0));
23041 }
23042
23043 #[test]
23044 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23045 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23046 p.clear_sequence_state();
23047 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23048 let _exact = ExactMtpCalibrationGuard::enter();
23049 let got = crate::gpu::cpu_scope(|| {
23050 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, rows)
23051 })
23052 .expect("teacher-forced MiMo probe");
23053 st.reset();
23054 p.mimo_mtp = Some(st);
23055 p.reset_mtp_probe_state();
23056 got
23057 }
23058
23059 for post_norm_hidden in [false, true] {
23060 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23061 let make = || {
23062 let mut p = mimo_test_pipeline();
23063 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23064 st.post_norm_hidden = post_norm_hidden;
23065 p.mimo_mtp = Some(st);
23066 p
23067 };
23068 let mut full = make();
23069 let expected = collect(&mut full, &ids, usize::MAX);
23070 for rows in [1usize, 7, 128] {
23071 let mut chunked = make();
23072 let got = collect(&mut chunked, &ids, rows);
23073 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23074 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23075 assert_eq!(
23076 got.observed_positions, expected.observed_positions,
23077 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23078 );
23079 assert_eq!(
23080 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23081 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23082 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23083 );
23084 assert_eq!(
23085 got.squared_mass
23086 .iter()
23087 .map(|v| v.to_bits())
23088 .collect::<Vec<_>>(),
23089 expected
23090 .squared_mass
23091 .iter()
23092 .map(|v| v.to_bits())
23093 .collect::<Vec<_>>(),
23094 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23095 );
23096 }
23097 }
23098 }
23099 }
23100
23101 #[test]
23107 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23108 for post in [false, true] {
23111 let mut p = mimo_test_pipeline();
23112 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23114 let mut st0 = mimo_test_mtp(3, 1.0);
23115 st0.post_norm_hidden = post;
23116 p.mimo_mtp = Some(st0);
23117 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23118 let hs = p.hidden_size;
23119 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23120 p.mimo_note_rows(&hb, 0);
23121 let mut st = p.mimo_mtp.take().unwrap();
23122 let k = 3;
23125 let mut inc = Vec::new();
23126 for t in 0..ids.len() - k - 1 {
23127 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23128 }
23129 let s = ids.len();
23132 let mut reference = vec![vec![0u32; k]; s - k - 1];
23133 let mut fresh = mimo_test_mtp(3, 1.0);
23134 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23135 let n = s - layer - 1;
23136 let mut cats = vec![0.0f32; n * 2 * hs];
23137 for j in 0..n {
23138 let e = p.embed_single(ids[j + layer + 1]);
23139 let raw = &hb[j * hs..(j + 1) * hs];
23140 let g = if post {
23141 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23142 } else {
23143 raw.to_vec()
23144 };
23145 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23146 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23147 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23148 }
23149 let mut x = vec![0.0f32; n * hs];
23150 m.eh_proj.matmat(&cats, n, &mut x, None);
23151 p.mimo_mtp_block(m, &mut x, n, 0);
23152 for (t, row) in reference.iter_mut().enumerate() {
23153 let y = inference::rms_norm(
23154 &x[t * hs..(t + 1) * hs],
23155 &m.final_norm,
23156 p.rms_eps,
23157 p.norm_style,
23158 );
23159 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23160 }
23161 }
23162 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23163 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23165 assert!(distinct.len() > 3, "{inc:?}");
23166 let last_t = ids.len() - k - 2;
23168 for m in &st.layers {
23169 assert_eq!(m.kv.seq_len, last_t + 1);
23170 }
23171 }
23172 }
23173
23174 #[test]
23181 fn mimo_speculative_greedy_equals_plain_greedy() {
23182 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23183 let _env = ScopedTestEnv::set(&[
23184 ("CMF_GPU", Some("0")),
23185 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23186 ]);
23187 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23188 let n = 24;
23189 let mut p = mimo_test_pipeline();
23190 let plain = mimo_greedy(&mut p, &ids, n, false);
23191 assert_eq!(plain.mtp_drafted, 0);
23192 assert_eq!(plain.token_ids.len(), n);
23193 let plain_kv = p.kv_cache.layers[0].seq_len;
23194
23195 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23197 let spec = mimo_greedy(&mut p, &ids, n, true);
23198 assert!(spec.mtp_drafted > 0, "the round must draft");
23199 assert_eq!(spec.token_ids, plain.token_ids);
23200 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23201
23202 let mut truth: Vec<u32> = ids.clone();
23205 truth.extend(&plain.token_ids);
23206 let mut noisy = truth.clone();
23207 for (i, t) in noisy.iter_mut().enumerate() {
23208 if i % 5 == 0 {
23209 *t = (*t + 1) % 64;
23210 }
23211 }
23212 let mut st = mimo_test_mtp(3, 1.0);
23213 st.draft_override = Some(noisy);
23214 p.mimo_mtp = Some(st);
23215 let spec = mimo_greedy(&mut p, &ids, n, true);
23216 assert_eq!(spec.token_ids, plain.token_ids);
23217 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23218 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23219 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23220 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23221 assert!(
23222 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23223 "{:?}",
23224 stats.accept_hist
23225 );
23226 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23227
23228 let mut st = mimo_test_mtp(3, 1.0);
23231 st.draft_override = Some(truth);
23232 p.mimo_mtp = Some(st);
23233 let spec = mimo_greedy(&mut p, &ids, n, true);
23234 assert_eq!(spec.token_ids, plain.token_ids);
23235 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23236 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23237
23238 let off = mimo_greedy(&mut p, &ids, n, false);
23240 assert_eq!(off.token_ids, plain.token_ids);
23241 assert_eq!(off.mtp_drafted, 0);
23242 }
23243
23244 #[test]
23253 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23254 let p = mimo_test_pipeline();
23255 assert_eq!(
23256 p.graph_attn_decline_reason(),
23257 Some("per-layer KV head counts")
23258 );
23259 assert_eq!(p.wgpu_graph_attn_decline(), None);
23260 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23261 assert_eq!(
23262 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23263 (1, 4, 4, None, false)
23264 );
23265 assert_eq!(g0.invf, p.inv_freq.as_slice());
23266 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23267 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23268 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23269 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23270 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23271 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23272 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23273
23274 let emb = p.embed_single(3);
23277 let mut lg = Vec::new();
23278 assert!(
23279 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23280 .is_none()
23281 );
23282 let mut hid = emb.clone();
23283 assert_eq!(
23284 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23285 crate::gpu::BatchGraphOutcome::Declined
23286 );
23287 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23288 assert!(p.try_multi_burst(3, 0, 4).is_none());
23289 assert!(
23290 p.graph_declines().is_empty(),
23291 "no attention decline logged: {:?}",
23292 p.graph_declines()
23293 );
23294 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23299 assert_eq!(plain().graph_attn_decline_reason(), None);
23300 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23301 assert!(
23302 plain().graph_attn_geom(0).is_none(),
23303 "uniform models keep the historical arms"
23304 );
23305 let mut q = plain();
23306 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23307 assert_eq!(
23308 q.graph_attn_decline_reason(),
23309 Some("learned attention sinks")
23310 );
23311 assert_eq!(
23312 q.graph_attn_geom(1).unwrap().sink,
23313 Some(&[0.25f32, -0.25][..])
23314 );
23315 let mut q = plain();
23316 q.set_attn_geometry(None, Some(2)).unwrap();
23317 assert_eq!(
23318 q.graph_attn_decline_reason(),
23319 Some("V heads narrower than Q/K heads")
23320 );
23321 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23322 let mut q = plain();
23323 q.sliding_layers = Some(vec![true, false]);
23324 q.swa = Some((4, usize::MAX));
23325 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23326 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23327 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23328
23329 let mut q = mimo_test_pipeline();
23333 q.rope_scale = 2.0;
23334 q.rope_scale_local = 1.25;
23335 assert_eq!(q.wgpu_graph_attn_decline(), None);
23336 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23337 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23338
23339 let mut q = plain();
23342 q.rope_scale = 2.0;
23343 assert_eq!(
23344 q.wgpu_graph_attn_decline(),
23345 Some("scaled RoPE without per-layer geometry")
23346 );
23347 }
23348
23349 #[cfg(target_os = "macos")]
23355 #[test]
23356 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23357 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23361 p.swa = Some((16, usize::MAX));
23362 p.sliding_layers = Some(vec![true, true]);
23363 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23364 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23365
23366 assert_eq!(
23367 p.graph_attn_decline_reason(),
23368 Some("learned attention sinks")
23369 );
23370 let swa_graph = p.metal_graph_swa();
23371 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23372 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23373 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23374 assert!(
23375 native_sink_attend,
23376 "the q1 sink admission must follow the SWA door"
23377 );
23378 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23381 }
23382
23383 #[test]
23384 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23385 let mut p = mimo_test_pipeline();
23386 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23387 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23388 for _ in 0..if li == 0 { 2 } else { 12 } {
23389 layer.append(&row, &row, &[]);
23390 }
23391 }
23392 p.mimo_verify_rewind(9).unwrap();
23393 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23394 for layer in &p.kv_cache.layers[1..] {
23395 assert_eq!(layer.seq_len, 9);
23396 }
23397 }
23398
23399 #[test]
23403 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23404 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23405 let _ = std::fs::remove_dir_all(&dir);
23406 let mut p = mimo_test_pipeline();
23407 let hs = p.hidden_size;
23408 let ids = [5u32, 9, 11, 2, 40];
23409 p.layer_dump = Some(dir.join("decode"));
23410 for (pos, &id) in ids.iter().enumerate() {
23411 let e = p.embed_single(id);
23412 let _ = p.forward_layers(&e, pos, None);
23413 }
23414 p.clear_sequence_state();
23415 p.layer_dump = Some(dir.join("prefill"));
23416 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23417 for pos in 0..ids.len() {
23418 for li in 0..p.num_layers {
23419 let name = format!("p{pos:06}_l{li:02}.f32");
23420 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23421 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23422 assert_eq!(a.len(), hs * 4, "{name}");
23423 assert_eq!(a, b, "{name}");
23424 }
23425 }
23426 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23427 let vals: Vec<f32> = last
23428 .chunks(4)
23429 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23430 .collect();
23431 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23432 let _ = std::fs::remove_dir_all(&dir);
23433 }
23434
23435 #[test]
23436 fn attn_geometry_and_sinks_are_validated() {
23437 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23438 assert!(
23439 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23440 "one entry per layer"
23441 );
23442 assert!(
23443 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23444 "3 does not divide 4"
23445 );
23446 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23447 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23448 assert!(
23449 p.set_attn_geometry(None, Some(5)).is_err(),
23450 "V wider than the head"
23451 );
23452 p.set_attn_geometry(None, Some(4)).unwrap();
23453 assert_eq!(
23454 p.v_head_dim, None,
23455 "v_head_dim == head_dim is the uniform case"
23456 );
23457 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23458 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23459 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23460 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23461 assert!(
23462 p.kv_cache.layers[1].sinks.is_some(),
23463 "a reshape keeps the layer's sinks"
23464 );
23465 assert_eq!(p.layer_geom(1).0, 4);
23466 assert!(
23467 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23468 "one sink per Q head"
23469 );
23470 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23471 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23472 }
23473
23474 #[test]
23477 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23478 let cfg = || {
23479 Some(crate::nystrom::O1Cfg {
23480 layers: crate::nystrom::O1Layers::All,
23481 m: 4,
23482 w: 8,
23483 sink: 2,
23484 rect: crate::nystrom::O1Rect::Aggregate,
23485 })
23486 };
23487 let mut p = mimo_test_pipeline();
23488 p.set_o1(cfg());
23489 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23490 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23491 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23492 q.sliding_layers = Some(vec![false, false, true]);
23493 q.swa = Some((4, usize::MAX));
23494 q.set_o1(cfg());
23495 assert_eq!(q.o1_flags, vec![true, false, false]);
23496 }
23497
23498 #[test]
23499 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23500 const B: usize = 19;
23501 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23502 p.set_o1(Some(crate::nystrom::O1Cfg {
23503 layers: crate::nystrom::O1Layers::All,
23504 m: 4,
23505 w: 8,
23506 sink: 2,
23507 rect: crate::nystrom::O1Rect::Aggregate,
23508 }));
23509 p.o1_begin_with_prefix(Some(B));
23510 let ids: Vec<u32> = (0..B as u32).collect();
23511 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23512
23513 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23514 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23515 let next = p.embed_single(B as u32);
23516 let _ = p.forward_layers(&next, B, None);
23517 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23518 }
23519
23520 #[test]
23521 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23522 const B: usize = 19;
23523 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23524 let gdn_cfg = crate::linear_core::GdnCfg {
23528 num_v_heads: 2,
23529 num_k_heads: 1,
23530 key_head_dim: 2,
23531 value_head_dim: 4,
23532 conv_kernel: 3,
23533 hidden_size: 8,
23534 rms_eps: 1e-6,
23535 output_gate_sigmoid: false,
23536 };
23537 let synth = |n: usize, salt: usize| -> Vec<f32> {
23538 (0..n)
23539 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23540 .collect()
23541 };
23542 let qt = |rows: usize, cols: usize, salt: usize| {
23543 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23544 };
23545 let c_dim = gdn_cfg.conv_dim();
23546 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23547 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23548 in_proj_qkv: qt(c_dim, 8, 1),
23549 in_proj_z: qt(vd, 8, 2),
23550 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23551 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23552 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23553 a_log: vec![0.2, 0.5],
23554 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23555 norm: vec![1.0; gdn_cfg.value_head_dim],
23556 out_proj: qt(8, vd, 7),
23557 });
23558 p.gdn_cfg = Some(gdn_cfg);
23559 p.set_o1(Some(crate::nystrom::O1Cfg {
23560 layers: crate::nystrom::O1Layers::All,
23561 m: 4,
23562 w: 8,
23563 sink: 2,
23564 rect: crate::nystrom::O1Rect::Aggregate,
23565 }));
23566 p.o1_begin_with_prefix(Some(B));
23567 for pos in 0..B - 2 {
23568 let emb = p.embed_single(pos as u32);
23569 let _ = p.forward_layers(&emb, pos, None);
23570 }
23571 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23572
23573 let e1 = p.embed_single((B - 2) as u32);
23574 let e2 = p.embed_single((B - 1) as u32);
23575 let _ = p.forward_pair(&e1, &e2, B - 2);
23576
23577 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23578 assert!(
23579 p.kv_cache
23580 .layers
23581 .iter()
23582 .enumerate()
23583 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23584 );
23585 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23586 assert_ne!(
23587 p.kv_cache.layers[0].linear_state, lane1_state,
23588 "real pair must commit GDN lane 2 before returning"
23589 );
23590 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23591 let next = p.embed_single(B as u32);
23592 let _ = p.forward_layers(&next, B, None);
23593 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23594 }
23595
23596 #[test]
23597 fn o1_error_observation_stays_terminal_until_reset() {
23598 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23599 p.set_o1(Some(crate::nystrom::O1Cfg {
23600 layers: crate::nystrom::O1Layers::All,
23601 m: 4,
23602 w: 8,
23603 sink: 2,
23604 rect: crate::nystrom::O1Rect::Aggregate,
23605 }));
23606 p.o1_begin();
23607 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23608
23609 assert!(p.o1_seal_checked().is_err());
23610 assert!(
23611 p.o1_seal_checked().is_err(),
23612 "retry must see the sticky error"
23613 );
23614 let k = vec![0.2f32; 4];
23615 let v = vec![0.3f32; 4];
23616 p.kv_cache.layers[0].append(&k, &v, &[]);
23617 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23618
23619 p.reset_session();
23620 p.o1_begin();
23621 p.kv_cache.layers[0].append(&k, &v, &[]);
23622 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23623 }
23624
23625 #[test]
23626 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23627 let ids = vec![1u32, 2, 3, 4, 5, 6];
23628 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23629 p.graph_logits = Some(vec![123.0]);
23630 p.graph_want_logits = true;
23631 p.graph_failed
23632 .store(true, std::sync::atomic::Ordering::Relaxed);
23633 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23634 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23635 assert!(err.contains("before NLL"));
23636 assert!(p.graph_logits.is_none());
23637 assert!(!p.graph_want_logits);
23638 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23639 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23640
23641 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23642 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23643 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23644 assert_eq!(actual.1, expected.1);
23645 assert!((actual.0 - expected.0).abs() < 1e-9);
23646 }
23647
23648 #[test]
23649 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23650 let ids = vec![1u32, 2, 3, 4, 5, 6];
23651 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23652 p.nll_test_fail_at = Some(1);
23653 let err = p
23654 .nll_ids_from(&ids, 0)
23655 .expect_err("one-shot forward failure");
23656 assert!(err.contains("forward") || err.contains("score row"));
23657 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23658 assert!(!p.graph_want_logits);
23659 assert!(p.graph_logits.is_none());
23660 assert!(p.kv_history.is_empty());
23661
23662 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23663 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23664 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23665 assert_eq!(actual.1, expected.1);
23666 assert!((actual.0 - expected.0).abs() < 1e-9);
23667 }
23668
23669 #[test]
23670 fn nll_serial_failure_before_first_row_is_reported() {
23671 let ids = vec![1u32, 2, 3, 4];
23672 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23673 p.nll_test_force_serial = true;
23674 p.nll_test_fail_at = Some(0);
23675 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23676 assert!(err.contains("serial forward"));
23677 assert!(p.kv_history.is_empty());
23678 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23679 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23680 }
23681
23682 #[test]
23683 fn ffn_probe_failure_discards_recorder_and_state() {
23684 let ids = vec![1u32, 2, 3, 4];
23685 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23686 p.nll_test_fail_at = Some(0);
23687 let err = p
23688 .probe_ffn_mass_batch(&ids)
23689 .expect_err("probe forward failure");
23690 assert!(err.contains("NLL"));
23691 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23692 assert!(p.kv_history.is_empty());
23693 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23694 }
23695
23696 #[test]
23697 fn nll_test_controls_are_pipeline_scoped() {
23698 let ids = vec![1u32, 2, 3, 4];
23699 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23700 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23701 failing.nll_test_force_serial = true;
23702 failing.nll_test_fail_at = Some(0);
23703
23704 assert!(!failing.can_prefill_batched());
23705 assert!(unaffected.can_prefill_batched());
23706 let expected = unaffected
23707 .nll_ids_from(&ids, 0)
23708 .expect("unaffected pipeline remains usable");
23709 let err = failing
23710 .nll_ids_from(&ids, 0)
23711 .expect_err("failure injection belongs to failing pipeline");
23712 assert!(err.contains("serial forward"));
23713 assert!(failing.nll_test_fail_at.is_none());
23714 assert!(unaffected.can_prefill_batched());
23715 let actual = unaffected
23716 .nll_ids_from(&ids, 0)
23717 .expect("unaffected pipeline remains reusable");
23718 assert_eq!(actual.1, expected.1);
23719 assert!((actual.0 - expected.0).abs() < 1e-9);
23720 }
23721
23722 #[test]
23723 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23724 let ids = vec![1u32, 2, 3, 4, 5, 6];
23725 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23726 p.graph_logits = Some(vec![123.0]);
23727 p.graph_want_logits = true;
23728 p.graph_failed
23729 .store(true, std::sync::atomic::Ordering::Relaxed);
23730 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23731
23732 let err = p
23733 .forward_ids(&ids, None)
23734 .expect_err("a failed forward must not become a valid head result");
23735 assert!(err.contains("forward_ids setup"));
23736 assert!(p.graph_logits.is_none());
23737 assert!(!p.graph_want_logits);
23738 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23739 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23740 assert_eq!(p.kv_cache.seq_len(), 0);
23741
23742 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23743 .forward_ids(&ids, None)
23744 .expect("fresh forward_ids");
23745 let actual = p
23746 .forward_ids(&ids, None)
23747 .expect("pipeline remains reusable after a failed forward");
23748 assert_eq!(actual.len(), expected.len());
23749 assert!(
23750 actual
23751 .iter()
23752 .zip(expected)
23753 .all(|(a, b)| (a - b).abs() < 1e-9)
23754 );
23755 assert_eq!(p.kv_cache.seq_len(), ids.len());
23756 }
23757
23758 #[test]
23759 fn sigmoid_router_floor_is_explicit_per_architecture() {
23760 let zero = || DenseFfn {
23766 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23767 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23768 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23769 act: Act::Silu,
23770 down_t: None,
23771 segs: Vec::new(),
23772 };
23773 let m = MoeFfn {
23774 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23775 experts: vec![zero(), zero()],
23776 top_k: 1,
23777 norm_topk_prob: true,
23778 router_sigmoid: true,
23779 sigmoid_denom_eps: 1e-20,
23780 expert_bias: None,
23781 routed_scaling: 2.5,
23782 route_tau: None,
23783 shared: None,
23784 stats: std::cell::RefCell::new(Vec::new()),
23785 act_sq: std::cell::RefCell::new(Vec::new()),
23786 act_rows: std::cell::RefCell::new(Vec::new()),
23787 mask: None,
23788 per_expert_scale: None,
23789 router_input_norm: false,
23790 resonance: None,
23791 grown: Vec::new(),
23792 };
23793 let logits = [-20.0f32, -20.0];
23794 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23795 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23796 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23797 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23798 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23799 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23800 assert!(legacy_wsum > minimax_wsum * 100.0);
23801 }
23802
23803 #[test]
23804 fn resonance_scores_match_formula_and_stable_tie() {
23805 let r = Resonance {
23806 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23808 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23809 k: 1,
23810 bias: vec![1.5, 0.5, 0.0],
23811 shell: Vec::new(),
23812 };
23813 let x = [1.0f32, 1.0];
23814 let mut got = vec![0.0; 3];
23815 r.scores(&x, &mut got);
23816 assert!((got[0] - 0.5).abs() < 1e-6);
23820 assert!((got[1] - 0.5).abs() < 1e-6);
23821 assert!(got[2].abs() < 1e-6);
23822 let best = got
23823 .iter()
23824 .enumerate()
23825 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23826 .map(|(i, _)| i);
23827 assert_eq!(best, Some(0));
23828 assert!(got.iter().all(|v| v.is_finite()));
23829 }
23830
23831 #[test]
23836 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23837 let plain = Resonance {
23843 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23844 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23845 k: 1,
23846 bias: vec![1.5, 0.5, 0.0, 0.0],
23847 shell: Vec::new(),
23848 };
23849 let shelled = Resonance {
23850 mu: plain.mu.clone(),
23851 u: plain.u.clone(),
23852 k: 1,
23853 bias: plain.bias.clone(),
23854 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23855 };
23856 assert!(!plain.has_shell());
23857 assert!(shelled.has_shell());
23858 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23859 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23860 set_growth_shell(Some(true));
23861 assert!(growth_shell_enabled());
23862 let x = [1.0f32, 1.0];
23865 plain.scores(&x, &mut a);
23866 shelled.scores(&x, &mut b);
23867 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23868 assert!(a[2] == 0.0 && a[3] == 0.0);
23869 let xo = [3.0f32, 0.0];
23872 plain.scores(&xo, &mut a);
23873 shelled.scores(&xo, &mut b);
23874 assert_eq!(a[2], -6.0);
23875 assert_eq!(a[3], -6.0);
23876 assert_eq!(
23877 bits(&a[..3]),
23878 bits(&b[..3]),
23879 "trunk rows + the boundary row"
23880 );
23881 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23882 assert_eq!(shelled.effective_shell(4), shelled.shell);
23883 set_growth_shell(Some(false));
23886 assert!(!growth_shell_enabled());
23887 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
23888 shelled.scores(&xo, &mut b);
23889 assert_eq!(bits(&a), bits(&b));
23890 set_growth_shell(None);
23891 let short = Resonance {
23894 shell: vec![f32::INFINITY, f32::INFINITY],
23895 ..shelled
23896 };
23897 set_growth_shell(Some(true));
23898 short.scores(&xo, &mut b);
23899 assert_eq!(bits(&a), bits(&b));
23900 set_growth_shell(None);
23901 }
23902
23903 #[test]
23907 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
23908 let zero = || DenseFfn {
23909 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23910 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23911 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23912 act: Act::Silu,
23913 down_t: None,
23914 segs: Vec::new(),
23915 };
23916 let moe = |sigmoid: bool| MoeFfn {
23917 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
23918 experts: vec![zero(), zero(), zero(), zero()],
23919 top_k: 1,
23920 norm_topk_prob: true,
23921 router_sigmoid: sigmoid,
23922 sigmoid_denom_eps: 1e-6,
23923 expert_bias: None,
23924 routed_scaling: 1.0,
23925 route_tau: None,
23926 shared: None,
23927 stats: std::cell::RefCell::new(Vec::new()),
23928 act_sq: std::cell::RefCell::new(Vec::new()),
23929 act_rows: std::cell::RefCell::new(Vec::new()),
23930 mask: None,
23931 per_expert_scale: None,
23932 router_input_norm: false,
23933 resonance: None,
23934 grown: Vec::new(),
23935 };
23936 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
23937 for sigmoid in [false, true] {
23938 let m = moe(sigmoid);
23939 let (idx, p, wsum) = moe_route(&logits, &m, None);
23940 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
23941 assert_eq!(p[1], 0.0);
23942 assert_eq!(p[3], 0.0);
23943 assert!(p[2] > p[0] && p[0] > 0.0);
23944 assert!(p.iter().all(|v| v.is_finite()));
23945 let w = p[2] / wsum;
23946 if sigmoid {
23947 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
23949 } else {
23950 assert_eq!(
23951 w, 1.0,
23952 "softmax: the renormalized top-1 weight is exactly 1"
23953 );
23954 }
23955 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
23958 assert_eq!(idx, vec![2]);
23959 }
23960 let m = moe(false);
23962 let (idx, _, _) = moe_route(
23963 &[
23964 f32::NEG_INFINITY,
23965 f32::NEG_INFINITY,
23966 f32::NEG_INFINITY,
23967 -9.0,
23968 ],
23969 &m,
23970 None,
23971 );
23972 assert_eq!(idx, vec![3]);
23973 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
23976 assert_eq!(idx, vec![0]);
23977 assert!(p.iter().all(|&v| v == 0.25));
23978 assert!(wsum.is_finite() && wsum > 0.0);
23979 }
23980
23981 #[cfg(feature = "gpu")]
23987 #[test]
23988 fn minimax_device_route_matches_host_sigmoid_contract() {
23989 match crate::gpu_wgpu::selected_and_up() {
23990 None => {
23991 crate::gpu_wgpu::skip_or_fail(module_path!());
23992 return;
23993 }
23994 Some(false) => {
24002 eprintln!(
24003 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24004 module_path!()
24005 );
24006 return;
24007 }
24008 Some(true) => {}
24009 }
24010 let n = 128usize;
24011 let top_k = 4usize;
24012 let logits: Vec<f32> = (0..n)
24013 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24014 .collect();
24015 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24016 let m = MoeFfn {
24017 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24018 experts: Vec::new(),
24019 top_k,
24020 norm_topk_prob: true,
24021 router_sigmoid: true,
24022 sigmoid_denom_eps: 1e-20,
24023 expert_bias: Some(bias.clone()),
24024 routed_scaling: 2.0,
24025 route_tau: None,
24026 shared: None,
24027 stats: std::cell::RefCell::new(Vec::new()),
24028 act_sq: std::cell::RefCell::new(Vec::new()),
24029 act_rows: std::cell::RefCell::new(Vec::new()),
24030 mask: None,
24031 per_expert_scale: None,
24032 router_input_norm: false,
24033 resonance: None,
24034 grown: Vec::new(),
24035 };
24036 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24037 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24038
24039 for shared in [Some(n as u32), None] {
24040 let mut gi = Vec::new();
24041 let mut gw = Vec::new();
24042 assert!(
24043 crate::gpu_wgpu::minimax_moe_route_for_test(
24044 &logits,
24045 Some(&bias),
24046 top_k,
24047 2.0,
24048 shared,
24049 &mut gi,
24050 &mut gw,
24051 ),
24052 "the device did not come up — the test checked nothing"
24053 );
24054 let routed = if shared.is_some() { top_k } else { gi.len() };
24055 assert_eq!(
24056 &gi[..routed],
24057 &ci[..],
24058 "different experts or order (shared={shared:?})"
24059 );
24060 let d = cw
24061 .iter()
24062 .zip(&gw[..routed])
24063 .map(|(a, b)| (a - b).abs())
24064 .fold(0.0f32, f32::max);
24065 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24066 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24067 if let Some(s) = shared {
24068 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24069 assert_eq!(gw[top_k], 1.0, "shared weight");
24070 }
24071 }
24072 }
24073
24074 #[cfg(feature = "gpu")]
24079 #[test]
24080 fn minimax_device_cold_weights_match_host_contract() {
24081 match crate::gpu_wgpu::selected_and_up() {
24082 None => {
24083 crate::gpu_wgpu::skip_or_fail(module_path!());
24084 return;
24085 }
24086 Some(false) => {
24094 eprintln!(
24095 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24096 module_path!()
24097 );
24098 return;
24099 }
24100 Some(true) => {}
24101 }
24102 let n = 128usize;
24103 let top_k = 4usize;
24104 let logits: Vec<f32> = (0..n)
24105 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24106 .collect();
24107 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24108 let m = MoeFfn {
24109 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24110 experts: Vec::new(),
24111 top_k,
24112 norm_topk_prob: true,
24113 router_sigmoid: true,
24114 sigmoid_denom_eps: 1e-20,
24115 expert_bias: Some(bias.clone()),
24116 routed_scaling: 2.0,
24117 route_tau: None,
24118 shared: None,
24119 stats: std::cell::RefCell::new(Vec::new()),
24120 act_sq: std::cell::RefCell::new(Vec::new()),
24121 act_rows: std::cell::RefCell::new(Vec::new()),
24122 mask: None,
24123 per_expert_scale: None,
24124 router_input_norm: false,
24125 resonance: None,
24126 grown: Vec::new(),
24127 };
24128 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24129 assert_eq!(ci.len(), top_k);
24130
24131 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24133 let mut cold = Vec::new();
24134 let mut idx = Vec::new();
24135 let mut dw = Vec::new();
24136 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24137 &logits,
24138 Some(&bias),
24139 top_k,
24140 2.0,
24141 &resident,
24142 n as u32,
24143 &mut cold,
24144 &mut idx,
24145 &mut dw,
24146 ));
24147 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24148 for (r, &e) in ci.iter().enumerate() {
24153 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24154 let host = p[e] / wsum;
24155 let d = (dw[r] - host).abs();
24156 assert!(
24157 d <= 1e-6 * host.abs().max(1.0),
24158 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24159 dw[r]
24160 );
24161 }
24162 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24163 assert_eq!(dw[top_k], 1.0, "shared weight");
24164
24165 let mut remap = resident.clone();
24167 remap[ci[0]] = u32::MAX;
24168 remap[ci[2]] = u32::MAX;
24169 let mut cold = Vec::new();
24170 let mut idx2 = Vec::new();
24171 let mut dw2 = Vec::new();
24172 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24173 &logits,
24174 Some(&bias),
24175 top_k,
24176 2.0,
24177 &remap,
24178 n as u32,
24179 &mut cold,
24180 &mut idx2,
24181 &mut dw2,
24182 ));
24183 let mut resident_weight_checked = 0usize;
24191 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24192 if dw2[r] == 0.0 {
24193 continue; }
24195 let e = slot as usize;
24196 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24197 let host = p[e] / wsum;
24198 let d = (dw2[r] - host).abs();
24199 assert!(
24200 d <= 1e-6 * host.abs().max(1.0),
24201 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24202 );
24203 resident_weight_checked += 1;
24204 }
24205 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24206 println!("partial bank: checked {resident_weight_checked} resident weights");
24207 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24208 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24209 ids.sort_unstable();
24210 let mut want = vec![ci[0], ci[2]];
24211 want.sort_unstable();
24212 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24213 for &(e, w) in &cold {
24214 let host = p[e] / wsum;
24215 let d = (w - host).abs();
24216 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24217 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24218 }
24219 }
24220
24221 #[test]
24226 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24227 let zero = || DenseFfn {
24228 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24229 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24230 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24231 act: Act::Silu,
24232 down_t: None,
24233 segs: Vec::new(),
24234 };
24235 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24236 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24237 experts: vec![zero(), zero(), zero()],
24238 top_k: 1,
24239 norm_topk_prob: norm_topk,
24240 router_sigmoid: false,
24241 sigmoid_denom_eps: 1e-6,
24242 expert_bias: None,
24243 routed_scaling: 1.0,
24244 route_tau: None,
24245 shared: None,
24246 stats: std::cell::RefCell::new(Vec::new()),
24247 act_sq: std::cell::RefCell::new(Vec::new()),
24248 act_rows: std::cell::RefCell::new(Vec::new()),
24249 mask: None,
24250 per_expert_scale: None,
24251 router_input_norm: false,
24252 resonance: resonant.then(|| Resonance {
24253 mu: vec![0.0; 6],
24254 u: Vec::new(),
24255 k: 0,
24256 bias: vec![0.0; 3],
24257 shell: Vec::new(),
24258 }),
24259 grown: Vec::new(),
24260 };
24261 let lo = -0.1f32;
24264 let hi = f32::from_bits(lo.to_bits() - 1);
24265 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24266 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24267 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24269 assert_eq!(
24270 idx,
24271 vec![0],
24272 "softmax tie → lower index (the gated contract)"
24273 );
24274 for norm in [true, false] {
24277 let m = moe(true, norm);
24278 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24279 assert_eq!(idx, vec![1], "norm_topk {norm}");
24280 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24281 assert_eq!(p[1] / wsum, 1.0);
24282 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24285 assert_eq!(idx, vec![0]);
24286 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24288 assert_eq!(idx, vec![2]);
24289 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24290 assert_eq!(idx, vec![0]);
24291 assert_eq!(p[0] / wsum, 1.0);
24292 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24295 assert_eq!(idx, vec![0]);
24296 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24297 }
24298 }
24299}