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 swa_trim_anchor: usize,
421 pub chunk_min_rows: usize,
425 pub sliding_layers: Option<Vec<bool>>,
428 pub no_rope_layers: Option<Vec<bool>>,
433 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
439 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
442 pub kv_prefix: KvPrefix,
446 pub last_prefill_tokens: usize,
449 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
452 pub rotary_dim_local: Option<usize>,
453 pub rope_scale: f32,
454 pub rope_scale_local: f32,
455 pub global_attn: Option<(usize, usize)>,
458 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
461 pub attn_v_norm: bool,
463 pub qk_norm_after_rope: bool,
465 pub proj_gate_sigmoid: bool,
468 pub final_softcap: Option<f32>,
470 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
474 pub attn_softcap: f32,
476 confidence_on: bool,
480 #[cfg(test)]
483 nll_test_fail_at: Option<usize>,
484 #[cfg(test)]
487 nll_test_force_serial: bool,
488}
489
490#[cfg(target_os = "macos")]
491impl Drop for Pipeline {
492 fn drop(&mut self) {
493 let _ = crate::gpu_metal::wait_replay();
495 crate::gpu::kv_mirror_drop(self.graph_kv_id);
496 }
497}
498
499#[cfg(not(target_os = "macos"))]
500impl Drop for Pipeline {
501 fn drop(&mut self) {
502 crate::gpu::graph_kv_reset(self.graph_kv_id);
506 }
507}
508
509pub struct PipelineWeights {
514 pub embed_tokens: QTensor,
516 pub layers: Vec<LayerWeights>,
518 pub lm_head: QTensor,
520 pub final_norm: Vec<f32>,
522}
523
524pub struct LayerWeights {
526 pub input_norm: Vec<f32>,
527 pub post_norm: Vec<f32>,
530 pub attn_out_norm: Option<Vec<f32>>,
533 pub layer_scale: Option<f32>,
535 pub ffn_out_norm: Option<Vec<f32>>,
538 pub ffn: FfnKind,
539 pub attn: AttnKind,
540}
541
542#[derive(Clone, Copy, PartialEq, Debug, Default)]
546pub enum Act {
547 #[default]
548 Silu,
549 GeluTanh,
550 Gelu,
552 Situ {
555 beta: f32,
556 linear_beta: f32,
557 },
558 SwiGluOai {
563 alpha: f32,
564 limit: f32,
565 },
566}
567
568impl Act {
569 pub fn from_arch(name: &str) -> Self {
570 if name == "gelu_tanh" {
571 Self::GeluTanh
572 } else if name == "gelu" {
573 Self::Gelu
574 } else {
575 Self::Silu
576 }
577 }
578
579 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
581 match arch.hidden_act.as_str() {
582 "situ" => Self::Situ {
583 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
584 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
585 },
586 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
591 alpha: 1.702,
592 limit: 7.0,
593 },
594 other => Self::from_arch(other),
595 }
596 }
597
598 #[inline]
603 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
604 Self::SwiGluOai { alpha, limit }
605 }
606
607 #[inline]
608 pub fn apply(self, x: f32) -> f32 {
609 match self {
610 Self::Silu => inference::silu(x),
611 Self::GeluTanh => inference::gelu_tanh(x),
612 Self::Gelu => inference::gelu_erf(x),
613 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
614 Self::SwiGluOai { alpha, limit } => {
615 let gate = x.min(limit);
616 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
617 }
618 }
619 }
620
621 #[inline]
624 pub fn combine(self, g: f32, u: f32) -> f32 {
625 match self {
626 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
627 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
628 }
629 Self::SwiGluOai { alpha, limit } => {
630 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
631 }
632 _ => self.apply(g) * u,
633 }
634 }
635
636 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
640 match self {
641 Self::Silu => Some(crate::gpu::GraphAct::Silu),
642 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
643 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
644 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
645 }
646 }
647
648 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
654 self.moe_job_graph_contract().map(|(act, _)| act)
655 }
656
657 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
663 match self {
664 Self::SwiGluOai { alpha, limit }
665 if alpha.to_bits() == 1.702f32.to_bits()
666 && limit.to_bits() == 7.0f32.to_bits() =>
667 {
668 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
669 }
670 _ => self.graph_act().map(|act| (act, 0.0)),
671 }
672 }
673}
674
675pub struct DenseFfn {
677 pub gate_proj: QTensor,
678 pub up_proj: QTensor,
679 pub down_proj: QTensor,
680 pub act: Act,
682 pub down_t: Option<QTensor>,
688 pub segs: Vec<FfnSeg>,
695}
696
697pub struct FfnSeg {
702 pub gate: QTensor,
703 pub up: QTensor,
704 pub down: QTensor,
705 pub start: usize,
706 pub width: usize,
707}
708
709pub enum FfnKind {
712 Dense(DenseFfn),
713 Moe(MoeFfn),
717 DenseMoe(Box<DenseMoeFfn>),
724}
725
726pub struct DenseMoeFfn {
728 pub dense: DenseFfn,
729 pub moe: MoeFfn,
730 pub post_norm_1: Vec<f32>,
732 pub pre_norm_2: Vec<f32>,
735 pub post_norm_2: Vec<f32>,
737}
738
739pub struct MoeFfn {
740 pub router: QTensor,
742 pub experts: Vec<DenseFfn>,
743 pub top_k: usize,
744 pub norm_topk_prob: bool,
745 pub router_sigmoid: bool,
748 pub sigmoid_denom_eps: f32,
754 pub expert_bias: Option<Vec<f32>>,
758 pub routed_scaling: f32,
761 pub route_tau: Option<f32>,
767 pub shared: Option<(DenseFfn, Option<QTensor>)>,
770 pub stats: std::cell::RefCell<Vec<u64>>,
774 pub act_sq: std::cell::RefCell<Vec<f64>>,
781 pub act_rows: std::cell::RefCell<Vec<f32>>,
787 pub mask: Option<Vec<bool>>,
792 pub per_expert_scale: Option<Vec<f32>>,
795 pub router_input_norm: bool,
799 pub resonance: Option<Resonance>,
803 pub grown: Vec<GrownExpert>,
809}
810
811#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct GrownExpert {
814 pub record: String,
816 pub record_index: usize,
818 pub layer: usize,
819 pub expert: usize,
823}
824
825pub struct Resonance {
827 pub mu: Vec<f32>,
829 pub u: Vec<f32>,
831 pub k: usize,
832 pub bias: Vec<f32>,
834 pub shell: Vec<f32>,
840}
841
842static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
845
846pub fn growth_shell_enabled() -> bool {
851 use std::sync::atomic::Ordering;
852 match GROWTH_SHELL.load(Ordering::Relaxed) {
853 1 => true,
854 2 => false,
855 _ => {
856 let off = std::env::var("CMF_GROWTH_SHELL")
857 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
858 .unwrap_or(false);
859 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
860 !off
861 }
862 }
863}
864
865pub fn set_growth_shell(on: Option<bool>) {
870 GROWTH_SHELL.store(
871 match on {
872 Some(true) => 1,
873 Some(false) => 2,
874 None => 0,
875 },
876 std::sync::atomic::Ordering::Relaxed,
877 );
878}
879
880impl Resonance {
881 pub fn has_shell(&self) -> bool {
883 self.shell.iter().any(|s| s.is_finite())
884 }
885
886 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
889 let mut out = vec![f32::INFINITY; ne];
890 if growth_shell_enabled() {
891 for (o, s) in out.iter_mut().zip(&self.shell) {
892 *o = *s;
893 }
894 }
895 out
896 }
897
898 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
903 let h = x.len();
904 let ne = out.len();
905 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
906 for e in 0..ne {
907 let mu = &self.mu[e * h..(e + 1) * h];
908 let mut d2 = 0.0f32;
909 for j in 0..h {
910 let d = x[j] - mu[j];
911 d2 += d * d;
912 }
913 let mut proj = 0.0f32;
914 for i in 0..self.k {
915 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
916 let mut p = 0.0f32;
917 for j in 0..h {
918 p += (x[j] - mu[j]) * u[j];
919 }
920 proj += p * p;
921 }
922 let err = d2 - proj;
923 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
924 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
925 out[e] = f32::NEG_INFINITY;
926 }
927 }
928 }
929}
930
931pub enum AttnKind {
934 Full {
936 wq: QTensor,
937 wk: QTensor,
938 wv: QTensor,
939 wo: QTensor,
940 q_norm: Option<Vec<f32>>,
941 k_norm: Option<Vec<f32>>,
942 output_gate: bool,
943 softplus_gate: Option<(QTensor, bool)>,
947 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
949 },
950 Linear(VmfPhaseWeights),
952 LinearGdn(GdnWeights),
954 ShortConv(ShortConvWeights),
957 Mla(Box<MlaWeights>),
965 Kda(Box<crate::linear_core::KdaWeights>),
969 Bounded(Box<crate::bounded::BoundedWeights>),
974}
975
976pub struct MlaWeights {
978 pub q_proj: QTensor,
982 pub q_a: Option<QTensor>,
985 pub q_a_norm: Option<Vec<f32>>,
986 pub kv_a: QTensor,
988 pub kv_a_norm: Vec<f32>,
990 pub kv_b: QTensor,
992 pub o_proj: QTensor,
994 pub nh: usize,
995 pub qk_rope: usize,
996 pub qk_nope: usize,
997 pub v_dim: usize,
998 pub lora: usize,
999 pub scale: f32,
1001 pub nope: bool,
1003}
1004
1005pub struct MtpModule {
1010 pub enorm: Vec<f32>,
1011 pub hnorm: Vec<f32>,
1012 pub eh_proj: QTensor,
1014 pub layer: LayerWeights,
1015 pub final_norm: Vec<f32>,
1016 pub kv: crate::kv_cache::LayerKvCache,
1017}
1018
1019const MTP_PROBE_FFN_ROWS: usize = 128;
1028
1029#[derive(Clone, Debug, PartialEq)]
1034pub struct MtpFfnProbe {
1035 pub mass: Vec<f64>,
1036 pub squared_mass: Vec<f64>,
1037 pub observed_positions: usize,
1038}
1039
1040#[cfg(target_os = "macos")]
1047enum MetalRowsItem<'a> {
1048 Gdn {
1049 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1050 first: usize,
1051 },
1052 Attn {
1053 l: crate::gpu_metal::AttnGpuLayer<'a>,
1054 li: usize,
1055 q_norm: Option<&'a [f32]>,
1056 k_norm: Option<&'a [f32]>,
1057 output_gate: bool,
1058 },
1059}
1060
1061#[cfg(target_os = "macos")]
1062struct MetalVerifyPending {
1063 graph: crate::gpu_metal::VerifyGraph,
1064 gdn_layers: Vec<usize>,
1065 attn_layers: Vec<(usize, usize)>,
1066}
1067
1068#[cfg(target_os = "macos")]
1072struct MetalWarmPending {
1073 graph: crate::gpu_metal::VerifyGraph,
1074 cpu_stored: usize,
1075 b: usize,
1076}
1077
1078#[cfg(target_os = "macos")]
1079enum MetalRowsRun {
1080 Declined,
1082 Failed,
1085 Completed(MetalVerifyPending),
1086}
1087
1088#[cfg(target_os = "macos")]
1089enum MetalPrefillOutcome {
1090 Declined,
1091 Failed,
1092 Completed(Vec<f32>),
1093}
1094
1095#[cfg(target_os = "macos")]
1096enum MetalBatchNllOutcome {
1097 Declined,
1098 Failed(String),
1099 Completed(f64, usize),
1100}
1101
1102#[derive(Clone, Copy)]
1108enum SpecTrial {
1109 Spec {
1110 t0: std::time::Instant,
1111 gen0: usize,
1112 rounds: usize,
1113 },
1114 Plain {
1115 t0: std::time::Instant,
1116 gen0: usize,
1117 },
1118 Decided {
1119 spec: bool,
1120 recheck_at: usize,
1121 },
1122}
1123
1124pub(crate) fn spec_time_level() -> u8 {
1128 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1129 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1130 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1131 Err(_) => 0,
1132 })
1133}
1134
1135fn graph_spec_wanted_by_policy(
1143 spec_env: Option<&str>,
1144 spec_default_ok: bool,
1145 penalized: bool,
1146 metal_wgpu: bool,
1147 native_metal: bool,
1148) -> bool {
1149 match spec_env {
1150 Some("0") => false,
1151 Some("1") => true,
1156 Some(_) => !native_metal,
1157 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1160 }
1161}
1162
1163struct SpecStampLog {
1169 t_last: std::time::Instant,
1170 items: Vec<(&'static str, f32)>,
1171}
1172
1173static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1174
1175pub(crate) fn spec_stamp(name: &'static str) {
1176 if spec_time_level() == 0 {
1177 return;
1178 }
1179 if let Ok(mut g) = SPEC_STAMPS.lock() {
1180 if let Some(log) = g.as_mut() {
1181 let now = std::time::Instant::now();
1182 log.items
1183 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1184 log.t_last = now;
1185 }
1186 }
1187}
1188
1189fn spec_stamps_begin() {
1190 if spec_time_level() == 0 {
1191 return;
1192 }
1193 if let Ok(mut g) = SPEC_STAMPS.lock() {
1194 *g = Some(SpecStampLog {
1195 t_last: std::time::Instant::now(),
1196 items: Vec::with_capacity(64),
1197 });
1198 }
1199}
1200
1201fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1202 SPEC_STAMPS
1203 .lock()
1204 .ok()
1205 .and_then(|mut g| g.take())
1206 .map(|l| l.items)
1207 .unwrap_or_default()
1208}
1209
1210fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1213 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1214 for &(n, ms) in items {
1215 match agg.iter_mut().find(|e| e.0 == n) {
1216 Some(e) => {
1217 e.1 += ms;
1218 e.2 += 1;
1219 }
1220 None => agg.push((n, ms, 1)),
1221 }
1222 }
1223 let mut s = String::with_capacity(agg.len() * 16);
1224 for (n, ms, k) in agg {
1225 if k > 1 {
1226 s.push_str(&format!("{n} {ms:.1}/{k} "));
1227 } else {
1228 s.push_str(&format!("{n} {ms:.1} "));
1229 }
1230 }
1231 s
1232}
1233
1234#[derive(Default, Clone, Copy)]
1256struct SpecMon {
1257 round_ms: f64,
1258 tokens: f64,
1259 plain_ms: f64,
1260 n: u32,
1261 fails: u32,
1262 zero_accept_rounds: u8,
1267 metal: bool,
1268}
1269
1270const SPEC_PROXY_TOKENS: f64 = 3.5;
1273const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1276
1277impl SpecMon {
1278 fn round(&mut self, dt_ms: f64, produced: usize) {
1279 if produced <= 1 {
1280 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1281 } else {
1282 self.zero_accept_rounds = 0;
1283 }
1284 self.n += 1;
1285 if self.n == 1 {
1286 return; }
1288 let a = if self.n == 2 { 1.0 } else { 0.3 };
1289 self.round_ms += a * (dt_ms - self.round_ms);
1290 self.tokens += a * (produced as f64 - self.tokens);
1291 }
1292 fn pays(&self) -> bool {
1293 if self.plain_ms > 0.0 {
1294 self.tokens * self.plain_ms > self.round_ms * 1.03
1295 } else {
1296 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1297 }
1298 }
1299 fn repeated_zero_acceptance(&self) -> bool {
1300 self.zero_accept_rounds >= 2
1301 }
1302 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1304 let n = generated.saturating_sub(gen0);
1305 if n >= 8 {
1306 return true;
1307 }
1308 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1309 }
1310}
1311
1312pub const KV_PREFIX_TAIL: usize = 128;
1315
1316#[derive(Debug, Clone, Default)]
1322pub struct KvPrefix {
1323 len: usize,
1324 hash: u64,
1325 tail: Vec<u32>,
1326 device: bool,
1331}
1332
1333impl KvPrefix {
1334 #[inline]
1335 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1336 for &id in ids {
1337 h ^= id as u64;
1338 h = h.wrapping_mul(0x100000001b3);
1339 h ^= h >> 29;
1340 }
1341 h
1342 }
1343
1344 pub fn clear(&mut self) {
1345 self.len = 0;
1346 self.hash = 0xcbf29ce484222325;
1347 self.tail.clear();
1348 self.device = false;
1349 }
1350
1351 pub fn on_device(&self) -> bool {
1353 self.device
1354 }
1355
1356 pub fn set_on_device(&mut self, device: bool) {
1358 self.device = device;
1359 }
1360
1361 pub fn len(&self) -> usize {
1363 self.len
1364 }
1365
1366 pub fn is_empty(&self) -> bool {
1367 self.len == 0
1368 }
1369
1370 pub fn tail_len(&self) -> usize {
1372 self.tail.len()
1373 }
1374
1375 pub fn set(&mut self, ids: &[u32]) {
1377 self.clear();
1378 self.extend(ids);
1379 }
1380
1381 pub fn extend(&mut self, more: &[u32]) {
1383 if self.len == 0 && self.hash == 0 {
1384 self.hash = 0xcbf29ce484222325;
1385 }
1386 self.hash = Self::fold(self.hash, more);
1387 self.len += more.len();
1388 if more.len() >= KV_PREFIX_TAIL {
1389 self.tail.clear();
1390 self.tail
1391 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1392 } else {
1393 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1394 self.tail.drain(..drop);
1395 self.tail.extend_from_slice(more);
1396 }
1397 }
1398
1399 pub fn extension(&self, ids: &[u32]) -> usize {
1403 if self.len == 0 || ids.len() <= self.len {
1404 return 0;
1405 }
1406 let t = self.tail.len();
1407 if ids[self.len - t..self.len] != self.tail[..] {
1408 return 0;
1409 }
1410 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1411 return 0;
1412 }
1413 self.len
1414 }
1415}
1416
1417pub struct GenerateResult {
1419 pub text: String,
1420 pub token_ids: Vec<u32>,
1421 pub prompt_tokens: usize,
1422 pub tokens_generated: usize,
1423 pub finish_reason: String,
1424 pub mtp_drafted: usize,
1426 pub mtp_accepted: usize,
1427 pub token_confidence: Vec<f32>,
1432 pub traces: Vec<TokenTrace>,
1435}
1436
1437#[derive(Clone, Debug)]
1442pub struct TokenTrace {
1443 pub t: usize,
1445 pub token_id: u32,
1447 pub confidence: f32,
1449 pub active_skill: Option<String>,
1451 pub recon: Option<f32>,
1455 pub switched: bool,
1458}
1459
1460#[cfg_attr(not(test), allow(dead_code))]
1465fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1466 let t = if temp > 1e-3 { temp } else { 1.0 };
1467 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1468 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1469 if sum > 0.0 {
1470 (((logits[id as usize] - max) / t).exp()) / sum
1471 } else {
1472 0.0
1473 }
1474}
1475
1476fn prefill_batched() -> bool {
1479 std::env::var("CMF_PREFILL")
1480 .map(|v| v != "seq")
1481 .unwrap_or(true)
1482}
1483
1484#[inline]
1488fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1489 let graph_quality = unmasked && prefer_graph;
1490 let fused_head_quality = graph_quality && native_metal;
1491 (graph_quality, fused_head_quality)
1492}
1493
1494#[derive(Clone, Copy)]
1498enum PrefillIn<'a> {
1499 Ids(&'a [u32]),
1500 Hidden(&'a [f32]),
1501}
1502
1503impl Pipeline {
1510 pub(crate) fn can_prefill_batched(&self) -> bool {
1511 #[cfg(test)]
1512 let force_serial = self.nll_test_force_serial;
1513 #[cfg(not(test))]
1514 let force_serial = false;
1515 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1516 }
1517
1518 fn automatic_gpu_prefix(&self) -> Option<usize> {
1521 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1522 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1523 }
1524
1525 pub fn prefill_chunk(&self) -> usize {
1529 let env = env_prefill_chunk();
1530 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1531 return prefill_chunk_rule(env, ChunkHost::here(), false);
1532 }
1533 prefill_chunk_rule(
1534 None,
1535 ChunkHost::Other,
1536 self.chunk_stack_facts().dense_on_discrete(),
1537 )
1538 }
1539
1540 fn per_token_stack(&self) -> bool {
1544 self.g3n.is_some() || self.g4e.is_some()
1545 }
1546
1547 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1548 let plain_dense = !self.weights.layers.is_empty()
1549 && !self.per_token_stack()
1550 && self.dsv4.is_none()
1551 && self.dsv41.is_none()
1552 && self.qwen4_exp.is_none()
1553 && self.weights.layers.iter().all(|lw| {
1554 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1555 });
1556 let gpu_on = crate::gpu::enabled();
1557 ChunkStackFacts {
1558 plain_dense,
1559 discrete: gpu_on && crate::gpu::discrete(),
1560 gpu_on,
1561 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1564 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1565 multi_gpu: self.gpu_plan.is_some(),
1566 o1: self.o1_active(),
1567 }
1568 }
1569}
1570
1571pub fn prefill_chunk() -> usize {
1580 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1581}
1582
1583fn env_prefill_chunk() -> Option<usize> {
1584 std::env::var("CMF_PREFILL_CHUNK")
1585 .ok()
1586 .and_then(|v| v.parse::<usize>().ok())
1587}
1588
1589#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1591enum ChunkHost {
1592 Macos,
1593 Aarch64,
1595 Other,
1597}
1598
1599impl ChunkHost {
1600 fn here() -> Self {
1601 if cfg!(target_os = "macos") {
1602 ChunkHost::Macos
1603 } else if cfg!(target_arch = "aarch64") {
1604 ChunkHost::Aarch64
1605 } else {
1606 ChunkHost::Other
1607 }
1608 }
1609}
1610
1611const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1618
1619fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1625 if let Some(n) = env {
1626 return n.max(1);
1627 }
1628 match host {
1629 ChunkHost::Macos => 512,
1630 ChunkHost::Aarch64 => 256,
1633 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1634 ChunkHost::Other => 48,
1635 }
1636}
1637
1638#[derive(Clone, Copy, Debug, Default)]
1640struct ChunkStackFacts {
1641 plain_dense: bool,
1644 discrete: bool,
1646 gpu_on: bool,
1648 capacity_split: bool,
1650 multi_gpu: bool,
1652 o1: bool,
1654}
1655
1656impl ChunkStackFacts {
1657 fn dense_on_discrete(self) -> bool {
1658 self.plain_dense
1659 && self.discrete
1660 && self.gpu_on
1661 && !self.capacity_split
1662 && !self.multi_gpu
1663 && !self.o1
1664 }
1665}
1666
1667#[inline]
1673fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1674 if end <= start || start >= input_len {
1675 return 0;
1676 }
1677 let rows = (end.min(input_len) - start).min(input_len - start);
1678 if end < input_len {
1679 rows
1680 } else {
1681 rows.saturating_sub(1)
1682 }
1683}
1684
1685pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1687
1688#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1690pub(crate) struct ReuseLayer {
1691 pub full: bool,
1694 pub host_rows: usize,
1697 pub device_rows: Option<usize>,
1699 pub device_state: bool,
1701}
1702
1703#[derive(Debug, Clone, PartialEq, Eq)]
1705pub(crate) enum ReusePlan {
1706 Ready,
1708 Pull(Vec<(usize, usize, usize)>),
1711 Fresh,
1713}
1714
1715pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1725 let mut pulls = Vec::new();
1726 for (li, l) in layers.iter().enumerate() {
1727 if !l.full {
1728 if l.device_state {
1729 return ReusePlan::Fresh;
1730 }
1731 continue;
1732 }
1733 if l.host_rows == reuse_from {
1734 continue;
1735 }
1736 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1737 pulls.push((li, l.host_rows, reuse_from));
1738 continue;
1739 }
1740 return ReusePlan::Fresh;
1741 }
1742 if pulls.is_empty() {
1743 ReusePlan::Ready
1744 } else {
1745 ReusePlan::Pull(pulls)
1746 }
1747}
1748
1749impl Pipeline {
1750 fn reset_fresh_mtp_draft_state(&mut self) {
1753 self.draft_full_streak = 0;
1754 }
1755
1756 fn clear_sequence_state(&mut self) {
1764 #[cfg(target_os = "macos")]
1767 let _ = crate::gpu_metal::wait_replay();
1768 self.kv_cache.clear();
1769 self.clear_history();
1772 self.graph_logits = None;
1773 if let Some(b) = &mut self.dsv41 {
1774 b.3.clear();
1775 }
1776 crate::gpu::graph_kv_reset(self.graph_kv_id);
1777 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1782 }
1783
1784 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1791 if self.graph_prefill_preferred() {
1792 return true;
1793 }
1794 let kv_id = self.graph_kv_id;
1795 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1796 .map(|li| {
1797 let full = matches!(
1798 self.weights.layers[self.phys_layer(li)].attn,
1799 AttnKind::Full { .. }
1800 );
1801 ReuseLayer {
1802 full,
1803 host_rows: self.kv_cache.layers[li].pos_len(),
1806 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1807 device_state: crate::gpu::graph_state_resident(kv_id, li),
1808 }
1809 })
1810 .collect();
1811 if layers
1815 .iter()
1816 .all(|l| l.device_rows.is_none() && !l.device_state)
1817 {
1818 return true;
1819 }
1820 let plan = kv_reuse_plan(reuse_from, &layers);
1821 let (what, rows, n) = match &plan {
1822 ReusePlan::Ready => ("host ready", 0, 0),
1823 ReusePlan::Fresh => ("fresh", 0, 0),
1824 ReusePlan::Pull(p) => (
1825 "pull",
1826 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1827 p.len(),
1828 ),
1829 };
1830 let t0 = std::time::Instant::now();
1831 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1832 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1833 eprintln!(
1834 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1835 if ok { "" } else { " (failed → fresh)" },
1836 t0.elapsed().as_secs_f64() * 1e3
1837 );
1838 }
1839 ok
1840 }
1841
1842 fn apply_kv_reuse_plan(
1843 &mut self,
1844 reuse_from: usize,
1845 plan: ReusePlan,
1846 layers: &[ReuseLayer],
1847 ) -> bool {
1848 let kv_id = self.graph_kv_id;
1849 match plan {
1850 ReusePlan::Fresh => return false,
1851 ReusePlan::Ready => {}
1852 ReusePlan::Pull(pulls) => {
1853 let (nkv, hd) = {
1858 let c = &self.kv_cache.layers[pulls[0].0];
1859 (c.num_kv_heads, c.head_dim)
1860 };
1861 let uniform = pulls.iter().all(|&(li, _, _)| {
1862 let c = &self.kv_cache.layers[li];
1863 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1864 });
1865 let batched = if uniform {
1866 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1867 } else {
1868 None
1869 };
1870 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1871 Some(rows) => rows,
1872 None => {
1873 let mut rows = Vec::with_capacity(pulls.len());
1874 for &(li, from, to) in &pulls {
1875 let (lnkv, lhd) = {
1876 let c = &self.kv_cache.layers[li];
1877 (c.num_kv_heads, c.head_dim)
1878 };
1879 let Some((k, v, first_valid)) =
1880 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1881 else {
1882 return false;
1883 };
1884 let need_from = match self.layer_window(li) {
1888 Some(w) => from.max((to + 1).saturating_sub(w)),
1889 None => from,
1890 };
1891 if first_valid > need_from {
1892 return false;
1893 }
1894 rows.push((k, v));
1895 }
1896 rows
1897 }
1898 };
1899 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1900 let cache = &mut self.kv_cache.layers[li];
1901 let row = cache.num_kv_heads * cache.head_dim;
1902 for p in 0..to - from {
1903 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1904 }
1905 if cache.pos_len() != to {
1906 return false;
1907 }
1908 }
1909 }
1910 }
1911 for (li, l) in layers.iter().enumerate() {
1915 if l.full
1916 && l.device_rows.is_some_and(|d| d > reuse_from)
1917 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1918 {
1919 return false;
1920 }
1921 }
1922 true
1923 }
1924
1925 fn finish_generation(
1931 &mut self,
1932 mtp: &mut Option<MtpModule>,
1933 router: &mut Option<crate::swarm::DynRouter>,
1934 clear_sequence: bool,
1935 ) {
1936 if router.is_some() {
1940 let _ = self.set_active_skill(None);
1941 }
1942 #[cfg(target_os = "macos")]
1949 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1950 if clear_sequence {
1951 self.clear_sequence_state();
1952 if let Some(m) = mtp.as_mut() {
1953 m.kv.clear();
1959 }
1960 if let Some(m) = self.mtp.as_mut() {
1961 m.kv.clear();
1965 }
1966 }
1967 self.graph_want_logits = false;
1968 self.graph_head_required = false;
1969 self.graph_logits = None;
1970 self.graph_failed
1971 .store(false, std::sync::atomic::Ordering::Relaxed);
1972 self.cancel
1973 .store(false, std::sync::atomic::Ordering::Relaxed);
1974 self.dyn_router = router.take().or(self.dyn_router.take());
1975 self.mtp = mtp.take().or(self.mtp.take());
1976 self.mtp_graph_mode = None;
1977 self.spec_forced = None;
1978 }
1979
1980 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1984 if self
1985 .graph_failed
1986 .swap(false, std::sync::atomic::Ordering::Relaxed)
1987 {
1988 self.cancel
1989 .store(false, std::sync::atomic::Ordering::Relaxed);
1990 self.clear_sequence_state();
1991 self.graph_logits = None;
1992 self.graph_want_logits = false;
1993 self.graph_head_required = false;
1994 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1995 }
1996 Ok(())
1997 }
1998
1999 #[cfg(target_os = "macos")]
2000 fn fail_metal_graph(&mut self, reason: &str) {
2001 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2002 self.clear_sequence_state();
2003 self.graph_logits = None;
2004 self.graph_failed
2005 .store(true, std::sync::atomic::Ordering::Relaxed);
2006 self.cancel
2007 .store(true, std::sync::atomic::Ordering::Relaxed);
2008 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2009 }
2010
2011 fn nll_begin(&mut self) -> Result<(), String> {
2016 if self
2017 .graph_failed
2018 .swap(false, std::sync::atomic::Ordering::Relaxed)
2019 {
2020 self.cancel
2021 .store(false, std::sync::atomic::Ordering::Relaxed);
2022 self.clear_sequence_state();
2023 self.graph_logits = None;
2024 self.graph_want_logits = false;
2025 self.graph_head_required = false;
2026 return Err("GPU graph failed before NLL scoring".to_string());
2027 }
2028 self.clear_sequence_state();
2029 self.graph_logits = None;
2030 self.graph_want_logits = false;
2031 self.graph_head_required = false;
2032 Ok(())
2033 }
2034
2035 fn nll_end(&mut self) {
2039 self.clear_sequence_state();
2040 self.graph_logits = None;
2041 self.graph_want_logits = false;
2042 self.graph_head_required = false;
2043 self.graph_failed
2044 .store(false, std::sync::atomic::Ordering::Relaxed);
2045 }
2046
2047 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2050 #[cfg(test)]
2051 if self.nll_test_fail_at == Some(pos) {
2052 self.nll_test_fail_at = None;
2053 self.graph_failed
2054 .store(true, std::sync::atomic::Ordering::Relaxed);
2055 self.cancel
2056 .store(true, std::sync::atomic::Ordering::Relaxed);
2057 }
2058 if self
2059 .graph_failed
2060 .swap(false, std::sync::atomic::Ordering::Relaxed)
2061 {
2062 self.cancel
2063 .store(false, std::sync::atomic::Ordering::Relaxed);
2064 self.clear_sequence_state();
2065 self.graph_logits = None;
2066 self.graph_want_logits = false;
2067 return Err(format!(
2068 "GPU graph failed during NLL {phase} at position {pos}"
2069 ));
2070 }
2071 Ok(())
2072 }
2073
2074 #[inline]
2078 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2079 virtual_idx % self.physical_layers
2080 }
2081
2082 #[inline]
2085 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2086 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2087 }
2088
2089 #[allow(clippy::too_many_arguments)]
2091
2092 #[cfg(target_os = "macos")]
2111 fn graph_prefill_preferred(&self) -> bool {
2112 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2113 if !crate::gpu::enabled_here()
2114 || !graph_force
2115 || std::env::var("CMF_GPU_BLOCK")
2116 .map(|v| v == "0")
2117 .unwrap_or(false)
2118 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2121 {
2122 return false;
2123 }
2124 self.weights
2125 .layers
2126 .iter()
2127 .any(|lw| {
2128 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2129 })
2130 }
2131
2132 #[cfg(not(target_os = "macos"))]
2139 fn batch_prefix_prefill(&self) -> bool {
2140 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2141 Ok("0") => return false,
2142 Ok("1") => true,
2143 _ => false,
2144 };
2145 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2146 && crate::gpu::enabled_here()
2147 && !self.graph_refused()
2148 && (forced || self.graph_attn_decline_reason().is_some())
2149 && self.wgpu_graph_attn_decline().is_none()
2150 && self.attn_softcap == 0.0
2151 && self
2152 .weights
2153 .layers
2154 .iter()
2155 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2156 && self.automatic_gpu_prefix().is_some()
2157 }
2158
2159 fn g4_batch_prefill(&self) -> bool {
2165 #[cfg(target_os = "macos")]
2166 {
2167 false
2168 }
2169 #[cfg(not(target_os = "macos"))]
2170 {
2171 !self.weights.layers.is_empty()
2175 && !self.per_token_stack()
2176 && (self.global_attn.is_some() || self.attn_v_norm)
2177 && crate::gpu::g4_batch_on()
2178 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2179 && crate::gpu::enabled_here()
2180 && crate::gpu::discrete()
2181 && !self.graph_refused()
2182 && !self.o1_active()
2183 && self.attn_softcap == 0.0
2184 && self.wgpu_graph_attn_decline().is_none()
2185 && self.weights.layers.iter().all(|lw| {
2186 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2187 })
2188 && self.automatic_gpu_prefix().is_none()
2189 }
2190 }
2191
2192 #[cfg(not(target_os = "macos"))]
2199 fn g4_batch_ingest(
2200 &mut self,
2201 ids: &[u32],
2202 pos0: usize,
2203 keep_all: bool,
2204 ) -> Result<(usize, Vec<f32>), String> {
2205 let hs = self.hidden_size;
2206 let chunk = self.generation_batch_k().max(1);
2207 let end_all = pos0 + ids.len();
2208 let mut pos = pos0;
2209 let mut out = Vec::new();
2210 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2211 let end = (pos + chunk).min(end_all);
2212 let bk = end - pos;
2213 let mut hiddens = vec![0f32; bk * hs];
2214 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2215 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2216 }
2217 let positions: Vec<usize> = (pos..end).collect();
2218 let t_chunk = std::time::Instant::now();
2219 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2220 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2221 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2222 eprintln!(
2223 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2224 bk as f64 / (ms / 1000.0)
2225 );
2226 }
2227 match outcome {
2228 crate::gpu::BatchGraphOutcome::Completed => {
2229 if keep_all {
2230 out.extend_from_slice(&hiddens);
2231 } else {
2232 out = hiddens[(bk - 1) * hs..].to_vec();
2233 }
2234 pos = end;
2235 }
2236 crate::gpu::BatchGraphOutcome::Failed => {
2237 return Err("Gemma-4 batched prefill failed after admission".into());
2238 }
2239 crate::gpu::BatchGraphOutcome::Declined => {
2240 #[cfg(feature = "gpu")]
2241 if pos > pos0 {
2242 self.pull_lagging_host_kv(0, self.num_layers, pos);
2243 }
2244 break;
2245 }
2246 }
2247 }
2248 Ok((pos - pos0, out))
2249 }
2250
2251 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2256 #[cfg(not(target_os = "macos"))]
2257 {
2258 if ids.is_empty() || !self.g4_batch_prefill() {
2259 return None;
2260 }
2261 self.clear_sequence_state();
2262 match self.g4_batch_ingest(ids, 0, true) {
2263 Ok((n, rows)) if n == ids.len() => Some(rows),
2264 _ => {
2265 self.clear_sequence_state();
2266 None
2267 }
2268 }
2269 }
2270 #[cfg(target_os = "macos")]
2271 {
2272 let _ = ids;
2273 None
2274 }
2275 }
2276
2277 #[cfg(not(target_os = "macos"))]
2278 fn graph_prefill_preferred(&self) -> bool {
2279 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2287 if !graph_on || !crate::gpu::enabled_here() {
2288 return false;
2289 }
2290 if self.embryo_resident_eligible() {
2294 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2299 }
2300 if self.o1_active() {
2314 return false;
2315 }
2316 if self.wgpu_graph_attn_decline().is_some() {
2320 return false;
2321 }
2322 if self
2323 .weights
2324 .layers
2325 .iter()
2326 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2327 {
2328 return true;
2329 }
2330 self.weights
2341 .layers
2342 .iter()
2343 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2344 && self.automatic_gpu_prefix().is_none()
2345 }
2346
2347 #[cfg(target_os = "macos")]
2348 fn q1_graph_gpu(
2349 &mut self,
2350 start: usize,
2351 upto: Option<usize>,
2352 position: usize,
2353 h: &mut [f32],
2354 ) -> usize {
2355 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2357 const MOE_WORKING_SET_REASON: &str =
2358 "model weights exceed Metal working set for native MoE graph";
2359 if self
2363 .graph_declines
2364 .borrow()
2365 .iter()
2366 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2367 {
2368 return start;
2369 }
2370 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2371 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2373 || !crate::gpu::enabled_here()
2374 || !graph_force
2375 || std::env::var("CMF_GPU_BLOCK")
2376 .map(|v| v == "0")
2377 .unwrap_or(false)
2378 {
2379 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2380 eprintln!(
2381 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2382 self.attn_softcap > 0.0,
2383 crate::gpu::enabled_here(),
2384 graph_force,
2385 );
2386 }
2387 if self.graph_head_required {
2388 self.fail_metal_graph("native graph front gate refused");
2389 }
2390 return start;
2391 }
2392 let gemma4 = self.metal_gemma4();
2402 let swa_graph = self.metal_graph_swa() || gemma4;
2403 let graph_attn_reason = self.graph_attn_decline_reason();
2410 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2411 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2412 if (self.swa.is_some() && !swa_graph)
2413 || (self.global_attn.is_some() && !gemma4)
2414 || self.attention_heads_per_layer.is_some()
2415 || (self.attn_v_norm && !gemma4)
2416 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2419 || self.weights.layers.iter().any(|lw| {
2420 ((lw.attn_out_norm.is_some()
2421 || lw.ffn_out_norm.is_some()
2422 || lw.layer_scale.is_some())
2423 && !gemma4)
2424 || matches!(&lw.ffn, FfnKind::Dense(d)
2425 if !(matches!(d.act, Act::Silu | Act::Gelu)
2426 || (gemma4 && d.act == Act::GeluTanh)))
2427 })
2428 {
2429 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2432 self.note_graph_decline("metal block graph", reason);
2433 }
2434 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2435 eprintln!(
2436 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2437 self.swa.is_some(),
2438 self.global_attn.is_some(),
2439 self.attention_heads_per_layer.is_some(),
2440 self.attn_v_norm,
2441 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2442 );
2443 }
2444 if self.graph_head_required {
2445 self.fail_metal_graph("native graph architecture gate refused");
2446 }
2447 return start;
2448 }
2449 let limit = upto
2452 .map(|u| u + 1)
2453 .unwrap_or(self.num_layers)
2454 .min(self.num_layers);
2455
2456 enum Item<'a> {
2457 Gdn {
2458 run: Vec<GdnGpuLayer<'a>>,
2459 first: usize,
2460 },
2461 Attn {
2462 l: AttnGpuLayer<'a>,
2463 li: usize,
2464 q_norm: Option<&'a [f32]>,
2465 k_norm: Option<&'a [f32]>,
2466 output_gate: bool,
2467 proj_gate: Option<(&'a QTensor, bool)>,
2471 head_gate_w: Option<&'a [f32]>,
2474 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2475 full_gpu: bool,
2478 },
2479 }
2480
2481 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2488 let attend_contract = attend_mode != "0"
2489 && attend_mode != "off"
2490 && self.head_dim % 4 == 0
2491 && self.head_dim <= 256
2492 && (self.rotary_dim == 0
2496 || (self.rotary_dim >= 2
2497 && self.rotary_dim <= self.head_dim
2498 && (self.rotary_dim / 2) % 32 == 0))
2499 && self.num_kv_heads > 0
2500 && self.num_heads % self.num_kv_heads == 0;
2501
2502 let mut plan: Vec<Item> = Vec::new();
2503 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2504 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2506 let mut scan = start;
2507 while scan < limit {
2508 let lw = &self.weights.layers[self.phys_layer(scan)];
2509 let ffn = match &lw.ffn {
2510 FfnKind::Dense(d) if d.segs.is_empty() => {
2511 let (Some(g), Some(u), Some(dn)) = (
2512 d.gate_proj.metal_graph_parts(),
2513 d.up_proj.metal_graph_parts(),
2514 d.down_proj.metal_graph_parts(),
2515 ) else {
2516 if block_diag {
2517 eprintln!(
2518 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2519 );
2520 }
2521 break;
2522 };
2523 MetalFfn::Dense {
2524 gate: g,
2525 up: u,
2526 down: dn,
2527 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2530 }
2531 }
2532 FfnKind::Moe(m) => {
2533 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2539 {
2540 MetalFfn::FlintMoe(flint)
2541 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2542 MetalFfn::Moe(moe)
2543 } else {
2544 if block_diag {
2545 eprintln!(
2546 "block-graph: L{scan} MoE outside the graph contract — run ends"
2547 );
2548 }
2549 break;
2550 };
2551 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2552 model_ref.get_or_insert_with(|| model.clone());
2553 }
2554 ffn
2555 }
2556 _ => {
2557 if block_diag {
2558 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2559 }
2560 break;
2561 }
2562 };
2563 match &lw.attn {
2564 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2565 let parts = (
2566 w.in_proj_qkv.metal_graph_parts(),
2567 w.in_proj_z.metal_graph_parts(),
2568 w.in_proj_a.f32_parts(),
2569 w.in_proj_b.f32_parts(),
2570 w.out_proj.metal_graph_parts(),
2571 );
2572 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2573 if block_diag {
2574 eprintln!(
2575 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2576 w.in_proj_qkv.metal_graph_parts().is_some(),
2577 w.in_proj_z.metal_graph_parts().is_some(),
2578 w.in_proj_a.f32_parts().is_some(),
2579 w.in_proj_b.f32_parts().is_some(),
2580 w.out_proj.metal_graph_parts().is_some(),
2581 );
2582 }
2583 break;
2584 };
2585 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2586 model_ref.get_or_insert_with(|| model.clone());
2587 }
2588 let gl = GdnGpuLayer {
2589 attn_norm: &lw.input_norm,
2590 post_norm: &lw.post_norm,
2591 qkv,
2592 z,
2593 a,
2594 b,
2595 out,
2596 ffn,
2597 conv1d: &w.conv1d,
2598 a_log: &w.a_log,
2599 dt_bias: &w.dt_bias,
2600 gnorm: &w.norm,
2601 };
2602 match plan.last_mut() {
2603 Some(Item::Gdn { run, .. }) => run.push(gl),
2604 _ => plan.push(Item::Gdn {
2605 run: vec![gl],
2606 first: scan,
2607 }),
2608 }
2609 }
2610 AttnKind::Full {
2611 wq,
2612 wk,
2613 wv,
2614 wo,
2615 q_norm,
2616 k_norm,
2617 output_gate,
2618 softplus_gate,
2619 bias,
2620 } if (!self.kv_cache.layers[scan].o1_sealed()
2621 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2626 && softplus_gate
2630 .as_ref()
2631 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2632 {
2633 let parts = (
2634 wq.metal_graph_parts(),
2635 wk.metal_graph_parts(),
2636 wv.metal_graph_parts(),
2637 wo.metal_graph_parts(),
2638 );
2639 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2640 break;
2641 };
2642 if let QTensor::Mapped { model, .. } = wq {
2643 model_ref.get_or_insert_with(|| model.clone());
2644 }
2645 let cache = &self.kv_cache.layers[scan];
2646 let o1_metal = cache.o1.is_some()
2650 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2651 && cache.o1_views().is_some();
2652 let head_gate_w = softplus_gate
2656 .as_ref()
2657 .and_then(|(g, _)| g.f32_parts())
2658 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2659 .map(|(d, _, _)| d);
2660 let (nkv_g, hd_g) = if gemma4 {
2663 let (a, b, _) = self.layer_geom(scan);
2664 (a, b)
2665 } else {
2666 (self.num_kv_heads, self.head_dim)
2667 };
2668 let full_gpu = attend_contract
2669 && softplus_gate.is_none() == head_gate_w.is_none()
2670 && cache.mode == crate::kv_cache::KvMode::F32
2671 && (cache.o1.is_none() || o1_metal)
2672 && bias.is_none()
2673 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2674 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2675 && pk.1 == nkv_g * hd_g
2676 && pv.1 == nkv_g * hd_g
2677 && po.2 == self.num_heads * hd_g;
2678 plan.push(Item::Attn {
2679 l: AttnGpuLayer {
2680 attn_norm: &lw.input_norm,
2681 post_norm: &lw.post_norm,
2682 wq: pq,
2683 wk: pk,
2684 wv: pv,
2685 wo: po,
2686 ffn,
2687 attn_out_norm: lw.attn_out_norm.as_deref(),
2688 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2689 layer_scale: lw.layer_scale,
2690 },
2691 li: scan,
2692 q_norm: q_norm.as_deref(),
2693 k_norm: k_norm.as_deref(),
2694 output_gate: *output_gate,
2695 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2696 head_gate_w,
2697 bias: bias
2698 .as_ref()
2699 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2700 full_gpu,
2701 });
2702 }
2703 _ => break,
2704 }
2705 scan += 1;
2706 }
2707 let Some(model) = model_ref else {
2708 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2709 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2710 }
2711 if self.graph_head_required {
2712 self.fail_metal_graph("native graph has no mapped model reference");
2713 }
2714 return start;
2715 };
2716 if plan.is_empty() {
2717 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2718 eprintln!("q1-graph: empty plan at layer {start}");
2719 }
2720 if self.graph_head_required {
2721 self.fail_metal_graph("native graph plan is empty");
2722 }
2723 return start;
2724 }
2725 let has_moe = plan.iter().any(|it| match it {
2726 Item::Gdn { run, .. } => run
2727 .iter()
2728 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2729 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2730 });
2731 let has_flint_moe = plan.iter().any(|it| match it {
2732 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2733 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2734 });
2735 let metal_working_set = crate::gpu_metal::working_set_bytes();
2736 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2737 let model_bytes = model.primary_bytes().len() as u64;
2738 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2744 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2745 let pager_cap = if !has_moe {
2754 None
2755 } else if has_flint_moe {
2756 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2760 } else {
2761 crate::gpu_metal::moe_pager_cap_bytes()
2762 };
2763 let pager_eligible =
2764 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2765 let mut pager_span: Option<(usize, usize)> = None;
2766 if pager_eligible {
2767 let cap = pager_cap.expect("checked above");
2768 let tail_span = (self.graph_want_logits
2772 && upto.is_none()
2773 && scan == self.num_layers
2774 && std::env::var("CMF_GPU_LMHEAD")
2775 .map(|v| v != "0")
2776 .unwrap_or(true))
2777 .then(|| self.weights.lm_head.metal_graph_parts())
2778 .flatten()
2779 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2780 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2781 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2782 Some(match span {
2783 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2784 None => (lo, hi),
2785 })
2786 };
2787 let page_span = |span: (usize, usize), include_tail: bool| {
2788 let (mut lo, mut hi) = span;
2789 if include_tail {
2790 if let Some((tail_lo, tail_hi)) = tail_span {
2791 lo = lo.min(tail_lo);
2792 hi = hi.max(tail_hi);
2793 }
2794 }
2795 crate::gpu_metal::page_round_span(lo, hi)
2796 };
2797 let mut selected = 0usize;
2798 let mut selected_span: Option<(usize, usize)> = None;
2799 let mut gdn_prefix: Option<(usize, usize)> = None;
2807 'items: for (i, item) in plan.iter().enumerate() {
2808 match item {
2809 Item::Attn { li, .. } => {
2810 let Some(span) = add_layer_span(selected_span, *li) else {
2811 break;
2812 };
2813 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2814 break;
2815 };
2816 if page_hi - page_lo > cap {
2817 break;
2818 }
2819 selected = i + 1;
2820 selected_span = Some((page_lo, page_hi));
2821 }
2822 Item::Gdn { first, run } => {
2823 let mut whole = None;
2830 for take in 1..=run.len() {
2831 whole = add_layer_span(whole, *first + take - 1);
2832 if whole.is_none() {
2833 break;
2834 }
2835 }
2836 let whole_fits_empty_page = whole
2837 .and_then(|span| page_span(span, false))
2838 .is_some_and(|(lo, hi)| hi - lo <= cap);
2839 if whole_fits_empty_page {
2840 let mut span = selected_span;
2841 for take in 1..=run.len() {
2842 let Some(next) = add_layer_span(span, *first + take - 1) else {
2843 break 'items;
2844 };
2845 span = Some(next);
2846 }
2847 let Some(span) = span else {
2848 break;
2849 };
2850 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2851 else {
2852 break;
2853 };
2854 if page_hi - page_lo > cap {
2855 break;
2856 }
2857 selected = i + 1;
2858 selected_span = Some((page_lo, page_hi));
2859 continue;
2860 }
2861 if selected != 0 {
2862 break;
2863 }
2864 let mut span = None;
2865 let mut best: Option<(usize, (usize, usize))> = None;
2866 for take in 1..=run.len() {
2867 let Some(next) = add_layer_span(span, *first + take - 1) else {
2868 break;
2869 };
2870 let Some((page_lo, page_hi)) = page_span(next, false) else {
2873 break;
2874 };
2875 if page_hi - page_lo > cap {
2876 break;
2877 }
2878 span = Some((page_lo, page_hi));
2879 best = Some((take, (page_lo, page_hi)));
2880 }
2881 let Some((take, page)) = best else {
2882 break;
2883 };
2884 selected = i + 1;
2885 selected_span = Some(page);
2886 if take != run.len() {
2887 gdn_prefix = Some((i, take));
2888 break 'items;
2889 }
2890 }
2891 }
2892 }
2893 if selected != 0 {
2894 if let Some((item, take)) = gdn_prefix {
2895 match &mut plan[item] {
2896 Item::Gdn { run, .. } => run.truncate(take),
2897 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2898 }
2899 }
2900 let (page_lo, page_hi) = selected_span.expect("selected page span");
2901 tracing::info!(
2902 page_start = page_lo,
2903 page_end = page_hi,
2904 page_mb = (page_hi - page_lo) / (1024 * 1024),
2905 cap_mb = cap / (1024 * 1024),
2906 graph_items = selected,
2907 "native Metal MoE graph uses transient bounded weight page"
2908 );
2909 plan.truncate(selected);
2910 pager_span = Some((page_lo, page_hi));
2911 } else {
2912 tracing::warn!(
2913 cap_mb = cap / (1024 * 1024),
2914 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2915 );
2916 }
2917 }
2918 if has_moe && needs_bounded_pager && pager_span.is_none() {
2927 tracing::info!(
2928 model_bytes,
2929 metal_working_set,
2930 "native Metal MoE graph declined before allocation"
2931 );
2932 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2933 if self.graph_head_required {
2934 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2935 }
2936 return start;
2937 }
2938 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2939 let dev_attend = attend_contract
2940 && (self.head_dim <= 128
2941 || has_moe
2942 || (self.head_dim <= 256 && has_gdn)
2948 || (self.head_dim <= 256 && swa_graph)
2952 || attend_mode == "force"
2953 || attend_mode == "256");
2954 if !dev_attend {
2955 for it in &mut plan {
2956 if let Item::Attn { li, full_gpu, .. } = it {
2957 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2960 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2961 if !keep_o1 {
2962 *full_gpu = false;
2963 }
2964 }
2965 }
2966 }
2967 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2968 use std::sync::atomic::{AtomicBool, Ordering};
2969 static SAID: AtomicBool = AtomicBool::new(false);
2970 if !SAID.swap(true, Ordering::Relaxed) {
2971 let fg = plan
2972 .iter()
2973 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2974 .count();
2975 let att = plan
2976 .iter()
2977 .filter(|it| matches!(it, Item::Attn { .. }))
2978 .count();
2979 eprintln!(
2980 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2981 plan.len(),
2982 self.head_dim,
2983 self.rotary_dim,
2984 self.num_kv_heads,
2985 self.num_heads,
2986 );
2987 }
2988 }
2989 let dims = GraphDims {
2990 hidden: self.hidden_size,
2991 eps: self.rms_eps as f32,
2992 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2993 };
2994 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2998 crate::gpu_metal::MoePagerPageScope::new(
2999 _mt0,
3000 position,
3001 start,
3002 page_lo,
3003 page_hi,
3004 pager_cap.unwrap_or(0),
3008 )
3009 });
3010 let graph_new = match pager_span {
3011 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3012 None => TokenGraph::new(&model, dims, h),
3013 };
3014 let Some(mut graph) = graph_new else {
3015 if self.graph_head_required {
3016 self.fail_metal_graph("native TokenGraph allocation refused");
3017 }
3018 return start;
3019 };
3020 if swa_graph && self.head_dim > 128 {
3021 graph.set_attend_blk(1, 32);
3031 }
3032 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3033 nv: cfg.num_v_heads,
3034 nk: cfg.num_k_heads,
3035 dk: cfg.key_head_dim,
3036 dv: cfg.value_head_dim,
3037 kk: cfg.conv_kernel,
3038 hidden: self.hidden_size,
3039 inter: self.intermediate_size,
3040 c_dim: cfg.conv_dim(),
3041 eps: cfg.rms_eps as f32,
3042 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3043 });
3044 let mut valid = 0usize;
3048 let mut end = start;
3049 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3051 static ONCE: std::sync::Once = std::sync::Once::new();
3052 ONCE.call_once(|| {
3053 for it in &plan {
3054 match it {
3055 Item::Gdn { first, run } => {
3056 eprintln!("plan: Gdn first={first} len={}", run.len())
3057 }
3058 Item::Attn { li, full_gpu, .. } => {
3059 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3060 }
3061 }
3062 }
3063 });
3064 }
3065 for item in &plan {
3066 let ok = match item {
3067 Item::Gdn { run, .. } => gcfg
3068 .as_ref()
3069 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3070 .unwrap_or(false),
3071 Item::Attn { l, .. } => graph.attn_ok(l),
3072 };
3073 if !ok {
3074 if block_diag {
3075 eprintln!(
3076 "block-graph: plan item {} ({}) failed graph preflight",
3077 valid,
3078 match item {
3079 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3080 Item::Attn { li, .. } => format!("Attn L{li}"),
3081 }
3082 );
3083 }
3084 break;
3085 }
3086 valid += 1;
3087 end += match item {
3088 Item::Gdn { run, .. } => run.len(),
3089 Item::Attn { .. } => 1,
3090 };
3091 }
3092 plan.truncate(valid);
3093 if plan.is_empty() {
3094 if self.graph_head_required {
3095 self.fail_metal_graph("native graph preflight produced no valid items");
3096 }
3097 return start;
3098 }
3099
3100 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3101 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3102 return start;
3103 }
3104
3105 let one_pass = |t: (usize, usize, usize)| {
3114 use cortiq_core::TensorDtype as D;
3115 matches!(
3116 model.tensors[t.0].dtype,
3117 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3118 )
3119 };
3120 let dense_fast = plan.iter().all(|it| match it {
3121 Item::Attn {
3122 l, li, full_gpu, ..
3123 } => {
3124 *full_gpu
3125 && self.kv_cache.layers[*li].o1.is_none()
3126 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3127 && match l.ffn {
3128 MetalFfn::Dense { gate, up, down, .. } => {
3129 one_pass(gate) && one_pass(up) && one_pass(down)
3130 }
3131 _ => false,
3132 }
3133 }
3134 Item::Gdn { .. } => false,
3135 });
3136 let moe_plan = !dense_fast
3140 && plan.iter().all(|it| match it {
3141 Item::Attn {
3142 l, li, full_gpu, ..
3143 } => {
3144 *full_gpu
3145 && self.kv_cache.layers[*li].o1.is_none()
3146 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3147 }
3148 Item::Gdn { .. } => false,
3149 });
3150 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3151 if block_diag {
3152 use std::sync::atomic::{AtomicBool, Ordering};
3153 static SAID_AB: AtomicBool = AtomicBool::new(false);
3154 if !SAID_AB.swap(true, Ordering::Relaxed) {
3155 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3156 }
3157 }
3158 let _mv_fast = match ab {
3159 Some((bits, _)) => {
3160 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3161 crate::gpu_metal::MvFastGuard::set_raw(bits)
3162 }
3163 None if moe_plan => {
3164 graph.set_dense_concurrent(
3165 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3166 );
3167 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3168 }
3169 None => {
3170 graph.set_dense_concurrent(dense_fast);
3171 let q8r4 = if swa_graph {
3174 crate::gpu_metal::DENSE_Q8R4
3175 } else {
3176 0
3177 };
3178 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3179 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3180 } else {
3181 0
3182 })
3183 }
3184 };
3185
3186 let pool = self.pool.clone();
3189 let (nh, nkv, hd, hs, eps) = (
3190 self.num_heads,
3191 self.num_kv_heads,
3192 self.head_dim,
3193 self.hidden_size,
3194 self.rms_eps,
3195 );
3196 let norm_style = self.norm_style;
3197 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3198 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3199 let kv_id = self.graph_kv_id;
3200 let mut pending: Vec<(usize, usize)> = Vec::new();
3203 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3206 for item in &plan {
3207 let _xt0 = std::time::Instant::now();
3208 let _xkind: u32 = match item {
3209 Item::Gdn { .. } => 2,
3210 Item::Attn { .. } => 3,
3211 };
3212 if self.loop_final_norm {
3214 let item_start = match item {
3215 Item::Gdn { first, .. } => *first,
3216 Item::Attn { li, .. } => *li,
3217 };
3218 if item_start > start && self.is_loop_end(item_start - 1) {
3219 graph.encode_loop_norm(&self.weights.final_norm);
3220 }
3221 }
3222 match item {
3223 Item::Gdn { run, first } => {
3224 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3225 if l.linear_state.len() != want {
3226 l.linear_state = vec![0f32; want];
3227 }
3228 }
3229 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3230 .iter()
3231 .map(|l| l.linear_state.as_slice())
3232 .collect();
3233 let _ig = std::time::Instant::now();
3234 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3235 tracing::error!("q1 graph: GDN run refused after validation");
3237 return start;
3238 }
3239 graph.commit_kind = 2;
3242 graph.commit();
3243 crate::gpu::stageprof(0, _ig.elapsed());
3244 pending.push((*first, run.len()));
3245 }
3246 Item::Attn {
3247 l,
3248 li,
3249 q_norm,
3250 k_norm,
3251 output_gate,
3252 proj_gate,
3253 head_gate_w,
3254 bias,
3255 full_gpu,
3256 } => {
3257 let _ia = std::time::Instant::now();
3258 let inv_freq_l = self.layer_inv_freq(*li);
3264 let rd_l = self.layer_geom(*li).2;
3265 let window_l = self.layer_window(*li);
3266 let head_gate_w = *head_gate_w;
3267 let (nkv, hd) = if gemma4 {
3270 let (a, b, _) = self.layer_geom(*li);
3271 (a, b)
3272 } else {
3273 (nkv, hd)
3274 };
3275 if *full_gpu {
3277 let cache = &self.kv_cache.layers[*li];
3278 let o1p = if cache.o1.is_some() {
3279 match cache.o1_views() {
3280 Some(views) => Some(crate::gpu::O1AttnParams {
3281 views,
3282 epoch: self.o1_epoch,
3283 }),
3284 None => None,
3286 }
3287 } else {
3288 None
3289 };
3290 let o1_layer = cache.o1.is_some();
3291 if o1_layer && o1p.is_none() {
3292 }
3294 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3295 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3296 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3297 debug_assert!(
3300 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3301 );
3302 let p = crate::gpu::AttnDeviceParams {
3303 kv_id,
3304 layer: *li,
3305 nh,
3306 nkv,
3307 hd,
3308 rd: rd_l,
3309 position,
3310 scale: self.attn_scale,
3311 eps: eps as f32,
3312 gemma,
3313 late_qk_norm: self.qk_norm_after_rope,
3314 output_gate: *output_gate,
3315 q_norm: *q_norm,
3316 k_norm: *k_norm,
3317 inv_freq: &inv_freq_l,
3318 rope_scale: self.layer_rope_scale(*li),
3319 cpu_k,
3320 cpu_v,
3321 cpu_stored,
3322 cpu_gen: cache.generation(),
3323 o1: o1p,
3324 window: window_l,
3325 softmax_sink: cache.sinks.as_deref(),
3326 head_gate: head_gate_w,
3327 v_norm: self.attn_v_norm,
3328 };
3329 let o1_bad = o1_layer && p.o1.is_none();
3330 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3331 {
3332 if p.o1.is_none() {
3334 dev_attn.push((*li, nkv, hd));
3335 }
3336 graph.commit_kind = 3;
3337 graph.commit();
3338 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3342 continue;
3343 }
3344 }
3346 graph.encode_attn_prefix(l);
3347 if let Err(err) = graph.sync_checked() {
3348 self.fail_metal_graph(&err);
3349 return start;
3350 }
3351 if !pending.is_empty() {
3352 let idxs: Vec<usize> =
3353 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3354 let mut outs: Vec<&mut [f32]> = self
3355 .kv_cache
3356 .layers
3357 .iter_mut()
3358 .enumerate()
3359 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3360 .map(|(_, s)| s.linear_state.as_mut_slice())
3361 .collect();
3362 graph.read_states(&mut outs);
3363 }
3364 let mut q_raw = attention::take_buf(l.wq.1);
3365 let mut k = attention::take_buf(l.wk.1);
3366 let mut v = attention::take_buf(l.wv.1);
3367 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3368 let mut gate_raw = proj_gate.map(|(gp, _)| {
3371 let mut normed = attention::take_buf(hs);
3372 graph.read_normed(&mut normed);
3373 let mut raw = attention::take_buf(gp.rows());
3374 gp.matvec(&normed, &mut raw, pool.as_deref());
3375 attention::recycle_buf(&mut normed);
3376 raw
3377 });
3378 let cfg = QwenAttnCfg {
3379 num_heads: nh,
3380 num_kv_heads: nkv,
3381 head_dim: hd,
3382 hidden_size: hs,
3383 position,
3384 inv_freq: &inv_freq_l,
3385 rotary_dim: rd_l,
3386 scale: self.attn_scale,
3387 softcap: self.attn_softcap,
3388 window: window_l,
3389 v_norm: self.attn_v_norm,
3392 qk_norm_after_rope: self.qk_norm_after_rope,
3393 gate_sigmoid: self.proj_gate_sigmoid,
3394 q_norm: *q_norm,
3395 k_norm: *k_norm,
3396 output_gate: *output_gate,
3397 softplus_gate: None,
3398 rope_scale: self.layer_rope_scale(*li),
3399 bias: *bias,
3400 rms_eps: eps,
3401 norm_style,
3402 pool: pool.as_deref(),
3403 v_head_dim: hd,
3404 };
3405 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3408 || std::env::var("CMF_ATTN_DUMP").is_ok();
3409 let _ = full_gpu;
3410 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3411 let mut ao = attention::qwen_attention_core(
3412 q_raw,
3413 k,
3414 v,
3415 &mut self.kv_cache.layers[*li],
3416 &cfg,
3417 );
3418 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3422 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3423 let (cq, _cg, _ck, _cv) =
3424 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3425 let cache = &self.kv_cache.layers[*li];
3426 let n = cache.head_keys(0).len() / hd;
3427 let mut bytes: Vec<u8> = Vec::new();
3428 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3429 bytes.extend_from_slice(&v.to_le_bytes());
3430 }
3431 for v in &cq {
3432 bytes.extend_from_slice(&v.to_le_bytes());
3433 }
3434 for g in 0..nkv {
3435 for v in cache.head_keys(g) {
3436 bytes.extend_from_slice(&v.to_le_bytes());
3437 }
3438 }
3439 for g in 0..nkv {
3440 for v in cache.head_values(g) {
3441 bytes.extend_from_slice(&v.to_le_bytes());
3442 }
3443 }
3444 let _ =
3445 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3446 }
3447 }
3448 if let Some((qr0, k0, v0)) =
3449 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3450 {
3451 let (cq, _cg, ck, cv) =
3452 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3453 let mut h_now = vec![0f32; hs];
3454 graph.read_h(&mut h_now);
3455 let cache = &self.kv_cache.layers[*li];
3456 let n_after = cache.head_keys(0).len() / hd;
3457 let stored = n_after.saturating_sub(1);
3461 let cpu_k: Vec<&[f32]> = (0..nkv)
3462 .map(|g| &cache.head_keys(g)[..stored * hd])
3463 .collect();
3464 let cpu_v: Vec<&[f32]> = (0..nkv)
3465 .map(|g| &cache.head_values(g)[..stored * hd])
3466 .collect();
3467 let p = crate::gpu::AttnDeviceParams {
3468 kv_id,
3469 layer: *li,
3470 nh,
3471 nkv,
3472 hd,
3473 rd: rd_l,
3477 position,
3478 scale: self.attn_scale,
3479 eps: eps as f32,
3480 gemma,
3481 late_qk_norm: self.qk_norm_after_rope,
3482 output_gate: *output_gate,
3483 q_norm: *q_norm,
3484 k_norm: *k_norm,
3485 inv_freq: &inv_freq_l,
3486 rope_scale: self.layer_rope_scale(*li),
3487 cpu_k,
3488 cpu_v,
3489 cpu_stored: stored,
3490 cpu_gen: cache.generation(),
3491 o1: None,
3492 window: window_l,
3493 softmax_sink: cache.sinks.as_deref(),
3494 head_gate: None,
3495 v_norm: self.attn_v_norm,
3496 };
3497 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3498 let md = |a: &[f32], b: &[f32]| {
3499 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3500 };
3501 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3502 eprintln!(
3503 "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}",
3504 nn(&cq),
3505 md(&cq, &dq),
3506 nn(&ck),
3507 md(&ck, &dk),
3508 nn(&cv),
3509 md(&cv, &dv),
3510 nn(&ao),
3511 md(&ao, &dao)
3512 );
3513 } else {
3514 eprintln!("attn-oracle L{li}: device probe declined");
3515 }
3516 }
3517 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3518 attention::apply_projected_gate(
3521 &mut ao,
3522 raw,
3523 per_head,
3524 hd,
3525 self.proj_gate_sigmoid,
3526 );
3527 }
3528 if let Some(mut raw) = gate_raw.take() {
3529 attention::recycle_buf(&mut raw);
3530 }
3531 graph.encode_attn_suffix(l, &ao);
3532 graph.commit();
3535 attention::recycle_buf(&mut ao);
3536 }
3537 }
3538
3539 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3540 }
3541 let mut lm_rows = None;
3546 if self.graph_want_logits
3547 && upto.is_none()
3548 && end == self.num_layers
3549 && std::env::var("CMF_GPU_LMHEAD")
3550 .map(|v| v != "0")
3551 .unwrap_or(true)
3552 {
3553 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3554 if graph.lm_head_ok(lm) {
3555 graph.encode_lm_head(&self.weights.final_norm, lm);
3556 lm_rows = Some(lm.1);
3557 }
3558 }
3559 }
3560 if self.graph_head_required && lm_rows.is_none() {
3561 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3562 self.fail_metal_graph("fused graph head was requested but not encodable");
3563 return start;
3564 }
3565 let _sy0 = std::time::Instant::now();
3566 if let Err(err) = graph.sync_checked() {
3567 if let Some(profile) = pager_profile_page.as_mut() {
3568 profile.finish(start, graph.pager_page_stats(), "sync-error");
3569 }
3570 self.fail_metal_graph(&err);
3571 return start;
3572 }
3573 let _rs0 = std::time::Instant::now();
3574 if !pending.is_empty() {
3575 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3576 let mut outs: Vec<&mut [f32]> = self
3577 .kv_cache
3578 .layers
3579 .iter_mut()
3580 .enumerate()
3581 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3582 .map(|(_, s)| s.linear_state.as_mut_slice())
3583 .collect();
3584 graph.read_states(&mut outs);
3585 }
3586 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3587 use std::sync::atomic::{AtomicU64, Ordering};
3588 static SY: AtomicU64 = AtomicU64::new(0);
3589 static RS: AtomicU64 = AtomicU64::new(0);
3590 static N: AtomicU64 = AtomicU64::new(0);
3591 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3592 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3593 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3594 if n % 100 == 0 {
3595 eprintln!(
3596 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3597 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3598 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3599 );
3600 }
3601 }
3602 if let Some(rows) = lm_rows {
3603 crate::gpu::hostprof_encode_done(_mt0);
3604 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3605 graph.read_logits(&mut lg);
3606 crate::gpu::hostprof_total(_mt0);
3607 lg.resize(self.vocab_size, 0.0);
3608 if let Some(c) = self.final_softcap {
3609 softcap_in_place(&mut lg, c, self.pool.as_deref());
3610 }
3611 self.graph_logits = Some(lg);
3612 }
3613 graph.read_h(h);
3614 if self.graph_head_required && self.graph_logits.is_none() {
3615 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3616 self.fail_metal_graph("fused graph head completed without logits readback");
3617 return start;
3618 }
3619 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3620 METAL_GRAPH_LAYERS.fetch_add(
3621 end.saturating_sub(start) as u64,
3622 std::sync::atomic::Ordering::Relaxed,
3623 );
3624 if self.graph_head_required {
3625 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3626 }
3627 for (li, nkv, hd) in dev_attn {
3631 let mut krow = attention::take_buf(nkv * hd);
3632 let mut vrow = attention::take_buf(nkv * hd);
3633 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3634 let cache = &mut self.kv_cache.layers[li];
3635 cache.append(&krow, &vrow, &[]);
3636 let n = cache.seq_len;
3637 let mut imp = attention::take_buf(n);
3638 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3639 cache.accumulate_imp(&imp);
3640 attention::recycle_buf(&mut imp);
3641 }
3642 attention::recycle_buf(&mut krow);
3643 attention::recycle_buf(&mut vrow);
3644 }
3645 if let Some(profile) = pager_profile_page.as_mut() {
3646 profile.finish(end, graph.pager_page_stats(), "page-complete");
3647 }
3648 if let Some((_, arm)) = ab {
3649 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3650 }
3651 end
3652 }
3653
3654 pub fn new(
3655 tokenizer: Tokenizer,
3656 weights: PipelineWeights,
3657 hidden_size: usize,
3658 intermediate_size: usize,
3659 num_heads: usize,
3660 num_kv_heads: usize,
3661 head_dim: usize,
3662 num_layers: usize,
3663 physical_layers: usize,
3664 loop_final_norm: bool,
3665 vocab_size: usize,
3666 rms_eps: f64,
3667 rope_base: f32,
3668 norm_style: NormStyle,
3669 max_seq_len: usize,
3670 sampler_config: SamplerConfig,
3671 ) -> Self {
3672 let rng = match sampler_config.seed {
3673 Some(s) => SplitMix64::new(s),
3674 None => SplitMix64::from_entropy(),
3675 };
3676 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3677 let pool = Pool::from_env();
3678 if let Some(p) = &pool {
3679 tracing::info!("worker pool: {} threads", p.n_workers());
3680 if let Some(model) = weights
3682 .lm_head
3683 .model_arc()
3684 .or_else(|| weights.embed_tokens.model_arc())
3685 {
3686 let regions: Vec<&[u8]> =
3687 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3688 p.bind_numa(®ions);
3689 }
3690 }
3691 Self {
3692 gpu_plan: None,
3693 tokenizer: std::sync::Arc::new(tokenizer),
3694 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3695 sampler_config,
3696 weights,
3697 hidden_size,
3698 intermediate_size,
3699 num_heads,
3700 num_kv_heads,
3701 head_dim,
3702 num_layers,
3703 physical_layers,
3704 loop_final_norm,
3705 vocab_size,
3706 rms_eps,
3707 rope_base,
3708 norm_style,
3709 rotary_dim: head_dim,
3710 attention_heads_per_layer: None,
3711 kv_heads_per_layer: None,
3712 v_head_dim: None,
3713 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3714 .filter(|v| !v.is_empty())
3715 .map(std::path::PathBuf::from),
3716 graph_declines: std::cell::RefCell::new(Vec::new()),
3717 mimo_moe: Default::default(),
3718 minicpm_tail: Default::default(),
3719 vmf_cfg: None,
3720 gdn_cfg: None,
3721 kda_cfg: None,
3722 g3n: None,
3723 g4e: None,
3724 dsv4: None,
3725 dsv41: None,
3726 dsv41_vision: None,
3727 dsv41_prefill: None,
3728 qwen4_exp: None,
3729 clover: None,
3730 clover_stats: clover::CloverStats::default(),
3731 dsv4_mtp: Vec::new(),
3732 dspark: None,
3733 dspark_pending: Vec::new(),
3734 dspark_hist: Vec::new(),
3735 dspark_real: Vec::new(),
3736 dspark_trunk_picks: Vec::new(),
3737 dspark_exp: Vec::new(),
3738 dspark_draft_ns: 0,
3739 m3_winners: Vec::new(),
3740 m3_counts: Vec::new(),
3741 m3_n_exp: 0,
3742 m3_tokens: 0,
3743 m3_prewarmed: false,
3744 logit_multiplier: None,
3745 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3746 graph_failed: std::sync::atomic::AtomicBool::new(false),
3747 kv_history: Vec::new(),
3748 kv_history_device: false,
3749 short_conv_cfg: None,
3750 mtp: None,
3751 mimo_mtp: None,
3752 verify_exact_moe: false,
3753 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3754 ignore_eos: false,
3755 draft_full_streak: 0,
3756 spec_k_adapt: None,
3757 spec_acc_ewma: 0.7,
3758 rng,
3759 sampler_scratch: SamplerScratch::default(),
3760 spec_forced: None,
3761 spec_q: Vec::new(),
3762 spec_p: Vec::new(),
3763 spec_res: Vec::new(),
3764 spec_qs: Vec::new(),
3765 spec_ps: Vec::new(),
3766 spec_ress: Vec::new(),
3767 mtp_graph_mode: None,
3768 #[cfg(target_os = "macos")]
3769 metal_verify: None,
3770 inv_freq,
3771 ws: ForwardScratch::new(hidden_size),
3772 pool,
3773 model: None,
3774 dyn_force_f32: false,
3775 dyn_skill_layers: Vec::new(),
3776 dyn_active: None,
3777 dyn_blend_loaded: false,
3778 dyn_phi_layer: None,
3779 dyn_phi_ema: Vec::new(),
3780 dyn_phi_seen: 0,
3781 dyn_router: None,
3782 o1_cfg: None,
3783 o1_epoch: 0,
3784 o1_flags: Vec::new(),
3785 trace: false,
3786 calib_temp: 1.0,
3787 confidence_on: true,
3788 embed_multiplier: 1.0,
3789 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3790 swa: None,
3791 swa_trim: None,
3792 swa_trim_anchor: usize::MAX,
3793 chunk_min_rows: 32,
3794 sliding_layers: None,
3795 no_rope_layers: None,
3796 anchor_core: None,
3797 bounded_rope: None,
3798 kv_prefix: KvPrefix::default(),
3799 last_prefill_tokens: 0,
3800 inv_freq_local: None,
3801 rotary_dim_local: None,
3802 rope_scale: 1.0,
3803 rope_scale_local: 1.0,
3804 global_attn: None,
3805 inv_freq_global: None,
3806 attn_v_norm: false,
3807 qk_norm_after_rope: false,
3808 proj_gate_sigmoid: false,
3809 final_softcap: None,
3810 head_clusters: None,
3811 attn_softcap: 0.0,
3812 graph_want_logits: false,
3813 graph_head_required: false,
3814 graph_logits: None,
3815 embryo_graph: None,
3816 graph_refused: std::sync::atomic::AtomicBool::new(false),
3817 graph_kv_id: {
3818 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3819 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3820 },
3821 #[cfg(test)]
3822 nll_test_fail_at: None,
3823 #[cfg(test)]
3824 nll_test_force_serial: false,
3825 }
3826 }
3827
3828 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3836 if let Err(e) = self.try_set_o1(cfg) {
3837 tracing::error!("{e}");
3838 }
3839 }
3840
3841 pub fn bounded_native(&self) -> bool {
3845 self.anchor_core.is_some()
3846 }
3847
3848 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3851 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3852 }
3853
3854 pub fn o1_refusal(&self) -> Option<String> {
3856 self.anchor_core.as_ref().map(|ac| {
3857 format!(
3858 "--o1 / CMF_O1 refused: the anchor is native bounded \
3859 (anchor_core kind={} window={} sink={}); the file's operator \
3860 is executed as-is and no post-hoc Nyström overlay applies",
3861 ac.kind, ac.window, ac.sink
3862 )
3863 })
3864 }
3865
3866 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3868 if let Some(c) = &cfg {
3869 if let Some(why) = self.o1_refusal() {
3870 self.o1_flags = Vec::new();
3871 self.o1_cfg = None;
3872 return Err(why);
3873 }
3874 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3875 self.o1_flags.clear();
3876 self.o1_cfg = None;
3877 return Err(format!(
3878 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3879 c.w, c.sink
3880 ));
3881 }
3882 }
3883 self.o1_flags = match &cfg {
3884 Some(c) => {
3885 let mut flags = c.layer_flags(self.num_layers);
3886 for (li, f) in flags.iter_mut().enumerate() {
3887 if *f
3893 && (!matches!(
3894 self.weights.layers[self.phys_layer(li)].attn,
3895 AttnKind::Full { .. }
3896 ) || self.layer_window(li).is_some()
3897 || self.kv_cache.layers[li].sinks.is_some()
3898 || self.layer_v_dim(li) != self.layer_geom(li).1)
3899 {
3900 *f = false;
3901 }
3902 }
3903 flags
3904 }
3905 None => Vec::new(),
3906 };
3907 if let Some(c) = &cfg {
3908 let n = self.o1_flags.iter().filter(|&&f| f).count();
3909 tracing::info!(
3910 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3911 self.num_layers,
3912 c.m,
3913 c.w,
3914 c.sink,
3915 c.rect
3916 );
3917 }
3918 self.o1_cfg = cfg;
3919 Ok(())
3920 }
3921
3922 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3928 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3929 return Err(format!(
3930 "anchor_core kind '{}' is not executable by this runtime",
3931 cfg.kind
3932 ));
3933 }
3934 if cfg.window == 0 {
3935 return Err("anchor_core.window must be >= 1".into());
3936 }
3937 let mut n = 0usize;
3938 for li in 0..self.num_layers {
3939 let pl = self.phys_layer(li);
3940 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3941 if w.window != cfg.window || w.sink != cfg.sink {
3942 return Err(format!(
3943 "layer {li}: bounded weights (window {} sink {}) disagree with \
3944 anchor_core (window {} sink {})",
3945 w.window, w.sink, cfg.window, cfg.sink
3946 ));
3947 }
3948 self.kv_cache.layers[li].install_bounded(cfg.window);
3949 n += 1;
3950 }
3951 }
3952 if n == 0 {
3953 return Err("anchor_core is present but no layer executes it".into());
3954 }
3955 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3956 self.bounded_rope = Some(std::sync::Arc::new(rope));
3957 self.anchor_core = Some(cfg.clone());
3958 self.embryo_graph = None;
3959 tracing::info!(
3960 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3961 cfg.kind,
3962 cfg.window,
3963 cfg.sink,
3964 self.kv_cache
3965 .layers
3966 .iter()
3967 .map(|l| l.bounded_state_bytes())
3968 .max()
3969 .unwrap_or(0)
3970 );
3971 Ok(())
3972 }
3973
3974 pub fn install_wire_identity(&mut self, identity: u64) {
3978 for li in 0..self.kv_cache.layers.len() {
3979 let pl = self.phys_layer(li);
3980 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3981 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3982 Some(AttnKind::Linear(_))
3983 | Some(AttnKind::LinearGdn(_))
3984 | Some(AttnKind::ShortConv(_))
3985 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3986 _ => crate::kv_cache::WireKind::Full,
3987 };
3988 let l = &mut self.kv_cache.layers[li];
3989 l.wire_kind = kind;
3990 l.wire_identity = identity;
3991 l.wire_layer = li as u32;
3994 }
3995 }
3996
3997 pub fn clear_history(&mut self) {
3999 self.kv_history.clear();
4000 self.kv_history_device = false;
4001 self.kv_prefix.clear();
4002 }
4003
4004 pub fn graph_refused(&self) -> bool {
4008 self.graph_refused
4009 .load(std::sync::atomic::Ordering::Relaxed)
4010 }
4011
4012 pub fn mark_graph_refused(&self) {
4014 if !self
4015 .graph_refused
4016 .swap(true, std::sync::atomic::Ordering::Relaxed)
4017 {
4018 tracing::info!(
4019 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4020 self.graph_kv_id
4021 );
4022 }
4023 }
4024
4025 pub fn device_sequence_position(&self) -> Option<usize> {
4029 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4030 }
4031
4032 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4037 let dev = self.device_sequence_position();
4038 if recorded_on_device {
4039 dev == Some(n) && self.embryo_resident_wanted()
4040 } else {
4041 dev.is_none()
4042 }
4043 }
4044
4045 pub(crate) fn invalidate_for_weight_change(&mut self) {
4053 self.clear_sequence_state();
4054 self.embryo_graph = None;
4055 }
4056
4057 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4063 let (n, on_device) = if self.bounded_native() {
4064 (
4065 self.kv_prefix.extension(input_ids),
4066 self.kv_prefix.on_device(),
4067 )
4068 } else {
4069 let h = &self.kv_history;
4070 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4071 (h.len(), self.kv_history_device)
4072 } else if !h.is_empty() && !self.kv_history_device {
4073 (self.rollback_prefix_len(input_ids), false)
4074 } else {
4075 (0, false)
4076 }
4077 };
4078 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4081 tracing::warn!(
4082 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4083 the device now holds {:?} — re-prefilling from zero",
4084 if on_device { "resident device" } else { "host" },
4085 self.device_sequence_position()
4086 );
4087 return 0;
4088 }
4089 n
4090 }
4091
4092 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4103 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4104 || self.device_sequence_position().is_some()
4105 {
4106 return 0;
4107 }
4108 let h = &self.kv_history;
4109 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4110 let n = lcp.min(input_ids.len().saturating_sub(1));
4111 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4112 return 0;
4113 }
4114 let kv_id = self.graph_kv_id;
4115 for li in 0..self.num_layers {
4116 let l = &self.kv_cache.layers[li];
4117 let tail_ok = l.base() == 0
4118 || self
4119 .layer_window(li)
4120 .is_some_and(|w| l.base() + w <= n + 1);
4121 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4122 || l.o1.is_some()
4123 || l.bounded.is_some()
4124 || !l.linear_state.is_empty()
4125 || l.mode != crate::kv_cache::KvMode::F32
4126 || l.pos_len() != h.len()
4127 || !tail_ok
4128 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4129 || crate::gpu::graph_state_resident(kv_id, li)
4130 {
4131 return 0;
4132 }
4133 }
4134 n
4135 }
4136
4137 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4140 self.cached_prefix_len(input_ids)
4141 }
4142
4143 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4149 let on_device = self.device_sequence_position().is_some();
4150 if self.bounded_native() {
4151 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4152 let prev_device = self.kv_prefix.on_device();
4153 self.kv_history.clear();
4154 self.kv_history_device = false;
4155 if keep && prev_device == on_device {
4156 self.kv_prefix.extend(&consumed[reused..]);
4157 } else {
4158 self.kv_prefix.set(consumed);
4159 }
4160 self.kv_prefix.set_on_device(on_device);
4161 } else {
4162 self.kv_history = consumed.to_vec();
4163 self.kv_history_device = on_device;
4164 }
4165 }
4166
4167 pub fn o1_active(&self) -> bool {
4169 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4170 }
4171
4172 pub fn generation_batch_k(&self) -> usize {
4190 if let Some(k) = std::env::var("CMF_BATCH_K")
4191 .ok()
4192 .and_then(|v| v.parse::<usize>().ok())
4193 {
4194 return k;
4195 }
4196 if self.g4_batch_prefill() {
4199 return 128;
4200 }
4201 #[cfg(not(target_os = "macos"))]
4202 if self.graph_prefill_preferred() && !self.o1_active() {
4203 let moe = self
4206 .weights
4207 .layers
4208 .iter()
4209 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4210 let q82 = crate::gpu::moe_q82_graph_on()
4216 && self.weights.layers.iter().any(|lw| {
4217 matches!(&lw.ffn, FfnKind::Moe(m)
4218 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4219 });
4220 return if q82 {
4221 128
4222 } else if moe {
4223 32
4224 } else {
4225 128
4226 };
4227 }
4228 0
4229 }
4230
4231 pub fn generation_graph_prefill(&self) -> bool {
4232 let graph = self.graph_prefill_preferred();
4233 #[cfg(not(target_os = "macos"))]
4244 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4245 return false;
4246 }
4247 graph
4248 }
4249
4250 pub fn o1_device_stats(&self) -> (usize, u64) {
4255 crate::gpu::o1_device_stats(self.graph_kv_id)
4256 }
4257
4258 pub fn o1_begin(&mut self) {
4263 self.o1_begin_with_prefix(None);
4264 }
4265
4266 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4270 if let Some(c) = &self.o1_cfg {
4271 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4272 let boundary = requested_prefix.map(|p| {
4273 p.max(
4274 crate::nystrom::o1_deferred_boundary(w, sink)
4275 .expect("o1 config boundary validated in set_o1"),
4276 )
4277 });
4278 for (li, &f) in self.o1_flags.iter().enumerate() {
4279 if f {
4280 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4281 }
4282 }
4283 }
4284 }
4285
4286 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4288 self.o1_cfg.as_ref().and_then(|c| {
4289 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4290 .map(|floor| requested_prefix.max(floor))
4291 })
4292 }
4293
4294 fn o1_note_transition(&mut self) {
4295 let mut transitioned = false;
4299 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4300 if flagged {
4301 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4302 }
4303 }
4304 if transitioned {
4305 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4306 }
4307 }
4308
4309 fn o1_pending(&self) -> bool {
4310 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4311 f && self.kv_cache.layers[li].seq_len > 0
4312 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4313 })
4314 }
4315
4316 fn o1_fail(&mut self, err: String) {
4317 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4318 self.clear_sequence_state();
4319 self.graph_failed
4320 .store(true, std::sync::atomic::Ordering::Relaxed);
4321 self.cancel
4322 .store(true, std::sync::atomic::Ordering::Relaxed);
4323 }
4324
4325 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4330 if self.o1_cfg.is_none() {
4331 return Ok(false);
4332 }
4333 let mut participating = false;
4334 for li in 0..self.num_layers {
4335 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4336 continue;
4337 }
4338 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4339 return Err(err);
4340 }
4341 if self.kv_cache.layers[li].seq_len == 0 {
4342 continue;
4343 }
4344 participating = true;
4345 let num_heads = self.layer_num_heads(li);
4346 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4347 }
4348 self.o1_note_transition();
4349 for li in 0..self.num_layers {
4350 if self.o1_flags.get(li).copied().unwrap_or(false) {
4351 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4352 return Err(err);
4353 }
4354 }
4355 }
4356 Ok(participating
4357 && (0..self.num_layers).all(|li| {
4358 !self.o1_flags.get(li).copied().unwrap_or(false)
4359 || self.kv_cache.layers[li].seq_len == 0
4360 || self.kv_cache.layers[li].o1_sealed()
4361 }))
4362 }
4363
4364 fn o1_progress(&mut self) {
4367 if !self.o1_active() {
4368 return;
4369 }
4370 for li in 0..self.num_layers {
4371 if self.o1_flags.get(li).copied().unwrap_or(false) {
4372 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4373 self.o1_fail(err);
4374 return;
4375 }
4376 }
4377 }
4378 self.o1_note_transition();
4382 if !self.o1_pending() {
4383 return;
4384 }
4385 if let Err(err) = self.o1_seal_checked() {
4386 self.o1_fail(err);
4387 }
4388 }
4389
4390 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4395 if self
4396 .graph_failed
4397 .swap(false, std::sync::atomic::Ordering::Relaxed)
4398 {
4399 self.cancel
4400 .store(false, std::sync::atomic::Ordering::Relaxed);
4401 self.clear_sequence_state();
4402 return Err(format!("{phase}: deferred O(1) transition failed"));
4403 }
4404 Ok(())
4405 }
4406
4407 pub fn o1_seal(&mut self) {
4411 if let Err(err) = self.o1_seal_checked() {
4412 self.o1_fail(err);
4413 }
4414 }
4415
4416 pub fn set_trace(&mut self, on: bool) {
4418 self.trace = on;
4419 }
4420
4421 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4424 self.rng = match config.seed {
4425 Some(seed) => SplitMix64::new(seed),
4426 None => SplitMix64::from_entropy(),
4427 };
4428 self.sampler_config = config;
4429 }
4430
4431 pub fn set_confidence(&mut self, on: bool) {
4436 self.confidence_on = on;
4437 }
4438
4439 pub fn set_calib_temp(&mut self, t: f32) {
4442 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4443 }
4444
4445 pub fn calib_temp(&self) -> f32 {
4447 self.calib_temp
4448 }
4449
4450 pub fn rope_inv_freq(&self) -> &[f32] {
4454 self.inv_freq.as_slice()
4455 }
4456
4457 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4460 self.rotary_dim = rotary_dim.min(self.head_dim);
4461 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4462 self.embryo_graph = None;
4466 }
4467
4468 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4469 QwenAttnCfg {
4470 num_heads: self.num_heads,
4471 num_kv_heads: self.num_kv_heads,
4472 head_dim: self.head_dim,
4473 hidden_size: self.hidden_size,
4474 position,
4475 inv_freq: &self.inv_freq,
4476 rotary_dim: self.rotary_dim,
4477 scale: self.attn_scale,
4478 softcap: self.attn_softcap,
4479 window: None,
4480 v_norm: false,
4481 qk_norm_after_rope: self.qk_norm_after_rope,
4482 gate_sigmoid: self.proj_gate_sigmoid,
4483 q_norm: None,
4484 k_norm: None,
4485 output_gate: false,
4486 softplus_gate: None,
4487 rope_scale: self.rope_scale,
4488 bias: None,
4489 rms_eps: self.rms_eps,
4490 norm_style: self.norm_style,
4491 pool: self.pool.as_deref(),
4492 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4493 }
4494 }
4495
4496 pub fn generate(
4498 &mut self,
4499 prompt: &str,
4500 max_tokens: usize,
4501 task_mask: Option<&TaskMask>,
4502 on_token: Option<TokenCallback>,
4503 ) -> Result<GenerateResult, String> {
4504 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4505 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4506 }
4507
4508 pub fn generate_from_vl(
4511 &mut self,
4512 input: &crate::dsv41_vision::PreparedVlInputs,
4513 max_tokens: usize,
4514 task_mask: Option<&TaskMask>,
4515 on_token: Option<TokenCallback>,
4516 ) -> Result<GenerateResult, String> {
4517 let Some(dsv41) = &self.dsv41 else {
4518 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4519 };
4520 if input.token_ids.is_empty() {
4521 return Err("empty V4.1 multimodal prompt".into());
4522 }
4523 if input.token_types.len() != input.token_ids.len() {
4524 return Err(format!(
4525 "V4.1 token type count {} != token count {}",
4526 input.token_types.len(),
4527 input.token_ids.len()
4528 ));
4529 }
4530 let dim = dsv41.2.dim;
4531 let mut embeddings = vec![None; input.token_ids.len()];
4532 let mut participates = vec![true; input.token_ids.len()];
4533 if !input.images.is_empty() {
4534 let vision = self
4535 .dsv41_vision
4536 .as_ref()
4537 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4538 for image in &input.images {
4539 let end = image.start.saturating_add(image.types.len());
4540 if end > input.token_ids.len() {
4541 return Err(format!(
4542 "V4.1 image span {}..{} exceeds prompt length {}",
4543 image.start,
4544 end,
4545 input.token_ids.len()
4546 ));
4547 }
4548 let mut span = vec![0.0f32; image.types.len() * dim];
4549 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4550 for (offset, &kind) in image.types.iter().enumerate() {
4551 let pos = image.start + offset;
4552 if input.token_types[pos] != kind {
4553 return Err(format!(
4554 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4555 input.token_types[pos]
4556 ));
4557 }
4558 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4559 participates[pos] = false;
4560 }
4561 }
4562 }
4563 for (pos, &kind) in input.token_types.iter().enumerate() {
4564 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4565 return Err(format!("V4.1 text position {pos} has an image embedding"));
4566 }
4567 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4568 return Err(format!("V4.1 image position {pos} has no image embedding"));
4569 }
4570 }
4571 self.dsv41_prefill = Some((embeddings, participates));
4572 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4573 self.dsv41_prefill = None;
4574 result
4575 }
4576
4577 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4579 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4580 }
4581
4582 pub fn generate_from_ids(
4590 &mut self,
4591 input_ids: &[u32],
4592 max_tokens: usize,
4593 task_mask: Option<&TaskMask>,
4594 on_token: Option<TokenCallback>,
4595 ) -> Result<GenerateResult, String> {
4596 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4597 }
4598
4599 pub fn generate_from_embeds(
4605 &mut self,
4606 input_ids: &[u32],
4607 prompt_rows: &[f32],
4608 max_tokens: usize,
4609 task_mask: Option<&TaskMask>,
4610 on_token: Option<TokenCallback>,
4611 ) -> Result<GenerateResult, String> {
4612 if input_ids.is_empty()
4613 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4614 {
4615 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4616 }
4617 if prompt_rows.iter().any(|x| !x.is_finite()) {
4618 return Err("embedded prompt contains non-finite values".into());
4619 }
4620 if !self.can_prefill_batched()
4621 || self.dyn_router.is_some()
4622 || self.o1_active()
4623 || self.mtp.is_some()
4624 || self.gpu_plan.is_some()
4625 {
4626 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4627 }
4628 self.generate_with_prompt_rows(
4629 input_ids,
4630 Some(prompt_rows),
4631 max_tokens,
4632 task_mask,
4633 on_token,
4634 )
4635 }
4636
4637 fn generate_with_prompt_rows(
4638 &mut self,
4639 input_ids: &[u32],
4640 prompt_rows: Option<&[f32]>,
4641 max_tokens: usize,
4642 task_mask: Option<&TaskMask>,
4643 mut on_token: Option<TokenCallback>,
4644 ) -> Result<GenerateResult, String> {
4645 #[cfg(target_os = "macos")]
4646 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4647 if std::env::var("CMF_TRACE_H").is_ok() {
4648 eprintln!("input_ids: {input_ids:?}");
4649 }
4650 if input_ids.is_empty() {
4651 return Err("empty prompt: nothing to generate from".to_string());
4652 }
4653 self.graph_failed
4657 .store(false, std::sync::atomic::Ordering::Relaxed);
4658 let task_mask = self.drop_open_mask(task_mask);
4663 if self.clover.is_some() {
4666 if prompt_rows.is_some() || task_mask.is_some() {
4667 return Err("block-diffusion generation takes token prompts without task masks".into());
4668 }
4669 return self.clover_generate(input_ids, max_tokens, on_token);
4670 }
4671
4672 let mut reuse_from = {
4680 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4681 if on
4682 && prompt_rows.is_none()
4683 && task_mask.is_none()
4684 && self.mtp.is_none()
4685 && !(self.mimo_mtp.is_some() && self.speculative)
4686 && self.o1_cfg.is_none()
4687 && self.dsv41.is_none()
4688 {
4689 self.cached_prefix_len(input_ids)
4690 } else {
4691 0
4692 }
4693 };
4694 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4697 reuse_from = 0;
4698 }
4699 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4702 for l in &mut self.kv_cache.layers {
4703 let d = l.pos_len() - reuse_from;
4704 l.truncate_last(d);
4705 }
4706 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4707 eprintln!(
4708 "kv-reuse: rolled back {} answer position(s) to the common prefix",
4709 self.kv_history.len() - reuse_from
4710 );
4711 }
4712 self.kv_history.truncate(reuse_from);
4713 self.graph_logits = None;
4714 }
4715 self.swa_trim_anchor = input_ids.len();
4716 self.last_prefill_tokens = input_ids.len() - reuse_from;
4717 let bounded_native = self.bounded_native();
4718 if reuse_from == 0 {
4719 self.reset_fresh_mtp_draft_state();
4724 self.clear_sequence_state();
4725 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4726 eprintln!(
4727 "kv-reuse: {} of {} prompt positions already cached",
4728 reuse_from,
4729 input_ids.len()
4730 );
4731 }
4732 crate::gpu::graph_race_begin_generation();
4733 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4737 std::env::var("CMF_O1_PREFILL")
4738 .ok()
4739 .and_then(|v| v.parse::<usize>().ok())
4740 .filter(|&p| p > 0)
4741 } else {
4742 None
4743 };
4744 if task_mask.is_none() {
4745 self.o1_begin_with_prefix(o1_prefill);
4746 }
4747
4748 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4754 #[cfg(target_os = "macos")]
4792 let metal_graph = crate::gpu::q1_force()
4793 && crate::gpu::enabled_here()
4794 && std::env::var("CMF_GPU_BLOCK")
4795 .map(|v| v != "0")
4796 .unwrap_or(true);
4797 #[cfg(target_os = "macos")]
4803 let native_metal_backend = crate::gpu::q1_force();
4804 #[cfg(not(target_os = "macos"))]
4805 let metal_graph = false;
4806 #[cfg(not(target_os = "macos"))]
4807 let native_metal_backend = false;
4808 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4809 let spec_cheap_round =
4813 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4814 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4815 || match spec_sample_env.as_deref() {
4816 Some("1") => true,
4817 Some(_) => false,
4818 None => metal_graph && spec_cheap_round,
4819 };
4820 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4836 for lw in &self.weights.layers {
4837 if let FfnKind::Dense(d) = &lw.ffn {
4838 dense_n += 1;
4839 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4840 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4841 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4842 {
4843 dense_q4tp += 1;
4844 }
4845 }
4846 }
4847 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4848 let penalized = !metal_graph
4860 && (self.sampler_config.repetition_penalty != 1.0
4861 || self.sampler_config.presence_penalty != 0.0
4862 || !self.sampler_config.suppress_tokens.is_empty());
4863 #[cfg(feature = "gpu")]
4868 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4869 #[cfg(not(feature = "gpu"))]
4870 let metal_wgpu = false;
4871 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4872 let spec_wanted = graph_spec_wanted_by_policy(
4873 spec_env.as_deref(),
4874 spec_default_ok,
4875 penalized,
4876 metal_wgpu,
4877 native_metal_backend,
4878 );
4879 if spec_wanted && metal_wgpu {
4880 tracing::warn!(
4881 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4882 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4883 );
4884 }
4885 let graph_spec = self.speculative
4889 && (graph_on || metal_graph)
4890 && self.mtp.is_some()
4891 && task_mask.is_none()
4892 && !self.o1_active()
4893 && spec_sampling_ok
4894 && spec_wanted;
4895 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4908 let pair_pays = host_pair_opt_in
4909 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4910 #[cfg(target_os = "macos")]
4914 if metal_graph {
4915 static SAID: std::sync::Once = std::sync::Once::new();
4916 SAID.call_once(|| {
4917 let spec = if graph_spec {
4918 let k = std::env::var("CMF_GRAPH_SPEC_K")
4919 .ok()
4920 .and_then(|v| v.parse::<usize>().ok())
4921 .filter(|&v| (1..=8).contains(&v))
4922 .unwrap_or(7);
4923 let arm = if self.sampler_config.temperature < 1e-6 {
4924 "greedy"
4925 } else {
4926 "sampling"
4927 };
4928 format!(
4929 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4930 Self::draft_vocab_rows(usize::MAX)
4931 )
4932 } else if !self.speculative {
4933 "spec off (CMF_MTP=0)".to_string()
4934 } else if self.mtp.is_none() {
4935 "spec off (no MTP head)".to_string()
4936 } else if !spec_sampling_ok {
4937 if spec_cheap_round {
4938 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4939 } else {
4940 "spec off (sampling without a top-k: the dense chain \
4941 costs more than it saves)"
4942 .to_string()
4943 }
4944 } else if !spec_wanted {
4945 if spec_env.is_none() {
4946 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
4947 } else {
4948 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
4949 }
4950 } else if task_mask.is_some() {
4951 "spec off (task mask)".to_string()
4952 } else if !graph_spec && !pair_pays {
4953 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
4954 .to_string()
4955 } else {
4956 "spec off (O(1) attention)".to_string()
4957 };
4958 let on = |var: &str| {
4959 if std::env::var(var).as_deref() == Ok("0") {
4960 "off"
4961 } else {
4962 "on"
4963 }
4964 };
4965 tracing::info!(
4966 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4967 MTP graph {}, attend {}, probe {}",
4968 if crate::gpu_metal::state4_on() {
4969 "on"
4970 } else {
4971 "off"
4972 },
4973 if crate::gpu_metal::async_replay_on() {
4974 "on"
4975 } else {
4976 "off"
4977 },
4978 on("CMF_METAL_PREFILL"),
4979 on("CMF_MTP_GRAPH"),
4980 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4981 if crate::gpu::probe_enabled() {
4982 "bypassed (q1 force)"
4983 } else {
4984 "off"
4985 },
4986 );
4987 });
4988 }
4989 let spec_active = self.speculative
4990 && self.mtp.is_some()
4991 && task_mask.is_none()
4992 && !self.o1_active()
4993 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4994 let mut mtp = if spec_active { self.mtp.take() } else { None };
4997 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4998 eprintln!(
4999 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5000 mtp.is_some(),
5001 self.speculative,
5002 self.sampler_config.temperature < 1e-6,
5003 );
5004 }
5005 if let Some(m) = &mut mtp {
5006 m.kv.clear();
5007 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5009 self.mtp_graph_mode = None;
5010 }
5011 let mimo_spec = self.speculative
5015 && self.mimo_mtp.is_some()
5016 && task_mask.is_none()
5017 && !self.o1_active()
5018 && self.dyn_router.is_none()
5019 && self.sampler_config.temperature < 1e-6
5020 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5021 if let Some(st) = self.mimo_mtp.as_mut() {
5022 st.reset();
5023 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5024 Self::mimo_mtp_hist_cap(st, input_ids.len());
5025 }
5026 }
5027 let mut router = if mtp.is_none() {
5031 self.dyn_router.take()
5032 } else {
5033 None
5034 };
5035 let mut reuse_from = reuse_from;
5036 if let Some(r) = &mut router {
5037 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5040 let _ = self.set_active_skill(None);
5043 reuse_from = 0;
5044 self.last_prefill_tokens = input_ids.len();
5045 }
5046 }
5047
5048 let mut all_ids = input_ids.to_vec();
5049 let mut generated = 0usize;
5050 let mut finish_reason = "max_tokens".to_string();
5051 let mut drafted = 0usize;
5052 let mut accepted = 0usize;
5053 let mut dsv4_spec_bad = 0usize;
5060 let mut dsv4_spec_retry_at = 0usize;
5061 let mut confidence: Vec<f32> = Vec::new();
5062 let trace_on = self.trace;
5063 let calib_temp = self.calib_temp;
5064 let mut traces: Vec<TokenTrace> = Vec::new();
5065
5066 let mut hidden = vec![0.0f32; self.hidden_size];
5072 let mut pos = reuse_from;
5073 let fuse_lm = mtp.is_none()
5082 && router.is_none()
5083 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5084 self.graph_logits = None;
5085 self.graph_want_logits = false;
5086 let _tpf = std::time::Instant::now();
5087 let batch_k = self.generation_batch_k();
5088 if let Some(rows) = prompt_rows {
5089 let hs = self.hidden_size;
5090 let chunk = self.prefill_chunk().max(1);
5091 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5092 let end = (pos + chunk).min(input_ids.len());
5093 let hb = match self.prefill_input_rows(
5094 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5095 pos,
5096 task_mask,
5097 ) {
5098 Ok(hb) => hb,
5099 Err(err) => {
5100 self.finish_generation(&mut mtp, &mut router, true);
5101 return Err(err);
5102 }
5103 };
5104 if mimo_spec {
5105 self.mimo_note_rows(&hb, pos);
5106 }
5107 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5108 pos = end;
5109 }
5110 }
5111 while self.qwen4_exp.is_some()
5122 && mtp.is_none()
5123 && pos < input_ids.len()
5124 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5125 {
5126 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5129 let want_logits = end == input_ids.len();
5130 let mut lg = Vec::new();
5131 if let Some(b) = &mut self.qwen4_exp {
5132 crate::qwen4_exp::forward_tokens(
5133 &b.0,
5134 &b.1,
5135 &b.2,
5136 &mut b.3,
5137 &input_ids[pos..end],
5138 pos,
5139 &self.inv_freq,
5140 self.pool.as_deref(),
5141 &mut lg,
5142 want_logits,
5143 );
5144 }
5145 if want_logits {
5146 self.graph_logits = Some(lg);
5147 }
5148 pos = end;
5149 hidden.fill(0.0);
5150 }
5151 while self.dsv4.is_some()
5152 && mtp.is_none()
5153 && pos < input_ids.len()
5154 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5155 {
5156 let end = (pos + prefill_chunk()).min(input_ids.len());
5157 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5158 let mut lg = Vec::new();
5159 if let Some(b) = &mut self.dsv4 {
5160 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5161 crate::dsv4::forward_chunk(
5162 g,
5163 layers,
5164 &cfg,
5165 st,
5166 &ids,
5167 pos,
5168 &self.inv_freq,
5169 self.pool.as_deref(),
5170 &mut lg,
5171 end == input_ids.len(),
5172 );
5173 }
5174 if end == input_ids.len() {
5175 self.graph_logits = Some(lg);
5176 }
5177 pos = end;
5178 hidden = vec![0.0; self.hidden_size];
5179 }
5180 let dsv41_prefill = self.dsv41_prefill.take();
5181 while self.dsv41.is_some()
5182 && mtp.is_none()
5183 && pos < input_ids.len()
5184 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5185 {
5186 let end = (pos + prefill_chunk()).min(input_ids.len());
5187 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5188 let mut lg = Vec::new();
5189 if let Some(b) = &mut self.dsv41 {
5190 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5191 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5192 crate::dsv41::forward_chunk_masked_with_embeddings(
5193 g,
5194 layers,
5195 cfg,
5196 st,
5197 &ids,
5198 pos,
5199 &embeddings[pos..end],
5200 &participates[pos..end],
5201 self.pool.as_deref(),
5202 &mut lg,
5203 );
5204 } else {
5205 crate::dsv41::forward_chunk(
5206 g,
5207 layers,
5208 cfg,
5209 st,
5210 &ids,
5211 pos,
5212 self.pool.as_deref(),
5213 &mut lg,
5214 );
5215 }
5216 }
5217 if end == input_ids.len() {
5218 self.graph_logits = Some(lg);
5219 }
5220 pos = end;
5221 hidden = vec![0.0; self.hidden_size];
5222 }
5223 let dyn_prefill = router.is_some();
5228 let o1_prefill_limit = o1_prefill
5236 .and_then(|requested| self.o1_effective_boundary(requested))
5237 .map(|boundary| boundary.min(input_ids.len()));
5238 let mut o1_sealed = false;
5239 if let Some(limit) = o1_prefill_limit {
5240 if self.can_prefill_batched() && limit > 2 {
5243 let chunk = self.prefill_chunk();
5244 let hs = self.hidden_size;
5245 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5246 let end = (pos + chunk).min(limit);
5247 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5248 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5249 pos = end;
5250 }
5251 } else {
5252 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5253 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5254 pos += 1;
5255 }
5256 }
5257 if pos >= limit {
5258 o1_sealed = match self.o1_seal_checked() {
5259 Ok(sealed) => sealed,
5260 Err(err) => {
5261 self.finish_generation(&mut mtp, &mut router, true);
5262 return Err(err);
5263 }
5264 };
5265 tracing::info!(
5266 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5267 o1_prefill.unwrap_or(0),
5268 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5269 .unwrap_or(limit),
5270 limit,
5271 input_ids.len()
5272 );
5273 }
5274 }
5275 let graph_prefill = self.graph_prefill_preferred();
5281 #[cfg(target_os = "macos")]
5289 if task_mask.is_none()
5290 && !dyn_prefill
5291 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5292 && crate::gpu::enabled_here()
5293 && self.gdn_cfg.is_some()
5294 && !self.per_token_stack()
5295 && input_ids.len() > 8
5296 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5297 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5298 {
5299 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5300 .ok()
5301 .and_then(|v| v.parse().ok())
5302 .filter(|&v| (16..=512).contains(&v))
5303 .unwrap_or(256);
5304 let hs = self.hidden_size;
5305 let _tp = std::time::Instant::now();
5306 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5307 let end = (pos + chunk).min(input_ids.len());
5308 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5309 MetalPrefillOutcome::Completed(hb) => hb,
5310 MetalPrefillOutcome::Declined => break,
5311 MetalPrefillOutcome::Failed => {
5312 self.finish_generation(&mut mtp, &mut router, true);
5313 return Err("ordinary Metal prefill failed after admission".into());
5314 }
5315 };
5316 if let Some(m) = &mut mtp {
5317 let n_pairs = if end < input_ids.len() {
5318 end - pos
5319 } else {
5320 end - pos - 1
5321 };
5322 if n_pairs > 0 {
5323 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5324 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5325 .collect();
5326 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5327 for (j, (h, t)) in pairs.iter().enumerate() {
5328 let h = h.to_vec();
5329 let _ = self.mtp_step(m, &h, *t, pos + j);
5330 }
5331 }
5332 }
5333 }
5334 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5335 pos = end;
5336 }
5337 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5338 eprintln!(
5339 "metal-prefill: {} of {} tokens in {:.1} ms",
5340 pos,
5341 input_ids.len(),
5342 _tp.elapsed().as_secs_f64() * 1e3
5343 );
5344 }
5345 }
5346 #[cfg(not(target_os = "macos"))]
5351 if task_mask.is_none()
5352 && !dyn_prefill
5353 && mtp.is_none()
5354 && o1_prefill.is_none()
5355 && batch_k > 0
5356 && pos + 1 < input_ids.len()
5357 && self.g4_batch_prefill()
5358 {
5359 let t_g4 = std::time::Instant::now();
5360 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5361 Ok((n, last)) => {
5362 if n > 0 {
5363 hidden.copy_from_slice(&last);
5364 }
5365 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5366 eprintln!(
5367 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5368 input_ids.len() - pos,
5369 t_g4.elapsed().as_secs_f64() * 1e3
5370 );
5371 }
5372 pos += n;
5373 }
5374 Err(e) => {
5375 self.finish_generation(&mut mtp, &mut router, true);
5376 return Err(e);
5377 }
5378 }
5379 }
5380 self.mimo_moe_prepare();
5381 #[cfg(not(target_os = "macos"))]
5387 if task_mask.is_none()
5388 && !dyn_prefill
5389 && !graph_prefill
5390 && mtp.is_none()
5391 && o1_prefill.is_none()
5392 && !self.o1_active()
5393 && input_ids.len() > 2
5394 && self.batch_prefix_prefill()
5395 {
5396 let chunk = self.prefill_chunk().max(1);
5397 let hs = self.hidden_size;
5398 let t_bp = std::time::Instant::now();
5399 let pos0 = pos;
5400 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5401 let end = (pos + chunk).min(input_ids.len());
5402 let bk = end - pos;
5403 let mut hiddens = vec![0f32; bk * hs];
5404 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5405 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5406 }
5407 let positions: Vec<usize> = (pos..end).collect();
5408 let mut run = 0usize;
5409 let outcome = self.try_batch_graph_wgpu_prefix(
5410 &mut hiddens,
5411 &positions,
5412 bk,
5413 None,
5414 Some(&mut run),
5415 );
5416 match outcome {
5417 crate::gpu::BatchGraphOutcome::Completed => {
5418 let hb = if run < self.num_layers {
5419 self.prefill_batch_span(
5420 PrefillIn::Hidden(&hiddens),
5421 pos,
5422 None,
5423 run,
5424 self.num_layers,
5425 )
5426 } else {
5427 hiddens
5428 };
5429 if mimo_spec {
5430 self.mimo_note_rows(&hb, pos);
5431 }
5432 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5433 pos = end;
5434 }
5435 crate::gpu::BatchGraphOutcome::Failed => {
5436 self.finish_generation(&mut mtp, &mut router, true);
5437 return Err("batched prefix prefill failed after admission".into());
5438 }
5439 crate::gpu::BatchGraphOutcome::Declined => {
5440 #[cfg(feature = "gpu")]
5443 if pos > pos0 {
5444 self.pull_lagging_host_kv(0, self.num_layers, pos);
5445 }
5446 break;
5447 }
5448 }
5449 }
5450 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5451 eprintln!(
5452 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5453 pos - pos0,
5454 input_ids.len(),
5455 t_bp.elapsed().as_secs_f64() * 1e3
5456 );
5457 }
5458 }
5459 if task_mask.is_none()
5460 && !dyn_prefill
5461 && !graph_prefill
5462 && self.can_prefill_batched()
5463 && !self.per_token_stack()
5464 && o1_prefill.is_none()
5465 && input_ids.len() > 2
5466 {
5467 let chunk = self.prefill_chunk();
5473 let hs = self.hidden_size;
5474 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5475 let end = (pos + chunk).min(input_ids.len());
5476 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5477 if mimo_spec {
5478 self.mimo_note_rows(&hb, pos);
5479 }
5480 if let Some(m) = &mut mtp {
5481 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5482 .ok()
5483 .and_then(|v| v.parse().ok())
5484 .unwrap_or(0);
5485 for p in pos..end {
5486 if p + 1 < input_ids.len() {
5487 if probe >= 1 && p + 2 < input_ids.len() {
5488 let (d1, mut hx) = self.mtp_step_h(
5492 m,
5493 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5494 input_ids[p + 1],
5495 p,
5496 );
5497 let mut ok = d1 == input_ids[p + 2];
5498 Self::chain_probe_note(0, ok);
5499 let mut d_prev = d1;
5500 let mut extra = 0usize;
5501 for j in 1..probe {
5502 if p + 2 + j >= input_ids.len() {
5503 break;
5504 }
5505 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5506 extra += 1;
5507 ok = ok && dj == input_ids[p + 2 + j];
5508 Self::chain_probe_note(j, ok);
5509 d_prev = dj;
5510 hx = hj;
5511 }
5512 m.kv.truncate_last(extra);
5513 } else {
5514 let _ = self.mtp_step(
5515 m,
5516 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5517 input_ids[p + 1],
5518 p,
5519 );
5520 }
5521 }
5522 }
5523 }
5524 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5525 pos = end;
5526 }
5527 }
5528 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5529 if task_mask.is_none()
5530 && !dyn_prefill
5531 && !graph_prefill
5532 && !pair_off
5533 && self.pair_supported()
5534 && o1_prefill.is_none()
5535 {
5536 while pos + 1 < input_ids.len()
5537 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5538 {
5539 let e1 = self.embed_single(input_ids[pos]);
5540 let e2 = self.embed_single(input_ids[pos + 1]);
5541 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5542 if mimo_spec {
5543 self.mimo_note_rows(&h1, pos);
5544 self.mimo_note_rows(&h2, pos + 1);
5545 }
5546 self.commit_linear_scratch();
5548 if let Some(m) = &mut mtp {
5549 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5550 if pos + 2 < input_ids.len() {
5551 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5552 .ok()
5553 .and_then(|v| v.parse().ok())
5554 .unwrap_or(0);
5555 if probe >= 1 && pos + 3 < input_ids.len() {
5556 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5560 let mut ok = d1 == input_ids[pos + 3];
5561 Self::chain_probe_note(0, ok);
5562 let mut d_prev = d1;
5563 let mut extra = 0usize;
5564 for j in 1..probe {
5565 if pos + 3 + j >= input_ids.len() {
5566 break;
5567 }
5568 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5569 extra += 1;
5570 ok = ok && dj == input_ids[pos + 3 + j];
5571 Self::chain_probe_note(j, ok);
5572 d_prev = dj;
5573 hx = hj;
5574 }
5575 m.kv.truncate_last(extra);
5576 } else {
5577 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5578 }
5579 }
5580 }
5581 hidden = h2;
5582 pos += 2;
5583 }
5584 }
5585 let o1_batch_ready = o1_sealed
5598 && o1_prefill.is_some()
5599 && mtp.is_none()
5600 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5601 && (0..self.num_layers).all(|li| {
5602 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5603 cache.o1.is_none() || cache.o1_views().is_some()
5604 });
5605 let mtp_batch_prefill = mtp.is_some()
5610 && graph_prefill
5611 && task_mask.is_none()
5612 && !dyn_prefill
5613 && !self.o1_active()
5614 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5615 if batch_k > 0
5616 && (graph_prefill || o1_batch_ready)
5617 && task_mask.is_none()
5618 && (!self.o1_active() || o1_batch_ready)
5619 && (mtp.is_none() || mtp_batch_prefill)
5620 && !dyn_prefill
5621 && pos + 1 < input_ids.len()
5622 {
5623 let hs = self.hidden_size;
5624 let chunk = batch_k;
5625 while pos < input_ids.len() {
5626 let end = (pos + chunk).min(input_ids.len());
5627 let bk = end - pos;
5628 let mut hiddens = vec![0f32; bk * hs];
5629 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5630 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5631 }
5632 let positions: Vec<usize> = (pos..end).collect();
5633 let t_chunk = std::time::Instant::now();
5634 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5635 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5636 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5637 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5638 eprintln!(
5639 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5640 if o1_batch_ready {
5641 "o1"
5642 } else if mtp_batch_prefill {
5643 "ordinary_mtp"
5644 } else {
5645 "ordinary"
5646 },
5647 bk as f64 / (ms / 1000.0)
5648 );
5649 }
5650 {
5651 use std::sync::atomic::{AtomicBool, Ordering};
5652 static SAID: AtomicBool = AtomicBool::new(false);
5653 if !SAID.swap(true, Ordering::Relaxed) {
5654 if ok_b {
5655 tracing::info!(
5656 "batched prefill: ACTIVE mode={} (k={bk})",
5657 if o1_batch_ready {
5658 "o1"
5659 } else if mtp_batch_prefill {
5660 "ordinary_mtp"
5661 } else {
5662 "ordinary"
5663 }
5664 );
5665 } else {
5666 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5667 }
5668 }
5669 }
5670 if ok_b {
5671 if mimo_spec {
5672 self.mimo_note_rows(&hiddens, pos);
5673 }
5674 if mtp_batch_prefill {
5675 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5676 if n_pairs > 0 {
5677 let rows: Vec<Vec<f32>> = (0..n_pairs)
5683 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5684 .collect();
5685 let pairs: Vec<(&[f32], u32)> = rows
5686 .iter()
5687 .enumerate()
5688 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5689 .collect();
5690 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5691 eprintln!(
5692 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5693 pos,
5694 n_pairs,
5695 pos + n_pairs - 1,
5696 );
5697 }
5698 let warm_error = if let Some(m) = mtp.as_mut() {
5699 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5700 } else {
5701 None
5702 };
5703 if let Some(err) = warm_error {
5704 self.finish_generation(&mut mtp, &mut router, true);
5709 return Err(err.to_string());
5710 }
5711 }
5712 }
5713 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5714 pos = end;
5715 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5716 self.finish_generation(&mut mtp, &mut router, true);
5721 return Err(if o1_batch_ready {
5722 "sealed O(1) batch graph failed after admission".to_string()
5723 } else {
5724 "ordinary recurrent batch graph failed after admission".to_string()
5725 });
5726 } else {
5727 break; }
5729 }
5730 }
5731 if graph_prefill
5735 && task_mask.is_none()
5736 && mtp.is_none()
5737 && !dyn_prefill
5738 && pos == 0
5739 && input_ids.len() > 1
5740 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5741 {
5742 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5743 self.graph_logits = Some(lg);
5744 hidden = vec![0.0; self.hidden_size];
5745 pos = input_ids.len();
5746 }
5747 }
5748 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5749 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5750 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5751 if mimo_spec {
5752 self.mimo_note_rows(&hidden, pos);
5753 }
5754 if let Some(m) = &mut mtp {
5755 if pos + 1 < input_ids.len() {
5756 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5762 .ok()
5763 .and_then(|v| v.parse().ok())
5764 .unwrap_or(0);
5765 if probe >= 1 && pos + 2 < input_ids.len() {
5766 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5767 let mut ok = d1 == input_ids[pos + 2];
5768 Self::chain_probe_note(0, ok);
5769 let mut d_prev = d1;
5770 let mut extra = 0usize;
5771 for j in 1..probe {
5772 if pos + 2 + j >= input_ids.len() {
5773 break;
5774 }
5775 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5776 extra += 1;
5777 ok = ok && dj == input_ids[pos + 2 + j];
5778 Self::chain_probe_note(j, ok);
5779 d_prev = dj;
5780 hx = hj;
5781 }
5782 m.kv.truncate_last(extra);
5785 } else {
5786 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5787 }
5788 }
5789 }
5790 pos += 1;
5791 }
5792 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5793 eprintln!(
5794 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5795 input_ids.len(),
5796 _tpf.elapsed().as_secs_f64() * 1000.0
5797 );
5798 }
5799 if self
5800 .graph_failed
5801 .swap(false, std::sync::atomic::Ordering::Relaxed)
5802 {
5803 self.finish_generation(&mut mtp, &mut router, true);
5808 return Err("GPU token graph failed during prefill".to_string());
5809 }
5810 if self
5813 .cancel
5814 .swap(false, std::sync::atomic::Ordering::Relaxed)
5815 {
5816 self.finish_generation(&mut mtp, &mut router, true);
5820 return Ok(GenerateResult {
5821 text: String::new(),
5822 token_ids: Vec::new(),
5823 prompt_tokens: input_ids.len(),
5824 tokens_generated: 0,
5825 finish_reason: "cancelled".to_string(),
5826 mtp_drafted: 0,
5827 mtp_accepted: 0,
5828 token_confidence: Vec::new(),
5829 traces: Vec::new(),
5830 });
5831 }
5832
5833 if !o1_sealed {
5836 match self.o1_seal_checked() {
5837 Ok(_) => {}
5838 Err(err) => {
5839 self.finish_generation(&mut mtp, &mut router, true);
5840 return Err(err);
5841 }
5842 }
5843 }
5844
5845 macro_rules! commit {
5847 ($id:expr) => {{
5848 all_ids.push($id);
5849 generated += 1;
5850 self.note_draft_id($id);
5851 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5852 finish_reason = "stop".to_string();
5853 false
5854 } else {
5855 let token_text = self.tokenizer.decode_token($id);
5856 let mut go = true;
5857 if let Some(ref mut cb) = on_token {
5858 if !cb(&token_text) {
5859 finish_reason = "cancelled".to_string();
5860 go = false;
5861 }
5862 }
5863 go
5864 }
5865 }};
5866 }
5867
5868 let mut spec_trial = SpecTrial::Spec {
5879 t0: std::time::Instant::now(),
5880 gen0: generated,
5881 rounds: 0,
5882 };
5883 let mut spec_mon = SpecMon {
5889 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5890 ..SpecMon::default()
5891 };
5892 let mut spec_watchdog_off = false;
5893 let mut spec_walls: Vec<f32> = Vec::new();
5896 let mut spec_round_end: Option<std::time::Instant> = None;
5899 if mimo_spec {
5900 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5901 if let Some(mut st) = self.mimo_mtp.take() {
5902 self.mimo_mtp_probe(&mut st, input_ids, &path);
5903 self.mimo_mtp = Some(st);
5904 }
5905 }
5906 }
5907 let mut next_pos = input_ids.len();
5909 'decode: while generated < max_tokens {
5910 if self
5911 .graph_failed
5912 .swap(false, std::sync::atomic::Ordering::Relaxed)
5913 {
5914 self.finish_generation(&mut mtp, &mut router, true);
5919 return Err("GPU token graph failed during decode".to_string());
5920 }
5921 if self
5922 .cancel
5923 .swap(false, std::sync::atomic::Ordering::Relaxed)
5924 {
5925 finish_reason = "cancelled".to_string();
5926 break 'decode;
5927 }
5928 if mimo_spec && next_pos > 0 {
5933 self.mimo_note_rows(&hidden, next_pos - 1);
5936 }
5937 let forced = self.spec_forced.take();
5938 let mut logits = match (forced, self.graph_logits.take()) {
5939 (Some(_), _) => Vec::new(),
5940 (None, Some(lg)) => lg,
5941 (None, None) => {
5942 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5943 #[cfg(feature = "gpu")]
5948 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5949 lg
5950 } else {
5951 inference::rms_norm_into(
5952 &hidden,
5953 &self.weights.final_norm,
5954 self.rms_eps,
5955 self.norm_style,
5956 &mut self.ws.n1,
5957 );
5958 self.lm_head_forward(&self.ws.n1)
5959 };
5960 #[cfg(not(feature = "gpu"))]
5961 let logits_out = {
5962 inference::rms_norm_into(
5963 &hidden,
5964 &self.weights.final_norm,
5965 self.rms_eps,
5966 self.norm_style,
5967 &mut self.ws.n1,
5968 );
5969 self.lm_head_forward(&self.ws.n1)
5970 };
5971 logits_out
5972 }
5973 };
5974 if generated
5977 == std::env::var("CMF_LOGIT_DUMP_STEP")
5978 .ok()
5979 .and_then(|v| v.parse().ok())
5980 .unwrap_or(0)
5981 {
5982 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5983 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5984 for v in hidden.iter().chain(logits.iter()) {
5985 bytes.extend_from_slice(&v.to_le_bytes());
5986 }
5987 if let Err(e) = std::fs::write(&path, &bytes) {
5988 eprintln!("logit dump: failed to write {path}: {e}");
5989 self.finish_generation(&mut mtp, &mut router, true);
5990 return Err(format!("logit dump write failed: {e}"));
5991 }
5992 }
5993 }
5994 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5998 if !logits.is_empty() {
5999 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6000 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6001 if let Err(e) =
6002 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6003 {
6004 eprintln!("logit dump: failed to write {}: {e}", path.display());
6005 }
6006 }
6007 }
6008 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6009 Some(c) => c,
6010 None => {
6011 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6012 sampler::sample_with_scratch_pool(
6013 &logits,
6014 &self.sampler_config,
6015 self.sampler_config.penalty_past(&all_ids, bounded_native),
6016 &mut self.rng,
6017 &mut self.sampler_scratch,
6018 self.pool.as_deref(),
6019 )
6020 }
6021 };
6022 if self.confidence_on {
6023 confidence.push(if logits.is_empty() {
6024 0.0
6025 } else {
6026 sampler::top1_prob_pool(
6027 self.pool.as_deref(),
6028 &mut self.sampler_scratch,
6029 &logits,
6030 t_next,
6031 calib_temp,
6032 )
6033 });
6034 }
6035 if !logits.is_empty() {
6036 attention::recycle_buf(&mut logits);
6037 }
6038 if trace_on {
6039 let skill = router.as_ref().and_then(|r| r.active_id());
6043 traces.push(TokenTrace {
6044 t: generated,
6045 token_id: t_next,
6046 confidence: confidence.last().copied().unwrap_or(0.0),
6047 active_skill: skill,
6048 recon: None,
6049 switched: false,
6050 });
6051 }
6052 if !commit!(t_next) {
6053 break 'decode;
6054 }
6055 if generated >= max_tokens {
6056 break 'decode;
6057 }
6058
6059 self.swa_trim_tails();
6062 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6063 static SAID: std::sync::Once = std::sync::Once::new();
6069 SAID.call_once(|| {
6070 tracing::warn!(
6071 "KV cache full at {} positions — evicting half; quality \
6072 will degrade. Raise CMF_MAX_SEQ.",
6073 self.kv_cache.max_seq_len,
6074 );
6075 });
6076 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6077 self.kv_cache.evict(keep);
6078 }
6079
6080 if graph_spec {
6083 match spec_trial {
6084 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6085 spec_mon.plain_ms =
6086 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6087 let keep = spec_mon.pays();
6088 tracing::info!(
6089 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6090 spec_mon.tokens,
6091 spec_mon.round_ms,
6092 spec_mon.plain_ms,
6093 if keep { "speculating" } else { "plain" }
6094 );
6095 spec_mon.fails = 0;
6096 spec_trial = SpecTrial::Decided {
6097 spec: keep,
6098 recheck_at: if keep { usize::MAX } else { generated + 128 },
6099 };
6100 }
6101 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6102 spec_mon.n = 0;
6103 spec_mon.zero_accept_rounds = 0;
6104 spec_trial = SpecTrial::Spec {
6105 t0: std::time::Instant::now(),
6106 gen0: generated,
6107 rounds: 0,
6108 };
6109 }
6110 _ => {}
6111 }
6112 spec_watchdog_off = matches!(
6113 spec_trial,
6114 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6115 );
6116 }
6117 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6119 let budget = max_tokens - generated - 1;
6120 if let Some(mut st) = self.mimo_mtp.take() {
6121 let k = st.depth.min(budget);
6122 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6123 self.mimo_mtp = Some(st);
6124 let r = match r {
6125 Ok(r) => r,
6126 Err(err) => {
6127 self.finish_generation(&mut mtp, &mut router, true);
6128 return Err(err);
6129 }
6130 };
6131 if let Some(r) = r {
6132 drafted += r.drafted;
6133 accepted += r.accepted.len();
6134 let mut stopped = false;
6135 for &id in &r.accepted {
6136 if self.confidence_on {
6137 confidence.push(0.0);
6138 }
6139 if !commit!(id) {
6140 stopped = true;
6141 break;
6142 }
6143 }
6144 if stopped {
6145 break 'decode;
6146 }
6147 next_pos += r.accepted.len() + 1;
6148 hidden = r.hidden;
6149 self.graph_logits = Some(r.logits);
6152 continue 'decode;
6153 }
6154 }
6155 }
6156 #[cfg(feature = "gpu")]
6159 if self.speculative
6160 && self.qwen4_exp.is_some()
6161 && task_mask.is_none()
6162 && self.sampler_config.temperature < 1e-6
6163 && generated + 1 < max_tokens
6164 && next_pos > 0
6165 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6166 {
6167 let r = match &mut self.qwen4_exp {
6168 Some(b) => crate::qwen4_exp::spec_round(
6169 &b.0,
6170 &b.1,
6171 &b.2,
6172 &mut b.3,
6173 next_pos,
6174 &all_ids,
6175 &self.inv_freq,
6176 self.pool.as_deref(),
6177 ),
6178 None => None,
6179 };
6180 if let Some(r) = r {
6181 drafted += r.drafted;
6182 accepted += r.accepted.len();
6183 let mut stopped = false;
6184 for &id in &r.accepted {
6185 if self.confidence_on {
6186 confidence.push(0.0);
6187 }
6188 if !commit!(id) {
6189 stopped = true;
6190 break;
6191 }
6192 }
6193 if stopped {
6194 break 'decode;
6195 }
6196 next_pos += r.accepted.len() + 1;
6197 hidden.fill(0.0);
6198 self.graph_logits = Some(r.logits);
6199 continue 'decode;
6200 }
6201 }
6202 match &mut mtp {
6203 #[cfg(feature = "gpu")]
6205 Some(m)
6206 if graph_spec
6207 && !spec_watchdog_off
6208 && generated + 1 < max_tokens
6209 && next_pos > 0 =>
6210 {
6211 let t_round = std::time::Instant::now();
6212 if spec_time_level() >= 2 {
6213 if let Some(t) = spec_round_end.take() {
6214 eprintln!(
6215 "spec-gap {:.2} ms (host between rounds)",
6216 t.elapsed().as_secs_f64() * 1e3
6217 );
6218 }
6219 }
6220 spec_stamps_begin();
6221 #[cfg(target_os = "macos")]
6226 let allocs0 =
6227 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6228 #[cfg(not(target_os = "macos"))]
6229 let allocs0 = 0u64;
6230 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6231 m,
6232 &hidden,
6233 t_next,
6234 next_pos,
6235 &mut drafted,
6236 &mut accepted,
6237 &mut all_ids,
6238 max_tokens - generated,
6239 ) {
6240 next_pos = n_pos;
6241 hidden = new_h;
6242 let level = spec_time_level();
6243 if level > 0 {
6244 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6245 let stamps = spec_stamps_take();
6246 let median = if spec_walls.len() >= 3 {
6249 let mut s = spec_walls.clone();
6250 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6251 Some(s[s.len() / 2])
6252 } else {
6253 None
6254 };
6255 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6256 #[cfg(target_os = "macos")]
6257 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6258 .load(std::sync::atomic::Ordering::Relaxed)
6259 - allocs0;
6260 #[cfg(not(target_os = "macos"))]
6261 let allocs = allocs0;
6262 eprintln!(
6263 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6264 extra.len() + 1,
6265 if allocs > 0 {
6266 format!(" [{allocs} new device buffers]")
6267 } else {
6268 String::new()
6269 },
6270 match (outlier, median) {
6271 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6272 _ => String::new(),
6273 }
6274 );
6275 if level >= 2 || outlier {
6276 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6277 eprintln!(
6278 "spec-stamps: {}| untracked {:.1}",
6279 spec_stamps_format(&stamps),
6280 wall - sum
6281 );
6282 }
6283 if spec_mon.n >= 1 {
6284 spec_walls.push(wall);
6285 }
6286 }
6287 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6291 spec_trial = Self::spec_trial_round(
6294 spec_trial,
6295 &mut spec_mon,
6296 generated + extra.len() + 1,
6297 );
6298 let mut stopped = false;
6299 for &id in &extra {
6300 if self.confidence_on {
6301 confidence.push(0.0);
6302 }
6303 if trace_on {
6316 traces.push(TokenTrace {
6317 t: generated,
6318 token_id: id,
6319 confidence: confidence.last().copied().unwrap_or(0.0),
6320 active_skill: None,
6321 recon: None,
6322 switched: false,
6323 });
6324 }
6325 if !commit!(id) {
6326 stopped = true;
6327 break;
6328 }
6329 }
6330 if stopped {
6331 break 'decode;
6332 }
6333 if spec_time_level() >= 2 {
6334 spec_round_end = Some(std::time::Instant::now());
6335 }
6336 continue 'decode;
6337 }
6338 if self
6339 .graph_failed
6340 .swap(false, std::sync::atomic::Ordering::Relaxed)
6341 {
6342 self.finish_generation(&mut mtp, &mut router, true);
6348 return Err("GPU MTP graph failed during speculative decode".to_string());
6349 }
6350 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6361 spec_mon.tokens = 0.0;
6362 spec_mon.fails = 3;
6363 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6364 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6365 next_pos += 1;
6366 continue 'decode;
6367 }
6368 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6370 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6371 drafted += 1;
6372 let emb1 = self.embed_single(t_next);
6373 let emb2 = self.embed_single(draft);
6374 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6375
6376 inference::rms_norm_into(
6377 &h1,
6378 &self.weights.final_norm,
6379 self.rms_eps,
6380 self.norm_style,
6381 &mut self.ws.n1,
6382 );
6383 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6384 let t_after = sampler::sample_with_scratch_pool(
6385 &logits1,
6386 &self.sampler_config,
6387 self.sampler_config.penalty_past(&all_ids, bounded_native),
6388 &mut self.rng,
6389 &mut self.sampler_scratch,
6390 self.pool.as_deref(),
6391 );
6392 if self.confidence_on {
6393 confidence.push(sampler::top1_prob_pool(
6394 self.pool.as_deref(),
6395 &mut self.sampler_scratch,
6396 &logits1,
6397 t_after,
6398 calib_temp,
6399 ));
6400 }
6401 attention::recycle_buf(&mut logits1);
6402 if trace_on {
6403 traces.push(TokenTrace {
6406 t: generated,
6407 token_id: t_after,
6408 confidence: confidence.last().copied().unwrap_or(0.0),
6409 active_skill: None,
6410 recon: None,
6411 switched: false,
6412 });
6413 }
6414 let stop = !commit!(t_after);
6415
6416 if t_after == draft {
6417 accepted += 1;
6418 self.commit_linear_scratch();
6419 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6420 hidden = h2;
6421 next_pos += 2;
6422 } else {
6423 for layer in &mut self.kv_cache.layers {
6425 layer.truncate_last(1);
6426 }
6427 if !stop {
6428 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6429 hidden = self.forward_layers(
6430 &self.embed_single(t_after),
6431 next_pos + 1,
6432 None,
6433 );
6434 }
6435 next_pos += 2;
6436 }
6437 if stop {
6438 break 'decode;
6439 }
6440 }
6441 _ => {
6443 #[cfg(feature = "gpu")]
6448 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6449 static SAID: std::sync::Once = std::sync::Once::new();
6450 SAID.call_once(|| {
6451 eprintln!(
6452 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6453 !self.dsv4_mtp.is_empty(),
6454 task_mask.is_none(),
6455 router.is_none(),
6456 !trace_on,
6457 self.sampler_config.temperature < 1e-6,
6458 self.sampler_config.repetition_penalty == 1.0,
6459 );
6460 });
6461 }
6462 #[cfg(feature = "gpu")]
6463 if Self::dsv4_spec_on()
6464 && self.dsv4.is_some()
6465 && !self.dsv4_mtp.is_empty()
6466 && task_mask.is_none()
6467 && router.is_none()
6468 && !trace_on
6469 && self.sampler_config.temperature < 1e-6
6470 && self.sampler_config.repetition_penalty == 1.0
6471 && generated + 1 < max_tokens
6472 && all_ids.len() >= 2
6473 && generated >= dsv4_spec_retry_at
6474 {
6475 let tip_token = all_ids[all_ids.len() - 2];
6476 let drafted0 = drafted;
6477 let round = self.dsv4_spec_step(
6478 tip_token,
6479 t_next,
6480 next_pos,
6481 max_tokens.saturating_sub(generated),
6482 &mut drafted,
6483 &mut accepted,
6484 );
6485 if drafted > drafted0 {
6486 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6487 if useful {
6488 dsv4_spec_bad = 0;
6489 } else {
6490 dsv4_spec_bad += 1;
6491 if dsv4_spec_bad >= 2 {
6492 dsv4_spec_bad = 0;
6493 dsv4_spec_retry_at = generated.saturating_add(32);
6494 tracing::info!(
6495 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6496 );
6497 }
6498 }
6499 }
6500 if let Some((extra, n_pos)) = round {
6501 next_pos = n_pos;
6502 let mut stopped = false;
6503 for &id in &extra {
6504 if self.confidence_on {
6505 confidence.push(0.0);
6506 }
6507 if !commit!(id) {
6508 stopped = true;
6509 break;
6510 }
6511 }
6512 if stopped {
6513 break 'decode;
6514 }
6515 continue 'decode;
6516 }
6517 }
6518 self.graph_want_logits = fuse_lm;
6519 let mut t_fwd = t_next;
6525 let pure_greedy = self.sampler_config.temperature < 1e-6
6526 && self.sampler_config.repetition_penalty == 1.0
6527 && self.sampler_config.presence_penalty == 0.0
6528 && self.sampler_config.suppress_tokens.is_empty();
6529 let burst_k = std::env::var("CMF_MULTISTEP")
6534 .ok()
6535 .and_then(|v| v.parse::<usize>().ok())
6536 .unwrap_or(0);
6537 if pure_greedy
6538 && burst_k >= 1
6539 && fuse_lm
6540 && task_mask.is_none()
6541 && router.is_none()
6542 && !trace_on
6543 && !self.confidence_on
6544 && std::env::var_os("CMF_FORCE_IDS").is_none()
6549 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6550 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6551 {
6552 let mut stopped = false;
6553 loop {
6554 if self
6559 .cancel
6560 .swap(false, std::sync::atomic::Ordering::Relaxed)
6561 {
6562 finish_reason = "cancelled".to_string();
6563 stopped = true;
6564 break;
6565 }
6566 let room = max_tokens.saturating_sub(generated);
6567 if room <= 2 {
6568 break;
6569 }
6570 let k = burst_k.min(room - 1);
6571 if k < 1 {
6572 break;
6573 }
6574 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6575 if self
6576 .graph_failed
6577 .swap(false, std::sync::atomic::Ordering::Relaxed)
6578 {
6579 self.finish_generation(&mut mtp, &mut router, true);
6580 return Err(
6581 "GPU token graph failed during greedy burst".to_string()
6582 );
6583 }
6584 break;
6585 };
6586 next_pos += k;
6587 for &id in &ids {
6588 if !commit!(id) {
6589 stopped = true;
6590 break;
6591 }
6592 }
6593 if stopped {
6594 break;
6595 }
6596 t_fwd = *ids.last().unwrap();
6597 }
6598 if stopped {
6599 break 'decode;
6600 }
6601 }
6602 #[cfg(target_os = "macos")]
6612 if graph_spec
6613 && spec_watchdog_off
6614 && next_pos > 0
6615 && self.mtp_graph_mode == Some(true)
6616 && crate::gpu::q1_force()
6617 {
6618 if let Some(m) = mtp.as_mut() {
6619 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6620 }
6621 }
6622 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6623 next_pos += 1;
6624 if let Some(r) = &mut router {
6627 let phi = self.dyn_phi_ema.clone();
6628 let decision = r.step(&phi, generated);
6629 if let Some(new_active) = decision {
6630 let _ = self.set_active_skill(new_active);
6631 }
6632 if trace_on {
6635 if let Some(last) = traces.last_mut() {
6636 let e = r.last_best_e();
6637 last.recon = e.is_finite().then_some(e);
6638 last.switched = decision.is_some();
6639 }
6640 }
6641 }
6642 }
6643 }
6644 }
6645
6646 let cancelled = finish_reason == "cancelled";
6647 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6651 if mimo_spec {
6652 if let Some(st) = self.mimo_mtp.as_ref() {
6653 let line = st.stats.line();
6654 tracing::info!("{line}");
6655 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6656 eprintln!("{line}");
6657 }
6658 }
6659 }
6660 self.finish_generation(&mut mtp, &mut router, cancelled);
6661
6662 let output_ids = &all_ids[input_ids.len()..];
6663 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6667 let consumed = std::mem::take(&mut all_ids);
6672 if dyn_switched {
6673 self.clear_sequence_state();
6674 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6675 self.clear_history();
6676 } else {
6677 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6678 }
6679 all_ids = consumed;
6680 let output_ids = &all_ids[input_ids.len()..];
6681 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6683 Ok(GenerateResult {
6684 text: self.tokenizer.decode(output_ids),
6685 token_ids: output_ids.to_vec(),
6686 prompt_tokens: input_ids.len(),
6687 tokens_generated: generated,
6688 finish_reason,
6689 mtp_drafted: drafted,
6690 mtp_accepted: accepted,
6691 token_confidence: confidence,
6692 traces,
6693 })
6694 }
6695
6696 fn mtp_step(
6700 &mut self,
6701 m: &mut MtpModule,
6702 hidden: &[f32],
6703 next_token: u32,
6704 position: usize,
6705 ) -> u32 {
6706 self.mtp_step_h(m, hidden, next_token, position).0
6707 }
6708
6709 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6713 use std::sync::Mutex;
6714 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6715 let mut t = T.lock().unwrap();
6716 if t.len() <= depth {
6717 t.resize(depth + 1, (0, 0));
6718 }
6719 t[depth].0 += 1;
6720 t[depth].1 += prefix_ok as u64;
6721 if depth == 0 && t[0].0 % 128 == 0 {
6722 let line: Vec<String> = t
6723 .iter()
6724 .enumerate()
6725 .map(|(d, (n, k))| {
6726 format!(
6727 "d{}={:.0}%({n})",
6728 d + 1,
6729 100.0 * *k as f64 / (*n).max(1) as f64
6730 )
6731 })
6732 .collect();
6733 eprintln!("mtp-chain: {}", line.join(" "));
6734 }
6735 }
6736
6737 fn mtp_step_hl(
6745 &mut self,
6746 m: &mut MtpModule,
6747 hidden: &[f32],
6748 next_token: u32,
6749 position: usize,
6750 ) -> (Vec<f32>, Vec<f32>) {
6751 #[cfg(target_os = "macos")]
6756 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6757 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6758 self.mtp_graph_mode = Some(true);
6759 return r;
6760 }
6761 if self.mtp_graph_mode == Some(true) {
6762 tracing::error!("mtp Metal graph failed after admission");
6763 self.clear_sequence_state();
6764 self.graph_failed
6765 .store(true, std::sync::atomic::Ordering::Relaxed);
6766 self.cancel
6767 .store(true, std::sync::atomic::Ordering::Relaxed);
6768 return (Vec::new(), Vec::new());
6769 }
6770 self.mtp_graph_mode = Some(false);
6771 }
6772 #[cfg(feature = "gpu")]
6773 if self.mtp_graph_mode != Some(false) {
6774 if !self.mtp_graph_ok(m) {
6775 if self.mtp_graph_mode == Some(true) {
6776 tracing::error!("mtp graph became unavailable after admission");
6781 self.clear_sequence_state();
6782 self.graph_failed
6783 .store(true, std::sync::atomic::Ordering::Relaxed);
6784 self.cancel
6785 .store(true, std::sync::atomic::Ordering::Relaxed);
6786 return (Vec::new(), Vec::new());
6787 }
6788 self.mtp_graph_mode = Some(false);
6789 } else {
6790 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6791 self.mtp_graph_mode = Some(true);
6792 return r;
6793 }
6794 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6795 return (Vec::new(), Vec::new());
6802 }
6803 tracing::error!("mtp graph failed or declined after admission");
6807 self.clear_sequence_state();
6808 self.graph_failed
6809 .store(true, std::sync::atomic::Ordering::Relaxed);
6810 self.cancel
6811 .store(true, std::sync::atomic::Ordering::Relaxed);
6812 return (Vec::new(), Vec::new());
6813 }
6814 }
6815 let e = self.embed_single(next_token);
6819 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6820 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6821 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6822 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6823 let mut x = vec![0.0f32; self.hidden_size];
6824 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6825
6826 let lw = &m.layer;
6828 inference::rms_norm_into(
6829 &x,
6830 &lw.input_norm,
6831 self.rms_eps,
6832 self.norm_style,
6833 &mut self.ws.n1,
6834 );
6835 let attn = match &lw.attn {
6836 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6838 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6839 AttnKind::Full {
6840 wq,
6841 wk,
6842 wv,
6843 wo,
6844 q_norm,
6845 k_norm,
6846 output_gate,
6847 softplus_gate,
6848 bias,
6849 } => {
6850 let mut cfg = self.attn_cfg(position);
6851 cfg.q_norm = q_norm.as_deref();
6852 cfg.k_norm = k_norm.as_deref();
6853 cfg.output_gate = *output_gate;
6854 cfg.softplus_gate = softplus_gate
6855 .as_ref()
6856 .map(|(gate, per_head)| (gate, *per_head));
6857 cfg.bias = bias
6858 .as_ref()
6859 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6860 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6861 }
6862 AttnKind::Linear(_)
6863 | AttnKind::LinearGdn(_)
6864 | AttnKind::ShortConv(_)
6865 | AttnKind::Bounded(_) => {
6866 unreachable!("MTP block is full attention")
6867 }
6868 };
6869 for (i, &a) in attn.iter().enumerate() {
6870 x[i] += a;
6871 }
6872 inference::rms_norm_into(
6873 &x,
6874 &lw.post_norm,
6875 self.rms_eps,
6876 self.norm_style,
6877 &mut self.ws.p1,
6878 );
6879 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6880 for (i, &f) in ffn.iter().enumerate() {
6881 x[i] += f;
6882 }
6883
6884 inference::rms_norm_into(
6885 &x,
6886 &m.final_norm,
6887 self.rms_eps,
6888 self.norm_style,
6889 &mut self.ws.n1,
6890 );
6891 let lg = self.lm_head_forward(&self.ws.n1);
6892 (lg, x)
6893 }
6894
6895 fn mtp_step_h(
6897 &mut self,
6898 m: &mut MtpModule,
6899 hidden: &[f32],
6900 next_token: u32,
6901 position: usize,
6902 ) -> (u32, Vec<f32>) {
6903 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6904 let draft = sampler::argmax(&lg);
6905 attention::recycle_buf(&mut lg);
6906 (draft, x)
6907 }
6908
6909 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6915 match trial {
6916 SpecTrial::Spec { t0, gen0, rounds } => {
6917 let rounds = rounds + 1;
6918 if mon.repeated_zero_acceptance() {
6926 tracing::info!(
6927 "speculation trial: no accepted drafts in two rounds — timing plain"
6928 );
6929 return SpecTrial::Plain {
6930 t0: std::time::Instant::now(),
6931 gen0: generated,
6932 };
6933 }
6934 if rounds >= 5 {
6935 if mon.plain_ms > 0.0 {
6936 let keep = mon.pays();
6937 mon.fails = 0;
6938 tracing::info!(
6939 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6940 mon.tokens,
6941 mon.round_ms,
6942 mon.plain_ms,
6943 if keep { "speculating" } else { "plain" }
6944 );
6945 SpecTrial::Decided {
6946 spec: keep,
6947 recheck_at: if keep { usize::MAX } else { generated + 128 },
6948 }
6949 } else if mon.pays() {
6950 mon.fails = 0;
6955 tracing::info!(
6956 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6957 mon.tokens,
6958 mon.round_ms,
6959 );
6960 SpecTrial::Decided {
6961 spec: true,
6962 recheck_at: usize::MAX,
6963 }
6964 } else {
6965 SpecTrial::Plain {
6966 t0: std::time::Instant::now(),
6967 gen0: generated,
6968 }
6969 }
6970 } else {
6971 SpecTrial::Spec { t0, gen0, rounds }
6972 }
6973 }
6974 SpecTrial::Decided { spec: true, .. } => {
6975 if mon.pays() {
6976 mon.fails = 0;
6977 trial
6978 } else {
6979 mon.fails += 1;
6980 if mon.fails >= 4 {
6981 if mon.plain_ms <= 0.0 {
6982 tracing::info!(
6986 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6987 mon.tokens,
6988 mon.round_ms,
6989 );
6990 return SpecTrial::Plain {
6991 t0: std::time::Instant::now(),
6992 gen0: generated,
6993 };
6994 }
6995 tracing::info!(
6996 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6997 mon.tokens,
6998 mon.round_ms,
6999 mon.plain_ms
7000 );
7001 SpecTrial::Decided {
7002 spec: false,
7003 recheck_at: generated + 128,
7004 }
7005 } else {
7006 trial
7007 }
7008 }
7009 }
7010 other => other,
7011 }
7012 }
7013
7014 fn mtp_kv_id(&self) -> u64 {
7017 self.graph_kv_id | (1u64 << 40)
7018 }
7019
7020 const MTP_LAYER_BASE: usize = 0;
7025
7026 #[cfg(feature = "gpu")]
7033 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7034 self.mtp_graph_mode != Some(true)
7035 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7036 }
7037
7038 #[cfg(feature = "gpu")]
7044 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7045 let mut ok = true;
7046 let mut expected = false;
7047 for li in 0..self.num_layers {
7048 if matches!(
7049 self.weights.layers[self.phys_layer(li)].attn,
7050 AttnKind::Full { .. }
7051 ) {
7052 expected = true;
7053 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7054 }
7055 }
7056 !expected || ok
7057 }
7058
7059 fn graph_gdn_layer_count(&self) -> usize {
7063 (0..self.num_layers)
7064 .filter(|&li| {
7065 matches!(
7066 &self.weights.layers[self.phys_layer(li)].attn,
7067 AttnKind::LinearGdn(_)
7068 )
7069 })
7070 .count()
7071 }
7072
7073 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7076 let e = self.embed_single(next_token);
7077 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7078 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7079 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7080 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7081 let mut x = vec![0.0f32; self.hidden_size];
7082 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7083 x
7084 }
7085
7086 #[cfg(feature = "gpu")]
7089 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7090 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7091 return false;
7092 }
7093 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7094 || !crate::gpu::enabled_here()
7095 || self.attn_softcap > 0.0
7096 || self.attention_heads_per_layer.is_some()
7097 || self.v_head_dim.is_some()
7100 {
7101 return false;
7102 }
7103 matches!(
7104 &m.layer.attn,
7105 AttnKind::Full {
7106 softplus_gate: None,
7107 ..
7108 }
7109 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7110 }
7111
7112 #[cfg(feature = "gpu")]
7116 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7117 if !self.mtp_block_graph_ok(m) {
7118 return false;
7119 }
7120 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7121 return false;
7122 };
7123 let FfnKind::Dense(d) = &m.layer.ffn else {
7124 return false;
7125 };
7126 d.segs.is_empty()
7127 && wq.graph_weight().is_some()
7128 && wk.graph_weight().is_some()
7129 && wv.graph_weight().is_some()
7130 && wo.graph_weight().is_some()
7131 && d.gate_proj.graph_weight().is_some()
7132 && d.up_proj.graph_weight().is_some()
7133 && d.down_proj.graph_weight().is_some()
7134 && self.weights.lm_head.graph_weight().is_some()
7135 }
7136
7137 #[cfg(feature = "gpu")]
7143 fn mtp_step_graph(
7144 &mut self,
7145 m: &mut MtpModule,
7146 hidden: &[f32],
7147 next_token: u32,
7148 position: usize,
7149 ) -> Option<(Vec<f32>, Vec<f32>)> {
7150 if !self.mtp_graph_ok(m) {
7151 return None;
7152 }
7153 let lw = &m.layer;
7154 let AttnKind::Full {
7155 wq,
7156 wk,
7157 wv,
7158 wo,
7159 q_norm,
7160 k_norm,
7161 output_gate,
7162 softplus_gate,
7163 bias,
7164 } = &lw.attn
7165 else {
7166 return None;
7167 };
7168 if softplus_gate.is_some() {
7169 return None;
7170 }
7171 let FfnKind::Dense(d) = &lw.ffn else {
7172 return None;
7173 };
7174 if !d.segs.is_empty() {
7175 return None; }
7177 let mut x = self.mtp_block_input(m, hidden, next_token);
7180 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7181 let (_, i, kind, rs) = t.graph_weight()?;
7182 Some(crate::gpu::GraphW {
7183 idx: i,
7184 kind,
7185 row_scale: rs,
7186 data: &[],
7187 prism: crate::gpu::GraphPrismOp::None,
7188 affine: false,
7189 })
7190 }
7191 let (model, _, _, _) = wq.graph_weight()?;
7192 let model = model.clone();
7193 let (lm_gw, lm_rows) = {
7194 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7195 let rows = if kind == 6 {
7199 self.draft_head_rows(self.weights.lm_head.rows())
7200 } else {
7201 self.weights.lm_head.rows()
7202 };
7203 (
7204 crate::gpu::GraphW {
7205 idx: i,
7206 kind,
7207 row_scale: rs,
7208 data: &[],
7209 prism: crate::gpu::GraphPrismOp::None,
7210 affine: false,
7211 },
7212 rows,
7213 )
7214 };
7215 let layer = crate::gpu::GraphLayer {
7216 input_norm: &lw.input_norm,
7217 attn: crate::gpu::GraphAttn::Full {
7218 wq: gw(wq)?,
7219 wk: gw(wk)?,
7220 wv: gw(wv)?,
7221 wo: gw(wo)?,
7222 q_norm: q_norm.as_deref(),
7223 k_norm: k_norm.as_deref(),
7224 late_qk_norm: self.qk_norm_after_rope,
7225 bias: bias
7226 .as_ref()
7227 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7228 output_gate: *output_gate,
7229 cpu_k: m.kv.k_heads(),
7230 cpu_v: m.kv.v_heads(),
7231 cpu_base: m.kv.base(),
7232 geom: None,
7233 head_gate: None,
7234 },
7235 post_norm: &lw.post_norm,
7236 attn_out_norm: None,
7237 ffn_out_norm: None,
7238 layer_scale: None,
7239 ffn: crate::gpu::GraphFfn::Dense {
7240 gate: gw(&d.gate_proj)?,
7241 up: gw(&d.up_proj)?,
7242 down: gw(&d.down_proj)?,
7243 act: d.act.graph_act()?,
7244 },
7245 };
7246 let nh = self.num_heads;
7247 let (nkv, hd, rd) = self.layer_geom(0);
7248 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7249 let mut logits = Vec::new();
7250 let ok = crate::gpu::forward_token_graph(
7251 &model,
7252 self.mtp_kv_id(),
7253 std::slice::from_ref(&layer),
7254 &[None],
7255 self.o1_epoch,
7256 &self.inv_freq,
7257 &mut x,
7258 nh,
7259 nkv,
7260 hd,
7261 self.attn_scale,
7262 rd,
7263 self.hidden_size,
7264 d.gate_proj.rows(),
7268 position,
7269 self.kv_cache.max_seq_len,
7270 gemma,
7271 self.rms_eps as f32,
7272 Some((&lm_gw, lm_rows)),
7273 &m.final_norm,
7274 &mut logits,
7275 &[],
7276 1,
7277 None,
7278 None,
7279 None,
7280 Self::MTP_LAYER_BASE,
7281 true,
7282 );
7283 match ok {
7284 crate::gpu::TokenGraphOutcome::Completed => {}
7285 crate::gpu::TokenGraphOutcome::Declined => return None,
7286 crate::gpu::TokenGraphOutcome::Failed => {
7287 self.clear_sequence_state();
7291 self.graph_failed
7292 .store(true, std::sync::atomic::Ordering::Relaxed);
7293 self.cancel
7294 .store(true, std::sync::atomic::Ordering::Relaxed);
7295 return None;
7296 }
7297 }
7298 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7302 Some((logits, x))
7303 }
7304
7305 #[cfg(feature = "gpu")]
7313 fn mtp_warm_graph(
7314 &mut self,
7315 m: &mut MtpModule,
7316 pairs: &[(&[f32], u32)],
7317 first_pos: usize,
7318 ) -> crate::gpu::BatchGraphOutcome {
7319 if pairs.is_empty() {
7320 return crate::gpu::BatchGraphOutcome::Completed;
7321 }
7322 if !self.mtp_block_graph_ok(m) {
7323 return crate::gpu::BatchGraphOutcome::Declined;
7324 }
7325 let hs = self.hidden_size;
7326 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7329 for (h, t) in pairs {
7330 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7331 }
7332 let lw = &m.layer;
7333 let AttnKind::Full {
7334 wq,
7335 wk,
7336 wv,
7337 wo,
7338 q_norm,
7339 k_norm,
7340 output_gate,
7341 bias,
7342 ..
7343 } = &lw.attn
7344 else {
7345 return crate::gpu::BatchGraphOutcome::Declined;
7346 };
7347 let FfnKind::Dense(d) = &lw.ffn else {
7348 return crate::gpu::BatchGraphOutcome::Declined;
7349 };
7350 if !d.segs.is_empty() {
7351 return crate::gpu::BatchGraphOutcome::Declined; }
7353 let (
7356 AttnKind::Full {
7357 softplus_gate: None,
7358 ..
7359 },
7360 Some(gact),
7361 ) = (&lw.attn, d.act.graph_act())
7362 else {
7363 return crate::gpu::BatchGraphOutcome::Declined;
7364 };
7365 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7366 let (_, i, kind, rs) = t.graph_weight()?;
7367 Some(crate::gpu::GraphW {
7368 idx: i,
7369 kind,
7370 row_scale: rs,
7371 data: &[],
7372 prism: crate::gpu::GraphPrismOp::None,
7373 affine: false,
7374 })
7375 }
7376 let Some((model, _, _, _)) = wq.graph_weight() else {
7377 return crate::gpu::BatchGraphOutcome::Declined;
7378 };
7379 let model = model.clone();
7380 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7381 gw(wq),
7382 gw(wk),
7383 gw(wv),
7384 gw(wo),
7385 gw(&d.gate_proj),
7386 gw(&d.up_proj),
7387 gw(&d.down_proj),
7388 ) else {
7389 return crate::gpu::BatchGraphOutcome::Declined;
7390 };
7391 let layer = crate::gpu::GraphLayer {
7392 input_norm: &lw.input_norm,
7393 attn: crate::gpu::GraphAttn::Full {
7394 wq: gwq,
7395 wk: gwk,
7396 wv: gwv,
7397 wo: gwo,
7398 q_norm: q_norm.as_deref(),
7399 k_norm: k_norm.as_deref(),
7400 late_qk_norm: self.qk_norm_after_rope,
7401 bias: bias
7402 .as_ref()
7403 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7404 output_gate: *output_gate,
7405 cpu_k: m.kv.k_heads(),
7406 cpu_v: m.kv.v_heads(),
7407 cpu_base: m.kv.base(),
7408 geom: None,
7409 head_gate: None,
7410 },
7411 post_norm: &lw.post_norm,
7412 attn_out_norm: None,
7413 ffn_out_norm: None,
7414 layer_scale: None,
7415 ffn: crate::gpu::GraphFfn::Dense {
7416 gate: gg,
7417 up: gu,
7418 down: gd,
7419 act: gact,
7420 },
7421 };
7422 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7423 let nh = self.num_heads;
7424 let (nkv, hd, rd) = self.layer_geom(0);
7425 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7426 crate::gpu::forward_batch_graph(
7427 &model,
7428 self.mtp_kv_id(),
7429 std::slice::from_ref(&layer),
7430 &self.inv_freq,
7431 &mut hiddens,
7432 nh,
7433 nkv,
7434 hd,
7435 rd,
7436 hs,
7437 d.gate_proj.rows(),
7440 &positions,
7441 self.kv_cache.max_seq_len,
7442 gemma,
7443 self.rms_eps as f32,
7444 self.attn_scale,
7445 pairs.len(),
7446 &[],
7447 0,
7448 None,
7449 None,
7450 )
7451 }
7452
7453 #[cfg(feature = "gpu")]
7460 fn mtp_warm_graph_fallback(
7461 &mut self,
7462 m: &mut MtpModule,
7463 pairs: &[(&[f32], u32)],
7464 first_pos: usize,
7465 ) -> bool {
7466 if pairs.is_empty() {
7467 return true;
7468 }
7469 let graphable = self.mtp_block_graph_ok(m);
7470 if !graphable {
7471 if self.mtp_graph_mode == Some(true) {
7475 return false;
7476 }
7477 self.mtp_graph_mode = Some(false);
7478 for (j, (h, t)) in pairs.iter().enumerate() {
7479 self.mtp_warm(m, h, *t, first_pos + j);
7480 }
7481 return true;
7482 }
7483
7484 for (j, (h, t)) in pairs.iter().enumerate() {
7489 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7490 return false;
7491 }
7492 }
7493 self.mtp_graph_mode = Some(true);
7494 true
7495 }
7496
7497 #[cfg(feature = "gpu")]
7502 fn mtp_warm_prefill_pairs(
7503 &mut self,
7504 m: &mut MtpModule,
7505 pairs: &[(&[f32], u32)],
7506 first_pos: usize,
7507 ) -> Result<(), &'static str> {
7508 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7513 if self.mtp_graph_mode == Some(true) {
7514 return Err("MTP token graph became unavailable after admission");
7515 }
7516 self.mtp_graph_mode = Some(false);
7517 for (j, (h, t)) in pairs.iter().enumerate() {
7518 self.mtp_warm(m, h, *t, first_pos + j);
7519 }
7520 return Ok(());
7521 }
7522 match self.mtp_warm_graph(m, pairs, first_pos) {
7523 crate::gpu::BatchGraphOutcome::Completed => {
7524 if !pairs.is_empty() {
7525 self.mtp_graph_mode = Some(true);
7526 }
7527 Ok(())
7528 }
7529 crate::gpu::BatchGraphOutcome::Declined => {
7530 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7531 Ok(())
7532 } else {
7533 Err("MTP warm-up fallback failed after device admission")
7534 }
7535 }
7536 crate::gpu::BatchGraphOutcome::Failed => {
7537 Err("MTP warm batch graph failed after admission")
7538 }
7539 }
7540 }
7541
7542 #[cfg(not(feature = "gpu"))]
7543 fn mtp_warm_prefill_pairs(
7544 &mut self,
7545 m: &mut MtpModule,
7546 pairs: &[(&[f32], u32)],
7547 first_pos: usize,
7548 ) -> Result<(), &'static str> {
7549 for (j, (h, t)) in pairs.iter().enumerate() {
7550 self.mtp_warm(m, h, *t, first_pos + j);
7551 }
7552 Ok(())
7553 }
7554
7555 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7559 let e = self.embed_single(next_token);
7560 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7561 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7562 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7563 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7564 let mut x = vec![0.0f32; self.hidden_size];
7565 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7566 inference::rms_norm_into(
7567 &x,
7568 &m.layer.input_norm,
7569 self.rms_eps,
7570 self.norm_style,
7571 &mut self.ws.n1,
7572 );
7573 let attn = match &m.layer.attn {
7574 AttnKind::Full {
7575 wq,
7576 wk,
7577 wv,
7578 wo,
7579 q_norm,
7580 k_norm,
7581 output_gate,
7582 softplus_gate,
7583 bias,
7584 } => {
7585 let mut cfg = self.attn_cfg(position);
7586 cfg.q_norm = q_norm.as_deref();
7587 cfg.k_norm = k_norm.as_deref();
7588 cfg.output_gate = *output_gate;
7589 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7590 cfg.bias = bias
7591 .as_ref()
7592 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7593 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7594 }
7595 _ => return,
7596 };
7597 let _ = attn;
7598 }
7599
7600 #[cfg(feature = "gpu")]
7607 #[allow(clippy::too_many_arguments)]
7608 fn graph_spec_step(
7609 &mut self,
7610 m: &mut MtpModule,
7611 hidden: &[f32],
7612 t_next: u32,
7613 next_pos: usize,
7614 drafted: &mut usize,
7615 accepted: &mut usize,
7616 all_ids: &mut Vec<u32>,
7620 room: usize,
7625 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7626 #[cfg(target_os = "macos")]
7637 let metal_native = crate::gpu::q1_force();
7638 #[cfg(not(target_os = "macos"))]
7639 let metal_native = false;
7640 #[cfg(feature = "gpu")]
7641 let k_default = if metal_native {
7642 7
7645 } else if crate::gpu_wgpu::verify_i8_on() {
7646 5
7647 } else {
7648 4
7649 };
7650 #[cfg(not(feature = "gpu"))]
7651 let k_default = 4;
7652 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7653 .ok()
7654 .and_then(|v| v.parse().ok())
7655 .filter(|&v| (1..=8).contains(&v));
7656 let (k_start, k_max) = if metal_native {
7661 (7, 7)
7662 } else {
7663 (3, k_default.max(5))
7664 };
7665 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7666 let k_spec = k_full.min(room).max(1);
7667 let k_capped = k_spec < k_full;
7670 if next_pos == 0 {
7671 return None;
7672 }
7673 let t_round = std::time::Instant::now();
7674 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7690 let sub0 = subs();
7691 let cfg = self.sampler_config.clone();
7696 let penalized = !(cfg.repetition_penalty == 1.0
7697 && cfg.presence_penalty == 0.0
7698 && cfg.suppress_tokens.is_empty());
7699 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7704 let sampling = cfg.temperature >= 1e-6;
7705 let sparse = sampling && sampler::sparse_ok(&cfg);
7711 let base_len = all_ids.len();
7712 if sampling && !sparse && self.spec_q.len() < k_spec {
7713 self.spec_q.resize_with(k_spec, Vec::new);
7714 }
7715 if sparse && self.spec_qs.len() < k_spec {
7716 self.spec_qs.resize_with(k_spec, Vec::new);
7717 }
7718 let mut drafts = Vec::with_capacity(k_spec);
7723 let mut hx = hidden.to_vec();
7724 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7727 spec_stamp("pro");
7728 #[cfg(target_os = "macos")]
7734 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7735 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7736 Ok(ids) => {
7737 self.mtp_graph_mode = Some(true);
7738 drafts = ids;
7739 }
7740 Err(true) => {
7741 tracing::error!("mtp Metal draft chain failed after commit");
7742 self.clear_sequence_state();
7743 self.graph_failed
7744 .store(true, std::sync::atomic::Ordering::Relaxed);
7745 self.cancel
7746 .store(true, std::sync::atomic::Ordering::Relaxed);
7747 return None;
7748 }
7749 Err(false) => {}
7750 }
7751 }
7752 for j in drafts.len()..k_spec {
7753 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7754 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7755 if spec_dbg {
7756 let saved = self.mtp_graph_mode;
7757 self.mtp_graph_mode = Some(false);
7758 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7759 self.mtp_graph_mode = saved;
7760 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7761 return None;
7762 }
7763 m.kv.truncate_last(1);
7764 dbg_ref = Some(r);
7765 }
7766 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7767 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7768 return None;
7769 }
7770 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7771 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7772 let dl = lg
7773 .iter()
7774 .zip(&lg_cpu)
7775 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7776 let dh = hj
7777 .iter()
7778 .zip(&h_cpu)
7779 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7780 eprintln!(
7781 "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 {}",
7782 next_pos - 1 + j,
7783 sampler::argmax(&lg_cpu),
7784 sampler::argmax(&lg),
7785 n(&h_cpu),
7786 n(&hj),
7787 m.kv.seq_len
7788 );
7789 }
7790 let dj = if sparse {
7791 let mut q = std::mem::take(&mut self.spec_qs[j]);
7792 let ok = sampler::sparse_distribution_into(
7793 &lg,
7794 &cfg,
7795 all_ids,
7796 &mut self.sampler_scratch,
7797 self.pool.as_deref(),
7798 &mut q,
7799 );
7800 let d = if ok {
7801 sampler::draw_sparse(&q, &mut self.rng)
7802 } else {
7803 let t = sampler::argmax(&lg);
7805 q.clear();
7806 q.push((t, 1.0));
7807 t
7808 };
7809 self.spec_qs[j] = q;
7810 all_ids.push(d);
7811 d
7812 } else if sampling {
7813 let mut q = std::mem::take(&mut self.spec_q[j]);
7814 sampler::distribution_into(
7815 &lg,
7816 &cfg,
7817 all_ids,
7818 &mut self.sampler_scratch,
7819 self.pool.as_deref(),
7820 &mut q,
7821 );
7822 let d = sampler::draw(&q, &mut self.rng);
7823 self.spec_q[j] = q;
7824 all_ids.push(d); d
7826 } else if greedy_pen {
7827 let d = sampler::argmax_penalized(
7828 &lg,
7829 &cfg,
7830 all_ids,
7831 &mut self.sampler_scratch,
7832 self.pool.as_deref(),
7833 );
7834 all_ids.push(d);
7835 d
7836 } else {
7837 sampler::argmax(&lg)
7838 };
7839 attention::recycle_buf(&mut lg);
7840 drafts.push(dj);
7841 hx = hj;
7842 spec_stamp("d.pick");
7843 }
7844 all_ids.truncate(base_len);
7845 *drafted += k_spec;
7846 let t_draft = t_round.elapsed();
7847 let sub_draft = subs();
7848 let b = k_spec + 1;
7851 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7852 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7853 let e = self.embed_single(t);
7854 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7855 }
7856 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7857 spec_stamp("v.emb");
7858 let (lm_gw, lm_rows) = {
7859 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7860 (
7861 crate::gpu::GraphW {
7862 idx: i,
7863 kind,
7864 row_scale: rs,
7865 data: &[],
7866 prism: crate::gpu::GraphPrismOp::None,
7867 affine: false,
7868 },
7869 self.weights.lm_head.rows(),
7870 )
7871 };
7872 let mut logits = Vec::new();
7873 let final_norm = self.weights.final_norm.clone();
7874 #[cfg(target_os = "macos")]
7883 let greedy_dev = metal_native
7884 && !sampling
7885 && !greedy_pen
7886 && !self.confidence_on
7887 && self.final_softcap.is_none()
7888 && self.vocab_size == lm_rows
7894 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7895 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7896 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7897 #[cfg(not(target_os = "macos"))]
7898 let greedy_dev = false;
7899 let mut dev_ids: Vec<u32> = Vec::new();
7900 #[cfg(target_os = "macos")]
7901 let verify_outcome = if metal_native {
7902 let lm = self.weights.lm_head.q1_parts()?;
7903 let n_score = self.vocab_size.min(lm_rows);
7904 self.try_batch_graph_metal(
7905 &mut hiddens,
7906 &positions,
7907 b,
7908 Some((lm, &final_norm, &mut logits)),
7909 if greedy_dev {
7910 Some((n_score, &mut dev_ids))
7911 } else {
7912 None
7913 },
7914 )
7915 } else {
7916 self.try_batch_graph_wgpu(
7917 &mut hiddens,
7918 &positions,
7919 b,
7920 Some(crate::gpu::SpecTail {
7921 lm: lm_gw,
7922 lm_rows,
7923 final_norm: &final_norm,
7924 logits_out: &mut logits,
7925 pick: None,
7926 }),
7927 )
7928 };
7929 #[cfg(not(target_os = "macos"))]
7930 let verify_outcome = self.try_batch_graph_wgpu(
7931 &mut hiddens,
7932 &positions,
7933 b,
7934 Some(crate::gpu::SpecTail {
7935 lm: lm_gw,
7936 lm_rows,
7937 final_norm: &final_norm,
7938 logits_out: &mut logits,
7939 pick: None,
7940 }),
7941 );
7942 match verify_outcome {
7943 crate::gpu::BatchGraphOutcome::Completed => {}
7944 crate::gpu::BatchGraphOutcome::Declined => {
7945 m.kv.truncate_last(k_spec);
7949 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7950 self.clear_sequence_state();
7951 self.graph_failed
7952 .store(true, std::sync::atomic::Ordering::Relaxed);
7953 self.cancel
7954 .store(true, std::sync::atomic::Ordering::Relaxed);
7955 tracing::error!("MTP graph mirror rewind failed after verify decline");
7956 }
7957 return None;
7958 }
7959 crate::gpu::BatchGraphOutcome::Failed => {
7960 self.clear_sequence_state();
7964 self.graph_failed
7965 .store(true, std::sync::atomic::Ordering::Relaxed);
7966 self.cancel
7967 .store(true, std::sync::atomic::Ordering::Relaxed);
7968 tracing::error!("MTP verify batch graph failed after admission");
7969 return None;
7970 }
7971 }
7972 #[cfg(target_os = "macos")]
7978 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7979 let snap: Vec<Vec<f32>> = self
7980 .kv_cache
7981 .layers
7982 .iter()
7983 .map(|l| l.linear_state.clone())
7984 .collect();
7985 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7986 let toks: Vec<u32> = std::iter::once(t_next)
7987 .chain(drafts.iter().copied())
7988 .collect();
7989 let want_save = self.graph_want_logits;
7990 self.graph_want_logits = false;
7991 for (i, &t) in toks.iter().enumerate() {
7992 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7993 let _ = self.graph_logits.take();
7994 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7998 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7999 }
8000 let ref_lg = self.logits_from_hidden(&hi);
8001 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8002 let ra = sampler::argmax(&ref_lg);
8003 let va = sampler::argmax(row);
8004 let mut md = 0f32;
8005 let mut rms = 0f64;
8006 for j in 0..lm_rows.min(ref_lg.len()) {
8007 let d = (ref_lg[j] - row[j]).abs();
8008 md = md.max(d);
8009 rms += (d as f64) * (d as f64);
8010 }
8011 let mut hd = 0f32;
8012 for j in 0..self.hidden_size {
8013 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8014 }
8015 eprintln!(
8016 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8017 next_pos + i,
8018 if ra == va { "OK" } else { "MISMATCH" },
8019 (rms / lm_rows as f64).sqrt()
8020 );
8021 }
8022 self.graph_want_logits = want_save;
8023 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8026 if l.linear_state.len() == st.len() {
8027 l.linear_state.copy_from_slice(&st);
8028 } else {
8029 l.linear_state = st;
8030 }
8031 }
8032 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8033 let extra = l.seq_len.saturating_sub(n0);
8034 if extra > 0 {
8035 l.truncate_last(extra);
8036 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8037 }
8038 }
8039 }
8040 let t_verify = t_round.elapsed();
8041 let sub_verify = subs();
8042 let mut a = 0usize;
8047 let mut forced: Option<u32> = None;
8048 let ids: Vec<u32> = if sparse {
8049 let mut p = std::mem::take(&mut self.spec_ps);
8050 let mut res = std::mem::take(&mut self.spec_ress);
8051 while a < k_spec {
8052 let ok = sampler::sparse_distribution_into(
8053 &logits[a * lm_rows..(a + 1) * lm_rows],
8054 &cfg,
8055 all_ids,
8056 &mut self.sampler_scratch,
8057 self.pool.as_deref(),
8058 &mut p,
8059 );
8060 if !ok {
8061 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8062 p.clear();
8063 p.push((t, 1.0));
8064 }
8065 match sampler::spec_accept_or_correct_sparse(
8066 &p,
8067 &self.spec_qs[a],
8068 drafts[a],
8069 &mut self.rng,
8070 &mut res,
8071 ) {
8072 None => {
8073 all_ids.push(drafts[a]);
8074 a += 1;
8075 }
8076 Some(c) => {
8077 forced = Some(c);
8078 break;
8079 }
8080 }
8081 }
8082 all_ids.truncate(base_len);
8083 self.spec_ps = p;
8084 self.spec_ress = res;
8085 drafts.clone()
8086 } else if sampling {
8087 let mut p = std::mem::take(&mut self.spec_p);
8088 let mut res = std::mem::take(&mut self.spec_res);
8089 while a < k_spec {
8090 sampler::distribution_into(
8091 &logits[a * lm_rows..(a + 1) * lm_rows],
8092 &cfg,
8093 all_ids,
8094 &mut self.sampler_scratch,
8095 self.pool.as_deref(),
8096 &mut p,
8097 );
8098 match sampler::spec_accept_or_correct(
8099 &p,
8100 &self.spec_q[a],
8101 drafts[a],
8102 &mut self.rng,
8103 &mut res,
8104 self.pool.as_deref(),
8105 ) {
8106 None => {
8107 all_ids.push(drafts[a]);
8108 a += 1;
8109 }
8110 Some(c) => {
8111 forced = Some(c);
8112 break;
8113 }
8114 }
8115 }
8116 all_ids.truncate(base_len);
8117 self.spec_p = p;
8118 self.spec_res = res;
8119 drafts.clone()
8121 } else if greedy_pen {
8122 let mut ids: Vec<u32> = Vec::with_capacity(b);
8126 for i in 0..b {
8127 let t = sampler::argmax_penalized(
8128 &logits[i * lm_rows..(i + 1) * lm_rows],
8129 &cfg,
8130 all_ids,
8131 &mut self.sampler_scratch,
8132 self.pool.as_deref(),
8133 );
8134 ids.push(t);
8135 if i < k_spec && t == drafts[i] {
8136 all_ids.push(t);
8137 } else {
8138 break;
8139 }
8140 }
8141 all_ids.truncate(base_len);
8142 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8143 a += 1;
8144 }
8145 ids
8148 } else if greedy_dev && dev_ids.len() == b {
8149 let ids = std::mem::take(&mut dev_ids);
8150 while a < k_spec && ids[a] == drafts[a] {
8151 a += 1;
8152 }
8153 ids
8154 } else {
8155 if logits.len() < b * lm_rows {
8156 self.clear_sequence_state();
8159 self.graph_failed
8160 .store(true, std::sync::atomic::Ordering::Relaxed);
8161 self.cancel
8162 .store(true, std::sync::atomic::Ordering::Relaxed);
8163 tracing::error!("Metal verify returned neither logits nor argmax ids");
8164 return None;
8165 }
8166 let ids: Vec<u32> = (0..b)
8167 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8168 .collect();
8169 while a < k_spec && ids[a] == drafts[a] {
8170 a += 1;
8171 }
8172 ids
8173 };
8174 spec_stamp("acc");
8175 if spec_dbg {
8176 eprintln!(
8177 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8178 drafts, ids
8179 );
8180 }
8181 #[cfg(target_os = "macos")]
8185 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8186 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8187 {
8188 let snap: Vec<Vec<f32>> = self
8189 .kv_cache
8190 .layers
8191 .iter()
8192 .map(|l| l.linear_state.clone())
8193 .collect();
8194 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8195 let toks: Vec<u32> = std::iter::once(t_next)
8196 .chain(drafts.iter().copied())
8197 .collect();
8198 let want_save = self.graph_want_logits;
8199 self.graph_want_logits = false;
8200 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8201 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8202 let _ = self.graph_logits.take();
8203 }
8204 self.graph_want_logits = want_save;
8205 let plain_states: Vec<Vec<f32>> = self
8206 .kv_cache
8207 .layers
8208 .iter()
8209 .map(|l| l.linear_state.clone())
8210 .collect();
8211 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8212 let mut rows = Vec::new();
8213 for (li, (l, n0)) in self
8214 .kv_cache
8215 .layers
8216 .iter_mut()
8217 .zip(attn_lens.iter())
8218 .enumerate()
8219 {
8220 let extra = l.seq_len.saturating_sub(*n0);
8221 if extra > 0 {
8222 let mut kk = Vec::new();
8223 let mut vv = Vec::new();
8224 for g in 0..nkv {
8225 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8226 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8227 }
8228 rows.push((li, kk, vv));
8229 l.truncate_last(extra);
8230 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8231 }
8232 }
8233 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8234 if l.linear_state.len() == st.len() {
8235 l.linear_state.copy_from_slice(&st);
8236 } else {
8237 l.linear_state = st;
8238 }
8239 }
8240 Some((plain_states, rows))
8241 } else {
8242 None
8243 };
8244 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8245 #[cfg(target_os = "macos")]
8254 let mut warm_pending: Option<MetalWarmPending> = None;
8255 #[cfg(target_os = "macos")]
8256 if metal_native {
8257 m.kv.truncate_last(k_spec.saturating_sub(1));
8258 if self.mtp_graph_mode == Some(true) {
8259 crate::gpu_metal::kv_mirror_set_stored(
8262 self.mtp_kv_id(),
8263 Self::MTP_LAYER_BASE,
8264 m.kv.seq_len,
8265 );
8266 if !warm_off && a > 0 {
8267 let pairs: Vec<(&[f32], u32)> = (0..a)
8268 .map(|j| {
8269 (
8270 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8271 ids[j],
8272 )
8273 })
8274 .collect();
8275 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8276 }
8277 }
8278 spec_stamp("c.wsub");
8279 }
8280 #[cfg(target_os = "macos")]
8282 if metal_native {
8283 if !self.metal_verify_commit(a) {
8286 self.clear_sequence_state();
8287 self.graph_failed
8288 .store(true, std::sync::atomic::Ordering::Relaxed);
8289 self.cancel
8290 .store(true, std::sync::atomic::Ordering::Relaxed);
8291 tracing::error!("Metal verify state/KV handoff failed after admission");
8292 return None;
8293 }
8294 if let Some((plain_states, rows)) = commit_ref {
8295 crate::gpu_metal::queue_fence();
8296 let _ = crate::gpu_metal::wait_replay();
8299 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8300 let mut worst_s = 0f32;
8301 let mut worst_li = 0usize;
8302 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8303 if l.linear_state.len() != ps.len() || ps.is_empty() {
8304 continue;
8305 }
8306 let d = l
8307 .linear_state
8308 .iter()
8309 .zip(ps)
8310 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8311 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8312 let rel = d / n.max(1e-6);
8313 if rel > worst_s {
8314 worst_s = rel;
8315 worst_li = li;
8316 }
8317 }
8318 let mut worst_k = 0f32;
8319 for (li, kk, vv) in &rows {
8320 let l = &self.kv_cache.layers[*li];
8321 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8322 let mut ck = Vec::new();
8323 let mut cv = Vec::new();
8324 for g in 0..nkv {
8325 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8326 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8327 }
8328 if ck.len() == kk.len() {
8329 let dk = ck
8330 .iter()
8331 .zip(kk)
8332 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8333 let dv = cv
8334 .iter()
8335 .zip(vv)
8336 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8337 worst_k = worst_k.max(dk).max(dv);
8338 } else {
8339 eprintln!(
8340 "commit-check L{li}: kv row count mismatch {} vs {}",
8341 ck.len(),
8342 kk.len()
8343 );
8344 }
8345 }
8346 eprintln!(
8347 "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}"
8348 );
8349 }
8350 }
8351 if !metal_native && a + 1 < b {
8352 let expected_gdn_layers = self.graph_gdn_layer_count();
8353 if expected_gdn_layers > 0
8354 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8355 {
8356 self.clear_sequence_state();
8357 self.graph_failed
8358 .store(true, std::sync::atomic::Ordering::Relaxed);
8359 self.cancel
8360 .store(true, std::sync::atomic::Ordering::Relaxed);
8361 tracing::error!("GDN speculative restore failed after verify");
8362 return None;
8363 }
8364 }
8365 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8366 self.clear_sequence_state();
8371 self.graph_failed
8372 .store(true, std::sync::atomic::Ordering::Relaxed);
8373 self.cancel
8374 .store(true, std::sync::atomic::Ordering::Relaxed);
8375 tracing::error!("trunk graph KV rewind failed after speculative verify");
8376 return None;
8377 }
8378 *accepted += a;
8379 if !metal_native {
8390 m.kv.truncate_last(k_spec.saturating_sub(1));
8392 }
8393 spec_stamp("c.trunc");
8394 if !metal_native
8395 && self.mtp_graph_mode == Some(true)
8396 && !self.rewind_mtp_graph_mirror(next_pos)
8397 {
8398 self.clear_sequence_state();
8402 self.graph_failed
8403 .store(true, std::sync::atomic::Ordering::Relaxed);
8404 self.cancel
8405 .store(true, std::sync::atomic::Ordering::Relaxed);
8406 tracing::error!("MTP graph mirror rewind failed after verify commit");
8407 return None;
8408 }
8409 if !warm_off && a > 0 {
8410 let mut warmed = false;
8413 #[cfg(target_os = "macos")]
8414 if metal_native && self.mtp_graph_mode == Some(true) {
8415 warmed = match warm_pending.take() {
8419 Some(p) => self.mtp_warm_batch_finish(m, p),
8420 None => false,
8421 };
8422 if !warmed {
8423 warmed = true;
8424 for j in 0..a {
8425 let row =
8426 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8427 if self
8428 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8429 .is_none()
8430 {
8431 warmed = false;
8432 break;
8433 }
8434 }
8435 }
8436 }
8437 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8438 let rows: Vec<Vec<f32>> = (0..a)
8439 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8440 .collect();
8441 let pairs: Vec<(&[f32], u32)> = rows
8442 .iter()
8443 .zip(ids.iter())
8444 .map(|(r, &t)| (r.as_slice(), t))
8445 .collect();
8446 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8447 Ok(()) => warmed = true,
8448 Err(err) => {
8449 tracing::error!("{err}");
8455 self.clear_sequence_state();
8456 self.graph_failed
8457 .store(true, std::sync::atomic::Ordering::Relaxed);
8458 self.cancel
8459 .store(true, std::sync::atomic::Ordering::Relaxed);
8460 return None;
8461 }
8462 }
8463 }
8464 if !warmed {
8465 for j in 0..a {
8466 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8467 let row = row.to_vec();
8468 self.mtp_warm(m, &row, ids[j], next_pos + j);
8469 }
8470 }
8471 }
8472 spec_stamp("c.warm");
8476 if let Some(c) = forced {
8477 self.spec_forced = Some(c);
8478 self.graph_logits = None;
8479 } else if greedy_dev && logits.is_empty() {
8480 self.spec_forced = Some(ids[a]);
8483 self.graph_logits = None;
8484 } else {
8485 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8486 row.resize(self.vocab_size, 0.0);
8487 if let Some(c) = self.final_softcap {
8488 for l in row.iter_mut() {
8489 *l = c * (*l / c).tanh();
8490 }
8491 }
8492 self.graph_logits = Some(row);
8493 }
8494 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8495 spec_stamp("c.row");
8496 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8502 let end = subs();
8503 eprintln!(
8504 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8505 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8506 t_draft.as_secs_f64() * 1e3,
8507 sub_draft - sub0,
8508 (t_verify - t_draft).as_secs_f64() * 1e3,
8509 sub_verify - sub_draft,
8510 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8511 end - sub_verify,
8512 self.draft_full_streak,
8513 );
8514 }
8515 if k_env.is_none() && !metal_native && !k_capped {
8520 let f = a as f32 / k_spec.max(1) as f32;
8524 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8525 let mut k_next = k_spec;
8526 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8527 k_next = k_spec + 1;
8528 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8529 k_next = k_spec - 1;
8530 }
8531 if k_next != k_spec {
8532 self.spec_acc_ewma = 0.6;
8533 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8534 eprintln!("spec-k: {k_spec} → {k_next}");
8535 }
8536 }
8537 self.spec_k_adapt = Some(k_next);
8538 }
8539 spec_stamp("end");
8540 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8541 }
8542
8543 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8552 if !self.pair_supported() {
8553 return (0.0, 0.0);
8554 }
8555 if self.gdn_cfg.is_some()
8563 && self.graph_prefill_preferred()
8564 && self.automatic_gpu_prefix().is_some()
8565 {
8566 tracing::info!(
8567 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8568 );
8569 return (0.0, 0.0);
8570 }
8571 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8578 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8579 let emb1 = self.embed_single(1);
8580 let emb2 = self.embed_single(2);
8581 let pos = self.kv_cache.seq_len();
8582
8583 let t0 = std::time::Instant::now();
8584 for _ in 0..iters {
8585 let _ = self.forward_layers(&emb1, pos, None);
8586 let _ = self.forward_layers(&emb2, pos + 1, None);
8587 for l in &mut self.kv_cache.layers {
8588 l.truncate_last(2);
8589 }
8590 }
8591 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8592
8593 let t1 = std::time::Instant::now();
8594 for _ in 0..iters {
8595 let _ = self.forward_pair(&emb1, &emb2, pos);
8596 for l in &mut self.kv_cache.layers {
8597 l.truncate_last(2);
8598 }
8599 }
8600 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8601 match graph_env {
8602 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8603 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8604 }
8605 (singles_ms, pair_ms)
8606 }
8607
8608 fn pair_supported(&self) -> bool {
8616 !self.weights.layers.is_empty()
8623 && !self.per_token_stack()
8624 && !self
8625 .weights
8626 .layers
8627 .iter()
8628 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8629 }
8630
8631 fn forward_pair(
8632 &mut self,
8633 emb1: &[f32],
8634 emb2: &[f32],
8635 position: usize,
8636 ) -> (Vec<f32>, Vec<f32>) {
8637 self.mimo_moe_prepare();
8640 let mut h1 = emb1.to_vec();
8641 let mut h2 = emb2.to_vec();
8642 let (_nkv, _hd, hs, _rd, eps) = (
8643 self.num_kv_heads,
8644 self.head_dim,
8645 self.hidden_size,
8646 self.rotary_dim,
8647 self.rms_eps,
8648 );
8649 let pool = self.pool.clone();
8650 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8657
8658 for li in 0..self.num_layers {
8659 let _capacity_tail = automatic_gpu_prefix
8660 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8661 .map(|_| crate::gpu::enter_cpu_scope());
8662 crate::gpu::set_layer(li as i64);
8665 let lw = &self.weights.layers[self.phys_layer(li)];
8666 inference::rms_norm_into(
8669 &h1,
8670 &lw.input_norm,
8671 self.rms_eps,
8672 self.norm_style,
8673 &mut self.ws.n1,
8674 );
8675 inference::rms_norm_into(
8676 &h2,
8677 &lw.input_norm,
8678 self.rms_eps,
8679 self.norm_style,
8680 &mut self.ws.n2,
8681 );
8682
8683 let (a1, a2) = match &lw.attn {
8684 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8685 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8686 AttnKind::Bounded(w) => {
8687 let rope = self
8690 .bounded_rope
8691 .clone()
8692 .expect("bounded layer without an installed rotation table");
8693 let cfg = crate::bounded::BoundedAttnCfg {
8694 num_heads: self.num_heads,
8695 num_kv_heads: self.num_kv_heads,
8696 head_dim: self.head_dim,
8697 hidden_size: hs,
8698 scale: self.attn_scale,
8699 rope: &rope,
8700 pool: pool.as_deref(),
8701 };
8702 let a1 = crate::bounded::bounded_attention(
8703 &self.ws.n1,
8704 w,
8705 &mut self.kv_cache.layers[li],
8706 &cfg,
8707 );
8708 let a2 = crate::bounded::bounded_attention(
8709 &self.ws.n2,
8710 w,
8711 &mut self.kv_cache.layers[li],
8712 &cfg,
8713 );
8714 (a1, a2)
8715 }
8716 AttnKind::Linear(w) => {
8717 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8718 let layer = &mut self.kv_cache.layers[li];
8719 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8720 vmf_phase_pair(
8721 &self.ws.n1,
8722 &self.ws.n2,
8723 w,
8724 &cfg,
8725 state,
8726 scratch,
8727 self.pool.as_deref(),
8728 )
8729 }
8730 AttnKind::LinearGdn(w) => {
8731 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8732 let layer = &mut self.kv_cache.layers[li];
8733 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8734 gdn_pair(
8735 &self.ws.n1,
8736 &self.ws.n2,
8737 w,
8738 &cfg,
8739 state,
8740 scratch,
8741 self.pool.as_deref(),
8742 )
8743 }
8744 AttnKind::ShortConv(w) => {
8745 let cfg = self
8746 .short_conv_cfg
8747 .expect("short-conv layer without short_conv_cfg");
8748 let layer = &mut self.kv_cache.layers[li];
8749 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8750 short_conv_pair(
8751 &self.ws.n1,
8752 &self.ws.n2,
8753 w,
8754 &cfg,
8755 state,
8756 scratch,
8757 self.pool.as_deref(),
8758 )
8759 }
8760 AttnKind::Full {
8761 wq,
8762 wk,
8763 wv,
8764 wo,
8765 q_norm,
8766 k_norm,
8767 output_gate,
8768 softplus_gate,
8769 bias,
8770 } => {
8771 let inv_freq_l = self.layer_inv_freq(li);
8772 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8773 let cfg = QwenAttnCfg {
8774 num_heads: self.layer_num_heads(li),
8775 num_kv_heads: nkv_l,
8776 head_dim: hd_l,
8777 hidden_size: hs,
8778 position,
8779 inv_freq: &inv_freq_l,
8780 rotary_dim: rd_l,
8781 scale: self.attn_scale,
8782 softcap: self.attn_softcap,
8783 window: self.layer_window(li),
8784 v_norm: self.attn_v_norm,
8785 qk_norm_after_rope: self.qk_norm_after_rope,
8786 gate_sigmoid: self.proj_gate_sigmoid,
8787 q_norm: q_norm.as_deref(),
8788 k_norm: k_norm.as_deref(),
8789 output_gate: *output_gate,
8790 softplus_gate: softplus_gate
8791 .as_ref()
8792 .map(|(gate, per_head)| (gate, *per_head)),
8793 rope_scale: self.layer_rope_scale(li),
8794 bias: bias
8795 .as_ref()
8796 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8797 rms_eps: eps,
8798 norm_style: self.norm_style,
8799 pool: pool.as_deref(),
8800 v_head_dim: self.layer_v_dim(li),
8801 };
8802 attention::qwen_attention_pair(
8803 &self.ws.n1,
8804 &self.ws.n2,
8805 wq,
8806 wk,
8807 wv,
8808 wo,
8809 &mut self.kv_cache.layers[li],
8810 &cfg,
8811 )
8812 }
8813 };
8814 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8815 Some(w) => (
8816 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8817 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8818 ),
8819 None => (a1, a2),
8820 };
8821 for i in 0..self.hidden_size {
8822 h1[i] += a1[i];
8823 h2[i] += a2[i];
8824 }
8825 let (mut a1, mut a2) = (a1, a2);
8826 attention::recycle_buf(&mut a1);
8827 attention::recycle_buf(&mut a2);
8828
8829 let lw = &self.weights.layers[self.phys_layer(li)];
8830 inference::rms_norm_into(
8831 &h1,
8832 &lw.post_norm,
8833 self.rms_eps,
8834 self.norm_style,
8835 &mut self.ws.p1,
8836 );
8837 inference::rms_norm_into(
8838 &h2,
8839 &lw.post_norm,
8840 self.rms_eps,
8841 self.norm_style,
8842 &mut self.ws.p2,
8843 );
8844 let (f1, f2) = match &lw.ffn {
8845 FfnKind::DenseMoe(dm) => (
8848 dense_moe_ffn(
8849 dm,
8850 &self.ws.p1,
8851 &h1,
8852 self.rms_eps,
8853 self.norm_style,
8854 self.pool.as_deref(),
8855 ),
8856 dense_moe_ffn(
8857 dm,
8858 &self.ws.p2,
8859 &h2,
8860 self.rms_eps,
8861 self.norm_style,
8862 self.pool.as_deref(),
8863 ),
8864 ),
8865 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8866 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8867 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8868 ),
8869 _ => ffn_forward_pair(
8870 &lw.ffn,
8871 &self.ws.p1,
8872 &self.ws.p2,
8873 self.pool.as_deref(),
8874 None,
8875 ),
8876 };
8877 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8878 Some(w) => (
8879 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8880 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8881 ),
8882 None => (f1, f2),
8883 };
8884 for i in 0..self.hidden_size {
8885 h1[i] += f1[i];
8886 h2[i] += f2[i];
8887 }
8888 let (mut f1, mut f2) = (f1, f2);
8889 attention::recycle_buf(&mut f1);
8890 attention::recycle_buf(&mut f2);
8891 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8892 for i in 0..self.hidden_size {
8893 h1[i] *= sc;
8894 h2[i] *= sc;
8895 }
8896 }
8897 if self.is_loop_end(li) && li + 1 < self.num_layers {
8899 h1 = inference::rms_norm(
8900 &h1,
8901 &self.weights.final_norm,
8902 self.rms_eps,
8903 self.norm_style,
8904 );
8905 h2 = inference::rms_norm(
8906 &h2,
8907 &self.weights.final_norm,
8908 self.rms_eps,
8909 self.norm_style,
8910 );
8911 }
8912 }
8913 crate::gpu::set_layer(-1); if self.o1_active() {
8920 self.commit_linear_scratch();
8921 }
8922 self.o1_progress();
8923 self.swa_trim_tails();
8924 (h1, h2)
8925 }
8926
8927 fn commit_linear_scratch(&mut self) {
8929 for layer in &mut self.kv_cache.layers {
8930 if !layer.linear_scratch.is_empty() {
8931 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8932 layer.linear_scratch.clear();
8933 }
8934 }
8935 }
8936
8937 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8945 #[cfg(target_os = "macos")]
8946 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8947 if ids.is_empty() {
8948 return Err("empty id sequence".to_string());
8949 }
8950
8951 self.nll_begin()?;
8952 let result = (|| {
8953 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8954 self.nll_check_graph("encoder prefill", 0)?;
8955 let expected = ids.len() * self.hidden_size;
8956 if hidden.len() != expected {
8957 return Err(format!(
8958 "encoder prefill returned {} values for {} rows of hidden size {}",
8959 hidden.len(),
8960 ids.len(),
8961 self.hidden_size
8962 ));
8963 }
8964
8965 let mut out = vec![0.0f32; expected];
8966 for (src, dst) in hidden
8967 .chunks_exact(self.hidden_size)
8968 .zip(out.chunks_exact_mut(self.hidden_size))
8969 {
8970 inference::rms_norm_into(
8971 src,
8972 &self.weights.final_norm,
8973 self.rms_eps,
8974 self.norm_style,
8975 dst,
8976 );
8977 }
8978 Ok(out)
8979 })();
8980 self.nll_end();
8981 result
8982 }
8983
8984 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8989 #[cfg(target_os = "macos")]
8990 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8991 if ids.is_empty() {
8992 return Err("empty id sequence".to_string());
8993 }
8994 self.nll_begin()?;
8995 let result = (|| {
8996 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8997 self.nll_check_graph("hidden prefill", 0)?;
8998 if hidden.len() != ids.len() * self.hidden_size {
8999 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9000 }
9001 Ok(hidden)
9002 })();
9003 self.nll_end();
9004 result
9005 }
9006
9007 pub fn forward_ids(
9010 &mut self,
9011 ids: &[u32],
9012 task_mask: Option<&TaskMask>,
9013 ) -> Result<Vec<f32>, String> {
9014 #[cfg(target_os = "macos")]
9015 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9016 if ids.is_empty() {
9017 return Err("empty id sequence".to_string());
9018 }
9019 self.clear_sequence_state();
9020 self.check_forward_graph("forward_ids setup", 0)?;
9021 if task_mask.is_none() {
9022 self.o1_begin();
9023 }
9024 let mut hidden = vec![0.0f32; self.hidden_size];
9025 let mut pos = 0usize;
9026 if let Some(b) = &mut self.dsv41 {
9027 let pool = self.pool.clone();
9028 let mut logits = Vec::new();
9029 crate::dsv41::forward_chunk(
9030 &b.0,
9031 &b.1,
9032 &b.2,
9033 &mut b.3,
9034 ids,
9035 0,
9036 pool.as_deref(),
9037 &mut logits,
9038 );
9039 if let Err(err) = self.o1_seal_checked() {
9040 self.clear_sequence_state();
9041 return Err(err);
9042 }
9043 return Ok(logits);
9044 }
9045 #[cfg(not(target_os = "macos"))]
9056 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9057 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9058 Ok(r) => r,
9059 Err(e) => {
9060 self.clear_sequence_state();
9061 return Err(e);
9062 }
9063 };
9064 if n > 0 {
9065 hidden.copy_from_slice(&last);
9066 pos = n;
9067 }
9068 }
9069 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9070 let chunk = self.prefill_chunk();
9074 let hs = self.hidden_size;
9075 while pos < ids.len() {
9076 let end = (pos + chunk).min(ids.len());
9077 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9078 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9079 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9080 pos = end;
9081 }
9082 }
9083 if task_mask.is_none()
9092 && !self.graph_prefill_preferred()
9093 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9094 && self.pair_supported()
9095 {
9096 while pos + 1 < ids.len() {
9097 let e1 = self.embed_single(ids[pos]);
9098 let e2 = self.embed_single(ids[pos + 1]);
9099 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9100 self.check_forward_graph("forward_ids pair", pos + 1)?;
9101 self.commit_linear_scratch();
9102 hidden = h2;
9103 pos += 2;
9104 }
9105 }
9106 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9109 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9110 self.graph_logits = Some(lg);
9111 hidden = vec![0.0; self.hidden_size];
9112 pos = ids.len();
9113 }
9114 }
9115 while pos < ids.len() {
9116 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9117 self.check_forward_graph("forward_ids", pos)?;
9118 pos += 1;
9119 }
9120 if let Some(logits) = self.graph_logits.take() {
9121 if let Err(err) = self.o1_seal_checked() {
9125 self.clear_sequence_state();
9126 return Err(err);
9127 }
9128 return Ok(logits);
9129 }
9130 if let Err(err) = self.o1_seal_checked() {
9134 self.clear_sequence_state();
9135 return Err(err);
9136 }
9137 let normed = inference::rms_norm(
9138 &hidden,
9139 &self.weights.final_norm,
9140 self.rms_eps,
9141 self.norm_style,
9142 );
9143 Ok(self.lm_head_forward(&normed))
9144 }
9145
9146 #[doc(hidden)]
9150 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9151 #[cfg(target_os = "macos")]
9152 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9153 if ids.is_empty() {
9154 return Err("empty id sequence".to_string());
9155 }
9156 self.clear_sequence_state();
9157 self.dsv41
9158 .as_ref()
9159 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9160 self.o1_begin();
9161 let rows = {
9162 let pool = self.pool.clone();
9163 let b = self
9164 .dsv41
9165 .as_mut()
9166 .expect("dsv41 checked above; state cannot change during forward");
9167 let mut rows = Vec::with_capacity(ids.len());
9168 for (position, &id) in ids.iter().enumerate() {
9169 let mut logits = Vec::new();
9170 crate::dsv41::forward_token(
9171 &b.0,
9172 &b.1,
9173 &b.2,
9174 &mut b.3,
9175 id,
9176 position,
9177 pool.as_deref(),
9178 &mut logits,
9179 );
9180 rows.push(logits);
9181 }
9182 rows
9183 };
9184 self.o1_seal();
9185 Ok(rows)
9186 }
9187
9188 pub fn dsv41_moe_capture(
9192 &mut self,
9193 ids: &[u32],
9194 lo: usize,
9195 hi: usize,
9196 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9197 #[cfg(target_os = "macos")]
9198 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9199 if ids.is_empty() {
9200 return Err("empty id sequence".to_string());
9201 }
9202 self.dsv41
9203 .as_ref()
9204 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9205 self.clear_sequence_state();
9206 let res = {
9207 let pool = self.pool.clone();
9208 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9209 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9210 };
9211 self.clear_sequence_state();
9212 res
9213 }
9214
9215 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9222 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9223 Ok((nll / cnt.max(1) as f64).exp())
9224 }
9225
9226 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9233 if !self.can_prefill_batched() {
9234 return self.nll_ids_from(ids, 0).map(|_| ());
9235 }
9236 self.nll_begin()?;
9237 let result: Result<(), String> = (|| {
9238 let chunk = chunk.max(1);
9239 let mut pos = 0usize;
9240 while pos < ids.len() {
9241 let end = (pos + chunk).min(ids.len());
9242 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9243 self.nll_check_graph("calibration prefill", pos)?;
9244 pos = end;
9245 }
9246 Ok(())
9247 })();
9248 self.nll_end();
9249 result
9250 }
9251
9252 pub fn calib_window_span(
9261 &mut self,
9262 ids: &[u32],
9263 input: Option<&[f32]>,
9264 from: usize,
9265 mid: usize,
9266 upto: usize,
9267 ) -> Result<Vec<f32>, String> {
9268 if !self.can_prefill_batched() {
9269 return Err("calib_window_span: this architecture has no batched prefill".into());
9270 }
9271 self.nll_begin()?;
9272 let result: Result<Vec<f32>, String> = (|| {
9273 let h = if mid > from {
9274 let inp = match input {
9275 Some(h) => PrefillIn::Hidden(h),
9276 None => PrefillIn::Ids(ids),
9277 };
9278 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9279 self.nll_check_graph("calibration span", 0)?;
9280 h
9281 } else {
9282 input.map(<[f32]>::to_vec).unwrap_or_default()
9283 };
9284 if upto > mid {
9285 let inp = if mid > from || input.is_some() {
9286 PrefillIn::Hidden(&h)
9287 } else {
9288 PrefillIn::Ids(ids)
9289 };
9290 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9291 self.nll_check_graph("calibration span", 0)?;
9292 }
9293 Ok(h)
9294 })();
9295 self.nll_end();
9296 result
9297 }
9298
9299 #[cfg(feature = "gpu")]
9303 pub fn qwen4_capture(
9304 &mut self,
9305 ids: &[u32],
9306 lo: usize,
9307 hi: usize,
9308 ) -> Result<Vec<Vec<f32>>, String> {
9309 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9310 crate::qwen4_exp::capture_window(
9311 &b.0,
9312 &b.1,
9313 &b.2,
9314 &mut b.3,
9315 ids,
9316 lo,
9317 hi,
9318 &self.inv_freq,
9319 self.pool.as_deref(),
9320 )
9321 }
9322
9323 #[cfg(feature = "gpu")]
9329 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9330 use crate::gpu_wgpu::clover as cap;
9331 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9332 let ne = (lo..hi)
9333 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9334 FfnKind::Moe(m) => Some(m.experts.len()),
9335 _ => None,
9336 })
9337 .ok_or("no MoE layer in the capture range")?;
9338 let chunk = self.prefill_chunk().max(1);
9339 self.clear_sequence_state();
9340 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9341 return Err("capture unavailable (no wgpu device)".into());
9342 }
9343 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9344 let res = (|| {
9345 for (ci, part) in ids.chunks(chunk).enumerate() {
9346 let mut hiddens = Vec::with_capacity(part.len() * hs);
9347 for &id in part {
9348 hiddens.extend_from_slice(&self.embed_single(id));
9349 }
9350 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9351 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9352 crate::gpu::BatchGraphOutcome::Completed => {}
9353 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9354 }
9355 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9356 if written < hi - lo {
9357 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9358 }
9359 for (o, r) in out.iter_mut().zip(rows) {
9360 o.extend_from_slice(&r);
9361 }
9362 }
9363 Ok(())
9364 })();
9365 cap::moe_capture_end();
9366 self.clear_sequence_state();
9367 res.map(|_| out)
9368 }
9369
9370 pub fn moe_expert_dirs(
9373 &self,
9374 li: usize,
9375 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9376 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9377 return None;
9378 };
9379 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9380 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9381 let shared = match &m.shared {
9382 Some((se, _)) => Some(dirs(se)?),
9383 None => None,
9384 };
9385 Some((routed, shared))
9386 }
9387
9388 pub fn qwen4_expert_dirs(
9390 &self,
9391 li: usize,
9392 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9393 let b = self.qwen4_exp.as_ref()?;
9394 crate::qwen4_exp::expert_dirs(&b.1, li)
9395 }
9396
9397 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9412 self.reset_mtp_probe_state();
9413 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9419 let result = if ids.len() < 2 {
9420 Err("MTP FFN probe needs at least two token ids".into())
9421 } else if ids
9422 .iter()
9423 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9424 {
9425 Err("MTP FFN probe received a token id outside the embedding table".into())
9426 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9427 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9430 } else if self.mimo_mtp.is_some() {
9431 let mut st = self
9432 .mimo_mtp
9433 .take()
9434 .expect("checked MiMo MTP before detaching it");
9435 let result = match self.probe_trunk_rows(ids) {
9436 Ok(trunk) => crate::gpu::cpu_scope(|| {
9437 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9438 }),
9439 Err(e) => Err(e),
9440 };
9441 st.reset();
9444 self.mimo_mtp = Some(st);
9445 result
9446 } else if self.mtp.is_some() {
9447 let mut mtp = self
9448 .mtp
9449 .take()
9450 .expect("checked canonical MTP before detaching it");
9451 let result = match self.probe_trunk_rows(ids) {
9452 Ok(trunk) => crate::gpu::cpu_scope(|| {
9453 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9454 }),
9455 Err(e) => Err(e),
9456 };
9457 mtp.kv.clear();
9458 self.mtp = Some(mtp);
9459 result
9460 } else {
9461 Err("MTP FFN probe: this pipeline has no draft stack".into())
9462 };
9463 self.reset_mtp_probe_state();
9466 result
9467 }
9468
9469 fn reset_mtp_probe_state(&mut self) {
9471 self.clear_sequence_state();
9472 if let Some(mtp) = self.mtp.as_mut() {
9473 mtp.kv.clear();
9474 }
9475 if let Some(mimo) = self.mimo_mtp.as_mut() {
9476 mimo.reset();
9477 }
9478 self.mtp_graph_mode = None;
9479 }
9480
9481 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9487 if ids.len() < 2 {
9488 return Err("MTP FFN probe needs at least two token ids".into());
9489 }
9490 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9491 self.clear_sequence_state();
9492 Ok(rows)
9493 }
9494
9495 fn mtp_ffn_probe_teacher_forced(
9501 &mut self,
9502 mtp: &mut MtpModule,
9503 ids: &[u32],
9504 trunk: &[f32],
9505 ) -> Result<Vec<MtpFfnProbe>, String> {
9506 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9507 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9508 };
9509 let inter = dense.gate_proj.rows();
9510 let (hs, n) = (self.hidden_size, ids.len() - 1);
9511 if trunk.len() != n * hs {
9512 return Err(format!(
9513 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9514 trunk.len()
9515 ));
9516 }
9517 let mut mass = vec![0.0f64; inter];
9518 let mut squared_mass = vec![0.0f64; inter];
9519 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9520 for position in 0..n {
9521 self.mtp_probe_ffn_input(
9522 mtp,
9523 &trunk[position * hs..(position + 1) * hs],
9524 ids[position + 1],
9525 position,
9526 )?;
9527 inputs.extend_from_slice(&self.ws.p1[..hs]);
9528 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9529 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9530 unreachable!("checked dense above");
9531 };
9532 accumulate_dense_ffn_activation_mass(
9533 dense,
9534 &inputs,
9535 inputs.len() / hs,
9536 self.pool.as_deref(),
9537 &mut mass,
9538 &mut squared_mass,
9539 )?;
9540 inputs.clear();
9541 }
9542 }
9543 Ok(vec![MtpFfnProbe {
9544 mass,
9545 squared_mass,
9546 observed_positions: n,
9547 }])
9548 }
9549
9550 fn mtp_probe_ffn_input(
9556 &mut self,
9557 mtp: &mut MtpModule,
9558 hidden: &[f32],
9559 next_token: u32,
9560 position: usize,
9561 ) -> Result<(), String> {
9562 let e = self.embed_single(next_token);
9563 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9564 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9565 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9566 inference::rms_norm_into(
9567 hidden,
9568 &mtp.hnorm,
9569 self.rms_eps,
9570 self.norm_style,
9571 cat_h,
9572 );
9573 let mut x = vec![0.0f32; self.hidden_size];
9574 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9575
9576 let lw = &mtp.layer;
9577 inference::rms_norm_into(
9578 &x,
9579 &lw.input_norm,
9580 self.rms_eps,
9581 self.norm_style,
9582 &mut self.ws.n1,
9583 );
9584 let attn = match &lw.attn {
9585 AttnKind::Full {
9586 wq,
9587 wk,
9588 wv,
9589 wo,
9590 q_norm,
9591 k_norm,
9592 output_gate,
9593 softplus_gate,
9594 bias,
9595 } => {
9596 let mut cfg = self.attn_cfg(position);
9597 cfg.q_norm = q_norm.as_deref();
9598 cfg.k_norm = k_norm.as_deref();
9599 cfg.output_gate = *output_gate;
9600 cfg.softplus_gate = softplus_gate
9601 .as_ref()
9602 .map(|(gate, per_head)| (gate, *per_head));
9603 cfg.bias = bias
9604 .as_ref()
9605 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9606 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9607 }
9608 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9609 };
9610 for (dst, src) in x.iter_mut().zip(attn) {
9611 *dst += src;
9612 }
9613 inference::rms_norm_into(
9614 &x,
9615 &lw.post_norm,
9616 self.rms_eps,
9617 self.norm_style,
9618 &mut self.ws.p1,
9619 );
9620 Ok(())
9621 }
9622
9623 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9628 self.clear_sequence_state();
9629 FFN_PROBE.with(|p| {
9630 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9631 });
9632 crate::gpu::cpu_scope(|| {
9633 for (pos, &id) in ids.iter().enumerate() {
9634 let emb = self.embed_single(id);
9635 let _ = self.forward_layers(&emb, pos, None);
9636 }
9637 });
9638 self.clear_sequence_state();
9639 FFN_PROBE
9640 .with(|p| p.borrow_mut().take())
9641 .unwrap_or_default()
9642 }
9643
9644 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9648 if let Err(err) = self.nll_begin() {
9649 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9653 self.nll_end();
9654 return Err(err);
9655 }
9656 FFN_PROBE.with(|p| {
9657 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9658 });
9659 let result: Result<(), String> = (|| {
9660 for chunk in ids.chunks(256) {
9661 if chunk.len() < 2 {
9662 continue;
9663 }
9664 self.nll_ids_masked(chunk, 0, None)?;
9665 }
9666 Ok(())
9667 })();
9668 self.nll_end();
9669 let probe = FFN_PROBE
9670 .with(|p| p.borrow_mut().take())
9671 .unwrap_or_default();
9672 match result {
9673 Ok(()) => Ok(probe),
9674 Err(err) => {
9675 drop(probe);
9676 Err(err)
9677 }
9678 }
9679 }
9680
9681 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9685 self.nll_begin()?;
9686 let result: Result<f64, String> = (|| {
9687 let mut nll = 0f64;
9688 let mut cnt = 0usize;
9689 let mut hidden = vec![0f32; self.hidden_size];
9690 for (pos, &id) in ids.iter().enumerate() {
9691 if pos > 0 {
9692 inference::rms_norm_into(
9693 &hidden,
9694 &self.weights.final_norm,
9695 self.rms_eps,
9696 self.norm_style,
9697 &mut self.ws.n1,
9698 );
9699 let mut logits = self.lm_head_forward(&self.ws.n1);
9700 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9701 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9702 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9703 nll -= p.max(1e-300).ln();
9704 cnt += 1;
9705 attention::recycle_buf(&mut logits);
9706 }
9707 let emb = self.embed_single(id);
9708 hidden = self.forward_layers(&emb, pos, Some(mask));
9709 self.nll_check_graph("masked serial forward", pos)?;
9710 let _ = self.graph_logits.take();
9714 }
9715 Ok((nll / cnt.max(1) as f64).exp())
9716 })();
9717 self.nll_end();
9718 result
9719 }
9720
9721 pub fn nll_ids_masked(
9740 &mut self,
9741 ids: &[u32],
9742 start: usize,
9743 task_mask: Option<&TaskMask>,
9744 ) -> Result<(f64, usize), String> {
9745 let task_mask = self.drop_open_mask(task_mask);
9746 self.nll_ids_inner(ids, start, task_mask)
9747 }
9748
9749 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9750 self.nll_ids_inner(ids, start, None)
9751 }
9752
9753 fn nll_ids_inner(
9754 &mut self,
9755 ids: &[u32],
9756 start: usize,
9757 task_mask: Option<&TaskMask>,
9758 ) -> Result<(f64, usize), String> {
9759 self.nll_begin()?;
9760 #[cfg(target_os = "macos")]
9762 if task_mask.is_none() && self.is_block_diffusion() {
9763 if let Some(r) = self.clover_metal_nll(ids, start) {
9764 self.nll_end();
9765 return r;
9766 }
9767 }
9768 let result: Result<(f64, usize), String> = (|| {
9769 let mut nll = 0f64;
9770 let mut cnt = 0usize;
9771 let (graph_quality, fused_head_quality) = nll_graph_policy(
9784 task_mask.is_none(),
9785 self.graph_prefill_preferred(),
9786 crate::gpu::q1_force(),
9787 );
9788 self.graph_head_required = fused_head_quality;
9789 self.graph_want_logits = fused_head_quality;
9790 #[cfg(target_os = "macos")]
9791 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9792 match self.nll_batch_metal(ids, start) {
9793 MetalBatchNllOutcome::Completed(nll, count) => {
9794 return Ok((nll, count));
9795 }
9796 MetalBatchNllOutcome::Declined => {}
9797 MetalBatchNllOutcome::Failed(err) => return Err(err),
9798 }
9799 }
9800 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9805 if self.can_prefill_batched() && !graph_quality && !force_serial {
9806 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9815 .ok()
9816 .and_then(|v| v.parse().ok())
9817 .filter(|&c: &usize| c > 0)
9818 .unwrap_or(128);
9819 const LM_SUB: usize = 32;
9820 let n = ids.len().saturating_sub(1);
9821 let hs = self.hidden_size;
9822 let rows = self.weights.lm_head.rows();
9823 let mut pos = 0usize;
9824 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9825 while pos < n {
9826 let end = (pos + chunk_len).min(n);
9827 let bsz = end - pos;
9828 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9829 self.nll_check_graph("batched prefill", pos)?;
9830 if state_trace && end % 256 == 0 {
9831 self.trace_recurrent_state(end);
9832 }
9833 let mut k0 = 0usize;
9834 while k0 < bsz {
9835 let k1 = (k0 + LM_SUB).min(bsz);
9836 let sb = k1 - k0;
9837 if pos + k1 <= start {
9840 k0 = k1;
9841 continue;
9842 }
9843 let mut normed = vec![0.0f32; sb * hs];
9844 for k in 0..sb {
9845 let r = inference::rms_norm(
9846 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9847 &self.weights.final_norm,
9848 self.rms_eps,
9849 self.norm_style,
9850 );
9851 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9852 }
9853 let mut logits = vec![0.0f32; sb * rows];
9854 self.weights
9855 .lm_head
9856 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9857 for k in 0..sb {
9858 if pos + k0 + k < start {
9859 continue;
9860 }
9861 self.nll_check_graph("batched score row", pos + k0 + k)?;
9862 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9863 if let Some(mu) = self.logit_multiplier {
9864 for v in lg.iter_mut() {
9865 *v *= mu;
9866 }
9867 }
9868 if let Some(c) = self.final_softcap {
9872 softcap_in_place(lg, c, self.pool.as_deref());
9873 }
9874 if let Some(cm) = self.head_clusters.clone() {
9877 self.hierarchical_head_logprobs(
9878 &normed[k * hs..(k + 1) * hs],
9879 &cm,
9880 lg,
9881 );
9882 }
9883 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9884 let target = ids[pos + k0 + k + 1] as usize;
9885 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9886 let lse: f64 = lg
9887 .iter()
9888 .map(|&v| ((v - max) as f64).exp())
9889 .sum::<f64>()
9890 .ln()
9891 + max as f64;
9892 nll += lse - lg[target] as f64;
9893 cnt += 1;
9894 if std::env::var("CMF_PPL_TRACE").is_ok() {
9895 let top = lg
9896 .iter()
9897 .enumerate()
9898 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9899 .map(|(i, _)| i)
9900 .unwrap_or(0);
9901 eprintln!(
9902 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9903 pos + k0 + k,
9904 target,
9905 lse - lg[target] as f64,
9906 top,
9907 lg[target],
9908 lg[top]
9909 );
9910 }
9911 }
9912 k0 = k1;
9913 }
9914 pos = end;
9915 }
9916 return Ok((nll, cnt));
9917 }
9918 for pos in 0..ids.len().saturating_sub(1) {
9919 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9920 self.nll_check_graph("serial forward", pos)?;
9921 let out_of_band = self.graph_logits.take();
9929 if self.graph_head_required && out_of_band.is_none() {
9930 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9931 return Err(format!(
9932 "fused Metal graph head did not complete at NLL position {pos}"
9933 ));
9934 }
9935 if pos < start {
9936 continue;
9937 }
9938 let logits = match out_of_band {
9939 Some(lg) => lg,
9940 None => {
9941 let normed = inference::rms_norm(
9942 &hidden,
9943 &self.weights.final_norm,
9944 self.rms_eps,
9945 self.norm_style,
9946 );
9947 self.lm_head_forward(&normed)
9951 }
9952 };
9953 let target = ids[pos + 1] as usize;
9954 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9955 let lse: f64 = logits
9956 .iter()
9957 .map(|&v| ((v - max) as f64).exp())
9958 .sum::<f64>()
9959 .ln()
9960 + max as f64;
9961 let tok_nll = lse - logits[target] as f64;
9962 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9963 let top = logits
9964 .iter()
9965 .enumerate()
9966 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9967 .map(|(i, _)| i)
9968 .unwrap_or(0);
9969 eprintln!(
9970 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9971 logits[target], logits[top]
9972 );
9973 }
9974 nll += tok_nll;
9975 cnt += 1;
9976 }
9977 Ok((nll, cnt))
9978 })();
9979 self.nll_end();
9980 result
9981 }
9982
9983 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9988 let normed = inference::rms_norm(
9989 hidden,
9990 &self.weights.final_norm,
9991 self.rms_eps,
9992 self.norm_style,
9993 );
9994 let mut logits = self.lm_head_forward(&normed);
9997 let target = target as usize;
9998 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9999 let lse: f64 = logits
10000 .iter()
10001 .map(|&v| ((v - max) as f64).exp())
10002 .sum::<f64>()
10003 .ln()
10004 + max as f64;
10005 let tok_nll = lse - logits[target] as f64;
10006 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10007 let top = logits
10008 .iter()
10009 .enumerate()
10010 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10011 .map(|(i, _)| i)
10012 .unwrap_or(0);
10013 eprintln!(
10014 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10015 logits[target], logits[top]
10016 );
10017 }
10018 attention::recycle_buf(&mut logits);
10019 tok_nll
10020 }
10021
10022 fn trace_recurrent_state(&self, pos: usize) {
10030 let stats = |v: &[f32]| -> (f64, f64) {
10031 if v.is_empty() {
10032 return (0.0, 0.0);
10033 }
10034 let (mut ss, mut mx) = (0f64, 0f64);
10035 for &x in v {
10036 ss += (x as f64) * (x as f64);
10037 mx = mx.max((x as f64).abs());
10038 }
10039 ((ss / v.len() as f64).sqrt(), mx)
10040 };
10041 for (li, l) in self.kv_cache.layers.iter().enumerate() {
10042 let lw = &self.weights.layers[self.phys_layer(li)];
10043 let (kind, s_len) = match &lw.attn {
10044 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10045 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10046 AttnKind::Bounded(_) => ("bounded", 0),
10047 AttnKind::Full { .. } => ("full", 0),
10048 _ => ("other", 0),
10049 };
10050 let (rms, max) = stats(&l.linear_state);
10051 let s_part = if kind == "vmf" {
10052 &l.linear_state[..s_len.min(l.linear_state.len())]
10053 } else if kind == "gdn" {
10054 let ring = l
10055 .linear_state
10056 .len()
10057 .saturating_sub(s_len)
10058 .min(l.linear_state.len());
10059 &l.linear_state[ring..]
10060 } else {
10061 &l.linear_state[..0]
10062 };
10063 let (s_rms, s_max) = stats(s_part);
10064 let (ring_rms, ring_len) = match &l.bounded {
10065 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10066 None => (0.0, 0),
10067 };
10068 eprintln!(
10069 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10070 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10071 l.linear_state.len(),
10072 l.seq_len
10073 );
10074 }
10075 }
10076
10077 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10095 self.nll_begin()?;
10100 let requested_prefix = (prefill > 0).then_some(prefill);
10101 self.o1_begin_with_prefix(requested_prefix);
10102 let n = ids.len().saturating_sub(1);
10103 let requested_start = prefill.min(n);
10104 let exact_end = if self.o1_active() {
10109 match requested_prefix {
10110 Some(requested) => self.o1_effective_boundary(requested),
10111 None => self
10112 .o1_cfg
10113 .as_ref()
10114 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10115 }
10116 .unwrap_or(requested_start)
10117 .min(n)
10118 } else {
10119 requested_start
10120 };
10121 let mut nll = 0f64;
10122 let mut cnt = 0usize;
10123
10124 let mut pos = 0usize;
10128 if self.can_prefill_batched() {
10129 const CHUNK: usize = 128;
10130 while pos < exact_end {
10131 let end = (pos + CHUNK).min(exact_end);
10132 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10133 if self
10134 .graph_failed
10135 .swap(false, std::sync::atomic::Ordering::Relaxed)
10136 {
10137 self.cancel
10138 .store(false, std::sync::atomic::Ordering::Relaxed);
10139 self.nll_end();
10140 return Err("GPU graph failed during O(1) NLL prefix".into());
10141 }
10142 for row in 0..end - pos {
10143 let score_pos = pos + row;
10144 if score_pos >= requested_start && score_pos < n {
10145 nll += self.nll_from_hidden(
10146 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10147 ids[score_pos + 1],
10148 score_pos,
10149 );
10150 cnt += 1;
10151 }
10152 }
10153 pos = end;
10154 }
10155 } else {
10156 while pos < exact_end {
10157 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10158 if self
10159 .graph_failed
10160 .swap(false, std::sync::atomic::Ordering::Relaxed)
10161 {
10162 self.cancel
10163 .store(false, std::sync::atomic::Ordering::Relaxed);
10164 self.nll_end();
10165 return Err("GPU graph failed during O(1) NLL prefix".into());
10166 }
10167 if pos >= requested_start && pos < n {
10168 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10169 cnt += 1;
10170 }
10171 pos += 1;
10172 }
10173 }
10174 self.o1_seal_checked().map_err(|err| {
10175 self.nll_end();
10176 err
10177 })?;
10178
10179 let batch_k = std::env::var("CMF_BATCH_K")
10188 .ok()
10189 .and_then(|v| v.parse::<usize>().ok())
10190 .unwrap_or(0);
10191 let batch_admitted = batch_k > 0
10192 && self.can_prefill_batched()
10193 && self.o1_active()
10194 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10195 && (0..self.num_layers).all(|li| {
10196 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10197 cache.o1.is_none() || cache.o1_views().is_some()
10198 });
10199 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10200 eprintln!(
10201 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10202 batch_admitted,
10203 batch_k,
10204 n.saturating_sub(exact_end),
10205 );
10206 }
10207 let mut batch_completed = false;
10208 if batch_admitted && exact_end < n {
10209 let hs = self.hidden_size;
10210 let mut batch_pos = exact_end;
10211 while batch_pos < n {
10212 let end = (batch_pos + batch_k).min(n);
10213 let bk = end - batch_pos;
10214 let mut hiddens = vec![0.0f32; bk * hs];
10215 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10216 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10217 }
10218 let positions: Vec<usize> = (batch_pos..end).collect();
10219 let t_batch = std::time::Instant::now();
10220 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10221 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10222 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10223 eprintln!(
10224 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10225 batch_pos,
10226 end.saturating_sub(1),
10227 bk as f64 / (ms / 1000.0),
10228 );
10229 }
10230 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10231 self.nll_end();
10232 return Err(err);
10233 }
10234 match outcome {
10235 crate::gpu::BatchGraphOutcome::Completed => {
10236 batch_completed = true;
10237 for row in 0..bk {
10238 nll += self.nll_from_hidden(
10239 &hiddens[row * hs..(row + 1) * hs],
10240 ids[batch_pos + row + 1],
10241 batch_pos + row,
10242 );
10243 cnt += 1;
10244 }
10245 batch_pos = end;
10246 }
10247 crate::gpu::BatchGraphOutcome::Declined => {
10248 if batch_completed {
10249 self.nll_end();
10250 return Err(format!(
10251 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10252 ));
10253 }
10254 break;
10255 }
10256 crate::gpu::BatchGraphOutcome::Failed => {
10257 self.nll_end();
10258 return Err(format!(
10259 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10260 ));
10261 }
10262 }
10263 }
10264 if batch_completed && cnt == n.saturating_sub(requested_start) {
10265 self.nll_end();
10266 return Ok((nll, cnt));
10267 }
10268 }
10269
10270 for pos in exact_end..n {
10275 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10276 if self
10277 .graph_failed
10278 .swap(false, std::sync::atomic::Ordering::Relaxed)
10279 {
10280 self.cancel
10281 .store(false, std::sync::atomic::Ordering::Relaxed);
10282 self.nll_end();
10283 return Err(format!(
10284 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10285 ));
10286 }
10287 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10288 cnt += 1;
10289 }
10290 self.nll_end();
10291 Ok((nll, cnt))
10292 }
10293
10294 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10302 self.clear_sequence_state();
10303 let n = ids.len().saturating_sub(1);
10304 let mut correct = Vec::with_capacity(n);
10305 let mut pmax = Vec::with_capacity(n);
10306 for pos in 0..n {
10307 let emb = self.embed_single(ids[pos]);
10308 let hidden = self.forward_layers(&emb, pos, None);
10309 let logits = if let Some(logits) = self.graph_logits.take() {
10310 logits
10311 } else {
10312 let normed = inference::rms_norm(
10313 &hidden,
10314 &self.weights.final_norm,
10315 self.rms_eps,
10316 self.norm_style,
10317 );
10318 self.lm_head_forward(&normed)
10322 };
10323 let target = ids[pos + 1] as usize;
10324 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10325 for (i, &v) in logits.iter().enumerate() {
10326 if v > mval {
10327 mval = v;
10328 amax = i;
10329 }
10330 }
10331 correct.push(amax == target);
10332 let row: Vec<f32> = temps
10333 .iter()
10334 .map(|&t| {
10335 let tt = t.max(1e-3);
10336 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10337 1.0 / s.max(1e-12) })
10339 .collect();
10340 pmax.push(row);
10341 }
10342 self.clear_sequence_state();
10343 (correct, pmax)
10344 }
10345
10346 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10353 if self.dyn_router.is_none() {
10354 return Ok((self.ppl_ids(ids)?, 0));
10355 }
10356 self.nll_begin()?;
10357 let saved_active = self.dyn_active;
10358 let mut router = self
10359 .dyn_router
10360 .take()
10361 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10362 router.reset();
10363 self.dyn_phi_seen = 0;
10364 let _ = self.set_active_skill(None);
10365
10366 let result: Result<(f64, usize), String> = (|| {
10367 let mut nll = 0f64;
10368 let mut cnt = 0usize;
10369 for pos in 0..ids.len().saturating_sub(1) {
10370 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10371 self.nll_check_graph("dynamic serial forward", pos)?;
10372 let out_of_band = self.graph_logits.take();
10373 let mut logits = match out_of_band {
10374 Some(lg) => lg,
10375 None => {
10376 let normed = inference::rms_norm(
10377 &hidden,
10378 &self.weights.final_norm,
10379 self.rms_eps,
10380 self.norm_style,
10381 );
10382 self.lm_head_forward(&normed)
10386 }
10387 };
10388 let target = ids[pos + 1] as usize;
10389 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10390 let lse: f64 = logits
10391 .iter()
10392 .map(|&v| ((v - max) as f64).exp())
10393 .sum::<f64>()
10394 .ln()
10395 + max as f64;
10396 let tok_nll = lse - logits[target] as f64;
10397 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10398 let top = logits
10399 .iter()
10400 .enumerate()
10401 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10402 .map(|(i, _)| i)
10403 .unwrap_or(0);
10404 eprintln!(
10405 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10406 logits[target], logits[top]
10407 );
10408 }
10409 nll += tok_nll;
10410 cnt += 1;
10411 attention::recycle_buf(&mut logits);
10412 let phi = self.dyn_phi_ema.clone();
10414 if let Some(new_active) = router.step(&phi, pos) {
10415 let _ = self.set_active_skill(new_active);
10416 }
10417 }
10418 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10419 })();
10420
10421 let _ = self.set_active_skill(saved_active);
10424 self.dyn_router = Some(router);
10425 self.nll_end();
10426 result
10427 }
10428
10429 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10431 self.clear_sequence_state();
10432 let mut acc = vec![0f32; self.hidden_size];
10433 for (pos, &id) in ids.iter().enumerate() {
10434 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10435 for (a, v) in acc.iter_mut().zip(&h) {
10436 *a += v;
10437 }
10438 }
10439 let n = ids.len().max(1) as f32;
10440 for a in acc.iter_mut() {
10441 *a /= n;
10442 }
10443 self.clear_sequence_state();
10444 acc
10445 }
10446
10447 pub fn probe_phi_span(
10461 &mut self,
10462 ids: &[u32],
10463 layer: usize,
10464 span: std::ops::Range<usize>,
10465 ) -> Vec<f32> {
10466 #[cfg(target_os = "macos")]
10467 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10468 let end = span.end.min(ids.len());
10469 let start = span.start.min(end);
10470 let reset = |p: &mut Self| p.clear_sequence_state();
10471 reset(self);
10472 let mut acc = vec![0f32; self.hidden_size];
10473 for (pos, &id) in ids[..end].iter().enumerate() {
10474 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10475 if pos >= start {
10476 for (a, v) in acc.iter_mut().zip(&h) {
10477 *a += v;
10478 }
10479 }
10480 }
10481 let n = end - start;
10482 if n > 0 {
10483 let n = n as f32;
10484 for a in acc.iter_mut() {
10485 *a /= n;
10486 }
10487 }
10488 reset(self);
10489 acc
10490 }
10491
10492 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10499 #[cfg(target_os = "macos")]
10500 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10501 self.graph_logits = None;
10502 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10503 if let Some(logits) = self.graph_logits.take() {
10504 return logits;
10505 }
10506 inference::rms_norm_into(
10507 &hidden,
10508 &self.weights.final_norm,
10509 self.rms_eps,
10510 self.norm_style,
10511 &mut self.ws.n1,
10512 );
10513 self.lm_head_forward(&self.ws.n1)
10514 }
10515
10516 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10522 self.prefill_batch_masked(ids, start_pos, None)
10523 }
10524
10525 fn prefill_batch_masked(
10531 &mut self,
10532 ids: &[u32],
10533 start_pos: usize,
10534 task_mask: Option<&TaskMask>,
10535 ) -> Vec<f32> {
10536 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10537 }
10538
10539 fn prefill_rows(
10547 &mut self,
10548 ids: &[u32],
10549 pos: usize,
10550 task_mask: Option<&TaskMask>,
10551 ) -> Result<Vec<f32>, String> {
10552 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10553 }
10554
10555 fn prefill_input_rows(
10556 &mut self,
10557 input: PrefillIn<'_>,
10558 pos: usize,
10559 task_mask: Option<&TaskMask>,
10560 ) -> Result<Vec<f32>, String> {
10561 self.mimo_moe_prepare();
10562 let hs = self.hidden_size;
10563 let bk = match input {
10564 PrefillIn::Ids(ids) => ids.len(),
10565 PrefillIn::Hidden(rows) => rows.len() / hs,
10566 };
10567 #[cfg(not(target_os = "macos"))]
10568 if task_mask.is_none()
10569 && !self.o1_active()
10570 && bk > 1
10571 && (self.batch_prefix_prefill()
10572 || (self.verify_exact_moe
10573 && crate::gpu::enabled_here()
10574 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10575 {
10576 let mut hiddens = match input {
10577 PrefillIn::Hidden(rows) => rows.to_vec(),
10578 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10579 };
10580 let positions: Vec<usize> = (pos..pos + bk).collect();
10581 let mut run = 0usize;
10582 match self.try_batch_graph_wgpu_prefix(
10583 &mut hiddens,
10584 &positions,
10585 bk,
10586 None,
10587 Some(&mut run),
10588 ) {
10589 crate::gpu::BatchGraphOutcome::Completed => {
10590 let out = if run < self.num_layers {
10591 self.prefill_batch_span(
10592 PrefillIn::Hidden(&hiddens),
10593 pos,
10594 None,
10595 run,
10596 self.num_layers,
10597 )
10598 } else {
10599 hiddens
10600 };
10601 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10602 Err("MiMo attention graph failed after admission".into())
10603 } else {
10604 Ok(out)
10605 };
10606 }
10607 crate::gpu::BatchGraphOutcome::Failed => {
10608 return Err("batched prefix prefill failed after admission".into());
10609 }
10610 crate::gpu::BatchGraphOutcome::Declined => {
10611 #[cfg(feature = "gpu")]
10613 self.pull_lagging_host_kv(0, self.num_layers, pos);
10614 }
10615 }
10616 }
10617 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10618 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10619 Err("batch tail graph failed after admission".into())
10620 } else {
10621 Ok(out)
10622 }
10623 }
10624
10625 fn prefill_batch_span(
10631 &mut self,
10632 input: PrefillIn<'_>,
10633 start_pos: usize,
10634 task_mask: Option<&TaskMask>,
10635 from: usize,
10636 upto_excl: usize,
10637 ) -> Vec<f32> {
10638 let hs = self.hidden_size;
10639 let b = match input {
10640 PrefillIn::Ids(ids) => ids.len(),
10641 PrefillIn::Hidden(hb) => hb.len() / hs,
10642 };
10643 let upto_excl = upto_excl.min(self.num_layers);
10644 let mut h: Vec<f32>;
10648 let mut h_ready;
10649 match input {
10650 PrefillIn::Ids(_) => {
10651 h = vec![0.0; b * hs];
10652 h_ready = false;
10653 }
10654 PrefillIn::Hidden(hb) => {
10655 h = hb.to_vec();
10656 h_ready = true;
10657 }
10658 }
10659 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10660 if let PrefillIn::Ids(ids) = input {
10661 for (bi, &id) in ids.iter().enumerate() {
10662 let e = me.embed_single(id);
10663 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10664 }
10665 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10666 if let Ok(t) = tp.parse::<usize>() {
10667 if t >= start_pos && t < start_pos + ids.len() {
10668 let bi = t - start_pos;
10669 let row = &h[bi * hs..(bi + 1) * hs];
10670 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10671 eprintln!(
10672 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10673 ids[bi],
10674 row[0],
10675 row[1],
10676 ids.len(),
10677 &ids[..ids.len().min(8)]
10678 );
10679 }
10680 }
10681 }
10682 }
10683 };
10684 let (_nkv, _hd, _rd, eps) = (
10685 self.num_kv_heads,
10686 self.head_dim,
10687 self.rotary_dim,
10688 self.rms_eps,
10689 );
10690 let pool = self.pool.clone();
10691 let norm_style = self.norm_style;
10692 self.mimo_moe_prepare();
10693 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10694
10695 #[cfg(target_os = "macos")]
10696 let mut chunk_skip_until = 0usize;
10697 for li in from..upto_excl {
10698 let _capacity_tail = automatic_gpu_prefix
10699 .filter(|&prefix| {
10700 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10701 })
10702 .map(|_| crate::gpu::enter_cpu_scope());
10703 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10708 let _fast_gemm = self
10709 .proj_gate_sigmoid
10710 .then(crate::gpu::enter_prefill_fast_gemm);
10711 #[cfg(target_os = "macos")]
10717 if task_mask.is_none() {
10718 if li < chunk_skip_until {
10719 continue;
10720 }
10721 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10727 fill_h(&mut h, self);
10728 h_ready = true;
10729 }
10730 let ids_for_embed = match input {
10731 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10732 PrefillIn::Hidden(_) => None,
10733 };
10734 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10735 if end > li {
10736 h_ready = true;
10737 chunk_skip_until = end;
10738 if self.is_loop_end(end - 1) && end < self.num_layers {
10741 for bi in 0..b {
10742 let normed = inference::rms_norm(
10743 &h[bi * hs..(bi + 1) * hs],
10744 &self.weights.final_norm,
10745 eps,
10746 norm_style,
10747 );
10748 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10749 }
10750 }
10751 continue;
10752 }
10753 }
10754 if !h_ready {
10755 fill_h(&mut h, self);
10756 h_ready = true;
10757 }
10758 if task_mask.is_none() && self.verify_exact_moe {
10759 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10760 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10761 crate::gpu::BatchGraphOutcome::Completed => continue,
10762 crate::gpu::BatchGraphOutcome::Failed => return h,
10763 crate::gpu::BatchGraphOutcome::Declined => {}
10764 }
10765 }
10766 #[cfg(feature = "gpu")]
10767 self.pull_lagging_host_kv(li, li + 1, start_pos);
10768 let lw = &self.weights.layers[self.phys_layer(li)];
10769 let t_attn = std::time::Instant::now();
10770 match &lw.attn {
10772 AttnKind::Kda(w) => {
10773 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10775 let mut normed = vec![0.0f32; b * hs];
10776 for bi in 0..b {
10777 inference::rms_norm_into(
10778 &h[bi * hs..(bi + 1) * hs],
10779 &lw.input_norm,
10780 eps,
10781 norm_style,
10782 &mut normed[bi * hs..(bi + 1) * hs],
10783 );
10784 }
10785 let attn = crate::linear_core::kda_forward_batch(
10786 &normed,
10787 b,
10788 w,
10789 &cfg,
10790 &mut self.kv_cache.layers[li].linear_state,
10791 pool.as_deref(),
10792 );
10793 for (dst, &a) in h.iter_mut().zip(&attn) {
10794 *dst += a;
10795 }
10796 }
10797 AttnKind::LinearGdn(w) => {
10798 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10800 let mut normed = vec![0.0f32; b * hs];
10801 for bi in 0..b {
10802 let r = inference::rms_norm(
10803 &h[bi * hs..(bi + 1) * hs],
10804 &lw.input_norm,
10805 eps,
10806 norm_style,
10807 );
10808 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10809 }
10810 let attn = crate::linear_core::gdn_forward_batch(
10811 &normed,
10812 b,
10813 w,
10814 &cfg,
10815 &mut self.kv_cache.layers[li].linear_state,
10816 pool.as_deref(),
10817 );
10818 for (dst, &a) in h.iter_mut().zip(&attn) {
10819 *dst += a;
10820 }
10821 }
10822 AttnKind::ShortConv(w) => {
10823 let cfg = self
10826 .short_conv_cfg
10827 .expect("short-conv layer without short_conv_cfg");
10828 let mut normed = vec![0.0f32; b * hs];
10829 for bi in 0..b {
10830 inference::rms_norm_into(
10831 &h[bi * hs..(bi + 1) * hs],
10832 &lw.input_norm,
10833 eps,
10834 norm_style,
10835 &mut normed[bi * hs..(bi + 1) * hs],
10836 );
10837 }
10838 let attn = short_conv_forward_batch(
10839 &normed,
10840 b,
10841 w,
10842 &cfg,
10843 &mut self.kv_cache.layers[li].linear_state,
10844 pool.as_deref(),
10845 );
10846 for (dst, &a) in h.iter_mut().zip(&attn) {
10847 *dst += a;
10848 }
10849 }
10850 AttnKind::Mla(w) => {
10851 let inv_freq_l = self.layer_inv_freq(li);
10854 let rs = self.layer_rope_scale(li);
10855 let mut normed = vec![0.0f32; hs];
10856 for bi in 0..b {
10857 inference::rms_norm_into(
10858 &h[bi * hs..(bi + 1) * hs],
10859 &lw.input_norm,
10860 eps,
10861 norm_style,
10862 &mut normed,
10863 );
10864 let ao = mla_attention(
10865 w,
10866 &normed,
10867 &mut self.kv_cache.layers[li],
10868 start_pos + bi,
10869 &inv_freq_l,
10870 rs,
10871 eps,
10872 pool.as_deref(),
10873 );
10874 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10875 *dst += a;
10876 }
10877 }
10878 }
10879 AttnKind::Full {
10880 wq,
10881 wk,
10882 wv,
10883 wo,
10884 q_norm,
10885 k_norm,
10886 output_gate,
10887 softplus_gate,
10888 bias,
10889 } => {
10890 let mut normed = vec![0.0f32; b * hs];
10894 for bi in 0..b {
10895 inference::rms_norm_into(
10896 &h[bi * hs..(bi + 1) * hs],
10897 &lw.input_norm,
10898 eps,
10899 norm_style,
10900 &mut normed[bi * hs..(bi + 1) * hs],
10901 );
10902 }
10903 let inv_freq_l = self.layer_inv_freq(li);
10904 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10905 let cfg = QwenAttnCfg {
10906 num_heads: self.layer_num_heads(li),
10907 num_kv_heads: nkv_l,
10908 head_dim: hd_l,
10909 hidden_size: hs,
10910 position: start_pos,
10911 inv_freq: &inv_freq_l,
10912 rotary_dim: rd_l,
10913 scale: self.attn_scale,
10914 softcap: self.attn_softcap,
10915 window: self.layer_window(li),
10916 v_norm: self.attn_v_norm,
10917 qk_norm_after_rope: self.qk_norm_after_rope,
10918 gate_sigmoid: self.proj_gate_sigmoid,
10919 q_norm: q_norm.as_deref(),
10920 k_norm: k_norm.as_deref(),
10921 output_gate: *output_gate,
10922 softplus_gate: softplus_gate
10923 .as_ref()
10924 .map(|(gate, per_head)| (gate, *per_head)),
10925 rope_scale: self.layer_rope_scale(li),
10926 bias: bias
10927 .as_ref()
10928 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10929 rms_eps: eps,
10930 norm_style,
10931 pool: pool.as_deref(),
10932 v_head_dim: self.layer_v_dim(li),
10933 };
10934 #[cfg(feature = "gpu")]
10935 let _mirror = self
10936 .prefill_mirror_target(li)
10937 .map(crate::gpu::enter_prefill_mirror);
10938 let mut attn = attention::qwen_attention_batch(
10939 &normed,
10940 b,
10941 wq,
10942 wk,
10943 wv,
10944 wo,
10945 &mut self.kv_cache.layers[li],
10946 &cfg,
10947 );
10948 if let Some(w) = &lw.attn_out_norm {
10949 for bi in 0..b {
10950 inference::rms_norm_into(
10951 &attn[bi * hs..(bi + 1) * hs],
10952 w,
10953 eps,
10954 norm_style,
10955 &mut normed[bi * hs..(bi + 1) * hs],
10956 );
10957 }
10958 attn.copy_from_slice(&normed);
10959 }
10960 for (dst, &a) in h.iter_mut().zip(&attn) {
10961 *dst += a;
10962 }
10963 }
10964 AttnKind::Bounded(w) => {
10965 let mut normed = vec![0.0f32; b * hs];
10968 for bi in 0..b {
10969 inference::rms_norm_into(
10970 &h[bi * hs..(bi + 1) * hs],
10971 &lw.input_norm,
10972 eps,
10973 norm_style,
10974 &mut normed[bi * hs..(bi + 1) * hs],
10975 );
10976 }
10977 let rope = self
10978 .bounded_rope
10979 .clone()
10980 .expect("bounded layer without an installed rotation table");
10981 let cfg = crate::bounded::BoundedAttnCfg {
10982 num_heads: self.num_heads,
10983 num_kv_heads: self.num_kv_heads,
10984 head_dim: self.head_dim,
10985 hidden_size: hs,
10986 scale: self.attn_scale,
10987 rope: &rope,
10988 pool: pool.as_deref(),
10989 };
10990 let mut attn = crate::bounded::bounded_attention_batch(
10991 &normed,
10992 b,
10993 w,
10994 &mut self.kv_cache.layers[li],
10995 &cfg,
10996 );
10997 if let Some(wn) = &lw.attn_out_norm {
10998 for bi in 0..b {
10999 inference::rms_norm_into(
11000 &attn[bi * hs..(bi + 1) * hs],
11001 wn,
11002 eps,
11003 norm_style,
11004 &mut normed[bi * hs..(bi + 1) * hs],
11005 );
11006 }
11007 attn.copy_from_slice(&normed);
11008 }
11009 for (dst, &a) in h.iter_mut().zip(&attn) {
11010 *dst += a;
11011 }
11012 attention::recycle_buf(&mut attn);
11013 }
11014 AttnKind::Linear(w) => {
11015 for bi in 0..b {
11016 let normed = inference::rms_norm(
11017 &h[bi * hs..(bi + 1) * hs],
11018 &lw.input_norm,
11019 eps,
11020 norm_style,
11021 );
11022 vmf_phase_forward(
11023 &normed,
11024 w,
11025 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11026 &mut self.kv_cache.layers[li].linear_state,
11027 pool.as_deref(),
11028 )
11029 .iter()
11030 .enumerate()
11031 .for_each(|(i, &a)| h[bi * hs + i] += a);
11032 }
11033 }
11034 }
11035
11036 let lw = &self.weights.layers[self.phys_layer(li)];
11038 let mut post = vec![0.0f32; b * hs];
11039 for bi in 0..b {
11040 let r =
11041 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11042 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11043 }
11044 let mask_row = task_mask
11047 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11048 .and_then(|m| m.ffn_masks.get(li))
11049 .map(|v| v.as_slice());
11050 let attn_ns = t_attn.elapsed().as_nanos() as u64;
11051 let t_ffn = std::time::Instant::now();
11052 let mut ffn = match &lw.ffn {
11053 FfnKind::Dense(d) if !d.segs.is_empty() => {
11054 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11055 }
11056 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11057 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11058 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11059 }
11060 FfnKind::Moe(m) if self.verify_exact_moe => {
11061 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11062 }
11063 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11066 let before = m.stats.borrow().clone();
11067 let out = crate::gpu::cpu_scope(|| {
11068 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11069 });
11070 self.mimo_moe.prime(li, m, &before);
11071 out
11072 }
11073 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11074 FfnKind::DenseMoe(dm) => {
11077 let mut out = vec![0.0f32; b * hs];
11078 for bi in 0..b {
11079 let r = dense_moe_ffn(
11080 dm,
11081 &post[bi * hs..(bi + 1) * hs],
11082 &h[bi * hs..(bi + 1) * hs],
11083 eps,
11084 norm_style,
11085 pool.as_deref(),
11086 );
11087 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11088 }
11089 out
11090 }
11091 };
11092 if prefill_prof_on() {
11093 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11094 PREFILL_SPLIT[1].fetch_add(
11095 t_ffn.elapsed().as_nanos() as u64,
11096 std::sync::atomic::Ordering::Relaxed,
11097 );
11098 }
11099 if let Some(w) = &lw.ffn_out_norm {
11100 for bi in 0..b {
11101 inference::rms_norm_into(
11102 &ffn[bi * hs..(bi + 1) * hs],
11103 w,
11104 eps,
11105 norm_style,
11106 &mut post[bi * hs..(bi + 1) * hs],
11107 );
11108 }
11109 ffn.copy_from_slice(&post);
11110 }
11111 for (dst, &f) in h.iter_mut().zip(&ffn) {
11112 *dst += f;
11113 }
11114 if let Some(sc) = lw.layer_scale {
11115 for v in h.iter_mut() {
11116 *v *= sc;
11117 }
11118 }
11119 if self.layer_dump.is_some() {
11121 for bi in 0..b {
11122 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11123 }
11124 }
11125 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11126 if let Ok(t) = tp.parse::<usize>() {
11127 if t >= start_pos && t < start_pos + b {
11128 let bi = t - start_pos;
11129 let row = &h[bi * hs..(bi + 1) * hs];
11130 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11131 eprintln!(
11132 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11133 row[0], row[1]
11134 );
11135 }
11136 }
11137 }
11138 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11142 let row = &h[(b - 1) * hs..b * hs];
11143 let rms =
11144 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11145 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11146 eprintln!(
11147 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11148 match &self.weights.layers[self.phys_layer(li)].attn {
11149 AttnKind::LinearGdn(_) => "gdn",
11150 AttnKind::Linear(_) => "vmf",
11151 AttnKind::ShortConv(_) => "conv",
11152 _ => "attn",
11153 },
11154 match &lw.ffn {
11155 FfnKind::Moe(_) => "moe",
11156 FfnKind::Dense(_) => "dense",
11157 FfnKind::DenseMoe(_) => "dense+moe",
11158 },
11159 );
11160 }
11161 if self.is_loop_end(li) && li + 1 < self.num_layers {
11163 for bi in 0..b {
11164 let normed = inference::rms_norm(
11165 &h[bi * hs..(bi + 1) * hs],
11166 &self.weights.final_norm,
11167 eps,
11168 norm_style,
11169 );
11170 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11171 }
11172 }
11173 if std::env::var("CMF_TRACE_H").is_ok() {
11174 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11175 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11176 eprintln!(
11177 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11178 lw.layer_scale
11179 );
11180 }
11181 }
11182 crate::gpu::set_layer(-1); if prefill_prof_on() {
11184 let ms = |a: &std::sync::atomic::AtomicU64| {
11185 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11186 };
11187 let sp = &attention::ATTN_SPLIT;
11188 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11189 eprintln!(
11190 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11191 host gate {:.1}], host loop {:.1}, attend {:.1} \
11192 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11193 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11194 ms(&PREFILL_SPLIT[0]),
11195 ms(&sp[0]),
11196 qkv_enq,
11197 qkv_wait,
11198 ms(&sp[6]),
11199 ms(&sp[1]),
11200 ms(&sp[2]),
11201 ms(&sp[4]),
11202 ms(&sp[5]),
11203 ca_wait,
11204 ms(&sp[3]),
11205 ms(&PREFILL_SPLIT[1]),
11206 );
11207 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11208 eprintln!(
11209 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11210 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11211 fallbacks (cumulative)",
11212 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11213 );
11214 }
11215 self.o1_progress();
11220 self.swa_trim_tails();
11222 h
11223 }
11224
11225 fn embed_single(&self, id: u32) -> Vec<f32> {
11227 let mut out = vec![0.0f32; self.hidden_size];
11228 if (id as usize) < self.weights.embed_tokens.rows() {
11229 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11230 }
11231 if self.embed_multiplier != 1.0 {
11232 for v in out.iter_mut() {
11233 *v *= self.embed_multiplier;
11234 }
11235 }
11236 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11240 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11241 v[0] = id as f32;
11242 return v;
11243 }
11244 if let Some(b) = &self.g4e {
11248 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11249 }
11250 if let Some(b) = &self.g3n {
11251 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11252 }
11253 out
11254 }
11255
11256 #[cfg(target_os = "macos")]
11262 fn chunk_run_gpu(
11263 &mut self,
11264 li0: usize,
11265 h: &mut [f32],
11266 b: usize,
11267 pos0: usize,
11268 embed_ids: Option<&[u32]>,
11269 cap: usize,
11270 ) -> usize {
11271 let gemma4 =
11278 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11279 if !crate::gpu::enabled_here()
11280 || std::env::var("CMF_GPU_CHUNK")
11281 .map(|v| v == "0")
11282 .unwrap_or(false)
11283 || b < self.chunk_min_rows
11284 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11287 || (self.global_attn.is_some() && !gemma4)
11288 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11292 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11295 || self.o1_active()
11298 || (self.attn_v_norm && !gemma4)
11299 || !self.metal_rope_scale_ok()
11300 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11301 {
11302 return li0;
11303 }
11304 let Some(model) = self.model.clone() else {
11305 return li0;
11306 };
11307 let (nh, nkv, hd, hs) = (
11308 self.num_heads,
11309 self.num_kv_heads,
11310 self.head_dim,
11311 self.hidden_size,
11312 );
11313 let loop_end = if self.loop_final_norm {
11317 ((li0 / self.physical_layers) + 1) * self.physical_layers
11318 } else {
11319 self.num_layers
11320 };
11321 let run_end = self.num_layers.min(loop_end).min(cap);
11322 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11327 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11328 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11329 let mut stored_at: Vec<usize> = Vec::new();
11330 for li in li0..run_end {
11331 let lw = &self.weights.layers[self.phys_layer(li)];
11332 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11333 && !gemma4
11334 {
11335 break;
11336 }
11337 let AttnKind::Full {
11338 wq,
11339 wk,
11340 wv,
11341 wo,
11342 q_norm,
11343 k_norm,
11344 output_gate: false,
11345 softplus_gate,
11346 bias,
11347 } = &lw.attn
11348 else {
11349 break;
11350 };
11351 let head_gate = match softplus_gate {
11353 None => None,
11354 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11355 Some((d, r, c)) if r == nh && c == hs => Some(d),
11356 _ => break,
11357 },
11358 Some(_) => break,
11359 };
11360 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11364 FfnKind::Dense(d)
11365 if (matches!(d.act, Act::Silu | Act::Gelu)
11366 || (gemma4 && d.act == Act::GeluTanh))
11367 && d.segs.is_empty() =>
11368 {
11369 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11370 }
11371 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11372 Some((router, experts, q8)) => {
11373 let e0 = &m.experts[0];
11374 let route = Box::new(move |lg: &[f32]| {
11375 let (idx, p, wsum) = moe_route(lg, m, None);
11377 let mut st = m.stats.borrow_mut();
11378 if st.len() < m.experts.len() {
11379 st.resize(m.experts.len(), 0);
11380 }
11381 for &e in &idx {
11382 st[e] += 1;
11383 }
11384 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11385 (idx, w)
11386 });
11387 (
11388 &e0.gate_proj,
11389 &e0.up_proj,
11390 &e0.down_proj,
11391 Act::Silu,
11392 Some(crate::gpu_metal::ChunkMoe {
11393 router,
11394 experts,
11395 q8,
11396 route,
11397 }),
11398 )
11399 }
11400 None => break,
11401 },
11402 _ => break,
11403 };
11404 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11410 t.q8_row_parts()
11411 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11412 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11413 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11414 }
11415 fn cf(t: &QTensor) -> &[f32] {
11416 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11417 }
11418 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11419 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
11420 if moe.is_none()
11430 && !gemma4
11431 && ffn_act != Act::Gelu
11432 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
11433 {
11434 break;
11435 }
11436 let parts = (
11437 cw(wq),
11438 cw(wk),
11439 cw(wv),
11440 cw(wo),
11441 cw(gate_t),
11442 cw(up_t),
11443 cw(down_t),
11444 );
11445 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11446 else {
11447 break;
11448 };
11449 let layer = &self.kv_cache.layers[li];
11450 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11451 break;
11452 }
11453 debug_assert!(
11455 layer.base() == 0
11456 || self
11457 .layer_window(li)
11458 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11459 );
11460 stored_at.push(layer.head_len(0));
11461 let (nkv, hd) = if gemma4 {
11464 let (a, bb, _) = self.layer_geom(li);
11465 (a, bb)
11466 } else {
11467 (nkv, hd)
11468 };
11469 layers.push(crate::gpu_metal::ChunkLayer {
11470 model: &model,
11471 kv_id: self.graph_kv_id,
11472 layer: li,
11473 wq: pq,
11474 wk: pk,
11475 wv: pv,
11476 wo: po,
11477 gate: pg,
11478 up: pu,
11479 down: pd,
11480 input_norm: &lw.input_norm,
11481 post_norm: &lw.post_norm,
11482 bias: bias
11483 .as_ref()
11484 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11485 q_norm: q_norm.as_deref(),
11486 k_norm: k_norm.as_deref(),
11487 inv_freq: &tables[li - li0],
11488 rope_scale: self.layer_rope_scale(li),
11489 rd: self.layer_geom(li).2,
11490 nh,
11491 nkv,
11492 hd,
11493 hs,
11494 inter: gate_t.rows(),
11495 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11496 late_qk_norm: self.qk_norm_after_rope,
11497 eps: self.rms_eps as f32,
11498 window: self.layer_window(li),
11499 head_gate,
11500 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11501 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11502 v_norm: self.attn_v_norm,
11503 scale: self.attn_scale,
11504 attn_out_norm: lw.attn_out_norm.as_deref(),
11505 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11506 layer_scale: lw.layer_scale,
11507 }),
11508 col,
11509 ffn_col: if moe.is_none() && !gemma4 {
11512 ffn_col
11513 } else {
11514 [&[][..]; 3]
11515 },
11516 moe,
11517 });
11518 }
11519 if layers.is_empty() {
11520 return li0;
11521 }
11522 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11524 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11525 .iter()
11526 .zip(&rows_kv)
11527 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11528 .collect();
11529 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11530 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11531 let li = layers[i].layer;
11532 let layer = &self.kv_cache.layers[li];
11533 let nkv = rows_kv[i].0;
11534 io.push(crate::gpu_metal::ChunkIo {
11535 cpu_stored: stored_at[i],
11536 cpu_gen: layer.generation(),
11537 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11538 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11539 out_k: ok,
11540 out_v: ov,
11541 imp: oi,
11542 });
11543 }
11544 let n_run = layers.len();
11545 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11546 let ep = embed_ids.and_then(|ids| {
11549 self.weights
11550 .embed_tokens
11551 .q8_row_parts()
11552 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11553 idx,
11554 rows,
11555 row_scale: rs,
11556 ids,
11557 mult: self.embed_multiplier,
11558 })
11559 });
11560 if embed_ids.is_some() && ep.is_none() {
11561 return li0;
11562 }
11563 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11564 return li0;
11565 }
11566 drop(io);
11567 drop(layers);
11568 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11571 let li = li0 + i;
11572 let row = rows_kv[i].1;
11573 let layer = &mut self.kv_cache.layers[li];
11574 for bi in 0..b {
11575 layer.append(
11576 &ok[bi * row..(bi + 1) * row],
11577 &ov[bi * row..(bi + 1) * row],
11578 &[],
11579 );
11580 }
11581 layer.accumulate_imp(oi);
11582 }
11583 last
11584 }
11585
11586 fn layer_is_local(&self, li: usize) -> bool {
11589 if let Some(layers) = &self.sliding_layers {
11590 return layers.get(li).copied().unwrap_or(false);
11591 }
11592 match self.swa {
11593 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11594 None => false,
11595 }
11596 }
11597
11598 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11601 if self.layer_is_local(li) {
11602 if let Some(f) = &self.inv_freq_local {
11603 return f.clone();
11604 }
11605 } else if let Some(f) = &self.inv_freq_global {
11606 return f.clone();
11607 }
11608 self.inv_freq.clone()
11609 }
11610
11611 fn layer_window(&self, li: usize) -> Option<usize> {
11613 self.swa
11614 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11615 }
11616
11617 fn swa_trim_tails(&mut self) {
11626 let Some((slack, align)) = self.swa_trim else {
11627 return;
11628 };
11629 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11630 return;
11631 }
11632 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11633 if let Some(w) = self.layer_window(li) {
11634 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
11635 }
11636 }
11637 }
11638
11639 fn layer_num_heads(&self, li: usize) -> usize {
11640 self.attention_heads_per_layer
11641 .as_ref()
11642 .and_then(|v| v.get(li).copied())
11643 .unwrap_or(self.num_heads)
11644 }
11645
11646 fn layer_rope_scale(&self, li: usize) -> f32 {
11647 if self.layer_is_local(li) {
11648 self.rope_scale_local
11649 } else {
11650 self.rope_scale
11651 }
11652 }
11653
11654 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11657 if !self.layer_is_local(li) {
11658 if let Some((ghd, gkv)) = self.global_attn {
11659 return (gkv, ghd, ghd);
11660 }
11661 }
11662 let rd = if self.layer_is_local(li) {
11663 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11664 } else {
11665 self.rotary_dim
11666 };
11667 let rd = if self
11669 .no_rope_layers
11670 .as_ref()
11671 .and_then(|v| v.get(li))
11672 .copied()
11673 == Some(true)
11674 {
11675 0
11676 } else {
11677 rd
11678 };
11679 (self.layer_num_kv_heads(li), self.head_dim, rd)
11680 }
11681
11682 fn layer_num_kv_heads(&self, li: usize) -> usize {
11685 self.kv_heads_per_layer
11686 .as_ref()
11687 .and_then(|v| v.get(self.phys_layer(li)).copied())
11688 .unwrap_or(self.num_kv_heads)
11689 }
11690
11691 fn layer_v_dim(&self, li: usize) -> usize {
11693 let (_, hd, _) = self.layer_geom(li);
11694 self.v_head_dim.unwrap_or(hd).min(hd)
11695 }
11696
11697 pub fn set_attn_geometry(
11706 &mut self,
11707 kv_heads_per_layer: Option<Vec<usize>>,
11708 v_head_dim: Option<usize>,
11709 ) -> Result<(), String> {
11710 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11711 if self.global_attn.is_some() {
11712 return Err(
11713 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11714 attention geometry"
11715 .into(),
11716 );
11717 }
11718 if self
11719 .weights
11720 .layers
11721 .iter()
11722 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11723 {
11724 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11725 }
11726 }
11727 if let Some(vd) = v_head_dim {
11728 if vd == 0 || vd > self.head_dim {
11729 return Err(format!(
11730 "v_head_dim {vd} must be in 1..={} (head_dim)",
11731 self.head_dim
11732 ));
11733 }
11734 }
11735 if let Some(v) = &kv_heads_per_layer {
11736 if v.len() != self.physical_layers {
11737 return Err(format!(
11738 "kv_heads_per_layer has {} entries, expected {} layers",
11739 v.len(),
11740 self.physical_layers
11741 ));
11742 }
11743 for (li, &nkv) in v.iter().enumerate() {
11744 let is_attn = matches!(
11745 self.weights.layers.get(li).map(|lw| &lw.attn),
11746 Some(AttnKind::Full { .. }) | None
11747 );
11748 if !is_attn {
11749 continue;
11750 }
11751 let nh = self
11752 .attention_heads_per_layer
11753 .as_ref()
11754 .and_then(|h| h.get(li).copied())
11755 .unwrap_or(self.num_heads);
11756 if nkv == 0 || nh % nkv != 0 {
11757 return Err(format!(
11758 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11759 ));
11760 }
11761 }
11762 }
11763 self.kv_heads_per_layer = kv_heads_per_layer;
11764 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11765 if self.kv_heads_per_layer.is_some() {
11766 for li in 0..self.kv_cache.layers.len() {
11767 let full = matches!(
11768 self.weights
11769 .layers
11770 .get(self.phys_layer(li))
11771 .map(|lw| &lw.attn),
11772 Some(AttnKind::Full { .. })
11773 );
11774 let nkv = self.layer_num_kv_heads(li);
11775 let cache = &self.kv_cache.layers[li];
11776 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11777 let sinks = cache.sinks.clone();
11778 self.kv_cache.layers[li] =
11779 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11780 self.kv_cache.layers[li].sinks = sinks;
11781 }
11782 }
11783 }
11784 Ok(())
11785 }
11786
11787 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11791 let Some(lw) = self.weights.layers.get(phys) else {
11792 return Err(format!("sinks for layer {phys}: no such layer"));
11793 };
11794 if !matches!(lw.attn, AttnKind::Full { .. }) {
11795 return Err(format!(
11796 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11797 ));
11798 }
11799 let nh = self
11800 .attention_heads_per_layer
11801 .as_ref()
11802 .and_then(|h| h.get(phys).copied())
11803 .unwrap_or(self.num_heads);
11804 if sinks.len() != nh {
11805 return Err(format!(
11806 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11807 sinks.len()
11808 ));
11809 }
11810 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11811 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11812 }
11813 for li in 0..self.kv_cache.layers.len() {
11814 if self.phys_layer(li) == phys {
11815 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11816 }
11817 }
11818 Ok(())
11819 }
11820
11821 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11831 if self.kv_heads_per_layer.is_some() {
11832 return Some("per-layer KV head counts");
11833 }
11834 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11835 return Some("V heads narrower than Q/K heads");
11836 }
11837 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11838 return Some("learned attention sinks");
11839 }
11840 if self.swa.is_some() || self.sliding_layers.is_some() {
11841 return Some("sliding-window layers");
11842 }
11843 None
11844 }
11845
11846 #[cfg(target_os = "macos")]
11854 fn metal_rope_scale_ok(&self) -> bool {
11855 !(self.qk_norm_after_rope
11856 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11857 }
11858
11859 #[cfg(target_os = "macos")]
11876 fn metal_graph_swa(&self) -> bool {
11877 (self.swa.is_some() || self.sliding_layers.is_some())
11878 && self.attention_heads_per_layer.is_none()
11879 && !self.attn_v_norm
11880 && self.attn_softcap == 0.0
11881 && self.kv_heads_per_layer.is_none()
11882 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11883 && self.global_attn.is_none()
11884 && self.inv_freq_global.is_none()
11885 && self.metal_rope_scale_ok()
11886 && !(0..self.num_layers).any(|li| {
11887 self.layer_is_local(li)
11888 && self.inv_freq_local.is_none()
11889 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11890 })
11891 }
11892
11893 #[cfg(target_os = "macos")]
11901 fn metal_gemma4(&self) -> bool {
11902 crate::gpu_metal::gemma4::enabled()
11903 && (self.global_attn.is_some() || self.attn_v_norm)
11904 && self.attention_heads_per_layer.is_none()
11905 && self.kv_heads_per_layer.is_none()
11906 && self.v_head_dim.is_none()
11907 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11908 && self.attn_softcap == 0.0
11909 && !self.qk_norm_after_rope
11910 && self.metal_rope_scale_ok()
11911 && self
11912 .weights
11913 .layers
11914 .iter()
11915 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11916 && !(0..self.num_layers).any(|li| {
11917 self.layer_is_local(li)
11918 && self.inv_freq_local.is_none()
11919 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11920 })
11921 }
11922
11923 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11930 let geometry_reason = self.graph_attn_decline_reason();
11934 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11935 if has_scaled_rope && geometry_reason.is_none() {
11936 return Some("scaled RoPE without per-layer geometry");
11937 }
11938 if has_scaled_rope && self.qk_norm_after_rope {
11943 return Some("scaled RoPE with post-RoPE Q/K norm");
11944 }
11945 geometry_reason?;
11946 if self.attention_heads_per_layer.is_some() {
11952 return Some("per-layer Q head counts with per-layer geometry");
11953 }
11954 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11958 if !gemma4
11959 && self.weights.layers.iter().any(|lw| {
11960 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11961 })
11962 {
11963 return Some("sandwich norms / layer scale with per-layer geometry");
11964 }
11965 if self.weights.layers.iter().any(|lw| {
11969 matches!(
11970 &lw.attn,
11971 AttnKind::Full {
11972 output_gate: true,
11973 ..
11974 }
11975 )
11976 }) && self.v_head_dim.is_some()
11977 {
11978 return Some("gated attention with V narrower than K");
11979 }
11980 if (0..self.num_layers).any(|li| {
11981 self.layer_is_local(li)
11982 && self.inv_freq_local.is_none()
11983 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11984 }) {
11985 return Some("local rotary width without a local RoPE table");
11986 }
11987 None
11988 }
11989
11990 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11994 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11997 return None;
11998 }
11999 if self.global_attn.is_some() {
12000 return Some("per-layer head width (Gemma-4 global layers)");
12001 }
12002 if self.attn_v_norm {
12003 return Some("V norm");
12004 }
12005 if self.weights.layers.iter().any(|lw| {
12006 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12007 }) {
12008 return Some("sandwich norms / layer scale");
12009 }
12010 None
12011 }
12012
12013 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
12018 self.graph_attn_decline_reason()?;
12019 let (nkv, hd, rd) = self.layer_geom(li);
12020 let invf: &[f32] = if self.layer_is_local(li) {
12021 match &self.inv_freq_local {
12022 Some(f) => f.as_slice(),
12023 None => self.inv_freq.as_slice(),
12024 }
12025 } else {
12026 match &self.inv_freq_global {
12027 Some(f) => f.as_slice(),
12028 None => self.inv_freq.as_slice(),
12029 }
12030 };
12031 Some(crate::gpu::GraphAttnGeom {
12032 nkv,
12033 hd,
12034 dv: self.layer_v_dim(li),
12035 rd,
12036 invf,
12037 rope_scale: self.layer_rope_scale(li),
12038 window: self.layer_window(li),
12039 sink: self.kv_cache.layers[li].sinks.as_deref(),
12040 v_norm: self.attn_v_norm,
12041 })
12042 }
12043
12044 #[cfg(feature = "gpu")]
12056 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12057 if !self.proj_gate_sigmoid
12058 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12059 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12060 || self.graph_refused()
12061 || self.o1_active()
12062 || self.attn_softcap > 0.0
12063 || self.wgpu_graph_attn_decline().is_some()
12064 {
12065 return None;
12066 }
12067 let g = self.graph_attn_geom(li)?;
12068 let (nkv, hd, _) = self.layer_geom(li);
12069 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12071 return None;
12072 }
12073 Some(crate::gpu::PrefillMirror {
12074 kv_id: self.graph_kv_id,
12075 layer: li,
12076 limit: self.kv_cache.max_seq_len,
12077 })
12078 }
12079
12080 #[cfg(feature = "gpu")]
12089 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12090 let kv_id = self.graph_kv_id;
12091 for li in from..upto.min(self.num_layers) {
12092 if !matches!(
12093 self.weights.layers[self.phys_layer(li)].attn,
12094 AttnKind::Full { .. }
12095 ) {
12096 continue;
12097 }
12098 let host = self.kv_cache.layers[li].pos_len();
12100 if host >= position {
12101 continue;
12102 }
12103 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12104 continue;
12105 };
12106 let to = dev.min(position);
12107 if to <= host {
12108 continue;
12109 }
12110 let (nkv, hd) = {
12111 let c = &self.kv_cache.layers[li];
12112 (c.num_kv_heads, c.head_dim)
12113 };
12114 let Some((k, v, first_valid)) =
12115 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
12116 else {
12117 continue;
12118 };
12119 let need_from = match self.layer_window(li) {
12122 Some(w) => host.max((position + 1).saturating_sub(w)),
12123 None => host,
12124 };
12125 if first_valid > need_from {
12126 tracing::warn!(
12127 "layer {li}: device KV rows {host}..{to} no longer resident \
12128 (from {first_valid}); host attention will miss them"
12129 );
12130 }
12131 let row = nkv * hd;
12132 let cache = &mut self.kv_cache.layers[li];
12133 for p in 0..to - host {
12134 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12135 }
12136 }
12137 }
12138
12139 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12142 let mut seen = self.graph_declines.borrow_mut();
12143 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12144 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12145 seen.push((site, reason));
12146 }
12147 }
12148
12149 pub fn graph_declines(&self) -> Vec<String> {
12152 self.graph_declines
12153 .borrow()
12154 .iter()
12155 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12156 .collect()
12157 }
12158
12159 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12168 let Some(dir) = &self.layer_dump else {
12169 return;
12170 };
12171 let mut bytes = Vec::with_capacity(row.len() * 4);
12172 for v in row {
12173 bytes.extend_from_slice(&v.to_le_bytes());
12174 }
12175 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12176 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12177 use std::sync::atomic::{AtomicBool, Ordering};
12178 static SAID: AtomicBool = AtomicBool::new(false);
12179 if !SAID.swap(true, Ordering::Relaxed) {
12180 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12181 }
12182 }
12183 }
12184
12185 fn mimo_moe_prepare(&mut self) {
12188 if !self.mimo_moe.is_undecided() {
12189 return;
12190 }
12191 let slot = {
12192 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12193 .filter_map(
12194 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12195 FfnKind::Moe(m) => Some((li, m)),
12196 _ => None,
12197 },
12198 )
12199 .collect();
12200 if layers.is_empty()
12203 || self.physical_layers != self.num_layers
12204 || self.gpu_plan.is_some()
12205 {
12206 crate::mimo_moe::Slot::Off
12207 } else {
12208 let graph_prefix =
12212 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12213 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12214 }
12215 };
12216 self.mimo_moe = slot;
12217 }
12218
12219 #[cfg(test)]
12220 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12221 self.graph_kv_id
12222 }
12223
12224 pub(crate) fn mimo_graph_layer_rows(
12228 &mut self,
12229 li: usize,
12230 h: &mut [f32],
12231 positions: &[usize],
12232 ) -> crate::gpu::BatchGraphOutcome {
12233 use crate::gpu::BatchGraphOutcome as Out;
12234 let b = positions.len();
12235 if !(1..=4).contains(&b)
12236 || h.len() != b * self.hidden_size
12237 || !self.mimo_moe.is_dynamic(li, true)
12238 || !crate::gpu::enabled_here()
12239 || !crate::gpu::wgpu_active()
12240 || self.o1_active()
12241 || self.physical_layers != self.num_layers
12242 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12246 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12247 || self.wgpu_graph_attn_decline().is_some()
12248 {
12249 return Out::Declined;
12250 }
12251 let attn_started = std::time::Instant::now();
12252 let outcome = {
12253 let lw = &self.weights.layers[li];
12254 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12255 return Out::Declined;
12256 }
12257 let FfnKind::Moe(m) = &lw.ffn else {
12258 return Out::Declined;
12259 };
12260 let AttnKind::Full {
12261 wq,
12262 wk,
12263 wv,
12264 wo,
12265 q_norm,
12266 k_norm,
12267 output_gate,
12268 softplus_gate,
12269 bias,
12270 } = &lw.attn
12271 else {
12272 return Out::Declined;
12273 };
12274 if *output_gate || softplus_gate.is_some() {
12275 return Out::Declined;
12276 }
12277 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12278 m.experts
12279 .first()?
12280 .gate_proj
12281 .mapped_q4tp()
12282 .map(|(m, _)| m.clone())
12283 }) else {
12284 return Out::Declined;
12285 };
12286 fn gw<'a>(
12287 t: &'a QTensor,
12288 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12289 ) -> Option<crate::gpu::GraphW<'a>> {
12290 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12291 if m.uid() != owner.uid() || t.has_prism_contract() {
12292 return None;
12293 }
12294 return Some(crate::gpu::GraphW {
12295 idx,
12296 kind,
12297 row_scale: rs,
12298 data: &[],
12299 prism: crate::gpu::GraphPrismOp::None,
12300 affine: false,
12301 });
12302 }
12303 t.as_f32().map(|data| crate::gpu::GraphW {
12304 idx: 0,
12305 kind: 4,
12306 row_scale: &[],
12307 data,
12308 prism: crate::gpu::GraphPrismOp::None,
12309 affine: false,
12310 })
12311 }
12312 let (Some(q), Some(k), Some(v), Some(o)) = (
12313 gw(wq, &model),
12314 gw(wk, &model),
12315 gw(wv, &model),
12316 gw(wo, &model),
12317 ) else {
12318 return Out::Declined;
12319 };
12320 let layer = crate::gpu::GraphLayer {
12321 input_norm: &lw.input_norm,
12322 post_norm: &lw.post_norm,
12323 attn_out_norm: None,
12324 ffn_out_norm: None,
12325 layer_scale: None,
12326 ffn: crate::gpu::GraphFfn::AttentionOnly,
12327 attn: crate::gpu::GraphAttn::Full {
12328 wq: q,
12329 wk: k,
12330 wv: v,
12331 wo: o,
12332 q_norm: q_norm.as_deref(),
12333 k_norm: k_norm.as_deref(),
12334 late_qk_norm: self.qk_norm_after_rope,
12335 bias: bias
12336 .as_ref()
12337 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12338 output_gate: false,
12339 cpu_k: self.kv_cache.layers[li].k_heads(),
12340 cpu_v: self.kv_cache.layers[li].v_heads(),
12341 cpu_base: self.kv_cache.layers[li].base(),
12342 geom: self.graph_attn_geom(li),
12343 head_gate: None,
12344 },
12345 };
12346 let (nkv, hd, rd) = self.layer_geom(li);
12347 crate::gpu::forward_batch_graph_at(
12348 &model,
12349 self.graph_kv_id,
12350 li,
12351 &[layer],
12352 &self.inv_freq,
12353 h,
12354 self.layer_num_heads(li),
12355 nkv,
12356 hd,
12357 rd,
12358 self.hidden_size,
12359 1,
12360 positions,
12361 self.kv_cache.max_seq_len,
12362 self.norm_style == cortiq_core::NormStyle::Gemma,
12363 self.rms_eps as f32,
12364 self.attn_scale,
12365 b,
12366 &[],
12367 self.o1_epoch,
12368 None,
12369 None,
12370 )
12371 };
12372 match outcome {
12373 Out::Completed => {}
12374 Out::Declined => return Out::Declined,
12375 Out::Failed => {
12376 self.graph_failed
12377 .store(true, std::sync::atomic::Ordering::Relaxed);
12378 return Out::Failed;
12379 }
12380 }
12381 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12382 let hs = self.hidden_size;
12383 let lw = &self.weights.layers[li];
12384 let FfnKind::Moe(m) = &lw.ffn else {
12385 unreachable!()
12386 };
12387 let mut post = vec![0.0; h.len()];
12388 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12389 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12390 }
12391 let mut ffn = if b == 1 {
12392 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12393 } else {
12394 moe_ffn_banked_rows(
12395 &mut self.mimo_moe,
12396 li,
12397 m,
12398 &post,
12399 b,
12400 hs,
12401 self.pool.as_deref(),
12402 )
12403 };
12404 for (x, &f) in h.iter_mut().zip(&ffn) {
12405 *x += f;
12406 }
12407 attention::recycle_buf(&mut ffn);
12408 if self.layer_dump.is_some() {
12409 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12410 self.dump_layer_row(pos, li, row);
12411 }
12412 }
12413 crate::mimo_moe::note_attention_graph(b, attn_ns);
12414 Out::Completed
12415 }
12416
12417 fn layer_attn_plain(&self, li: usize) -> bool {
12418 self.kv_heads_per_layer.is_none()
12419 && self.v_head_dim.is_none()
12420 && self.global_attn.is_none()
12421 && self.layer_window(li).is_none()
12422 && self.kv_cache.layers[li].sinks.is_none()
12423 }
12424
12425 fn forward_layers(
12427 &mut self,
12428 hidden: &[f32],
12429 position: usize,
12430 task_mask: Option<&TaskMask>,
12431 ) -> Vec<f32> {
12432 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12433 self.o1_progress();
12434 self.swa_trim_tails();
12435 out
12436 }
12437
12438 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12446 self.embed_single(id)
12447 }
12448
12449 pub fn split_supported(&self) -> Result<(), String> {
12453 if self.dsv4.is_some() {
12454 return Err(
12455 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12456 );
12457 }
12458 if self.dsv41.is_some() {
12459 return Err(
12460 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12461 .into(),
12462 );
12463 }
12464 if self.qwen4_exp.is_some() {
12465 return Err(
12466 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12467 );
12468 }
12469 if self.per_token_stack() {
12470 return Err(
12471 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12472 );
12473 }
12474 Ok(())
12475 }
12476
12477 pub fn forward_span(
12482 &mut self,
12483 hidden: &[f32],
12484 position: usize,
12485 from: usize,
12486 upto: usize,
12487 task_mask: Option<&TaskMask>,
12488 ) -> Result<Vec<f32>, String> {
12489 self.split_supported()?;
12490 if from > upto || upto >= self.num_layers {
12491 return Err(format!(
12492 "forward_span: layer range {from}..={upto} outside 0..{}",
12493 self.num_layers
12494 ));
12495 }
12496 if hidden.len() != self.hidden_size {
12497 return Err(format!(
12498 "forward_span: hidden len {} ≠ hidden_size {}",
12499 hidden.len(),
12500 self.hidden_size
12501 ));
12502 }
12503 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12504 self.o1_progress();
12505 self.swa_trim_tails();
12506 if self
12507 .graph_failed
12508 .swap(false, std::sync::atomic::Ordering::Relaxed)
12509 {
12510 self.cancel
12511 .store(false, std::sync::atomic::Ordering::Relaxed);
12512 self.clear_sequence_state();
12513 return Err("forward_span: deferred O(1) transition failed".into());
12514 }
12515 Ok(out)
12516 }
12517
12518 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12521 let normed = inference::rms_norm(
12522 hidden,
12523 &self.weights.final_norm,
12524 self.rms_eps,
12525 self.norm_style,
12526 );
12527 self.lm_head_forward(&normed)
12528 }
12529
12530 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12532 sampler::sample_with_scratch(
12533 logits,
12534 &self.sampler_config,
12535 past_tokens,
12536 &mut self.rng,
12537 &mut self.sampler_scratch,
12538 )
12539 }
12540
12541 pub fn reset_session(&mut self) {
12543 self.clear_sequence_state();
12544 }
12545
12546 pub fn prefill_span_ids(
12552 &mut self,
12553 ids: &[u32],
12554 start_pos: usize,
12555 upto: usize,
12556 task_mask: Option<&TaskMask>,
12557 ) -> Result<Vec<f32>, String> {
12558 self.split_supported()?;
12559 if upto >= self.num_layers {
12560 return Err(format!(
12561 "prefill_span_ids: upto {upto} outside 0..{}",
12562 self.num_layers
12563 ));
12564 }
12565 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12569 let out =
12570 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12571 self.check_o1_progress_failure("prefill_span_ids")?;
12572 Ok(out)
12573 } else {
12574 let hs = self.hidden_size;
12575 let mut out = Vec::with_capacity(ids.len() * hs);
12576 for (i, &id) in ids.iter().enumerate() {
12577 let emb = self.embed_id(id);
12578 out.extend_from_slice(&self.forward_span(
12579 &emb,
12580 start_pos + i,
12581 0,
12582 upto,
12583 task_mask,
12584 )?);
12585 }
12586 Ok(out)
12587 }
12588 }
12589
12590 pub fn prefill_span_hidden(
12593 &mut self,
12594 hidden: &[f32],
12595 start_pos: usize,
12596 from: usize,
12597 upto: usize,
12598 task_mask: Option<&TaskMask>,
12599 ) -> Result<Vec<f32>, String> {
12600 self.split_supported()?;
12601 let hs = self.hidden_size;
12602 if hidden.is_empty() || hidden.len() % hs != 0 {
12603 return Err(format!(
12604 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12605 hidden.len()
12606 ));
12607 }
12608 if from > upto || upto >= self.num_layers {
12609 return Err(format!(
12610 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12611 self.num_layers
12612 ));
12613 }
12614 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12615 let out = self.prefill_batch_span(
12616 PrefillIn::Hidden(hidden),
12617 start_pos,
12618 task_mask,
12619 from,
12620 upto + 1,
12621 );
12622 self.check_o1_progress_failure("prefill_span_hidden")?;
12623 Ok(out)
12624 } else {
12625 let b = hidden.len() / hs;
12626 let mut out = Vec::with_capacity(hidden.len());
12627 for i in 0..b {
12628 let h = self.forward_span(
12629 &hidden[i * hs..(i + 1) * hs],
12630 start_pos + i,
12631 from,
12632 upto,
12633 task_mask,
12634 )?;
12635 out.extend_from_slice(&h);
12636 }
12637 Ok(out)
12638 }
12639 }
12640
12641 fn try_token_graph_wgpu(
12645 &self,
12646 hidden: &[f32],
12647 position: usize,
12648 logits_out: &mut Vec<f32>,
12649 layers_run: &mut usize,
12650 ) -> Option<Result<Vec<f32>, ()>> {
12651 self.try_token_graph_wgpu_steps(
12652 hidden,
12653 position,
12654 logits_out,
12655 1,
12656 None,
12657 Some(layers_run),
12658 0,
12659 self.num_layers,
12660 )
12661 }
12662
12663 fn try_token_graph_wgpu_span(
12667 &self,
12668 hidden: &[f32],
12669 position: usize,
12670 logits_out: &mut Vec<f32>,
12671 from: usize,
12672 upto_excl: usize,
12673 layers_run: &mut usize,
12674 ) -> Option<Result<Vec<f32>, ()>> {
12675 self.try_token_graph_wgpu_steps(
12676 hidden,
12677 position,
12678 logits_out,
12679 1,
12680 None,
12681 Some(layers_run),
12682 from,
12683 upto_excl,
12684 )
12685 }
12686
12687 #[cfg(feature = "gpu")]
12691 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12692 if !crate::minicpm_tail::minimax_pool_on()
12693 || !crate::gpu::enabled_here()
12694 || self.head_clusters.is_some()
12695 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12696 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12697 {
12698 return None;
12699 }
12700 let (model, i, kind, rs) = self
12701 .weights
12702 .lm_head
12703 .graph_weight()
12704 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12705 let name = &model.tensors[i].name;
12706 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12707 crate::gpu::GraphPrismOp::InverseEmbedding
12708 } else if crate::prism::is_forward_weight(&model, name) {
12709 crate::gpu::GraphPrismOp::Forward
12710 } else {
12711 crate::gpu::GraphPrismOp::None
12712 };
12713 let gw = crate::gpu::GraphW {
12714 idx: i,
12715 kind,
12716 row_scale: rs,
12717 data: &[],
12718 prism,
12719 affine: crate::prism::is_affine_target(&model, name),
12720 };
12721 let rows = self.weights.lm_head.rows();
12722 let mut h = hidden.to_vec();
12723 let mut lg = Vec::new();
12724 let mut layers_run = 0usize;
12725 let outcome = crate::gpu::forward_token_graph(
12726 &model,
12727 self.graph_kv_id,
12728 &[],
12729 &[],
12730 self.o1_epoch,
12731 &self.inv_freq,
12732 &mut h,
12733 self.num_heads,
12734 self.num_kv_heads,
12735 self.head_dim,
12736 self.attn_scale,
12737 self.head_dim,
12738 self.hidden_size,
12739 self.intermediate_size,
12740 0,
12741 self.kv_cache.max_seq_len,
12742 self.norm_style == cortiq_core::NormStyle::Gemma,
12743 self.rms_eps as f32,
12744 Some((&gw, rows)),
12745 &self.weights.final_norm,
12746 &mut lg,
12747 &[],
12748 1,
12749 None,
12750 None,
12751 Some(&mut layers_run),
12752 0,
12753 false,
12754 );
12755 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12756 return None;
12757 }
12758 lg.resize(self.vocab_size, 0.0);
12761 if let Some(m) = self.logit_multiplier {
12762 for l in lg.iter_mut() {
12763 *l *= m;
12764 }
12765 }
12766 if let Some(c) = self.final_softcap {
12767 softcap_in_place(&mut lg, c, self.pool.as_deref());
12768 }
12769 Some(lg)
12770 }
12771
12772 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12775 if winners.is_empty() {
12776 return;
12777 }
12778 if self.m3_n_exp == 0 {
12779 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12780 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12781 }
12782 for &e in winners {
12783 let e = e as usize;
12784 if e < self.m3_n_exp && li < self.num_layers {
12785 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12786 }
12787 }
12788 if li + 1 == self.num_layers {
12789 self.m3_tokens += 1;
12790 if self.m3_tokens % 16 == 0 {
12791 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12792 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12793 let _ = std::fs::write(&path, bytes);
12794 }
12795 }
12796 }
12797 }
12798
12799 fn m3_prewarm(&mut self) {
12804 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12805 return;
12806 };
12807 let Ok(bytes) = std::fs::read(&path) else {
12808 tracing::warn!("m3 prewarm: cannot read profile {path}");
12809 return;
12810 };
12811 let counts: Vec<u32> = bytes
12812 .chunks_exact(4)
12813 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12814 .collect();
12815 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12816 if counts.len() != self.num_layers * n_exp {
12817 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12818 return;
12819 }
12820 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12821 .ok()
12822 .and_then(|v| v.parse().ok())
12823 .unwrap_or(300);
12824 let mut ranked: Vec<(u32, usize, usize)> = counts
12825 .iter()
12826 .enumerate()
12827 .filter(|(_, c)| **c > 0)
12828 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12829 .collect();
12830 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12831 ranked.truncate(limit);
12832 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12833 for (_, li, e) in ranked {
12834 per_layer[li].push(e);
12835 }
12836 let mut admitted = 0usize;
12837 for (li, ids) in per_layer.iter().enumerate() {
12838 if ids.is_empty() {
12839 continue;
12840 }
12841 let lw = &self.weights.layers[self.phys_layer(li)];
12842 let FfnKind::Moe(m) = &lw.ffn else {
12843 continue;
12844 };
12845 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12847 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12848 admitted += ids.len();
12849 }
12850 }
12851 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12852 }
12853
12854 #[cfg(feature = "gpu")]
12865 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12866 if self.o1_active() || self.attn_softcap > 0.0 {
12867 return Ok(false);
12868 }
12869 if !self.m3_prewarmed {
12870 self.m3_prewarmed = true;
12871 self.m3_prewarm();
12872 }
12873 let frame = {
12874 let lw = &self.weights.layers[self.phys_layer(li)];
12875 let FfnKind::Moe(m) = &lw.ffn else {
12876 return Ok(false);
12877 };
12878 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12879 return Ok(false);
12880 }
12881 let t_arm = std::time::Instant::now();
12882 if self.m3_winners.len() != self.num_layers {
12883 self.m3_winners = vec![Vec::new(); self.num_layers];
12884 }
12885 let pred = std::mem::take(&mut self.m3_winners[li]);
12886 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12887 if armed.is_none() {
12888 self.m3_winners[li] = pred; }
12890 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12891 match armed {
12892 Some(frame) => frame,
12893 None => return Ok(false),
12896 }
12897 };
12898 crate::gpu_wgpu::pooled_frame_enter(frame);
12899 let want_logits = self.graph_want_logits;
12903 self.graph_want_logits = false;
12904 let mut lg = Vec::new();
12905 let mut gl = 0usize;
12906 let t_span = std::time::Instant::now();
12907 let span = crate::gpu::gpu_scope(|| {
12908 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12909 });
12910 self.graph_want_logits = want_logits;
12911 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12912 let hh = match span {
12913 Some(Ok(hh)) => hh,
12914 Some(Err(())) => return Err(()),
12915 None => {
12916 return Ok(false);
12919 }
12920 };
12921 if gl != 1 {
12922 return if gl == 0 { Ok(false) } else { Err(()) };
12925 }
12926 *h = hh;
12927 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12928 self.m3_note_route(li, &winners);
12929 if self.m3_winners.len() != self.num_layers {
12930 self.m3_winners = vec![Vec::new(); self.num_layers];
12931 }
12932 self.m3_winners[li] = winners;
12933 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12934 cold.retain(|&(_, w)| {
12937 if w.is_finite() {
12938 true
12939 } else {
12940 tracing::error!("pooled tail: non-finite cold weight from expert route");
12941 false
12942 }
12943 });
12944 if cold.is_empty() {
12945 return Ok(true);
12946 }
12947 let x = crate::gpu_wgpu::pooled_frame_take_input();
12948 if x.len() != self.hidden_size {
12949 return Err(());
12950 }
12951 let lw = &self.weights.layers[self.phys_layer(li)];
12957 let FfnKind::Moe(m) = &lw.ffn else {
12958 return Err(());
12959 };
12960 let t_cold = std::time::Instant::now();
12961 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12962 let mut out = crate::attention::take_buf(self.hidden_size);
12963 let on_device = !force_host
12964 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12965 if !on_device {
12966 crate::attention::recycle_buf(&mut out);
12967 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12968 .iter()
12969 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12970 .collect::<Result<_, ()>>()?;
12971 out = crate::gpu::cpu_scope(|| {
12972 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12973 });
12974 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12975 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12976 }
12977 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12978 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12979 if out.len() != h.len() {
12980 crate::attention::recycle_buf(&mut out);
12981 return Err(());
12982 }
12983 for (dst, src) in h.iter_mut().zip(out.iter()) {
12984 *dst += src;
12985 }
12986 crate::attention::recycle_buf(&mut out);
12987 if on_device {
12988 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12989 }
12990 Ok(true)
12991 }
12992
12993 #[cfg(not(feature = "gpu"))]
12994 fn m3_pooled_layer(
12995 &mut self,
12996 _li: usize,
12997 _position: usize,
12998 _h: &mut Vec<f32>,
12999 ) -> Result<bool, ()> {
13000 Ok(false)
13001 }
13002
13003 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
13007 if self.o1_active() || self.attn_softcap > 0.0 {
13008 return None;
13009 }
13010 if let Some(reason) = self.wgpu_graph_attn_decline() {
13013 self.note_graph_decline("wgpu multi-burst", reason);
13014 return None;
13015 }
13016 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
13017 if !graph_on || self.graph_refused() {
13018 return None;
13025 }
13026 let emb = self.embed_single(t_next);
13027 let mut lg = Vec::new();
13028 let mut ids = Vec::new();
13029 match self.try_token_graph_wgpu_steps(
13030 &emb,
13031 position,
13032 &mut lg,
13033 k,
13034 Some(&mut ids),
13035 None,
13036 0,
13037 self.num_layers,
13038 ) {
13039 Some(Ok(_)) => {}
13040 Some(Err(())) => {
13041 self.graph_failed
13046 .store(true, std::sync::atomic::Ordering::Relaxed);
13047 return None;
13048 }
13049 None => return None,
13050 }
13051 (ids.len() == k).then_some(ids)
13052 }
13053
13054 fn try_token_graph_wgpu_steps(
13058 &self,
13059 hidden: &[f32],
13060 position: usize,
13061 logits_out: &mut Vec<f32>,
13062 steps: usize,
13063 ids_out: Option<&mut Vec<u32>>,
13064 layers_run: Option<&mut usize>,
13065 from: usize,
13066 upto_excl: usize,
13067 ) -> Option<Result<Vec<f32>, ()>> {
13068 let upto_excl = match self.mimo_moe.graph_prefix_end() {
13071 Some(end) if end < upto_excl => {
13072 if steps != 1 || layers_run.is_none() || from >= end {
13073 return None;
13074 }
13075 end
13076 }
13077 _ => upto_excl,
13078 };
13079 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13082 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13083 return None;
13087 }
13088 if let Some(reason) = self.wgpu_graph_attn_decline() {
13095 self.note_graph_decline("wgpu token graph", reason);
13096 return None;
13097 }
13098 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13103 .map(|li| {
13104 if !o1_gpu {
13105 return None;
13106 }
13107 self.kv_cache.layers[self.phys_layer(li)].o1_views()
13108 })
13109 .collect();
13110 if self.o1_active() && o1_gpu {
13111 let want: usize = (from..upto_excl)
13114 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
13115 .count();
13116 let have = o1_views.iter().filter(|v| v.is_some()).count();
13117 if want == 0 || have != want {
13118 use std::sync::atomic::{AtomicUsize, Ordering};
13128 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13129 let code = have * 1000 + want;
13130 if LAST.swap(code, Ordering::Relaxed) != code {
13131 tracing::warn!(
13132 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13133 );
13134 }
13135 return None;
13136 }
13137 }
13138 let nh = self.num_heads;
13139 let (nkv, hd, rd) = self.layer_geom(0);
13140 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13141 let mut layers = Vec::with_capacity(upto_excl - from);
13142 let mut model = None;
13143 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13144 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13145 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13147 let name = &m.tensors[i].name;
13148 let prism = if crate::prism::is_inverse_embedding(m, name) {
13149 crate::gpu::GraphPrismOp::InverseEmbedding
13150 } else if crate::prism::is_forward_weight(m, name) {
13151 crate::gpu::GraphPrismOp::Forward
13152 } else {
13153 crate::gpu::GraphPrismOp::None
13154 };
13155 return Some(crate::gpu::GraphW {
13156 idx: i,
13157 kind,
13158 row_scale: rs,
13159 data: &[],
13160 prism,
13161 affine: crate::prism::is_affine_target(m, name),
13162 });
13163 }
13164 match t.as_f32() {
13166 Some(d) => Some(crate::gpu::GraphW {
13167 idx: 0,
13168 kind: 4,
13169 row_scale: &[],
13170 data: d,
13171 prism: crate::gpu::GraphPrismOp::None,
13172 affine: false,
13173 }),
13174 None => {
13175 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13176 eprintln!("batch graph: weight has no graph/f32 representation");
13177 }
13178 None
13179 }
13180 }
13181 }
13182 for li in from..upto_excl {
13183 let lw = &self.weights.layers[self.phys_layer(li)];
13184 if dbg {
13185 let ak = match &lw.attn {
13186 AttnKind::Mla(_) => "Mla".into(),
13187 AttnKind::Full {
13188 output_gate, bias, ..
13189 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13190 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13191 AttnKind::Kda(_) => "Kda".into(),
13192 AttnKind::Linear(_) => "Linear".into(),
13193 AttnKind::ShortConv(_) => "ShortConv".into(),
13194 AttnKind::Bounded(_) => "Bounded".into(),
13195 };
13196 let fk = match &lw.ffn {
13197 FfnKind::Dense(_) => "Dense",
13198 FfnKind::Moe(_) => "Moe",
13199 FfnKind::DenseMoe(_) => "DenseMoe",
13200 };
13201 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13202 }
13203 let gffn = match &lw.ffn {
13204 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13208 FfnKind::Dense(d) => {
13209 let Some(act) = d.act.wgpu_graph_act() else {
13213 self.note_graph_decline(
13214 "wgpu token graph",
13215 "dense FFN activation without a graph kernel",
13216 );
13217 return None;
13218 };
13219 crate::gpu::GraphFfn::Dense {
13220 gate: gw(&d.gate_proj)?,
13221 up: gw(&d.up_proj)?,
13222 down: gw(&d.down_proj)?,
13223 act,
13224 }
13225 }
13226 FfnKind::Moe(m) => {
13227 if m.route_tau.is_some() || m.mask.is_some() {
13235 return None;
13236 }
13237 let shared = m.shared.as_ref();
13238 let inter = m.experts.first()?.gate_proj.rows();
13244 let detached_shared = match shared {
13252 Some((se, None))
13253 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13254 {
13255 if !matches!(se.act, Act::Silu)
13256 || se.gate_proj.mapped_q4tp().is_none()
13257 || se.up_proj.mapped_q4tp().is_none()
13258 || se.down_proj.mapped_q4tp().is_none()
13259 {
13260 return None;
13261 }
13262 Some(crate::gpu::GraphMoeDetachedShared {
13263 gate: gw(&se.gate_proj)?,
13264 up: gw(&se.up_proj)?,
13265 down: gw(&se.down_proj)?,
13266 width: se.gate_proj.rows(),
13267 })
13268 }
13269 Some((se, Some(_)))
13273 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13274 {
13275 return None;
13276 }
13277 _ => None,
13278 };
13279 let has_shared = shared.is_some() && detached_shared.is_none();
13280 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13281 let sgate = match shared {
13282 Some((_, Some(sg))) if has_shared => gw(sg)?,
13283 _ => gw(&m.router)?,
13287 };
13288 let router = gw(&m.router)?;
13289 if router.prism != crate::gpu::GraphPrismOp::None
13295 || sgate.prism != crate::gpu::GraphPrismOp::None
13296 || router.affine
13297 || sgate.affine
13298 {
13299 tracing::warn!(
13300 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13301 );
13302 return None;
13303 }
13304 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13305 let mut q4tp: Option<bool> = None;
13308 let mut gu_q2: Option<bool> = None;
13311 let mut q82: Option<bool> = None;
13313 let mut moe_act = None;
13317 for e in m
13318 .experts
13319 .iter()
13320 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13321 {
13322 let Some(e_act) = e.act.wgpu_graph_act() else {
13323 return None;
13324 };
13325 match moe_act {
13326 Some(existing) if existing != e_act => return None,
13327 None => moe_act = Some(e_act),
13328 _ => {}
13329 }
13330 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13331 return None;
13332 }
13333 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13338 let Some((em, ei, _, _)) = expert_weight
13339 .graph_weight()
13340 .or_else(|| expert_weight.graph_weight_descriptor())
13341 else {
13342 return None;
13343 };
13344 let name = &em.tensors[ei].name;
13345 if crate::prism::is_forward_weight(em, name)
13346 || crate::prism::is_inverse_embedding(em, name)
13347 || crate::prism::is_affine_target(em, name)
13348 {
13349 tracing::warn!(
13350 "resident MoE declined: expert Prism/affine transform is not implemented"
13351 );
13352 return None;
13353 }
13354 }
13355 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13356 Some((mm, gi)) => (
13357 mm,
13358 gi,
13359 e.up_proj.mapped_q4t()?.1,
13360 e.down_proj.mapped_q4t()?.1,
13361 false,
13362 false,
13363 false,
13364 ),
13365 None => match e.gate_proj.mapped_q2tp() {
13366 Some((mm, gi)) => (
13367 mm,
13368 gi,
13369 e.up_proj.mapped_q2tp()?.1,
13370 e.down_proj.mapped_q4tp()?.1,
13371 true,
13372 true,
13373 false,
13374 ),
13375 None => match e.gate_proj.mapped_q4tp() {
13376 Some((mm, gi)) => (
13377 mm,
13378 gi,
13379 e.up_proj.mapped_q4tp()?.1,
13380 e.down_proj.mapped_q4tp()?.1,
13381 true,
13382 false,
13383 false,
13384 ),
13385 None => {
13388 if !crate::gpu::moe_q82_graph_on() {
13389 return None;
13390 }
13391 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13392 (
13393 mm,
13394 gi,
13395 e.up_proj.mapped_q8_2f()?.1,
13396 e.down_proj.mapped_q8_2f()?.1,
13397 false,
13398 false,
13399 true,
13400 )
13401 }
13402 },
13403 },
13404 };
13405 if *q4tp.get_or_insert(is_p) != is_p
13406 || *gu_q2.get_or_insert(is_q2) != is_q2
13407 || *q82.get_or_insert(is_8) != is_8
13408 {
13409 tracing::warn!(
13415 "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."
13416 );
13417 return None;
13418 }
13419 model.get_or_insert_with(|| mm.clone());
13420 experts.push((gi, ui, di));
13421 }
13422 crate::gpu::GraphFfn::Moe {
13423 router,
13424 shared_gate: sgate,
13425 experts,
13426 n_exp: m.experts.len(),
13427 top_k: std::env::var("CMF_TOPK_PROBE")
13433 .ok()
13434 .and_then(|v| v.parse::<usize>().ok())
13435 .filter(|k| *k > 0 && *k <= m.top_k)
13436 .unwrap_or(m.top_k),
13437 inter,
13438 act: moe_act?,
13439 norm_topk: m.norm_topk_prob,
13440 q4tp: q4tp?,
13441 gu_q2: gu_q2.unwrap_or(false),
13442 q82: q82.unwrap_or(false),
13443 sigmoid: m.router_sigmoid,
13444 sigmoid_denom_eps: m.sigmoid_denom_eps,
13445 bias: m.expert_bias.as_deref(),
13446 has_shared,
13447 detached_shared,
13448 shared_gated,
13449 route_scale: m.routed_scaling,
13450 }
13451 }
13452 };
13453 let attn = match &lw.attn {
13454 AttnKind::Full {
13455 wq,
13456 wk,
13457 wv,
13458 wo,
13459 q_norm,
13460 k_norm,
13461 output_gate,
13462 softplus_gate,
13463 bias,
13464 } => {
13465 if self.attention_heads_per_layer.is_some() {
13466 return None;
13467 }
13468 let head_gate = match softplus_gate {
13472 None => None,
13473 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13474 Some(_) => {
13475 self.note_graph_decline(
13476 "wgpu token graph",
13477 "projected softplus / per-element output gate",
13478 );
13479 return None;
13480 }
13481 };
13482 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13483 model = Some(m.clone());
13484 crate::gpu::GraphAttn::Full {
13485 wq: gw(wq)?,
13486 wk: gw(wk)?,
13487 wv: gw(wv)?,
13488 wo: gw(wo)?,
13489 q_norm: q_norm.as_deref(),
13490 k_norm: k_norm.as_deref(),
13491 late_qk_norm: self.qk_norm_after_rope,
13492 bias: bias
13493 .as_ref()
13494 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13495 output_gate: *output_gate,
13496 cpu_k: self.kv_cache.layers[li].k_heads(),
13497 cpu_v: self.kv_cache.layers[li].v_heads(),
13498 cpu_base: self.kv_cache.layers[li].base(),
13499 geom: self.graph_attn_geom(li),
13500 head_gate,
13501 }
13502 }
13503 AttnKind::LinearGdn(w) => {
13504 let cfg = self.gdn_cfg?;
13505 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13506 model = Some(m.clone());
13507 crate::gpu::GraphAttn::Gdn {
13508 qkv: gw(&w.in_proj_qkv)?,
13509 z: gw(&w.in_proj_z)?,
13510 a: gw(&w.in_proj_a)?,
13511 b: gw(&w.in_proj_b)?,
13512 out: gw(&w.out_proj)?,
13513 conv1d: &w.conv1d,
13514 a_log: &w.a_log,
13515 dt_bias: &w.dt_bias,
13516 norm: &w.norm,
13517 nv: cfg.num_v_heads,
13518 nk: cfg.num_k_heads,
13519 dk: cfg.key_head_dim,
13520 dv: cfg.value_head_dim,
13521 kk: cfg.conv_kernel,
13522 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13523 preview: None,
13524 }
13525 }
13526 AttnKind::ShortConv(w) => {
13527 let cfg = self.short_conv_cfg?;
13528 let (m, _, _, _) = w
13529 .in_proj
13530 .graph_weight()
13531 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13532 model = Some(m.clone());
13533 crate::gpu::GraphAttn::ShortConv {
13534 inp: gw(&w.in_proj)?,
13535 out: gw(&w.out_proj)?,
13536 taps: &w.conv,
13537 kernel: cfg.kernel,
13538 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13539 }
13540 }
13541 _ => return None,
13542 };
13543 layers.push(crate::gpu::GraphLayer {
13544 input_norm: &lw.input_norm,
13545 attn,
13546 post_norm: &lw.post_norm,
13547 attn_out_norm: lw.attn_out_norm.as_deref(),
13548 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13549 layer_scale: lw.layer_scale,
13550 ffn: gffn,
13551 });
13552 }
13553 let model = model?;
13554 let lm_gw = if upto_excl == self.num_layers
13560 && self.graph_want_logits
13561 && std::env::var("CMF_GPU_LMHEAD")
13562 .map(|v| v != "0")
13563 .unwrap_or(true)
13564 {
13565 self.weights
13566 .lm_head
13567 .graph_weight()
13568 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13569 .map(|(m, i, kind, rs)| {
13570 let name = &m.tensors[i].name;
13571 let prism = if crate::prism::is_inverse_embedding(m, name) {
13572 crate::gpu::GraphPrismOp::InverseEmbedding
13573 } else if crate::prism::is_forward_weight(m, name) {
13574 crate::gpu::GraphPrismOp::Forward
13575 } else {
13576 crate::gpu::GraphPrismOp::None
13577 };
13578 (
13579 crate::gpu::GraphW {
13580 idx: i,
13581 kind,
13582 row_scale: rs,
13583 data: &[],
13584 prism,
13585 affine: crate::prism::is_affine_target(m, name),
13586 },
13587 self.weights.lm_head.rows(),
13588 )
13589 })
13590 } else {
13591 None
13592 };
13593 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13594 let emb_gw = if steps > 1 {
13596 self.weights
13597 .embed_tokens
13598 .graph_weight()
13599 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13600 .map(|(m, i, kind, rs)| {
13601 let name = &m.tensors[i].name;
13602 let prism = if crate::prism::is_inverse_embedding(m, name) {
13603 crate::gpu::GraphPrismOp::InverseEmbedding
13604 } else if crate::prism::is_forward_weight(m, name) {
13605 crate::gpu::GraphPrismOp::Forward
13606 } else {
13607 crate::gpu::GraphPrismOp::None
13608 };
13609 (
13610 crate::gpu::GraphW {
13611 idx: i,
13612 kind,
13613 row_scale: rs,
13614 data: &[],
13615 prism,
13616 affine: crate::prism::is_affine_target(m, name),
13617 },
13618 self.weights.embed_tokens.rows(),
13619 self.embed_multiplier,
13620 )
13621 })
13622 } else {
13623 None
13624 };
13625
13626 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13632 (from..upto_excl.min(self.num_layers - 1))
13633 .filter(|&li| (li + 1) % self.physical_layers == 0)
13634 .map(|li| li - from)
13635 .collect()
13636 } else {
13637 Vec::new()
13638 };
13639 let mut h = hidden.to_vec();
13640 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13646 let outcome = crate::gpu::forward_token_graph(
13647 &model,
13648 self.graph_kv_id,
13649 &layers,
13650 &o1_views,
13651 self.o1_epoch,
13652 &self.inv_freq,
13653 &mut h,
13654 nh,
13655 nkv,
13656 hd,
13657 self.attn_scale,
13658 rd,
13659 self.hidden_size,
13660 self.intermediate_size,
13661 position,
13662 self.kv_cache.max_seq_len,
13663 gemma,
13664 self.rms_eps as f32,
13665 lm,
13666 &self.weights.final_norm,
13667 logits_out,
13668 &loop_norm_at,
13669 steps,
13670 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13671 ids_out,
13672 layers_run,
13673 from,
13674 dump_hidden,
13675 );
13676 match outcome {
13677 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13678 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13679 crate::gpu::TokenGraphOutcome::Declined => None,
13680 }
13681 }
13682
13683 #[cfg(target_os = "macos")]
13692 #[allow(clippy::type_complexity)]
13693 fn metal_rows_plan(
13694 &self,
13695 ) -> Option<(
13696 Vec<MetalRowsItem<'_>>,
13697 std::sync::Arc<cortiq_core::CmfModel>,
13698 Option<crate::gpu_metal::GdnGpuCfg>,
13699 )> {
13700 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13701 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13702 if !graph_force
13703 || !crate::gpu::enabled_here()
13704 || std::env::var("CMF_GPU_BLOCK")
13705 .map(|v| v == "0")
13706 .unwrap_or(false)
13707 || self.attn_softcap > 0.0
13708 || self.o1_active()
13709 || self.swa.is_some()
13710 || self.global_attn.is_some()
13711 || self.attention_heads_per_layer.is_some()
13712 || self.graph_attn_decline_reason().is_some()
13714 || self.attn_v_norm
13715 || self.loop_final_norm
13716 || !self.metal_rope_scale_ok()
13717 {
13718 return None;
13719 }
13720 let attend_contract = self.head_dim % 4 == 0
13721 && self.head_dim <= 256
13722 && self.rotary_dim >= 2
13723 && self.rotary_dim <= self.head_dim
13724 && (self.rotary_dim / 2) % 32 == 0
13725 && self.num_kv_heads > 0
13726 && self.num_heads % self.num_kv_heads == 0;
13727 if !attend_contract {
13728 return None;
13729 }
13730 let mut plan: Vec<MetalRowsItem> = Vec::new();
13731 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13732 for li in 0..self.num_layers {
13733 let lw = &self.weights.layers[self.phys_layer(li)];
13734 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13735 return None;
13736 }
13737 let ffn = match &lw.ffn {
13738 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13739 let (Some(g), Some(u), Some(dn)) = (
13740 d.gate_proj.metal_graph_parts(),
13741 d.up_proj.metal_graph_parts(),
13742 d.down_proj.metal_graph_parts(),
13743 ) else {
13744 return None;
13745 };
13746 MetalFfn::Dense {
13747 gate: g,
13748 up: u,
13749 down: dn,
13750 act: 0, }
13752 }
13753 _ => return None,
13754 };
13755 match &lw.attn {
13756 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13757 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13758 w.in_proj_qkv.metal_graph_parts(),
13759 w.in_proj_z.metal_graph_parts(),
13760 w.in_proj_a.f32_parts(),
13761 w.in_proj_b.f32_parts(),
13762 w.out_proj.metal_graph_parts(),
13763 ) else {
13764 return None;
13765 };
13766 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13767 model_ref.get_or_insert_with(|| model.clone());
13768 }
13769 let gl = GdnGpuLayer {
13770 attn_norm: &lw.input_norm,
13771 post_norm: &lw.post_norm,
13772 qkv,
13773 z,
13774 a,
13775 b: bb,
13776 out,
13777 ffn,
13778 conv1d: &w.conv1d,
13779 a_log: &w.a_log,
13780 dt_bias: &w.dt_bias,
13781 gnorm: &w.norm,
13782 };
13783 match plan.last_mut() {
13784 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13785 _ => plan.push(MetalRowsItem::Gdn {
13786 run: vec![gl],
13787 first: li,
13788 }),
13789 }
13790 }
13791 AttnKind::Full {
13792 wq,
13793 wk,
13794 wv,
13795 wo,
13796 q_norm,
13797 k_norm,
13798 output_gate,
13799 softplus_gate: None,
13800 bias: None,
13801 } => {
13802 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13803 wq.metal_graph_parts(),
13804 wk.metal_graph_parts(),
13805 wv.metal_graph_parts(),
13806 wo.metal_graph_parts(),
13807 ) else {
13808 return None;
13809 };
13810 if let QTensor::Mapped { model, .. } = wq {
13811 model_ref.get_or_insert_with(|| model.clone());
13812 }
13813 let cache = &self.kv_cache.layers[li];
13814 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13815 return None;
13816 }
13817 plan.push(MetalRowsItem::Attn {
13818 l: AttnGpuLayer {
13819 attn_norm: &lw.input_norm,
13820 post_norm: &lw.post_norm,
13821 wq: pq,
13822 wk: pk,
13823 wv: pv,
13824 wo: po,
13825 ffn,
13826 attn_out_norm: None,
13827 ffn_out_norm: None,
13828 layer_scale: None,
13829 },
13830 li,
13831 q_norm: q_norm.as_deref(),
13832 k_norm: k_norm.as_deref(),
13833 output_gate: *output_gate,
13834 });
13835 }
13836 _ => return None,
13837 }
13838 }
13839 let model = model_ref?;
13840 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13841 nv: cfg.num_v_heads,
13842 nk: cfg.num_k_heads,
13843 dk: cfg.key_head_dim,
13844 dv: cfg.value_head_dim,
13845 kk: cfg.conv_kernel,
13846 hidden: self.hidden_size,
13847 inter: self.intermediate_size,
13848 c_dim: cfg.conv_dim(),
13849 eps: cfg.rms_eps as f32,
13850 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13851 });
13852 Some((plan, model, gcfg))
13853 }
13854
13855 #[cfg(target_os = "macos")]
13857 #[allow(clippy::too_many_arguments)]
13858 fn metal_attn_params<'a>(
13859 li: usize,
13860 cache: &'a crate::kv_cache::LayerKvCache,
13861 q_norm: Option<&'a [f32]>,
13862 k_norm: Option<&'a [f32]>,
13863 output_gate: bool,
13864 inv_freq: &'a [f32],
13865 rope_scale: f32,
13866 geom: (usize, usize, usize, usize),
13867 pos0: usize,
13868 kv_id: u64,
13869 scale: f32,
13870 eps: f32,
13871 gemma: bool,
13872 late_qk_norm: bool,
13873 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13874 let (nh, nkv, hd, rd) = geom;
13875 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13876 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13877 let cpu_stored = cpu_k[0].len() / hd;
13878 (
13879 crate::gpu_metal::AttnDeviceParams {
13880 kv_id,
13881 layer: li,
13882 nh,
13883 nkv,
13884 hd,
13885 rd,
13886 position: pos0,
13887 scale,
13888 eps,
13889 gemma,
13890 late_qk_norm,
13891 output_gate,
13892 q_norm,
13893 k_norm,
13894 inv_freq,
13895 rope_scale,
13896 cpu_k,
13897 cpu_v,
13898 cpu_stored,
13899 cpu_gen: cache.generation(),
13900 o1: None,
13901 window: None,
13902 softmax_sink: cache.sinks.as_deref(),
13903 head_gate: None,
13904 v_norm: false,
13905 },
13906 cpu_stored,
13907 )
13908 }
13909
13910 #[cfg(target_os = "macos")]
13915 #[allow(clippy::type_complexity)]
13916 fn metal_rows_run(
13917 &mut self,
13918 hiddens: &mut [f32],
13919 pos0: usize,
13920 b: usize,
13921 prefill: bool,
13922 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13923 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13927 ) -> MetalRowsRun {
13928 use crate::gpu_metal::{GraphDims, VerifyGraph};
13929 if !crate::gpu_metal::wait_replay() {
13935 tracing::error!("Metal rows graph: the pending async replay failed");
13936 return MetalRowsRun::Failed;
13937 }
13938 spec_stamp("v.wait");
13939 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13945 if want > 0 {
13946 let phys = self.physical_layers.max(1);
13947 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13948 let is_gdn = self
13949 .weights
13950 .layers
13951 .get(li % phys)
13952 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13953 if is_gdn && l.linear_state.len() != want {
13954 l.linear_state = vec![0f32; want];
13955 }
13956 }
13957 }
13958 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13959 return MetalRowsRun::Declined;
13960 };
13961 spec_stamp("v.plan");
13962 let dims = GraphDims {
13963 hidden: self.hidden_size,
13964 eps: self.rms_eps as f32,
13965 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13966 };
13967 let Some(mut graph) = (if prefill {
13968 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13969 } else {
13970 VerifyGraph::new(&model, dims, hiddens, b)
13971 }) else {
13972 return MetalRowsRun::Declined;
13973 };
13974 let geom = (
13975 self.num_heads,
13976 self.num_kv_heads,
13977 self.head_dim,
13978 self.rotary_dim,
13979 );
13980 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13981 let eps = self.rms_eps as f32;
13982 let kv_id = self.graph_kv_id;
13983 let inv_freq = self.inv_freq.clone();
13984 for item in &plan {
13985 let ok = match item {
13986 MetalRowsItem::Gdn { run, .. } => gcfg
13987 .as_ref()
13988 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13989 .unwrap_or(false),
13990 MetalRowsItem::Attn {
13991 l,
13992 li,
13993 q_norm,
13994 k_norm,
13995 output_gate,
13996 } => {
13997 let (p, _) = Self::metal_attn_params(
13998 *li,
13999 &self.kv_cache.layers[*li],
14000 *q_norm,
14001 *k_norm,
14002 *output_gate,
14003 &inv_freq,
14004 self.layer_rope_scale(*li),
14005 geom,
14006 pos0,
14007 kv_id,
14008 self.attn_scale,
14009 eps,
14010 gemma,
14011 self.qk_norm_after_rope,
14012 );
14013 graph.attn_ok(l, &p)
14014 }
14015 };
14016 if !ok {
14017 use std::sync::atomic::{AtomicBool, Ordering};
14018 static SAID: AtomicBool = AtomicBool::new(false);
14019 if !SAID.swap(true, Ordering::Relaxed) {
14020 tracing::warn!("metal rows graph: a layer failed preflight — declining");
14021 }
14022 return MetalRowsRun::Declined;
14023 }
14024 }
14025 let lm = match &spec {
14026 Some((lm, _, _)) => {
14027 if !graph.lm_head_ok(*lm) {
14028 return MetalRowsRun::Declined;
14029 }
14030 Some(*lm)
14031 }
14032 None => None,
14033 };
14034 let mut gdn_layers = Vec::new();
14035 let mut attn_layers = Vec::new();
14036 for item in &plan {
14037 match item {
14038 MetalRowsItem::Gdn { run, first } => {
14039 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14040 .iter()
14041 .map(|l| l.linear_state.as_slice())
14042 .collect();
14043 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14044 return MetalRowsRun::Declined;
14045 }
14046 gdn_layers.extend(*first..*first + run.len());
14047 }
14048 MetalRowsItem::Attn {
14049 l,
14050 li,
14051 q_norm,
14052 k_norm,
14053 output_gate,
14054 } => {
14055 let (p, cpu_stored) = Self::metal_attn_params(
14056 *li,
14057 &self.kv_cache.layers[*li],
14058 *q_norm,
14059 *k_norm,
14060 *output_gate,
14061 &inv_freq,
14062 self.layer_rope_scale(*li),
14063 geom,
14064 pos0,
14065 kv_id,
14066 self.attn_scale,
14067 eps,
14068 gemma,
14069 self.qk_norm_after_rope,
14070 );
14071 if !graph.encode_attn_b(l, &p) {
14072 return MetalRowsRun::Declined;
14073 }
14074 attn_layers.push((*li, cpu_stored));
14075 }
14076 }
14077 }
14078 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
14079 if !graph.encode_lm_head_b(final_norm, lm) {
14080 return MetalRowsRun::Declined;
14081 }
14082 if let Some((n, _)) = argmax_out.as_ref() {
14087 if !graph.encode_argmax_b(*n) {
14088 argmax_out = None;
14089 }
14090 }
14091 }
14092 spec_stamp("v.enc");
14093 if !graph.sync() {
14094 return MetalRowsRun::Failed;
14095 }
14096 spec_stamp("v.gpu");
14097 match (spec, argmax_out) {
14098 (Some(_), Some((_, ids))) => {
14099 ids.resize(b, 0);
14100 if !graph.read_argmax(ids) {
14101 return MetalRowsRun::Failed;
14102 }
14103 spec_stamp("v.am");
14104 }
14105 (Some((lm, _, logits)), None) => {
14106 logits.resize(b * lm.1, 0.0);
14107 if !graph.read_logits(logits) {
14108 return MetalRowsRun::Failed;
14109 }
14110 spec_stamp("v.lg");
14111 }
14112 (None, _) => {}
14113 }
14114 if !graph.read_hidden(hiddens) {
14115 return MetalRowsRun::Failed;
14116 }
14117 spec_stamp("v.hid");
14118 MetalRowsRun::Completed(MetalVerifyPending {
14119 graph,
14120 gdn_layers,
14121 attn_layers,
14122 })
14123 }
14124
14125 #[cfg(target_os = "macos")]
14131 fn try_batch_graph_metal(
14132 &mut self,
14133 hiddens: &mut [f32],
14134 positions: &[usize],
14135 b: usize,
14136 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14137 argmax_out: Option<(usize, &mut Vec<u32>)>,
14138 ) -> crate::gpu::BatchGraphOutcome {
14139 let _t0 = std::time::Instant::now();
14140 if positions.len() != b
14141 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14142 || hiddens.len() != b * self.hidden_size
14143 {
14144 return crate::gpu::BatchGraphOutcome::Declined;
14145 }
14146 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14147 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14148 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14149 MetalRowsRun::Completed(pending) => pending,
14150 };
14151 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14152 eprintln!(
14153 "metal-verify: {:.1} ms | b={b}",
14154 _t0.elapsed().as_secs_f64() * 1e3
14155 );
14156 }
14157 self.metal_verify = Some(pending);
14158 crate::gpu::BatchGraphOutcome::Completed
14159 }
14160
14161 #[cfg(target_os = "macos")]
14166 fn prefill_rows_metal(
14167 &mut self,
14168 ids: &[u32],
14169 start_pos: usize,
14170 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14171 ) -> MetalPrefillOutcome {
14172 let b = ids.len();
14173 if b == 0 || b > 512 {
14174 return MetalPrefillOutcome::Declined;
14175 }
14176 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14177 let with_head = spec.is_some();
14178 let hs = self.hidden_size;
14179 let mut hiddens = vec![0f32; b * hs];
14180 for (j, &id) in ids.iter().enumerate() {
14181 let e = self.embed_single(id);
14182 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14183 }
14184 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14185 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14186 MetalRowsRun::Failed => {
14187 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14188 return MetalPrefillOutcome::Failed;
14189 }
14190 MetalRowsRun::Completed(pending) => pending,
14191 };
14192 let idxs = pending.gdn_layers.clone();
14194 let mut outs: Vec<&mut [f32]> = self
14195 .kv_cache
14196 .layers
14197 .iter_mut()
14198 .enumerate()
14199 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14200 .map(|(_, l)| l.linear_state.as_mut_slice())
14201 .collect();
14202 if !pending.graph.finish_states(&mut outs) {
14203 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14204 return MetalPrefillOutcome::Failed;
14205 }
14206 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14207 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14211 for (li, cpu_stored) in &pending.attn_layers {
14212 let mut kbuf = vec![0f32; b * nkv * hd];
14213 let mut vbuf = vec![0f32; b * nkv * hd];
14214 if !crate::gpu_metal::kv_mirror_read_rows(
14215 self.graph_kv_id,
14216 *li,
14217 nkv,
14218 hd,
14219 *cpu_stored,
14220 b,
14221 &mut kbuf,
14222 &mut vbuf,
14223 ) {
14224 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14225 return MetalPrefillOutcome::Failed;
14226 }
14227 rows.push((*li, *cpu_stored, kbuf, vbuf));
14228 }
14229 for (li, cpu_stored, kbuf, vbuf) in rows {
14230 let cache = &mut self.kv_cache.layers[li];
14231 for r in 0..b {
14232 cache.append(
14233 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14234 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14235 &[],
14236 );
14237 }
14238 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14239 }
14240 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14241 if with_head {
14242 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14243 }
14244 MetalPrefillOutcome::Completed(hiddens)
14245 }
14246
14247 #[cfg(target_os = "macos")]
14248 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14249 self.prefill_rows_metal(ids, start_pos, None)
14250 }
14251
14252 #[cfg(target_os = "macos")]
14257 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14258 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14259 return MetalBatchNllOutcome::Declined;
14260 }
14261 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14262 return MetalBatchNllOutcome::Declined;
14263 };
14264 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14265 .ok()
14266 .and_then(|v| v.parse::<usize>().ok())
14267 .filter(|&v| (1..=512).contains(&v))
14268 .unwrap_or(32);
14269 let final_norm = self.weights.final_norm.clone();
14270 let mut nll = 0.0f64;
14271 let mut count = 0usize;
14272 let mut pos = 0usize;
14273 let mut completed = 0usize;
14274 while pos < ids.len() {
14275 let end = (pos + chunk).min(ids.len());
14276 let mut logits = Vec::new();
14277 let outcome =
14278 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14279 match outcome {
14280 MetalPrefillOutcome::Declined => {
14281 return if completed == 0 {
14282 MetalBatchNllOutcome::Declined
14283 } else {
14284 MetalBatchNllOutcome::Failed(format!(
14285 "ordinary Metal NLL batch declined after {completed} chunks"
14286 ))
14287 };
14288 }
14289 MetalPrefillOutcome::Failed => {
14290 return MetalBatchNllOutcome::Failed(
14291 "ordinary Metal NLL batch failed after admission".to_string(),
14292 );
14293 }
14294 MetalPrefillOutcome::Completed(_) => {}
14295 }
14296 completed += 1;
14297 let vocab = self.vocab_size.min(lm.1);
14298 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14299 return MetalBatchNllOutcome::Failed(
14300 "ordinary Metal NLL head returned an invalid shape".to_string(),
14301 );
14302 }
14303 for row in 0..(end - pos) {
14304 let absolute = pos + row;
14305 if absolute < start || absolute + 1 >= ids.len() {
14306 continue;
14307 }
14308 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14309 if let Some(mu) = self.logit_multiplier {
14310 for v in lg.iter_mut() {
14311 *v *= mu;
14312 }
14313 }
14314 if let Some(c) = self.final_softcap {
14315 softcap_in_place(lg, c, self.pool.as_deref());
14316 }
14317 let target = ids[absolute + 1] as usize;
14318 if target >= vocab {
14319 return MetalBatchNllOutcome::Failed(format!(
14320 "target token {target} exceeds Metal head rows {vocab}"
14321 ));
14322 }
14323 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14324 let lse: f64 = lg
14325 .iter()
14326 .map(|&v| ((v - max) as f64).exp())
14327 .sum::<f64>()
14328 .ln()
14329 + max as f64;
14330 nll += lse - lg[target] as f64;
14331 count += 1;
14332 }
14333 pos = end;
14334 }
14335 MetalBatchNllOutcome::Completed(nll, count)
14336 }
14337
14338 #[cfg(target_os = "macos")]
14342 fn metal_verify_commit(&mut self, a: usize) -> bool {
14343 let Some(mut pending) = self.metal_verify.take() else {
14344 return false;
14345 };
14346 let n = a + 1;
14347 let idxs = pending.gdn_layers.clone();
14349 let mut outs: Vec<&mut [f32]> = self
14350 .kv_cache
14351 .layers
14352 .iter_mut()
14353 .enumerate()
14354 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14355 .map(|(_, l)| l.linear_state.as_mut_slice())
14356 .collect();
14357 if !pending.graph.commit(n, &mut outs) {
14358 return false;
14359 }
14360 spec_stamp("c.replay");
14361 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14362 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14366 for (li, cpu_stored) in &pending.attn_layers {
14367 let mut kbuf = vec![0f32; n * nkv * hd];
14368 let mut vbuf = vec![0f32; n * nkv * hd];
14369 if !crate::gpu_metal::kv_mirror_read_rows(
14370 self.graph_kv_id,
14371 *li,
14372 nkv,
14373 hd,
14374 *cpu_stored,
14375 n,
14376 &mut kbuf,
14377 &mut vbuf,
14378 ) {
14379 return false;
14380 }
14381 rows.push((*li, *cpu_stored, kbuf, vbuf));
14382 }
14383 for (li, cpu_stored, kbuf, vbuf) in rows {
14384 let cache = &mut self.kv_cache.layers[li];
14385 for r in 0..n {
14386 cache.append(
14387 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14388 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14389 &[],
14390 );
14391 }
14392 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14393 }
14394 spec_stamp("c.kv");
14395 true
14396 }
14397
14398 #[cfg(target_os = "macos")]
14405 fn mtp_warm_batch_submit(
14406 &mut self,
14407 m: &mut MtpModule,
14408 pairs: &[(&[f32], u32)],
14409 first_pos: usize,
14410 ) -> Option<MetalWarmPending> {
14411 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14412 let b = pairs.len();
14413 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14414 return None;
14415 }
14416 let AttnKind::Full {
14417 wq,
14418 wk,
14419 wv,
14420 wo,
14421 q_norm,
14422 k_norm,
14423 output_gate,
14424 softplus_gate: None,
14425 bias: None,
14426 } = &m.layer.attn
14427 else {
14428 return None;
14429 };
14430 let FfnKind::Dense(d) = &m.layer.ffn else {
14431 return None;
14432 };
14433 if !d.segs.is_empty() {
14434 return None;
14435 }
14436 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14437 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14438 else {
14439 return None;
14440 };
14441 let (Some(g), Some(u), Some(dn)) = (
14442 d.gate_proj.q1_parts(),
14443 d.up_proj.q1_parts(),
14444 d.down_proj.q1_parts(),
14445 ) else {
14446 return None;
14447 };
14448 let Some(eh) = m.eh_proj.q1_parts() else {
14449 return None;
14450 };
14451 let QTensor::Mapped { model, .. } = wq else {
14452 return None;
14453 };
14454 let model = model.clone();
14455 let hs = self.hidden_size;
14456 let mut cat = vec![0f32; b * 2 * hs];
14458 for (j, (h, tok)) in pairs.iter().enumerate() {
14459 let e = self.embed_single(*tok);
14460 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14461 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14462 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14463 }
14464 let dims = GraphDims {
14465 hidden: hs,
14466 eps: self.rms_eps as f32,
14467 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14468 };
14469 spec_stamp("w.cat");
14470 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14471 return None;
14472 };
14473 spec_stamp("w.new");
14474 let l = AttnGpuLayer {
14475 attn_norm: &m.layer.input_norm,
14476 post_norm: &m.layer.post_norm,
14477 wq: pq,
14478 wk: pk,
14479 wv: pv,
14480 wo: po,
14481 ffn: MetalFfn::Dense {
14482 gate: g,
14483 up: u,
14484 down: dn,
14485 act: (d.act == Act::Gelu) as u8,
14486 },
14487 attn_out_norm: None,
14488 ffn_out_norm: None,
14489 layer_scale: None,
14490 };
14491 let (nh, nkv, hd, rd) = (
14492 self.num_heads,
14493 self.num_kv_heads,
14494 self.head_dim,
14495 self.rotary_dim,
14496 );
14497 let inv_freq = self.inv_freq.clone();
14498 let cpu_stored;
14499 {
14500 let cache = &m.kv;
14501 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14502 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14503 cpu_stored = cpu_k[0].len() / hd;
14504 if cpu_stored > first_pos {
14509 spec_stamp("w.decl");
14510 return None;
14511 }
14512 let p = AttnDeviceParams {
14513 kv_id: self.mtp_kv_id(),
14514 layer: Self::MTP_LAYER_BASE,
14515 nh,
14516 nkv,
14517 hd,
14518 rd,
14519 position: first_pos,
14520 scale: self.attn_scale,
14521 eps: self.rms_eps as f32,
14522 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14523 late_qk_norm: self.qk_norm_after_rope,
14524 output_gate: *output_gate,
14525 q_norm: q_norm.as_deref(),
14526 k_norm: k_norm.as_deref(),
14527 inv_freq: &inv_freq,
14528 rope_scale: self.rope_scale,
14529 cpu_k,
14530 cpu_v,
14531 cpu_stored,
14532 cpu_gen: cache.generation(),
14533 o1: None,
14534 window: None,
14535 softmax_sink: None,
14536 head_gate: None,
14537 v_norm: false,
14538 };
14539 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14540 return None;
14541 }
14542 }
14543 spec_stamp("w.enc");
14544 if !graph.submit() {
14545 return None;
14546 }
14547 spec_stamp("w.sub");
14548 Some(MetalWarmPending {
14549 graph,
14550 cpu_stored,
14551 b,
14552 })
14553 }
14554
14555 #[cfg(target_os = "macos")]
14558 fn mtp_warm_batch_metal(
14559 &mut self,
14560 m: &mut MtpModule,
14561 pairs: &[(&[f32], u32)],
14562 first_pos: usize,
14563 ) -> bool {
14564 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14565 Some(p) => self.mtp_warm_batch_finish(m, p),
14566 None => false,
14567 }
14568 }
14569
14570 #[cfg(target_os = "macos")]
14575 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14576 let MetalWarmPending {
14577 mut graph,
14578 cpu_stored,
14579 b,
14580 } = pending;
14581 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14582 if !graph.sync() {
14583 return false;
14584 }
14585 spec_stamp("w.gpu");
14586 let mut kbuf = vec![0f32; b * nkv * hd];
14587 let mut vbuf = vec![0f32; b * nkv * hd];
14588 if !crate::gpu_metal::kv_mirror_read_rows(
14589 self.mtp_kv_id(),
14590 Self::MTP_LAYER_BASE,
14591 nkv,
14592 hd,
14593 cpu_stored,
14594 b,
14595 &mut kbuf,
14596 &mut vbuf,
14597 ) {
14598 return false;
14599 }
14600 for r in 0..b {
14601 m.kv.append(
14602 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14603 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14604 &[],
14605 );
14606 }
14607 crate::gpu_metal::kv_mirror_set_stored(
14608 self.mtp_kv_id(),
14609 Self::MTP_LAYER_BASE,
14610 cpu_stored + b,
14611 );
14612 spec_stamp("w.kv");
14613 true
14614 }
14615
14616 pub(crate) fn note_draft_id(&mut self, id: u32) {
14623 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14624 if (id as usize) >= cut {
14625 self.draft_full_streak = 16;
14626 } else {
14627 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14628 }
14629 }
14630
14631 fn draft_head_rows(&self, head_rows: usize) -> usize {
14634 if self.draft_full_streak > 0 {
14635 head_rows
14636 } else {
14637 Self::draft_vocab_rows(head_rows)
14638 }
14639 }
14640
14641 fn draft_vocab_rows(head_rows: usize) -> usize {
14644 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14645 let n = *N.get_or_init(|| {
14646 std::env::var("CMF_DRAFT_VOCAB")
14647 .ok()
14648 .and_then(|v| v.parse().ok())
14649 .unwrap_or(65536)
14650 });
14651 if n == 0 { head_rows } else { n.min(head_rows) }
14652 }
14653
14654 #[cfg(target_os = "macos")]
14659 fn mtp_step_metal(
14660 &mut self,
14661 m: &mut MtpModule,
14662 hidden: &[f32],
14663 next_token: u32,
14664 position: usize,
14665 want_logits: bool,
14666 ) -> Option<(Vec<f32>, Vec<f32>)> {
14667 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14668 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14669 || !crate::gpu::q1_force()
14670 || !crate::gpu::enabled_here()
14671 || self.attn_softcap > 0.0
14672 || self.attention_heads_per_layer.is_some()
14673 || m.kv.mode != crate::kv_cache::KvMode::F32
14674 || m.kv.o1.is_some()
14675 {
14676 return None;
14677 }
14678 let AttnKind::Full {
14679 wq,
14680 wk,
14681 wv,
14682 wo,
14683 q_norm,
14684 k_norm,
14685 output_gate,
14686 softplus_gate: None,
14687 bias: None,
14688 } = &m.layer.attn
14689 else {
14690 return None;
14691 };
14692 let FfnKind::Dense(d) = &m.layer.ffn else {
14693 return None;
14694 };
14695 if d.act != Act::Silu || !d.segs.is_empty() {
14696 return None;
14697 }
14698 let (pq, pk, pv, po) = (
14699 wq.q1_parts()?,
14700 wk.q1_parts()?,
14701 wv.q1_parts()?,
14702 wo.q1_parts()?,
14703 );
14704 let (g, u, dn) = (
14705 d.gate_proj.q1_parts()?,
14706 d.up_proj.q1_parts()?,
14707 d.down_proj.q1_parts()?,
14708 );
14709 let QTensor::Mapped { model, .. } = wq else {
14710 return None;
14711 };
14712 let model = model.clone();
14713 let lm = if want_logits {
14714 Some(self.weights.lm_head.q1_parts()?)
14715 } else {
14716 None
14717 };
14718 let dims = GraphDims {
14719 hidden: self.hidden_size,
14720 eps: self.rms_eps as f32,
14721 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14722 };
14723 let hs = self.hidden_size;
14726 let mut x = vec![0f32; hs];
14727 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14734 crate::gpu_metal::DENSE_MV,
14735 );
14736 let mut graph = TokenGraph::new(&model, dims, &x)?;
14737 let mut folded = false;
14738 if let Some(eh) = m.eh_proj.q1_parts() {
14739 let e = self.embed_single(next_token);
14740 let mut cat = vec![0.0f32; 2 * hs];
14741 let (cat_e, cat_h) = cat.split_at_mut(hs);
14742 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14743 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14744 folded = graph.encode_input_proj(eh, &cat);
14745 }
14746 if !folded {
14747 x = self.mtp_block_input(m, hidden, next_token);
14748 graph = TokenGraph::new(&model, dims, &x)?;
14749 }
14750 spec_stamp("d.in");
14751 let l = AttnGpuLayer {
14752 attn_norm: &m.layer.input_norm,
14753 post_norm: &m.layer.post_norm,
14754 wq: pq,
14755 wk: pk,
14756 wv: pv,
14757 wo: po,
14758 ffn: MetalFfn::Dense {
14759 gate: g,
14760 up: u,
14761 down: dn,
14762 act: (d.act == Act::Gelu) as u8,
14763 },
14764 attn_out_norm: None,
14765 ffn_out_norm: None,
14766 layer_scale: None,
14767 };
14768 let (nh, nkv, hd, rd) = (
14769 self.num_heads,
14770 self.num_kv_heads,
14771 self.head_dim,
14772 self.rotary_dim,
14773 );
14774 let inv_freq = self.inv_freq.clone();
14775 {
14776 let cache = &m.kv;
14777 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14778 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14779 let cpu_stored = cpu_k[0].len() / hd;
14780 let p = AttnDeviceParams {
14781 kv_id: self.mtp_kv_id(),
14782 layer: Self::MTP_LAYER_BASE,
14783 nh,
14784 nkv,
14785 hd,
14786 rd,
14787 position,
14788 scale: self.attn_scale,
14789 eps: self.rms_eps as f32,
14790 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14791 late_qk_norm: self.qk_norm_after_rope,
14792 output_gate: *output_gate,
14793 q_norm: q_norm.as_deref(),
14794 k_norm: k_norm.as_deref(),
14795 inv_freq: &inv_freq,
14796 rope_scale: self.rope_scale,
14797 cpu_k,
14798 cpu_v,
14799 cpu_stored,
14800 cpu_gen: cache.generation(),
14801 o1: None,
14802 window: None,
14803 softmax_sink: None,
14804 head_gate: None,
14805 v_norm: false,
14806 };
14807 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14808 return None;
14809 }
14810 }
14811 let draft_rows = if let Some(lm) = lm {
14817 self.draft_head_rows(lm.1)
14818 } else {
14819 0
14820 };
14821 if let Some(lm) = lm {
14822 if !graph.lm_head_ok(lm) {
14823 return None;
14824 }
14825 if draft_rows < lm.1 {
14826 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14827 return None;
14828 }
14829 } else {
14830 graph.encode_lm_head(&m.final_norm, lm);
14831 }
14832 }
14833 spec_stamp("d.enc");
14834 if graph.sync_checked().is_err() {
14835 return None;
14836 }
14837 spec_stamp("d.gpu");
14838 let mut logits = Vec::new();
14839 if let Some(lm) = lm {
14840 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14841 logits = attention::take_buf(n_read);
14842 graph.read_logits(&mut logits);
14843 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14845 }
14846 graph.finish(&mut x);
14847 let mut krow = attention::take_buf(nkv * hd);
14848 let mut vrow = attention::take_buf(nkv * hd);
14849 if crate::gpu_metal::kv_mirror_read_last(
14850 self.mtp_kv_id(),
14851 Self::MTP_LAYER_BASE,
14852 nkv,
14853 hd,
14854 &mut krow,
14855 &mut vrow,
14856 ) {
14857 m.kv.append(&krow, &vrow, &[]);
14858 }
14859 attention::recycle_buf(&mut krow);
14860 attention::recycle_buf(&mut vrow);
14861 spec_stamp("d.rd");
14862 Some((logits, x))
14863 }
14864
14865 fn mtp_chain_on() -> bool {
14878 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14879 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14880 }
14881
14882 #[cfg(target_os = "macos")]
14894 fn mtp_draft_chain_metal(
14895 &mut self,
14896 m: &mut MtpModule,
14897 hidden: &[f32],
14898 t_next: u32,
14899 position: usize,
14900 k: usize,
14901 ) -> Result<Vec<u32>, bool> {
14902 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14903 if k == 0
14904 || k > 64
14905 || !Self::mtp_chain_on()
14906 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14907 || !crate::gpu::q1_force()
14908 || !crate::gpu::enabled_here()
14909 || self.attn_softcap > 0.0
14910 || self.attention_heads_per_layer.is_some()
14911 || m.kv.mode != crate::kv_cache::KvMode::F32
14912 || m.kv.o1.is_some()
14913 || self.dsv4.is_some()
14915 || self.dsv41.is_some()
14916 || self.qwen4_exp.is_some()
14917 || self.per_token_stack()
14918 {
14919 return Err(false);
14920 }
14921 let AttnKind::Full {
14922 wq,
14923 wk,
14924 wv,
14925 wo,
14926 q_norm,
14927 k_norm,
14928 output_gate,
14929 softplus_gate: None,
14930 bias: None,
14931 } = &m.layer.attn
14932 else {
14933 return Err(false);
14934 };
14935 let FfnKind::Dense(d) = &m.layer.ffn else {
14936 return Err(false);
14937 };
14938 if d.act != Act::Silu || !d.segs.is_empty() {
14939 return Err(false);
14940 }
14941 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14942 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14943 else {
14944 return Err(false);
14945 };
14946 let (Some(g), Some(u), Some(dn)) = (
14947 d.gate_proj.q1_parts(),
14948 d.up_proj.q1_parts(),
14949 d.down_proj.q1_parts(),
14950 ) else {
14951 return Err(false);
14952 };
14953 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14954 return Err(false);
14955 };
14956 let QTensor::Mapped { model, .. } = wq else {
14957 return Err(false);
14958 };
14959 let model = model.clone();
14960 let QTensor::Mapped {
14963 model: em,
14964 idx: eidx,
14965 dtype: cortiq_core::TensorDtype::Q4TiledP,
14966 ..
14967 } = &self.weights.embed_tokens
14968 else {
14969 return Err(false);
14970 };
14971 if !std::sync::Arc::ptr_eq(em, &model)
14972 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14973 {
14974 return Err(false);
14975 }
14976 let embed = (
14977 *eidx,
14978 self.weights.embed_tokens.rows(),
14979 self.weights.embed_tokens.cols(),
14980 );
14981 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14982 return Err(false);
14983 }
14984 let dims = GraphDims {
14985 hidden: self.hidden_size,
14986 eps: self.rms_eps as f32,
14987 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14988 };
14989 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14993 crate::gpu_metal::DENSE_MV,
14994 );
14995 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14996 return Err(false);
14997 };
14998 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14999 return Err(false);
15000 }
15001 let l = AttnGpuLayer {
15002 attn_norm: &m.layer.input_norm,
15003 post_norm: &m.layer.post_norm,
15004 wq: pq,
15005 wk: pk,
15006 wv: pv,
15007 wo: po,
15008 ffn: MetalFfn::Dense {
15009 gate: g,
15010 up: u,
15011 down: dn,
15012 act: (d.act == Act::Gelu) as u8,
15013 },
15014 attn_out_norm: None,
15015 ffn_out_norm: None,
15016 layer_scale: None,
15017 };
15018 let (nh, nkv, hd, rd) = (
15019 self.num_heads,
15020 self.num_kv_heads,
15021 self.head_dim,
15022 self.rotary_dim,
15023 );
15024 let inv_freq = self.inv_freq.clone();
15025 let draft_rows = self.draft_head_rows(lm.1);
15026 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15027 if n_arg == 0 {
15028 return Err(false);
15029 }
15030 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15037 let t_chain = std::time::Instant::now();
15038 graph.chain_ids_init(t_next, k);
15039 let cpu_stored;
15040 {
15041 let cache = &m.kv;
15042 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15043 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15044 cpu_stored = cpu_k[0].len() / hd;
15045 for j in 0..k {
15046 if !graph.encode_chain_input(
15047 embed,
15048 j as u32,
15049 &m.enorm,
15050 &m.hnorm,
15051 self.embed_multiplier,
15052 eh,
15053 ) {
15054 return Err(false);
15055 }
15056 let p = AttnDeviceParams {
15060 kv_id: self.mtp_kv_id(),
15061 layer: Self::MTP_LAYER_BASE,
15062 nh,
15063 nkv,
15064 hd,
15065 rd,
15066 position: position + j,
15067 scale: self.attn_scale,
15068 eps: self.rms_eps as f32,
15069 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15070 late_qk_norm: self.qk_norm_after_rope,
15071 output_gate: *output_gate,
15072 q_norm: q_norm.as_deref(),
15073 k_norm: k_norm.as_deref(),
15074 inv_freq: &inv_freq,
15075 rope_scale: self.rope_scale,
15076 cpu_k: cpu_k.clone(),
15077 cpu_v: cpu_v.clone(),
15078 cpu_stored: cpu_stored + j,
15079 cpu_gen: cache.generation(),
15080 o1: None,
15081 window: None,
15082 softmax_sink: None,
15083 head_gate: None,
15084 v_norm: false,
15085 };
15086 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15087 return Err(false);
15088 }
15089 if draft_rows < lm.1 {
15090 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15091 return Err(false);
15092 }
15093 } else {
15094 graph.encode_lm_head(&m.final_norm, lm);
15095 }
15096 if !graph.encode_argmax(n_arg, j as u32 + 1) {
15097 return Err(false);
15098 }
15099 if split {
15100 graph.commit();
15103 }
15104 }
15105 }
15106 let t_enc = t_chain.elapsed();
15107 if graph.sync_checked().is_err() {
15108 return Err(true);
15109 }
15110 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
15111 eprintln!(
15112 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
15113 t_enc.as_secs_f64() * 1e3,
15114 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
15115 if split { ", split" } else { "" }
15116 );
15117 }
15118 let mut ids = vec![0u32; k];
15119 if !graph.chain_ids_read(&mut ids) {
15120 return Err(true);
15121 }
15122 let mut kbuf = vec![0f32; k * nkv * hd];
15123 let mut vbuf = vec![0f32; k * nkv * hd];
15124 if !crate::gpu_metal::kv_mirror_read_rows(
15125 self.mtp_kv_id(),
15126 Self::MTP_LAYER_BASE,
15127 nkv,
15128 hd,
15129 cpu_stored,
15130 k,
15131 &mut kbuf,
15132 &mut vbuf,
15133 ) {
15134 return Err(true);
15135 }
15136 for r in 0..k {
15137 m.kv.append(
15138 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15139 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15140 &[],
15141 );
15142 }
15143 Ok(ids)
15144 }
15145
15146 fn try_batch_graph_wgpu(
15147 &self,
15148 hiddens: &mut [f32],
15149 positions: &[usize],
15150 k: usize,
15151 spec: Option<crate::gpu::SpecTail<'_>>,
15152 ) -> crate::gpu::BatchGraphOutcome {
15153 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15154 }
15155
15156 fn try_batch_graph_wgpu_prefix(
15161 &self,
15162 hiddens: &mut [f32],
15163 positions: &[usize],
15164 k: usize,
15165 spec: Option<crate::gpu::SpecTail<'_>>,
15166 layers_run: Option<&mut usize>,
15167 ) -> crate::gpu::BatchGraphOutcome {
15168 let graph_end = match self.mimo_moe.graph_prefix_end() {
15169 Some(end) if end < self.num_layers => {
15170 if layers_run.is_none() || spec.is_some() || end == 0 {
15171 return crate::gpu::BatchGraphOutcome::Declined;
15172 }
15173 end
15174 }
15175 _ => self.num_layers,
15176 };
15177 let _tb = std::time::Instant::now();
15178 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15179 if self.attn_softcap > 0.0 {
15180 return crate::gpu::BatchGraphOutcome::Declined; }
15182 if let Some(reason) = self
15185 .wgpu_graph_attn_decline()
15186 .or_else(|| self.batch_graph_gemma_decline())
15187 {
15188 self.note_graph_decline("wgpu batch graph", reason);
15189 return crate::gpu::BatchGraphOutcome::Declined;
15190 }
15191 let nh = self.num_heads;
15192 let (nkv, hd, rd) = self.layer_geom(0);
15193 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15194 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15195 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15196 {
15197 let name = &m.tensors[i].name;
15198 let prism = if crate::prism::is_inverse_embedding(m, name) {
15199 crate::gpu::GraphPrismOp::InverseEmbedding
15200 } else if crate::prism::is_forward_weight(m, name) {
15201 crate::gpu::GraphPrismOp::Forward
15202 } else {
15203 crate::gpu::GraphPrismOp::None
15204 };
15205 return Some(crate::gpu::GraphW {
15206 idx: i,
15207 kind,
15208 row_scale: rs,
15209 data: &[],
15210 prism,
15211 affine: crate::prism::is_affine_target(m, name),
15212 });
15213 }
15214 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15215 eprintln!(
15216 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15217 t.rows(),
15218 t.cols()
15219 );
15220 }
15221 t.as_f32().map(|d| crate::gpu::GraphW {
15222 idx: 0,
15223 kind: 4,
15224 row_scale: &[],
15225 data: d,
15226 prism: crate::gpu::GraphPrismOp::None,
15227 affine: false,
15228 })
15229 }
15230 let built: Option<(
15231 Vec<crate::gpu::GraphLayer<'_>>,
15232 std::sync::Arc<cortiq_core::CmfModel>,
15233 )> = (|| {
15234 let mut layers = Vec::with_capacity(graph_end);
15235 let mut model = None;
15236 for li in 0..graph_end {
15237 let lw = &self.weights.layers[self.phys_layer(li)];
15238 let gffn = match &lw.ffn {
15245 FfnKind::Dense(d) if !d.segs.is_empty() => {
15246 if batch_debug {
15247 eprintln!("batch graph: dense segmented FFN at layer {li}");
15248 }
15249 return None;
15250 }
15251 FfnKind::Dense(d) => {
15252 let Some(act) = d.act.wgpu_graph_act() else {
15253 if batch_debug {
15254 eprintln!(
15255 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15256 d.act
15257 );
15258 }
15259 return None;
15260 };
15261 crate::gpu::GraphFfn::Dense {
15262 gate: gw(&d.gate_proj)?,
15263 up: gw(&d.up_proj)?,
15264 down: gw(&d.down_proj)?,
15265 act,
15266 }
15267 }
15268 FfnKind::Moe(m) => {
15269 if m.route_tau.is_some() || m.mask.is_some() {
15276 return None;
15277 }
15278 let shared = m.shared.as_ref();
15282 let has_shared = shared.is_some();
15283 let shared_gated = matches!(shared, Some((_, Some(_))));
15284 let sgate = match shared {
15285 Some((_, Some(sg))) => gw(sg)?,
15286 _ => gw(&m.router)?,
15290 };
15291 let router = gw(&m.router)?;
15292 if router.prism != crate::gpu::GraphPrismOp::None
15298 || router.affine
15299 || sgate.prism != crate::gpu::GraphPrismOp::None
15300 || sgate.affine
15301 {
15302 return None;
15303 }
15304 let inter = m.experts.first()?.gate_proj.rows();
15305 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15306 let mut q4tp: Option<bool> = None;
15307 let mut gu_q2: Option<bool> = None;
15308 let mut q82: Option<bool> = None;
15309 let mut moe_act = None;
15313 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15314 let Some(e_act) = e.act.wgpu_graph_act() else {
15315 return None;
15316 };
15317 match moe_act {
15318 Some(existing) if existing != e_act => return None,
15319 None => moe_act = Some(e_act),
15320 _ => {}
15321 }
15322 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15323 return None;
15324 }
15325 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15329 {
15330 Some((mm, gi)) => (
15331 mm,
15332 gi,
15333 e.up_proj.mapped_q4t()?.1,
15334 e.down_proj.mapped_q4t()?.1,
15335 false,
15336 false,
15337 false,
15338 ),
15339 None => match e.gate_proj.mapped_q2tp() {
15340 Some((mm, gi)) => (
15341 mm,
15342 gi,
15343 e.up_proj.mapped_q2tp()?.1,
15344 e.down_proj.mapped_q4tp()?.1,
15345 true,
15346 true,
15347 false,
15348 ),
15349 None => match e.gate_proj.mapped_q4tp() {
15350 Some((mm, gi)) => (
15351 mm,
15352 gi,
15353 e.up_proj.mapped_q4tp()?.1,
15354 e.down_proj.mapped_q4tp()?.1,
15355 true,
15356 false,
15357 false,
15358 ),
15359 None => {
15360 if !crate::gpu::moe_q82_graph_on() {
15361 return None;
15362 }
15363 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15364 (
15365 mm,
15366 gi,
15367 e.up_proj.mapped_q8_2f()?.1,
15368 e.down_proj.mapped_q8_2f()?.1,
15369 false,
15370 false,
15371 true,
15372 )
15373 }
15374 },
15375 },
15376 };
15377 if *q4tp.get_or_insert(is_p) != is_p
15378 || *gu_q2.get_or_insert(is_q2) != is_q2
15379 || *q82.get_or_insert(is_8) != is_8
15380 {
15381 return None;
15382 }
15383 if [gi, ui, di].into_iter().any(|idx| {
15384 mm.tensors.get(idx).is_some_and(|t| {
15385 crate::prism::is_forward_weight(mm, &t.name)
15386 || crate::prism::is_affine_target(mm, &t.name)
15387 })
15388 }) {
15389 return None;
15390 }
15391 model.get_or_insert_with(|| mm.clone());
15392 experts.push((gi, ui, di));
15393 }
15394 crate::gpu::GraphFfn::Moe {
15395 router,
15396 shared_gate: sgate,
15397 experts,
15398 n_exp: m.experts.len(),
15399 top_k: m.top_k,
15400 inter,
15401 act: moe_act?,
15402 norm_topk: m.norm_topk_prob,
15403 q4tp: q4tp?,
15404 gu_q2: gu_q2.unwrap_or(false),
15405 q82: q82.unwrap_or(false),
15406 sigmoid: m.router_sigmoid,
15407 sigmoid_denom_eps: m.sigmoid_denom_eps,
15408 bias: m.expert_bias.as_deref(),
15409 has_shared,
15410 detached_shared: None,
15411 shared_gated,
15412 route_scale: m.routed_scaling,
15413 }
15414 }
15415 _ => return None,
15416 };
15417 let attn = match &lw.attn {
15418 AttnKind::Full {
15419 wq,
15420 wk,
15421 wv,
15422 wo,
15423 q_norm,
15424 k_norm,
15425 output_gate,
15426 softplus_gate,
15427 bias,
15428 } => {
15429 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15430 if batch_debug {
15431 eprintln!(
15432 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15433 softplus_gate.is_some(),
15434 self.attention_heads_per_layer.is_some()
15435 );
15436 }
15437 return None;
15438 }
15439 let (m, _, _, _) =
15440 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15441 model = Some(m.clone());
15442 crate::gpu::GraphAttn::Full {
15443 wq: gw(wq)?,
15444 wk: gw(wk)?,
15445 wv: gw(wv)?,
15446 wo: gw(wo)?,
15447 q_norm: q_norm.as_deref(),
15448 k_norm: k_norm.as_deref(),
15449 late_qk_norm: self.qk_norm_after_rope,
15450 bias: bias
15451 .as_ref()
15452 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15453 output_gate: *output_gate,
15454 cpu_k: self.kv_cache.layers[li].k_heads(),
15455 cpu_v: self.kv_cache.layers[li].v_heads(),
15456 cpu_base: self.kv_cache.layers[li].base(),
15457 geom: self.graph_attn_geom(li),
15458 head_gate: None,
15461 }
15462 }
15463 AttnKind::LinearGdn(w) => {
15464 let Some(cfg) = self.gdn_cfg else {
15465 if batch_debug {
15466 eprintln!("batch graph: no GDN config at layer {li}");
15467 }
15468 return None;
15469 };
15470 let (m, _, _, _) = w
15471 .in_proj_qkv
15472 .graph_weight()
15473 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15474 model = Some(m.clone());
15475 crate::gpu::GraphAttn::Gdn {
15476 qkv: gw(&w.in_proj_qkv)?,
15477 z: gw(&w.in_proj_z)?,
15478 a: gw(&w.in_proj_a)?,
15479 b: gw(&w.in_proj_b)?,
15480 out: gw(&w.out_proj)?,
15481 conv1d: &w.conv1d,
15482 a_log: &w.a_log,
15483 dt_bias: &w.dt_bias,
15484 norm: &w.norm,
15485 nv: cfg.num_v_heads,
15486 nk: cfg.num_k_heads,
15487 dk: cfg.key_head_dim,
15488 dv: cfg.value_head_dim,
15489 kk: cfg.conv_kernel,
15490 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15491 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15492 }
15493 }
15494 _ => return None,
15495 };
15496 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15499 layers.push(crate::gpu::GraphLayer {
15500 input_norm: &lw.input_norm,
15501 attn,
15502 post_norm: &lw.post_norm,
15503 attn_out_norm: if g4 {
15504 lw.attn_out_norm.as_deref()
15505 } else {
15506 None
15507 },
15508 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15509 layer_scale: if g4 { lw.layer_scale } else { None },
15510 ffn: gffn,
15511 });
15512 }
15513 Some((layers, model?))
15514 })();
15515 let Some((layers, model)) = built else {
15516 {
15517 use std::sync::atomic::{AtomicBool, Ordering};
15518 static SAID: AtomicBool = AtomicBool::new(false);
15519 if !SAID.swap(true, Ordering::Relaxed) {
15520 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15521 }
15522 }
15523 return crate::gpu::BatchGraphOutcome::Declined;
15524 };
15525 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15526 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15527 }
15528 crate::gpu::forward_batch_graph(
15529 &model,
15530 self.graph_kv_id,
15531 &layers,
15532 &self.inv_freq,
15533 hiddens,
15534 nh,
15535 nkv,
15536 hd,
15537 rd,
15538 self.hidden_size,
15539 self.intermediate_size,
15540 positions,
15541 self.kv_cache.max_seq_len,
15542 gemma,
15543 self.rms_eps as f32,
15544 self.attn_scale,
15545 k,
15546 &(0..graph_end)
15547 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15548 .collect::<Vec<_>>(),
15549 self.o1_epoch,
15550 spec,
15551 layers_run,
15552 )
15553 }
15554
15555 fn draft_probe() -> bool {
15559 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15560 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15561 }
15562
15563 #[cfg(feature = "gpu")]
15575 fn dsv4_spec_on() -> bool {
15576 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15577 *ON.get_or_init(|| {
15578 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15582 return v != "0";
15583 }
15584 std::env::var("CMF_DSV4_SPEC")
15591 .map(|v| v != "0")
15592 .unwrap_or_else(|_| {
15593 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15594 })
15595 })
15596 }
15597
15598 #[cfg(feature = "gpu")]
15605 fn dsv4_spec_step(
15606 &mut self,
15607 tip_token: u32,
15608 t_next: u32,
15609 next_pos: usize,
15610 max_extra: usize,
15611 drafted: &mut usize,
15612 accepted_ctr: &mut usize,
15613 ) -> Option<(Vec<u32>, usize)> {
15614 let t_all = std::time::Instant::now();
15615 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15616 thread_local! {
15617 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15618 const { std::cell::Cell::new(None) };
15619 }
15620 LAST.with(|l| {
15621 if let Some(prev) = l.get() {
15622 eprintln!(
15623 "between rounds {:.1} ms",
15624 prev.elapsed().as_secs_f64() * 1e3
15625 );
15626 }
15627 l.set(Some(std::time::Instant::now()));
15628 });
15629 }
15630 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15631 eprintln!("spec_step: enter pos={next_pos}");
15632 }
15633 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15634 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15635 if self.dspark.is_none() {
15637 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15638 if t.is_empty() {
15639 return None;
15640 }
15641 crate::dsv4::dspark_arm(&t, cfg.dim);
15642 self.dspark = Some(crate::dsv4::DsparkState::new(
15643 self.dsv4_mtp.len(),
15644 &cfg,
15645 t.len(),
15646 ));
15647 }
15648 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15649 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15650 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15651 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15652 }
15653 let pack = pack?;
15654 let block = crate::dsv4::dspark_block();
15655 let b_box = self.dsv4.as_mut()?;
15656 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15657 let ds = self.dspark.as_mut()?;
15658 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15661 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15662 if dbg {
15663 eprintln!("spec_step: no capture");
15664 }
15665 return None;
15666 }
15667 ds.have_hidden = true;
15668 let tip_pos = next_pos.checked_sub(1)?;
15669 let draft_started = std::time::Instant::now();
15670 let mut conf = Vec::new();
15671 let props = crate::dsv4::dspark_draft_gpu(
15672 g,
15673 &self.dsv4_mtp,
15674 &cfg,
15675 ds,
15676 pack,
15677 st.kv_id,
15678 tip_token,
15679 tip_pos,
15680 self.pool.as_deref(),
15681 &mut conf,
15682 );
15683 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15684 *drafted += block;
15685 if props.is_empty() || props[0] != t_next {
15686 if dbg {
15687 eprintln!(
15688 "spec_step: draft {} (props0={:?} t_next={t_next})",
15689 if props.is_empty() {
15690 "empty"
15691 } else {
15692 "miss"
15693 },
15694 props.first()
15695 );
15696 }
15697 return None;
15698 }
15699 let mut k_verify = crate::dsv4::dspark_verify_k()
15706 .min(props.len())
15707 .min(max_extra.saturating_add(1));
15708 let conf_min = {
15714 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15715 *M.get_or_init(|| {
15716 std::env::var("CMF_DSPARK_CONF_MIN")
15717 .ok()
15718 .and_then(|v| v.parse().ok())
15719 .unwrap_or(0.0)
15720 })
15721 };
15722 if conf_min > 0.0 && conf.len() >= props.len() {
15723 let mut keep = 1usize;
15724 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15725 keep += 1;
15726 }
15727 k_verify = k_verify.min(keep.max(2));
15728 }
15729 if k_verify < 2 {
15730 return None;
15731 }
15732 let mut fed = Vec::with_capacity(k_verify);
15733 fed.push(t_next);
15734 fed.extend_from_slice(&props[1..k_verify]);
15735 let mut argmax = Vec::new();
15736 let mut logits_all = Vec::new();
15737 let mut walked = Vec::new();
15738 let txn = crate::dsv4::dsv4_verify_chunk(
15739 g,
15740 layers,
15741 &cfg,
15742 st,
15743 &fed,
15744 next_pos,
15745 &self.inv_freq,
15746 self.pool.as_deref(),
15747 &targets,
15748 &mut argmax,
15749 &mut logits_all,
15750 &mut walked,
15751 );
15752 if txn.is_none() && dbg {
15753 eprintln!("spec_step: verify declined");
15754 }
15755 let txn = txn?;
15756 let spec_gpu_end = txn.gpu_end;
15757 let b = fed.len();
15758 let mut accepted = 1usize;
15759 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15760 accepted += 1;
15761 }
15762 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15767 accepted = 1;
15768 }
15769 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15770 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15771 }
15772 let t_fin = std::time::Instant::now();
15773 if !crate::dsv4::dsv4_spec_finish(
15774 g,
15775 layers,
15776 &cfg,
15777 st,
15778 txn,
15779 accepted,
15780 &fed,
15781 &self.inv_freq,
15782 self.pool.as_deref(),
15783 ) {
15784 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15785 return None;
15786 }
15787 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15788 eprintln!(
15789 "finish(k={accepted}): {:.1} ms",
15790 t_fin.elapsed().as_secs_f64() * 1e3
15791 );
15792 }
15793 *accepted_ctr += accepted - 1;
15794 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15799 let dev_caps: Vec<usize> = targets
15804 .iter()
15805 .copied()
15806 .filter(|&t| t < spec_gpu_end)
15807 .collect();
15808 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15809 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15810 return None;
15811 }
15812 for t in 0..accepted {
15813 let tip = t + 1 == accepted;
15814 for (slot, &tl) in targets.iter().enumerate() {
15815 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15816 let lo = (di * b + t) * hc * dim;
15817 crate::dsv4::dspark_capture(
15818 &caps_all[lo..lo + hc * dim],
15819 &cfg,
15820 slot,
15821 &mut ds.main_hidden,
15822 );
15823 } else if tip
15824 && crate::dsv4::dspark_peek_slot(slot, dim, {
15825 let lo = slot * dim;
15826 &mut ds.main_hidden[lo..lo + dim]
15827 })
15828 {
15829 } else {
15834 crate::dsv4::dspark_capture(
15838 &walked[t * hc * dim..(t + 1) * hc * dim],
15839 &cfg,
15840 slot,
15841 &mut ds.main_hidden,
15842 );
15843 }
15844 }
15845 crate::dsv4::dspark_ring_append(
15846 g,
15847 &self.dsv4_mtp,
15848 &cfg,
15849 ds,
15850 next_pos + t,
15851 self.pool.as_deref(),
15852 );
15853 }
15854 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15855 self.graph_logits = Some(row);
15856 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15861 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15862 crate::dsv4::pick_tally_arm();
15863 }
15864 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15865 eprintln!(
15866 "spec_step total {:.1} ms (k={accepted})",
15867 t_all.elapsed().as_secs_f64() * 1e3
15868 );
15869 }
15870 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15871 }
15872
15873 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15874 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15875 return;
15876 }
15877 let trunk_now = crate::dsv4::pick_tally_take();
15879 crate::dsv4::trunk_freq_note(&trunk_now);
15880 if !trunk_now.is_empty() {
15881 self.dspark_trunk_picks.push(trunk_now);
15882 let keep = crate::dsv4::dspark_block();
15883 if self.dspark_trunk_picks.len() > keep {
15884 self.dspark_trunk_picks.remove(0);
15885 }
15886 }
15887 for p in std::mem::take(&mut self.dspark_pending) {
15890 let Some(i) = position.checked_sub(p.0 + 1) else {
15891 continue;
15892 };
15893 let mut p = p;
15894 if i < p.1.len() {
15895 if p.2 && p.1[i] == token_id {
15896 p.3 = i + 1;
15897 } else {
15898 p.2 = false;
15899 }
15900 if i + 1 < p.1.len() {
15901 self.dspark_pending.push(p);
15902 continue;
15903 }
15904 }
15905 self.dspark_hist.push(p.3);
15906 self.dspark_real.push(token_id);
15907 }
15908 let Some(b) = &mut self.dsv4 else { return };
15909 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15910 let n_layers = layers.len();
15911 if self.dspark.is_none() {
15912 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15913 if t.is_empty() {
15914 return;
15915 }
15916 eprintln!(
15917 "DSpark: capture from layers {t:?}, block {}",
15918 crate::dsv4::dspark_block()
15919 );
15920 crate::dsv4::dspark_arm(&t, cfg.dim);
15921 self.dspark = Some(crate::dsv4::DsparkState::new(
15922 self.dsv4_mtp.len(),
15923 &cfg,
15924 t.len(),
15925 ));
15926 }
15927 let ds = self.dspark.as_mut().unwrap();
15928 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15929 return; }
15931 let mut conf = Vec::new();
15932 crate::dsv4::pick_tally_arm();
15933 let draft_started = std::time::Instant::now();
15938 #[cfg(feature = "gpu")]
15939 let gpu_draft = crate::dsv4::dspark_gpu_on();
15940 #[cfg(not(feature = "gpu"))]
15941 let gpu_draft = false;
15942 let props = if gpu_draft {
15943 #[cfg(feature = "gpu")]
15944 {
15945 let kv_id = b.3.kv_id;
15946 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15947 Some(pk) => crate::dsv4::dspark_draft_gpu(
15948 g,
15949 &self.dsv4_mtp,
15950 &cfg,
15951 ds,
15952 pk,
15953 kv_id,
15954 token_id,
15955 position,
15956 self.pool.as_deref(),
15957 &mut conf,
15958 ),
15959 None => Vec::new(),
15960 }
15961 }
15962 #[cfg(not(feature = "gpu"))]
15963 Vec::new()
15964 } else {
15965 crate::gpu::cpu_scope(|| {
15966 crate::dsv4::dspark_draft(
15967 g,
15968 &self.dsv4_mtp,
15969 &cfg,
15970 ds,
15971 token_id,
15972 position,
15973 self.pool.as_deref(),
15974 &mut conf,
15975 )
15976 })
15977 };
15978 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15979 let draft_picks = crate::dsv4::pick_tally_take();
15980 crate::dsv4::dspark_freq_note(&draft_picks);
15981 crate::dsv4::pick_tally_arm();
15984 if !props.is_empty() {
15985 let (tu, tt) = {
15989 let flat: Vec<(usize, Vec<usize>)> = self
15990 .dspark_trunk_picks
15991 .iter()
15992 .flat_map(|v| v.iter().cloned())
15993 .collect();
15994 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15996 std::collections::HashMap::new();
15997 for (li, picks) in flat {
15998 per.entry(li).or_default().extend(picks);
15999 }
16000 let n = per.len().max(1);
16001 let mut u = 0usize;
16002 let mut t = 0usize;
16003 for (_, v) in per {
16004 t += v.len();
16005 u += v.iter().collect::<std::collections::HashSet<_>>().len();
16006 }
16007 (u / n, t / n)
16008 };
16009 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
16010 self.dspark_exp.push((tu, tt, du, dt));
16011 self.dspark_pending.push((position, props, true, 0));
16012 }
16013 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
16014 let n = self.dspark_hist.len() as f32;
16015 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
16016 let block = crate::dsv4::dspark_block();
16017 let mut at = vec![0usize; block + 1];
16018 for &k in &self.dspark_hist {
16019 at[k] += 1;
16020 }
16021 let mut surv = Vec::with_capacity(block);
16023 for i in 1..=block {
16024 let k = at[i..].iter().sum::<usize>() as f32 / n;
16025 surv.push(format!("{k:.2}"));
16026 }
16027 let distinct = self
16028 .dspark_real
16029 .iter()
16030 .collect::<std::collections::HashSet<_>>()
16031 .len();
16032 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16033 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16034 });
16035 let m = self.dspark_exp.len().max(1);
16036 eprintln!(
16037 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16038 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16039 self.dspark_hist.len(),
16040 mean + 1.0,
16041 surv.join(" ")
16042 );
16043 eprintln!(
16044 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16045 trunk experts {}/{} per layer over {block} tokens, \
16046 draft {}/{} per block, draft {:.2} ms/block",
16047 self.dspark_real.len(),
16048 tu / m,
16049 tt / m,
16050 du / m,
16051 dt / m,
16052 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16053 );
16054 }
16055 }
16056
16057 fn forward_layers_upto(
16058 &mut self,
16059 hidden: &[f32],
16060 position: usize,
16061 task_mask: Option<&TaskMask>,
16062 upto: Option<usize>,
16063 ) -> Vec<f32> {
16064 if let Some(plan) = self.gpu_plan.clone() {
16070 if upto.is_none() && plan.len() > 1 {
16071 let mut h = hidden.to_vec();
16072 for &(dev, from, upto_incl) in plan.iter() {
16073 h = crate::gpu::with_device(dev, || {
16074 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
16075 });
16076 }
16077 return h;
16078 }
16079 }
16080 self.forward_layers_span(hidden, position, task_mask, 0, upto)
16081 }
16082
16083 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
16088 self.set_gpu_plan_at(devices, None)
16089 }
16090
16091 pub fn set_gpu_plan_at(
16095 &mut self,
16096 devices: Option<&[usize]>,
16097 at: Option<usize>,
16098 ) -> Result<(), String> {
16099 let Some(devs) = devices.filter(|d| d.len() > 1) else {
16100 self.gpu_plan = None;
16101 return Ok(());
16102 };
16103 self.split_supported()?;
16104 let n = self.num_layers;
16105 if devs.len() > n {
16106 return Err(format!("{} devices for {n} layers", devs.len()));
16107 }
16108 if let Some(k) = at {
16109 if k == 0 || k >= n {
16110 return Err(format!("split at {k}: the model has {n} layers"));
16111 }
16112 if devs.len() == 2 {
16113 self.gpu_plan = Some(std::sync::Arc::new(vec![
16114 (devs[0], 0, k - 1),
16115 (devs[1], k, n - 1),
16116 ]));
16117 return Ok(());
16118 }
16119 return Err(format!(
16120 "an explicit split point takes exactly 2 devices, got {}",
16121 devs.len()
16122 ));
16123 }
16124 let per = n.div_ceil(devs.len());
16125 let mut plan = Vec::with_capacity(devs.len());
16126 let mut from = 0usize;
16127 for &d in devs {
16128 if from >= n {
16129 break;
16130 }
16131 let upto = (from + per - 1).min(n - 1);
16132 plan.push((d, from, upto));
16133 from = upto + 1;
16134 }
16135 self.gpu_plan = Some(std::sync::Arc::new(plan));
16136 Ok(())
16137 }
16138
16139 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16141 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16142 }
16143
16144 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16150 if let Some(x) = t.as_f32() {
16151 return x.to_vec();
16152 }
16153 let mut out = vec![0.0; t.rows() * t.cols()];
16154 for r in 0..t.rows() {
16155 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16156 }
16157 out
16158 }
16159
16160 fn embryo_resident_eligible(&self) -> bool {
16161 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16164 || self.num_layers != self.physical_layers
16165 || self.loop_final_norm
16166 || self.weights.layers.len() != self.num_layers
16167 || self.head_clusters.is_none()
16168 || self.final_softcap.is_some()
16169 || self.logit_multiplier.is_some()
16170 || self.attn_softcap != 0.0
16171 || self.mtp.is_some()
16172 || self.per_token_stack()
16173 || self.dsv4.is_some()
16174 || self.dsv41.is_some()
16175 || self.qwen4_exp.is_some()
16176 || self.dyn_router.is_some()
16183 || self.dyn_phi_layer.is_some()
16184 || self.dyn_blend_loaded
16185 || self.o1_cfg.is_some()
16186 || self.swa.is_some()
16187 || self.sliding_layers.is_some()
16188 || self.global_attn.is_some()
16189 || self.attention_heads_per_layer.is_some()
16190 || self.attn_v_norm
16191 || self
16192 .kv_cache
16193 .layers
16194 .iter()
16195 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16196 || self.rope_scale != 1.0
16197 || self.rope_scale_local != 1.0
16198 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16199 || self.hidden_size == 0
16200 || self.hidden_size > 1024
16201 || self.intermediate_size > 1024
16202 || self.num_heads == 0
16203 || self.num_kv_heads == 0
16204 || self.num_heads % self.num_kv_heads != 0
16205 || self.num_heads.saturating_mul(self.head_dim) > 1024
16206 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16207 || self.vocab_size == 0
16208 || self.kv_cache.max_seq_len == 0
16209 || self.rotary_dim == 0
16210 || self.rotary_dim > self.head_dim
16211 || self.rotary_dim % 2 != 0
16212 || self.inv_freq.len() < self.rotary_dim / 2
16213 {
16214 return false;
16215 }
16216 if self.weights.lm_head.as_f32().is_none()
16227 || self.weights.embed_tokens.as_f32().is_none()
16228 || self.weights.lm_head.rows() < self.vocab_size
16229 || self.weights.lm_head.cols() != self.hidden_size
16230 || self.weights.embed_tokens.rows() < self.vocab_size
16231 || self.weights.embed_tokens.cols() != self.hidden_size
16232 || self.weights.final_norm.len() != self.hidden_size
16233 {
16234 return false;
16235 }
16236 if let Some(cfg) = self.vmf_cfg {
16237 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16238 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16239 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16240 || cfg.state_len() == 0
16241 {
16242 return false;
16243 }
16244 }
16245 if let Some(g) = self.gdn_cfg {
16246 if g.num_v_heads == 0
16251 || g.num_k_heads == 0
16252 || g.num_v_heads % g.num_k_heads != 0
16253 || g.key_head_dim == 0
16254 || g.key_head_dim > 128
16255 || g.value_head_dim == 0
16256 || g.value_head_dim > 256
16257 || g.value_head_dim % 4 != 0
16258 || g.conv_kernel == 0
16259 || g.num_v_heads > 512
16260 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16261 || g.conv_dim() > 2048
16262 || g.conv_dim() % 4 != 0
16263 || g.hidden_size != self.hidden_size
16264 || g.output_gate_sigmoid
16265 || g.rms_eps != self.rms_eps
16266 || g.state_len() == 0
16267 {
16268 return false;
16269 }
16270 }
16271 let mut full_seen = false;
16272 for lw in &self.weights.layers {
16273 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16274 return false;
16275 }
16276 match &lw.attn {
16277 AttnKind::LinearGdn(w) => {
16278 let Some(g) = self.gdn_cfg else {
16279 return false;
16280 };
16281 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16282 if w.in_proj_qkv.rows() != g.conv_dim()
16283 || w.in_proj_qkv.cols() != self.hidden_size
16284 || w.in_proj_qkv.as_f32().is_none()
16285 || w.in_proj_z.rows() != nv * dv
16286 || w.in_proj_z.cols() != self.hidden_size
16287 || w.in_proj_z.as_f32().is_none()
16288 || w.in_proj_a.rows() != nv
16289 || w.in_proj_a.cols() != self.hidden_size
16290 || w.in_proj_a.as_f32().is_none()
16291 || w.in_proj_b.rows() != nv
16292 || w.in_proj_b.cols() != self.hidden_size
16293 || w.in_proj_b.as_f32().is_none()
16294 || w.conv1d.len() != g.conv_dim() * kk
16295 || w.a_log.len() != nv
16296 || w.dt_bias.len() != nv
16297 || w.norm.len() != dv
16298 || w.out_proj.rows() != self.hidden_size
16299 || w.out_proj.cols() != nv * dv
16300 || w.out_proj.as_f32().is_none()
16301 {
16302 return false;
16303 }
16304 }
16305 AttnKind::Linear(w) => {
16306 let Some(cfg) = self.vmf_cfg else {
16307 return false;
16308 };
16309 if w.thq.rows() != cfg.num_heads * cfg.nphase
16310 || w.thq.cols() != self.hidden_size
16311 || w.thq.as_f32().is_none()
16312 || w.thk.rows() != cfg.num_heads * cfg.nphase
16313 || w.thk.cols() != self.hidden_size
16314 || w.thk.as_f32().is_none()
16315 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16316 || w.v_proj.cols() != self.hidden_size
16317 || w.v_proj.as_f32().is_none()
16318 || w.out_proj.rows() != self.hidden_size
16319 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16320 || w.out_proj.as_f32().is_none()
16321 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16322 {
16323 return false;
16324 }
16325 if let Some((kg, kb)) = &w.k_gate {
16326 if kg.rows() != cfg.num_heads
16327 || kg.cols() != self.hidden_size
16328 || kg.as_f32().is_none()
16329 || kb.len() != cfg.num_heads
16330 {
16331 return false;
16332 }
16333 }
16334 if let Some(conv) = &w.conv {
16335 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16336 return false;
16337 }
16338 }
16339 }
16340 AttnKind::Full {
16341 wq,
16342 wk,
16343 wv,
16344 wo,
16345 q_norm,
16346 k_norm,
16347 output_gate,
16348 softplus_gate,
16349 bias,
16350 } => {
16351 if full_seen
16352 || q_norm.is_some()
16353 || k_norm.is_some()
16354 || *output_gate
16355 || softplus_gate.is_some()
16356 || bias.is_some()
16357 || wq.as_f32().is_none()
16358 || wk.as_f32().is_none()
16359 || wv.as_f32().is_none()
16360 || wo.as_f32().is_none()
16361 || wq.rows() != self.num_heads * self.head_dim
16362 || wk.rows() != self.num_kv_heads * self.head_dim
16363 || wv.rows() != self.num_kv_heads * self.head_dim
16364 || wq.cols() != self.hidden_size
16365 || wk.cols() != self.hidden_size
16366 || wv.cols() != self.hidden_size
16367 || wo.rows() != self.hidden_size
16368 || wo.cols() != self.num_heads * self.head_dim
16369 {
16370 return false;
16371 }
16372 full_seen = true;
16373 }
16374 AttnKind::Bounded(w) => {
16375 let Some(ac) = self.anchor_core.as_ref() else {
16378 return false;
16379 };
16380 if self.bounded_rope.is_none()
16381 || w.window != ac.window
16382 || w.sink != ac.sink
16383 || w.window == 0
16384 || w.window + w.sink > 256
16385 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16386 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16387 || w.wq.as_f32().is_none()
16388 || w.wk.as_f32().is_none()
16389 || w.wv.as_f32().is_none()
16390 || w.wo.as_f32().is_none()
16391 || w.wq.rows() != self.num_heads * self.head_dim
16392 || w.wk.rows() != self.num_kv_heads * self.head_dim
16393 || w.wv.rows() != self.num_kv_heads * self.head_dim
16394 || w.wq.cols() != self.hidden_size
16395 || w.wk.cols() != self.hidden_size
16396 || w.wv.cols() != self.hidden_size
16397 || w.wo.rows() != self.hidden_size
16398 || w.wo.cols() != self.num_heads * self.head_dim
16399 {
16400 return false;
16401 }
16402 }
16403 _ => return false,
16404 }
16405 match &lw.ffn {
16406 FfnKind::Dense(d) => {
16407 if d.act != Act::Silu
16408 || !d.segs.is_empty()
16409 || d.gate_proj.as_f32().is_none()
16410 || d.up_proj.as_f32().is_none()
16411 || d.down_proj.as_f32().is_none()
16412 || d.gate_proj.rows() != self.intermediate_size
16413 || d.gate_proj.cols() != self.hidden_size
16414 || d.up_proj.rows() != self.intermediate_size
16415 || d.up_proj.cols() != self.hidden_size
16416 || d.down_proj.rows() != self.hidden_size
16417 || d.down_proj.cols() != self.intermediate_size
16418 {
16419 return false;
16420 }
16421 }
16422 FfnKind::Moe(m) => {
16423 if m.resonance.is_none()
16424 || m.top_k != 1
16425 || m.router_sigmoid
16426 || !m.norm_topk_prob
16427 || m.expert_bias.is_some()
16428 || m.routed_scaling != 1.0
16429 || m.route_tau.is_some()
16430 || m.shared.is_none()
16431 || m.mask.is_some()
16432 || m.per_expert_scale.is_some()
16433 || m.router_input_norm
16434 || m.experts.is_empty()
16435 || m.experts.len() > 8
16436 {
16437 return false;
16438 }
16439 let r = m.resonance.as_ref().unwrap();
16440 if r.mu.len() != m.experts.len() * self.hidden_size
16441 || r.bias.len() != m.experts.len()
16442 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16443 || r.k > 128
16444 {
16445 return false;
16446 }
16447 let Some((shared, gate)) = &m.shared else {
16448 return false;
16449 };
16450 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16451 return false;
16452 }
16453 if shared.gate_proj.as_f32().is_none()
16454 || shared.up_proj.as_f32().is_none()
16455 || shared.down_proj.as_f32().is_none()
16456 || shared.gate_proj.rows() != self.intermediate_size
16457 || shared.gate_proj.cols() != self.hidden_size
16458 || shared.up_proj.rows() != self.intermediate_size
16459 || shared.up_proj.cols() != self.hidden_size
16460 || shared.down_proj.rows() != self.hidden_size
16461 || shared.down_proj.cols() != self.intermediate_size
16462 {
16463 return false;
16464 }
16465 for e in &m.experts {
16466 if e.act != Act::Silu
16467 || !e.segs.is_empty()
16468 || e.gate_proj.as_f32().is_none()
16469 || e.up_proj.as_f32().is_none()
16470 || e.down_proj.as_f32().is_none()
16471 || e.gate_proj.rows() != self.intermediate_size
16472 || e.gate_proj.cols() != self.hidden_size
16473 || e.up_proj.rows() != self.intermediate_size
16474 || e.up_proj.cols() != self.hidden_size
16475 || e.down_proj.rows() != self.hidden_size
16476 || e.down_proj.cols() != self.intermediate_size
16477 {
16478 return false;
16479 }
16480 }
16481 }
16482 FfnKind::DenseMoe(_) => return false,
16483 }
16484 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16485 return false;
16486 }
16487 }
16488 if full_seen && self.anchor_core.is_some() {
16489 return false;
16490 }
16491 full_seen || self.num_layers > 0
16492 }
16493
16494 fn embryo_resident_wanted(&self) -> bool {
16499 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16500 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16501 && matches!(
16502 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16503 Ok("1") | Ok("parallel")
16504 )
16505 && crate::gpu::enabled_here()
16506 && !self.graph_refused()
16507 && self.embryo_resident_eligible()
16508 }
16509
16510 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16518 if ids.len() < 2
16519 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16520 || !self.embryo_resident_wanted()
16521 {
16522 return None;
16523 }
16524 let model = self.ensure_embryo_graph()?;
16525 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16526 .ok()
16527 .and_then(|v| v.parse::<usize>().ok())
16528 .filter(|&v| v >= 1)
16529 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16530 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16531 let hs = self.hidden_size;
16532 let n = ids.len();
16533 let mut pos = start;
16534 let mut last = None;
16535 let mut rows = Vec::with_capacity(cmax * hs);
16536 while pos < n {
16537 let end = (pos + cmax).min(n);
16538 rows.clear();
16539 for &id in &ids[pos..end] {
16540 rows.extend_from_slice(&self.embed_single(id));
16541 }
16542 let mut lg = Vec::new();
16543 if !crate::gpu::forward_embryo_graph_chunk(
16544 &model,
16545 self.graph_kv_id,
16546 &rows,
16547 pos,
16548 end - pos,
16549 &mut lg,
16550 ) {
16551 if pos == start {
16552 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16553 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16554 }
16555 return None;
16556 }
16557 self.kv_cache.clear();
16561 self.clear_history();
16562 crate::gpu::graph_kv_reset(self.graph_kv_id);
16563 panic!(
16564 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16565 );
16566 }
16567 last = Some(lg);
16568 pos = end;
16569 }
16570 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16571 eprintln!(
16572 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16573 n - start,
16574 (n - start).div_ceil(cmax)
16575 );
16576 }
16577 last
16578 }
16579
16580 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16581 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16582 const UMAX: u32 = u32::MAX;
16583 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16584 const REC: usize = 64;
16585 struct Pack {
16586 data: Vec<f32>,
16587 }
16588 impl Pack {
16589 fn put(&mut self, x: &[f32]) -> u32 {
16590 if x.is_empty() {
16591 return u32::MAX;
16592 }
16593 let off = self.data.len();
16594 self.data.extend_from_slice(x);
16595 off as u32
16596 }
16597 }
16598 let vmf = self.vmf_cfg;
16602 let gdn = self.gdn_cfg;
16603 let mut pack = Pack { data: Vec::new() };
16604 let mut meta = vec![0u32; HEADER];
16605 meta[0] = self.hidden_size as u32;
16606 meta[1] = self.intermediate_size as u32;
16607 meta[2] = self.vocab_size as u32;
16608 meta[3] = self.num_layers as u32;
16609 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16610 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16611 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16612 if let Some(g) = gdn {
16613 meta[24] = g.num_v_heads as u32;
16614 meta[25] = g.num_k_heads as u32;
16615 meta[26] = g.key_head_dim as u32;
16616 meta[27] = g.value_head_dim as u32;
16617 meta[28] = g.conv_kernel as u32;
16618 meta[29] = g.conv_dim() as u32;
16619 }
16620 meta[7] = self.num_heads as u32;
16621 meta[8] = self.num_kv_heads as u32;
16622 meta[9] = self.head_dim as u32;
16623 meta[10] = self.kv_cache.max_seq_len as u32;
16624 let clusters = self.head_clusters.as_ref().unwrap();
16625 let cluster_count = clusters.len() / self.hidden_size;
16626 if clusters.len() % self.hidden_size != 0
16627 || cluster_count == 0
16628 || cluster_count > 1024
16629 || self.vocab_size % cluster_count != 0
16630 || self.weights.lm_head.rows() < self.vocab_size
16631 || self.weights.final_norm.len() != self.hidden_size
16632 {
16633 return None;
16634 }
16635 meta[11] = cluster_count as u32;
16636 meta[12] = (self.vocab_size / cluster_count) as u32;
16637 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16638 meta[16] = self.rotary_dim as u32;
16639 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16640 meta[19] = (self.rms_eps as f32).to_bits();
16641 let max_conv = self
16642 .weights
16643 .layers
16644 .iter()
16645 .filter_map(|lw| match &lw.attn {
16646 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16647 _ => None,
16648 })
16649 .max()
16650 .unwrap_or(1);
16651 let phase_stride = vmf
16657 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16658 .unwrap_or(0);
16659 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16660 let state_stride = phase_stride.max(gdn_stride);
16661 let bounded = self.anchor_core.clone();
16665 let (anchor_window, anchor_sink) = bounded
16666 .as_ref()
16667 .map(|ac| (ac.window, ac.sink))
16668 .unwrap_or((0, 0));
16669 let kv_stride = if bounded.is_some() {
16670 2usize
16671 .saturating_mul(self.num_kv_heads)
16672 .saturating_mul(anchor_window)
16673 .saturating_mul(self.head_dim)
16674 } else {
16675 2usize
16676 .saturating_mul(self.num_kv_heads)
16677 .saturating_mul(self.kv_cache.max_seq_len)
16678 .saturating_mul(self.head_dim)
16679 };
16680 meta[14] = state_stride as u32;
16681 meta[15] = kv_stride as u32;
16682 meta[18] = anchor_window as u32;
16683 meta[20] = anchor_sink as u32;
16684 meta[21] = match &self.bounded_rope {
16685 Some(rope) => {
16686 let off = pack.put(&rope.cos);
16688 let _ = pack.put(&rope.sin);
16689 off
16690 }
16691 None => UMAX,
16692 };
16693 let mut full_seen = false;
16694 let mut bounded_seen = 0usize;
16695 let mut phase_seen = 0usize;
16699 let mut gdn_seen = 0usize;
16700 for (li, lw) in self.weights.layers.iter().enumerate() {
16701 let base = meta.len();
16702 meta.resize(base + REC, UMAX);
16703 meta[base] = match &lw.attn {
16704 AttnKind::Linear(w) if w.phase_delta => 1,
16705 AttnKind::Linear(_) => 0,
16706 AttnKind::Full { .. } => 2,
16707 AttnKind::Bounded(_) => 3,
16708 AttnKind::LinearGdn(_) => 4,
16709 _ => UMAX,
16710 };
16711 meta[base + 1] = pack.put(&lw.input_norm);
16712 meta[base + 2] = pack.put(&lw.post_norm);
16713 meta[base + 25] = match &lw.attn {
16714 AttnKind::Linear(_) => {
16715 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16716 phase_seen += 1;
16717 off
16718 }
16719 AttnKind::LinearGdn(_) => {
16720 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16721 gdn_seen += 1;
16722 off
16723 }
16724 _ => UMAX,
16725 };
16726 match &lw.attn {
16727 AttnKind::LinearGdn(w) => {
16728 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16731 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16732 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16733 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16734 meta[base + 60] = pack.put(&w.conv1d);
16735 meta[base + 61] = pack.put(&w.a_log);
16736 meta[base + 62] = pack.put(&w.dt_bias);
16737 meta[base + 63] = pack.put(&w.norm);
16738 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16739 meta[base + 24] = 0;
16740 }
16741 AttnKind::Linear(w) => {
16742 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16743 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16744 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16745 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16746 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16747 meta[base + 7] = pack.put(&decay);
16748 if let Some((kg, kb)) = &w.k_gate {
16749 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16750 meta[base + 9] = pack.put(kb);
16751 }
16752 if let Some(conv) = &w.conv {
16753 meta[base + 10] = pack.put(conv);
16754 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16755 } else {
16756 meta[base + 24] = 0;
16757 }
16758 }
16759 AttnKind::Full { wq, wk, wv, wo, .. } => {
16760 full_seen = true;
16761 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16762 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16763 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16764 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16765 meta[base + 26] = (li * kv_stride) as u32;
16766 }
16767 AttnKind::Bounded(w) => {
16768 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16769 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16770 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16771 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16772 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16774 meta[base + 27] = pack.put(&w.sink_k);
16775 meta[base + 28] = pack.put(&w.sink_v);
16776 bounded_seen += 1;
16777 }
16778 _ => return None,
16779 }
16780 match &lw.ffn {
16781 FfnKind::Dense(d) => {
16782 meta[base + 15] = 0;
16783 meta[base + 16] = 0;
16784 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16785 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16786 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16787 }
16788 FfnKind::Moe(m) => {
16789 let r = m.resonance.as_ref().unwrap();
16790 let (shared, _) = m.shared.as_ref().unwrap();
16791 meta[base + 15] = 1;
16792 meta[base + 16] = m.experts.len() as u32;
16793 meta[base + 17] = pack.put(&r.mu);
16794 meta[base + 18] = pack.put(&r.u);
16795 meta[base + 19] = pack.put(&r.bias);
16796 meta[base + 20] = r.k as u32;
16797 let mut shell = r.effective_shell(m.experts.len());
16806 shell.push(f32::NEG_INFINITY);
16807 meta[base + 30] = pack.put(&shell);
16808 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16809 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16810 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16811 for (e, ex) in m.experts.iter().enumerate() {
16812 meta[base + 32 + e * 3] =
16813 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16814 meta[base + 33 + e * 3] =
16815 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16816 meta[base + 34 + e * 3] =
16817 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16818 }
16819 }
16820 FfnKind::DenseMoe(_) => return None,
16821 }
16822 }
16823 if !full_seen && self.num_layers == 0 {
16824 return None;
16825 }
16826 let id = {
16827 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16828 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16829 };
16830 let model = crate::gpu::EmbryoGraphModel {
16831 id,
16832 hidden: self.hidden_size,
16833 intermediate: self.intermediate_size,
16834 vocab: self.vocab_size,
16835 layers: self.num_layers,
16836 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16837 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16838 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16839 anchor_q_heads: self.num_heads,
16840 anchor_kv_heads: self.num_kv_heads,
16841 anchor_head_dim: self.head_dim,
16842 rotary_dim: self.rotary_dim,
16843 max_seq: self.kv_cache.max_seq_len,
16844 cluster_count,
16845 cluster_size: self.vocab_size / cluster_count,
16846 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16847 state_stride,
16848 kv_stride,
16849 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16850 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16851 weights: pack.data,
16852 meta,
16853 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16854 clusters: clusters.as_ref().clone(),
16855 final_norm: self.weights.final_norm.clone(),
16856 inv_freq: self.inv_freq.as_ref().clone(),
16857 bounded: bounded.is_some(),
16858 kv_layers: if bounded.is_some() {
16859 bounded_seen
16860 } else {
16861 self.num_layers
16862 },
16863 state_layers: phase_seen + gdn_seen,
16864 anchor_window,
16865 anchor_sink,
16866 phase_layers: phase_seen,
16867 gdn_layers: gdn_seen,
16868 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16869 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16870 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16871 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16872 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16873 };
16874 self.embryo_graph = Some(std::sync::Arc::new(model));
16875 }
16876 self.embryo_graph.clone()
16877 }
16878
16879 fn forward_layers_span(
16880 &mut self,
16881 hidden: &[f32],
16882 position: usize,
16883 task_mask: Option<&TaskMask>,
16884 from: usize,
16885 upto: Option<usize>,
16886 ) -> Vec<f32> {
16887 #[cfg(target_os = "macos")]
16888 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16889 position,
16890 from,
16891 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16892 );
16893 debug_assert!(
16894 from == 0
16895 || (self.dsv4.is_none()
16896 && self.dsv41.is_none()
16897 && self.qwen4_exp.is_none()
16898 && !self.per_token_stack())
16899 );
16900 #[cfg(target_os = "macos")]
16906 if !crate::gpu_metal::wait_replay() {
16907 self.fail_metal_graph("the pending async replay failed before a plain forward");
16908 return vec![0.0; self.hidden_size];
16909 }
16910 if let Some(b) = &mut self.qwen4_exp {
16911 let _ = (task_mask, upto);
16912 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16913 let mut logits = Vec::new();
16914 crate::qwen4_exp::forward_token(
16915 &b.0,
16916 &b.1,
16917 &b.2,
16918 &mut b.3,
16919 token_id,
16920 position,
16921 &self.inv_freq,
16922 self.pool.as_deref(),
16923 &mut logits,
16924 true,
16925 );
16926 self.graph_logits = Some(logits);
16927 return vec![0.0; self.hidden_size];
16928 }
16929 if let Some(b) = &mut self.dsv4 {
16935 let _ = (task_mask, upto);
16936 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16937 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16938 st.pos = position;
16939 let mut logits = Vec::new();
16940 crate::dsv4::forward_token(
16941 g,
16942 layers,
16943 &cfg,
16944 st,
16945 token_id,
16946 &self.inv_freq,
16947 self.pool.as_deref(),
16948 &mut logits,
16949 );
16950 self.graph_logits = Some(logits);
16951 self.dspark_probe(position, token_id);
16952 return vec![0.0; self.hidden_size];
16955 }
16956 if let Some(b) = &mut self.dsv41 {
16958 let _ = (task_mask, upto);
16959 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16960 let mut logits = Vec::new();
16961 crate::dsv41::forward_token(
16962 &b.0,
16963 &b.1,
16964 &b.2,
16965 &mut b.3,
16966 token_id,
16967 position,
16968 self.pool.as_deref(),
16969 &mut logits,
16970 );
16971 self.graph_logits = Some(logits);
16972 return vec![0.0; self.hidden_size];
16973 }
16974 if let Some(b) = &self.g4e {
16980 let _ = (task_mask, upto);
16981 return crate::g4e::g4e_forward(
16982 &b.0,
16983 &b.1,
16984 hidden,
16985 position,
16986 &mut self.kv_cache.layers,
16987 self.num_heads,
16988 self.num_kv_heads,
16989 self.pool.as_deref(),
16990 );
16991 }
16992 if let Some(b) = &self.g3n {
16993 let _ = (task_mask, upto);
16994 return crate::g3n::g3n_forward(
16995 &b.0,
16996 &b.1,
16997 hidden,
16998 position,
16999 &mut self.kv_cache.layers,
17000 self.num_heads,
17001 self.num_kv_heads,
17002 self.head_dim,
17003 self.pool.as_deref(),
17004 );
17005 }
17006 if from == 0
17013 && upto.is_none()
17014 && task_mask.is_none()
17015 && self.anchor_core.is_some()
17016 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
17017 {
17018 static ONCE: std::sync::Once = std::sync::Once::new();
17019 ONCE.call_once(|| {
17020 eprintln!(
17021 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
17022 unsupported={} eligible={}",
17023 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17024 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17025 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17026 crate::gpu::enabled_here(),
17027 self.graph_refused(),
17028 self.embryo_resident_eligible(),
17029 );
17030 });
17031 }
17032 if from == 0
17033 && upto.is_none()
17034 && task_mask.is_none()
17035 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17039 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17040 && matches!(
17044 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17045 Ok("1") | Ok("parallel")
17046 )
17047 && crate::gpu::enabled_here()
17048 && !self.graph_refused()
17049 && (position == 0 || self.device_sequence_position().is_some())
17055 && self.embryo_resident_eligible()
17056 && let Some(model) = self.ensure_embryo_graph()
17057 {
17058 let mut lg = Vec::new();
17059 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17060 {
17061 self.graph_logits = Some(lg);
17062 return vec![0.0; self.hidden_size];
17063 }
17064 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17065 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17066 }
17067 self.mark_graph_refused();
17073 if position != 0 {
17074 self.kv_cache.clear();
17078 self.clear_history();
17079 crate::gpu::graph_kv_reset(self.graph_kv_id);
17080 panic!(
17081 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
17082 );
17083 }
17084 }
17085 let mut h = hidden.to_vec();
17086 self.mimo_moe_prepare();
17089 let _mimo_q8 = self
17090 .mimo_moe
17091 .is_on()
17092 .then(crate::qtensor::enter_full_gpu_q8_scope);
17093 let (nh, _nkv, _hd, hs, _rd, eps) = (
17096 self.num_heads,
17097 self.num_kv_heads,
17098 self.head_dim,
17099 self.hidden_size,
17100 self.rotary_dim,
17101 self.rms_eps,
17102 );
17103 let pool = self.pool.clone();
17104 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
17116 let graph_on = match graph_env.as_deref() {
17117 Some("0") => false,
17118 Some("prefill") => false, Some(_) => true,
17120 None => crate::gpu::wgpu_graph_default(),
17126 };
17127 let graph_trusted =
17128 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17129 let race_eligible =
17130 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17131 let mut tail_start = 0usize;
17132 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17133 let t_graph = std::time::Instant::now();
17134 let mut lg = Vec::new();
17135 let mut gl = 0usize;
17136 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17137 let declined = built.is_none();
17138 let built = match built {
17139 Some(Ok(hh)) => Some(hh),
17140 Some(Err(())) => {
17141 self.clear_sequence_state();
17145 self.graph_failed
17146 .store(true, std::sync::atomic::Ordering::Relaxed);
17147 self.cancel
17148 .store(true, std::sync::atomic::Ordering::Relaxed);
17149 tracing::error!("token graph failed after admission; sequence state cleared");
17150 return vec![0.0; self.hidden_size];
17151 }
17152 None => None,
17153 };
17154 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17159 self.mark_graph_refused();
17160 }
17161 graph_note(built.is_some(), gl, self.num_layers);
17162 if let Some(hh) = built {
17163 let dur = t_graph.elapsed();
17164 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17165 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17166 }
17167 if gl > 0 && gl < self.num_layers {
17168 h = hh;
17174 tail_start = gl;
17175 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17176 if !graph_trusted {
17177 crate::gpu::graph_race_record(true, dur);
17178 }
17179 if !lg.is_empty() {
17180 lg.resize(self.vocab_size, 0.0);
17183 if let Some(c) = self.final_softcap {
17184 softcap_in_place(&mut lg, c, self.pool.as_deref());
17185 }
17186 self.graph_logits = Some(lg);
17187 }
17188 return hh;
17189 }
17190 }
17196 }
17197 let span = from > 0 || upto.is_some();
17221 if span && graph_on && task_mask.is_none() && graph_trusted {
17222 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17223 let mut lg = Vec::new();
17224 let mut gl = 0usize;
17225 let span_res =
17226 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17227 let span_res = match span_res {
17228 Some(Ok(hh)) => Some(hh),
17229 Some(Err(())) => {
17230 self.clear_sequence_state();
17231 self.graph_failed
17232 .store(true, std::sync::atomic::Ordering::Relaxed);
17233 self.cancel
17234 .store(true, std::sync::atomic::Ordering::Relaxed);
17235 tracing::error!(
17236 "span token graph failed after admission; sequence state cleared"
17237 );
17238 return vec![0.0; self.hidden_size];
17239 }
17240 None => None,
17241 };
17242 graph_note(span_res.is_some(), gl, upto_excl - from);
17243 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17244 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17248 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17249 eprintln!(
17250 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17251 upto_excl - from,
17252 span_res.is_some()
17253 );
17254 }
17255 }
17256 if let Some(hh) = span_res {
17257 if gl == upto_excl - from {
17258 if !lg.is_empty() {
17259 lg.resize(self.vocab_size, 0.0);
17260 if let Some(c) = self.final_softcap {
17261 softcap_in_place(&mut lg, c, self.pool.as_deref());
17262 }
17263 self.graph_logits = Some(lg);
17264 }
17265 crate::gpu::set_layer(-1);
17266 return hh;
17267 }
17268 h = hh;
17270 tail_start = from + gl;
17271 }
17272 }
17273 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17278
17279 let host_tail = tail_start > from;
17289 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17290 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17291
17292 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17293 #[cfg(target_os = "macos")]
17294 let mut gpu_skip_until = 0usize;
17295 for li in tail_start.max(from)..self.num_layers {
17296 let _capacity_tail = automatic_gpu_prefix
17297 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17298 .map(|_| crate::gpu::enter_cpu_scope());
17299 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17301 if li > u {
17302 break;
17303 }
17304 }
17305 if let Some(mask) = task_mask {
17306 if !mask.layer_alive(li) {
17307 continue; }
17309 }
17310 if host_tail && task_mask.is_none() {
17315 match self.m3_pooled_layer(li, position, &mut h) {
17316 Ok(true) => {
17317 crate::gpu::set_layer(-1);
17318 continue;
17319 }
17320 Ok(false) => {}
17321 Err(()) => {
17322 self.clear_sequence_state();
17323 self.graph_failed
17324 .store(true, std::sync::atomic::Ordering::Relaxed);
17325 self.cancel
17326 .store(true, std::sync::atomic::Ordering::Relaxed);
17327 tracing::error!(
17328 "pooled tail token graph failed after admission; sequence state cleared"
17329 );
17330 return vec![0.0; self.hidden_size];
17331 }
17332 }
17333 }
17334 #[cfg(target_os = "macos")]
17338 {
17339 if li < gpu_skip_until {
17340 continue;
17341 }
17342 if task_mask.is_none() {
17343 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17344 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17345 return vec![0.0; self.hidden_size];
17349 }
17350 if end > li {
17351 gpu_skip_until = end;
17352 if self.is_loop_end(end - 1) && end < self.num_layers {
17355 h = inference::rms_norm(
17356 &h,
17357 &self.weights.final_norm,
17358 self.rms_eps,
17359 self.norm_style,
17360 );
17361 }
17362 continue;
17363 }
17364 }
17365 }
17366
17367 if task_mask.is_none() {
17368 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17369 crate::gpu::BatchGraphOutcome::Completed => continue,
17370 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17371 crate::gpu::BatchGraphOutcome::Declined => {}
17372 }
17373 }
17374 #[cfg(feature = "gpu")]
17375 self.pull_lagging_host_kv(li, li + 1, position);
17376 let lw = &self.weights.layers[self.phys_layer(li)];
17377 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17378 if tp.parse::<usize>().ok() == Some(position) {
17379 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17380 eprintln!(
17381 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17382 h[0], h[1]
17383 );
17384 }
17385 }
17386 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17389 inference::rms_norm_into(
17390 &h,
17391 &lw.input_norm,
17392 self.rms_eps,
17393 self.norm_style,
17394 &mut self.ws.n1,
17395 );
17396 drop(prof);
17397
17398 let attn_out = match &lw.attn {
17399 AttnKind::Mla(w) => {
17400 let inv_freq_l = self.layer_inv_freq(li);
17401 let rs = self.layer_rope_scale(li);
17402 let eps = self.rms_eps;
17403 let pool = self.pool.clone();
17404 mla_attention(
17405 w,
17406 &self.ws.n1,
17407 &mut self.kv_cache.layers[li],
17408 position,
17409 &inv_freq_l,
17410 rs,
17411 eps,
17412 pool.as_deref(),
17413 )
17414 }
17415 AttnKind::Linear(w) => {
17416 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17417 vmf_phase_forward(
17418 &self.ws.n1,
17419 w,
17420 &cfg,
17421 &mut self.kv_cache.layers[li].linear_state,
17422 self.pool.as_deref(),
17423 )
17424 }
17425 AttnKind::Kda(w) => {
17426 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17427 crate::linear_core::kda_forward(
17428 &self.ws.n1,
17429 w,
17430 &cfg,
17431 &mut self.kv_cache.layers[li].linear_state,
17432 self.pool.as_deref(),
17433 )
17434 }
17435 AttnKind::LinearGdn(w) => {
17436 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17437 gdn_forward(
17438 &self.ws.n1,
17439 w,
17440 &cfg,
17441 &mut self.kv_cache.layers[li].linear_state,
17442 self.pool.as_deref(),
17443 )
17444 }
17445 AttnKind::ShortConv(w) => {
17446 let cfg = self
17447 .short_conv_cfg
17448 .expect("short-conv layer without short_conv_cfg");
17449 short_conv_forward(
17450 &self.ws.n1,
17451 w,
17452 &cfg,
17453 &mut self.kv_cache.layers[li].linear_state,
17454 self.pool.as_deref(),
17455 )
17456 }
17457 AttnKind::Bounded(w) => {
17458 let rope = self
17461 .bounded_rope
17462 .clone()
17463 .expect("bounded layer without an installed rotation table");
17464 let cfg = crate::bounded::BoundedAttnCfg {
17465 num_heads: self.num_heads,
17466 num_kv_heads: self.num_kv_heads,
17467 head_dim: self.head_dim,
17468 hidden_size: hs,
17469 scale: self.attn_scale,
17470 rope: &rope,
17471 pool: pool.as_deref(),
17472 };
17473 crate::bounded::bounded_attention(
17474 &self.ws.n1,
17475 w,
17476 &mut self.kv_cache.layers[li],
17477 &cfg,
17478 )
17479 }
17480 AttnKind::Full {
17481 wq,
17482 wk,
17483 wv,
17484 wo,
17485 q_norm,
17486 k_norm,
17487 output_gate,
17488 softplus_gate,
17489 bias,
17490 } if self.kv_cache.layers[li].o1_sealed() => {
17491 let inv_freq_l = self.layer_inv_freq(li);
17494 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17495 let cfg = QwenAttnCfg {
17496 num_heads: self.layer_num_heads(li),
17497 num_kv_heads: nkv_l,
17498 head_dim: hd_l,
17499 hidden_size: hs,
17500 position,
17501 inv_freq: &inv_freq_l,
17502 rotary_dim: rd_l,
17503 scale: self.attn_scale,
17504 softcap: self.attn_softcap,
17505 window: None,
17506 v_norm: self.attn_v_norm,
17507 qk_norm_after_rope: self.qk_norm_after_rope,
17508 gate_sigmoid: self.proj_gate_sigmoid,
17509 q_norm: q_norm.as_deref(),
17510 k_norm: k_norm.as_deref(),
17511 output_gate: *output_gate,
17512 softplus_gate: softplus_gate
17513 .as_ref()
17514 .map(|(gate, per_head)| (gate, *per_head)),
17515 rope_scale: self.layer_rope_scale(li),
17516 bias: bias
17517 .as_ref()
17518 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17519 rms_eps: eps,
17520 norm_style: self.norm_style,
17521 pool: pool.as_deref(),
17522 v_head_dim: self.layer_v_dim(li),
17523 };
17524 attention::qwen_attention_nystrom(
17525 &self.ws.n1,
17526 wq,
17527 wk,
17528 wv,
17529 wo,
17530 &mut self.kv_cache.layers[li],
17531 &cfg,
17532 )
17533 }
17534 AttnKind::Full {
17535 wq,
17536 wk,
17537 wv,
17538 wo,
17539 q_norm,
17540 k_norm,
17541 output_gate,
17542 softplus_gate,
17543 bias,
17544 } => 'attn: {
17545 let dropin_reason =
17550 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17551 if let Some(reason) = dropin_reason {
17552 self.note_graph_decline("wgpu attn dropin", reason);
17553 }
17554 if graph_on
17555 && dropin_reason.is_none()
17556 && !*output_gate
17557 && softplus_gate.is_none()
17558 && self.attention_heads_per_layer.is_none()
17559 && bias.is_none()
17560 && task_mask.is_none()
17561 {
17562 let inv_freq_l = self.layer_inv_freq(li);
17563 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17564 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17565 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17566 wq.mapped_q1(),
17567 wk.mapped_q1(),
17568 wv.mapped_q1(),
17569 wo.mapped_q1(),
17570 ) {
17571 let gm = gm.clone();
17572 let mut out = vec![0f32; hs];
17573 let cache = &self.kv_cache.layers[li];
17574 if crate::gpu::attn_dropin(
17575 &gm,
17576 self.graph_kv_id,
17577 li,
17578 &self.ws.n1,
17579 qi,
17580 ki,
17581 vi,
17582 oi,
17583 q_norm.as_deref(),
17584 k_norm.as_deref(),
17585 self.qk_norm_after_rope,
17586 &inv_freq_l,
17587 nh,
17588 nkv_l,
17589 hd_l,
17590 rd_l,
17591 hs,
17592 position,
17593 self.kv_cache.max_seq_len,
17594 gemma,
17595 eps as f32,
17596 cache.k_heads(),
17597 cache.v_heads(),
17598 &mut out,
17599 ) {
17600 break 'attn out;
17601 }
17602 }
17603 }
17604 let masked = task_mask
17605 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17606 .unwrap_or(false);
17607 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17608 let plain = self.layer_attn_plain(li);
17611 match (masked, f32_view) {
17612 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17615 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17616 attention::multi_head_attention(
17617 &self.ws.n1,
17618 q,
17619 k,
17620 v,
17621 o,
17622 &mut self.kv_cache.layers[li],
17623 self.num_heads,
17624 self.num_kv_heads,
17625 self.head_dim,
17626 self.hidden_size,
17627 position,
17628 &active_heads,
17629 &self.inv_freq,
17630 )
17631 }
17632 (masked, _) => {
17633 if masked {
17634 tracing::warn!(
17635 "layer {li}: head mask on quantized weights or on a \
17636 window/sink/per-layer-geometry layer not supported \
17637 yet — executing dense"
17638 );
17639 }
17640 let inv_freq_l = self.layer_inv_freq(li);
17641 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17642 let cfg = QwenAttnCfg {
17643 num_heads: self.layer_num_heads(li),
17644 num_kv_heads: nkv_l,
17645 head_dim: hd_l,
17646 hidden_size: hs,
17647 position,
17648 inv_freq: &inv_freq_l,
17649 rotary_dim: rd_l,
17650 scale: self.attn_scale,
17651 softcap: self.attn_softcap,
17652 window: self.layer_window(li),
17653 v_norm: self.attn_v_norm,
17654 qk_norm_after_rope: self.qk_norm_after_rope,
17655 gate_sigmoid: self.proj_gate_sigmoid,
17656 q_norm: q_norm.as_deref(),
17657 k_norm: k_norm.as_deref(),
17658 output_gate: *output_gate,
17659 softplus_gate: softplus_gate
17660 .as_ref()
17661 .map(|(gate, per_head)| (gate, *per_head)),
17662 rope_scale: self.layer_rope_scale(li),
17663 bias: bias
17664 .as_ref()
17665 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17666 rms_eps: eps,
17667 norm_style: self.norm_style,
17668 pool: pool.as_deref(),
17669 v_head_dim: self.layer_v_dim(li),
17670 };
17671 attention::qwen_attention(
17672 &self.ws.n1,
17673 wq,
17674 wk,
17675 wv,
17676 wo,
17677 &mut self.kv_cache.layers[li],
17678 &cfg,
17679 )
17680 }
17681 }
17682 }
17683 };
17684 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17687 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17688 None => attn_out,
17689 };
17690 let lw = &self.weights.layers[self.phys_layer(li)];
17691 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17692 inference::add_rmsnorm_fused_into(
17693 &mut h,
17694 &attn_out,
17695 &lw.post_norm,
17696 self.rms_eps,
17697 self.norm_style,
17698 &mut self.ws.p1,
17699 );
17700 drop(prof);
17701 let mut attn_out = attn_out;
17702 attention::recycle_buf(&mut attn_out);
17703 let post_normed = &self.ws.p1;
17704
17705 let ffn_masked = task_mask
17706 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17707 .unwrap_or(false);
17708 let ffn_out = match (ffn_masked, &lw.ffn) {
17720 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17724 let row = task_mask
17725 .and_then(|tm| tm.ffn_masks.get(li))
17726 .map(|v| v.as_slice());
17727 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17728 }
17729 (true, FfnKind::Dense(d)) => {
17730 let tm = task_mask.unwrap();
17731 let alive = tm.ffn_active_count(li);
17732 let deep = alive * 2 <= self.intermediate_size;
17733 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17734 let active = tm.ffn_active_indices(li);
17735 sparse_ffn_quant(
17736 d,
17737 post_normed,
17738 &active,
17739 self.hidden_size,
17740 self.pool.as_deref(),
17741 )
17742 } else if deep
17743 && let (Some(g), Some(u), Some(dn)) = (
17744 d.gate_proj.as_f32(),
17745 d.up_proj.as_f32(),
17746 d.down_proj.as_f32(),
17747 )
17748 {
17749 let active = tm.ffn_active_indices(li);
17750 inference::sparse_ffn_forward(
17751 post_normed,
17752 g,
17753 u,
17754 dn,
17755 self.hidden_size,
17756 self.intermediate_size,
17757 &active,
17758 self.pool.as_deref(),
17759 )
17760 } else {
17761 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17762 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17763 }
17764 }
17765 (true, FfnKind::Moe(m)) => {
17766 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17770 ffn_forward(
17771 &lw.ffn,
17772 post_normed,
17773 self.pool.as_deref(),
17774 allowed.as_deref(),
17775 )
17776 }
17777 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17778 dm,
17779 post_normed,
17780 &h,
17781 self.rms_eps,
17782 self.norm_style,
17783 self.pool.as_deref(),
17784 ),
17785 (false, _) => match &lw.ffn {
17786 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17787 dm,
17788 post_normed,
17789 &h,
17790 self.rms_eps,
17791 self.norm_style,
17792 self.pool.as_deref(),
17793 ),
17794 FfnKind::Moe(m)
17795 if task_mask.is_none()
17796 && host_tail
17797 && li >= tail_start
17798 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17799 {
17800 moe_ffn_minicpm_tail(
17801 &mut self.minicpm_tail,
17802 li,
17803 self.num_layers,
17804 m,
17805 post_normed,
17806 self.pool.as_deref(),
17807 )
17808 }
17809 FfnKind::Moe(m)
17810 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17811 {
17812 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17813 }
17814 _ => {
17815 let allowed = match (&lw.ffn, task_mask) {
17816 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17817 _ => None,
17818 };
17819 ffn_forward(
17820 &lw.ffn,
17821 post_normed,
17822 self.pool.as_deref(),
17823 allowed.as_deref(),
17824 )
17825 }
17826 },
17827 };
17828 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17829 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17830 None => ffn_out,
17831 };
17832 for (i, &f) in ffn_out.iter().enumerate() {
17833 h[i] += f;
17834 }
17835 let mut ffn_out = ffn_out;
17836 attention::recycle_buf(&mut ffn_out);
17837
17838 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17840 for v in h.iter_mut() {
17841 *v *= sc;
17842 }
17843 }
17844 if self.layer_dump.is_some() {
17846 self.dump_layer_row(position, li, &h);
17847 }
17848
17849 if self.is_loop_end(li) && li + 1 < self.num_layers {
17852 h = inference::rms_norm(
17853 &h,
17854 &self.weights.final_norm,
17855 self.rms_eps,
17856 self.norm_style,
17857 );
17858 }
17859
17860 if self.dyn_phi_layer == Some(li) {
17864 self.update_dyn_phi(&h);
17865 }
17866 }
17867 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17869 crate::gpu::graph_race_record(false, t.elapsed());
17870 }
17871
17872 h
17873 }
17874
17875 fn update_dyn_phi(&mut self, h: &[f32]) {
17878 const A: f32 = 0.2;
17879 if self.dyn_phi_ema.len() != h.len() {
17880 self.dyn_phi_ema = vec![0.0; h.len()];
17881 self.dyn_phi_seen = 0;
17882 }
17883 if self.dyn_phi_seen == 0 {
17884 self.dyn_phi_ema.copy_from_slice(h);
17885 } else {
17886 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17887 *e = (1.0 - A) * *e + A * v;
17888 }
17889 }
17890 self.dyn_phi_seen += 1;
17891 }
17892
17893 pub fn dyn_phi(&self) -> &[f32] {
17895 &self.dyn_phi_ema
17896 }
17897
17898 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17900 self.dyn_phi_layer = layer;
17901 self.dyn_phi_ema.clear();
17902 self.dyn_phi_seen = 0;
17903 }
17904
17905 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17907 let Some(model) = &self.model else {
17908 return Vec::new();
17909 };
17910 model
17911 .header
17912 .skills
17913 .iter()
17914 .enumerate()
17915 .filter_map(|(i, sk)| {
17916 if sk.is_v2() {
17920 return None;
17921 }
17922 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17923 let sel = sk.selection.as_ref()?;
17924 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17925 })
17926 .collect()
17927 }
17928
17929 pub fn active_skill(&self) -> Option<usize> {
17931 self.dyn_active
17932 }
17933
17934 pub fn enable_dynamic_routing(&mut self) -> usize {
17939 use crate::swarm::{DynRouter, RoutableSkill};
17940 let Some(model) = self.model.clone() else {
17941 return 0;
17942 };
17943 if let Some(r) = &model.header.router {
17950 tracing::warn!(
17951 "dynamic routing disabled: this file declares router policy '{}' with \
17952 granularity \"{}\" — the request-level decision applies instead",
17953 r.policy,
17954 r.granularity
17955 );
17956 return 0;
17957 }
17958 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17964 || model.header.skills.iter().any(|s| s.is_v2())
17965 {
17966 tracing::warn!(
17967 "dynamic routing disabled: this file carries format-v2 skill records \
17968 (SKILLS_V2) — they route per request through a router policy only"
17969 );
17970 return 0;
17971 }
17972 if self.dyn_blend_loaded {
17975 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17976 return 0;
17977 }
17978 if let Some(a) = self.dyn_active {
17982 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17983 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17984 return 0;
17985 }
17986 }
17987 let hidden = self.hidden_size;
17988 let mut skills = Vec::new();
17989 for (idx, id, _phi) in self.dynamic_skills() {
17990 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17991 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17992 skills.push(rs);
17993 }
17994 }
17995 }
17996 if skills.is_empty() {
17997 return 0;
17998 }
17999 let phi = skills[0].phi_layer;
18001 if skills.iter().any(|s| s.phi_layer != phi) {
18002 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
18003 }
18004 let n = skills.len();
18005 self.set_dyn_phi_layer(Some(phi));
18006 self.dyn_router = Some(DynRouter::new(skills));
18007 n
18008 }
18009
18010 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
18012 self.dyn_router
18013 .as_ref()
18014 .map(|r| r.switches.clone())
18015 .unwrap_or_default()
18016 }
18017
18018 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
18021 let _mimo_q8 = self
18022 .mimo_moe
18023 .is_on()
18024 .then(crate::qtensor::enter_full_gpu_q8_scope);
18025 let rows = self.weights.lm_head.rows();
18026 let mut logits = attention::take_buf(rows.min(self.vocab_size));
18027 let served = self.mimo_moe.is_on()
18031 && crate::gpu::mimo_q8_short_enabled()
18032 && rows == self.vocab_size
18033 && !self.weights.lm_head.has_prism_contract()
18034 && self
18035 .weights
18036 .lm_head
18037 .graph_weight()
18038 .is_some_and(|(model, idx, kind, _)| {
18039 kind == 7
18040 && crate::gpu::q82_short_rows(
18041 model,
18042 idx,
18043 hidden,
18044 1,
18045 rows,
18046 self.hidden_size,
18047 &mut logits,
18048 )
18049 });
18050 if !served {
18051 self.weights
18052 .lm_head
18053 .matvec(hidden, &mut logits, self.pool.as_deref());
18054 }
18055 logits.resize(self.vocab_size, 0.0);
18056 if let Some(m) = self.logit_multiplier {
18057 for l in logits.iter_mut() {
18058 *l *= m;
18059 }
18060 }
18061 if let Some(c) = self.final_softcap {
18062 softcap_in_place(&mut logits, c, self.pool.as_deref());
18063 }
18064 if let Some(cm) = self.head_clusters.as_ref() {
18065 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18066 }
18067 logits
18068 }
18069
18070 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18073 let h = hidden.len();
18074 let ncl = cm.len() / h.max(1);
18075 if ncl == 0 || logits.len() % ncl != 0 {
18076 return;
18077 }
18078 let cs = logits.len() / ncl;
18079 let mut lc = vec![0.0f32; ncl];
18081 for c in 0..ncl {
18082 let row = &cm[c * h..(c + 1) * h];
18083 let mut s = 0.0f32;
18084 for j in 0..h {
18085 s += row[j] * hidden[j];
18086 }
18087 lc[c] = s;
18088 }
18089 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18090 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
18091 for c in 0..ncl {
18092 let blk = &mut logits[c * cs..(c + 1) * cs];
18093 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18094 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
18095 let add = lc[c] - lse - bl;
18096 for v in blk.iter_mut() {
18097 *v += add;
18098 }
18099 }
18100 }
18101
18102 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
18107 #[cfg(target_os = "macos")]
18108 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
18109 self.clear_sequence_state();
18110 crate::gpu::graph_race_begin_generation();
18114 if task_mask.is_none() {
18115 self.o1_begin();
18116 }
18117 let mut hidden = vec![0.0f32; self.hidden_size];
18118 for (pos, &id) in ids.iter().enumerate() {
18119 let emb = self.embed_single(id);
18120 hidden = self.forward_layers(&emb, pos, task_mask);
18121 }
18122 if let Err(err) = self.o1_seal_checked() {
18123 self.o1_fail(err);
18124 }
18125 if let Some(logits) = self.graph_logits.take() {
18128 return logits;
18129 }
18130 inference::rms_norm_into(
18131 &hidden,
18132 &self.weights.final_norm,
18133 self.rms_eps,
18134 self.norm_style,
18135 &mut self.ws.n1,
18136 );
18137 self.lm_head_forward(&self.ws.n1)
18138 }
18139}
18140
18141pub fn create_test_pipeline(
18143 hidden_size: usize,
18144 intermediate_size: usize,
18145 num_heads: usize,
18146 num_kv_heads: usize,
18147 head_dim: usize,
18148 num_layers: usize,
18149 vocab_size: usize,
18150) -> Pipeline {
18151 let synth = |n: usize, salt: usize| -> Vec<f32> {
18154 (0..n)
18155 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18156 .collect()
18157 };
18158 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18159 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18160 };
18161 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18162 .map(|li| LayerWeights {
18163 input_norm: vec![1.0; hidden_size],
18164 post_norm: vec![1.0; hidden_size],
18165 attn_out_norm: None,
18166 ffn_out_norm: None,
18167 layer_scale: None,
18168 ffn: FfnKind::Dense(DenseFfn {
18169 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18170 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18171 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18172 act: Act::Silu,
18173 down_t: None,
18174 segs: Vec::new(),
18175 }),
18176 attn: AttnKind::Full {
18177 bias: None,
18178 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18179 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18180 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18181 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18182 q_norm: None,
18183 k_norm: None,
18184 output_gate: false,
18185 softplus_gate: None,
18186 },
18187 })
18188 .collect();
18189
18190 Pipeline::new(
18191 Tokenizer::byte_level(),
18192 PipelineWeights {
18193 embed_tokens: qt(vocab_size, hidden_size, 100),
18194 layers: layer_weights,
18195 lm_head: qt(vocab_size, hidden_size, 200),
18196 final_norm: vec![1.0; hidden_size],
18197 },
18198 hidden_size,
18199 intermediate_size,
18200 num_heads,
18201 num_kv_heads,
18202 head_dim,
18203 num_layers,
18204 num_layers, false, vocab_size,
18207 1e-6,
18208 10_000.0,
18209 NormStyle::Qwen,
18210 4096,
18211 SamplerConfig {
18212 seed: Some(42),
18213 ..Default::default()
18214 },
18215 )
18216}
18217
18218#[inline]
18223fn mask_bit(row: &[u8], j: usize) -> bool {
18224 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18225}
18226
18227fn mask_gain() -> f32 {
18238 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18239 *G.get_or_init(|| {
18240 std::env::var("CMF_FFN_MASK_GAIN")
18241 .ok()
18242 .and_then(|v| v.parse().ok())
18243 .unwrap_or(1.0)
18244 })
18245}
18246
18247fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18248 let fill = meanfill().and_then(|(i, v)| {
18251 let li = crate::gpu::cur_layer();
18252 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18253 });
18254 for r in 0..rows {
18255 let base = r * inter;
18256 for (bi, &byte) in row.iter().enumerate() {
18257 if byte == 0xFF {
18258 continue;
18259 }
18260 let j0 = bi * 8;
18261 for bit in 0..8 {
18262 let j = j0 + bit;
18263 if j < inter && byte & (1 << bit) == 0 {
18264 g[base + j] = fill.map_or(0.0, |f| f[j]);
18265 }
18266 }
18267 }
18268 }
18269 let gain = mask_gain();
18270 if gain != 1.0 {
18271 for v in g[..rows * inter].iter_mut() {
18272 *v *= gain;
18273 }
18274 }
18275}
18276
18277#[inline]
18279fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18280 row.is_none_or(|r| mask_bit(r, i))
18281}
18282
18283fn all_bits_on(row: &[u8], n: usize) -> bool {
18286 (0..n).all(|i| mask_bit(row, i))
18287}
18288
18289fn tube_topk() -> usize {
18297 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18298 *K.get_or_init(|| {
18299 std::env::var("CMF_TUBE_TOPK")
18300 .ok()
18301 .and_then(|v| v.parse().ok())
18302 .unwrap_or(0)
18303 })
18304}
18305
18306fn tube_score_oracle() -> bool {
18307 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18308 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18309}
18310
18311fn tube_ffn_routed(
18318 d: &DenseFfn,
18319 xs: &[f32],
18320 b: usize,
18321 pool: Option<&Pool>,
18322 mask_row: Option<&[u8]>,
18323 k: usize,
18324) -> Vec<f32> {
18325 let hidden = d.down_proj.rows();
18326 let core = d.gate_proj.rows();
18327 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18328 let mut out = match (b, core_full, mask_row) {
18329 (1, true, _) => dense_ffn(d, xs, pool),
18330 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18331 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18332 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18333 };
18334 let cand: Vec<usize> = (0..d.segs.len())
18335 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18336 .collect();
18337 if cand.is_empty() {
18338 return out;
18339 }
18340 let oracle = tube_score_oracle();
18344 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18345 let mut scores = vec![0f32; b * cand.len()];
18346 for (ci, &i) in cand.iter().enumerate() {
18347 let seg = &d.segs[i];
18348 let w = seg.width;
18349 let mut g = vec![0.0f32; b * w];
18350 if b == 1 {
18351 seg.gate.matvec(xs, &mut g, pool);
18352 } else {
18353 seg.gate.matmat(xs, b, &mut g, pool);
18354 }
18355 for v in g.iter_mut() {
18356 *v = Act::Silu.combine(*v, 1.0);
18357 }
18358 if !oracle {
18359 for t in 0..b {
18360 scores[t * cand.len() + ci] =
18361 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18362 }
18363 }
18364 if oracle || b > 1 {
18365 let mut u = vec![0.0f32; b * w];
18366 if b == 1 {
18367 seg.up.matvec(xs, &mut u, pool);
18368 } else {
18369 seg.up.matmat(xs, b, &mut u, pool);
18370 }
18371 for (a, &v) in g.iter_mut().zip(u.iter()) {
18372 *a *= v;
18373 }
18374 if oracle {
18375 for t in 0..b {
18376 scores[t * cand.len() + ci] =
18377 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18378 }
18379 }
18380 }
18381 acts.push(g);
18382 }
18383 let keep = k.min(cand.len());
18385 let mut scratch: Vec<f32> = Vec::new();
18386 for t in 0..b {
18387 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18388 .map(|ci| (scores[t * cand.len() + ci], ci))
18389 .collect();
18390 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18391 let mut alive = vec![false; cand.len()];
18392 for &(_, ci) in sc.iter().take(keep) {
18393 alive[ci] = true;
18394 }
18395 if b > 1 {
18396 for (ci, a) in acts.iter_mut().enumerate() {
18397 if !alive[ci] {
18398 let w = d.segs[cand[ci]].width;
18399 a[t * w..(t + 1) * w].fill(0.0);
18400 }
18401 }
18402 } else {
18403 for (ci, &i) in cand.iter().enumerate() {
18407 if !alive[ci] {
18408 continue;
18409 }
18410 let seg = &d.segs[i];
18411 let w = seg.width;
18412 let g = &mut acts[ci];
18413 if !tube_score_oracle() {
18414 scratch.clear();
18415 scratch.resize(w, 0.0);
18416 seg.up.matvec(xs, &mut scratch, pool);
18417 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18418 *a *= v;
18419 }
18420 }
18421 let mut acc = vec![0.0f32; hidden];
18422 seg.down.matvec(g, &mut acc, pool);
18423 for (o, a) in out.iter_mut().zip(&acc) {
18424 *o += *a;
18425 }
18426 }
18427 }
18428 }
18429 if b > 1 {
18430 for (ci, &i) in cand.iter().enumerate() {
18431 let seg = &d.segs[i];
18432 let mut acc = vec![0.0f32; b * hidden];
18433 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18434 for (o, a) in out.iter_mut().zip(&acc) {
18435 *o += *a;
18436 }
18437 }
18438 }
18439 out
18440}
18441
18442fn tube_ffn(
18448 d: &DenseFfn,
18449 xs: &[f32],
18450 b: usize,
18451 pool: Option<&Pool>,
18452 mask_row: Option<&[u8]>,
18453) -> Vec<f32> {
18454 if tube_topk() > 0 {
18455 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18456 }
18457 let hidden = d.down_proj.rows();
18458 let core = d.gate_proj.rows();
18459 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18460 let mut out = match (b, core_full, mask_row) {
18461 (1, true, _) => dense_ffn(d, xs, pool),
18462 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18463 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18464 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18465 };
18466 TUBE_SCRATCH.with(|sc| {
18467 let mut sc = sc.borrow_mut();
18468 let [g, u, acc] = &mut *sc;
18469 for seg in &d.segs {
18470 if !tube_bit(mask_row, seg.start) {
18471 continue;
18472 }
18473 let w = seg.width;
18474 g.resize(b * w, 0.0);
18475 if b == 1
18476 && d.act == Act::Silu
18477 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18478 {
18479 } else {
18481 u.resize(b * w, 0.0);
18482 if b == 1 {
18483 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18484 } else {
18485 seg.gate.matmat(xs, b, g, pool);
18486 seg.up.matmat(xs, b, u, pool);
18487 }
18488 for i in 0..b * w {
18489 g[i] = d.act.combine(g[i], u[i]);
18490 }
18491 }
18492 acc.resize(b * hidden, 0.0);
18493 acc.fill(0.0);
18494 if b == 1 {
18495 seg.down.matvec(g, acc, pool);
18496 } else {
18497 seg.down.matmat(g, b, acc, pool);
18498 }
18499 for (o, a) in out.iter_mut().zip(acc.iter()) {
18500 *o += *a;
18501 }
18502 }
18503 out
18504 })
18505}
18506
18507thread_local! {
18508 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18512 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18513}
18514
18515static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18518 std::sync::atomic::AtomicU64::new(0),
18519 std::sync::atomic::AtomicU64::new(0),
18520];
18521
18522fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18526 #[cfg(feature = "gpu")]
18527 {
18528 use std::sync::atomic::Ordering::Relaxed;
18529 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18530 (
18531 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18532 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18533 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18534 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18535 )
18536 }
18537 #[cfg(not(feature = "gpu"))]
18538 (0.0, 0.0, 0.0, 0)
18539}
18540
18541fn wgpu_mirror_counters() -> [u64; 6] {
18544 #[cfg(feature = "gpu")]
18545 {
18546 crate::gpu_wgpu::MIRROR_EVENTS
18547 .each_ref()
18548 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18549 }
18550 #[cfg(not(feature = "gpu"))]
18551 [0; 6]
18552}
18553
18554fn prefill_prof_on() -> bool {
18555 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18556 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18557}
18558
18559pub(crate) fn accumulate_dense_ffn_activation_mass(
18568 d: &DenseFfn,
18569 xs: &[f32],
18570 b: usize,
18571 pool: Option<&Pool>,
18572 mass: &mut [f64],
18573 squared_mass: &mut [f64],
18574) -> Result<(), String> {
18575 let inter = d.gate_proj.rows();
18576 let hidden = d.gate_proj.cols();
18577 if mass.len() != inter || squared_mass.len() != inter {
18578 return Err(format!(
18579 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18580 mass.len(),
18581 squared_mass.len()
18582 ));
18583 }
18584 if d.up_proj.rows() != inter
18585 || d.up_proj.cols() != hidden
18586 || d.down_proj.cols() != inter
18587 || d.down_proj.rows() != hidden
18588 {
18589 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18590 }
18591 if !d.segs.is_empty() {
18592 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18593 }
18594 if xs.len() != b.saturating_mul(hidden) {
18595 return Err(format!(
18596 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18597 xs.len()
18598 ));
18599 }
18600 if b == 0 {
18601 return Ok(());
18602 }
18603 let mut gate = vec![0.0f32; b * inter];
18604 let mut up = vec![0.0f32; b * inter];
18605 d.gate_proj.matmat(xs, b, &mut gate, pool);
18606 d.up_proj.matmat(xs, b, &mut up, pool);
18607 for row in 0..b {
18608 for i in 0..inter {
18609 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18610 let a = a as f64;
18611 mass[i] += a.abs();
18612 squared_mass[i] += a * a;
18613 }
18614 }
18615 Ok(())
18616}
18617
18618fn dense_ffn_batch(
18619 d: &DenseFfn,
18620 xs: &[f32],
18621 b: usize,
18622 pool: Option<&Pool>,
18623 mask_row: Option<&[u8]>,
18624) -> Vec<f32> {
18625 let inter = d.gate_proj.rows();
18626 let hidden = d.down_proj.rows();
18627 let fused_act = d.act.wgpu_graph_act();
18639 if mask_row.is_none()
18640 && fused_act.is_some()
18641 && b >= 32
18642 && crate::gpu::enabled_here()
18643 && !crate::gpu::mm_killed()
18644 && refit_dir().is_none()
18649 && !ffn_probe_active()
18654 && !crate::gptq_capture::capturing()
18658 {
18659 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18660 d.gate_proj.mapped_q4t(),
18661 d.up_proj.mapped_q4t(),
18662 d.down_proj.mapped_q4t(),
18663 ) {
18664 let mut out = vec![0.0f32; b * hidden];
18665 let act = fused_act.expect("checked above");
18666 if crate::gpu::q4_ffn_act(
18667 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18668 ) {
18669 return out;
18670 }
18671 }
18672 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18677 d.gate_proj.mapped_q4tp(),
18678 d.up_proj.mapped_q4tp(),
18679 d.down_proj.mapped_q4tp(),
18680 ) {
18681 let mut out = vec![0.0f32; b * hidden];
18682 let act = fused_act.expect("checked above");
18683 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18684 {
18685 return out;
18686 }
18687 }
18688 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18694 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18695 d.gate_proj.mapped_device_gemm(),
18696 d.up_proj.mapped_device_gemm(),
18697 d.down_proj.mapped_device_gemm(),
18698 ) {
18699 let mut out = vec![0.0f32; b * hidden];
18700 let act = fused_act.expect("checked above");
18701 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18702 return out;
18703 }
18704 }
18705 }
18706 let mut g = vec![0.0f32; b * inter];
18707 d.gate_proj.matmat(xs, b, &mut g, pool);
18708 let mut u = vec![0.0f32; b * inter];
18709 d.up_proj.matmat(xs, b, &mut u, pool);
18710 if gate_topk() > 0 && d.act == Act::Silu {
18711 for t in 0..b {
18712 let row = &mut g[t * inter..(t + 1) * inter];
18713 for v in row.iter_mut() {
18714 *v = Act::Silu.combine(*v, 1.0);
18715 }
18716 keep_top_k(row, gate_topk());
18717 }
18718 for i in 0..b * inter {
18719 g[i] *= u[i];
18720 }
18721 } else {
18722 for i in 0..b * inter {
18723 g[i] = d.act.combine(g[i], u[i]);
18724 }
18725 }
18726 if let Some(row) = mask_row {
18727 zero_masked_cols(&mut g, b, inter, row);
18728 }
18729 if oracle_topk() > 0 {
18730 for t in 0..b {
18731 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18732 }
18733 }
18734 let mut out = vec![0.0f32; b * hidden];
18735 d.down_proj.matmat(&g, b, &mut out, pool);
18736 if refit_dir().is_some() {
18737 let li = crate::gpu::cur_layer();
18738 if li >= 0 {
18739 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18740 }
18741 }
18742 FFN_PROBE.with(|pr| {
18746 if let Some(acc) = pr.borrow_mut().as_mut() {
18747 let li = crate::gpu::cur_layer();
18748 if li < 0 {
18749 return;
18750 }
18751 let Some(row) = acc.get_mut(li as usize) else {
18752 return;
18753 };
18754 let sq = probe_sq();
18755 for t in 0..b {
18756 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18757 *a += if sq {
18758 (v as f64) * (v as f64)
18759 } else {
18760 (v as f64).abs()
18761 };
18762 }
18763 }
18764 }
18765 });
18766 out
18767}
18768
18769fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18774 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18775 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18776 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18777 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18778 if (!on && !dump) || b == 0 {
18779 return;
18780 }
18781 let hidden = xs.len() / b;
18782 if on {
18783 let mut acc = m.act_sq.borrow_mut();
18784 if acc.len() < hidden {
18785 acc.resize(hidden, 0.0);
18786 }
18787 for t in 0..b {
18788 let row = &xs[t * hidden..(t + 1) * hidden];
18789 for (a, &v) in acc.iter_mut().zip(row) {
18790 *a += (v as f64) * (v as f64);
18791 }
18792 }
18793 }
18794 if dump {
18795 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18798 .ok()
18799 .and_then(|v| v.parse().ok())
18800 .unwrap_or(4096);
18801 let mut rows = m.act_rows.borrow_mut();
18802 if rows.len() < cap * hidden {
18803 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18804 rows.extend_from_slice(&xs[..take * hidden]);
18805 }
18806 }
18807}
18808
18809#[derive(Clone, Copy)]
18812struct SendVecs(*mut Vec<f32>);
18813unsafe impl Send for SendVecs {}
18814unsafe impl Sync for SendVecs {}
18815impl SendVecs {
18816 #[inline]
18817 fn at(self, i: usize) -> *mut Vec<f32> {
18818 unsafe { self.0.add(i) }
18819 }
18820}
18821
18822fn moe_ffn_batch(
18823 m: &MoeFfn,
18824 xs: &[f32],
18825 b: usize,
18826 hidden: usize,
18827 pool: Option<&Pool>,
18828 allowed: Option<&[bool]>,
18829) -> Vec<f32> {
18830 accumulate_act(m, xs, b);
18831 let ne = m.experts.len();
18832 let mut logits = vec![0.0f32; b * ne];
18833 match &m.resonance {
18834 Some(r) => {
18835 let hdim = xs.len() / b.max(1);
18836 for bi in 0..b {
18837 r.scores(
18838 &xs[bi * hdim..(bi + 1) * hdim],
18839 &mut logits[bi * ne..(bi + 1) * ne],
18840 );
18841 }
18842 }
18843 None => m.router.matmat(xs, b, &mut logits, pool),
18844 }
18845
18846 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18849 {
18850 let mut st = m.stats.borrow_mut();
18851 if st.len() < ne {
18852 st.resize(ne, 0);
18853 }
18854 for bi in 0..b {
18855 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18856 for &e in &idx {
18857 st[e] += 1;
18858 assign[e].push((bi, p[e] / wsum));
18859 }
18860 }
18861 }
18862
18863 let mut out = vec![0.0f32; b * hidden];
18864 let cols = m.experts[0].gate_proj.cols();
18865 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18866 let sb = list.len();
18867 let mut sub = vec![0.0f32; sb * cols];
18868 for (k, &(bi, _)) in list.iter().enumerate() {
18869 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18870 }
18871 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18872 for (k, &(bi, w)) in list.iter().enumerate() {
18873 for i in 0..hidden {
18874 out[bi * hidden + i] += w * eo[k * hidden + i];
18875 }
18876 }
18877 };
18878 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18884 if pool.is_some() && active.len() >= 8 {
18885 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18886 {
18887 let panel_ptr = SendVecs(panels.as_mut_ptr());
18888 let experts = &m.experts;
18891 let (active_r, assign_r) = (&active, &assign);
18892 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18893 let run = |start: usize, end: usize| {
18894 let _cpu_scope = inherit_cpu();
18895 #[cfg(target_os = "macos")]
18903 let _metal_cpu = crate::gpu::enter_cpu_scope();
18904 for ai in start..end {
18905 let e = active_r[ai];
18906 let list = &assign_r[e];
18907 let sb = list.len();
18908 let mut sub = vec![0.0f32; sb * cols];
18909 for (k, &(bi, _)) in list.iter().enumerate() {
18910 sub[k * cols..(k + 1) * cols]
18911 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18912 }
18913 unsafe {
18915 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18916 }
18917 }
18918 };
18919 match pool {
18920 Some(p) => p.run_rows(active.len(), &run),
18921 None => run(0, active.len()),
18922 }
18923 }
18924 for (ai, &e) in active.iter().enumerate() {
18925 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18926 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18927 for i in 0..hidden {
18928 out[bi * hidden + i] += w * eo[i];
18929 }
18930 }
18931 }
18932 } else {
18933 for &e in &active {
18934 run_expert(&m.experts[e], &assign[e], &mut out);
18935 }
18936 }
18937 if let Some((se, gate)) = &m.shared {
18938 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18939 let mut gl = vec![0.0f32; b];
18940 gate.matmat(xs, b, &mut gl, pool);
18941 (0..b)
18942 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18943 .collect()
18944 } else {
18945 (0..b).map(|bi| (bi, 1.0)).collect()
18946 };
18947 run_expert(se, &all, &mut out);
18948 }
18949 out
18950}
18951
18952fn moe_ffn_rows_exact(
18964 m: &MoeFfn,
18965 xs: &[f32],
18966 b: usize,
18967 hidden: usize,
18968 pool: Option<&Pool>,
18969) -> Vec<f32> {
18970 let mut out = vec![0.0f32; b * hidden];
18971 let per_row = |out: &mut [f32]| {
18972 for r in 0..b {
18973 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18974 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18975 }
18976 };
18977 let covered = !crate::gpu::enabled_here()
18978 && moe_batch_enabled()
18979 && m.shared.is_none()
18980 && m.resonance.is_none()
18981 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18982 && m.experts.iter().all(|d| d.act == Act::Silu);
18983 if !covered {
18984 per_row(&mut out);
18985 return out;
18986 }
18987 let ne = m.experts.len();
18988 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18990 for r in 0..b {
18991 let x = &xs[r * hidden..(r + 1) * hidden];
18992 accumulate_act(m, x, 1);
18993 let mut logits = vec![0.0f32; ne];
18994 m.router.matvec(x, &mut logits, pool);
18995 let (idx, p, wsum) = moe_route(&logits, m, None);
18996 {
18997 let mut st = m.stats.borrow_mut();
18998 if st.len() < ne {
18999 st.resize(ne, 0);
19000 }
19001 for &e in &idx {
19002 st[e] += 1;
19003 }
19004 }
19005 let w: Vec<f32> = idx
19006 .iter()
19007 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
19008 .collect();
19009 routes.push((idx, w));
19010 }
19011 if routes.iter().any(|(idx, _)| idx.is_empty()) {
19012 per_row(&mut out);
19013 return out;
19014 }
19015 let mut experts: Vec<usize> = Vec::new();
19017 let mut groups: Vec<Vec<usize>> = Vec::new();
19018 for (r, (idx, _)) in routes.iter().enumerate() {
19019 for &e in idx {
19020 match experts.iter().position(|&x| x == e) {
19021 Some(g) => groups[g].push(r),
19022 None => {
19023 experts.push(e);
19024 groups.push(vec![r]);
19025 }
19026 }
19027 }
19028 }
19029 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19030 let inter = m.experts[experts[0]].gate_proj.rows();
19031 let pairs: Vec<(&QTensor, &QTensor)> = experts
19032 .iter()
19033 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19034 .collect();
19035 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19036 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19037 per_row(&mut out);
19038 return out;
19039 }
19040 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19041 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19042 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19043 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19044 per_row(&mut out);
19045 return out;
19046 }
19047 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19049 let mut p = 0usize;
19050 for (g, &e) in experts.iter().enumerate() {
19051 for &r in &groups[g] {
19052 slot.insert((r, e), p);
19053 p += 1;
19054 }
19055 }
19056 for (r, (idx, w)) in routes.iter().enumerate() {
19057 let terms: Vec<(&[f32], f32)> = idx
19058 .iter()
19059 .zip(w)
19060 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19061 .collect();
19062 let row = &mut out[r * hidden..(r + 1) * hidden];
19063 for (i, dst) in row.iter_mut().enumerate() {
19064 let mut acc = 0f32;
19066 for (d, we) in &terms {
19067 acc += we * d[i];
19068 }
19069 *dst = acc;
19070 }
19071 }
19072 out
19073}
19074
19075pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
19079 match pool {
19080 Some(p) if v.len() >= 1 << 15 => {
19081 let base = v.as_mut_ptr() as usize;
19082 p.run_rows(v.len(), &|s, e| {
19083 let sl =
19086 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
19087 for x in sl {
19088 *x = c * (*x / c).tanh();
19089 }
19090 });
19091 }
19092 _ => {
19093 for x in v.iter_mut() {
19094 *x = c * (*x / c).tanh();
19095 }
19096 }
19097 }
19098}
19099
19100thread_local! {
19101 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
19105 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
19106}
19107
19108fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19110 if gate_topk() > 0
19113 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
19114 {
19115 return out;
19116 }
19117 let prism_body = d.gate_proj.has_prism_contract()
19133 || d.up_proj.has_prism_contract()
19134 || d.down_proj.has_prism_contract();
19135 if !prism_body
19136 && crate::gpu::enabled_here()
19137 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19138 {
19139 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19140 crate::gpu::ProbeArm::Gpu
19141 } else {
19142 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19143 };
19144 match arm {
19145 crate::gpu::ProbeArm::Gpu => {
19146 let t0 = std::time::Instant::now();
19147 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19148 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19149 return out;
19150 }
19151 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19155 }
19156 crate::gpu::ProbeArm::CpuTimed => {
19157 let t0 = std::time::Instant::now();
19158 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19159 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19160 return out;
19161 }
19162 crate::gpu::ProbeArm::Cpu => {
19163 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19164 }
19165 }
19166 }
19167 dense_ffn_cpu(d, x, pool)
19168}
19169
19170fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19172 let inter = d.gate_proj.rows();
19173 FFN_SCRATCH.with(|s| {
19174 let mut s = s.borrow_mut();
19175 let [g, u, ..] = &mut *s;
19176 g.resize(inter, 0.0);
19177 if gate_topk() > 0 {
19180 u.resize(inter, 0.0);
19184 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19185 for i in 0..inter {
19186 g[i] = Act::Silu.combine(g[i], 1.0);
19187 }
19188 keep_top_k(g, gate_topk());
19189 for i in 0..inter {
19190 g[i] *= u[i];
19191 }
19192 } else if d.act == Act::Silu && {
19193 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19194 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19195 } {
19196 } else {
19198 u.resize(inter, 0.0);
19199 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19201 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19202 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19203 for i in 0..inter {
19204 g[i] = d.act.combine(g[i], u[i]);
19205 }
19206 }
19207 FFN_PROBE.with(|pr| {
19215 if let Some(acc) = pr.borrow_mut().as_mut() {
19216 let li = crate::gpu::cur_layer();
19217 if li >= 0 {
19218 if let Some(row) = acc.get_mut(li as usize) {
19219 match probe_topk() {
19220 0 if probe_sq() => {
19221 for (a, &v) in row.iter_mut().zip(g.iter()) {
19222 *a += (v as f64) * (v as f64);
19223 }
19224 }
19225 0 if probe_signed() => {
19226 for (a, &v) in row.iter_mut().zip(g.iter()) {
19227 *a += v as f64;
19228 }
19229 }
19230 0 => {
19231 for (a, &v) in row.iter_mut().zip(g.iter()) {
19232 *a += (v as f64).abs();
19233 }
19234 }
19235 k => {
19236 let n = g.len();
19237 let k = k.min(n);
19238 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19239 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19240 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19241 });
19242 let thr = *kth;
19243 for (a, &v) in row.iter_mut().zip(g.iter()) {
19244 if v.abs() >= thr {
19245 *a += 1.0;
19246 }
19247 }
19248 }
19249 }
19250 }
19251 }
19252 }
19253 });
19254 if oracle_topk() > 0 {
19255 keep_top_k(g, oracle_topk());
19256 }
19257 {
19258 let li = crate::gpu::cur_layer();
19259 if li >= 0 {
19260 adump_row(li as usize, g);
19261 }
19262 }
19263 let mut out = attention::take_buf(d.down_proj.rows());
19264 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19265 d.down_proj.matvec(g, &mut out, pool);
19266 out
19267 })
19268}
19269
19270pub struct RefitAcc {
19283 pub support: Vec<u32>,
19284 pub gss: Vec<f32>,
19285 pub ya: Vec<f32>,
19286 pub hidden: usize,
19287 pub tokens: u64,
19288 pub buf_g: Vec<f32>,
19294 pub buf_o: Vec<f32>,
19295 pub buf_t: usize,
19296}
19297
19298type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19302
19303static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19304 std::sync::OnceLock::new();
19305
19306fn ffn_probe_active() -> bool {
19309 FFN_PROBE.with(|p| p.borrow().is_some())
19310}
19311
19312fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19313 REFIT
19314 .get_or_init(|| {
19315 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19316 (
19317 d,
19318 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19319 )
19320 })
19321 })
19322 .as_ref()
19323}
19324
19325fn refit_accumulate(
19327 li: usize,
19328 g: &[f32],
19329 b: usize,
19330 inter: usize,
19331 out: &[f32],
19332 hidden: usize,
19333 pool: Option<&Pool>,
19334) {
19335 let Some((dir, map)) = refit_dir() else {
19336 return;
19337 };
19338 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19339 let (from, to) = *SPAN.get_or_init(|| {
19340 let g = |k: &str, d: usize| {
19341 std::env::var(k)
19342 .ok()
19343 .and_then(|v| v.parse().ok())
19344 .unwrap_or(d)
19345 };
19346 (
19347 g("CMF_FFN_REFIT_FROM", 0),
19348 g("CMF_FFN_REFIT_TO", usize::MAX),
19349 )
19350 });
19351 if li < from || li > to {
19352 return;
19353 }
19354 let mut guard = map.lock().unwrap();
19355 let (map, shared) = &mut *guard;
19356 let acc = match map.entry(li) {
19357 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19358 std::collections::hash_map::Entry::Vacant(e) => {
19359 let path = format!("{dir}/support.{li}.u32");
19360 let Ok(bytes) = std::fs::read(&path) else {
19361 eprintln!("refit: no {path} — layer {li} skipped");
19362 return;
19363 };
19364 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19365 let support: Vec<u32> = bytes[4..4 + n * 4]
19366 .chunks_exact(4)
19367 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19368 .collect();
19369 eprintln!(
19370 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19371 (n * n + hidden * n) as f64 * 4.0 / 1e6
19372 );
19373 e.insert(RefitAcc {
19374 gss: vec![0.0; n * n],
19375 ya: vec![0.0; hidden * n],
19376 buf_g: Vec::new(),
19377 buf_o: Vec::new(),
19378 buf_t: 0,
19379 support,
19380 hidden,
19381 tokens: 0,
19382 })
19383 }
19384 };
19385 let ns = acc.support.len();
19386 let cap = refit_batch();
19388 if acc.buf_g.is_empty() {
19389 acc.buf_g = vec![0.0; ns * cap];
19390 acc.buf_o = vec![0.0; hidden * cap];
19391 }
19392 let take = b.min(cap - acc.buf_t);
19393 for t in 0..take {
19394 let col = acc.buf_t + t;
19395 for (j, &n) in acc.support.iter().enumerate() {
19396 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19397 }
19398 for h in 0..hidden {
19399 acc.buf_o[h * cap + col] = out[t * hidden + h];
19400 }
19401 }
19402 acc.buf_t += take;
19403 acc.tokens += take as u64;
19404 if acc.buf_t < cap {
19405 return;
19406 }
19407 let bt = acc.buf_t;
19408 acc.buf_t = 0;
19409 let RefitAcc {
19419 gss,
19420 ya,
19421 buf_g,
19422 buf_o,
19423 ..
19424 } = acc;
19425 let need = (ns * ns).max(hidden * ns);
19426 if shared.len() < need {
19427 shared.resize(need, 0.0);
19428 }
19429 let scratch = &mut shared[..];
19430 let _ = bt;
19431 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19432 add_into(gss, &scratch[..ns * ns], pool);
19433 if crate::gpu::gemm_nt_f32_transient(
19434 buf_o,
19435 buf_g,
19436 &mut scratch[..hidden * ns],
19437 hidden,
19438 cap,
19439 ns,
19440 ) {
19441 add_into(ya, &scratch[..hidden * ns], pool);
19442 } else {
19443 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19444 }
19445 } else {
19446 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19447 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19448 }
19449 }
19453
19454fn refit_batch() -> usize {
19456 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19457 *B.get_or_init(|| {
19458 std::env::var("CMF_FFN_REFIT_BATCH")
19459 .ok()
19460 .and_then(|v| v.parse().ok())
19461 .unwrap_or(4096)
19462 })
19463}
19464
19465fn accum_outer_t(
19468 c: &mut [f32],
19469 m: usize,
19470 n: usize,
19471 b: usize,
19472 left: &[f32],
19473 right: &[f32],
19474 pool: Option<&Pool>,
19475) {
19476 let ptr = SendMut(c.as_mut_ptr());
19477 let body = |i: usize| {
19478 let ptr = &ptr;
19479 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19480 for t in 0..b {
19481 let a = left[i * b + t];
19482 if a == 0.0 {
19483 continue;
19484 }
19485 for (j, o) in row.iter_mut().enumerate() {
19486 *o += a * right[j * b + t];
19487 }
19488 }
19489 };
19490 match pool {
19491 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19492 for i in s..e {
19493 body(i);
19494 }
19495 }),
19496 _ => {
19497 for i in 0..m {
19498 body(i);
19499 }
19500 }
19501 }
19502}
19503
19504fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19507 let n = dst.len().min(src.len());
19508 match pool {
19509 Some(p) if n >= 1 << 16 => {
19510 let ptr = SendMut(dst.as_mut_ptr());
19511 let f = |s: usize, e: usize| {
19512 let ptr = &ptr;
19513 for blk in s..e {
19514 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19515 for i in a..b {
19516 unsafe { *ptr.0.add(i) += src[i] };
19517 }
19518 }
19519 };
19520 p.run_rows(n.div_ceil(4096), &f);
19521 }
19522 _ => {
19523 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19524 *d += *v;
19525 }
19526 }
19527 }
19528}
19529
19530fn accum_outer(
19535 c: &mut [f32],
19536 m: usize,
19537 n: usize,
19538 b: usize,
19539 left: &[f32],
19540 right: &[f32],
19541 pool: Option<&Pool>,
19542) {
19543 const TILE: usize = 32;
19544 let tiles = m.div_ceil(TILE);
19545 let cp = SendMut(c.as_mut_ptr());
19546 let body = |ti: usize| {
19547 let cp = &cp;
19548 let i0 = ti * TILE;
19549 let i1 = (i0 + TILE).min(m);
19550 for t in 0..b {
19551 let r = &right[t * n..t * n + n];
19552 for i in i0..i1 {
19553 let a = left[i * b + t];
19554 if a == 0.0 {
19555 continue;
19556 }
19557 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19559 for (o, v) in row.iter_mut().zip(r) {
19560 *o += a * *v;
19561 }
19562 }
19563 }
19564 };
19565 match pool {
19566 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19567 for ti in s..e {
19568 body(ti);
19569 }
19570 }),
19571 _ => {
19572 for ti in 0..tiles {
19573 body(ti);
19574 }
19575 }
19576 }
19577}
19578
19579pub fn refit_flush() -> usize {
19581 let Some((dir, map)) = refit_dir() else {
19582 return 0;
19583 };
19584 let guard = map.lock().unwrap();
19585 let mut n = 0;
19586 for (li, acc) in guard.0.iter() {
19587 let w = |name: &str, v: &[f32]| {
19590 let path = format!("{dir}/{name}.{li}.f32");
19591 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19592 match std::fs::write(&path, &bytes) {
19593 Ok(()) => {}
19594 Err(e) => eprintln!(
19595 "refit: FAILED to write {path} ({} MB): {e}",
19596 bytes.len() / 1_000_000
19597 ),
19598 }
19599 };
19600 w("gss", &acc.gss);
19601 w("ya", &acc.ya);
19602 println!(
19603 "refit L{li}: {} support, {} tokens, hidden {}",
19604 acc.support.len(),
19605 acc.tokens,
19606 acc.hidden
19607 );
19608 n += 1;
19609 }
19610 n
19611}
19612
19613fn adump_row(li: usize, g: &[f32]) {
19618 use std::io::Write as _;
19619 static FILES: std::sync::OnceLock<
19620 Option<(
19621 String,
19622 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19623 )>,
19624 > = std::sync::OnceLock::new();
19625 let Some((prefix, map)) = FILES
19626 .get_or_init(|| {
19627 std::env::var("CMF_FFN_ADUMP")
19628 .ok()
19629 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19630 })
19631 .as_ref()
19632 else {
19633 return;
19634 };
19635 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19638 let (from, to) = *SPAN.get_or_init(|| {
19639 let g = |k: &str, d: usize| {
19640 std::env::var(k)
19641 .ok()
19642 .and_then(|v| v.parse().ok())
19643 .unwrap_or(d)
19644 };
19645 (
19646 g("CMF_FFN_ADUMP_FROM", 0),
19647 g("CMF_FFN_ADUMP_TO", usize::MAX),
19648 )
19649 });
19650 if li < from || li > to {
19651 return;
19652 }
19653 let mut map = map.lock().unwrap();
19654 let f = map.entry(li).or_insert_with(|| {
19655 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19656 });
19657 let mut bytes = Vec::with_capacity(g.len() * 2);
19658 for v in g {
19659 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19660 }
19661 let _ = f.write_all(&bytes);
19662}
19663
19664fn oracle_topk() -> usize {
19670 if exact_mtp_calibration_active() {
19671 return 0;
19672 }
19673 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19674 *K.get_or_init(|| {
19675 std::env::var("CMF_FFN_ORACLE_TOPK")
19676 .ok()
19677 .and_then(|v| v.parse().ok())
19678 .unwrap_or(0)
19679 })
19680}
19681
19682fn gate_topk() -> usize {
19688 if exact_mtp_calibration_active() {
19689 return 0;
19690 }
19691 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19692 *K.get_or_init(|| {
19693 std::env::var("CMF_FFN_GATE_TOPK")
19694 .ok()
19695 .and_then(|v| v.parse().ok())
19696 .unwrap_or(0)
19697 })
19698}
19699
19700fn gate_block() -> usize {
19707 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19708 *B.get_or_init(|| {
19709 std::env::var("CMF_FFN_GATE_BLOCK")
19710 .ok()
19711 .and_then(|v| v.parse().ok())
19712 .unwrap_or(1)
19713 })
19714}
19715
19716fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19718 let n = g.len();
19719 let nb = n.div_ceil(block);
19720 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19721 if kb >= nb {
19722 return;
19723 }
19724 let mut score: Vec<f32> = (0..nb)
19725 .map(|b| {
19726 g[b * block..((b + 1) * block).min(n)]
19727 .iter()
19728 .map(|v| v * v)
19729 .sum::<f32>()
19730 })
19731 .collect();
19732 let mut ord = score.clone();
19733 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19734 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19735 });
19736 let thr = *kth;
19737 for b in 0..nb {
19738 if score[b] < thr {
19739 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19740 }
19741 }
19742 score.clear();
19743}
19744
19745fn keep_top_k(g: &mut [f32], k: usize) {
19747 if gate_block() > 1 {
19748 return keep_top_blocks(g, k, gate_block());
19749 }
19750 let n = g.len();
19751 if k == 0 || k >= n {
19752 return;
19753 }
19754 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19755 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19756 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19757 });
19758 let thr = *kth;
19759 for v in g.iter_mut() {
19760 if v.abs() < thr {
19761 *v = 0.0;
19762 }
19763 }
19764}
19765
19766fn probe_sq() -> bool {
19770 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19771 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19772}
19773
19774fn probe_signed() -> bool {
19778 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19779 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19780}
19781
19782fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19790 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19791 M.get_or_init(|| {
19792 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19793 let b = std::fs::read(&p).ok()?;
19794 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19795 let vals: Vec<f32> = b[8..]
19796 .chunks_exact(4)
19797 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19798 .collect();
19799 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19800 Some((inter, vals))
19801 })
19802 .as_ref()
19803}
19804
19805fn probe_topk() -> usize {
19808 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19809 *K.get_or_init(|| {
19810 std::env::var("CMF_FFN_PROBE_TOPK")
19811 .ok()
19812 .and_then(|v| v.parse().ok())
19813 .unwrap_or(0)
19814 })
19815}
19816
19817thread_local! {
19818 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19821 const { std::cell::RefCell::new(None) };
19822}
19823
19824fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19837 if d.gate_proj.has_prism_contract()
19842 || d.up_proj.has_prism_contract()
19843 || d.down_proj.has_prism_contract()
19844 {
19845 return None;
19846 }
19847 let dt = d.down_t.as_ref()?;
19848 let inter = d.gate_proj.rows();
19849 let hidden = dt.cols();
19850 if k == 0 || k >= inter || d.act != Act::Silu {
19851 return None;
19852 }
19853 DYN_SCRATCH.with(|sc| {
19854 let mut sc = sc.borrow_mut();
19855 let DynScratch {
19856 g,
19857 mag,
19858 live,
19859 parts,
19860 } = &mut *sc;
19861 g.resize(inter, 0.0);
19862 d.gate_proj.matvec(x, g, pool);
19863 for v in g.iter_mut() {
19864 *v = inference::silu(*v);
19865 }
19866 mag.clear();
19869 mag.extend(g.iter().map(|v| v.abs()));
19870 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19871 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19872 });
19873 let thr = *kth;
19874 live.clear();
19875 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19876 let mut out = vec![0.0f32; hidden];
19877 match pool {
19878 Some(p) if live.len() >= 64 => {
19879 let nw = p.n_workers() + 1;
19880 parts.clear();
19881 parts.resize(nw * hidden, 0.0);
19882 let ptr = SendMut(parts.as_mut_ptr());
19883 let n = live.len();
19884 let live_ref: &[u32] = live;
19885 let g_ref: &[f32] = g;
19886 p.run(&|w, workers| {
19887 let chunk = n.div_ceil(workers);
19888 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19889 if s >= e {
19890 return;
19891 }
19892 WORKER_SCRATCH.with(|ws| {
19893 let mut ws = ws.borrow_mut();
19894 let [scratch, acc] = &mut *ws;
19895 scratch.resize(hidden.max(x.len()), 0.0);
19896 acc.clear();
19897 acc.resize(hidden, 0.0);
19898 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19899 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19902 d.up_proj.prefetch_row(nx as usize);
19903 dt.prefetch_row(nx as usize);
19904 }
19905 let idx = nrm as usize;
19906 let up = d.up_proj.row_dot(idx, x, scratch);
19907 let a = g_ref[idx] * up;
19908 if a != 0.0 {
19909 dt.add_row_scaled(idx, a, acc, scratch);
19910 }
19911 }
19912 for (j, v) in acc.iter().enumerate() {
19913 unsafe { *ptr.at(w * hidden + j) = *v };
19914 }
19915 });
19916 });
19917 for w in 0..nw {
19918 for (j, o) in out.iter_mut().enumerate() {
19919 *o += parts[w * hidden + j];
19920 }
19921 }
19922 }
19923 _ => {
19924 WORKER_SCRATCH.with(|ws| {
19925 let mut ws = ws.borrow_mut();
19926 let [scratch, _acc] = &mut *ws;
19927 scratch.resize(hidden.max(x.len()), 0.0);
19928 for &nrm in live.iter() {
19929 let idx = nrm as usize;
19930 let up = d.up_proj.row_dot(idx, x, scratch);
19931 let a = g[idx] * up;
19932 if a != 0.0 {
19933 dt.add_row_scaled(idx, a, &mut out, scratch);
19934 }
19935 }
19936 });
19937 }
19938 }
19939 Some(out)
19940 })
19941}
19942
19943struct DynScratch {
19946 g: Vec<f32>,
19947 mag: Vec<f32>,
19948 live: Vec<u32>,
19949 parts: Vec<f32>,
19950}
19951
19952thread_local! {
19953 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19954 std::cell::RefCell::new(DynScratch {
19955 g: Vec::new(),
19956 mag: Vec::new(),
19957 live: Vec::new(),
19958 parts: Vec::new(),
19959 })
19960 };
19961 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19963 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19964}
19965
19966fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19971 let inter = d.gate_proj.rows();
19972 FFN_SCRATCH.with(|s| {
19973 let mut s = s.borrow_mut();
19974 let [g, u, ..] = &mut *s;
19975 g.resize(inter, 0.0);
19976 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19977 } else {
19979 u.resize(inter, 0.0);
19980 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19981 for i in 0..inter {
19982 g[i] = d.act.combine(g[i], u[i]);
19983 }
19984 }
19985 zero_masked_cols(g, 1, inter, mask_row);
19986 let mut out = attention::take_buf(d.down_proj.rows());
19987 d.down_proj.matvec(g, &mut out, pool);
19988 out
19989 })
19990}
19991
19992fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19998 if d.gate_proj.has_prism_contract()
19999 || d.up_proj.has_prism_contract()
20000 || d.down_proj.has_prism_contract()
20001 {
20002 return None;
20003 }
20004 d.act.moe_job_graph_contract()?;
20007 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
20010 return None;
20011 }
20012 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
20013 let mut model_ref = None;
20014 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
20015 let model = model_ref?;
20016 let hidden = jobs[0].down.1;
20017 let mut out = attention::take_buf(hidden);
20018 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20019 Some(out)
20020 } else {
20021 let mut out = out;
20022 attention::recycle_buf(&mut out);
20023 None
20024 }
20025}
20026
20027#[allow(clippy::type_complexity)]
20032#[allow(clippy::type_complexity)]
20033pub(crate) fn moe_parts(
20034 t: &QTensor,
20035) -> Option<(
20036 &std::sync::Arc<cortiq_core::CmfModel>,
20037 usize,
20038 usize,
20039 usize,
20040 &[f32],
20041 &[f32],
20042 bool,
20043 bool,
20044 bool,
20045)> {
20046 match t {
20047 QTensor::Mapped {
20048 model,
20049 idx,
20050 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20051 rows,
20052 cols,
20053 row_scale,
20054 col_field,
20055 ..
20056 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20057 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20058 )),
20059 QTensor::Mapped {
20061 model,
20062 idx,
20063 dtype: cortiq_core::TensorDtype::Q1,
20064 rows,
20065 cols,
20066 ..
20067 } => Some((
20068 model,
20069 *idx,
20070 *rows,
20071 *cols,
20072 &[][..],
20073 &[][..],
20074 true,
20075 false,
20076 false,
20077 )),
20078 QTensor::Mapped {
20080 model,
20081 idx,
20082 dtype: cortiq_core::TensorDtype::Q4Tiled,
20083 rows,
20084 cols,
20085 ..
20086 } => Some((
20087 model,
20088 *idx,
20089 *rows,
20090 *cols,
20091 &[][..],
20092 &[][..],
20093 false,
20094 true,
20095 false,
20096 )),
20097 QTensor::Mapped {
20099 model,
20100 idx,
20101 dtype: cortiq_core::TensorDtype::Q4TiledP,
20102 rows,
20103 cols,
20104 ..
20105 } => Some((
20106 model,
20107 *idx,
20108 *rows,
20109 *cols,
20110 &[][..],
20111 &[][..],
20112 false,
20113 true,
20114 false,
20115 )),
20116 QTensor::Mapped {
20120 model,
20121 idx,
20122 dtype: cortiq_core::TensorDtype::Q2TiledP,
20123 rows,
20124 cols,
20125 ..
20126 } => Some((
20127 model,
20128 *idx,
20129 *rows,
20130 *cols,
20131 &[][..],
20132 &[][..],
20133 false,
20134 true,
20135 true,
20136 )),
20137 _ => None,
20138 }
20139}
20140
20141#[cfg(target_os = "macos")]
20148#[allow(clippy::type_complexity)]
20149fn chunk_moe_parts(
20150 m: &MoeFfn,
20151 hidden: usize,
20152) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20153 if m.shared.is_some()
20154 || m.per_expert_scale.is_some()
20155 || m.resonance.is_some()
20156 || m.route_tau.is_some()
20157 || m.router_input_norm
20158 || m.experts.is_empty()
20159 || m.top_k == 0
20160 || std::env::var("CMF_RMS_TRACE").is_ok()
20161 || std::env::var("CMF_ACT_DUMP").is_ok()
20162 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20163 {
20164 return None;
20165 }
20166 let (rf, rr, rc) = m.router.f32_parts()?;
20167 if rr != m.experts.len() || rc != hidden {
20168 return None;
20169 }
20170 let inter = m.experts[0].gate_proj.rows();
20171 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20174 let experts = m
20175 .experts
20176 .iter()
20177 .map(|e| {
20178 if e.act != Act::Silu
20179 || !e.segs.is_empty()
20180 || e.gate_proj.rows() != inter
20181 || e.up_proj.rows() != inter
20182 || e.down_proj.cols() != inter
20183 || e.gate_proj.cols() != hidden
20184 || e.down_proj.rows() != hidden
20185 {
20186 return None;
20187 }
20188 if q8 {
20189 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20190 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20191 }
20192 Some((
20193 e.gate_proj.mapped_q4tp()?.1,
20194 e.up_proj.mapped_q4tp()?.1,
20195 e.down_proj.mapped_q4tp()?.1,
20196 ))
20197 })
20198 .collect::<Option<Vec<_>>>()?;
20199 Some((rf, experts, q8))
20200}
20201
20202#[cfg(target_os = "macos")]
20215fn metal_flint_moe_graph_parts(
20216 m: &MoeFfn,
20217 hidden: usize,
20218 scratch_key: usize,
20219) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20220 use std::sync::Arc;
20221
20222 const HIDDEN: usize = 2_048;
20223 const ROUTED_INTER: usize = 512;
20224 const SHARED_INTER: usize = 1_024;
20225 const EXPERTS: usize = 256;
20226 const TOP_K: usize = 6;
20227
20228 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20229 return None;
20230 }
20231 let routed0 = m.experts.first()?;
20232 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20233 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20234 if router_rows != EXPERTS || router_cols != HIDDEN {
20235 return None;
20236 }
20237 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20238 return None;
20239 }
20240 let same_q2 = |t: &QTensor| -> Option<usize> {
20241 let (owner, idx) = t.mapped_q2tp()?;
20242 Arc::ptr_eq(&owner, &model).then_some(idx)
20243 };
20244 let same_q4 = |t: &QTensor| -> Option<usize> {
20245 let (owner, idx) = t.mapped_q4tp()?;
20246 Arc::ptr_eq(&owner, &model).then_some(idx)
20247 };
20248 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20249 if e.act != Act::Silu
20250 || !e.segs.is_empty()
20251 || e.gate_proj.rows() != ROUTED_INTER
20252 || e.gate_proj.cols() != HIDDEN
20253 || e.up_proj.rows() != ROUTED_INTER
20254 || e.up_proj.cols() != HIDDEN
20255 || e.down_proj.rows() != HIDDEN
20256 || e.down_proj.cols() != ROUTED_INTER
20257 {
20258 return None;
20259 }
20260 Some((
20261 same_q2(&e.gate_proj)?,
20262 same_q2(&e.up_proj)?,
20263 same_q4(&e.down_proj)?,
20264 ))
20265 };
20266 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20267 if e.act != Act::Silu
20268 || !e.segs.is_empty()
20269 || e.gate_proj.rows() != SHARED_INTER
20270 || e.gate_proj.cols() != HIDDEN
20271 || e.up_proj.rows() != SHARED_INTER
20272 || e.up_proj.cols() != HIDDEN
20273 || e.down_proj.rows() != HIDDEN
20274 || e.down_proj.cols() != SHARED_INTER
20275 {
20276 return None;
20277 }
20278 Some((
20279 same_q4(&e.gate_proj)?,
20280 same_q4(&e.up_proj)?,
20281 same_q4(&e.down_proj)?,
20282 ))
20283 };
20284 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20285 if experts.len() != EXPERTS {
20286 return None;
20287 }
20288 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20289 if shared_gate.is_some() {
20290 return None;
20291 }
20292 Some(crate::gpu::GpuFlintMoe {
20293 router,
20294 bias: m.expert_bias.as_deref(),
20295 experts,
20296 scratch_key,
20297 shared: shared(shared_ffn)?,
20298 top_k: m.top_k,
20299 norm_topk: m.norm_topk_prob,
20300 route_scale: m.routed_scaling,
20301 sigmoid: m.router_sigmoid,
20302 sigmoid_denom_eps: m.sigmoid_denom_eps,
20303 })
20304}
20305
20306#[cfg(target_os = "macos")]
20307fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20308 if m.router_input_norm
20309 || m.route_tau.is_some()
20310 || m.mask.is_some()
20311 || m.per_expert_scale.is_some()
20312 || m.experts.is_empty()
20313 || m.top_k == 0
20314 || m.resonance.is_some()
20315 {
20316 return None;
20317 }
20318 let (sh, sg) = match &m.shared {
20323 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20324 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20325 None => return None,
20326 };
20327 let (rf, rr, rc) = m.router.f32_parts()?;
20328 if rr != m.experts.len() || rc != hidden {
20329 return None;
20330 }
20331 let shared_gated = sg.is_some();
20332 let sf = match sg {
20333 Some(sg) => {
20334 let (sf, sr, sc) = sg.f32_parts()?;
20335 if sr * sc != hidden {
20336 return None;
20337 }
20338 sf
20339 }
20340 None => &rf[..hidden],
20343 };
20344 if let Some(b) = &m.expert_bias {
20345 if b.len() != m.experts.len() {
20346 return None;
20347 }
20348 }
20349 let inter = m.experts[0].gate_proj.rows();
20350 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20353 let q8 = crate::gpu_metal::moe_q8::enabled()
20357 && m.shared.is_none()
20358 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20359 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20360 if e.act != Act::Silu
20361 || e.gate_proj.rows() != inter
20362 || e.gate_proj.cols() != hidden
20363 || e.up_proj.rows() != inter
20364 || e.up_proj.cols() != hidden
20365 || e.down_proj.rows() != hidden
20366 || e.down_proj.cols() != inter
20367 {
20368 return None;
20369 }
20370 if q8 {
20371 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20372 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20373 }
20374 let pick = |t: &QTensor| -> Option<usize> {
20375 if gu_q2 {
20376 t.mapped_q2tp().map(|(_, i)| i)
20377 } else {
20378 t.mapped_q4tp().map(|(_, i)| i)
20379 }
20380 };
20381 Some((
20382 pick(&e.gate_proj)?,
20383 pick(&e.up_proj)?,
20384 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20385 ))
20386 };
20387 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20388 let shared = match sh {
20389 Some(sh) => Some(trio(sh)?),
20390 None => None,
20391 };
20392 Some(crate::gpu::GpuMoe {
20393 router: rf,
20394 sgate: sf,
20395 experts,
20396 shared,
20397 n_exp: m.experts.len(),
20398 top_k: m.top_k,
20399 inter,
20400 norm_topk: m.norm_topk_prob,
20401 route_scale: m.routed_scaling,
20402 gu_q2,
20403 q8,
20404 sigmoid: m.router_sigmoid,
20405 sigmoid_denom_eps: m.sigmoid_denom_eps,
20406 bias: m.expert_bias.as_deref(),
20407 shared_gated,
20408 })
20409}
20410
20411fn forced_ids_at(step: usize) -> Option<u32> {
20417 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20418 IDS.get_or_init(|| {
20419 std::env::var("CMF_FORCE_IDS")
20420 .ok()
20421 .and_then(|p| std::fs::read_to_string(p).ok())
20422 .map(|s| {
20423 s.split_whitespace()
20424 .filter_map(|t| t.parse().ok())
20425 .collect()
20426 })
20427 .unwrap_or_default()
20428 })
20429 .get(step)
20430 .copied()
20431}
20432
20433pub(crate) fn moe_push_job_parts<'a>(
20437 gate: &'a QTensor,
20438 up: &'a QTensor,
20439 down: &'a QTensor,
20440 x: &[f32],
20441 w: f32,
20442 swiglu_limit: f32,
20443 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20444 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20445) -> Option<()> {
20446 use crate::qtensor::prescale;
20447 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20448 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20449 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20450 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20451 return None; }
20453 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20456 return None;
20457 }
20458 if !gq2 && dq2 {
20459 return None;
20460 }
20461 model_ref.get_or_insert_with(|| gm.clone());
20462 let dt = |cf: &[f32]| {
20463 if cf.is_empty() {
20464 cortiq_core::TensorDtype::Q8Row
20465 } else {
20466 cortiq_core::TensorDtype::Q8_2f
20467 }
20468 };
20469 jobs.push(crate::gpu::MoeJob {
20470 gate: (gi, gr, gc, grs),
20471 up: (ui, ur, uc, urs),
20472 down: (di, dr, dc, drs),
20473 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20474 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20475 down_col: dcf,
20476 w,
20477 q1: gq1,
20478 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20479 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20480 gu_q2: gq2,
20481 swiglu_limit,
20482 act: 0,
20483 });
20484 Some(())
20485}
20486
20487fn moe_push_job<'a>(
20489 d: &'a DenseFfn,
20490 x: &[f32],
20491 w: f32,
20492 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20493 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20494) -> Option<()> {
20495 use crate::qtensor::prescale;
20496 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20501 let act = graph_act.code();
20502 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20503 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20504 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20505 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20506 return None; }
20508 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20509 return None;
20510 }
20511 if !gq2 && dq2 {
20512 return None;
20513 }
20514 model_ref.get_or_insert_with(|| gm.clone());
20515 let gdt = if gcf.is_empty() {
20516 cortiq_core::TensorDtype::Q8Row
20517 } else {
20518 cortiq_core::TensorDtype::Q8_2f
20519 };
20520 let udt = if ucf.is_empty() {
20521 cortiq_core::TensorDtype::Q8Row
20522 } else {
20523 cortiq_core::TensorDtype::Q8_2f
20524 };
20525 jobs.push(crate::gpu::MoeJob {
20526 gate: (gi, gr, gc, grs),
20527 up: (ui, ur, uc, urs),
20528 down: (di, dr, dc, drs),
20529 xs_gate: prescale(x, gcf, gdt).into_owned(),
20530 xs_up: prescale(x, ucf, udt).into_owned(),
20531 down_col: dcf,
20532 w,
20533 q1: gq1,
20534 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20535 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20536 gu_q2: gq2,
20537 swiglu_limit,
20538 act,
20539 });
20540 Some(())
20541}
20542
20543fn sparse_ffn_quant(
20550 d: &DenseFfn,
20551 x: &[f32],
20552 active: &[u16],
20553 hidden: usize,
20554 pool: Option<&Pool>,
20555) -> Vec<f32> {
20556 let n = active.len();
20557 let inter = d.gate_proj.rows();
20558 let mut act = vec![0.0f32; n];
20559 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20562 let compute = |ai: usize| -> f32 {
20563 let idx = active[ai] as usize;
20564 if idx >= inter {
20565 return 0.0; }
20567 let mut s = if need_scratch {
20568 vec![0.0f32; hidden]
20569 } else {
20570 Vec::new()
20571 };
20572 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20573 let up = d.up_proj.row_dot(idx, x, &mut s);
20574 d.act.combine(gate, up)
20575 };
20576 match pool {
20577 Some(p) if n >= 256 => {
20578 let ptr = SendMut(act.as_mut_ptr());
20579 p.run(&|widx, nw| {
20580 let chunk = n.div_ceil(nw);
20581 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20582 for ai in s..e {
20583 unsafe { *ptr.at(ai) = compute(ai) };
20584 }
20585 });
20586 }
20587 _ => {
20588 for (ai, a) in act.iter_mut().enumerate() {
20589 *a = compute(ai);
20590 }
20591 }
20592 }
20593 let mut out = vec![0.0f32; hidden];
20595 for (ai, &idx) in active.iter().enumerate() {
20596 let w = act[ai];
20597 if w.abs() >= 1e-12 && (idx as usize) < inter {
20598 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20599 }
20600 }
20601 out
20602}
20603
20604#[doc(hidden)]
20606pub fn sparse_ffn_quant_for_test(
20607 d: &DenseFfn,
20608 x: &[f32],
20609 active: &[u16],
20610 hidden: usize,
20611) -> Vec<f32> {
20612 sparse_ffn_quant(d, x, active, hidden, None)
20613}
20614
20615fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20619 let deq = |t: &QTensor| -> Vec<f32> {
20620 let (rows, cols) = (t.rows(), t.cols());
20621 let mut out = vec![0.0f32; rows * cols];
20622 for r in 0..rows {
20623 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20624 }
20625 out
20626 };
20627 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20628}
20629
20630struct SendMut(*mut f32);
20632unsafe impl Send for SendMut {}
20633unsafe impl Sync for SendMut {}
20634impl SendMut {
20635 #[inline]
20636 #[allow(clippy::mut_from_ref)]
20639 unsafe fn at(&self, i: usize) -> &mut f32 {
20640 unsafe { &mut *self.0.add(i) }
20641 }
20642}
20643
20644pub(crate) fn moe_route(
20656 logits: &[f32],
20657 m: &MoeFfn,
20658 allowed: Option<&[bool]>,
20659) -> (Vec<usize>, Vec<f32>, f32) {
20660 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20661}
20662
20663pub(crate) fn moe_route_with_eps(
20673 logits: &[f32],
20674 m: &MoeFfn,
20675 allowed: Option<&[bool]>,
20676 sigmoid_denom_eps: f32,
20677) -> (Vec<usize>, Vec<f32>, f32) {
20678 let ne = logits.len();
20679 let admit = |e: usize| {
20685 m.mask.as_ref().is_none_or(|mk| mk[e])
20686 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20687 };
20688 if m.resonance.is_some() && m.top_k == 1 {
20701 let mut best: Option<usize> = None;
20702 for e in (0..ne).filter(|&e| admit(e)) {
20703 let l = logits[e];
20704 if l == f32::NEG_INFINITY || l.is_nan() {
20705 continue;
20706 }
20707 if best.is_none_or(|b| l > logits[b]) {
20708 best = Some(e);
20709 }
20710 }
20711 if let Some(b) = best {
20712 let mut p = vec![0.0f32; ne];
20713 p[b] = 1.0;
20714 return (vec![b], p, 1.0 / m.routed_scaling);
20715 }
20716 }
20717 let p: Vec<f32> = if m.router_sigmoid {
20723 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20724 } else {
20725 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20726 if mx == f32::NEG_INFINITY {
20727 vec![1.0 / ne.max(1) as f32; ne]
20728 } else {
20729 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20730 let s: f32 = e.iter().sum();
20731 for v in &mut e {
20732 *v /= s;
20733 }
20734 e
20735 }
20736 };
20737 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20738 match &m.expert_bias {
20740 Some(b) => idx.sort_unstable_by(|&x, &y| {
20741 (p[y] + b[y])
20742 .partial_cmp(&(p[x] + b[x]))
20743 .unwrap()
20744 .then(x.cmp(&y))
20745 }),
20746 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20747 }
20748 idx.truncate(m.top_k);
20749 if let Some(tau) = m.route_tau {
20753 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20754 if total > 0.0 {
20755 let mut acc = 0.0f32;
20756 let mut keep = idx.len();
20757 for (i, &e) in idx.iter().enumerate() {
20758 acc += p[e];
20759 if acc >= tau * total {
20760 keep = i + 1;
20761 break;
20762 }
20763 }
20764 idx.truncate(keep);
20765 }
20766 }
20767 let wsum: f32 = if m.norm_topk_prob {
20768 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20769 (if m.router_sigmoid {
20773 s + sigmoid_denom_eps
20774 } else {
20775 s
20776 }) / m.routed_scaling
20777 } else {
20778 1.0 / m.routed_scaling
20779 };
20780 (idx, p, wsum)
20781}
20782
20783fn moe_trace(idx: &[usize]) {
20785 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20786}
20787
20788pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20791 use std::io::Write;
20792 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20793 std::sync::OnceLock::new();
20794 let Some(f) = F.get_or_init(|| {
20795 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20796 Some(std::sync::Mutex::new(
20797 std::fs::OpenOptions::new()
20798 .create(true)
20799 .append(true)
20800 .open(p)
20801 .ok()?,
20802 ))
20803 }) else {
20804 return;
20805 };
20806 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20807 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20808}
20809
20810pub(crate) fn moe_ffn(
20813 m: &MoeFfn,
20814 x: &[f32],
20815 pool: Option<&Pool>,
20816 allowed: Option<&[bool]>,
20817) -> Vec<f32> {
20818 let r = moe_ffn_route(m, x, pool, allowed);
20819 moe_ffn_experts(m, x, &r, pool)
20820}
20821
20822pub(crate) struct MoeRoute {
20826 pub idx: Vec<usize>,
20827 pub p: Vec<f32>,
20828 pub wsum: f32,
20829 pub logits: Vec<f32>,
20830}
20831
20832pub(crate) fn moe_ffn_route(
20838 m: &MoeFfn,
20839 x: &[f32],
20840 pool: Option<&Pool>,
20841 allowed: Option<&[bool]>,
20842) -> MoeRoute {
20843 accumulate_act(m, x, 1);
20844 let ne = m.experts.len();
20845 let mut logits = vec![0.0f32; ne];
20846 match &m.resonance {
20847 Some(r) => r.scores(x, &mut logits),
20848 None => m.router.matvec(x, &mut logits, pool),
20849 }
20850 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20851 {
20852 let mut st = m.stats.borrow_mut();
20853 if st.len() < ne {
20854 st.resize(ne, 0);
20855 }
20856 for &e in &idx {
20857 st[e] += 1;
20858 }
20859 }
20860 moe_trace(&idx);
20866 MoeRoute {
20867 idx,
20868 p,
20869 wsum,
20870 logits,
20871 }
20872}
20873
20874#[cfg(target_os = "macos")]
20883fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20884 const HIDDEN: usize = 2_048;
20885 const ROUTED_INTER: usize = 512;
20886 const SHARED_INTER: usize = 1_024;
20887 const EXPERTS: usize = 256;
20888 const TOP_K: usize = 6;
20889
20890 let Some(routed) = m.experts.first() else {
20891 return false;
20892 };
20893 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20894 return false;
20895 };
20896 let Some((shared, shared_gate)) = &m.shared else {
20897 return false;
20898 };
20899 if shared_gate.is_some()
20900 || model.header.arch.arch_name != "flint"
20901 || model.header.arch.prism_hadamard.is_some()
20902 || model.header.arch.hidden_size != HIDDEN
20903 || m.experts.len() != EXPERTS
20904 || m.top_k != TOP_K
20905 || !m.router_sigmoid
20906 || !m.norm_topk_prob
20907 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20908 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20909 || m.route_tau.is_some()
20910 || m.mask.is_some()
20911 || m.per_expert_scale.is_some()
20912 || m.router_input_norm
20913 || m.resonance.is_some()
20914 || !m.grown.is_empty()
20915 {
20916 return false;
20917 }
20918
20919 let routed_q2 = routed.act == Act::Silu
20920 && routed.segs.is_empty()
20921 && routed.gate_proj.rows() == ROUTED_INTER
20922 && routed.gate_proj.cols() == HIDDEN
20923 && routed.up_proj.rows() == ROUTED_INTER
20924 && routed.up_proj.cols() == HIDDEN
20925 && routed.down_proj.rows() == HIDDEN
20926 && routed.down_proj.cols() == ROUTED_INTER
20927 && routed.up_proj.mapped_q2tp().is_some()
20928 && routed.down_proj.mapped_q4tp().is_some();
20929 let shared_q4 = shared.act == Act::Silu
20930 && shared.segs.is_empty()
20931 && shared.gate_proj.rows() == SHARED_INTER
20932 && shared.gate_proj.cols() == HIDDEN
20933 && shared.up_proj.rows() == SHARED_INTER
20934 && shared.up_proj.cols() == HIDDEN
20935 && shared.down_proj.rows() == HIDDEN
20936 && shared.down_proj.cols() == SHARED_INTER
20937 && shared.gate_proj.mapped_q4tp().is_some()
20938 && shared.up_proj.mapped_q4tp().is_some()
20939 && shared.down_proj.mapped_q4tp().is_some();
20940 routed_q2 && shared_q4
20941}
20942
20943pub(crate) fn moe_ffn_experts(
20946 m: &MoeFfn,
20947 x: &[f32],
20948 r: &MoeRoute,
20949 pool: Option<&Pool>,
20950) -> Vec<f32> {
20951 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20952 if crate::gpu::enabled_here() {
20957 #[cfg(target_os = "macos")]
20958 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20959 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20965 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20966 });
20967 }
20968 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20969 crate::gpu::ProbeArm::Gpu => {
20970 let t0 = std::time::Instant::now();
20971 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20972 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20973 return out;
20974 }
20975 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20976 }
20977 crate::gpu::ProbeArm::CpuTimed => {
20978 let t0 = std::time::Instant::now();
20979 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20980 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20981 return out;
20982 }
20983 crate::gpu::ProbeArm::Cpu => {
20984 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20985 }
20986 }
20987 }
20988 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20989}
20990
20991fn moe_ffn_minicpm_tail(
20995 slot: &mut crate::minicpm_tail::MiniCpmTail,
20996 li: usize,
20997 total_layers: usize,
20998 m: &MoeFfn,
20999 x: &[f32],
21000 pool: Option<&Pool>,
21001) -> Vec<f32> {
21002 let route = moe_ffn_route(m, x, pool, None);
21003 match slot.forward(li, m, x, &route, pool, total_layers) {
21004 Some(out) => out,
21005 None => moe_ffn_experts(m, x, &route, pool),
21010 }
21011}
21012
21013fn moe_ffn_banked(
21017 slot: &mut crate::mimo_moe::Slot,
21018 li: usize,
21019 m: &MoeFfn,
21020 x: &[f32],
21021 pool: Option<&Pool>,
21022) -> Vec<f32> {
21023 let t0 = std::time::Instant::now();
21024 let r = moe_ffn_route(m, x, pool, None);
21025 slot.note_route(t0.elapsed().as_nanos() as u64);
21026 match slot.forward(li, m, x, &r, pool) {
21027 Some(out) => out,
21028 None => crate::qtensor::float_activations_scope(|| {
21029 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21030 }),
21031 }
21032}
21033
21034fn moe_ffn_banked_rows(
21036 slot: &mut crate::mimo_moe::Slot,
21037 li: usize,
21038 m: &MoeFfn,
21039 xs: &[f32],
21040 b: usize,
21041 hidden: usize,
21042 pool: Option<&Pool>,
21043) -> Vec<f32> {
21044 let t0 = std::time::Instant::now();
21045 let routes: Vec<_> = xs
21046 .chunks_exact(hidden)
21047 .map(|x| moe_ffn_route(m, x, pool, None))
21048 .collect();
21049 slot.note_route(t0.elapsed().as_nanos() as u64);
21050 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21051 return out;
21052 }
21053 let mut out = Vec::with_capacity(b * hidden);
21054 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21055 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21056 crate::qtensor::float_activations_scope(|| {
21058 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21059 })
21060 });
21061 out.extend(row);
21062 }
21063 out
21064}
21065
21066fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21071 use std::sync::atomic::{AtomicBool, Ordering};
21072 if built {
21073 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
21074 if total_layers > 0 && layers_run < total_layers {
21075 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
21076 } else {
21077 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
21078 }
21079 } else {
21080 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
21081 }
21082 static SAID: AtomicBool = AtomicBool::new(false);
21083 if !SAID.swap(true, Ordering::Relaxed) {
21084 if built {
21085 tracing::info!("wgpu whole-token graph: ACTIVE");
21086 } else {
21087 tracing::warn!("wgpu whole-token graph refused — per-op path");
21088 }
21089 }
21090}
21091
21092pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21096pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21097pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21101pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21103
21104pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21108pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21109pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
21110 std::sync::atomic::AtomicU64::new(0);
21111pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21112pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21113pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
21117 std::sync::atomic::AtomicU64::new(0);
21118pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21119pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21120 std::sync::atomic::AtomicU64::new(0);
21121pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21122 std::sync::atomic::AtomicU64::new(0);
21123
21124fn moe_batch_enabled() -> bool {
21127 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21128 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21129}
21130
21131fn moe_ffn_cpu_batched(
21137 m: &MoeFfn,
21138 x: &[f32],
21139 idx: &[usize],
21140 p: &[f32],
21141 wsum: f32,
21142 pool: Option<&Pool>,
21143) -> Option<Vec<f32>> {
21144 if idx.is_empty() || !moe_batch_enabled() {
21145 return None;
21146 }
21147 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21151 return None;
21152 }
21153 let n = idx.len() + usize::from(m.shared.is_some());
21154 let mut pairs = Vec::with_capacity(n);
21155 let mut downs = Vec::with_capacity(n);
21156 let mut ws = Vec::with_capacity(n);
21157 for &e in idx {
21158 let d = &m.experts[e];
21159 if d.act != Act::Silu {
21160 return None;
21161 }
21162 pairs.push((&d.gate_proj, &d.up_proj));
21163 downs.push(&d.down_proj);
21164 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21165 }
21166 if let Some((se, gate)) = &m.shared {
21169 if se.act != Act::Silu {
21170 return None;
21171 }
21172 let g = gate.as_ref().map_or(1.0, |gate| {
21173 let mut gl = [0.0f32; 1];
21174 gate.matvec(x, &mut gl, pool);
21175 1.0 / (1.0 + (-gl[0]).exp())
21176 });
21177 pairs.push((&se.gate_proj, &se.up_proj));
21178 downs.push(&se.down_proj);
21179 ws.push(g);
21180 }
21181 let inter = pairs[0].0.rows();
21182 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21183 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21184 return None;
21185 }
21186 let mut out = attention::take_buf(x.len());
21187 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21188 attention::recycle_buf(&mut out);
21189 return None;
21190 }
21191 Some(out)
21192}
21193
21194pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21202pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21203pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21204pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21205pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21206
21207pub(crate) fn moe_cold_experts_cpu(
21208 experts: &[(&DenseFfn, f32)],
21209 x: &[f32],
21210 pool: Option<&Pool>,
21211) -> Vec<f32> {
21212 let mut out = attention::take_buf(x.len());
21213 if experts.is_empty() {
21214 return out;
21215 }
21216 let pairs: Vec<_> = experts
21217 .iter()
21218 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21219 .collect();
21220 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21221 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21222 let inter = experts[0].0.gate_proj.rows();
21223 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21224 let silu_only = experts
21232 .iter()
21233 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21234 if silu_only
21235 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21236 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21237 {
21238 return out;
21239 }
21240 out.fill(0.0);
21241 for &(expert, weight) in experts {
21242 let mut one = dense_ffn(expert, x, pool);
21243 for (o, v) in out.iter_mut().zip(&one) {
21244 *o += weight * v;
21245 }
21246 attention::recycle_buf(&mut one);
21247 }
21248 out
21249}
21250
21251pub(crate) fn moe_cold_experts_rows_cpu(
21255 jobs: &[Vec<(&DenseFfn, f32)>],
21256 xs: &[f32],
21257 hidden: usize,
21258 pool: Option<&Pool>,
21259) -> Vec<f32> {
21260 let mut out = vec![0.0; xs.len()];
21261 let mut experts: Vec<&DenseFfn> = Vec::new();
21262 let mut groups: Vec<Vec<usize>> = Vec::new();
21263 let mut terms = vec![Vec::new(); jobs.len()];
21264 for (r, row) in jobs.iter().enumerate() {
21265 for &(e, w) in row {
21266 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21267 Some(g) => g,
21268 None => {
21269 experts.push(e);
21270 groups.push(Vec::new());
21271 groups.len() - 1
21272 }
21273 };
21274 terms[r].push((g, groups[g].len(), w));
21275 groups[g].push(r);
21276 }
21277 }
21278 if experts.is_empty() {
21279 return out;
21280 }
21281 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21282 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21283 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21284 let count: usize = lens.iter().sum();
21285 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21286 let mut ds = vec![vec![0.0; hidden]; count];
21287 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21288 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21289 {
21290 let mut offset = 0;
21291 let offsets: Vec<_> = lens
21292 .iter()
21293 .map(|&n| {
21294 let start = offset;
21295 offset += n;
21296 start
21297 })
21298 .collect();
21299 for (r, terms) in terms.iter().enumerate() {
21300 for &(g, slot, w) in terms {
21301 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21302 .iter_mut()
21303 .zip(&ds[offsets[g] + slot])
21304 {
21305 *o += w * v;
21306 }
21307 }
21308 }
21309 } else {
21310 for (r, jobs) in jobs.iter().enumerate() {
21311 if !jobs.is_empty() {
21312 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21313 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21314 attention::recycle_buf(&mut row);
21315 }
21316 }
21317 }
21318 out
21319}
21320
21321fn moe_ffn_cpu(
21323 m: &MoeFfn,
21324 x: &[f32],
21325 idx: &[usize],
21326 p: &[f32],
21327 wsum: f32,
21328 pool: Option<&Pool>,
21329) -> Vec<f32> {
21330 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21331 return out;
21332 }
21333 let mut out = attention::take_buf(x.len());
21334 for &e in idx {
21335 let mut eo = dense_ffn(&m.experts[e], x, pool);
21336 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21337 for i in 0..out.len() {
21338 out[i] += w * eo[i];
21339 }
21340 attention::recycle_buf(&mut eo);
21341 }
21342 if let Some((se, gate)) = &m.shared {
21343 let mut so = dense_ffn(se, x, pool);
21344 let g = gate.as_ref().map_or(1.0, |gate| {
21345 let mut gl = [0.0f32; 1];
21346 gate.matvec(x, &mut gl, pool);
21347 1.0 / (1.0 + (-gl[0]).exp())
21348 });
21349 for i in 0..out.len() {
21350 out[i] += g * so[i];
21351 }
21352 attention::recycle_buf(&mut so);
21353 }
21354 out
21355}
21356
21357#[allow(clippy::too_many_arguments)]
21365pub(crate) fn mla_attention(
21366 w: &MlaWeights,
21367 normed: &[f32],
21368 cache: &mut crate::kv_cache::LayerKvCache,
21369 position: usize,
21370 inv_freq: &[f32],
21371 rope_scale: f32,
21372 eps: f64,
21373 pool: Option<&Pool>,
21374) -> Vec<f32> {
21375 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21376 let hd = dr + dn;
21377 let mut q = vec![0.0f32; nh * hd];
21378 match (&w.q_a, &w.q_a_norm) {
21379 (Some(qa), Some(qn)) => {
21380 let mut t = vec![0.0f32; qa.rows()];
21381 qa.matvec(normed, &mut t, pool);
21382 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21383 w.q_proj.matvec(&tn, &mut q, pool);
21384 }
21385 _ => w.q_proj.matvec(normed, &mut q, pool),
21386 }
21387 let mut ca = vec![0.0f32; lora + dr];
21388 w.kv_a.matvec(normed, &mut ca, pool);
21389 let (c_lat, k_rope) = ca.split_at_mut(lora);
21390 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21391 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21392 w.kv_b.matvec(&latn, &mut kvb, pool);
21393 if !w.nope {
21394 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21395 }
21396 for h in 0..nh {
21397 if !w.nope {
21398 attention::rope_rotate_scaled(
21399 &mut q[h * hd..h * hd + dr],
21400 position,
21401 inv_freq,
21402 rope_scale,
21403 );
21404 }
21405 }
21406 let mut k = vec![0.0f32; nh * hd];
21407 let mut v = vec![0.0f32; nh * hd];
21408 for h in 0..nh {
21409 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21410 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21411 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21412 }
21413 cache.append(&k, &v, &vec![true; nh]);
21414 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21415 attention::recycle_buf(&mut imp);
21416 let mut ov = vec![0.0f32; nh * dv];
21417 for h in 0..nh {
21418 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21419 }
21420 let mut out = vec![0.0f32; w.o_proj.rows()];
21421 w.o_proj.matvec(&ov, &mut out, pool);
21422 out
21423}
21424
21425fn dense_moe_ffn(
21432 dm: &DenseMoeFfn,
21433 x_normed: &[f32],
21434 h_raw: &[f32],
21435 eps: f64,
21436 norm_style: NormStyle,
21437 pool: Option<&Pool>,
21438) -> Vec<f32> {
21439 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21440 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21441 let m = &dm.moe;
21442 let ne = m.experts.len();
21443 let mut logits = vec![0.0f32; ne];
21444 if m.router_input_norm {
21445 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21446 let inv = 1.0 / (ss + eps as f32).sqrt();
21447 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21448 m.router.matvec(&xr, &mut logits, pool);
21449 } else {
21450 m.router.matvec(h_raw, &mut logits, pool);
21451 }
21452 let (idx, p, wsum) = moe_route(&logits, m, None);
21453 {
21454 let mut st = m.stats.borrow_mut();
21455 if st.len() < ne {
21456 st.resize(ne, 0);
21457 }
21458 for &e in &idx {
21459 st[e] += 1;
21460 }
21461 }
21462 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21463 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21464 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21465 for (di, mi) in d.iter_mut().zip(&mo) {
21466 *di += mi;
21467 }
21468 d
21469}
21470
21471fn moe_gpu_refused(why: &'static str) {
21478 use std::sync::atomic::{AtomicBool, Ordering};
21479 static SAID: AtomicBool = AtomicBool::new(false);
21480 if !SAID.swap(true, Ordering::Relaxed) {
21481 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21482 }
21483}
21484
21485fn moe_ffn_gpu(
21486 m: &MoeFfn,
21487 x: &[f32],
21488 idx: &[usize],
21489 p: &[f32],
21490 wsum: f32,
21491 pool: Option<&Pool>,
21492) -> Option<Vec<f32>> {
21493 use crate::gpu::MoeJob;
21494
21495 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21496 let mut model_ref = None;
21497 for &e in idx {
21498 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21499 moe_gpu_refused("push_job(expert)");
21500 return None;
21501 }
21502 }
21503 if let Some((se, gate)) = &m.shared {
21504 let g = gate.as_ref().map_or(1.0, |gate| {
21505 let mut gl = [0.0f32; 1];
21506 gate.matvec(x, &mut gl, pool);
21507 1.0 / (1.0 + (-gl[0]).exp())
21508 });
21509 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21510 moe_gpu_refused("push_job(shared)");
21511 return None;
21512 }
21513 }
21514 let Some(model) = model_ref else {
21515 moe_gpu_refused("no model_ref");
21516 return None;
21517 };
21518 let hidden = jobs[0].down.1;
21519 let mut out = vec![0.0f32; hidden];
21520 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21521 Some(out)
21522 } else {
21523 moe_gpu_refused("gpu::moe_block");
21524 None
21525 }
21526}
21527
21528fn ffn_forward(
21530 ffn: &FfnKind,
21531 x: &[f32],
21532 pool: Option<&Pool>,
21533 experts_allowed: Option<&[bool]>,
21534) -> Vec<f32> {
21535 match ffn {
21536 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21537 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21538 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21539 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21543 }
21544}
21545
21546fn ffn_forward_pair(
21550 ffn: &FfnKind,
21551 x1: &[f32],
21552 x2: &[f32],
21553 pool: Option<&Pool>,
21554 experts_allowed: Option<&[bool]>,
21555) -> (Vec<f32>, Vec<f32>) {
21556 let d = match ffn {
21557 FfnKind::Dense(d) if !d.segs.is_empty() => {
21560 return (
21561 tube_ffn(d, x1, 1, pool, None),
21562 tube_ffn(d, x2, 1, pool, None),
21563 );
21564 }
21565 FfnKind::Dense(d) => d,
21566 FfnKind::Moe(m) => {
21567 return (
21568 moe_ffn(m, x1, pool, experts_allowed),
21569 moe_ffn(m, x2, pool, experts_allowed),
21570 );
21571 }
21572 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21573 };
21574 let inter = d.gate_proj.rows();
21575 FFN_SCRATCH.with(|s| {
21576 let mut s = s.borrow_mut();
21577 let [g1, g2, u1, u2] = &mut *s;
21578 g1.resize(inter, 0.0);
21579 g2.resize(inter, 0.0);
21580 u1.resize(inter, 0.0);
21581 u2.resize(inter, 0.0);
21582 QTensor::matvec2_many(
21585 [&d.gate_proj, &d.up_proj],
21586 x1,
21587 x2,
21588 [g1.as_mut_slice(), u1.as_mut_slice()],
21589 [g2.as_mut_slice(), u2.as_mut_slice()],
21590 pool,
21591 );
21592 for i in 0..inter {
21593 g1[i] = d.act.combine(g1[i], u1[i]);
21594 g2[i] = d.act.combine(g2[i], u2[i]);
21595 }
21596 let mut o1 = attention::take_buf(d.down_proj.rows());
21597 let mut o2 = attention::take_buf(d.down_proj.rows());
21598 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21599 (o1, o2)
21600 })
21601}
21602
21603#[cfg(test)]
21604mod tests {
21605 use std::ffi::OsString;
21606 use std::sync::Mutex;
21607
21608 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21611
21612 struct ScopedTestEnv {
21613 previous: Vec<(&'static str, Option<OsString>)>,
21614 }
21615
21616 impl ScopedTestEnv {
21617 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21618 let previous = vars
21619 .iter()
21620 .map(|(name, _)| (*name, std::env::var_os(name)))
21621 .collect();
21622 for &(name, value) in vars {
21623 unsafe {
21624 match value {
21625 Some(value) => std::env::set_var(name, value),
21626 None => std::env::remove_var(name),
21627 }
21628 }
21629 }
21630 Self { previous }
21631 }
21632 }
21633
21634 impl Drop for ScopedTestEnv {
21635 fn drop(&mut self) {
21636 for (name, value) in self.previous.drain(..).rev() {
21637 unsafe {
21638 match value {
21639 Some(value) => std::env::set_var(name, value),
21640 None => std::env::remove_var(name),
21641 }
21642 }
21643 }
21644 }
21645 }
21646
21647 #[test]
21648 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21649 use super::pad_mtp_draft_logits;
21650
21651 let mut logits = vec![-3.0, -1.0, -2.0];
21652 pad_mtp_draft_logits(&mut logits, 6);
21653 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21654 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21655 assert_eq!(
21656 logits
21657 .iter()
21658 .enumerate()
21659 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21660 .map(|(i, _)| i),
21661 Some(1),
21662 "an unevaluated row cannot win an all-negative shortlist"
21663 );
21664 }
21665
21666 #[test]
21667 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21668 use super::{Pipeline, SpecMon, SpecTrial};
21669
21670 let mut mon = SpecMon::default();
21671 let start = std::time::Instant::now();
21672 mon.round(1.0, 1);
21673 let first = Pipeline::spec_trial_round(
21674 SpecTrial::Spec {
21675 t0: start,
21676 gen0: 0,
21677 rounds: 0,
21678 },
21679 &mut mon,
21680 1,
21681 );
21682 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21683
21684 mon.round(1.0, 1);
21685 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21686 assert!(
21687 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21688 "two verifier-tip-only rounds must stop the losing MTP trial"
21689 );
21690
21691 let mut viable = SpecMon::default();
21694 viable.round(1.0, 1);
21695 viable.round(1.0, 2);
21696 let continued = Pipeline::spec_trial_round(
21697 SpecTrial::Spec {
21698 t0: start,
21699 gen0: 0,
21700 rounds: 1,
21701 },
21702 &mut viable,
21703 3,
21704 );
21705 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21706 }
21707
21708 #[test]
21709 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21710 let mut p = spark_test_pipeline(None);
21711 let head_rows = 248_070;
21712 p.draft_full_streak = 7;
21713 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21714
21715 p.reset_fresh_mtp_draft_state();
21716 assert_eq!(
21717 p.draft_head_rows(head_rows),
21718 super::Pipeline::draft_vocab_rows(head_rows),
21719 "a fresh request returns to its configured shortlist"
21720 );
21721
21722 p.note_draft_id(u32::MAX);
21725 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21726 }
21727
21728 #[test]
21729 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21730 use super::graph_spec_wanted_by_policy;
21731
21732 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21736 assert!(!graph_spec_wanted_by_policy(
21737 Some("anything-but-1"),
21738 true,
21739 false,
21740 false,
21741 true,
21742 ));
21743 assert!(graph_spec_wanted_by_policy(
21744 Some("1"),
21745 false,
21746 true,
21747 false,
21748 true,
21749 ));
21750 assert!(!graph_spec_wanted_by_policy(
21751 Some("0"),
21752 true,
21753 false,
21754 false,
21755 true,
21756 ));
21757
21758 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21761 assert!(graph_spec_wanted_by_policy(
21762 Some("legacy-force"),
21763 false,
21764 true,
21765 true,
21766 false,
21767 ));
21768 }
21769
21770 #[test]
21771 fn encoder_hidden_states_are_normalized_and_reusable() {
21772 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21773 let ids = [1, 7, 11, 19];
21774
21775 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21776 let second = p
21777 .encode_hidden_states(&ids)
21778 .expect("reusable encoder forward");
21779 assert_eq!(first.len(), ids.len() * 8);
21780 assert!(first.iter().all(|v| v.is_finite()));
21781 assert_eq!(
21782 first, second,
21783 "encoder forward must start from a fresh state"
21784 );
21785
21786 assert!(p.forward_ids(&ids, None).is_ok());
21789 }
21790
21791 #[test]
21796 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21797 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21798 let dense_card = ChunkStackFacts {
21799 plain_dense: true,
21800 discrete: true,
21801 gpu_on: true,
21802 ..Default::default()
21803 };
21804 assert!(dense_card.dense_on_discrete());
21805 assert_eq!(
21807 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21808 DISCRETE_DENSE_PREFILL_CHUNK
21809 );
21810 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21811 for (label, facts) in [
21812 (
21813 "GDN hybrid / MoE / DeepSeek stack",
21814 ChunkStackFacts {
21815 plain_dense: false,
21816 ..dense_card
21817 },
21818 ),
21819 (
21820 "integrated GPU",
21821 ChunkStackFacts {
21822 discrete: false,
21823 ..dense_card
21824 },
21825 ),
21826 (
21827 "CPU only",
21828 ChunkStackFacts {
21829 gpu_on: false,
21830 discrete: false,
21831 ..dense_card
21832 },
21833 ),
21834 (
21835 "capacity split",
21836 ChunkStackFacts {
21837 capacity_split: true,
21838 ..dense_card
21839 },
21840 ),
21841 (
21842 "multi-GPU plan",
21843 ChunkStackFacts {
21844 multi_gpu: true,
21845 ..dense_card
21846 },
21847 ),
21848 (
21849 "O(1) layers",
21850 ChunkStackFacts {
21851 o1: true,
21852 ..dense_card
21853 },
21854 ),
21855 ] {
21856 assert!(!facts.dense_on_discrete(), "{label}");
21857 assert_eq!(
21858 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21859 48,
21860 "{label} keeps the historical x86 chunk"
21861 );
21862 }
21863 for dense in [false, true] {
21865 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21866 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21867 }
21868 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21870 for dense in [false, true] {
21871 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21872 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21873 }
21874 }
21875 }
21876
21877 #[test]
21878 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21879 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21880 let full = |host_rows, device_rows| ReuseLayer {
21881 full: true,
21882 host_rows,
21883 device_rows,
21884 device_state: false,
21885 };
21886 assert_eq!(
21889 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21890 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21891 );
21892 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21894 assert_eq!(
21896 kv_reuse_plan(339, &[full(339, Some(345))]),
21897 ReusePlan::Ready
21898 );
21899 assert_eq!(
21901 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21902 ReusePlan::Pull(vec![(0, 300, 339)])
21903 );
21904 assert_eq!(
21906 kv_reuse_plan(339, &[full(300, Some(320))]),
21907 ReusePlan::Fresh
21908 );
21909 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21910 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21911 let conv = |device_state| ReuseLayer {
21914 full: false,
21915 host_rows: 0,
21916 device_rows: None,
21917 device_state,
21918 };
21919 assert_eq!(
21920 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21921 ReusePlan::Fresh
21922 );
21923 assert_eq!(
21924 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21925 ReusePlan::Ready
21926 );
21927 }
21928
21929 #[test]
21930 fn nll_graph_policy_scopes_only_the_fused_head() {
21931 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21932 ("vulkan graph", true, true, false, true, false),
21934 ("native Metal graph", true, true, true, true, true),
21936 ("masked", false, true, false, false, false),
21938 ("graph disabled", true, false, true, false, false),
21939 ] {
21940 let (graph_quality, graph_head_required) =
21941 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21942 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21943 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21944 }
21945 }
21946
21947 #[test]
21948 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21949 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21950 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21951 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21952 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21953 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21954 }
21955
21956 #[test]
21957 fn cancel_flag_stops_generation() {
21958 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21959 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21962 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21963 assert_eq!(r.finish_reason, "cancelled");
21964 assert!(
21965 r.token_ids.is_empty(),
21966 "no tokens after cancel: {:?}",
21967 r.token_ids
21968 );
21969 assert_eq!(p.kv_cache.seq_len(), 0);
21970 assert!(p.kv_history.is_empty());
21971 assert!(!p.graph_want_logits);
21972 assert!(p.graph_logits.is_none());
21973 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21975 assert_ne!(r2.finish_reason, "cancelled");
21976 }
21977 use super::*;
21978
21979 #[test]
21980 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21981 let arch = |arch_name: &str, hidden_act: &str| {
21985 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21986 "arch_name": arch_name,
21987 "hidden_size": 8,
21988 "intermediate_size": 16,
21989 "num_layers": 1,
21990 "num_attention_heads": 1,
21991 "num_kv_heads": 1,
21992 "head_dim": 8,
21993 "vocab_size": 16,
21994 "layer_types": ["FullAttention"],
21995 "rms_norm_eps": 1e-6,
21996 "max_position_embeddings": 16,
21997 "hidden_act": hidden_act
21998 }))
21999 .expect("minimal architecture")
22000 };
22001 let minimax = arch("minimax_m3_vl", "swigluoai");
22002 assert_eq!(
22003 Act::from_arch_full(&minimax),
22004 Act::SwiGluOai {
22005 alpha: 1.702,
22006 limit: 7.0,
22007 }
22008 );
22009
22010 let unrelated = arch("qwen2", "swigluoai");
22013 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
22014
22015 let non_oai = arch("minimax_m3_vl", "gelu");
22019 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
22020 }
22021
22022 #[test]
22023 fn minimax_swiglu_oai_act_matches_scalar_contract() {
22024 let act = Act::swiglu_oai(1.702, 7.0);
22025 let got = act.combine(10.0, -10.0);
22026 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22027 assert!((got - expected).abs() < 1e-6);
22028 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22029 assert_eq!(
22030 act.wgpu_graph_act(),
22031 Some(crate::gpu::GraphAct::SwiGluOaiM3),
22032 "the WGPU-only arm accepts only the frozen M3 constants"
22033 );
22034 assert_eq!(
22035 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22036 None,
22037 "nearby OAI constants must not borrow MiniMax's kernel"
22038 );
22039 assert_eq!(
22040 act.moe_job_graph_contract(),
22041 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22042 "the direct MoE path carries the frozen clamp with the marker"
22043 );
22044 assert_eq!(
22045 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22046 None,
22047 "a nearby clamp must not borrow MiniMax's device contract"
22048 );
22049
22050 let gate_only = act.apply(-10.0);
22053 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22054 assert!((gate_only - expected_gate).abs() < 1e-10);
22055 }
22056
22057 #[test]
22058 fn explicit_sliding_schedule_selects_local_rope_profile() {
22059 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22064 p.swa = Some((1024, usize::MAX));
22065 p.sliding_layers = Some(vec![true, true, true, false]);
22066 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22067 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22068 p.rope_scale = 1.277_258_9;
22069 p.rope_scale_local = 1.0;
22070
22071 for li in 0..3 {
22072 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22073 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
22074 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
22075 }
22076 assert_eq!(p.layer_window(3), None);
22077 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
22078 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
22079 }
22080
22081 #[test]
22089 fn dynamic_ffn_equals_the_zeroing_arm() {
22090 let (hidden, inter) = (8usize, 32usize);
22091 let synth = |n: usize, salt: usize| -> Vec<f32> {
22092 (0..n)
22093 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
22094 .collect()
22095 };
22096 let down = synth(hidden * inter, 3);
22097 let mut down_t = vec![0.0f32; inter * hidden];
22098 for r in 0..hidden {
22099 for c in 0..inter {
22100 down_t[c * hidden + r] = down[r * inter + c];
22101 }
22102 }
22103 let d = DenseFfn {
22104 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22105 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22106 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
22107 act: Act::Silu,
22108 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
22109 segs: Vec::new(),
22110 };
22111 let x = synth(hidden, 11);
22112 let k = 12usize;
22113 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
22114 let mut g = vec![0.0f32; inter];
22116 d.gate_proj.matvec(&x, &mut g, None);
22117 let mut u = vec![0.0f32; inter];
22118 d.up_proj.matvec(&x, &mut u, None);
22119 for v in g.iter_mut() {
22120 *v = inference::silu(*v);
22121 }
22122 keep_top_k(&mut g, k);
22123 for i in 0..inter {
22124 g[i] *= u[i];
22125 }
22126 let mut want = vec![0.0f32; hidden];
22127 d.down_proj.matvec(&g, &mut want, None);
22128 for (a, b) in want.iter().zip(&got) {
22129 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22130 }
22131 }
22132
22133 #[test]
22139 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22140 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22141 let inter = core + tube;
22142 let synth = |n: usize, salt: usize| -> Vec<f32> {
22143 (0..n)
22144 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22145 .collect()
22146 };
22147 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22148 let d_all = synth(hidden * inter, 3);
22149 let dense = DenseFfn {
22151 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22152 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22153 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22154 act: Act::Silu,
22155 down_t: None,
22156 segs: Vec::new(),
22157 };
22158 let rows =
22159 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22160 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22161 let mut o = Vec::with_capacity(hidden * (b - a));
22162 for r in 0..hidden {
22163 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22164 }
22165 o
22166 };
22167 let tubed = DenseFfn {
22168 down_t: None,
22169 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22170 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22171 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22172 act: Act::Silu,
22173 segs: vec![FfnSeg {
22174 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22175 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22176 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22177 start: core,
22178 width: tube,
22179 }],
22180 };
22181 let x = synth(hidden, 7);
22182 let want = dense_ffn(&dense, &x, None);
22183 let got = tube_ffn(&tubed, &x, 1, None, None);
22184 for (a, b) in want.iter().zip(&got) {
22185 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22186 }
22187 let mut bits = vec![0u8; inter.div_ceil(8)];
22189 for n in 0..core {
22190 bits[n / 8] |= 1 << (n % 8);
22191 }
22192 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22193 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22194 for (a, b) in masked.iter().zip(&closed) {
22195 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22196 }
22197 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22199 for (a, b) in closed.iter().zip(&batch) {
22200 assert_eq!(a, b, "batch arm disagrees with decode arm");
22201 }
22202 }
22203
22204 #[test]
22206 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22207 let (hidden, inter) = (16usize, 40usize);
22208 let synth = |n: usize, salt: usize| -> Vec<f32> {
22209 (0..n)
22210 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22211 .collect()
22212 };
22213 let d = DenseFfn {
22214 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22215 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22216 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22217 act: Act::Silu,
22218 down_t: None,
22219 segs: Vec::new(),
22220 };
22221 let x = synth(hidden, 9);
22222 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22224
22225 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22226
22227 let mut g = vec![0.0f32; inter];
22229 d.gate_proj.matvec(&x, &mut g, None);
22230 let mut u = vec![0.0f32; inter];
22231 d.up_proj.matvec(&x, &mut u, None);
22232 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22233 for i in 0..inter {
22234 g[i] = if act_set.contains(&(i as u16)) {
22235 inference::silu(g[i]) * u[i]
22236 } else {
22237 0.0
22238 };
22239 }
22240 let mut reference = vec![0.0f32; hidden];
22241 d.down_proj.matvec(&g, &mut reference, None);
22242
22243 let max_d = sparse
22244 .iter()
22245 .zip(&reference)
22246 .map(|(a, b)| (a - b).abs())
22247 .fold(0.0f32, f32::max);
22248 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22249 }
22250
22251 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22254 let (h, inter, heads, kv, hd) = (
22255 p.hidden_size,
22256 inter,
22257 p.num_heads,
22258 p.num_kv_heads,
22259 p.head_dim,
22260 );
22261 let synth = |n: usize, salt: usize| -> Vec<f32> {
22262 (0..n)
22263 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22264 .collect()
22265 };
22266 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22267 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22268 };
22269 p.mtp = Some(MtpModule {
22270 enorm: vec![1.0; h],
22271 hnorm: vec![1.0; h],
22272 eh_proj: qt(h, 2 * h, 301),
22273 layer: LayerWeights {
22274 input_norm: vec![1.0; h],
22275 post_norm: vec![1.0; h],
22276 attn_out_norm: None,
22277 ffn_out_norm: None,
22278 layer_scale: None,
22279 ffn: FfnKind::Dense(DenseFfn {
22280 gate_proj: qt(inter, h, 315),
22281 up_proj: qt(inter, h, 316),
22282 down_proj: qt(h, inter, 317),
22283 act: Act::Silu,
22284 down_t: None,
22285 segs: Vec::new(),
22286 }),
22287 attn: AttnKind::Full {
22288 bias: None,
22289 wq: qt(heads * hd, h, 311),
22290 wk: qt(kv * hd, h, 312),
22291 wv: qt(kv * hd, h, 313),
22292 wo: qt(h, heads * hd, 314),
22293 q_norm: None,
22294 k_norm: None,
22295 output_gate: false,
22296 softplus_gate: None,
22297 },
22298 },
22299 final_norm: vec![1.0; h],
22300 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22301 });
22302 }
22303
22304 fn attach_test_mtp(p: &mut Pipeline) {
22306 attach_test_mtp_width(p, p.intermediate_size);
22307 }
22308
22309 #[test]
22310 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22311 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22312 attach_test_mtp_width(&mut p, 11);
22315 let ids = [3, 9, 4, 12, 7, 2];
22316 let trunk = p.probe_ffn_mass(&ids);
22317 assert_eq!(trunk[0].len(), p.intermediate_size);
22318
22319 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22320 assert_eq!(got.len(), 1);
22321 assert_eq!(got[0].mass.len(), 11);
22322 assert_eq!(got[0].squared_mass.len(), 11);
22323 assert_eq!(got[0].observed_positions, ids.len() - 1);
22324 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22325 assert!(got[0]
22326 .mass
22327 .iter()
22328 .zip(&got[0].squared_mass)
22329 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22330 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22331 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22332
22333 let h = p.hidden_size;
22337 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22338 panic!("synthetic MTP must be dense")
22339 };
22340 dense.segs.push(FfnSeg {
22341 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22342 up: QTensor::from_f32(vec![0.0; h], 1, h),
22343 down: QTensor::from_f32(vec![0.0; h], h, 1),
22344 start: dense.gate_proj.rows(),
22345 width: 1,
22346 });
22347 let err = p.probe_mtp_ffn(&ids);
22348 assert!(err
22349 .unwrap_err()
22350 .contains("tube-segmented draft FFNs are not supported"));
22351 assert_eq!(p.kv_cache.seq_len(), 0);
22352 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22353 }
22354
22355 #[test]
22356 fn speculative_equals_vanilla_greedy() {
22357 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22358 let _env = ScopedTestEnv::set(&[
22361 ("CMF_GPU", Some("0")),
22362 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22363 ("CMF_MTP_PAIR", Some("1")),
22364 ]);
22365 let run = |spec: bool| {
22366 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22367 p.sampler_config.temperature = 0.0;
22368 attach_test_mtp(&mut p);
22369 p.speculative = spec;
22370 let r = p.generate("abcdef", 12, None, None).unwrap();
22371 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22372 };
22373 let (vanilla, d0, _) = run(false);
22374 let (spec, d1, a1) = run(true);
22375 assert_eq!(d0, 0, "vanilla path must not draft");
22376 assert!(d1 > 0, "speculative path must draft");
22377 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22378 assert_eq!(
22379 vanilla, spec,
22380 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22381 );
22382 }
22383
22384 #[test]
22385 fn speculative_accepts_constant_oracle() {
22386 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22387 let _env = ScopedTestEnv::set(&[
22388 ("CMF_GPU", Some("0")),
22389 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22390 ("CMF_MTP_PAIR", Some("1")),
22391 ]);
22392 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22393 p.sampler_config.temperature = 0.0;
22394 p.sampler_config.repetition_penalty = 1.0;
22395 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22398 attach_test_mtp(&mut p);
22399 p.speculative = true;
22400 let r = p.generate("abcd", 10, None, None).unwrap();
22401 assert!(r.mtp_drafted > 0);
22402 assert_eq!(
22403 r.mtp_accepted, r.mtp_drafted,
22404 "constant logits → every draft accepted"
22405 );
22406 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22409 }
22410
22411 #[test]
22412 fn host_fused_mtp_pair_is_opt_in() {
22413 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22414 let _env = ScopedTestEnv::set(&[
22415 ("CMF_GPU", Some("0")),
22416 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22417 ("CMF_MTP_PAIR", None),
22418 ]);
22419 let run = |spec: bool| {
22420 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22421 p.sampler_config.temperature = 0.0;
22422 attach_test_mtp(&mut p);
22423 p.speculative = spec;
22424 let r = p.generate("abcdef", 12, None, None).unwrap();
22425 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22426 };
22427 let (plain, d0, _) = run(false);
22428 let (guarded, d1, a1) = run(true);
22429 assert_eq!(d0, 0);
22430 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22431 assert_eq!(a1, 0);
22432 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22433 }
22434
22435 #[test]
22436 fn empty_prompt_is_an_error_not_a_panic() {
22437 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22438 let r = p.generate("", 4, None, None);
22439 assert!(r.is_err(), "empty prompt must be a clean error");
22440 }
22441
22442 #[test]
22443 fn every_token_enters_kv_exactly_once() {
22444 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22445 p.sampler_config.temperature = 0.0;
22447 let r = p.generate("abc", 2, None, None).unwrap();
22448 assert_eq!(r.prompt_tokens, 3);
22449 assert_eq!(
22453 p.kv_cache.seq_len(),
22454 3 + r.tokens_generated - 1,
22455 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22456 );
22457 }
22458
22459 #[test]
22460 fn generation_is_reproducible_with_seed() {
22461 let run = || {
22462 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22463 p.generate("hello", 8, None, None).unwrap().token_ids
22464 };
22465 assert_eq!(run(), run());
22466 }
22467
22468 #[test]
22469 fn resetting_sampler_restarts_the_seeded_stream() {
22470 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22471 let config = SamplerConfig {
22472 seed: Some(1234),
22473 ..SamplerConfig::default()
22474 };
22475 p.set_sampler_config(config.clone());
22476 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22477 p.set_sampler_config(config);
22478 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22479 assert_eq!(first, second);
22480 }
22481
22482 #[test]
22483 fn eviction_bounds_the_cache() {
22484 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22485 p.kv_cache.max_seq_len = 6;
22486 p.sampler_config.temperature = 0.0;
22487 let _ = p.generate("abcd", 12, None, None).unwrap();
22488 assert!(
22489 p.kv_cache.seq_len() <= 6 + 1,
22490 "cache must stay bounded by max_seq_len (got {})",
22491 p.kv_cache.seq_len()
22492 );
22493 }
22494
22495 #[test]
22496 fn confidence_matches_tokens_and_is_a_probability() {
22497 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22498 p.sampler_config.temperature = 0.0;
22499 p.sampler_config.repetition_penalty = 1.0;
22500 let r = p.generate("abcd", 10, None, None).unwrap();
22501 assert_eq!(
22502 r.token_confidence.len(),
22503 r.token_ids.len(),
22504 "one confidence per emitted token"
22505 );
22506 for &c in &r.token_confidence {
22507 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22508 }
22509 let logits = [1.0f32, 3.0, 0.5, 3.0];
22511 let p0 = top1_prob_t(&logits, 1, 1.0);
22512 let p1 = top1_prob_t(&logits, 3, 1.0);
22513 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22514 assert!(p0 > 0.0 && p0 < 1.0);
22515 let sharp = top1_prob_t(&logits, 1, 1.0);
22517 let soft = top1_prob_t(&logits, 1, 2.0);
22518 assert!(soft < sharp, "higher temperature lowers peak confidence");
22519 }
22520
22521 #[test]
22522 fn trace_is_opt_in_and_parallels_the_output() {
22523 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22525 p.sampler_config.temperature = 0.0;
22526 p.sampler_config.repetition_penalty = 1.0;
22527 let r = p.generate("abcd", 10, None, None).unwrap();
22528 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22529
22530 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22532 p.sampler_config.temperature = 0.0;
22533 p.sampler_config.repetition_penalty = 1.0;
22534 p.set_trace(true);
22535 let r = p.generate("abcd", 10, None, None).unwrap();
22536 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22537 for (i, tr) in r.traces.iter().enumerate() {
22538 assert_eq!(tr.t, i, "trace index is sequential");
22539 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22540 assert_eq!(
22541 tr.confidence, r.token_confidence[i],
22542 "trace confidence matches the confidence channel"
22543 );
22544 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22546 }
22547 }
22548
22549 #[test]
22550 fn explain_prefill_logits_match_greedy_first_token() {
22551 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22555 p.sampler_config.temperature = 0.0;
22556 p.sampler_config.repetition_penalty = 1.0;
22557 let ids = p.tokenizer.encode("abcd");
22558 let logits = p.prefill_next_logits(&ids, None);
22559 let argmax = logits
22560 .iter()
22561 .enumerate()
22562 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22563 .unwrap()
22564 .0 as u32;
22565 let r = p.generate("abcd", 1, None, None).unwrap();
22566 assert_eq!(
22567 argmax, r.token_ids[0],
22568 "explain preview must match greedy emit"
22569 );
22570 }
22571
22572 #[test]
22573 fn laguna_shared_expert_is_unconditionally_added() {
22574 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22575 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22576 let zero_dense = || DenseFfn {
22577 gate_proj: matrix(vec![0.0; 4]),
22578 up_proj: matrix(vec![0.0; 4]),
22579 down_proj: matrix(vec![0.0; 4]),
22580 act: Act::Silu,
22581 down_t: None,
22582 segs: Vec::new(),
22583 };
22584 let shared = DenseFfn {
22585 gate_proj: identity(),
22586 up_proj: identity(),
22587 down_proj: identity(),
22588 act: Act::Silu,
22589 down_t: None,
22590 segs: Vec::new(),
22591 };
22592 let x = [1.0, 2.0];
22593 let expected = dense_ffn(&shared, &x, None);
22594 let moe = MoeFfn {
22595 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22596 experts: vec![zero_dense()],
22597 top_k: 1,
22598 norm_topk_prob: true,
22599 router_sigmoid: true,
22600 sigmoid_denom_eps: 1e-6,
22601 expert_bias: None,
22602 routed_scaling: 1.0,
22603 route_tau: None,
22604 shared: Some((shared, None)),
22605 stats: std::cell::RefCell::new(Vec::new()),
22606 act_sq: std::cell::RefCell::new(Vec::new()),
22607 act_rows: std::cell::RefCell::new(Vec::new()),
22608 mask: None,
22609 per_expert_scale: None,
22610 router_input_norm: false,
22611 resonance: None,
22612 grown: Vec::new(),
22613 };
22614 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22615 for (actual, expected) in actual.iter().zip(expected) {
22616 assert!((actual - expected).abs() < 1e-6);
22617 }
22618 }
22619
22620 fn mimo_test_pipeline() -> Pipeline {
22629 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22630 let kvh = [1usize, 2, 2, 1];
22631 let synth = |n: usize, salt: usize| -> Vec<f32> {
22632 (0..n)
22633 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22634 .collect()
22635 };
22636 let qt = |rows: usize, cols: usize, salt: usize| {
22637 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22638 };
22639 let dense = |inter: usize, salt: usize| DenseFfn {
22640 gate_proj: qt(inter, hs, salt),
22641 up_proj: qt(inter, hs, salt + 1),
22642 down_proj: qt(hs, inter, salt + 2),
22643 act: Act::Silu,
22644 down_t: None,
22645 segs: Vec::new(),
22646 };
22647 let layers: Vec<LayerWeights> = (0..4)
22648 .map(|li| LayerWeights {
22649 input_norm: vec![1.0; hs],
22650 post_norm: vec![1.0; hs],
22651 attn_out_norm: None,
22652 ffn_out_norm: None,
22653 layer_scale: None,
22654 ffn: if li == 0 {
22655 FfnKind::Dense(dense(inter, 50))
22656 } else {
22657 FfnKind::Moe(MoeFfn {
22658 router: qt(4, hs, 60 + li),
22659 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22660 top_k: 2,
22661 norm_topk_prob: true,
22662 router_sigmoid: true,
22663 sigmoid_denom_eps: 1e-6,
22664 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22665 routed_scaling: 1.0,
22666 route_tau: None,
22667 shared: None,
22668 stats: std::cell::RefCell::new(Vec::new()),
22669 act_sq: std::cell::RefCell::new(Vec::new()),
22670 act_rows: std::cell::RefCell::new(Vec::new()),
22671 mask: None,
22672 per_expert_scale: None,
22673 router_input_norm: false,
22674 resonance: None,
22675 grown: Vec::new(),
22676 })
22677 },
22678 attn: AttnKind::Full {
22679 wq: qt(nh * hd, hs, li * 10 + 1),
22680 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22681 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22682 wo: qt(hs, nh * vd, li * 10 + 4),
22683 q_norm: None,
22684 k_norm: None,
22685 output_gate: false,
22686 softplus_gate: None,
22687 bias: None,
22688 },
22689 })
22690 .collect();
22691 let mut p = Pipeline::new(
22692 Tokenizer::byte_level(),
22693 PipelineWeights {
22694 embed_tokens: qt(vocab, hs, 100),
22695 layers,
22696 lm_head: qt(vocab, hs, 200),
22697 final_norm: vec![1.0; hs],
22698 },
22699 hs,
22700 inter,
22701 nh,
22702 1, hd,
22704 4,
22705 4,
22706 false,
22707 vocab,
22708 1e-6,
22709 1e7,
22710 NormStyle::Qwen,
22711 4096,
22712 SamplerConfig {
22713 seed: Some(7),
22714 ..Default::default()
22715 },
22716 );
22717 p.layer_dump = None;
22719 p.set_rotary(4, 1e7);
22720 p.sliding_layers = Some(vec![false, true, true, false]);
22721 p.swa = Some((3, usize::MAX));
22722 p.rotary_dim_local = Some(4);
22723 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22724 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22725 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22726 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22727 p
22728 }
22729
22730 #[test]
22731 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22732 let mut p = mimo_test_pipeline();
22733 p.speculative = false;
22734 p.ignore_eos = true;
22735 p.sampler_config.temperature = 0.0;
22736 p.sampler_config.repetition_penalty = 1.0;
22737 let a = vec![3, 5, 7, 9, 11, 13];
22738 let b = vec![4, 8, 12, 16, 20, 24];
22739 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22740 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22741 let actual = p
22745 .generate_from_embeds(&a, &rows, 8, None, None)
22746 .unwrap()
22747 .token_ids;
22748 assert_eq!(actual, expected);
22749 assert!(p.kv_history.is_empty());
22750 let mut extended = a.clone();
22751 extended.push(17);
22752 let after_media = p
22753 .generate_from_ids(&extended, 8, None, None)
22754 .unwrap()
22755 .token_ids;
22756 p.reset_session();
22757 let fresh = p
22758 .generate_from_ids(&extended, 8, None, None)
22759 .unwrap()
22760 .token_ids;
22761 assert_eq!(after_media, fresh);
22762 assert!(
22763 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22764 .is_err()
22765 );
22766 p.reset_session();
22770 p.generate_from_ids(&a, 1, None, None).unwrap();
22771 let mut media_ids = p.kv_history.clone();
22772 assert!(!media_ids.is_empty());
22773 media_ids.extend_from_slice(&[19, 21, 23]);
22774 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22775 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22776 let mut oracle = mimo_test_pipeline();
22777 oracle.speculative = false;
22778 oracle.ignore_eos = true;
22779 oracle.sampler_config.temperature = 0.0;
22780 oracle.sampler_config.repetition_penalty = 1.0;
22781 let expected = oracle
22782 .generate_from_ids(&source_ids, 8, None, None)
22783 .unwrap()
22784 .token_ids;
22785 assert_eq!(
22786 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22787 .unwrap()
22788 .token_ids,
22789 expected
22790 );
22791 assert!(p.kv_history.is_empty());
22792 let mut bad = rows;
22793 bad[0] = f32::NAN;
22794 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22795 }
22796
22797 fn f32_bits(v: &[f32]) -> Vec<u32> {
22798 v.iter().map(|x| x.to_bits()).collect()
22799 }
22800
22801 #[test]
22808 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22809 let mut p = mimo_test_pipeline();
22810 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22811 assert_eq!(kv, vec![1, 2, 2, 1]);
22812 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22813 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22814 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22815 let hs = p.hidden_size;
22816 let mut decode = Vec::new();
22817 for (pos, &id) in ids.iter().enumerate() {
22818 let e = p.embed_single(id);
22819 let h = p.forward_layers(&e, pos, None);
22820 decode.push(p.logits_from_hidden(&h));
22821 }
22822 for l in &p.kv_cache.layers {
22823 assert_eq!(l.seq_len, 12);
22824 assert_eq!(l.head_values(0).len(), 12 * 8);
22826 }
22827 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22828
22829 p.clear_sequence_state();
22830 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22831 for pos in 0..ids.len() {
22832 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22833 assert_eq!(
22834 f32_bits(&decode[pos]),
22835 f32_bits(&lg),
22836 "whole prompt, pos {pos}"
22837 );
22838 }
22839
22840 p.clear_sequence_state();
22841 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22842 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22843 for pos in 0..ids.len() {
22844 let row = if pos < 5 {
22845 &a[pos * hs..(pos + 1) * hs]
22846 } else {
22847 &b[(pos - 5) * hs..(pos - 4) * hs]
22848 };
22849 let lg = p.logits_from_hidden(row);
22850 assert_eq!(
22851 f32_bits(&decode[pos]),
22852 f32_bits(&lg),
22853 "two chunks, pos {pos}"
22854 );
22855 }
22856
22857 let last = |p: &mut Pipeline| {
22860 p.clear_sequence_state();
22861 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22862 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22863 };
22864 let base = last(&mut p);
22865 let mut no_sinks = mimo_test_pipeline();
22866 for l in &mut no_sinks.kv_cache.layers {
22867 l.sinks = None;
22868 }
22869 assert_ne!(
22870 f32_bits(&last(&mut no_sinks)),
22871 f32_bits(&base),
22872 "sinks are live"
22873 );
22874 let mut wide = mimo_test_pipeline();
22875 wide.swa = Some((64, usize::MAX));
22876 assert_ne!(
22877 f32_bits(&last(&mut wide)),
22878 f32_bits(&base),
22879 "window is live"
22880 );
22881
22882 p.clear_sequence_state();
22884 p.ignore_eos = true;
22885 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22886 assert_eq!(r.token_ids.len(), 4);
22887 }
22888
22889 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22892 let (hs, nh) = (16usize, 4usize);
22893 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22894 p.layer_dump = None;
22895 p.swa = Some((6, 4));
22896 p.proj_gate_sigmoid = true;
22897 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22898 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22899 let g: Vec<f32> = (0..nh * hs)
22900 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22901 .collect();
22902 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22903 }
22904 }
22905 p.swa_trim = trim;
22906 p
22907 }
22908
22909 #[test]
22915 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22916 let mut a = spark_test_pipeline(None);
22917 let mut b = spark_test_pipeline(Some((2, 4)));
22918 assert_eq!(
22919 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22920 vec![Some(6), Some(6), Some(6), None]
22921 );
22922 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22923 let mut pos = 0usize;
22924 for &n in [5usize, 13, 7, 16].iter().cycle() {
22925 if pos >= ids.len() {
22926 break;
22927 }
22928 let end = (pos + n).min(ids.len());
22929 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22930 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22931 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22932 pos = end;
22933 }
22934 for _ in 0..23 {
22935 let e = a.embed_single(((pos * 7) % 64) as u32);
22936 let ha = a.forward_layers(&e, pos, None);
22937 let hb = b.forward_layers(&e, pos, None);
22938 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22939 for li in 0..3 {
22940 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22941 }
22942 pos += 1;
22943 }
22944 for round in 0..9 {
22945 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22946 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22947 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22948 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22949 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22950 if round % 2 == 1 {
22951 for p in [&mut a, &mut b] {
22953 for l in &mut p.kv_cache.layers {
22954 l.truncate_last(2);
22955 }
22956 }
22957 } else {
22958 pos += 2;
22959 }
22960 }
22961 let e = a.embed_single(9);
22962 let (ha, hb) = (
22963 a.forward_layers(&e, pos, None),
22964 b.forward_layers(&e, pos, None),
22965 );
22966 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22967 for li in 0..3 {
22968 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22969 assert!(lb.base() > 0, "layer {li} trimmed");
22970 assert_eq!(la.base(), 0);
22971 assert_eq!(lb.pos_len(), la.seq_len);
22972 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22973 }
22974 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22975 assert_eq!(
22976 (gb.base(), gb.seq_len),
22977 (0, ga.seq_len),
22978 "the global layer keeps all"
22979 );
22980 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22981 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22982 }
22983
22984 #[test]
22989 fn swa_trim_wire_handoff_continues_bitwise() {
22990 let mut a = spark_test_pipeline(None);
22991 let mut b = spark_test_pipeline(Some((2, 4)));
22992 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22993 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22994 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22995 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22996 let mut pos = ids.len();
22997 for _ in 0..5 {
22998 let e = a.embed_single(((pos * 5) % 64) as u32);
22999 assert_eq!(
23000 f32_bits(&a.forward_layers(&e, pos, None)),
23001 f32_bits(&b.forward_layers(&e, pos, None))
23002 );
23003 pos += 1;
23004 }
23005 let mut c = spark_test_pipeline(Some((2, 4)));
23006 for li in 0..4 {
23007 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
23008 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
23009 }
23010 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
23011 assert_eq!(c.kv_cache.seq_len(), pos);
23012 for step in 0..17 {
23013 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
23014 let ha = a.forward_layers(&e, pos, None);
23015 let hc = c.forward_layers(&e, pos, None);
23016 assert_eq!(
23017 f32_bits(&ha),
23018 f32_bits(&hc),
23019 "after the hand-off, step {step}"
23020 );
23021 pos += 1;
23022 }
23023 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23024 }
23025
23026 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23031 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23032 let synth = |len: usize, salt: usize| -> Vec<f32> {
23033 (0..len)
23034 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23035 .collect()
23036 };
23037 let qt = |rows: usize, cols: usize, salt: usize| {
23038 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23039 };
23040 let layers = widths
23041 .iter()
23042 .copied()
23043 .enumerate()
23044 .map(|(k, inter)| {
23045 let s = 500 + k * 40;
23046 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23047 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23048 MtpModule {
23049 enorm: vec![1.0; hs],
23050 hnorm: vec![1.0; hs],
23051 eh_proj: qt(hs, 2 * hs, s),
23052 layer: LayerWeights {
23053 input_norm: vec![1.0; hs],
23054 post_norm: vec![1.0; hs],
23055 attn_out_norm: None,
23056 ffn_out_norm: None,
23057 layer_scale: None,
23058 attn: AttnKind::Full {
23059 wq: qt(nh * hd, hs, s + 1),
23060 wk: qt(nkv * hd, hs, s + 2),
23061 wv: qt(nkv * vd, hs, s + 3),
23062 wo: qt(hs, nh * vd, s + 4),
23063 q_norm: None,
23064 k_norm: None,
23065 output_gate: false,
23066 softplus_gate: None,
23067 bias: None,
23068 },
23069 ffn: FfnKind::Dense(DenseFfn {
23070 gate_proj: qt(inter, hs, s + 5),
23071 up_proj: qt(inter, hs, s + 6),
23072 down_proj: qt(hs, inter, s + 7),
23073 act: Act::Silu,
23074 down_t: None,
23075 segs: Vec::new(),
23076 }),
23077 },
23078 final_norm: vec![1.0; hs],
23079 kv,
23080 }
23081 })
23082 .collect();
23083 mimo_mtp::MimoMtp::from_layers(layers)
23084 }
23085
23086 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
23087 mimo_test_mtp_widths(&vec![24; n], gain)
23088 }
23089
23090 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
23091 p.clear_sequence_state();
23092 p.speculative = spec;
23093 p.ignore_eos = true;
23094 p.sampler_config.temperature = 0.0;
23095 p.generate_from_ids(ids, n, None, None).unwrap()
23096 }
23097
23098 #[test]
23099 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
23100 let mut p = mimo_test_pipeline();
23101 let widths = [7usize, 13, 29];
23102 assert_ne!(widths[0], p.intermediate_size);
23103 assert_ne!(widths[1], p.intermediate_size);
23104 assert_ne!(widths[2], p.intermediate_size);
23105 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
23106 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
23107
23108 let trunk = p.probe_ffn_mass(&ids);
23109 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
23110 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
23111 assert_eq!(got.len(), widths.len());
23112 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
23113 assert_eq!(stat.mass.len(), width, "draft layer {k}");
23114 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
23115 assert_eq!(stat.observed_positions, ids.len() - k - 1);
23116 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
23117 assert!(stat
23118 .mass
23119 .iter()
23120 .zip(&stat.squared_mass)
23121 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23122 }
23123 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23124 let st = p.mimo_mtp.as_ref().unwrap();
23125 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23126
23127 let h = p.hidden_size;
23130 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23131 panic!("synthetic MiMo MTP must be dense")
23132 };
23133 dense.segs.push(FfnSeg {
23134 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23135 up: QTensor::from_f32(vec![0.0; h], 1, h),
23136 down: QTensor::from_f32(vec![0.0; h], h, 1),
23137 start: dense.gate_proj.rows(),
23138 width: 1,
23139 });
23140 assert!(p
23141 .probe_mtp_ffn(&ids)
23142 .unwrap_err()
23143 .contains("tube-segmented draft FFNs are not supported"));
23144 assert_eq!(p.kv_cache.seq_len(), 0);
23145 assert!(p
23146 .mimo_mtp
23147 .as_ref()
23148 .unwrap()
23149 .layers
23150 .iter()
23151 .all(|m| m.kv.seq_len == 0));
23152 }
23153
23154 #[test]
23155 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23156 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23157 p.clear_sequence_state();
23158 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23159 let _exact = ExactMtpCalibrationGuard::enter();
23160 let got = crate::gpu::cpu_scope(|| {
23161 let trunk = p.probe_trunk_rows(ids)?;
23162 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
23163 })
23164 .expect("teacher-forced MiMo probe");
23165 st.reset();
23166 p.mimo_mtp = Some(st);
23167 p.reset_mtp_probe_state();
23168 got
23169 }
23170
23171 for post_norm_hidden in [false, true] {
23172 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23173 let make = || {
23174 let mut p = mimo_test_pipeline();
23175 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23176 st.post_norm_hidden = post_norm_hidden;
23177 p.mimo_mtp = Some(st);
23178 p
23179 };
23180 let mut full = make();
23181 let expected = collect(&mut full, &ids, usize::MAX);
23182 for rows in [1usize, 7, 128] {
23183 let mut chunked = make();
23184 let got = collect(&mut chunked, &ids, rows);
23185 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23186 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23187 assert_eq!(
23188 got.observed_positions, expected.observed_positions,
23189 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23190 );
23191 assert_eq!(
23192 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23193 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23194 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23195 );
23196 assert_eq!(
23197 got.squared_mass
23198 .iter()
23199 .map(|v| v.to_bits())
23200 .collect::<Vec<_>>(),
23201 expected
23202 .squared_mass
23203 .iter()
23204 .map(|v| v.to_bits())
23205 .collect::<Vec<_>>(),
23206 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23207 );
23208 }
23209 }
23210 }
23211 }
23212
23213 #[test]
23219 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23220 for post in [false, true] {
23223 let mut p = mimo_test_pipeline();
23224 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23226 let mut st0 = mimo_test_mtp(3, 1.0);
23227 st0.post_norm_hidden = post;
23228 p.mimo_mtp = Some(st0);
23229 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23230 let hs = p.hidden_size;
23231 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23232 p.mimo_note_rows(&hb, 0);
23233 let mut st = p.mimo_mtp.take().unwrap();
23234 let k = 3;
23237 let mut inc = Vec::new();
23238 for t in 0..ids.len() - k - 1 {
23239 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23240 }
23241 let s = ids.len();
23244 let mut reference = vec![vec![0u32; k]; s - k - 1];
23245 let mut fresh = mimo_test_mtp(3, 1.0);
23246 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23247 let n = s - layer - 1;
23248 let mut cats = vec![0.0f32; n * 2 * hs];
23249 for j in 0..n {
23250 let e = p.embed_single(ids[j + layer + 1]);
23251 let raw = &hb[j * hs..(j + 1) * hs];
23252 let g = if post {
23253 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23254 } else {
23255 raw.to_vec()
23256 };
23257 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23258 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23259 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23260 }
23261 let mut x = vec![0.0f32; n * hs];
23262 m.eh_proj.matmat(&cats, n, &mut x, None);
23263 p.mimo_mtp_block(m, &mut x, n, 0);
23264 for (t, row) in reference.iter_mut().enumerate() {
23265 let y = inference::rms_norm(
23266 &x[t * hs..(t + 1) * hs],
23267 &m.final_norm,
23268 p.rms_eps,
23269 p.norm_style,
23270 );
23271 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23272 }
23273 }
23274 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23275 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23277 assert!(distinct.len() > 3, "{inc:?}");
23278 let last_t = ids.len() - k - 2;
23280 for m in &st.layers {
23281 assert_eq!(m.kv.seq_len, last_t + 1);
23282 }
23283 }
23284 }
23285
23286 #[test]
23293 fn mimo_speculative_greedy_equals_plain_greedy() {
23294 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23295 let _env = ScopedTestEnv::set(&[
23296 ("CMF_GPU", Some("0")),
23297 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23298 ]);
23299 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23300 let n = 24;
23301 let mut p = mimo_test_pipeline();
23302 let plain = mimo_greedy(&mut p, &ids, n, false);
23303 assert_eq!(plain.mtp_drafted, 0);
23304 assert_eq!(plain.token_ids.len(), n);
23305 let plain_kv = p.kv_cache.layers[0].seq_len;
23306
23307 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23309 let spec = mimo_greedy(&mut p, &ids, n, true);
23310 assert!(spec.mtp_drafted > 0, "the round must draft");
23311 assert_eq!(spec.token_ids, plain.token_ids);
23312 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23313
23314 let mut truth: Vec<u32> = ids.clone();
23317 truth.extend(&plain.token_ids);
23318 let mut noisy = truth.clone();
23319 for (i, t) in noisy.iter_mut().enumerate() {
23320 if i % 5 == 0 {
23321 *t = (*t + 1) % 64;
23322 }
23323 }
23324 let mut st = mimo_test_mtp(3, 1.0);
23325 st.draft_override = Some(noisy);
23326 p.mimo_mtp = Some(st);
23327 let spec = mimo_greedy(&mut p, &ids, n, true);
23328 assert_eq!(spec.token_ids, plain.token_ids);
23329 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23330 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23331 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23332 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23333 assert!(
23334 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23335 "{:?}",
23336 stats.accept_hist
23337 );
23338 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23339
23340 let mut st = mimo_test_mtp(3, 1.0);
23343 st.draft_override = Some(truth);
23344 p.mimo_mtp = Some(st);
23345 let spec = mimo_greedy(&mut p, &ids, n, true);
23346 assert_eq!(spec.token_ids, plain.token_ids);
23347 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23348 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23349
23350 let off = mimo_greedy(&mut p, &ids, n, false);
23352 assert_eq!(off.token_ids, plain.token_ids);
23353 assert_eq!(off.mtp_drafted, 0);
23354 }
23355
23356 #[test]
23365 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23366 let p = mimo_test_pipeline();
23367 assert_eq!(
23368 p.graph_attn_decline_reason(),
23369 Some("per-layer KV head counts")
23370 );
23371 assert_eq!(p.wgpu_graph_attn_decline(), None);
23372 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23373 assert_eq!(
23374 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23375 (1, 4, 4, None, false)
23376 );
23377 assert_eq!(g0.invf, p.inv_freq.as_slice());
23378 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23379 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23380 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23381 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23382 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23383 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23384 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23385
23386 let emb = p.embed_single(3);
23389 let mut lg = Vec::new();
23390 assert!(
23391 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23392 .is_none()
23393 );
23394 let mut hid = emb.clone();
23395 assert_eq!(
23396 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23397 crate::gpu::BatchGraphOutcome::Declined
23398 );
23399 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23400 assert!(p.try_multi_burst(3, 0, 4).is_none());
23401 assert!(
23402 p.graph_declines().is_empty(),
23403 "no attention decline logged: {:?}",
23404 p.graph_declines()
23405 );
23406 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23411 assert_eq!(plain().graph_attn_decline_reason(), None);
23412 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23413 assert!(
23414 plain().graph_attn_geom(0).is_none(),
23415 "uniform models keep the historical arms"
23416 );
23417 let mut q = plain();
23418 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23419 assert_eq!(
23420 q.graph_attn_decline_reason(),
23421 Some("learned attention sinks")
23422 );
23423 assert_eq!(
23424 q.graph_attn_geom(1).unwrap().sink,
23425 Some(&[0.25f32, -0.25][..])
23426 );
23427 let mut q = plain();
23428 q.set_attn_geometry(None, Some(2)).unwrap();
23429 assert_eq!(
23430 q.graph_attn_decline_reason(),
23431 Some("V heads narrower than Q/K heads")
23432 );
23433 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23434 let mut q = plain();
23435 q.sliding_layers = Some(vec![true, false]);
23436 q.swa = Some((4, usize::MAX));
23437 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23438 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23439 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23440
23441 let mut q = mimo_test_pipeline();
23445 q.rope_scale = 2.0;
23446 q.rope_scale_local = 1.25;
23447 assert_eq!(q.wgpu_graph_attn_decline(), None);
23448 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23449 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23450
23451 let mut q = plain();
23454 q.rope_scale = 2.0;
23455 assert_eq!(
23456 q.wgpu_graph_attn_decline(),
23457 Some("scaled RoPE without per-layer geometry")
23458 );
23459 }
23460
23461 #[cfg(target_os = "macos")]
23467 #[test]
23468 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23469 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23473 p.swa = Some((16, usize::MAX));
23474 p.sliding_layers = Some(vec![true, true]);
23475 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23476 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23477
23478 assert_eq!(
23479 p.graph_attn_decline_reason(),
23480 Some("learned attention sinks")
23481 );
23482 let swa_graph = p.metal_graph_swa();
23483 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23484 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23485 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23486 assert!(
23487 native_sink_attend,
23488 "the q1 sink admission must follow the SWA door"
23489 );
23490 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23493 }
23494
23495 #[test]
23496 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23497 let mut p = mimo_test_pipeline();
23498 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23499 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23500 for _ in 0..if li == 0 { 2 } else { 12 } {
23501 layer.append(&row, &row, &[]);
23502 }
23503 }
23504 p.mimo_verify_rewind(9).unwrap();
23505 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23506 for layer in &p.kv_cache.layers[1..] {
23507 assert_eq!(layer.seq_len, 9);
23508 }
23509 }
23510
23511 #[test]
23515 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23516 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23517 let _ = std::fs::remove_dir_all(&dir);
23518 let mut p = mimo_test_pipeline();
23519 let hs = p.hidden_size;
23520 let ids = [5u32, 9, 11, 2, 40];
23521 p.layer_dump = Some(dir.join("decode"));
23522 for (pos, &id) in ids.iter().enumerate() {
23523 let e = p.embed_single(id);
23524 let _ = p.forward_layers(&e, pos, None);
23525 }
23526 p.clear_sequence_state();
23527 p.layer_dump = Some(dir.join("prefill"));
23528 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23529 for pos in 0..ids.len() {
23530 for li in 0..p.num_layers {
23531 let name = format!("p{pos:06}_l{li:02}.f32");
23532 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23533 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23534 assert_eq!(a.len(), hs * 4, "{name}");
23535 assert_eq!(a, b, "{name}");
23536 }
23537 }
23538 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23539 let vals: Vec<f32> = last
23540 .chunks(4)
23541 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23542 .collect();
23543 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23544 let _ = std::fs::remove_dir_all(&dir);
23545 }
23546
23547 #[test]
23548 fn attn_geometry_and_sinks_are_validated() {
23549 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23550 assert!(
23551 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23552 "one entry per layer"
23553 );
23554 assert!(
23555 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23556 "3 does not divide 4"
23557 );
23558 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23559 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23560 assert!(
23561 p.set_attn_geometry(None, Some(5)).is_err(),
23562 "V wider than the head"
23563 );
23564 p.set_attn_geometry(None, Some(4)).unwrap();
23565 assert_eq!(
23566 p.v_head_dim, None,
23567 "v_head_dim == head_dim is the uniform case"
23568 );
23569 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23570 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23571 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23572 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23573 assert!(
23574 p.kv_cache.layers[1].sinks.is_some(),
23575 "a reshape keeps the layer's sinks"
23576 );
23577 assert_eq!(p.layer_geom(1).0, 4);
23578 assert!(
23579 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23580 "one sink per Q head"
23581 );
23582 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23583 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23584 }
23585
23586 #[test]
23589 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23590 let cfg = || {
23591 Some(crate::nystrom::O1Cfg {
23592 layers: crate::nystrom::O1Layers::All,
23593 m: 4,
23594 w: 8,
23595 sink: 2,
23596 rect: crate::nystrom::O1Rect::Aggregate,
23597 })
23598 };
23599 let mut p = mimo_test_pipeline();
23600 p.set_o1(cfg());
23601 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23602 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23603 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23604 q.sliding_layers = Some(vec![false, false, true]);
23605 q.swa = Some((4, usize::MAX));
23606 q.set_o1(cfg());
23607 assert_eq!(q.o1_flags, vec![true, false, false]);
23608 }
23609
23610 #[test]
23611 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23612 const B: usize = 19;
23613 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23614 p.set_o1(Some(crate::nystrom::O1Cfg {
23615 layers: crate::nystrom::O1Layers::All,
23616 m: 4,
23617 w: 8,
23618 sink: 2,
23619 rect: crate::nystrom::O1Rect::Aggregate,
23620 }));
23621 p.o1_begin_with_prefix(Some(B));
23622 let ids: Vec<u32> = (0..B as u32).collect();
23623 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23624
23625 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23626 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23627 let next = p.embed_single(B as u32);
23628 let _ = p.forward_layers(&next, B, None);
23629 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23630 }
23631
23632 #[test]
23633 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23634 const B: usize = 19;
23635 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23636 let gdn_cfg = crate::linear_core::GdnCfg {
23640 num_v_heads: 2,
23641 num_k_heads: 1,
23642 key_head_dim: 2,
23643 value_head_dim: 4,
23644 conv_kernel: 3,
23645 hidden_size: 8,
23646 rms_eps: 1e-6,
23647 output_gate_sigmoid: false,
23648 };
23649 let synth = |n: usize, salt: usize| -> Vec<f32> {
23650 (0..n)
23651 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23652 .collect()
23653 };
23654 let qt = |rows: usize, cols: usize, salt: usize| {
23655 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23656 };
23657 let c_dim = gdn_cfg.conv_dim();
23658 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23659 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23660 in_proj_qkv: qt(c_dim, 8, 1),
23661 in_proj_z: qt(vd, 8, 2),
23662 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23663 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23664 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23665 a_log: vec![0.2, 0.5],
23666 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23667 norm: vec![1.0; gdn_cfg.value_head_dim],
23668 out_proj: qt(8, vd, 7),
23669 });
23670 p.gdn_cfg = Some(gdn_cfg);
23671 p.set_o1(Some(crate::nystrom::O1Cfg {
23672 layers: crate::nystrom::O1Layers::All,
23673 m: 4,
23674 w: 8,
23675 sink: 2,
23676 rect: crate::nystrom::O1Rect::Aggregate,
23677 }));
23678 p.o1_begin_with_prefix(Some(B));
23679 for pos in 0..B - 2 {
23680 let emb = p.embed_single(pos as u32);
23681 let _ = p.forward_layers(&emb, pos, None);
23682 }
23683 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23684
23685 let e1 = p.embed_single((B - 2) as u32);
23686 let e2 = p.embed_single((B - 1) as u32);
23687 let _ = p.forward_pair(&e1, &e2, B - 2);
23688
23689 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23690 assert!(
23691 p.kv_cache
23692 .layers
23693 .iter()
23694 .enumerate()
23695 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23696 );
23697 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23698 assert_ne!(
23699 p.kv_cache.layers[0].linear_state, lane1_state,
23700 "real pair must commit GDN lane 2 before returning"
23701 );
23702 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23703 let next = p.embed_single(B as u32);
23704 let _ = p.forward_layers(&next, B, None);
23705 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23706 }
23707
23708 #[test]
23709 fn o1_error_observation_stays_terminal_until_reset() {
23710 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23711 p.set_o1(Some(crate::nystrom::O1Cfg {
23712 layers: crate::nystrom::O1Layers::All,
23713 m: 4,
23714 w: 8,
23715 sink: 2,
23716 rect: crate::nystrom::O1Rect::Aggregate,
23717 }));
23718 p.o1_begin();
23719 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23720
23721 assert!(p.o1_seal_checked().is_err());
23722 assert!(
23723 p.o1_seal_checked().is_err(),
23724 "retry must see the sticky error"
23725 );
23726 let k = vec![0.2f32; 4];
23727 let v = vec![0.3f32; 4];
23728 p.kv_cache.layers[0].append(&k, &v, &[]);
23729 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23730
23731 p.reset_session();
23732 p.o1_begin();
23733 p.kv_cache.layers[0].append(&k, &v, &[]);
23734 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23735 }
23736
23737 #[test]
23738 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23739 let ids = vec![1u32, 2, 3, 4, 5, 6];
23740 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23741 p.graph_logits = Some(vec![123.0]);
23742 p.graph_want_logits = true;
23743 p.graph_failed
23744 .store(true, std::sync::atomic::Ordering::Relaxed);
23745 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23746 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23747 assert!(err.contains("before NLL"));
23748 assert!(p.graph_logits.is_none());
23749 assert!(!p.graph_want_logits);
23750 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23751 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23752
23753 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23754 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23755 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23756 assert_eq!(actual.1, expected.1);
23757 assert!((actual.0 - expected.0).abs() < 1e-9);
23758 }
23759
23760 #[test]
23761 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23762 let ids = vec![1u32, 2, 3, 4, 5, 6];
23763 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23764 p.nll_test_fail_at = Some(1);
23765 let err = p
23766 .nll_ids_from(&ids, 0)
23767 .expect_err("one-shot forward failure");
23768 assert!(err.contains("forward") || err.contains("score row"));
23769 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23770 assert!(!p.graph_want_logits);
23771 assert!(p.graph_logits.is_none());
23772 assert!(p.kv_history.is_empty());
23773
23774 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23775 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23776 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23777 assert_eq!(actual.1, expected.1);
23778 assert!((actual.0 - expected.0).abs() < 1e-9);
23779 }
23780
23781 #[test]
23782 fn nll_serial_failure_before_first_row_is_reported() {
23783 let ids = vec![1u32, 2, 3, 4];
23784 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23785 p.nll_test_force_serial = true;
23786 p.nll_test_fail_at = Some(0);
23787 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23788 assert!(err.contains("serial forward"));
23789 assert!(p.kv_history.is_empty());
23790 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23791 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23792 }
23793
23794 #[test]
23795 fn ffn_probe_failure_discards_recorder_and_state() {
23796 let ids = vec![1u32, 2, 3, 4];
23797 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23798 p.nll_test_fail_at = Some(0);
23799 let err = p
23800 .probe_ffn_mass_batch(&ids)
23801 .expect_err("probe forward failure");
23802 assert!(err.contains("NLL"));
23803 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23804 assert!(p.kv_history.is_empty());
23805 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23806 }
23807
23808 #[test]
23809 fn nll_test_controls_are_pipeline_scoped() {
23810 let ids = vec![1u32, 2, 3, 4];
23811 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23812 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23813 failing.nll_test_force_serial = true;
23814 failing.nll_test_fail_at = Some(0);
23815
23816 assert!(!failing.can_prefill_batched());
23817 assert!(unaffected.can_prefill_batched());
23818 let expected = unaffected
23819 .nll_ids_from(&ids, 0)
23820 .expect("unaffected pipeline remains usable");
23821 let err = failing
23822 .nll_ids_from(&ids, 0)
23823 .expect_err("failure injection belongs to failing pipeline");
23824 assert!(err.contains("serial forward"));
23825 assert!(failing.nll_test_fail_at.is_none());
23826 assert!(unaffected.can_prefill_batched());
23827 let actual = unaffected
23828 .nll_ids_from(&ids, 0)
23829 .expect("unaffected pipeline remains reusable");
23830 assert_eq!(actual.1, expected.1);
23831 assert!((actual.0 - expected.0).abs() < 1e-9);
23832 }
23833
23834 #[test]
23835 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23836 let ids = vec![1u32, 2, 3, 4, 5, 6];
23837 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23838 p.graph_logits = Some(vec![123.0]);
23839 p.graph_want_logits = true;
23840 p.graph_failed
23841 .store(true, std::sync::atomic::Ordering::Relaxed);
23842 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23843
23844 let err = p
23845 .forward_ids(&ids, None)
23846 .expect_err("a failed forward must not become a valid head result");
23847 assert!(err.contains("forward_ids setup"));
23848 assert!(p.graph_logits.is_none());
23849 assert!(!p.graph_want_logits);
23850 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23851 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23852 assert_eq!(p.kv_cache.seq_len(), 0);
23853
23854 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23855 .forward_ids(&ids, None)
23856 .expect("fresh forward_ids");
23857 let actual = p
23858 .forward_ids(&ids, None)
23859 .expect("pipeline remains reusable after a failed forward");
23860 assert_eq!(actual.len(), expected.len());
23861 assert!(
23862 actual
23863 .iter()
23864 .zip(expected)
23865 .all(|(a, b)| (a - b).abs() < 1e-9)
23866 );
23867 assert_eq!(p.kv_cache.seq_len(), ids.len());
23868 }
23869
23870 #[test]
23871 fn sigmoid_router_floor_is_explicit_per_architecture() {
23872 let zero = || DenseFfn {
23878 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23879 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23880 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23881 act: Act::Silu,
23882 down_t: None,
23883 segs: Vec::new(),
23884 };
23885 let m = MoeFfn {
23886 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23887 experts: vec![zero(), zero()],
23888 top_k: 1,
23889 norm_topk_prob: true,
23890 router_sigmoid: true,
23891 sigmoid_denom_eps: 1e-20,
23892 expert_bias: None,
23893 routed_scaling: 2.5,
23894 route_tau: None,
23895 shared: None,
23896 stats: std::cell::RefCell::new(Vec::new()),
23897 act_sq: std::cell::RefCell::new(Vec::new()),
23898 act_rows: std::cell::RefCell::new(Vec::new()),
23899 mask: None,
23900 per_expert_scale: None,
23901 router_input_norm: false,
23902 resonance: None,
23903 grown: Vec::new(),
23904 };
23905 let logits = [-20.0f32, -20.0];
23906 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23907 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23908 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23909 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23910 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23911 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23912 assert!(legacy_wsum > minimax_wsum * 100.0);
23913 }
23914
23915 #[test]
23916 fn resonance_scores_match_formula_and_stable_tie() {
23917 let r = Resonance {
23918 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23920 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23921 k: 1,
23922 bias: vec![1.5, 0.5, 0.0],
23923 shell: Vec::new(),
23924 };
23925 let x = [1.0f32, 1.0];
23926 let mut got = vec![0.0; 3];
23927 r.scores(&x, &mut got);
23928 assert!((got[0] - 0.5).abs() < 1e-6);
23932 assert!((got[1] - 0.5).abs() < 1e-6);
23933 assert!(got[2].abs() < 1e-6);
23934 let best = got
23935 .iter()
23936 .enumerate()
23937 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23938 .map(|(i, _)| i);
23939 assert_eq!(best, Some(0));
23940 assert!(got.iter().all(|v| v.is_finite()));
23941 }
23942
23943 #[test]
23948 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23949 let plain = Resonance {
23955 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23956 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23957 k: 1,
23958 bias: vec![1.5, 0.5, 0.0, 0.0],
23959 shell: Vec::new(),
23960 };
23961 let shelled = Resonance {
23962 mu: plain.mu.clone(),
23963 u: plain.u.clone(),
23964 k: 1,
23965 bias: plain.bias.clone(),
23966 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23967 };
23968 assert!(!plain.has_shell());
23969 assert!(shelled.has_shell());
23970 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23971 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23972 set_growth_shell(Some(true));
23973 assert!(growth_shell_enabled());
23974 let x = [1.0f32, 1.0];
23977 plain.scores(&x, &mut a);
23978 shelled.scores(&x, &mut b);
23979 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23980 assert!(a[2] == 0.0 && a[3] == 0.0);
23981 let xo = [3.0f32, 0.0];
23984 plain.scores(&xo, &mut a);
23985 shelled.scores(&xo, &mut b);
23986 assert_eq!(a[2], -6.0);
23987 assert_eq!(a[3], -6.0);
23988 assert_eq!(
23989 bits(&a[..3]),
23990 bits(&b[..3]),
23991 "trunk rows + the boundary row"
23992 );
23993 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23994 assert_eq!(shelled.effective_shell(4), shelled.shell);
23995 set_growth_shell(Some(false));
23998 assert!(!growth_shell_enabled());
23999 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
24000 shelled.scores(&xo, &mut b);
24001 assert_eq!(bits(&a), bits(&b));
24002 set_growth_shell(None);
24003 let short = Resonance {
24006 shell: vec![f32::INFINITY, f32::INFINITY],
24007 ..shelled
24008 };
24009 set_growth_shell(Some(true));
24010 short.scores(&xo, &mut b);
24011 assert_eq!(bits(&a), bits(&b));
24012 set_growth_shell(None);
24013 }
24014
24015 #[test]
24019 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
24020 let zero = || DenseFfn {
24021 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24022 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24023 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24024 act: Act::Silu,
24025 down_t: None,
24026 segs: Vec::new(),
24027 };
24028 let moe = |sigmoid: bool| MoeFfn {
24029 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24030 experts: vec![zero(), zero(), zero(), zero()],
24031 top_k: 1,
24032 norm_topk_prob: true,
24033 router_sigmoid: sigmoid,
24034 sigmoid_denom_eps: 1e-6,
24035 expert_bias: None,
24036 routed_scaling: 1.0,
24037 route_tau: None,
24038 shared: None,
24039 stats: std::cell::RefCell::new(Vec::new()),
24040 act_sq: std::cell::RefCell::new(Vec::new()),
24041 act_rows: std::cell::RefCell::new(Vec::new()),
24042 mask: None,
24043 per_expert_scale: None,
24044 router_input_norm: false,
24045 resonance: None,
24046 grown: Vec::new(),
24047 };
24048 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24049 for sigmoid in [false, true] {
24050 let m = moe(sigmoid);
24051 let (idx, p, wsum) = moe_route(&logits, &m, None);
24052 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24053 assert_eq!(p[1], 0.0);
24054 assert_eq!(p[3], 0.0);
24055 assert!(p[2] > p[0] && p[0] > 0.0);
24056 assert!(p.iter().all(|v| v.is_finite()));
24057 let w = p[2] / wsum;
24058 if sigmoid {
24059 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24061 } else {
24062 assert_eq!(
24063 w, 1.0,
24064 "softmax: the renormalized top-1 weight is exactly 1"
24065 );
24066 }
24067 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24070 assert_eq!(idx, vec![2]);
24071 }
24072 let m = moe(false);
24074 let (idx, _, _) = moe_route(
24075 &[
24076 f32::NEG_INFINITY,
24077 f32::NEG_INFINITY,
24078 f32::NEG_INFINITY,
24079 -9.0,
24080 ],
24081 &m,
24082 None,
24083 );
24084 assert_eq!(idx, vec![3]);
24085 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
24088 assert_eq!(idx, vec![0]);
24089 assert!(p.iter().all(|&v| v == 0.25));
24090 assert!(wsum.is_finite() && wsum > 0.0);
24091 }
24092
24093 #[cfg(feature = "gpu")]
24099 #[test]
24100 fn minimax_device_route_matches_host_sigmoid_contract() {
24101 match crate::gpu_wgpu::selected_and_up() {
24102 None => {
24103 crate::gpu_wgpu::skip_or_fail(module_path!());
24104 return;
24105 }
24106 Some(false) => {
24114 eprintln!(
24115 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24116 module_path!()
24117 );
24118 return;
24119 }
24120 Some(true) => {}
24121 }
24122 let n = 128usize;
24123 let top_k = 4usize;
24124 let logits: Vec<f32> = (0..n)
24125 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24126 .collect();
24127 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24128 let m = MoeFfn {
24129 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24130 experts: Vec::new(),
24131 top_k,
24132 norm_topk_prob: true,
24133 router_sigmoid: true,
24134 sigmoid_denom_eps: 1e-20,
24135 expert_bias: Some(bias.clone()),
24136 routed_scaling: 2.0,
24137 route_tau: None,
24138 shared: None,
24139 stats: std::cell::RefCell::new(Vec::new()),
24140 act_sq: std::cell::RefCell::new(Vec::new()),
24141 act_rows: std::cell::RefCell::new(Vec::new()),
24142 mask: None,
24143 per_expert_scale: None,
24144 router_input_norm: false,
24145 resonance: None,
24146 grown: Vec::new(),
24147 };
24148 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24149 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24150
24151 for shared in [Some(n as u32), None] {
24152 let mut gi = Vec::new();
24153 let mut gw = Vec::new();
24154 assert!(
24155 crate::gpu_wgpu::minimax_moe_route_for_test(
24156 &logits,
24157 Some(&bias),
24158 top_k,
24159 2.0,
24160 shared,
24161 &mut gi,
24162 &mut gw,
24163 ),
24164 "the device did not come up — the test checked nothing"
24165 );
24166 let routed = if shared.is_some() { top_k } else { gi.len() };
24167 assert_eq!(
24168 &gi[..routed],
24169 &ci[..],
24170 "different experts or order (shared={shared:?})"
24171 );
24172 let d = cw
24173 .iter()
24174 .zip(&gw[..routed])
24175 .map(|(a, b)| (a - b).abs())
24176 .fold(0.0f32, f32::max);
24177 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24178 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24179 if let Some(s) = shared {
24180 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24181 assert_eq!(gw[top_k], 1.0, "shared weight");
24182 }
24183 }
24184 }
24185
24186 #[cfg(feature = "gpu")]
24191 #[test]
24192 fn minimax_device_cold_weights_match_host_contract() {
24193 match crate::gpu_wgpu::selected_and_up() {
24194 None => {
24195 crate::gpu_wgpu::skip_or_fail(module_path!());
24196 return;
24197 }
24198 Some(false) => {
24206 eprintln!(
24207 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24208 module_path!()
24209 );
24210 return;
24211 }
24212 Some(true) => {}
24213 }
24214 let n = 128usize;
24215 let top_k = 4usize;
24216 let logits: Vec<f32> = (0..n)
24217 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24218 .collect();
24219 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24220 let m = MoeFfn {
24221 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24222 experts: Vec::new(),
24223 top_k,
24224 norm_topk_prob: true,
24225 router_sigmoid: true,
24226 sigmoid_denom_eps: 1e-20,
24227 expert_bias: Some(bias.clone()),
24228 routed_scaling: 2.0,
24229 route_tau: None,
24230 shared: None,
24231 stats: std::cell::RefCell::new(Vec::new()),
24232 act_sq: std::cell::RefCell::new(Vec::new()),
24233 act_rows: std::cell::RefCell::new(Vec::new()),
24234 mask: None,
24235 per_expert_scale: None,
24236 router_input_norm: false,
24237 resonance: None,
24238 grown: Vec::new(),
24239 };
24240 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24241 assert_eq!(ci.len(), top_k);
24242
24243 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24245 let mut cold = Vec::new();
24246 let mut idx = Vec::new();
24247 let mut dw = Vec::new();
24248 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24249 &logits,
24250 Some(&bias),
24251 top_k,
24252 2.0,
24253 &resident,
24254 n as u32,
24255 &mut cold,
24256 &mut idx,
24257 &mut dw,
24258 ));
24259 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24260 for (r, &e) in ci.iter().enumerate() {
24265 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24266 let host = p[e] / wsum;
24267 let d = (dw[r] - host).abs();
24268 assert!(
24269 d <= 1e-6 * host.abs().max(1.0),
24270 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24271 dw[r]
24272 );
24273 }
24274 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24275 assert_eq!(dw[top_k], 1.0, "shared weight");
24276
24277 let mut remap = resident.clone();
24279 remap[ci[0]] = u32::MAX;
24280 remap[ci[2]] = u32::MAX;
24281 let mut cold = Vec::new();
24282 let mut idx2 = Vec::new();
24283 let mut dw2 = Vec::new();
24284 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24285 &logits,
24286 Some(&bias),
24287 top_k,
24288 2.0,
24289 &remap,
24290 n as u32,
24291 &mut cold,
24292 &mut idx2,
24293 &mut dw2,
24294 ));
24295 let mut resident_weight_checked = 0usize;
24303 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24304 if dw2[r] == 0.0 {
24305 continue; }
24307 let e = slot as usize;
24308 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24309 let host = p[e] / wsum;
24310 let d = (dw2[r] - host).abs();
24311 assert!(
24312 d <= 1e-6 * host.abs().max(1.0),
24313 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24314 );
24315 resident_weight_checked += 1;
24316 }
24317 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24318 println!("partial bank: checked {resident_weight_checked} resident weights");
24319 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24320 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24321 ids.sort_unstable();
24322 let mut want = vec![ci[0], ci[2]];
24323 want.sort_unstable();
24324 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24325 for &(e, w) in &cold {
24326 let host = p[e] / wsum;
24327 let d = (w - host).abs();
24328 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24329 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24330 }
24331 }
24332
24333 #[test]
24338 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24339 let zero = || DenseFfn {
24340 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24341 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24342 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24343 act: Act::Silu,
24344 down_t: None,
24345 segs: Vec::new(),
24346 };
24347 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24348 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24349 experts: vec![zero(), zero(), zero()],
24350 top_k: 1,
24351 norm_topk_prob: norm_topk,
24352 router_sigmoid: false,
24353 sigmoid_denom_eps: 1e-6,
24354 expert_bias: None,
24355 routed_scaling: 1.0,
24356 route_tau: None,
24357 shared: None,
24358 stats: std::cell::RefCell::new(Vec::new()),
24359 act_sq: std::cell::RefCell::new(Vec::new()),
24360 act_rows: std::cell::RefCell::new(Vec::new()),
24361 mask: None,
24362 per_expert_scale: None,
24363 router_input_norm: false,
24364 resonance: resonant.then(|| Resonance {
24365 mu: vec![0.0; 6],
24366 u: Vec::new(),
24367 k: 0,
24368 bias: vec![0.0; 3],
24369 shell: Vec::new(),
24370 }),
24371 grown: Vec::new(),
24372 };
24373 let lo = -0.1f32;
24376 let hi = f32::from_bits(lo.to_bits() - 1);
24377 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24378 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24379 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24381 assert_eq!(
24382 idx,
24383 vec![0],
24384 "softmax tie → lower index (the gated contract)"
24385 );
24386 for norm in [true, false] {
24389 let m = moe(true, norm);
24390 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24391 assert_eq!(idx, vec![1], "norm_topk {norm}");
24392 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24393 assert_eq!(p[1] / wsum, 1.0);
24394 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24397 assert_eq!(idx, vec![0]);
24398 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24400 assert_eq!(idx, vec![2]);
24401 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24402 assert_eq!(idx, vec![0]);
24403 assert_eq!(p[0] / wsum, 1.0);
24404 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24407 assert_eq!(idx, vec![0]);
24408 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24409 }
24410 }
24411}