1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16use crate::kv_cache::KvCache;
17use crate::linear_core::{
18 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
19 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
20 vmf_phase_pair,
21};
22use crate::pool::Pool;
23use crate::qtensor::QTensor;
24use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
25use crate::tokenizer::Tokenizer;
26use cortiq_core::mask::TaskMask;
27use cortiq_core::types::NormStyle;
28
29pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
30 std::sync::atomic::AtomicBool::new(false);
31
32thread_local! {
37 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
38}
39
40struct ExactMtpCalibrationGuard;
41
42impl ExactMtpCalibrationGuard {
43 fn enter() -> Self {
44 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
45 Self
46 }
47}
48
49impl Drop for ExactMtpCalibrationGuard {
50 fn drop(&mut self) {
51 EXACT_MTP_CALIBRATION.with(|depth| {
52 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
53 depth.set(depth.get().saturating_sub(1));
54 });
55 }
56}
57
58fn exact_mtp_calibration_active() -> bool {
59 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
60}
61
62fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
66 logits.resize(vocab_size, f32::NEG_INFINITY);
67}
68
69struct ForwardScratch {
73 n1: Vec<f32>,
74 n2: Vec<f32>,
75 p1: Vec<f32>,
76 p2: Vec<f32>,
77}
78
79impl ForwardScratch {
80 fn new(hidden: usize) -> Self {
81 Self {
82 n1: vec![0.0; hidden],
83 n2: vec![0.0; hidden],
84 p1: vec![0.0; hidden],
85 p2: vec![0.0; hidden],
86 }
87 }
88}
89
90pub struct Pipeline {
92 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
97 pub tokenizer: std::sync::Arc<Tokenizer>,
100 pub kv_cache: KvCache,
101 pub sampler_config: SamplerConfig,
102 pub weights: PipelineWeights,
103 pub hidden_size: usize,
104 pub intermediate_size: usize,
105 pub num_heads: usize,
106 pub num_kv_heads: usize,
107 pub head_dim: usize,
108 pub num_layers: usize,
110 pub physical_layers: usize,
112 pub loop_final_norm: bool,
114 pub vocab_size: usize,
115 pub rms_eps: f64,
116 pub rope_base: f32,
117 pub norm_style: NormStyle,
118 pub rotary_dim: usize,
120 pub attention_heads_per_layer: Option<Vec<usize>>,
122 pub kv_heads_per_layer: Option<Vec<usize>>,
127 pub v_head_dim: Option<usize>,
132 pub layer_dump: Option<std::path::PathBuf>,
145 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
148 pub(crate) mimo_moe: crate::mimo_moe::Slot,
151 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
156 pub vmf_cfg: Option<VmfPhaseCfg>,
158 pub gdn_cfg: Option<GdnCfg>,
160 pub logit_multiplier: Option<f32>,
162 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
167 graph_failed: std::sync::atomic::AtomicBool,
172 pub kv_history: Vec<u32>,
177 pub kv_history_device: bool,
180 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
182 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
185 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
188 pub dsv4: Option<
192 Box<(
193 crate::dsv4::Dsv4Globals,
194 Vec<crate::dsv4::Dsv4Layer>,
195 crate::dsv4::Dsv4Cfg,
196 crate::dsv4::Dsv4State,
197 )>,
198 >,
199 pub dsv41: Option<
203 Box<(
204 crate::dsv41::Dsv41Globals,
205 Vec<crate::dsv41::Dsv41Layer>,
206 crate::dsv41::Dsv41Cfg,
207 crate::dsv41::Dsv41State,
208 )>,
209 >,
210 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
212 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
214 pub qwen4_exp: Option<
217 Box<(
218 crate::qwen4_exp::Globals,
219 Vec<crate::qwen4_exp::Layer>,
220 crate::qwen4_exp::Cfg,
221 crate::qwen4_exp::State,
222 )>,
223 >,
224 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
228 pub dspark: Option<crate::dsv4::DsparkState>,
230 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
233 pub dspark_hist: Vec<usize>,
235 pub dspark_real: Vec<u32>,
239 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
243 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
245 pub dspark_draft_ns: u128,
249 m3_winners: Vec<Vec<u32>>,
251 m3_counts: Vec<u32>,
253 m3_n_exp: usize,
254 m3_tokens: u64,
255 m3_prewarmed: bool,
256 pub short_conv_cfg: Option<ShortConvCfg>,
259 pub mtp: Option<MtpModule>,
261 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
265 verify_exact_moe: bool,
268 pub speculative: bool,
270 pub ignore_eos: bool,
276 pub draft_full_streak: u32,
282 pub spec_k_adapt: Option<usize>,
290 pub spec_acc_ewma: f32,
292 rng: SplitMix64,
293 sampler_scratch: SamplerScratch,
294 spec_forced: Option<u32>,
300 spec_q: Vec<Vec<f32>>,
301 spec_p: Vec<f32>,
302 spec_res: Vec<f32>,
303 spec_qs: Vec<sampler::Sparse>,
305 spec_ps: sampler::Sparse,
306 spec_ress: sampler::Sparse,
307 mtp_graph_mode: Option<bool>,
314 #[cfg(target_os = "macos")]
317 metal_verify: Option<MetalVerifyPending>,
318 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
322 ws: ForwardScratch,
326 pool: Option<std::sync::Arc<Pool>>,
328 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
332 pub(crate) dyn_force_f32: bool,
334 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
339 pub(crate) dyn_active: Option<usize>,
345 pub(crate) dyn_blend_loaded: bool,
349 pub(crate) dyn_phi_layer: Option<usize>,
352 dyn_phi_ema: Vec<f32>,
354 dyn_phi_seen: usize,
355 pub dyn_router: Option<crate::swarm::DynRouter>,
358 o1_cfg: Option<crate::nystrom::O1Cfg>,
361 o1_epoch: u64,
364 o1_flags: Vec<bool>,
366 trace: bool,
369 calib_temp: f32,
372 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
374 graph_kv_id: u64,
375 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
378 graph_want_logits: bool,
379 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
383 graph_head_required: bool,
384 graph_logits: Option<Vec<f32>>,
387 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
390 graph_refused: std::sync::atomic::AtomicBool,
396 pub embed_multiplier: f32,
398 pub attn_scale: f32,
401 pub swa: Option<(usize, usize)>,
404 pub swa_trim: Option<(usize, usize)>,
411 pub sliding_layers: Option<Vec<bool>>,
414 pub no_rope_layers: Option<Vec<bool>>,
419 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
425 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
428 pub kv_prefix: KvPrefix,
432 pub last_prefill_tokens: usize,
435 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
438 pub rotary_dim_local: Option<usize>,
439 pub rope_scale: f32,
440 pub rope_scale_local: f32,
441 pub global_attn: Option<(usize, usize)>,
444 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
447 pub attn_v_norm: bool,
449 pub qk_norm_after_rope: bool,
451 pub proj_gate_sigmoid: bool,
454 pub final_softcap: Option<f32>,
456 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
460 pub attn_softcap: f32,
462 confidence_on: bool,
466 #[cfg(test)]
469 nll_test_fail_at: Option<usize>,
470 #[cfg(test)]
473 nll_test_force_serial: bool,
474}
475
476#[cfg(target_os = "macos")]
477impl Drop for Pipeline {
478 fn drop(&mut self) {
479 let _ = crate::gpu_metal::wait_replay();
481 crate::gpu::kv_mirror_drop(self.graph_kv_id);
482 }
483}
484
485#[cfg(not(target_os = "macos"))]
486impl Drop for Pipeline {
487 fn drop(&mut self) {
488 crate::gpu::graph_kv_reset(self.graph_kv_id);
492 }
493}
494
495pub struct PipelineWeights {
500 pub embed_tokens: QTensor,
502 pub layers: Vec<LayerWeights>,
504 pub lm_head: QTensor,
506 pub final_norm: Vec<f32>,
508}
509
510pub struct LayerWeights {
512 pub input_norm: Vec<f32>,
513 pub post_norm: Vec<f32>,
516 pub attn_out_norm: Option<Vec<f32>>,
519 pub layer_scale: Option<f32>,
521 pub ffn_out_norm: Option<Vec<f32>>,
524 pub ffn: FfnKind,
525 pub attn: AttnKind,
526}
527
528#[derive(Clone, Copy, PartialEq, Debug, Default)]
532pub enum Act {
533 #[default]
534 Silu,
535 GeluTanh,
536 Gelu,
538 Situ {
541 beta: f32,
542 linear_beta: f32,
543 },
544 SwiGluOai {
549 alpha: f32,
550 limit: f32,
551 },
552}
553
554impl Act {
555 pub fn from_arch(name: &str) -> Self {
556 if name == "gelu_tanh" {
557 Self::GeluTanh
558 } else if name == "gelu" {
559 Self::Gelu
560 } else {
561 Self::Silu
562 }
563 }
564
565 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
567 match arch.hidden_act.as_str() {
568 "situ" => Self::Situ {
569 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
570 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
571 },
572 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
577 alpha: 1.702,
578 limit: 7.0,
579 },
580 other => Self::from_arch(other),
581 }
582 }
583
584 #[inline]
589 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
590 Self::SwiGluOai { alpha, limit }
591 }
592
593 #[inline]
594 pub fn apply(self, x: f32) -> f32 {
595 match self {
596 Self::Silu => inference::silu(x),
597 Self::GeluTanh => inference::gelu_tanh(x),
598 Self::Gelu => inference::gelu_erf(x),
599 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
600 Self::SwiGluOai { alpha, limit } => {
601 let gate = x.min(limit);
602 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
603 }
604 }
605 }
606
607 #[inline]
610 pub fn combine(self, g: f32, u: f32) -> f32 {
611 match self {
612 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
613 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
614 }
615 Self::SwiGluOai { alpha, limit } => {
616 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
617 }
618 _ => self.apply(g) * u,
619 }
620 }
621
622 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
626 match self {
627 Self::Silu => Some(crate::gpu::GraphAct::Silu),
628 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
629 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
630 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
631 }
632 }
633
634 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
640 self.moe_job_graph_contract().map(|(act, _)| act)
641 }
642
643 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
649 match self {
650 Self::SwiGluOai { alpha, limit }
651 if alpha.to_bits() == 1.702f32.to_bits()
652 && limit.to_bits() == 7.0f32.to_bits() =>
653 {
654 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
655 }
656 _ => self.graph_act().map(|act| (act, 0.0)),
657 }
658 }
659}
660
661pub struct DenseFfn {
663 pub gate_proj: QTensor,
664 pub up_proj: QTensor,
665 pub down_proj: QTensor,
666 pub act: Act,
668 pub down_t: Option<QTensor>,
674 pub segs: Vec<FfnSeg>,
681}
682
683pub struct FfnSeg {
688 pub gate: QTensor,
689 pub up: QTensor,
690 pub down: QTensor,
691 pub start: usize,
692 pub width: usize,
693}
694
695pub enum FfnKind {
698 Dense(DenseFfn),
699 Moe(MoeFfn),
703 DenseMoe(Box<DenseMoeFfn>),
710}
711
712pub struct DenseMoeFfn {
714 pub dense: DenseFfn,
715 pub moe: MoeFfn,
716 pub post_norm_1: Vec<f32>,
718 pub pre_norm_2: Vec<f32>,
721 pub post_norm_2: Vec<f32>,
723}
724
725pub struct MoeFfn {
726 pub router: QTensor,
728 pub experts: Vec<DenseFfn>,
729 pub top_k: usize,
730 pub norm_topk_prob: bool,
731 pub router_sigmoid: bool,
734 pub sigmoid_denom_eps: f32,
740 pub expert_bias: Option<Vec<f32>>,
744 pub routed_scaling: f32,
747 pub route_tau: Option<f32>,
753 pub shared: Option<(DenseFfn, Option<QTensor>)>,
756 pub stats: std::cell::RefCell<Vec<u64>>,
760 pub act_sq: std::cell::RefCell<Vec<f64>>,
767 pub act_rows: std::cell::RefCell<Vec<f32>>,
773 pub mask: Option<Vec<bool>>,
778 pub per_expert_scale: Option<Vec<f32>>,
781 pub router_input_norm: bool,
785 pub resonance: Option<Resonance>,
789 pub grown: Vec<GrownExpert>,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
799pub struct GrownExpert {
800 pub record: String,
802 pub record_index: usize,
804 pub layer: usize,
805 pub expert: usize,
809}
810
811pub struct Resonance {
813 pub mu: Vec<f32>,
815 pub u: Vec<f32>,
817 pub k: usize,
818 pub bias: Vec<f32>,
820 pub shell: Vec<f32>,
826}
827
828static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
831
832pub fn growth_shell_enabled() -> bool {
837 use std::sync::atomic::Ordering;
838 match GROWTH_SHELL.load(Ordering::Relaxed) {
839 1 => true,
840 2 => false,
841 _ => {
842 let off = std::env::var("CMF_GROWTH_SHELL")
843 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
844 .unwrap_or(false);
845 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
846 !off
847 }
848 }
849}
850
851pub fn set_growth_shell(on: Option<bool>) {
856 GROWTH_SHELL.store(
857 match on {
858 Some(true) => 1,
859 Some(false) => 2,
860 None => 0,
861 },
862 std::sync::atomic::Ordering::Relaxed,
863 );
864}
865
866impl Resonance {
867 pub fn has_shell(&self) -> bool {
869 self.shell.iter().any(|s| s.is_finite())
870 }
871
872 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
875 let mut out = vec![f32::INFINITY; ne];
876 if growth_shell_enabled() {
877 for (o, s) in out.iter_mut().zip(&self.shell) {
878 *o = *s;
879 }
880 }
881 out
882 }
883
884 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
889 let h = x.len();
890 let ne = out.len();
891 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
892 for e in 0..ne {
893 let mu = &self.mu[e * h..(e + 1) * h];
894 let mut d2 = 0.0f32;
895 for j in 0..h {
896 let d = x[j] - mu[j];
897 d2 += d * d;
898 }
899 let mut proj = 0.0f32;
900 for i in 0..self.k {
901 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
902 let mut p = 0.0f32;
903 for j in 0..h {
904 p += (x[j] - mu[j]) * u[j];
905 }
906 proj += p * p;
907 }
908 let err = d2 - proj;
909 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
910 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
911 out[e] = f32::NEG_INFINITY;
912 }
913 }
914 }
915}
916
917pub enum AttnKind {
920 Full {
922 wq: QTensor,
923 wk: QTensor,
924 wv: QTensor,
925 wo: QTensor,
926 q_norm: Option<Vec<f32>>,
927 k_norm: Option<Vec<f32>>,
928 output_gate: bool,
929 softplus_gate: Option<(QTensor, bool)>,
933 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
935 },
936 Linear(VmfPhaseWeights),
938 LinearGdn(GdnWeights),
940 ShortConv(ShortConvWeights),
943 Mla(Box<MlaWeights>),
951 Kda(Box<crate::linear_core::KdaWeights>),
955 Bounded(Box<crate::bounded::BoundedWeights>),
960}
961
962pub struct MlaWeights {
964 pub q_proj: QTensor,
968 pub q_a: Option<QTensor>,
971 pub q_a_norm: Option<Vec<f32>>,
972 pub kv_a: QTensor,
974 pub kv_a_norm: Vec<f32>,
976 pub kv_b: QTensor,
978 pub o_proj: QTensor,
980 pub nh: usize,
981 pub qk_rope: usize,
982 pub qk_nope: usize,
983 pub v_dim: usize,
984 pub lora: usize,
985 pub scale: f32,
987 pub nope: bool,
989}
990
991pub struct MtpModule {
996 pub enorm: Vec<f32>,
997 pub hnorm: Vec<f32>,
998 pub eh_proj: QTensor,
1000 pub layer: LayerWeights,
1001 pub final_norm: Vec<f32>,
1002 pub kv: crate::kv_cache::LayerKvCache,
1003}
1004
1005#[derive(Clone, Debug, PartialEq)]
1017pub struct MtpFfnProbe {
1018 pub mass: Vec<f64>,
1019 pub squared_mass: Vec<f64>,
1020 pub observed_positions: usize,
1021}
1022
1023#[cfg(target_os = "macos")]
1030enum MetalRowsItem<'a> {
1031 Gdn {
1032 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1033 first: usize,
1034 },
1035 Attn {
1036 l: crate::gpu_metal::AttnGpuLayer<'a>,
1037 li: usize,
1038 q_norm: Option<&'a [f32]>,
1039 k_norm: Option<&'a [f32]>,
1040 output_gate: bool,
1041 },
1042}
1043
1044#[cfg(target_os = "macos")]
1045struct MetalVerifyPending {
1046 graph: crate::gpu_metal::VerifyGraph,
1047 gdn_layers: Vec<usize>,
1048 attn_layers: Vec<(usize, usize)>,
1049}
1050
1051#[cfg(target_os = "macos")]
1055struct MetalWarmPending {
1056 graph: crate::gpu_metal::VerifyGraph,
1057 cpu_stored: usize,
1058 b: usize,
1059}
1060
1061#[cfg(target_os = "macos")]
1062enum MetalRowsRun {
1063 Declined,
1065 Failed,
1068 Completed(MetalVerifyPending),
1069}
1070
1071#[cfg(target_os = "macos")]
1072enum MetalPrefillOutcome {
1073 Declined,
1074 Failed,
1075 Completed(Vec<f32>),
1076}
1077
1078#[cfg(target_os = "macos")]
1079enum MetalBatchNllOutcome {
1080 Declined,
1081 Failed(String),
1082 Completed(f64, usize),
1083}
1084
1085#[derive(Clone, Copy)]
1091enum SpecTrial {
1092 Spec {
1093 t0: std::time::Instant,
1094 gen0: usize,
1095 rounds: usize,
1096 },
1097 Plain {
1098 t0: std::time::Instant,
1099 gen0: usize,
1100 },
1101 Decided {
1102 spec: bool,
1103 recheck_at: usize,
1104 },
1105}
1106
1107pub(crate) fn spec_time_level() -> u8 {
1111 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1112 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1113 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1114 Err(_) => 0,
1115 })
1116}
1117
1118fn graph_spec_wanted_by_policy(
1126 spec_env: Option<&str>,
1127 spec_default_ok: bool,
1128 penalized: bool,
1129 metal_wgpu: bool,
1130 native_metal: bool,
1131) -> bool {
1132 match spec_env {
1133 Some("0") => false,
1134 Some("1") => true,
1139 Some(_) => !native_metal,
1140 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1143 }
1144}
1145
1146struct SpecStampLog {
1152 t_last: std::time::Instant,
1153 items: Vec<(&'static str, f32)>,
1154}
1155
1156static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1157
1158pub(crate) fn spec_stamp(name: &'static str) {
1159 if spec_time_level() == 0 {
1160 return;
1161 }
1162 if let Ok(mut g) = SPEC_STAMPS.lock() {
1163 if let Some(log) = g.as_mut() {
1164 let now = std::time::Instant::now();
1165 log.items
1166 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1167 log.t_last = now;
1168 }
1169 }
1170}
1171
1172fn spec_stamps_begin() {
1173 if spec_time_level() == 0 {
1174 return;
1175 }
1176 if let Ok(mut g) = SPEC_STAMPS.lock() {
1177 *g = Some(SpecStampLog {
1178 t_last: std::time::Instant::now(),
1179 items: Vec::with_capacity(64),
1180 });
1181 }
1182}
1183
1184fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1185 SPEC_STAMPS
1186 .lock()
1187 .ok()
1188 .and_then(|mut g| g.take())
1189 .map(|l| l.items)
1190 .unwrap_or_default()
1191}
1192
1193fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1196 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1197 for &(n, ms) in items {
1198 match agg.iter_mut().find(|e| e.0 == n) {
1199 Some(e) => {
1200 e.1 += ms;
1201 e.2 += 1;
1202 }
1203 None => agg.push((n, ms, 1)),
1204 }
1205 }
1206 let mut s = String::with_capacity(agg.len() * 16);
1207 for (n, ms, k) in agg {
1208 if k > 1 {
1209 s.push_str(&format!("{n} {ms:.1}/{k} "));
1210 } else {
1211 s.push_str(&format!("{n} {ms:.1} "));
1212 }
1213 }
1214 s
1215}
1216
1217#[derive(Default, Clone, Copy)]
1239struct SpecMon {
1240 round_ms: f64,
1241 tokens: f64,
1242 plain_ms: f64,
1243 n: u32,
1244 fails: u32,
1245 zero_accept_rounds: u8,
1250 metal: bool,
1251}
1252
1253const SPEC_PROXY_TOKENS: f64 = 3.5;
1256const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1259
1260impl SpecMon {
1261 fn round(&mut self, dt_ms: f64, produced: usize) {
1262 if produced <= 1 {
1263 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1264 } else {
1265 self.zero_accept_rounds = 0;
1266 }
1267 self.n += 1;
1268 if self.n == 1 {
1269 return; }
1271 let a = if self.n == 2 { 1.0 } else { 0.3 };
1272 self.round_ms += a * (dt_ms - self.round_ms);
1273 self.tokens += a * (produced as f64 - self.tokens);
1274 }
1275 fn pays(&self) -> bool {
1276 if self.plain_ms > 0.0 {
1277 self.tokens * self.plain_ms > self.round_ms * 1.03
1278 } else {
1279 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1280 }
1281 }
1282 fn repeated_zero_acceptance(&self) -> bool {
1283 self.zero_accept_rounds >= 2
1284 }
1285 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1287 let n = generated.saturating_sub(gen0);
1288 if n >= 8 {
1289 return true;
1290 }
1291 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1292 }
1293}
1294
1295pub const KV_PREFIX_TAIL: usize = 128;
1298
1299#[derive(Debug, Clone, Default)]
1305pub struct KvPrefix {
1306 len: usize,
1307 hash: u64,
1308 tail: Vec<u32>,
1309 device: bool,
1314}
1315
1316impl KvPrefix {
1317 #[inline]
1318 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1319 for &id in ids {
1320 h ^= id as u64;
1321 h = h.wrapping_mul(0x100000001b3);
1322 h ^= h >> 29;
1323 }
1324 h
1325 }
1326
1327 pub fn clear(&mut self) {
1328 self.len = 0;
1329 self.hash = 0xcbf29ce484222325;
1330 self.tail.clear();
1331 self.device = false;
1332 }
1333
1334 pub fn on_device(&self) -> bool {
1336 self.device
1337 }
1338
1339 pub fn set_on_device(&mut self, device: bool) {
1341 self.device = device;
1342 }
1343
1344 pub fn len(&self) -> usize {
1346 self.len
1347 }
1348
1349 pub fn is_empty(&self) -> bool {
1350 self.len == 0
1351 }
1352
1353 pub fn tail_len(&self) -> usize {
1355 self.tail.len()
1356 }
1357
1358 pub fn set(&mut self, ids: &[u32]) {
1360 self.clear();
1361 self.extend(ids);
1362 }
1363
1364 pub fn extend(&mut self, more: &[u32]) {
1366 if self.len == 0 && self.hash == 0 {
1367 self.hash = 0xcbf29ce484222325;
1368 }
1369 self.hash = Self::fold(self.hash, more);
1370 self.len += more.len();
1371 if more.len() >= KV_PREFIX_TAIL {
1372 self.tail.clear();
1373 self.tail
1374 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1375 } else {
1376 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1377 self.tail.drain(..drop);
1378 self.tail.extend_from_slice(more);
1379 }
1380 }
1381
1382 pub fn extension(&self, ids: &[u32]) -> usize {
1386 if self.len == 0 || ids.len() <= self.len {
1387 return 0;
1388 }
1389 let t = self.tail.len();
1390 if ids[self.len - t..self.len] != self.tail[..] {
1391 return 0;
1392 }
1393 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1394 return 0;
1395 }
1396 self.len
1397 }
1398}
1399
1400pub struct GenerateResult {
1402 pub text: String,
1403 pub token_ids: Vec<u32>,
1404 pub prompt_tokens: usize,
1405 pub tokens_generated: usize,
1406 pub finish_reason: String,
1407 pub mtp_drafted: usize,
1409 pub mtp_accepted: usize,
1410 pub token_confidence: Vec<f32>,
1415 pub traces: Vec<TokenTrace>,
1418}
1419
1420#[derive(Clone, Debug)]
1425pub struct TokenTrace {
1426 pub t: usize,
1428 pub token_id: u32,
1430 pub confidence: f32,
1432 pub active_skill: Option<String>,
1434 pub recon: Option<f32>,
1438 pub switched: bool,
1441}
1442
1443#[cfg_attr(not(test), allow(dead_code))]
1448fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1449 let t = if temp > 1e-3 { temp } else { 1.0 };
1450 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1451 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1452 if sum > 0.0 {
1453 (((logits[id as usize] - max) / t).exp()) / sum
1454 } else {
1455 0.0
1456 }
1457}
1458
1459fn prefill_batched() -> bool {
1462 std::env::var("CMF_PREFILL")
1463 .map(|v| v != "seq")
1464 .unwrap_or(true)
1465}
1466
1467#[inline]
1471fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1472 let graph_quality = unmasked && prefer_graph;
1473 let fused_head_quality = graph_quality && native_metal;
1474 (graph_quality, fused_head_quality)
1475}
1476
1477#[derive(Clone, Copy)]
1481enum PrefillIn<'a> {
1482 Ids(&'a [u32]),
1483 Hidden(&'a [f32]),
1484}
1485
1486impl Pipeline {
1493 fn can_prefill_batched(&self) -> bool {
1494 #[cfg(test)]
1495 let force_serial = self.nll_test_force_serial;
1496 #[cfg(not(test))]
1497 let force_serial = false;
1498 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1499 }
1500
1501 fn automatic_gpu_prefix(&self) -> Option<usize> {
1504 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1505 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1506 }
1507
1508 pub fn prefill_chunk(&self) -> usize {
1512 let env = env_prefill_chunk();
1513 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1514 return prefill_chunk_rule(env, ChunkHost::here(), false);
1515 }
1516 prefill_chunk_rule(
1517 None,
1518 ChunkHost::Other,
1519 self.chunk_stack_facts().dense_on_discrete(),
1520 )
1521 }
1522
1523 fn per_token_stack(&self) -> bool {
1527 self.g3n.is_some() || self.g4e.is_some()
1528 }
1529
1530 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1531 let plain_dense = !self.weights.layers.is_empty()
1532 && !self.per_token_stack()
1533 && self.dsv4.is_none()
1534 && self.dsv41.is_none()
1535 && self.qwen4_exp.is_none()
1536 && self.weights.layers.iter().all(|lw| {
1537 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1538 });
1539 let gpu_on = crate::gpu::enabled();
1540 ChunkStackFacts {
1541 plain_dense,
1542 discrete: gpu_on && crate::gpu::discrete(),
1543 gpu_on,
1544 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1547 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1548 multi_gpu: self.gpu_plan.is_some(),
1549 o1: self.o1_active(),
1550 }
1551 }
1552}
1553
1554pub fn prefill_chunk() -> usize {
1563 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1564}
1565
1566fn env_prefill_chunk() -> Option<usize> {
1567 std::env::var("CMF_PREFILL_CHUNK")
1568 .ok()
1569 .and_then(|v| v.parse::<usize>().ok())
1570}
1571
1572#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1574enum ChunkHost {
1575 Macos,
1576 Aarch64,
1578 Other,
1580}
1581
1582impl ChunkHost {
1583 fn here() -> Self {
1584 if cfg!(target_os = "macos") {
1585 ChunkHost::Macos
1586 } else if cfg!(target_arch = "aarch64") {
1587 ChunkHost::Aarch64
1588 } else {
1589 ChunkHost::Other
1590 }
1591 }
1592}
1593
1594const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1601
1602fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1608 if let Some(n) = env {
1609 return n.max(1);
1610 }
1611 match host {
1612 ChunkHost::Macos => 512,
1613 ChunkHost::Aarch64 => 256,
1616 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1617 ChunkHost::Other => 48,
1618 }
1619}
1620
1621#[derive(Clone, Copy, Debug, Default)]
1623struct ChunkStackFacts {
1624 plain_dense: bool,
1627 discrete: bool,
1629 gpu_on: bool,
1631 capacity_split: bool,
1633 multi_gpu: bool,
1635 o1: bool,
1637}
1638
1639impl ChunkStackFacts {
1640 fn dense_on_discrete(self) -> bool {
1641 self.plain_dense
1642 && self.discrete
1643 && self.gpu_on
1644 && !self.capacity_split
1645 && !self.multi_gpu
1646 && !self.o1
1647 }
1648}
1649
1650#[inline]
1656fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1657 if end <= start || start >= input_len {
1658 return 0;
1659 }
1660 let rows = (end.min(input_len) - start).min(input_len - start);
1661 if end < input_len {
1662 rows
1663 } else {
1664 rows.saturating_sub(1)
1665 }
1666}
1667
1668pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1673pub(crate) struct ReuseLayer {
1674 pub full: bool,
1677 pub host_rows: usize,
1680 pub device_rows: Option<usize>,
1682 pub device_state: bool,
1684}
1685
1686#[derive(Debug, Clone, PartialEq, Eq)]
1688pub(crate) enum ReusePlan {
1689 Ready,
1691 Pull(Vec<(usize, usize, usize)>),
1694 Fresh,
1696}
1697
1698pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1708 let mut pulls = Vec::new();
1709 for (li, l) in layers.iter().enumerate() {
1710 if !l.full {
1711 if l.device_state {
1712 return ReusePlan::Fresh;
1713 }
1714 continue;
1715 }
1716 if l.host_rows == reuse_from {
1717 continue;
1718 }
1719 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1720 pulls.push((li, l.host_rows, reuse_from));
1721 continue;
1722 }
1723 return ReusePlan::Fresh;
1724 }
1725 if pulls.is_empty() {
1726 ReusePlan::Ready
1727 } else {
1728 ReusePlan::Pull(pulls)
1729 }
1730}
1731
1732impl Pipeline {
1733 fn reset_fresh_mtp_draft_state(&mut self) {
1736 self.draft_full_streak = 0;
1737 }
1738
1739 fn clear_sequence_state(&mut self) {
1747 #[cfg(target_os = "macos")]
1750 let _ = crate::gpu_metal::wait_replay();
1751 self.kv_cache.clear();
1752 self.clear_history();
1755 self.graph_logits = None;
1756 if let Some(b) = &mut self.dsv41 {
1757 b.3.clear();
1758 }
1759 crate::gpu::graph_kv_reset(self.graph_kv_id);
1760 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1765 }
1766
1767 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1774 if self.graph_prefill_preferred() {
1775 return true;
1776 }
1777 let kv_id = self.graph_kv_id;
1778 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1779 .map(|li| {
1780 let full = matches!(
1781 self.weights.layers[self.phys_layer(li)].attn,
1782 AttnKind::Full { .. }
1783 );
1784 ReuseLayer {
1785 full,
1786 host_rows: self.kv_cache.layers[li].pos_len(),
1789 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1790 device_state: crate::gpu::graph_state_resident(kv_id, li),
1791 }
1792 })
1793 .collect();
1794 if layers
1798 .iter()
1799 .all(|l| l.device_rows.is_none() && !l.device_state)
1800 {
1801 return true;
1802 }
1803 let plan = kv_reuse_plan(reuse_from, &layers);
1804 let (what, rows, n) = match &plan {
1805 ReusePlan::Ready => ("host ready", 0, 0),
1806 ReusePlan::Fresh => ("fresh", 0, 0),
1807 ReusePlan::Pull(p) => (
1808 "pull",
1809 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1810 p.len(),
1811 ),
1812 };
1813 let t0 = std::time::Instant::now();
1814 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1815 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1816 eprintln!(
1817 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1818 if ok { "" } else { " (failed → fresh)" },
1819 t0.elapsed().as_secs_f64() * 1e3
1820 );
1821 }
1822 ok
1823 }
1824
1825 fn apply_kv_reuse_plan(
1826 &mut self,
1827 reuse_from: usize,
1828 plan: ReusePlan,
1829 layers: &[ReuseLayer],
1830 ) -> bool {
1831 let kv_id = self.graph_kv_id;
1832 match plan {
1833 ReusePlan::Fresh => return false,
1834 ReusePlan::Ready => {}
1835 ReusePlan::Pull(pulls) => {
1836 let (nkv, hd) = {
1841 let c = &self.kv_cache.layers[pulls[0].0];
1842 (c.num_kv_heads, c.head_dim)
1843 };
1844 let uniform = pulls.iter().all(|&(li, _, _)| {
1845 let c = &self.kv_cache.layers[li];
1846 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1847 });
1848 let batched = if uniform {
1849 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1850 } else {
1851 None
1852 };
1853 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1854 Some(rows) => rows,
1855 None => {
1856 let mut rows = Vec::with_capacity(pulls.len());
1857 for &(li, from, to) in &pulls {
1858 let (lnkv, lhd) = {
1859 let c = &self.kv_cache.layers[li];
1860 (c.num_kv_heads, c.head_dim)
1861 };
1862 let Some((k, v, first_valid)) =
1863 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1864 else {
1865 return false;
1866 };
1867 let need_from = match self.layer_window(li) {
1871 Some(w) => from.max((to + 1).saturating_sub(w)),
1872 None => from,
1873 };
1874 if first_valid > need_from {
1875 return false;
1876 }
1877 rows.push((k, v));
1878 }
1879 rows
1880 }
1881 };
1882 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1883 let cache = &mut self.kv_cache.layers[li];
1884 let row = cache.num_kv_heads * cache.head_dim;
1885 for p in 0..to - from {
1886 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1887 }
1888 if cache.pos_len() != to {
1889 return false;
1890 }
1891 }
1892 }
1893 }
1894 for (li, l) in layers.iter().enumerate() {
1898 if l.full
1899 && l.device_rows.is_some_and(|d| d > reuse_from)
1900 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1901 {
1902 return false;
1903 }
1904 }
1905 true
1906 }
1907
1908 fn finish_generation(
1914 &mut self,
1915 mtp: &mut Option<MtpModule>,
1916 router: &mut Option<crate::swarm::DynRouter>,
1917 clear_sequence: bool,
1918 ) {
1919 if router.is_some() {
1923 let _ = self.set_active_skill(None);
1924 }
1925 #[cfg(target_os = "macos")]
1932 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1933 if clear_sequence {
1934 self.clear_sequence_state();
1935 if let Some(m) = mtp.as_mut() {
1936 m.kv.clear();
1942 }
1943 if let Some(m) = self.mtp.as_mut() {
1944 m.kv.clear();
1948 }
1949 }
1950 self.graph_want_logits = false;
1951 self.graph_head_required = false;
1952 self.graph_logits = None;
1953 self.graph_failed
1954 .store(false, std::sync::atomic::Ordering::Relaxed);
1955 self.cancel
1956 .store(false, std::sync::atomic::Ordering::Relaxed);
1957 self.dyn_router = router.take().or(self.dyn_router.take());
1958 self.mtp = mtp.take().or(self.mtp.take());
1959 self.mtp_graph_mode = None;
1960 self.spec_forced = None;
1961 }
1962
1963 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1967 if self
1968 .graph_failed
1969 .swap(false, std::sync::atomic::Ordering::Relaxed)
1970 {
1971 self.cancel
1972 .store(false, std::sync::atomic::Ordering::Relaxed);
1973 self.clear_sequence_state();
1974 self.graph_logits = None;
1975 self.graph_want_logits = false;
1976 self.graph_head_required = false;
1977 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1978 }
1979 Ok(())
1980 }
1981
1982 #[cfg(target_os = "macos")]
1983 fn fail_metal_graph(&mut self, reason: &str) {
1984 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1985 self.clear_sequence_state();
1986 self.graph_logits = None;
1987 self.graph_failed
1988 .store(true, std::sync::atomic::Ordering::Relaxed);
1989 self.cancel
1990 .store(true, std::sync::atomic::Ordering::Relaxed);
1991 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1992 }
1993
1994 fn nll_begin(&mut self) -> Result<(), String> {
1999 if self
2000 .graph_failed
2001 .swap(false, std::sync::atomic::Ordering::Relaxed)
2002 {
2003 self.cancel
2004 .store(false, std::sync::atomic::Ordering::Relaxed);
2005 self.clear_sequence_state();
2006 self.graph_logits = None;
2007 self.graph_want_logits = false;
2008 self.graph_head_required = false;
2009 return Err("GPU graph failed before NLL scoring".to_string());
2010 }
2011 self.clear_sequence_state();
2012 self.graph_logits = None;
2013 self.graph_want_logits = false;
2014 self.graph_head_required = false;
2015 Ok(())
2016 }
2017
2018 fn nll_end(&mut self) {
2022 self.clear_sequence_state();
2023 self.graph_logits = None;
2024 self.graph_want_logits = false;
2025 self.graph_head_required = false;
2026 self.graph_failed
2027 .store(false, std::sync::atomic::Ordering::Relaxed);
2028 }
2029
2030 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2033 #[cfg(test)]
2034 if self.nll_test_fail_at == Some(pos) {
2035 self.nll_test_fail_at = None;
2036 self.graph_failed
2037 .store(true, std::sync::atomic::Ordering::Relaxed);
2038 self.cancel
2039 .store(true, std::sync::atomic::Ordering::Relaxed);
2040 }
2041 if self
2042 .graph_failed
2043 .swap(false, std::sync::atomic::Ordering::Relaxed)
2044 {
2045 self.cancel
2046 .store(false, std::sync::atomic::Ordering::Relaxed);
2047 self.clear_sequence_state();
2048 self.graph_logits = None;
2049 self.graph_want_logits = false;
2050 return Err(format!(
2051 "GPU graph failed during NLL {phase} at position {pos}"
2052 ));
2053 }
2054 Ok(())
2055 }
2056
2057 #[inline]
2061 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2062 virtual_idx % self.physical_layers
2063 }
2064
2065 #[inline]
2068 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2069 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2070 }
2071
2072 #[allow(clippy::too_many_arguments)]
2074
2075 #[cfg(target_os = "macos")]
2094 fn graph_prefill_preferred(&self) -> bool {
2095 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2096 if !crate::gpu::enabled_here()
2097 || !graph_force
2098 || std::env::var("CMF_GPU_BLOCK")
2099 .map(|v| v == "0")
2100 .unwrap_or(false)
2101 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2104 {
2105 return false;
2106 }
2107 self.weights
2108 .layers
2109 .iter()
2110 .any(|lw| {
2111 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2112 })
2113 }
2114
2115 #[cfg(not(target_os = "macos"))]
2122 fn batch_prefix_prefill(&self) -> bool {
2123 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2124 Ok("0") => return false,
2125 Ok("1") => true,
2126 _ => false,
2127 };
2128 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2129 && crate::gpu::enabled_here()
2130 && !self.graph_refused()
2131 && (forced || self.graph_attn_decline_reason().is_some())
2132 && self.wgpu_graph_attn_decline().is_none()
2133 && self.attn_softcap == 0.0
2134 && self
2135 .weights
2136 .layers
2137 .iter()
2138 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2139 && self.automatic_gpu_prefix().is_some()
2140 }
2141
2142 fn g4_batch_prefill(&self) -> bool {
2148 #[cfg(target_os = "macos")]
2149 {
2150 false
2151 }
2152 #[cfg(not(target_os = "macos"))]
2153 {
2154 !self.weights.layers.is_empty()
2158 && !self.per_token_stack()
2159 && (self.global_attn.is_some() || self.attn_v_norm)
2160 && crate::gpu::g4_batch_on()
2161 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2162 && crate::gpu::enabled_here()
2163 && crate::gpu::discrete()
2164 && !self.graph_refused()
2165 && !self.o1_active()
2166 && self.attn_softcap == 0.0
2167 && self.wgpu_graph_attn_decline().is_none()
2168 && self.weights.layers.iter().all(|lw| {
2169 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2170 })
2171 && self.automatic_gpu_prefix().is_none()
2172 }
2173 }
2174
2175 #[cfg(not(target_os = "macos"))]
2182 fn g4_batch_ingest(
2183 &mut self,
2184 ids: &[u32],
2185 pos0: usize,
2186 keep_all: bool,
2187 ) -> Result<(usize, Vec<f32>), String> {
2188 let hs = self.hidden_size;
2189 let chunk = self.generation_batch_k().max(1);
2190 let end_all = pos0 + ids.len();
2191 let mut pos = pos0;
2192 let mut out = Vec::new();
2193 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2194 let end = (pos + chunk).min(end_all);
2195 let bk = end - pos;
2196 let mut hiddens = vec![0f32; bk * hs];
2197 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2198 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2199 }
2200 let positions: Vec<usize> = (pos..end).collect();
2201 let t_chunk = std::time::Instant::now();
2202 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2203 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2204 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2205 eprintln!(
2206 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2207 bk as f64 / (ms / 1000.0)
2208 );
2209 }
2210 match outcome {
2211 crate::gpu::BatchGraphOutcome::Completed => {
2212 if keep_all {
2213 out.extend_from_slice(&hiddens);
2214 } else {
2215 out = hiddens[(bk - 1) * hs..].to_vec();
2216 }
2217 pos = end;
2218 }
2219 crate::gpu::BatchGraphOutcome::Failed => {
2220 return Err("Gemma-4 batched prefill failed after admission".into());
2221 }
2222 crate::gpu::BatchGraphOutcome::Declined => {
2223 #[cfg(feature = "gpu")]
2224 if pos > pos0 {
2225 self.pull_lagging_host_kv(0, self.num_layers, pos);
2226 }
2227 break;
2228 }
2229 }
2230 }
2231 Ok((pos - pos0, out))
2232 }
2233
2234 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2239 #[cfg(not(target_os = "macos"))]
2240 {
2241 if ids.is_empty() || !self.g4_batch_prefill() {
2242 return None;
2243 }
2244 self.clear_sequence_state();
2245 match self.g4_batch_ingest(ids, 0, true) {
2246 Ok((n, rows)) if n == ids.len() => Some(rows),
2247 _ => {
2248 self.clear_sequence_state();
2249 None
2250 }
2251 }
2252 }
2253 #[cfg(target_os = "macos")]
2254 {
2255 let _ = ids;
2256 None
2257 }
2258 }
2259
2260 #[cfg(not(target_os = "macos"))]
2261 fn graph_prefill_preferred(&self) -> bool {
2262 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2270 if !graph_on || !crate::gpu::enabled_here() {
2271 return false;
2272 }
2273 if self.embryo_resident_eligible() {
2277 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2282 }
2283 if self.o1_active() {
2297 return false;
2298 }
2299 if self.wgpu_graph_attn_decline().is_some() {
2303 return false;
2304 }
2305 if self
2306 .weights
2307 .layers
2308 .iter()
2309 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2310 {
2311 return true;
2312 }
2313 self.weights
2324 .layers
2325 .iter()
2326 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2327 && self.automatic_gpu_prefix().is_none()
2328 }
2329
2330 #[cfg(target_os = "macos")]
2331 fn q1_graph_gpu(
2332 &mut self,
2333 start: usize,
2334 upto: Option<usize>,
2335 position: usize,
2336 h: &mut [f32],
2337 ) -> usize {
2338 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2340 const MOE_WORKING_SET_REASON: &str =
2341 "model weights exceed Metal working set for native MoE graph";
2342 if self
2346 .graph_declines
2347 .borrow()
2348 .iter()
2349 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2350 {
2351 return start;
2352 }
2353 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2354 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2356 || !crate::gpu::enabled_here()
2357 || !graph_force
2358 || std::env::var("CMF_GPU_BLOCK")
2359 .map(|v| v == "0")
2360 .unwrap_or(false)
2361 {
2362 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2363 eprintln!(
2364 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2365 self.attn_softcap > 0.0,
2366 crate::gpu::enabled_here(),
2367 graph_force,
2368 );
2369 }
2370 if self.graph_head_required {
2371 self.fail_metal_graph("native graph front gate refused");
2372 }
2373 return start;
2374 }
2375 let gemma4 = self.metal_gemma4();
2385 let swa_graph = self.metal_graph_swa() || gemma4;
2386 let graph_attn_reason = self.graph_attn_decline_reason();
2393 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2394 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2395 if (self.swa.is_some() && !swa_graph)
2396 || (self.global_attn.is_some() && !gemma4)
2397 || self.attention_heads_per_layer.is_some()
2398 || (self.attn_v_norm && !gemma4)
2399 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2402 || self.weights.layers.iter().any(|lw| {
2403 ((lw.attn_out_norm.is_some()
2404 || lw.ffn_out_norm.is_some()
2405 || lw.layer_scale.is_some())
2406 && !gemma4)
2407 || matches!(&lw.ffn, FfnKind::Dense(d)
2408 if !(matches!(d.act, Act::Silu | Act::Gelu)
2409 || (gemma4 && d.act == Act::GeluTanh)))
2410 })
2411 {
2412 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2415 self.note_graph_decline("metal block graph", reason);
2416 }
2417 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2418 eprintln!(
2419 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2420 self.swa.is_some(),
2421 self.global_attn.is_some(),
2422 self.attention_heads_per_layer.is_some(),
2423 self.attn_v_norm,
2424 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2425 );
2426 }
2427 if self.graph_head_required {
2428 self.fail_metal_graph("native graph architecture gate refused");
2429 }
2430 return start;
2431 }
2432 let limit = upto
2435 .map(|u| u + 1)
2436 .unwrap_or(self.num_layers)
2437 .min(self.num_layers);
2438
2439 enum Item<'a> {
2440 Gdn {
2441 run: Vec<GdnGpuLayer<'a>>,
2442 first: usize,
2443 },
2444 Attn {
2445 l: AttnGpuLayer<'a>,
2446 li: usize,
2447 q_norm: Option<&'a [f32]>,
2448 k_norm: Option<&'a [f32]>,
2449 output_gate: bool,
2450 proj_gate: Option<(&'a QTensor, bool)>,
2454 head_gate_w: Option<&'a [f32]>,
2457 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2458 full_gpu: bool,
2461 },
2462 }
2463
2464 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2471 let attend_contract = attend_mode != "0"
2472 && attend_mode != "off"
2473 && self.head_dim % 4 == 0
2474 && self.head_dim <= 256
2475 && (self.rotary_dim == 0
2479 || (self.rotary_dim >= 2
2480 && self.rotary_dim <= self.head_dim
2481 && (self.rotary_dim / 2) % 32 == 0))
2482 && self.num_kv_heads > 0
2483 && self.num_heads % self.num_kv_heads == 0;
2484
2485 let mut plan: Vec<Item> = Vec::new();
2486 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2487 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2489 let mut scan = start;
2490 while scan < limit {
2491 let lw = &self.weights.layers[self.phys_layer(scan)];
2492 let ffn = match &lw.ffn {
2493 FfnKind::Dense(d) if d.segs.is_empty() => {
2494 let (Some(g), Some(u), Some(dn)) = (
2495 d.gate_proj.metal_graph_parts(),
2496 d.up_proj.metal_graph_parts(),
2497 d.down_proj.metal_graph_parts(),
2498 ) else {
2499 if block_diag {
2500 eprintln!(
2501 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2502 );
2503 }
2504 break;
2505 };
2506 MetalFfn::Dense {
2507 gate: g,
2508 up: u,
2509 down: dn,
2510 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2513 }
2514 }
2515 FfnKind::Moe(m) => {
2516 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2522 {
2523 MetalFfn::FlintMoe(flint)
2524 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2525 MetalFfn::Moe(moe)
2526 } else {
2527 if block_diag {
2528 eprintln!(
2529 "block-graph: L{scan} MoE outside the graph contract — run ends"
2530 );
2531 }
2532 break;
2533 };
2534 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2535 model_ref.get_or_insert_with(|| model.clone());
2536 }
2537 ffn
2538 }
2539 _ => {
2540 if block_diag {
2541 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2542 }
2543 break;
2544 }
2545 };
2546 match &lw.attn {
2547 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2548 let parts = (
2549 w.in_proj_qkv.metal_graph_parts(),
2550 w.in_proj_z.metal_graph_parts(),
2551 w.in_proj_a.f32_parts(),
2552 w.in_proj_b.f32_parts(),
2553 w.out_proj.metal_graph_parts(),
2554 );
2555 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2556 if block_diag {
2557 eprintln!(
2558 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2559 w.in_proj_qkv.metal_graph_parts().is_some(),
2560 w.in_proj_z.metal_graph_parts().is_some(),
2561 w.in_proj_a.f32_parts().is_some(),
2562 w.in_proj_b.f32_parts().is_some(),
2563 w.out_proj.metal_graph_parts().is_some(),
2564 );
2565 }
2566 break;
2567 };
2568 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2569 model_ref.get_or_insert_with(|| model.clone());
2570 }
2571 let gl = GdnGpuLayer {
2572 attn_norm: &lw.input_norm,
2573 post_norm: &lw.post_norm,
2574 qkv,
2575 z,
2576 a,
2577 b,
2578 out,
2579 ffn,
2580 conv1d: &w.conv1d,
2581 a_log: &w.a_log,
2582 dt_bias: &w.dt_bias,
2583 gnorm: &w.norm,
2584 };
2585 match plan.last_mut() {
2586 Some(Item::Gdn { run, .. }) => run.push(gl),
2587 _ => plan.push(Item::Gdn {
2588 run: vec![gl],
2589 first: scan,
2590 }),
2591 }
2592 }
2593 AttnKind::Full {
2594 wq,
2595 wk,
2596 wv,
2597 wo,
2598 q_norm,
2599 k_norm,
2600 output_gate,
2601 softplus_gate,
2602 bias,
2603 } if (!self.kv_cache.layers[scan].o1_sealed()
2604 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2609 && softplus_gate
2613 .as_ref()
2614 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2615 {
2616 let parts = (
2617 wq.metal_graph_parts(),
2618 wk.metal_graph_parts(),
2619 wv.metal_graph_parts(),
2620 wo.metal_graph_parts(),
2621 );
2622 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2623 break;
2624 };
2625 if let QTensor::Mapped { model, .. } = wq {
2626 model_ref.get_or_insert_with(|| model.clone());
2627 }
2628 let cache = &self.kv_cache.layers[scan];
2629 let o1_metal = cache.o1.is_some()
2633 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2634 && cache.o1_views().is_some();
2635 let head_gate_w = softplus_gate
2639 .as_ref()
2640 .and_then(|(g, _)| g.f32_parts())
2641 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2642 .map(|(d, _, _)| d);
2643 let (nkv_g, hd_g) = if gemma4 {
2646 let (a, b, _) = self.layer_geom(scan);
2647 (a, b)
2648 } else {
2649 (self.num_kv_heads, self.head_dim)
2650 };
2651 let full_gpu = attend_contract
2652 && softplus_gate.is_none() == head_gate_w.is_none()
2653 && cache.mode == crate::kv_cache::KvMode::F32
2654 && (cache.o1.is_none() || o1_metal)
2655 && bias.is_none()
2656 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2657 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2658 && pk.1 == nkv_g * hd_g
2659 && pv.1 == nkv_g * hd_g
2660 && po.2 == self.num_heads * hd_g;
2661 plan.push(Item::Attn {
2662 l: AttnGpuLayer {
2663 attn_norm: &lw.input_norm,
2664 post_norm: &lw.post_norm,
2665 wq: pq,
2666 wk: pk,
2667 wv: pv,
2668 wo: po,
2669 ffn,
2670 attn_out_norm: lw.attn_out_norm.as_deref(),
2671 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2672 layer_scale: lw.layer_scale,
2673 },
2674 li: scan,
2675 q_norm: q_norm.as_deref(),
2676 k_norm: k_norm.as_deref(),
2677 output_gate: *output_gate,
2678 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2679 head_gate_w,
2680 bias: bias
2681 .as_ref()
2682 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2683 full_gpu,
2684 });
2685 }
2686 _ => break,
2687 }
2688 scan += 1;
2689 }
2690 let Some(model) = model_ref else {
2691 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2692 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2693 }
2694 if self.graph_head_required {
2695 self.fail_metal_graph("native graph has no mapped model reference");
2696 }
2697 return start;
2698 };
2699 if plan.is_empty() {
2700 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2701 eprintln!("q1-graph: empty plan at layer {start}");
2702 }
2703 if self.graph_head_required {
2704 self.fail_metal_graph("native graph plan is empty");
2705 }
2706 return start;
2707 }
2708 let has_moe = plan.iter().any(|it| match it {
2709 Item::Gdn { run, .. } => run
2710 .iter()
2711 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2712 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2713 });
2714 let has_flint_moe = plan.iter().any(|it| match it {
2715 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2716 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2717 });
2718 let metal_working_set = crate::gpu_metal::working_set_bytes();
2719 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2720 let model_bytes = model.primary_bytes().len() as u64;
2721 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2727 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2728 let pager_cap = if !has_moe {
2737 None
2738 } else if has_flint_moe {
2739 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2743 } else {
2744 crate::gpu_metal::moe_pager_cap_bytes()
2745 };
2746 let pager_eligible =
2747 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2748 let mut pager_span: Option<(usize, usize)> = None;
2749 if pager_eligible {
2750 let cap = pager_cap.expect("checked above");
2751 let tail_span = (self.graph_want_logits
2755 && upto.is_none()
2756 && scan == self.num_layers
2757 && std::env::var("CMF_GPU_LMHEAD")
2758 .map(|v| v != "0")
2759 .unwrap_or(true))
2760 .then(|| self.weights.lm_head.metal_graph_parts())
2761 .flatten()
2762 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2763 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2764 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2765 Some(match span {
2766 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2767 None => (lo, hi),
2768 })
2769 };
2770 let page_span = |span: (usize, usize), include_tail: bool| {
2771 let (mut lo, mut hi) = span;
2772 if include_tail {
2773 if let Some((tail_lo, tail_hi)) = tail_span {
2774 lo = lo.min(tail_lo);
2775 hi = hi.max(tail_hi);
2776 }
2777 }
2778 crate::gpu_metal::page_round_span(lo, hi)
2779 };
2780 let mut selected = 0usize;
2781 let mut selected_span: Option<(usize, usize)> = None;
2782 let mut gdn_prefix: Option<(usize, usize)> = None;
2790 'items: for (i, item) in plan.iter().enumerate() {
2791 match item {
2792 Item::Attn { li, .. } => {
2793 let Some(span) = add_layer_span(selected_span, *li) else {
2794 break;
2795 };
2796 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2797 break;
2798 };
2799 if page_hi - page_lo > cap {
2800 break;
2801 }
2802 selected = i + 1;
2803 selected_span = Some((page_lo, page_hi));
2804 }
2805 Item::Gdn { first, run } => {
2806 let mut whole = None;
2813 for take in 1..=run.len() {
2814 whole = add_layer_span(whole, *first + take - 1);
2815 if whole.is_none() {
2816 break;
2817 }
2818 }
2819 let whole_fits_empty_page = whole
2820 .and_then(|span| page_span(span, false))
2821 .is_some_and(|(lo, hi)| hi - lo <= cap);
2822 if whole_fits_empty_page {
2823 let mut span = selected_span;
2824 for take in 1..=run.len() {
2825 let Some(next) = add_layer_span(span, *first + take - 1) else {
2826 break 'items;
2827 };
2828 span = Some(next);
2829 }
2830 let Some(span) = span else {
2831 break;
2832 };
2833 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2834 else {
2835 break;
2836 };
2837 if page_hi - page_lo > cap {
2838 break;
2839 }
2840 selected = i + 1;
2841 selected_span = Some((page_lo, page_hi));
2842 continue;
2843 }
2844 if selected != 0 {
2845 break;
2846 }
2847 let mut span = None;
2848 let mut best: Option<(usize, (usize, usize))> = None;
2849 for take in 1..=run.len() {
2850 let Some(next) = add_layer_span(span, *first + take - 1) else {
2851 break;
2852 };
2853 let Some((page_lo, page_hi)) = page_span(next, false) else {
2856 break;
2857 };
2858 if page_hi - page_lo > cap {
2859 break;
2860 }
2861 span = Some((page_lo, page_hi));
2862 best = Some((take, (page_lo, page_hi)));
2863 }
2864 let Some((take, page)) = best else {
2865 break;
2866 };
2867 selected = i + 1;
2868 selected_span = Some(page);
2869 if take != run.len() {
2870 gdn_prefix = Some((i, take));
2871 break 'items;
2872 }
2873 }
2874 }
2875 }
2876 if selected != 0 {
2877 if let Some((item, take)) = gdn_prefix {
2878 match &mut plan[item] {
2879 Item::Gdn { run, .. } => run.truncate(take),
2880 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2881 }
2882 }
2883 let (page_lo, page_hi) = selected_span.expect("selected page span");
2884 tracing::info!(
2885 page_start = page_lo,
2886 page_end = page_hi,
2887 page_mb = (page_hi - page_lo) / (1024 * 1024),
2888 cap_mb = cap / (1024 * 1024),
2889 graph_items = selected,
2890 "native Metal MoE graph uses transient bounded weight page"
2891 );
2892 plan.truncate(selected);
2893 pager_span = Some((page_lo, page_hi));
2894 } else {
2895 tracing::warn!(
2896 cap_mb = cap / (1024 * 1024),
2897 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2898 );
2899 }
2900 }
2901 if has_moe && needs_bounded_pager && pager_span.is_none() {
2910 tracing::info!(
2911 model_bytes,
2912 metal_working_set,
2913 "native Metal MoE graph declined before allocation"
2914 );
2915 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2916 if self.graph_head_required {
2917 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2918 }
2919 return start;
2920 }
2921 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2922 let dev_attend = attend_contract
2923 && (self.head_dim <= 128
2924 || has_moe
2925 || (self.head_dim <= 256 && has_gdn)
2931 || (self.head_dim <= 256 && swa_graph)
2935 || attend_mode == "force"
2936 || attend_mode == "256");
2937 if !dev_attend {
2938 for it in &mut plan {
2939 if let Item::Attn { li, full_gpu, .. } = it {
2940 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2943 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2944 if !keep_o1 {
2945 *full_gpu = false;
2946 }
2947 }
2948 }
2949 }
2950 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2951 use std::sync::atomic::{AtomicBool, Ordering};
2952 static SAID: AtomicBool = AtomicBool::new(false);
2953 if !SAID.swap(true, Ordering::Relaxed) {
2954 let fg = plan
2955 .iter()
2956 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2957 .count();
2958 let att = plan
2959 .iter()
2960 .filter(|it| matches!(it, Item::Attn { .. }))
2961 .count();
2962 eprintln!(
2963 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2964 plan.len(),
2965 self.head_dim,
2966 self.rotary_dim,
2967 self.num_kv_heads,
2968 self.num_heads,
2969 );
2970 }
2971 }
2972 let dims = GraphDims {
2973 hidden: self.hidden_size,
2974 eps: self.rms_eps as f32,
2975 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2976 };
2977 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2981 crate::gpu_metal::MoePagerPageScope::new(
2982 _mt0,
2983 position,
2984 start,
2985 page_lo,
2986 page_hi,
2987 pager_cap.unwrap_or(0),
2991 )
2992 });
2993 let graph_new = match pager_span {
2994 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
2995 None => TokenGraph::new(&model, dims, h),
2996 };
2997 let Some(mut graph) = graph_new else {
2998 if self.graph_head_required {
2999 self.fail_metal_graph("native TokenGraph allocation refused");
3000 }
3001 return start;
3002 };
3003 if swa_graph && self.head_dim > 128 {
3004 graph.set_attend_blk_from(64);
3010 }
3011 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3012 nv: cfg.num_v_heads,
3013 nk: cfg.num_k_heads,
3014 dk: cfg.key_head_dim,
3015 dv: cfg.value_head_dim,
3016 kk: cfg.conv_kernel,
3017 hidden: self.hidden_size,
3018 inter: self.intermediate_size,
3019 c_dim: cfg.conv_dim(),
3020 eps: cfg.rms_eps as f32,
3021 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3022 });
3023 let mut valid = 0usize;
3027 let mut end = start;
3028 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3030 static ONCE: std::sync::Once = std::sync::Once::new();
3031 ONCE.call_once(|| {
3032 for it in &plan {
3033 match it {
3034 Item::Gdn { first, run } => {
3035 eprintln!("plan: Gdn first={first} len={}", run.len())
3036 }
3037 Item::Attn { li, full_gpu, .. } => {
3038 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3039 }
3040 }
3041 }
3042 });
3043 }
3044 for item in &plan {
3045 let ok = match item {
3046 Item::Gdn { run, .. } => gcfg
3047 .as_ref()
3048 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3049 .unwrap_or(false),
3050 Item::Attn { l, .. } => graph.attn_ok(l),
3051 };
3052 if !ok {
3053 if block_diag {
3054 eprintln!(
3055 "block-graph: plan item {} ({}) failed graph preflight",
3056 valid,
3057 match item {
3058 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3059 Item::Attn { li, .. } => format!("Attn L{li}"),
3060 }
3061 );
3062 }
3063 break;
3064 }
3065 valid += 1;
3066 end += match item {
3067 Item::Gdn { run, .. } => run.len(),
3068 Item::Attn { .. } => 1,
3069 };
3070 }
3071 plan.truncate(valid);
3072 if plan.is_empty() {
3073 if self.graph_head_required {
3074 self.fail_metal_graph("native graph preflight produced no valid items");
3075 }
3076 return start;
3077 }
3078
3079 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3080 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3081 return start;
3082 }
3083
3084 let one_pass = |t: (usize, usize, usize)| {
3093 use cortiq_core::TensorDtype as D;
3094 matches!(
3095 model.tensors[t.0].dtype,
3096 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3097 )
3098 };
3099 let dense_fast = plan.iter().all(|it| match it {
3100 Item::Attn {
3101 l, li, full_gpu, ..
3102 } => {
3103 *full_gpu
3104 && self.kv_cache.layers[*li].o1.is_none()
3105 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3106 && match l.ffn {
3107 MetalFfn::Dense { gate, up, down, .. } => {
3108 one_pass(gate) && one_pass(up) && one_pass(down)
3109 }
3110 _ => false,
3111 }
3112 }
3113 Item::Gdn { .. } => false,
3114 });
3115 let moe_plan = !dense_fast
3119 && plan.iter().all(|it| match it {
3120 Item::Attn {
3121 l, li, full_gpu, ..
3122 } => {
3123 *full_gpu
3124 && self.kv_cache.layers[*li].o1.is_none()
3125 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3126 }
3127 Item::Gdn { .. } => false,
3128 });
3129 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3130 if block_diag {
3131 use std::sync::atomic::{AtomicBool, Ordering};
3132 static SAID_AB: AtomicBool = AtomicBool::new(false);
3133 if !SAID_AB.swap(true, Ordering::Relaxed) {
3134 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3135 }
3136 }
3137 let _mv_fast = match ab {
3138 Some((bits, _)) => {
3139 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3140 crate::gpu_metal::MvFastGuard::set_raw(bits)
3141 }
3142 None if moe_plan => {
3143 graph.set_dense_concurrent(
3144 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3145 );
3146 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3147 }
3148 None => {
3149 graph.set_dense_concurrent(dense_fast);
3150 let q8r4 = if swa_graph {
3153 crate::gpu_metal::DENSE_Q8R4
3154 } else {
3155 0
3156 };
3157 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3158 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3159 } else {
3160 0
3161 })
3162 }
3163 };
3164
3165 let pool = self.pool.clone();
3168 let (nh, nkv, hd, hs, eps) = (
3169 self.num_heads,
3170 self.num_kv_heads,
3171 self.head_dim,
3172 self.hidden_size,
3173 self.rms_eps,
3174 );
3175 let norm_style = self.norm_style;
3176 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3177 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3178 let kv_id = self.graph_kv_id;
3179 let mut pending: Vec<(usize, usize)> = Vec::new();
3182 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3185 for item in &plan {
3186 let _xt0 = std::time::Instant::now();
3187 let _xkind: u32 = match item {
3188 Item::Gdn { .. } => 2,
3189 Item::Attn { .. } => 3,
3190 };
3191 if self.loop_final_norm {
3193 let item_start = match item {
3194 Item::Gdn { first, .. } => *first,
3195 Item::Attn { li, .. } => *li,
3196 };
3197 if item_start > start && self.is_loop_end(item_start - 1) {
3198 graph.encode_loop_norm(&self.weights.final_norm);
3199 }
3200 }
3201 match item {
3202 Item::Gdn { run, first } => {
3203 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3204 if l.linear_state.len() != want {
3205 l.linear_state = vec![0f32; want];
3206 }
3207 }
3208 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3209 .iter()
3210 .map(|l| l.linear_state.as_slice())
3211 .collect();
3212 let _ig = std::time::Instant::now();
3213 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3214 tracing::error!("q1 graph: GDN run refused after validation");
3216 return start;
3217 }
3218 graph.commit_kind = 2;
3221 graph.commit();
3222 crate::gpu::stageprof(0, _ig.elapsed());
3223 pending.push((*first, run.len()));
3224 }
3225 Item::Attn {
3226 l,
3227 li,
3228 q_norm,
3229 k_norm,
3230 output_gate,
3231 proj_gate,
3232 head_gate_w,
3233 bias,
3234 full_gpu,
3235 } => {
3236 let _ia = std::time::Instant::now();
3237 let inv_freq_l = self.layer_inv_freq(*li);
3243 let rd_l = self.layer_geom(*li).2;
3244 let window_l = self.layer_window(*li);
3245 let head_gate_w = *head_gate_w;
3246 let (nkv, hd) = if gemma4 {
3249 let (a, b, _) = self.layer_geom(*li);
3250 (a, b)
3251 } else {
3252 (nkv, hd)
3253 };
3254 if *full_gpu {
3256 let cache = &self.kv_cache.layers[*li];
3257 let o1p = if cache.o1.is_some() {
3258 match cache.o1_views() {
3259 Some(views) => Some(crate::gpu::O1AttnParams {
3260 views,
3261 epoch: self.o1_epoch,
3262 }),
3263 None => None,
3265 }
3266 } else {
3267 None
3268 };
3269 let o1_layer = cache.o1.is_some();
3270 if o1_layer && o1p.is_none() {
3271 }
3273 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3274 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3275 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3276 debug_assert!(
3279 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3280 );
3281 let p = crate::gpu::AttnDeviceParams {
3282 kv_id,
3283 layer: *li,
3284 nh,
3285 nkv,
3286 hd,
3287 rd: rd_l,
3288 position,
3289 scale: self.attn_scale,
3290 eps: eps as f32,
3291 gemma,
3292 late_qk_norm: self.qk_norm_after_rope,
3293 output_gate: *output_gate,
3294 q_norm: *q_norm,
3295 k_norm: *k_norm,
3296 inv_freq: &inv_freq_l,
3297 rope_scale: self.layer_rope_scale(*li),
3298 cpu_k,
3299 cpu_v,
3300 cpu_stored,
3301 cpu_gen: cache.generation(),
3302 o1: o1p,
3303 window: window_l,
3304 softmax_sink: cache.sinks.as_deref(),
3305 head_gate: head_gate_w,
3306 v_norm: self.attn_v_norm,
3307 };
3308 let o1_bad = o1_layer && p.o1.is_none();
3309 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3310 {
3311 if p.o1.is_none() {
3313 dev_attn.push((*li, nkv, hd));
3314 }
3315 graph.commit_kind = 3;
3316 graph.commit();
3317 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3321 continue;
3322 }
3323 }
3325 graph.encode_attn_prefix(l);
3326 if let Err(err) = graph.sync_checked() {
3327 self.fail_metal_graph(&err);
3328 return start;
3329 }
3330 if !pending.is_empty() {
3331 let idxs: Vec<usize> =
3332 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3333 let mut outs: Vec<&mut [f32]> = self
3334 .kv_cache
3335 .layers
3336 .iter_mut()
3337 .enumerate()
3338 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3339 .map(|(_, s)| s.linear_state.as_mut_slice())
3340 .collect();
3341 graph.read_states(&mut outs);
3342 }
3343 let mut q_raw = attention::take_buf(l.wq.1);
3344 let mut k = attention::take_buf(l.wk.1);
3345 let mut v = attention::take_buf(l.wv.1);
3346 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3347 let mut gate_raw = proj_gate.map(|(gp, _)| {
3350 let mut normed = attention::take_buf(hs);
3351 graph.read_normed(&mut normed);
3352 let mut raw = attention::take_buf(gp.rows());
3353 gp.matvec(&normed, &mut raw, pool.as_deref());
3354 attention::recycle_buf(&mut normed);
3355 raw
3356 });
3357 let cfg = QwenAttnCfg {
3358 num_heads: nh,
3359 num_kv_heads: nkv,
3360 head_dim: hd,
3361 hidden_size: hs,
3362 position,
3363 inv_freq: &inv_freq_l,
3364 rotary_dim: rd_l,
3365 scale: self.attn_scale,
3366 softcap: self.attn_softcap,
3367 window: window_l,
3368 v_norm: self.attn_v_norm,
3371 qk_norm_after_rope: self.qk_norm_after_rope,
3372 gate_sigmoid: self.proj_gate_sigmoid,
3373 q_norm: *q_norm,
3374 k_norm: *k_norm,
3375 output_gate: *output_gate,
3376 softplus_gate: None,
3377 rope_scale: self.layer_rope_scale(*li),
3378 bias: *bias,
3379 rms_eps: eps,
3380 norm_style,
3381 pool: pool.as_deref(),
3382 v_head_dim: hd,
3383 };
3384 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3387 || std::env::var("CMF_ATTN_DUMP").is_ok();
3388 let _ = full_gpu;
3389 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3390 let mut ao = attention::qwen_attention_core(
3391 q_raw,
3392 k,
3393 v,
3394 &mut self.kv_cache.layers[*li],
3395 &cfg,
3396 );
3397 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3401 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3402 let (cq, _cg, _ck, _cv) =
3403 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3404 let cache = &self.kv_cache.layers[*li];
3405 let n = cache.head_keys(0).len() / hd;
3406 let mut bytes: Vec<u8> = Vec::new();
3407 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3408 bytes.extend_from_slice(&v.to_le_bytes());
3409 }
3410 for v in &cq {
3411 bytes.extend_from_slice(&v.to_le_bytes());
3412 }
3413 for g in 0..nkv {
3414 for v in cache.head_keys(g) {
3415 bytes.extend_from_slice(&v.to_le_bytes());
3416 }
3417 }
3418 for g in 0..nkv {
3419 for v in cache.head_values(g) {
3420 bytes.extend_from_slice(&v.to_le_bytes());
3421 }
3422 }
3423 let _ =
3424 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3425 }
3426 }
3427 if let Some((qr0, k0, v0)) =
3428 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3429 {
3430 let (cq, _cg, ck, cv) =
3431 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3432 let mut h_now = vec![0f32; hs];
3433 graph.read_h(&mut h_now);
3434 let cache = &self.kv_cache.layers[*li];
3435 let n_after = cache.head_keys(0).len() / hd;
3436 let stored = n_after.saturating_sub(1);
3440 let cpu_k: Vec<&[f32]> = (0..nkv)
3441 .map(|g| &cache.head_keys(g)[..stored * hd])
3442 .collect();
3443 let cpu_v: Vec<&[f32]> = (0..nkv)
3444 .map(|g| &cache.head_values(g)[..stored * hd])
3445 .collect();
3446 let p = crate::gpu::AttnDeviceParams {
3447 kv_id,
3448 layer: *li,
3449 nh,
3450 nkv,
3451 hd,
3452 rd: rd_l,
3456 position,
3457 scale: self.attn_scale,
3458 eps: eps as f32,
3459 gemma,
3460 late_qk_norm: self.qk_norm_after_rope,
3461 output_gate: *output_gate,
3462 q_norm: *q_norm,
3463 k_norm: *k_norm,
3464 inv_freq: &inv_freq_l,
3465 rope_scale: self.layer_rope_scale(*li),
3466 cpu_k,
3467 cpu_v,
3468 cpu_stored: stored,
3469 cpu_gen: cache.generation(),
3470 o1: None,
3471 window: window_l,
3472 softmax_sink: cache.sinks.as_deref(),
3473 head_gate: None,
3474 v_norm: self.attn_v_norm,
3475 };
3476 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3477 let md = |a: &[f32], b: &[f32]| {
3478 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3479 };
3480 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3481 eprintln!(
3482 "attn-oracle L{li} pos {position}: |q| {:.2} max|dq| {:.4} | |k| {:.2} max|dk| {:.4} | |v| {:.2} max|dv| {:.4} | |ao| {:.2} max|dao| {:.4}",
3483 nn(&cq),
3484 md(&cq, &dq),
3485 nn(&ck),
3486 md(&ck, &dk),
3487 nn(&cv),
3488 md(&cv, &dv),
3489 nn(&ao),
3490 md(&ao, &dao)
3491 );
3492 } else {
3493 eprintln!("attn-oracle L{li}: device probe declined");
3494 }
3495 }
3496 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3497 attention::apply_projected_gate(
3500 &mut ao,
3501 raw,
3502 per_head,
3503 hd,
3504 self.proj_gate_sigmoid,
3505 );
3506 }
3507 if let Some(mut raw) = gate_raw.take() {
3508 attention::recycle_buf(&mut raw);
3509 }
3510 graph.encode_attn_suffix(l, &ao);
3511 graph.commit();
3514 attention::recycle_buf(&mut ao);
3515 }
3516 }
3517
3518 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3519 }
3520 let mut lm_rows = None;
3525 if self.graph_want_logits
3526 && upto.is_none()
3527 && end == self.num_layers
3528 && std::env::var("CMF_GPU_LMHEAD")
3529 .map(|v| v != "0")
3530 .unwrap_or(true)
3531 {
3532 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3533 if graph.lm_head_ok(lm) {
3534 graph.encode_lm_head(&self.weights.final_norm, lm);
3535 lm_rows = Some(lm.1);
3536 }
3537 }
3538 }
3539 if self.graph_head_required && lm_rows.is_none() {
3540 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3541 self.fail_metal_graph("fused graph head was requested but not encodable");
3542 return start;
3543 }
3544 let _sy0 = std::time::Instant::now();
3545 if let Err(err) = graph.sync_checked() {
3546 if let Some(profile) = pager_profile_page.as_mut() {
3547 profile.finish(start, graph.pager_page_stats(), "sync-error");
3548 }
3549 self.fail_metal_graph(&err);
3550 return start;
3551 }
3552 let _rs0 = std::time::Instant::now();
3553 if !pending.is_empty() {
3554 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3555 let mut outs: Vec<&mut [f32]> = self
3556 .kv_cache
3557 .layers
3558 .iter_mut()
3559 .enumerate()
3560 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3561 .map(|(_, s)| s.linear_state.as_mut_slice())
3562 .collect();
3563 graph.read_states(&mut outs);
3564 }
3565 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3566 use std::sync::atomic::{AtomicU64, Ordering};
3567 static SY: AtomicU64 = AtomicU64::new(0);
3568 static RS: AtomicU64 = AtomicU64::new(0);
3569 static N: AtomicU64 = AtomicU64::new(0);
3570 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3571 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3572 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3573 if n % 100 == 0 {
3574 eprintln!(
3575 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3576 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3577 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3578 );
3579 }
3580 }
3581 if let Some(rows) = lm_rows {
3582 crate::gpu::hostprof_encode_done(_mt0);
3583 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3584 graph.read_logits(&mut lg);
3585 crate::gpu::hostprof_total(_mt0);
3586 lg.resize(self.vocab_size, 0.0);
3587 if let Some(c) = self.final_softcap {
3588 softcap_in_place(&mut lg, c, self.pool.as_deref());
3589 }
3590 self.graph_logits = Some(lg);
3591 }
3592 graph.read_h(h);
3593 if self.graph_head_required && self.graph_logits.is_none() {
3594 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3595 self.fail_metal_graph("fused graph head completed without logits readback");
3596 return start;
3597 }
3598 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3599 METAL_GRAPH_LAYERS.fetch_add(
3600 end.saturating_sub(start) as u64,
3601 std::sync::atomic::Ordering::Relaxed,
3602 );
3603 if self.graph_head_required {
3604 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3605 }
3606 for (li, nkv, hd) in dev_attn {
3610 let mut krow = attention::take_buf(nkv * hd);
3611 let mut vrow = attention::take_buf(nkv * hd);
3612 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3613 let cache = &mut self.kv_cache.layers[li];
3614 cache.append(&krow, &vrow, &[]);
3615 let n = cache.seq_len;
3616 let mut imp = attention::take_buf(n);
3617 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3618 cache.accumulate_imp(&imp);
3619 attention::recycle_buf(&mut imp);
3620 }
3621 attention::recycle_buf(&mut krow);
3622 attention::recycle_buf(&mut vrow);
3623 }
3624 if let Some(profile) = pager_profile_page.as_mut() {
3625 profile.finish(end, graph.pager_page_stats(), "page-complete");
3626 }
3627 if let Some((_, arm)) = ab {
3628 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3629 }
3630 end
3631 }
3632
3633 pub fn new(
3634 tokenizer: Tokenizer,
3635 weights: PipelineWeights,
3636 hidden_size: usize,
3637 intermediate_size: usize,
3638 num_heads: usize,
3639 num_kv_heads: usize,
3640 head_dim: usize,
3641 num_layers: usize,
3642 physical_layers: usize,
3643 loop_final_norm: bool,
3644 vocab_size: usize,
3645 rms_eps: f64,
3646 rope_base: f32,
3647 norm_style: NormStyle,
3648 max_seq_len: usize,
3649 sampler_config: SamplerConfig,
3650 ) -> Self {
3651 let rng = match sampler_config.seed {
3652 Some(s) => SplitMix64::new(s),
3653 None => SplitMix64::from_entropy(),
3654 };
3655 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3656 let pool = Pool::from_env();
3657 if let Some(p) = &pool {
3658 tracing::info!("worker pool: {} threads", p.n_workers());
3659 if let Some(model) = weights
3661 .lm_head
3662 .model_arc()
3663 .or_else(|| weights.embed_tokens.model_arc())
3664 {
3665 let regions: Vec<&[u8]> =
3666 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3667 p.bind_numa(®ions);
3668 }
3669 }
3670 Self {
3671 gpu_plan: None,
3672 tokenizer: std::sync::Arc::new(tokenizer),
3673 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3674 sampler_config,
3675 weights,
3676 hidden_size,
3677 intermediate_size,
3678 num_heads,
3679 num_kv_heads,
3680 head_dim,
3681 num_layers,
3682 physical_layers,
3683 loop_final_norm,
3684 vocab_size,
3685 rms_eps,
3686 rope_base,
3687 norm_style,
3688 rotary_dim: head_dim,
3689 attention_heads_per_layer: None,
3690 kv_heads_per_layer: None,
3691 v_head_dim: None,
3692 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3693 .filter(|v| !v.is_empty())
3694 .map(std::path::PathBuf::from),
3695 graph_declines: std::cell::RefCell::new(Vec::new()),
3696 mimo_moe: Default::default(),
3697 minicpm_tail: Default::default(),
3698 vmf_cfg: None,
3699 gdn_cfg: None,
3700 kda_cfg: None,
3701 g3n: None,
3702 g4e: None,
3703 dsv4: None,
3704 dsv41: None,
3705 dsv41_vision: None,
3706 dsv41_prefill: None,
3707 qwen4_exp: None,
3708 dsv4_mtp: Vec::new(),
3709 dspark: None,
3710 dspark_pending: Vec::new(),
3711 dspark_hist: Vec::new(),
3712 dspark_real: Vec::new(),
3713 dspark_trunk_picks: Vec::new(),
3714 dspark_exp: Vec::new(),
3715 dspark_draft_ns: 0,
3716 m3_winners: Vec::new(),
3717 m3_counts: Vec::new(),
3718 m3_n_exp: 0,
3719 m3_tokens: 0,
3720 m3_prewarmed: false,
3721 logit_multiplier: None,
3722 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3723 graph_failed: std::sync::atomic::AtomicBool::new(false),
3724 kv_history: Vec::new(),
3725 kv_history_device: false,
3726 short_conv_cfg: None,
3727 mtp: None,
3728 mimo_mtp: None,
3729 verify_exact_moe: false,
3730 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3731 ignore_eos: false,
3732 draft_full_streak: 0,
3733 spec_k_adapt: None,
3734 spec_acc_ewma: 0.7,
3735 rng,
3736 sampler_scratch: SamplerScratch::default(),
3737 spec_forced: None,
3738 spec_q: Vec::new(),
3739 spec_p: Vec::new(),
3740 spec_res: Vec::new(),
3741 spec_qs: Vec::new(),
3742 spec_ps: Vec::new(),
3743 spec_ress: Vec::new(),
3744 mtp_graph_mode: None,
3745 #[cfg(target_os = "macos")]
3746 metal_verify: None,
3747 inv_freq,
3748 ws: ForwardScratch::new(hidden_size),
3749 pool,
3750 model: None,
3751 dyn_force_f32: false,
3752 dyn_skill_layers: Vec::new(),
3753 dyn_active: None,
3754 dyn_blend_loaded: false,
3755 dyn_phi_layer: None,
3756 dyn_phi_ema: Vec::new(),
3757 dyn_phi_seen: 0,
3758 dyn_router: None,
3759 o1_cfg: None,
3760 o1_epoch: 0,
3761 o1_flags: Vec::new(),
3762 trace: false,
3763 calib_temp: 1.0,
3764 confidence_on: true,
3765 embed_multiplier: 1.0,
3766 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3767 swa: None,
3768 swa_trim: None,
3769 sliding_layers: None,
3770 no_rope_layers: None,
3771 anchor_core: None,
3772 bounded_rope: None,
3773 kv_prefix: KvPrefix::default(),
3774 last_prefill_tokens: 0,
3775 inv_freq_local: None,
3776 rotary_dim_local: None,
3777 rope_scale: 1.0,
3778 rope_scale_local: 1.0,
3779 global_attn: None,
3780 inv_freq_global: None,
3781 attn_v_norm: false,
3782 qk_norm_after_rope: false,
3783 proj_gate_sigmoid: false,
3784 final_softcap: None,
3785 head_clusters: None,
3786 attn_softcap: 0.0,
3787 graph_want_logits: false,
3788 graph_head_required: false,
3789 graph_logits: None,
3790 embryo_graph: None,
3791 graph_refused: std::sync::atomic::AtomicBool::new(false),
3792 graph_kv_id: {
3793 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3794 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3795 },
3796 #[cfg(test)]
3797 nll_test_fail_at: None,
3798 #[cfg(test)]
3799 nll_test_force_serial: false,
3800 }
3801 }
3802
3803 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3811 if let Err(e) = self.try_set_o1(cfg) {
3812 tracing::error!("{e}");
3813 }
3814 }
3815
3816 pub fn bounded_native(&self) -> bool {
3820 self.anchor_core.is_some()
3821 }
3822
3823 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3826 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3827 }
3828
3829 pub fn o1_refusal(&self) -> Option<String> {
3831 self.anchor_core.as_ref().map(|ac| {
3832 format!(
3833 "--o1 / CMF_O1 refused: the anchor is native bounded \
3834 (anchor_core kind={} window={} sink={}); the file's operator \
3835 is executed as-is and no post-hoc Nyström overlay applies",
3836 ac.kind, ac.window, ac.sink
3837 )
3838 })
3839 }
3840
3841 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3843 if let Some(c) = &cfg {
3844 if let Some(why) = self.o1_refusal() {
3845 self.o1_flags = Vec::new();
3846 self.o1_cfg = None;
3847 return Err(why);
3848 }
3849 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3850 self.o1_flags.clear();
3851 self.o1_cfg = None;
3852 return Err(format!(
3853 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3854 c.w, c.sink
3855 ));
3856 }
3857 }
3858 self.o1_flags = match &cfg {
3859 Some(c) => {
3860 let mut flags = c.layer_flags(self.num_layers);
3861 for (li, f) in flags.iter_mut().enumerate() {
3862 if *f
3868 && (!matches!(
3869 self.weights.layers[self.phys_layer(li)].attn,
3870 AttnKind::Full { .. }
3871 ) || self.layer_window(li).is_some()
3872 || self.kv_cache.layers[li].sinks.is_some()
3873 || self.layer_v_dim(li) != self.layer_geom(li).1)
3874 {
3875 *f = false;
3876 }
3877 }
3878 flags
3879 }
3880 None => Vec::new(),
3881 };
3882 if let Some(c) = &cfg {
3883 let n = self.o1_flags.iter().filter(|&&f| f).count();
3884 tracing::info!(
3885 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3886 self.num_layers,
3887 c.m,
3888 c.w,
3889 c.sink,
3890 c.rect
3891 );
3892 }
3893 self.o1_cfg = cfg;
3894 Ok(())
3895 }
3896
3897 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3903 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3904 return Err(format!(
3905 "anchor_core kind '{}' is not executable by this runtime",
3906 cfg.kind
3907 ));
3908 }
3909 if cfg.window == 0 {
3910 return Err("anchor_core.window must be >= 1".into());
3911 }
3912 let mut n = 0usize;
3913 for li in 0..self.num_layers {
3914 let pl = self.phys_layer(li);
3915 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3916 if w.window != cfg.window || w.sink != cfg.sink {
3917 return Err(format!(
3918 "layer {li}: bounded weights (window {} sink {}) disagree with \
3919 anchor_core (window {} sink {})",
3920 w.window, w.sink, cfg.window, cfg.sink
3921 ));
3922 }
3923 self.kv_cache.layers[li].install_bounded(cfg.window);
3924 n += 1;
3925 }
3926 }
3927 if n == 0 {
3928 return Err("anchor_core is present but no layer executes it".into());
3929 }
3930 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3931 self.bounded_rope = Some(std::sync::Arc::new(rope));
3932 self.anchor_core = Some(cfg.clone());
3933 self.embryo_graph = None;
3934 tracing::info!(
3935 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3936 cfg.kind,
3937 cfg.window,
3938 cfg.sink,
3939 self.kv_cache
3940 .layers
3941 .iter()
3942 .map(|l| l.bounded_state_bytes())
3943 .max()
3944 .unwrap_or(0)
3945 );
3946 Ok(())
3947 }
3948
3949 pub fn install_wire_identity(&mut self, identity: u64) {
3953 for li in 0..self.kv_cache.layers.len() {
3954 let pl = self.phys_layer(li);
3955 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3956 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3957 Some(AttnKind::Linear(_))
3958 | Some(AttnKind::LinearGdn(_))
3959 | Some(AttnKind::ShortConv(_))
3960 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3961 _ => crate::kv_cache::WireKind::Full,
3962 };
3963 let l = &mut self.kv_cache.layers[li];
3964 l.wire_kind = kind;
3965 l.wire_identity = identity;
3966 l.wire_layer = li as u32;
3969 }
3970 }
3971
3972 pub fn clear_history(&mut self) {
3974 self.kv_history.clear();
3975 self.kv_history_device = false;
3976 self.kv_prefix.clear();
3977 }
3978
3979 pub fn graph_refused(&self) -> bool {
3983 self.graph_refused
3984 .load(std::sync::atomic::Ordering::Relaxed)
3985 }
3986
3987 pub fn mark_graph_refused(&self) {
3989 if !self
3990 .graph_refused
3991 .swap(true, std::sync::atomic::Ordering::Relaxed)
3992 {
3993 tracing::info!(
3994 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3995 self.graph_kv_id
3996 );
3997 }
3998 }
3999
4000 pub fn device_sequence_position(&self) -> Option<usize> {
4004 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4005 }
4006
4007 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4012 let dev = self.device_sequence_position();
4013 if recorded_on_device {
4014 dev == Some(n) && self.embryo_resident_wanted()
4015 } else {
4016 dev.is_none()
4017 }
4018 }
4019
4020 pub(crate) fn invalidate_for_weight_change(&mut self) {
4028 self.clear_sequence_state();
4029 self.embryo_graph = None;
4030 }
4031
4032 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4038 let (n, on_device) = if self.bounded_native() {
4039 (
4040 self.kv_prefix.extension(input_ids),
4041 self.kv_prefix.on_device(),
4042 )
4043 } else {
4044 let h = &self.kv_history;
4045 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4046 (h.len(), self.kv_history_device)
4047 } else {
4048 (0, false)
4049 }
4050 };
4051 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4054 tracing::warn!(
4055 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4056 the device now holds {:?} — re-prefilling from zero",
4057 if on_device { "resident device" } else { "host" },
4058 self.device_sequence_position()
4059 );
4060 return 0;
4061 }
4062 n
4063 }
4064
4065 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4068 self.cached_prefix_len(input_ids)
4069 }
4070
4071 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4077 let on_device = self.device_sequence_position().is_some();
4078 if self.bounded_native() {
4079 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4080 let prev_device = self.kv_prefix.on_device();
4081 self.kv_history.clear();
4082 self.kv_history_device = false;
4083 if keep && prev_device == on_device {
4084 self.kv_prefix.extend(&consumed[reused..]);
4085 } else {
4086 self.kv_prefix.set(consumed);
4087 }
4088 self.kv_prefix.set_on_device(on_device);
4089 } else {
4090 self.kv_history = consumed.to_vec();
4091 self.kv_history_device = on_device;
4092 }
4093 }
4094
4095 pub fn o1_active(&self) -> bool {
4097 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4098 }
4099
4100 pub fn generation_batch_k(&self) -> usize {
4118 if let Some(k) = std::env::var("CMF_BATCH_K")
4119 .ok()
4120 .and_then(|v| v.parse::<usize>().ok())
4121 {
4122 return k;
4123 }
4124 if self.g4_batch_prefill() {
4127 return 128;
4128 }
4129 #[cfg(not(target_os = "macos"))]
4130 if self.graph_prefill_preferred() && !self.o1_active() {
4131 let moe = self
4134 .weights
4135 .layers
4136 .iter()
4137 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4138 let q82 = crate::gpu::moe_q82_graph_on()
4144 && self.weights.layers.iter().any(|lw| {
4145 matches!(&lw.ffn, FfnKind::Moe(m)
4146 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4147 });
4148 return if q82 {
4149 128
4150 } else if moe {
4151 32
4152 } else {
4153 128
4154 };
4155 }
4156 0
4157 }
4158
4159 pub fn generation_graph_prefill(&self) -> bool {
4160 let graph = self.graph_prefill_preferred();
4161 #[cfg(not(target_os = "macos"))]
4172 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4173 return false;
4174 }
4175 graph
4176 }
4177
4178 pub fn o1_device_stats(&self) -> (usize, u64) {
4183 crate::gpu::o1_device_stats(self.graph_kv_id)
4184 }
4185
4186 pub fn o1_begin(&mut self) {
4191 self.o1_begin_with_prefix(None);
4192 }
4193
4194 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4198 if let Some(c) = &self.o1_cfg {
4199 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4200 let boundary = requested_prefix.map(|p| {
4201 p.max(
4202 crate::nystrom::o1_deferred_boundary(w, sink)
4203 .expect("o1 config boundary validated in set_o1"),
4204 )
4205 });
4206 for (li, &f) in self.o1_flags.iter().enumerate() {
4207 if f {
4208 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4209 }
4210 }
4211 }
4212 }
4213
4214 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4216 self.o1_cfg.as_ref().and_then(|c| {
4217 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4218 .map(|floor| requested_prefix.max(floor))
4219 })
4220 }
4221
4222 fn o1_note_transition(&mut self) {
4223 let mut transitioned = false;
4227 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4228 if flagged {
4229 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4230 }
4231 }
4232 if transitioned {
4233 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4234 }
4235 }
4236
4237 fn o1_pending(&self) -> bool {
4238 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4239 f && self.kv_cache.layers[li].seq_len > 0
4240 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4241 })
4242 }
4243
4244 fn o1_fail(&mut self, err: String) {
4245 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4246 self.clear_sequence_state();
4247 self.graph_failed
4248 .store(true, std::sync::atomic::Ordering::Relaxed);
4249 self.cancel
4250 .store(true, std::sync::atomic::Ordering::Relaxed);
4251 }
4252
4253 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4258 if self.o1_cfg.is_none() {
4259 return Ok(false);
4260 }
4261 let mut participating = false;
4262 for li in 0..self.num_layers {
4263 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4264 continue;
4265 }
4266 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4267 return Err(err);
4268 }
4269 if self.kv_cache.layers[li].seq_len == 0 {
4270 continue;
4271 }
4272 participating = true;
4273 let num_heads = self.layer_num_heads(li);
4274 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4275 }
4276 self.o1_note_transition();
4277 for li in 0..self.num_layers {
4278 if self.o1_flags.get(li).copied().unwrap_or(false) {
4279 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4280 return Err(err);
4281 }
4282 }
4283 }
4284 Ok(participating
4285 && (0..self.num_layers).all(|li| {
4286 !self.o1_flags.get(li).copied().unwrap_or(false)
4287 || self.kv_cache.layers[li].seq_len == 0
4288 || self.kv_cache.layers[li].o1_sealed()
4289 }))
4290 }
4291
4292 fn o1_progress(&mut self) {
4295 if !self.o1_active() {
4296 return;
4297 }
4298 for li in 0..self.num_layers {
4299 if self.o1_flags.get(li).copied().unwrap_or(false) {
4300 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4301 self.o1_fail(err);
4302 return;
4303 }
4304 }
4305 }
4306 self.o1_note_transition();
4310 if !self.o1_pending() {
4311 return;
4312 }
4313 if let Err(err) = self.o1_seal_checked() {
4314 self.o1_fail(err);
4315 }
4316 }
4317
4318 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4323 if self
4324 .graph_failed
4325 .swap(false, std::sync::atomic::Ordering::Relaxed)
4326 {
4327 self.cancel
4328 .store(false, std::sync::atomic::Ordering::Relaxed);
4329 self.clear_sequence_state();
4330 return Err(format!("{phase}: deferred O(1) transition failed"));
4331 }
4332 Ok(())
4333 }
4334
4335 pub fn o1_seal(&mut self) {
4339 if let Err(err) = self.o1_seal_checked() {
4340 self.o1_fail(err);
4341 }
4342 }
4343
4344 pub fn set_trace(&mut self, on: bool) {
4346 self.trace = on;
4347 }
4348
4349 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4352 self.rng = match config.seed {
4353 Some(seed) => SplitMix64::new(seed),
4354 None => SplitMix64::from_entropy(),
4355 };
4356 self.sampler_config = config;
4357 }
4358
4359 pub fn set_confidence(&mut self, on: bool) {
4364 self.confidence_on = on;
4365 }
4366
4367 pub fn set_calib_temp(&mut self, t: f32) {
4370 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4371 }
4372
4373 pub fn calib_temp(&self) -> f32 {
4375 self.calib_temp
4376 }
4377
4378 pub fn rope_inv_freq(&self) -> &[f32] {
4382 self.inv_freq.as_slice()
4383 }
4384
4385 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4388 self.rotary_dim = rotary_dim.min(self.head_dim);
4389 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4390 self.embryo_graph = None;
4394 }
4395
4396 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4397 QwenAttnCfg {
4398 num_heads: self.num_heads,
4399 num_kv_heads: self.num_kv_heads,
4400 head_dim: self.head_dim,
4401 hidden_size: self.hidden_size,
4402 position,
4403 inv_freq: &self.inv_freq,
4404 rotary_dim: self.rotary_dim,
4405 scale: self.attn_scale,
4406 softcap: self.attn_softcap,
4407 window: None,
4408 v_norm: false,
4409 qk_norm_after_rope: self.qk_norm_after_rope,
4410 gate_sigmoid: self.proj_gate_sigmoid,
4411 q_norm: None,
4412 k_norm: None,
4413 output_gate: false,
4414 softplus_gate: None,
4415 rope_scale: self.rope_scale,
4416 bias: None,
4417 rms_eps: self.rms_eps,
4418 norm_style: self.norm_style,
4419 pool: self.pool.as_deref(),
4420 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4421 }
4422 }
4423
4424 pub fn generate(
4426 &mut self,
4427 prompt: &str,
4428 max_tokens: usize,
4429 task_mask: Option<&TaskMask>,
4430 on_token: Option<TokenCallback>,
4431 ) -> Result<GenerateResult, String> {
4432 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4433 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4434 }
4435
4436 pub fn generate_from_vl(
4439 &mut self,
4440 input: &crate::dsv41_vision::PreparedVlInputs,
4441 max_tokens: usize,
4442 task_mask: Option<&TaskMask>,
4443 on_token: Option<TokenCallback>,
4444 ) -> Result<GenerateResult, String> {
4445 let Some(dsv41) = &self.dsv41 else {
4446 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4447 };
4448 if input.token_ids.is_empty() {
4449 return Err("empty V4.1 multimodal prompt".into());
4450 }
4451 if input.token_types.len() != input.token_ids.len() {
4452 return Err(format!(
4453 "V4.1 token type count {} != token count {}",
4454 input.token_types.len(),
4455 input.token_ids.len()
4456 ));
4457 }
4458 let dim = dsv41.2.dim;
4459 let mut embeddings = vec![None; input.token_ids.len()];
4460 let mut participates = vec![true; input.token_ids.len()];
4461 if !input.images.is_empty() {
4462 let vision = self
4463 .dsv41_vision
4464 .as_ref()
4465 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4466 for image in &input.images {
4467 let end = image.start.saturating_add(image.types.len());
4468 if end > input.token_ids.len() {
4469 return Err(format!(
4470 "V4.1 image span {}..{} exceeds prompt length {}",
4471 image.start,
4472 end,
4473 input.token_ids.len()
4474 ));
4475 }
4476 let mut span = vec![0.0f32; image.types.len() * dim];
4477 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4478 for (offset, &kind) in image.types.iter().enumerate() {
4479 let pos = image.start + offset;
4480 if input.token_types[pos] != kind {
4481 return Err(format!(
4482 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4483 input.token_types[pos]
4484 ));
4485 }
4486 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4487 participates[pos] = false;
4488 }
4489 }
4490 }
4491 for (pos, &kind) in input.token_types.iter().enumerate() {
4492 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4493 return Err(format!("V4.1 text position {pos} has an image embedding"));
4494 }
4495 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4496 return Err(format!("V4.1 image position {pos} has no image embedding"));
4497 }
4498 }
4499 self.dsv41_prefill = Some((embeddings, participates));
4500 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4501 self.dsv41_prefill = None;
4502 result
4503 }
4504
4505 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4507 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4508 }
4509
4510 pub fn generate_from_ids(
4518 &mut self,
4519 input_ids: &[u32],
4520 max_tokens: usize,
4521 task_mask: Option<&TaskMask>,
4522 on_token: Option<TokenCallback>,
4523 ) -> Result<GenerateResult, String> {
4524 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4525 }
4526
4527 pub fn generate_from_embeds(
4533 &mut self,
4534 input_ids: &[u32],
4535 prompt_rows: &[f32],
4536 max_tokens: usize,
4537 task_mask: Option<&TaskMask>,
4538 on_token: Option<TokenCallback>,
4539 ) -> Result<GenerateResult, String> {
4540 if input_ids.is_empty()
4541 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4542 {
4543 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4544 }
4545 if prompt_rows.iter().any(|x| !x.is_finite()) {
4546 return Err("embedded prompt contains non-finite values".into());
4547 }
4548 if !self.can_prefill_batched()
4549 || self.dyn_router.is_some()
4550 || self.o1_active()
4551 || self.mtp.is_some()
4552 || self.gpu_plan.is_some()
4553 {
4554 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4555 }
4556 self.generate_with_prompt_rows(
4557 input_ids,
4558 Some(prompt_rows),
4559 max_tokens,
4560 task_mask,
4561 on_token,
4562 )
4563 }
4564
4565 fn generate_with_prompt_rows(
4566 &mut self,
4567 input_ids: &[u32],
4568 prompt_rows: Option<&[f32]>,
4569 max_tokens: usize,
4570 task_mask: Option<&TaskMask>,
4571 mut on_token: Option<TokenCallback>,
4572 ) -> Result<GenerateResult, String> {
4573 #[cfg(target_os = "macos")]
4574 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4575 if std::env::var("CMF_TRACE_H").is_ok() {
4576 eprintln!("input_ids: {input_ids:?}");
4577 }
4578 if input_ids.is_empty() {
4579 return Err("empty prompt: nothing to generate from".to_string());
4580 }
4581 self.graph_failed
4585 .store(false, std::sync::atomic::Ordering::Relaxed);
4586 let task_mask = self.drop_open_mask(task_mask);
4591
4592 let mut reuse_from = {
4600 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4601 if on
4602 && prompt_rows.is_none()
4603 && task_mask.is_none()
4604 && self.mtp.is_none()
4605 && !(self.mimo_mtp.is_some() && self.speculative)
4606 && self.o1_cfg.is_none()
4607 && self.dsv41.is_none()
4608 {
4609 self.cached_prefix_len(input_ids)
4610 } else {
4611 0
4612 }
4613 };
4614 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4617 reuse_from = 0;
4618 }
4619 self.last_prefill_tokens = input_ids.len() - reuse_from;
4620 let bounded_native = self.bounded_native();
4621 if reuse_from == 0 {
4622 self.reset_fresh_mtp_draft_state();
4627 self.clear_sequence_state();
4628 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4629 eprintln!(
4630 "kv-reuse: {} of {} prompt positions already cached",
4631 reuse_from,
4632 input_ids.len()
4633 );
4634 }
4635 crate::gpu::graph_race_begin_generation();
4636 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4640 std::env::var("CMF_O1_PREFILL")
4641 .ok()
4642 .and_then(|v| v.parse::<usize>().ok())
4643 .filter(|&p| p > 0)
4644 } else {
4645 None
4646 };
4647 if task_mask.is_none() {
4648 self.o1_begin_with_prefix(o1_prefill);
4649 }
4650
4651 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4657 #[cfg(target_os = "macos")]
4695 let metal_graph = crate::gpu::q1_force()
4696 && crate::gpu::enabled_here()
4697 && std::env::var("CMF_GPU_BLOCK")
4698 .map(|v| v != "0")
4699 .unwrap_or(true);
4700 #[cfg(target_os = "macos")]
4706 let native_metal_backend = crate::gpu::q1_force();
4707 #[cfg(not(target_os = "macos"))]
4708 let metal_graph = false;
4709 #[cfg(not(target_os = "macos"))]
4710 let native_metal_backend = false;
4711 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4712 let spec_cheap_round =
4716 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4717 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4718 || match spec_sample_env.as_deref() {
4719 Some("1") => true,
4720 Some(_) => false,
4721 None => metal_graph && spec_cheap_round,
4722 };
4723 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4739 for lw in &self.weights.layers {
4740 if let FfnKind::Dense(d) = &lw.ffn {
4741 dense_n += 1;
4742 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4743 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4744 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4745 {
4746 dense_q4tp += 1;
4747 }
4748 }
4749 }
4750 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4751 let penalized = !metal_graph
4763 && (self.sampler_config.repetition_penalty != 1.0
4764 || self.sampler_config.presence_penalty != 0.0
4765 || !self.sampler_config.suppress_tokens.is_empty());
4766 #[cfg(feature = "gpu")]
4771 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4772 #[cfg(not(feature = "gpu"))]
4773 let metal_wgpu = false;
4774 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4775 let spec_wanted = graph_spec_wanted_by_policy(
4776 spec_env.as_deref(),
4777 spec_default_ok,
4778 penalized,
4779 metal_wgpu,
4780 native_metal_backend,
4781 );
4782 if spec_wanted && metal_wgpu {
4783 tracing::warn!(
4784 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4785 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4786 );
4787 }
4788 let graph_spec = self.speculative
4792 && (graph_on || metal_graph)
4793 && self.mtp.is_some()
4794 && task_mask.is_none()
4795 && !self.o1_active()
4796 && spec_sampling_ok
4797 && spec_wanted;
4798 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4811 let pair_pays = host_pair_opt_in
4812 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4813 #[cfg(target_os = "macos")]
4817 if metal_graph {
4818 static SAID: std::sync::Once = std::sync::Once::new();
4819 SAID.call_once(|| {
4820 let spec = if graph_spec {
4821 let k = std::env::var("CMF_GRAPH_SPEC_K")
4822 .ok()
4823 .and_then(|v| v.parse::<usize>().ok())
4824 .filter(|&v| (1..=8).contains(&v))
4825 .unwrap_or(7);
4826 let arm = if self.sampler_config.temperature < 1e-6 {
4827 "greedy"
4828 } else {
4829 "sampling"
4830 };
4831 format!(
4832 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4833 Self::draft_vocab_rows(usize::MAX)
4834 )
4835 } else if !self.speculative {
4836 "spec off (CMF_MTP=0)".to_string()
4837 } else if self.mtp.is_none() {
4838 "spec off (no MTP head)".to_string()
4839 } else if !spec_sampling_ok {
4840 if spec_cheap_round {
4841 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4842 } else {
4843 "spec off (sampling without a top-k: the dense chain \
4844 costs more than it saves)"
4845 .to_string()
4846 }
4847 } else if !spec_wanted {
4848 if spec_env.is_none() {
4849 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
4850 } else {
4851 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
4852 }
4853 } else if task_mask.is_some() {
4854 "spec off (task mask)".to_string()
4855 } else if !graph_spec && !pair_pays {
4856 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
4857 .to_string()
4858 } else {
4859 "spec off (O(1) attention)".to_string()
4860 };
4861 let on = |var: &str| {
4862 if std::env::var(var).as_deref() == Ok("0") {
4863 "off"
4864 } else {
4865 "on"
4866 }
4867 };
4868 tracing::info!(
4869 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4870 MTP graph {}, attend {}, probe {}",
4871 if crate::gpu_metal::state4_on() {
4872 "on"
4873 } else {
4874 "off"
4875 },
4876 if crate::gpu_metal::async_replay_on() {
4877 "on"
4878 } else {
4879 "off"
4880 },
4881 on("CMF_METAL_PREFILL"),
4882 on("CMF_MTP_GRAPH"),
4883 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4884 if crate::gpu::probe_enabled() {
4885 "bypassed (q1 force)"
4886 } else {
4887 "off"
4888 },
4889 );
4890 });
4891 }
4892 let spec_active = self.speculative
4893 && self.mtp.is_some()
4894 && task_mask.is_none()
4895 && !self.o1_active()
4896 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4897 let mut mtp = if spec_active { self.mtp.take() } else { None };
4900 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4901 eprintln!(
4902 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4903 mtp.is_some(),
4904 self.speculative,
4905 self.sampler_config.temperature < 1e-6,
4906 );
4907 }
4908 if let Some(m) = &mut mtp {
4909 m.kv.clear();
4910 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4912 self.mtp_graph_mode = None;
4913 }
4914 let mimo_spec = self.speculative
4918 && self.mimo_mtp.is_some()
4919 && task_mask.is_none()
4920 && !self.o1_active()
4921 && self.dyn_router.is_none()
4922 && self.sampler_config.temperature < 1e-6
4923 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4924 if let Some(st) = self.mimo_mtp.as_mut() {
4925 st.reset();
4926 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4927 Self::mimo_mtp_hist_cap(st, input_ids.len());
4928 }
4929 }
4930 let mut router = if mtp.is_none() {
4934 self.dyn_router.take()
4935 } else {
4936 None
4937 };
4938 let mut reuse_from = reuse_from;
4939 if let Some(r) = &mut router {
4940 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4943 let _ = self.set_active_skill(None);
4946 reuse_from = 0;
4947 self.last_prefill_tokens = input_ids.len();
4948 }
4949 }
4950
4951 let mut all_ids = input_ids.to_vec();
4952 let mut generated = 0usize;
4953 let mut finish_reason = "max_tokens".to_string();
4954 let mut drafted = 0usize;
4955 let mut accepted = 0usize;
4956 let mut dsv4_spec_bad = 0usize;
4963 let mut dsv4_spec_retry_at = 0usize;
4964 let mut confidence: Vec<f32> = Vec::new();
4965 let trace_on = self.trace;
4966 let calib_temp = self.calib_temp;
4967 let mut traces: Vec<TokenTrace> = Vec::new();
4968
4969 let mut hidden = vec![0.0f32; self.hidden_size];
4975 let mut pos = reuse_from;
4976 let fuse_lm = mtp.is_none()
4985 && router.is_none()
4986 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4987 self.graph_logits = None;
4988 self.graph_want_logits = false;
4989 let _tpf = std::time::Instant::now();
4990 let batch_k = self.generation_batch_k();
4991 if let Some(rows) = prompt_rows {
4992 let hs = self.hidden_size;
4993 let chunk = self.prefill_chunk().max(1);
4994 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4995 let end = (pos + chunk).min(input_ids.len());
4996 let hb = match self.prefill_input_rows(
4997 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
4998 pos,
4999 task_mask,
5000 ) {
5001 Ok(hb) => hb,
5002 Err(err) => {
5003 self.finish_generation(&mut mtp, &mut router, true);
5004 return Err(err);
5005 }
5006 };
5007 if mimo_spec {
5008 self.mimo_note_rows(&hb, pos);
5009 }
5010 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5011 pos = end;
5012 }
5013 }
5014 while self.qwen4_exp.is_some()
5025 && mtp.is_none()
5026 && pos < input_ids.len()
5027 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5028 {
5029 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5032 let want_logits = end == input_ids.len();
5033 let mut lg = Vec::new();
5034 if let Some(b) = &mut self.qwen4_exp {
5035 crate::qwen4_exp::forward_tokens(
5036 &b.0,
5037 &b.1,
5038 &b.2,
5039 &mut b.3,
5040 &input_ids[pos..end],
5041 pos,
5042 &self.inv_freq,
5043 self.pool.as_deref(),
5044 &mut lg,
5045 want_logits,
5046 );
5047 }
5048 if want_logits {
5049 self.graph_logits = Some(lg);
5050 }
5051 pos = end;
5052 hidden.fill(0.0);
5053 }
5054 while self.dsv4.is_some()
5055 && mtp.is_none()
5056 && pos < input_ids.len()
5057 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5058 {
5059 let end = (pos + prefill_chunk()).min(input_ids.len());
5060 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5061 let mut lg = Vec::new();
5062 if let Some(b) = &mut self.dsv4 {
5063 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5064 crate::dsv4::forward_chunk(
5065 g,
5066 layers,
5067 &cfg,
5068 st,
5069 &ids,
5070 pos,
5071 &self.inv_freq,
5072 self.pool.as_deref(),
5073 &mut lg,
5074 end == input_ids.len(),
5075 );
5076 }
5077 if end == input_ids.len() {
5078 self.graph_logits = Some(lg);
5079 }
5080 pos = end;
5081 hidden = vec![0.0; self.hidden_size];
5082 }
5083 let dsv41_prefill = self.dsv41_prefill.take();
5084 while self.dsv41.is_some()
5085 && mtp.is_none()
5086 && pos < input_ids.len()
5087 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5088 {
5089 let end = (pos + prefill_chunk()).min(input_ids.len());
5090 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5091 let mut lg = Vec::new();
5092 if let Some(b) = &mut self.dsv41 {
5093 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5094 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5095 crate::dsv41::forward_chunk_masked_with_embeddings(
5096 g,
5097 layers,
5098 cfg,
5099 st,
5100 &ids,
5101 pos,
5102 &embeddings[pos..end],
5103 &participates[pos..end],
5104 self.pool.as_deref(),
5105 &mut lg,
5106 );
5107 } else {
5108 crate::dsv41::forward_chunk(
5109 g,
5110 layers,
5111 cfg,
5112 st,
5113 &ids,
5114 pos,
5115 self.pool.as_deref(),
5116 &mut lg,
5117 );
5118 }
5119 }
5120 if end == input_ids.len() {
5121 self.graph_logits = Some(lg);
5122 }
5123 pos = end;
5124 hidden = vec![0.0; self.hidden_size];
5125 }
5126 let dyn_prefill = router.is_some();
5131 let o1_prefill_limit = o1_prefill
5139 .and_then(|requested| self.o1_effective_boundary(requested))
5140 .map(|boundary| boundary.min(input_ids.len()));
5141 let mut o1_sealed = false;
5142 if let Some(limit) = o1_prefill_limit {
5143 if self.can_prefill_batched() && limit > 2 {
5146 let chunk = self.prefill_chunk();
5147 let hs = self.hidden_size;
5148 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5149 let end = (pos + chunk).min(limit);
5150 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5151 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5152 pos = end;
5153 }
5154 } else {
5155 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5156 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5157 pos += 1;
5158 }
5159 }
5160 if pos >= limit {
5161 o1_sealed = match self.o1_seal_checked() {
5162 Ok(sealed) => sealed,
5163 Err(err) => {
5164 self.finish_generation(&mut mtp, &mut router, true);
5165 return Err(err);
5166 }
5167 };
5168 tracing::info!(
5169 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5170 o1_prefill.unwrap_or(0),
5171 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5172 .unwrap_or(limit),
5173 limit,
5174 input_ids.len()
5175 );
5176 }
5177 }
5178 let graph_prefill = self.graph_prefill_preferred();
5184 #[cfg(target_os = "macos")]
5192 if task_mask.is_none()
5193 && !dyn_prefill
5194 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5195 && crate::gpu::enabled_here()
5196 && self.gdn_cfg.is_some()
5197 && !self.per_token_stack()
5198 && input_ids.len() > 8
5199 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5200 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5201 {
5202 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5203 .ok()
5204 .and_then(|v| v.parse().ok())
5205 .filter(|&v| (16..=512).contains(&v))
5206 .unwrap_or(256);
5207 let hs = self.hidden_size;
5208 let _tp = std::time::Instant::now();
5209 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5210 let end = (pos + chunk).min(input_ids.len());
5211 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5212 MetalPrefillOutcome::Completed(hb) => hb,
5213 MetalPrefillOutcome::Declined => break,
5214 MetalPrefillOutcome::Failed => {
5215 self.finish_generation(&mut mtp, &mut router, true);
5216 return Err("ordinary Metal prefill failed after admission".into());
5217 }
5218 };
5219 if let Some(m) = &mut mtp {
5220 let n_pairs = if end < input_ids.len() {
5221 end - pos
5222 } else {
5223 end - pos - 1
5224 };
5225 if n_pairs > 0 {
5226 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5227 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5228 .collect();
5229 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5230 for (j, (h, t)) in pairs.iter().enumerate() {
5231 let h = h.to_vec();
5232 let _ = self.mtp_step(m, &h, *t, pos + j);
5233 }
5234 }
5235 }
5236 }
5237 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5238 pos = end;
5239 }
5240 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5241 eprintln!(
5242 "metal-prefill: {} of {} tokens in {:.1} ms",
5243 pos,
5244 input_ids.len(),
5245 _tp.elapsed().as_secs_f64() * 1e3
5246 );
5247 }
5248 }
5249 #[cfg(not(target_os = "macos"))]
5254 if task_mask.is_none()
5255 && !dyn_prefill
5256 && mtp.is_none()
5257 && o1_prefill.is_none()
5258 && batch_k > 0
5259 && pos + 1 < input_ids.len()
5260 && self.g4_batch_prefill()
5261 {
5262 let t_g4 = std::time::Instant::now();
5263 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5264 Ok((n, last)) => {
5265 if n > 0 {
5266 hidden.copy_from_slice(&last);
5267 }
5268 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5269 eprintln!(
5270 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5271 input_ids.len() - pos,
5272 t_g4.elapsed().as_secs_f64() * 1e3
5273 );
5274 }
5275 pos += n;
5276 }
5277 Err(e) => {
5278 self.finish_generation(&mut mtp, &mut router, true);
5279 return Err(e);
5280 }
5281 }
5282 }
5283 self.mimo_moe_prepare();
5284 #[cfg(not(target_os = "macos"))]
5290 if task_mask.is_none()
5291 && !dyn_prefill
5292 && !graph_prefill
5293 && mtp.is_none()
5294 && o1_prefill.is_none()
5295 && !self.o1_active()
5296 && input_ids.len() > 2
5297 && self.batch_prefix_prefill()
5298 {
5299 let chunk = self.prefill_chunk().max(1);
5300 let hs = self.hidden_size;
5301 let t_bp = std::time::Instant::now();
5302 let pos0 = pos;
5303 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5304 let end = (pos + chunk).min(input_ids.len());
5305 let bk = end - pos;
5306 let mut hiddens = vec![0f32; bk * hs];
5307 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5308 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5309 }
5310 let positions: Vec<usize> = (pos..end).collect();
5311 let mut run = 0usize;
5312 let outcome = self.try_batch_graph_wgpu_prefix(
5313 &mut hiddens,
5314 &positions,
5315 bk,
5316 None,
5317 Some(&mut run),
5318 );
5319 match outcome {
5320 crate::gpu::BatchGraphOutcome::Completed => {
5321 let hb = if run < self.num_layers {
5322 self.prefill_batch_span(
5323 PrefillIn::Hidden(&hiddens),
5324 pos,
5325 None,
5326 run,
5327 self.num_layers,
5328 )
5329 } else {
5330 hiddens
5331 };
5332 if mimo_spec {
5333 self.mimo_note_rows(&hb, pos);
5334 }
5335 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5336 pos = end;
5337 }
5338 crate::gpu::BatchGraphOutcome::Failed => {
5339 self.finish_generation(&mut mtp, &mut router, true);
5340 return Err("batched prefix prefill failed after admission".into());
5341 }
5342 crate::gpu::BatchGraphOutcome::Declined => {
5343 #[cfg(feature = "gpu")]
5346 if pos > pos0 {
5347 self.pull_lagging_host_kv(0, self.num_layers, pos);
5348 }
5349 break;
5350 }
5351 }
5352 }
5353 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5354 eprintln!(
5355 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5356 pos - pos0,
5357 input_ids.len(),
5358 t_bp.elapsed().as_secs_f64() * 1e3
5359 );
5360 }
5361 }
5362 if task_mask.is_none()
5363 && !dyn_prefill
5364 && !graph_prefill
5365 && self.can_prefill_batched()
5366 && !self.per_token_stack()
5367 && o1_prefill.is_none()
5368 && input_ids.len() > 2
5369 {
5370 let chunk = self.prefill_chunk();
5376 let hs = self.hidden_size;
5377 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5378 let end = (pos + chunk).min(input_ids.len());
5379 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5380 if mimo_spec {
5381 self.mimo_note_rows(&hb, pos);
5382 }
5383 if let Some(m) = &mut mtp {
5384 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5385 .ok()
5386 .and_then(|v| v.parse().ok())
5387 .unwrap_or(0);
5388 for p in pos..end {
5389 if p + 1 < input_ids.len() {
5390 if probe >= 1 && p + 2 < input_ids.len() {
5391 let (d1, mut hx) = self.mtp_step_h(
5395 m,
5396 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5397 input_ids[p + 1],
5398 p,
5399 );
5400 let mut ok = d1 == input_ids[p + 2];
5401 Self::chain_probe_note(0, ok);
5402 let mut d_prev = d1;
5403 let mut extra = 0usize;
5404 for j in 1..probe {
5405 if p + 2 + j >= input_ids.len() {
5406 break;
5407 }
5408 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5409 extra += 1;
5410 ok = ok && dj == input_ids[p + 2 + j];
5411 Self::chain_probe_note(j, ok);
5412 d_prev = dj;
5413 hx = hj;
5414 }
5415 m.kv.truncate_last(extra);
5416 } else {
5417 let _ = self.mtp_step(
5418 m,
5419 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5420 input_ids[p + 1],
5421 p,
5422 );
5423 }
5424 }
5425 }
5426 }
5427 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5428 pos = end;
5429 }
5430 }
5431 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5432 if task_mask.is_none()
5433 && !dyn_prefill
5434 && !graph_prefill
5435 && !pair_off
5436 && self.pair_supported()
5437 && o1_prefill.is_none()
5438 {
5439 while pos + 1 < input_ids.len()
5440 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5441 {
5442 let e1 = self.embed_single(input_ids[pos]);
5443 let e2 = self.embed_single(input_ids[pos + 1]);
5444 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5445 if mimo_spec {
5446 self.mimo_note_rows(&h1, pos);
5447 self.mimo_note_rows(&h2, pos + 1);
5448 }
5449 self.commit_linear_scratch();
5451 if let Some(m) = &mut mtp {
5452 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5453 if pos + 2 < input_ids.len() {
5454 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5455 .ok()
5456 .and_then(|v| v.parse().ok())
5457 .unwrap_or(0);
5458 if probe >= 1 && pos + 3 < input_ids.len() {
5459 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5463 let mut ok = d1 == input_ids[pos + 3];
5464 Self::chain_probe_note(0, ok);
5465 let mut d_prev = d1;
5466 let mut extra = 0usize;
5467 for j in 1..probe {
5468 if pos + 3 + j >= input_ids.len() {
5469 break;
5470 }
5471 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5472 extra += 1;
5473 ok = ok && dj == input_ids[pos + 3 + j];
5474 Self::chain_probe_note(j, ok);
5475 d_prev = dj;
5476 hx = hj;
5477 }
5478 m.kv.truncate_last(extra);
5479 } else {
5480 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5481 }
5482 }
5483 }
5484 hidden = h2;
5485 pos += 2;
5486 }
5487 }
5488 let o1_batch_ready = o1_sealed
5501 && o1_prefill.is_some()
5502 && mtp.is_none()
5503 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5504 && (0..self.num_layers).all(|li| {
5505 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5506 cache.o1.is_none() || cache.o1_views().is_some()
5507 });
5508 let mtp_batch_prefill = mtp.is_some()
5513 && graph_prefill
5514 && task_mask.is_none()
5515 && !dyn_prefill
5516 && !self.o1_active()
5517 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5518 if batch_k > 0
5519 && (graph_prefill || o1_batch_ready)
5520 && task_mask.is_none()
5521 && (!self.o1_active() || o1_batch_ready)
5522 && (mtp.is_none() || mtp_batch_prefill)
5523 && !dyn_prefill
5524 && pos + 1 < input_ids.len()
5525 {
5526 let hs = self.hidden_size;
5527 let chunk = batch_k;
5528 while pos < input_ids.len() {
5529 let end = (pos + chunk).min(input_ids.len());
5530 let bk = end - pos;
5531 let mut hiddens = vec![0f32; bk * hs];
5532 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5533 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5534 }
5535 let positions: Vec<usize> = (pos..end).collect();
5536 let t_chunk = std::time::Instant::now();
5537 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5538 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5539 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5540 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5541 eprintln!(
5542 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5543 if o1_batch_ready {
5544 "o1"
5545 } else if mtp_batch_prefill {
5546 "ordinary_mtp"
5547 } else {
5548 "ordinary"
5549 },
5550 bk as f64 / (ms / 1000.0)
5551 );
5552 }
5553 {
5554 use std::sync::atomic::{AtomicBool, Ordering};
5555 static SAID: AtomicBool = AtomicBool::new(false);
5556 if !SAID.swap(true, Ordering::Relaxed) {
5557 if ok_b {
5558 tracing::info!(
5559 "batched prefill: ACTIVE mode={} (k={bk})",
5560 if o1_batch_ready {
5561 "o1"
5562 } else if mtp_batch_prefill {
5563 "ordinary_mtp"
5564 } else {
5565 "ordinary"
5566 }
5567 );
5568 } else {
5569 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5570 }
5571 }
5572 }
5573 if ok_b {
5574 if mimo_spec {
5575 self.mimo_note_rows(&hiddens, pos);
5576 }
5577 if mtp_batch_prefill {
5578 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5579 if n_pairs > 0 {
5580 let rows: Vec<Vec<f32>> = (0..n_pairs)
5586 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5587 .collect();
5588 let pairs: Vec<(&[f32], u32)> = rows
5589 .iter()
5590 .enumerate()
5591 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5592 .collect();
5593 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5594 eprintln!(
5595 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5596 pos,
5597 n_pairs,
5598 pos + n_pairs - 1,
5599 );
5600 }
5601 let warm_error = if let Some(m) = mtp.as_mut() {
5602 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5603 } else {
5604 None
5605 };
5606 if let Some(err) = warm_error {
5607 self.finish_generation(&mut mtp, &mut router, true);
5612 return Err(err.to_string());
5613 }
5614 }
5615 }
5616 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5617 pos = end;
5618 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5619 self.finish_generation(&mut mtp, &mut router, true);
5624 return Err(if o1_batch_ready {
5625 "sealed O(1) batch graph failed after admission".to_string()
5626 } else {
5627 "ordinary recurrent batch graph failed after admission".to_string()
5628 });
5629 } else {
5630 break; }
5632 }
5633 }
5634 if graph_prefill
5638 && task_mask.is_none()
5639 && mtp.is_none()
5640 && !dyn_prefill
5641 && pos == 0
5642 && input_ids.len() > 1
5643 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5644 {
5645 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5646 self.graph_logits = Some(lg);
5647 hidden = vec![0.0; self.hidden_size];
5648 pos = input_ids.len();
5649 }
5650 }
5651 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5652 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5653 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5654 if mimo_spec {
5655 self.mimo_note_rows(&hidden, pos);
5656 }
5657 if let Some(m) = &mut mtp {
5658 if pos + 1 < input_ids.len() {
5659 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5665 .ok()
5666 .and_then(|v| v.parse().ok())
5667 .unwrap_or(0);
5668 if probe >= 1 && pos + 2 < input_ids.len() {
5669 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5670 let mut ok = d1 == input_ids[pos + 2];
5671 Self::chain_probe_note(0, ok);
5672 let mut d_prev = d1;
5673 let mut extra = 0usize;
5674 for j in 1..probe {
5675 if pos + 2 + j >= input_ids.len() {
5676 break;
5677 }
5678 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5679 extra += 1;
5680 ok = ok && dj == input_ids[pos + 2 + j];
5681 Self::chain_probe_note(j, ok);
5682 d_prev = dj;
5683 hx = hj;
5684 }
5685 m.kv.truncate_last(extra);
5688 } else {
5689 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5690 }
5691 }
5692 }
5693 pos += 1;
5694 }
5695 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5696 eprintln!(
5697 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5698 input_ids.len(),
5699 _tpf.elapsed().as_secs_f64() * 1000.0
5700 );
5701 }
5702 if self
5703 .graph_failed
5704 .swap(false, std::sync::atomic::Ordering::Relaxed)
5705 {
5706 self.finish_generation(&mut mtp, &mut router, true);
5711 return Err("GPU token graph failed during prefill".to_string());
5712 }
5713 if self
5716 .cancel
5717 .swap(false, std::sync::atomic::Ordering::Relaxed)
5718 {
5719 self.finish_generation(&mut mtp, &mut router, true);
5723 return Ok(GenerateResult {
5724 text: String::new(),
5725 token_ids: Vec::new(),
5726 prompt_tokens: input_ids.len(),
5727 tokens_generated: 0,
5728 finish_reason: "cancelled".to_string(),
5729 mtp_drafted: 0,
5730 mtp_accepted: 0,
5731 token_confidence: Vec::new(),
5732 traces: Vec::new(),
5733 });
5734 }
5735
5736 if !o1_sealed {
5739 match self.o1_seal_checked() {
5740 Ok(_) => {}
5741 Err(err) => {
5742 self.finish_generation(&mut mtp, &mut router, true);
5743 return Err(err);
5744 }
5745 }
5746 }
5747
5748 macro_rules! commit {
5750 ($id:expr) => {{
5751 all_ids.push($id);
5752 generated += 1;
5753 self.note_draft_id($id);
5754 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5755 finish_reason = "stop".to_string();
5756 false
5757 } else {
5758 let token_text = self.tokenizer.decode_token($id);
5759 let mut go = true;
5760 if let Some(ref mut cb) = on_token {
5761 if !cb(&token_text) {
5762 finish_reason = "cancelled".to_string();
5763 go = false;
5764 }
5765 }
5766 go
5767 }
5768 }};
5769 }
5770
5771 let mut spec_trial = SpecTrial::Spec {
5782 t0: std::time::Instant::now(),
5783 gen0: generated,
5784 rounds: 0,
5785 };
5786 let mut spec_mon = SpecMon {
5792 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5793 ..SpecMon::default()
5794 };
5795 let mut spec_watchdog_off = false;
5796 let mut spec_walls: Vec<f32> = Vec::new();
5799 let mut spec_round_end: Option<std::time::Instant> = None;
5802 if mimo_spec {
5803 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5804 if let Some(mut st) = self.mimo_mtp.take() {
5805 self.mimo_mtp_probe(&mut st, input_ids, &path);
5806 self.mimo_mtp = Some(st);
5807 }
5808 }
5809 }
5810 let mut next_pos = input_ids.len();
5812 'decode: while generated < max_tokens {
5813 if self
5814 .graph_failed
5815 .swap(false, std::sync::atomic::Ordering::Relaxed)
5816 {
5817 self.finish_generation(&mut mtp, &mut router, true);
5822 return Err("GPU token graph failed during decode".to_string());
5823 }
5824 if self
5825 .cancel
5826 .swap(false, std::sync::atomic::Ordering::Relaxed)
5827 {
5828 finish_reason = "cancelled".to_string();
5829 break 'decode;
5830 }
5831 if mimo_spec && next_pos > 0 {
5836 self.mimo_note_rows(&hidden, next_pos - 1);
5839 }
5840 let forced = self.spec_forced.take();
5841 let mut logits = match (forced, self.graph_logits.take()) {
5842 (Some(_), _) => Vec::new(),
5843 (None, Some(lg)) => lg,
5844 (None, None) => {
5845 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5846 #[cfg(feature = "gpu")]
5851 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5852 lg
5853 } else {
5854 inference::rms_norm_into(
5855 &hidden,
5856 &self.weights.final_norm,
5857 self.rms_eps,
5858 self.norm_style,
5859 &mut self.ws.n1,
5860 );
5861 self.lm_head_forward(&self.ws.n1)
5862 };
5863 #[cfg(not(feature = "gpu"))]
5864 let logits_out = {
5865 inference::rms_norm_into(
5866 &hidden,
5867 &self.weights.final_norm,
5868 self.rms_eps,
5869 self.norm_style,
5870 &mut self.ws.n1,
5871 );
5872 self.lm_head_forward(&self.ws.n1)
5873 };
5874 logits_out
5875 }
5876 };
5877 if generated
5880 == std::env::var("CMF_LOGIT_DUMP_STEP")
5881 .ok()
5882 .and_then(|v| v.parse().ok())
5883 .unwrap_or(0)
5884 {
5885 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5886 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5887 for v in hidden.iter().chain(logits.iter()) {
5888 bytes.extend_from_slice(&v.to_le_bytes());
5889 }
5890 if let Err(e) = std::fs::write(&path, &bytes) {
5891 eprintln!("logit dump: failed to write {path}: {e}");
5892 self.finish_generation(&mut mtp, &mut router, true);
5893 return Err(format!("logit dump write failed: {e}"));
5894 }
5895 }
5896 }
5897 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5901 if !logits.is_empty() {
5902 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5903 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5904 if let Err(e) =
5905 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5906 {
5907 eprintln!("logit dump: failed to write {}: {e}", path.display());
5908 }
5909 }
5910 }
5911 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5912 Some(c) => c,
5913 None => {
5914 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5915 sampler::sample_with_scratch_pool(
5916 &logits,
5917 &self.sampler_config,
5918 self.sampler_config.penalty_past(&all_ids, bounded_native),
5919 &mut self.rng,
5920 &mut self.sampler_scratch,
5921 self.pool.as_deref(),
5922 )
5923 }
5924 };
5925 if self.confidence_on {
5926 confidence.push(if logits.is_empty() {
5927 0.0
5928 } else {
5929 sampler::top1_prob_pool(
5930 self.pool.as_deref(),
5931 &mut self.sampler_scratch,
5932 &logits,
5933 t_next,
5934 calib_temp,
5935 )
5936 });
5937 }
5938 if !logits.is_empty() {
5939 attention::recycle_buf(&mut logits);
5940 }
5941 if trace_on {
5942 let skill = router.as_ref().and_then(|r| r.active_id());
5946 traces.push(TokenTrace {
5947 t: generated,
5948 token_id: t_next,
5949 confidence: confidence.last().copied().unwrap_or(0.0),
5950 active_skill: skill,
5951 recon: None,
5952 switched: false,
5953 });
5954 }
5955 if !commit!(t_next) {
5956 break 'decode;
5957 }
5958 if generated >= max_tokens {
5959 break 'decode;
5960 }
5961
5962 self.swa_trim_tails();
5965 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5966 static SAID: std::sync::Once = std::sync::Once::new();
5972 SAID.call_once(|| {
5973 tracing::warn!(
5974 "KV cache full at {} positions — evicting half; quality \
5975 will degrade. Raise CMF_MAX_SEQ.",
5976 self.kv_cache.max_seq_len,
5977 );
5978 });
5979 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5980 self.kv_cache.evict(keep);
5981 }
5982
5983 if graph_spec {
5986 match spec_trial {
5987 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5988 spec_mon.plain_ms =
5989 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5990 let keep = spec_mon.pays();
5991 tracing::info!(
5992 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5993 spec_mon.tokens,
5994 spec_mon.round_ms,
5995 spec_mon.plain_ms,
5996 if keep { "speculating" } else { "plain" }
5997 );
5998 spec_mon.fails = 0;
5999 spec_trial = SpecTrial::Decided {
6000 spec: keep,
6001 recheck_at: if keep { usize::MAX } else { generated + 128 },
6002 };
6003 }
6004 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6005 spec_mon.n = 0;
6006 spec_mon.zero_accept_rounds = 0;
6007 spec_trial = SpecTrial::Spec {
6008 t0: std::time::Instant::now(),
6009 gen0: generated,
6010 rounds: 0,
6011 };
6012 }
6013 _ => {}
6014 }
6015 spec_watchdog_off = matches!(
6016 spec_trial,
6017 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6018 );
6019 }
6020 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6022 let budget = max_tokens - generated - 1;
6023 if let Some(mut st) = self.mimo_mtp.take() {
6024 let k = st.depth.min(budget);
6025 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6026 self.mimo_mtp = Some(st);
6027 let r = match r {
6028 Ok(r) => r,
6029 Err(err) => {
6030 self.finish_generation(&mut mtp, &mut router, true);
6031 return Err(err);
6032 }
6033 };
6034 if let Some(r) = r {
6035 drafted += r.drafted;
6036 accepted += r.accepted.len();
6037 let mut stopped = false;
6038 for &id in &r.accepted {
6039 if self.confidence_on {
6040 confidence.push(0.0);
6041 }
6042 if !commit!(id) {
6043 stopped = true;
6044 break;
6045 }
6046 }
6047 if stopped {
6048 break 'decode;
6049 }
6050 next_pos += r.accepted.len() + 1;
6051 hidden = r.hidden;
6052 self.graph_logits = Some(r.logits);
6055 continue 'decode;
6056 }
6057 }
6058 }
6059 #[cfg(feature = "gpu")]
6062 if self.speculative
6063 && self.qwen4_exp.is_some()
6064 && task_mask.is_none()
6065 && self.sampler_config.temperature < 1e-6
6066 && generated + 1 < max_tokens
6067 && next_pos > 0
6068 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6069 {
6070 let r = match &mut self.qwen4_exp {
6071 Some(b) => crate::qwen4_exp::spec_round(
6072 &b.0,
6073 &b.1,
6074 &b.2,
6075 &mut b.3,
6076 next_pos,
6077 &all_ids,
6078 &self.inv_freq,
6079 self.pool.as_deref(),
6080 ),
6081 None => None,
6082 };
6083 if let Some(r) = r {
6084 drafted += r.drafted;
6085 accepted += r.accepted.len();
6086 let mut stopped = false;
6087 for &id in &r.accepted {
6088 if self.confidence_on {
6089 confidence.push(0.0);
6090 }
6091 if !commit!(id) {
6092 stopped = true;
6093 break;
6094 }
6095 }
6096 if stopped {
6097 break 'decode;
6098 }
6099 next_pos += r.accepted.len() + 1;
6100 hidden.fill(0.0);
6101 self.graph_logits = Some(r.logits);
6102 continue 'decode;
6103 }
6104 }
6105 match &mut mtp {
6106 #[cfg(feature = "gpu")]
6108 Some(m)
6109 if graph_spec
6110 && !spec_watchdog_off
6111 && generated + 1 < max_tokens
6112 && next_pos > 0 =>
6113 {
6114 let t_round = std::time::Instant::now();
6115 if spec_time_level() >= 2 {
6116 if let Some(t) = spec_round_end.take() {
6117 eprintln!(
6118 "spec-gap {:.2} ms (host between rounds)",
6119 t.elapsed().as_secs_f64() * 1e3
6120 );
6121 }
6122 }
6123 spec_stamps_begin();
6124 #[cfg(target_os = "macos")]
6129 let allocs0 =
6130 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6131 #[cfg(not(target_os = "macos"))]
6132 let allocs0 = 0u64;
6133 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6134 m,
6135 &hidden,
6136 t_next,
6137 next_pos,
6138 &mut drafted,
6139 &mut accepted,
6140 &mut all_ids,
6141 max_tokens - generated,
6142 ) {
6143 next_pos = n_pos;
6144 hidden = new_h;
6145 let level = spec_time_level();
6146 if level > 0 {
6147 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6148 let stamps = spec_stamps_take();
6149 let median = if spec_walls.len() >= 3 {
6152 let mut s = spec_walls.clone();
6153 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6154 Some(s[s.len() / 2])
6155 } else {
6156 None
6157 };
6158 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6159 #[cfg(target_os = "macos")]
6160 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6161 .load(std::sync::atomic::Ordering::Relaxed)
6162 - allocs0;
6163 #[cfg(not(target_os = "macos"))]
6164 let allocs = allocs0;
6165 eprintln!(
6166 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6167 extra.len() + 1,
6168 if allocs > 0 {
6169 format!(" [{allocs} new device buffers]")
6170 } else {
6171 String::new()
6172 },
6173 match (outlier, median) {
6174 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6175 _ => String::new(),
6176 }
6177 );
6178 if level >= 2 || outlier {
6179 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6180 eprintln!(
6181 "spec-stamps: {}| untracked {:.1}",
6182 spec_stamps_format(&stamps),
6183 wall - sum
6184 );
6185 }
6186 if spec_mon.n >= 1 {
6187 spec_walls.push(wall);
6188 }
6189 }
6190 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6194 spec_trial = Self::spec_trial_round(
6197 spec_trial,
6198 &mut spec_mon,
6199 generated + extra.len() + 1,
6200 );
6201 let mut stopped = false;
6202 for &id in &extra {
6203 if self.confidence_on {
6204 confidence.push(0.0);
6205 }
6206 if trace_on {
6219 traces.push(TokenTrace {
6220 t: generated,
6221 token_id: id,
6222 confidence: confidence.last().copied().unwrap_or(0.0),
6223 active_skill: None,
6224 recon: None,
6225 switched: false,
6226 });
6227 }
6228 if !commit!(id) {
6229 stopped = true;
6230 break;
6231 }
6232 }
6233 if stopped {
6234 break 'decode;
6235 }
6236 if spec_time_level() >= 2 {
6237 spec_round_end = Some(std::time::Instant::now());
6238 }
6239 continue 'decode;
6240 }
6241 if self
6242 .graph_failed
6243 .swap(false, std::sync::atomic::Ordering::Relaxed)
6244 {
6245 self.finish_generation(&mut mtp, &mut router, true);
6251 return Err("GPU MTP graph failed during speculative decode".to_string());
6252 }
6253 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6264 spec_mon.tokens = 0.0;
6265 spec_mon.fails = 3;
6266 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6267 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6268 next_pos += 1;
6269 continue 'decode;
6270 }
6271 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6273 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6274 drafted += 1;
6275 let emb1 = self.embed_single(t_next);
6276 let emb2 = self.embed_single(draft);
6277 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6278
6279 inference::rms_norm_into(
6280 &h1,
6281 &self.weights.final_norm,
6282 self.rms_eps,
6283 self.norm_style,
6284 &mut self.ws.n1,
6285 );
6286 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6287 let t_after = sampler::sample_with_scratch_pool(
6288 &logits1,
6289 &self.sampler_config,
6290 self.sampler_config.penalty_past(&all_ids, bounded_native),
6291 &mut self.rng,
6292 &mut self.sampler_scratch,
6293 self.pool.as_deref(),
6294 );
6295 if self.confidence_on {
6296 confidence.push(sampler::top1_prob_pool(
6297 self.pool.as_deref(),
6298 &mut self.sampler_scratch,
6299 &logits1,
6300 t_after,
6301 calib_temp,
6302 ));
6303 }
6304 attention::recycle_buf(&mut logits1);
6305 if trace_on {
6306 traces.push(TokenTrace {
6309 t: generated,
6310 token_id: t_after,
6311 confidence: confidence.last().copied().unwrap_or(0.0),
6312 active_skill: None,
6313 recon: None,
6314 switched: false,
6315 });
6316 }
6317 let stop = !commit!(t_after);
6318
6319 if t_after == draft {
6320 accepted += 1;
6321 self.commit_linear_scratch();
6322 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6323 hidden = h2;
6324 next_pos += 2;
6325 } else {
6326 for layer in &mut self.kv_cache.layers {
6328 layer.truncate_last(1);
6329 }
6330 if !stop {
6331 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6332 hidden = self.forward_layers(
6333 &self.embed_single(t_after),
6334 next_pos + 1,
6335 None,
6336 );
6337 }
6338 next_pos += 2;
6339 }
6340 if stop {
6341 break 'decode;
6342 }
6343 }
6344 _ => {
6346 #[cfg(feature = "gpu")]
6351 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6352 static SAID: std::sync::Once = std::sync::Once::new();
6353 SAID.call_once(|| {
6354 eprintln!(
6355 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
6356 !self.dsv4_mtp.is_empty(),
6357 task_mask.is_none(),
6358 router.is_none(),
6359 !trace_on,
6360 self.sampler_config.temperature < 1e-6,
6361 self.sampler_config.repetition_penalty == 1.0,
6362 );
6363 });
6364 }
6365 #[cfg(feature = "gpu")]
6366 if Self::dsv4_spec_on()
6367 && self.dsv4.is_some()
6368 && !self.dsv4_mtp.is_empty()
6369 && task_mask.is_none()
6370 && router.is_none()
6371 && !trace_on
6372 && self.sampler_config.temperature < 1e-6
6373 && self.sampler_config.repetition_penalty == 1.0
6374 && generated + 1 < max_tokens
6375 && all_ids.len() >= 2
6376 && generated >= dsv4_spec_retry_at
6377 {
6378 let tip_token = all_ids[all_ids.len() - 2];
6379 let drafted0 = drafted;
6380 let round = self.dsv4_spec_step(
6381 tip_token,
6382 t_next,
6383 next_pos,
6384 max_tokens.saturating_sub(generated),
6385 &mut drafted,
6386 &mut accepted,
6387 );
6388 if drafted > drafted0 {
6389 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6390 if useful {
6391 dsv4_spec_bad = 0;
6392 } else {
6393 dsv4_spec_bad += 1;
6394 if dsv4_spec_bad >= 2 {
6395 dsv4_spec_bad = 0;
6396 dsv4_spec_retry_at = generated.saturating_add(32);
6397 tracing::info!(
6398 "dsv4: draft не окупился дважды — точный walk на 32 токена"
6399 );
6400 }
6401 }
6402 }
6403 if let Some((extra, n_pos)) = round {
6404 next_pos = n_pos;
6405 let mut stopped = false;
6406 for &id in &extra {
6407 if self.confidence_on {
6408 confidence.push(0.0);
6409 }
6410 if !commit!(id) {
6411 stopped = true;
6412 break;
6413 }
6414 }
6415 if stopped {
6416 break 'decode;
6417 }
6418 continue 'decode;
6419 }
6420 }
6421 self.graph_want_logits = fuse_lm;
6422 let mut t_fwd = t_next;
6428 let pure_greedy = self.sampler_config.temperature < 1e-6
6429 && self.sampler_config.repetition_penalty == 1.0
6430 && self.sampler_config.presence_penalty == 0.0
6431 && self.sampler_config.suppress_tokens.is_empty();
6432 let burst_k = std::env::var("CMF_MULTISTEP")
6437 .ok()
6438 .and_then(|v| v.parse::<usize>().ok())
6439 .unwrap_or(0);
6440 if pure_greedy
6441 && burst_k >= 1
6442 && fuse_lm
6443 && task_mask.is_none()
6444 && router.is_none()
6445 && !trace_on
6446 && !self.confidence_on
6447 && std::env::var_os("CMF_FORCE_IDS").is_none()
6452 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6453 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6454 {
6455 let mut stopped = false;
6456 loop {
6457 if self
6462 .cancel
6463 .swap(false, std::sync::atomic::Ordering::Relaxed)
6464 {
6465 finish_reason = "cancelled".to_string();
6466 stopped = true;
6467 break;
6468 }
6469 let room = max_tokens.saturating_sub(generated);
6470 if room <= 2 {
6471 break;
6472 }
6473 let k = burst_k.min(room - 1);
6474 if k < 1 {
6475 break;
6476 }
6477 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6478 if self
6479 .graph_failed
6480 .swap(false, std::sync::atomic::Ordering::Relaxed)
6481 {
6482 self.finish_generation(&mut mtp, &mut router, true);
6483 return Err(
6484 "GPU token graph failed during greedy burst".to_string()
6485 );
6486 }
6487 break;
6488 };
6489 next_pos += k;
6490 for &id in &ids {
6491 if !commit!(id) {
6492 stopped = true;
6493 break;
6494 }
6495 }
6496 if stopped {
6497 break;
6498 }
6499 t_fwd = *ids.last().unwrap();
6500 }
6501 if stopped {
6502 break 'decode;
6503 }
6504 }
6505 #[cfg(target_os = "macos")]
6515 if graph_spec
6516 && spec_watchdog_off
6517 && next_pos > 0
6518 && self.mtp_graph_mode == Some(true)
6519 && crate::gpu::q1_force()
6520 {
6521 if let Some(m) = mtp.as_mut() {
6522 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6523 }
6524 }
6525 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6526 next_pos += 1;
6527 if let Some(r) = &mut router {
6530 let phi = self.dyn_phi_ema.clone();
6531 let decision = r.step(&phi, generated);
6532 if let Some(new_active) = decision {
6533 let _ = self.set_active_skill(new_active);
6534 }
6535 if trace_on {
6538 if let Some(last) = traces.last_mut() {
6539 let e = r.last_best_e();
6540 last.recon = e.is_finite().then_some(e);
6541 last.switched = decision.is_some();
6542 }
6543 }
6544 }
6545 }
6546 }
6547 }
6548
6549 let cancelled = finish_reason == "cancelled";
6550 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6554 if mimo_spec {
6555 if let Some(st) = self.mimo_mtp.as_ref() {
6556 let line = st.stats.line();
6557 tracing::info!("{line}");
6558 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6559 eprintln!("{line}");
6560 }
6561 }
6562 }
6563 self.finish_generation(&mut mtp, &mut router, cancelled);
6564
6565 let output_ids = &all_ids[input_ids.len()..];
6566 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6570 let consumed = std::mem::take(&mut all_ids);
6575 if dyn_switched {
6576 self.clear_sequence_state();
6577 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6578 self.clear_history();
6579 } else {
6580 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6581 }
6582 all_ids = consumed;
6583 let output_ids = &all_ids[input_ids.len()..];
6584 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6586 Ok(GenerateResult {
6587 text: self.tokenizer.decode(output_ids),
6588 token_ids: output_ids.to_vec(),
6589 prompt_tokens: input_ids.len(),
6590 tokens_generated: generated,
6591 finish_reason,
6592 mtp_drafted: drafted,
6593 mtp_accepted: accepted,
6594 token_confidence: confidence,
6595 traces,
6596 })
6597 }
6598
6599 fn mtp_step(
6603 &mut self,
6604 m: &mut MtpModule,
6605 hidden: &[f32],
6606 next_token: u32,
6607 position: usize,
6608 ) -> u32 {
6609 self.mtp_step_h(m, hidden, next_token, position).0
6610 }
6611
6612 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6616 use std::sync::Mutex;
6617 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6618 let mut t = T.lock().unwrap();
6619 if t.len() <= depth {
6620 t.resize(depth + 1, (0, 0));
6621 }
6622 t[depth].0 += 1;
6623 t[depth].1 += prefix_ok as u64;
6624 if depth == 0 && t[0].0 % 128 == 0 {
6625 let line: Vec<String> = t
6626 .iter()
6627 .enumerate()
6628 .map(|(d, (n, k))| {
6629 format!(
6630 "d{}={:.0}%({n})",
6631 d + 1,
6632 100.0 * *k as f64 / (*n).max(1) as f64
6633 )
6634 })
6635 .collect();
6636 eprintln!("mtp-chain: {}", line.join(" "));
6637 }
6638 }
6639
6640 fn mtp_step_hl(
6648 &mut self,
6649 m: &mut MtpModule,
6650 hidden: &[f32],
6651 next_token: u32,
6652 position: usize,
6653 ) -> (Vec<f32>, Vec<f32>) {
6654 #[cfg(target_os = "macos")]
6659 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6660 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6661 self.mtp_graph_mode = Some(true);
6662 return r;
6663 }
6664 if self.mtp_graph_mode == Some(true) {
6665 tracing::error!("mtp Metal graph failed after admission");
6666 self.clear_sequence_state();
6667 self.graph_failed
6668 .store(true, std::sync::atomic::Ordering::Relaxed);
6669 self.cancel
6670 .store(true, std::sync::atomic::Ordering::Relaxed);
6671 return (Vec::new(), Vec::new());
6672 }
6673 self.mtp_graph_mode = Some(false);
6674 }
6675 #[cfg(feature = "gpu")]
6676 if self.mtp_graph_mode != Some(false) {
6677 if !self.mtp_graph_ok(m) {
6678 if self.mtp_graph_mode == Some(true) {
6679 tracing::error!("mtp graph became unavailable after admission");
6684 self.clear_sequence_state();
6685 self.graph_failed
6686 .store(true, std::sync::atomic::Ordering::Relaxed);
6687 self.cancel
6688 .store(true, std::sync::atomic::Ordering::Relaxed);
6689 return (Vec::new(), Vec::new());
6690 }
6691 self.mtp_graph_mode = Some(false);
6692 } else {
6693 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6694 self.mtp_graph_mode = Some(true);
6695 return r;
6696 }
6697 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6698 return (Vec::new(), Vec::new());
6705 }
6706 tracing::error!("mtp graph failed or declined after admission");
6710 self.clear_sequence_state();
6711 self.graph_failed
6712 .store(true, std::sync::atomic::Ordering::Relaxed);
6713 self.cancel
6714 .store(true, std::sync::atomic::Ordering::Relaxed);
6715 return (Vec::new(), Vec::new());
6716 }
6717 }
6718 let e = self.embed_single(next_token);
6722 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6723 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6724 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6725 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6726 let mut x = vec![0.0f32; self.hidden_size];
6727 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6728
6729 let lw = &m.layer;
6731 inference::rms_norm_into(
6732 &x,
6733 &lw.input_norm,
6734 self.rms_eps,
6735 self.norm_style,
6736 &mut self.ws.n1,
6737 );
6738 let attn = match &lw.attn {
6739 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6741 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6742 AttnKind::Full {
6743 wq,
6744 wk,
6745 wv,
6746 wo,
6747 q_norm,
6748 k_norm,
6749 output_gate,
6750 softplus_gate,
6751 bias,
6752 } => {
6753 let mut cfg = self.attn_cfg(position);
6754 cfg.q_norm = q_norm.as_deref();
6755 cfg.k_norm = k_norm.as_deref();
6756 cfg.output_gate = *output_gate;
6757 cfg.softplus_gate = softplus_gate
6758 .as_ref()
6759 .map(|(gate, per_head)| (gate, *per_head));
6760 cfg.bias = bias
6761 .as_ref()
6762 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6763 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6764 }
6765 AttnKind::Linear(_)
6766 | AttnKind::LinearGdn(_)
6767 | AttnKind::ShortConv(_)
6768 | AttnKind::Bounded(_) => {
6769 unreachable!("MTP block is full attention")
6770 }
6771 };
6772 for (i, &a) in attn.iter().enumerate() {
6773 x[i] += a;
6774 }
6775 inference::rms_norm_into(
6776 &x,
6777 &lw.post_norm,
6778 self.rms_eps,
6779 self.norm_style,
6780 &mut self.ws.p1,
6781 );
6782 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6783 for (i, &f) in ffn.iter().enumerate() {
6784 x[i] += f;
6785 }
6786
6787 inference::rms_norm_into(
6788 &x,
6789 &m.final_norm,
6790 self.rms_eps,
6791 self.norm_style,
6792 &mut self.ws.n1,
6793 );
6794 let lg = self.lm_head_forward(&self.ws.n1);
6795 (lg, x)
6796 }
6797
6798 fn mtp_step_h(
6800 &mut self,
6801 m: &mut MtpModule,
6802 hidden: &[f32],
6803 next_token: u32,
6804 position: usize,
6805 ) -> (u32, Vec<f32>) {
6806 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6807 let draft = sampler::argmax(&lg);
6808 attention::recycle_buf(&mut lg);
6809 (draft, x)
6810 }
6811
6812 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6818 match trial {
6819 SpecTrial::Spec { t0, gen0, rounds } => {
6820 let rounds = rounds + 1;
6821 if mon.repeated_zero_acceptance() {
6829 tracing::info!(
6830 "speculation trial: no accepted drafts in two rounds — timing plain"
6831 );
6832 return SpecTrial::Plain {
6833 t0: std::time::Instant::now(),
6834 gen0: generated,
6835 };
6836 }
6837 if rounds >= 5 {
6838 if mon.plain_ms > 0.0 {
6839 let keep = mon.pays();
6840 mon.fails = 0;
6841 tracing::info!(
6842 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6843 mon.tokens,
6844 mon.round_ms,
6845 mon.plain_ms,
6846 if keep { "speculating" } else { "plain" }
6847 );
6848 SpecTrial::Decided {
6849 spec: keep,
6850 recheck_at: if keep { usize::MAX } else { generated + 128 },
6851 }
6852 } else if mon.pays() {
6853 mon.fails = 0;
6858 tracing::info!(
6859 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6860 mon.tokens,
6861 mon.round_ms,
6862 );
6863 SpecTrial::Decided {
6864 spec: true,
6865 recheck_at: usize::MAX,
6866 }
6867 } else {
6868 SpecTrial::Plain {
6869 t0: std::time::Instant::now(),
6870 gen0: generated,
6871 }
6872 }
6873 } else {
6874 SpecTrial::Spec { t0, gen0, rounds }
6875 }
6876 }
6877 SpecTrial::Decided { spec: true, .. } => {
6878 if mon.pays() {
6879 mon.fails = 0;
6880 trial
6881 } else {
6882 mon.fails += 1;
6883 if mon.fails >= 4 {
6884 if mon.plain_ms <= 0.0 {
6885 tracing::info!(
6889 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6890 mon.tokens,
6891 mon.round_ms,
6892 );
6893 return SpecTrial::Plain {
6894 t0: std::time::Instant::now(),
6895 gen0: generated,
6896 };
6897 }
6898 tracing::info!(
6899 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6900 mon.tokens,
6901 mon.round_ms,
6902 mon.plain_ms
6903 );
6904 SpecTrial::Decided {
6905 spec: false,
6906 recheck_at: generated + 128,
6907 }
6908 } else {
6909 trial
6910 }
6911 }
6912 }
6913 other => other,
6914 }
6915 }
6916
6917 fn mtp_kv_id(&self) -> u64 {
6920 self.graph_kv_id | (1u64 << 40)
6921 }
6922
6923 const MTP_LAYER_BASE: usize = 0;
6928
6929 #[cfg(feature = "gpu")]
6936 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6937 self.mtp_graph_mode != Some(true)
6938 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6939 }
6940
6941 #[cfg(feature = "gpu")]
6947 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6948 let mut ok = true;
6949 let mut expected = false;
6950 for li in 0..self.num_layers {
6951 if matches!(
6952 self.weights.layers[self.phys_layer(li)].attn,
6953 AttnKind::Full { .. }
6954 ) {
6955 expected = true;
6956 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6957 }
6958 }
6959 !expected || ok
6960 }
6961
6962 fn graph_gdn_layer_count(&self) -> usize {
6966 (0..self.num_layers)
6967 .filter(|&li| {
6968 matches!(
6969 &self.weights.layers[self.phys_layer(li)].attn,
6970 AttnKind::LinearGdn(_)
6971 )
6972 })
6973 .count()
6974 }
6975
6976 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6979 let e = self.embed_single(next_token);
6980 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6981 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6982 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6983 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6984 let mut x = vec![0.0f32; self.hidden_size];
6985 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6986 x
6987 }
6988
6989 #[cfg(feature = "gpu")]
6992 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6993 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6994 return false;
6995 }
6996 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6997 || !crate::gpu::enabled_here()
6998 || self.attn_softcap > 0.0
6999 || self.attention_heads_per_layer.is_some()
7000 || self.v_head_dim.is_some()
7003 {
7004 return false;
7005 }
7006 matches!(
7007 &m.layer.attn,
7008 AttnKind::Full {
7009 softplus_gate: None,
7010 ..
7011 }
7012 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7013 }
7014
7015 #[cfg(feature = "gpu")]
7019 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7020 if !self.mtp_block_graph_ok(m) {
7021 return false;
7022 }
7023 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7024 return false;
7025 };
7026 let FfnKind::Dense(d) = &m.layer.ffn else {
7027 return false;
7028 };
7029 d.segs.is_empty()
7030 && wq.graph_weight().is_some()
7031 && wk.graph_weight().is_some()
7032 && wv.graph_weight().is_some()
7033 && wo.graph_weight().is_some()
7034 && d.gate_proj.graph_weight().is_some()
7035 && d.up_proj.graph_weight().is_some()
7036 && d.down_proj.graph_weight().is_some()
7037 && self.weights.lm_head.graph_weight().is_some()
7038 }
7039
7040 #[cfg(feature = "gpu")]
7046 fn mtp_step_graph(
7047 &mut self,
7048 m: &mut MtpModule,
7049 hidden: &[f32],
7050 next_token: u32,
7051 position: usize,
7052 ) -> Option<(Vec<f32>, Vec<f32>)> {
7053 if !self.mtp_graph_ok(m) {
7054 return None;
7055 }
7056 let lw = &m.layer;
7057 let AttnKind::Full {
7058 wq,
7059 wk,
7060 wv,
7061 wo,
7062 q_norm,
7063 k_norm,
7064 output_gate,
7065 softplus_gate,
7066 bias,
7067 } = &lw.attn
7068 else {
7069 return None;
7070 };
7071 if softplus_gate.is_some() {
7072 return None;
7073 }
7074 let FfnKind::Dense(d) = &lw.ffn else {
7075 return None;
7076 };
7077 if !d.segs.is_empty() {
7078 return None; }
7080 let mut x = self.mtp_block_input(m, hidden, next_token);
7083 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7084 let (_, i, kind, rs) = t.graph_weight()?;
7085 Some(crate::gpu::GraphW {
7086 idx: i,
7087 kind,
7088 row_scale: rs,
7089 data: &[],
7090 prism: crate::gpu::GraphPrismOp::None,
7091 affine: false,
7092 })
7093 }
7094 let (model, _, _, _) = wq.graph_weight()?;
7095 let model = model.clone();
7096 let (lm_gw, lm_rows) = {
7097 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7098 let rows = if kind == 6 {
7102 self.draft_head_rows(self.weights.lm_head.rows())
7103 } else {
7104 self.weights.lm_head.rows()
7105 };
7106 (
7107 crate::gpu::GraphW {
7108 idx: i,
7109 kind,
7110 row_scale: rs,
7111 data: &[],
7112 prism: crate::gpu::GraphPrismOp::None,
7113 affine: false,
7114 },
7115 rows,
7116 )
7117 };
7118 let layer = crate::gpu::GraphLayer {
7119 input_norm: &lw.input_norm,
7120 attn: crate::gpu::GraphAttn::Full {
7121 wq: gw(wq)?,
7122 wk: gw(wk)?,
7123 wv: gw(wv)?,
7124 wo: gw(wo)?,
7125 q_norm: q_norm.as_deref(),
7126 k_norm: k_norm.as_deref(),
7127 late_qk_norm: self.qk_norm_after_rope,
7128 bias: bias
7129 .as_ref()
7130 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7131 output_gate: *output_gate,
7132 cpu_k: m.kv.k_heads(),
7133 cpu_v: m.kv.v_heads(),
7134 cpu_base: m.kv.base(),
7135 geom: None,
7136 head_gate: None,
7137 },
7138 post_norm: &lw.post_norm,
7139 attn_out_norm: None,
7140 ffn_out_norm: None,
7141 layer_scale: None,
7142 ffn: crate::gpu::GraphFfn::Dense {
7143 gate: gw(&d.gate_proj)?,
7144 up: gw(&d.up_proj)?,
7145 down: gw(&d.down_proj)?,
7146 act: d.act.graph_act()?,
7147 },
7148 };
7149 let nh = self.num_heads;
7150 let (nkv, hd, rd) = self.layer_geom(0);
7151 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7152 let mut logits = Vec::new();
7153 let ok = crate::gpu::forward_token_graph(
7154 &model,
7155 self.mtp_kv_id(),
7156 std::slice::from_ref(&layer),
7157 &[None],
7158 self.o1_epoch,
7159 &self.inv_freq,
7160 &mut x,
7161 nh,
7162 nkv,
7163 hd,
7164 self.attn_scale,
7165 rd,
7166 self.hidden_size,
7167 d.gate_proj.rows(),
7171 position,
7172 self.kv_cache.max_seq_len,
7173 gemma,
7174 self.rms_eps as f32,
7175 Some((&lm_gw, lm_rows)),
7176 &m.final_norm,
7177 &mut logits,
7178 &[],
7179 1,
7180 None,
7181 None,
7182 None,
7183 Self::MTP_LAYER_BASE,
7184 true,
7185 );
7186 match ok {
7187 crate::gpu::TokenGraphOutcome::Completed => {}
7188 crate::gpu::TokenGraphOutcome::Declined => return None,
7189 crate::gpu::TokenGraphOutcome::Failed => {
7190 self.clear_sequence_state();
7194 self.graph_failed
7195 .store(true, std::sync::atomic::Ordering::Relaxed);
7196 self.cancel
7197 .store(true, std::sync::atomic::Ordering::Relaxed);
7198 return None;
7199 }
7200 }
7201 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7205 Some((logits, x))
7206 }
7207
7208 #[cfg(feature = "gpu")]
7216 fn mtp_warm_graph(
7217 &mut self,
7218 m: &mut MtpModule,
7219 pairs: &[(&[f32], u32)],
7220 first_pos: usize,
7221 ) -> crate::gpu::BatchGraphOutcome {
7222 if pairs.is_empty() {
7223 return crate::gpu::BatchGraphOutcome::Completed;
7224 }
7225 if !self.mtp_block_graph_ok(m) {
7226 return crate::gpu::BatchGraphOutcome::Declined;
7227 }
7228 let hs = self.hidden_size;
7229 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7232 for (h, t) in pairs {
7233 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7234 }
7235 let lw = &m.layer;
7236 let AttnKind::Full {
7237 wq,
7238 wk,
7239 wv,
7240 wo,
7241 q_norm,
7242 k_norm,
7243 output_gate,
7244 bias,
7245 ..
7246 } = &lw.attn
7247 else {
7248 return crate::gpu::BatchGraphOutcome::Declined;
7249 };
7250 let FfnKind::Dense(d) = &lw.ffn else {
7251 return crate::gpu::BatchGraphOutcome::Declined;
7252 };
7253 if !d.segs.is_empty() {
7254 return crate::gpu::BatchGraphOutcome::Declined; }
7256 let (
7259 AttnKind::Full {
7260 softplus_gate: None,
7261 ..
7262 },
7263 Some(gact),
7264 ) = (&lw.attn, d.act.graph_act())
7265 else {
7266 return crate::gpu::BatchGraphOutcome::Declined;
7267 };
7268 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7269 let (_, i, kind, rs) = t.graph_weight()?;
7270 Some(crate::gpu::GraphW {
7271 idx: i,
7272 kind,
7273 row_scale: rs,
7274 data: &[],
7275 prism: crate::gpu::GraphPrismOp::None,
7276 affine: false,
7277 })
7278 }
7279 let Some((model, _, _, _)) = wq.graph_weight() else {
7280 return crate::gpu::BatchGraphOutcome::Declined;
7281 };
7282 let model = model.clone();
7283 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7284 gw(wq),
7285 gw(wk),
7286 gw(wv),
7287 gw(wo),
7288 gw(&d.gate_proj),
7289 gw(&d.up_proj),
7290 gw(&d.down_proj),
7291 ) else {
7292 return crate::gpu::BatchGraphOutcome::Declined;
7293 };
7294 let layer = crate::gpu::GraphLayer {
7295 input_norm: &lw.input_norm,
7296 attn: crate::gpu::GraphAttn::Full {
7297 wq: gwq,
7298 wk: gwk,
7299 wv: gwv,
7300 wo: gwo,
7301 q_norm: q_norm.as_deref(),
7302 k_norm: k_norm.as_deref(),
7303 late_qk_norm: self.qk_norm_after_rope,
7304 bias: bias
7305 .as_ref()
7306 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7307 output_gate: *output_gate,
7308 cpu_k: m.kv.k_heads(),
7309 cpu_v: m.kv.v_heads(),
7310 cpu_base: m.kv.base(),
7311 geom: None,
7312 head_gate: None,
7313 },
7314 post_norm: &lw.post_norm,
7315 attn_out_norm: None,
7316 ffn_out_norm: None,
7317 layer_scale: None,
7318 ffn: crate::gpu::GraphFfn::Dense {
7319 gate: gg,
7320 up: gu,
7321 down: gd,
7322 act: gact,
7323 },
7324 };
7325 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7326 let nh = self.num_heads;
7327 let (nkv, hd, rd) = self.layer_geom(0);
7328 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7329 crate::gpu::forward_batch_graph(
7330 &model,
7331 self.mtp_kv_id(),
7332 std::slice::from_ref(&layer),
7333 &self.inv_freq,
7334 &mut hiddens,
7335 nh,
7336 nkv,
7337 hd,
7338 rd,
7339 hs,
7340 d.gate_proj.rows(),
7343 &positions,
7344 self.kv_cache.max_seq_len,
7345 gemma,
7346 self.rms_eps as f32,
7347 self.attn_scale,
7348 pairs.len(),
7349 &[],
7350 0,
7351 None,
7352 None,
7353 )
7354 }
7355
7356 #[cfg(feature = "gpu")]
7363 fn mtp_warm_graph_fallback(
7364 &mut self,
7365 m: &mut MtpModule,
7366 pairs: &[(&[f32], u32)],
7367 first_pos: usize,
7368 ) -> bool {
7369 if pairs.is_empty() {
7370 return true;
7371 }
7372 let graphable = self.mtp_block_graph_ok(m);
7373 if !graphable {
7374 if self.mtp_graph_mode == Some(true) {
7378 return false;
7379 }
7380 self.mtp_graph_mode = Some(false);
7381 for (j, (h, t)) in pairs.iter().enumerate() {
7382 self.mtp_warm(m, h, *t, first_pos + j);
7383 }
7384 return true;
7385 }
7386
7387 for (j, (h, t)) in pairs.iter().enumerate() {
7392 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7393 return false;
7394 }
7395 }
7396 self.mtp_graph_mode = Some(true);
7397 true
7398 }
7399
7400 #[cfg(feature = "gpu")]
7405 fn mtp_warm_prefill_pairs(
7406 &mut self,
7407 m: &mut MtpModule,
7408 pairs: &[(&[f32], u32)],
7409 first_pos: usize,
7410 ) -> Result<(), &'static str> {
7411 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7416 if self.mtp_graph_mode == Some(true) {
7417 return Err("MTP token graph became unavailable after admission");
7418 }
7419 self.mtp_graph_mode = Some(false);
7420 for (j, (h, t)) in pairs.iter().enumerate() {
7421 self.mtp_warm(m, h, *t, first_pos + j);
7422 }
7423 return Ok(());
7424 }
7425 match self.mtp_warm_graph(m, pairs, first_pos) {
7426 crate::gpu::BatchGraphOutcome::Completed => {
7427 if !pairs.is_empty() {
7428 self.mtp_graph_mode = Some(true);
7429 }
7430 Ok(())
7431 }
7432 crate::gpu::BatchGraphOutcome::Declined => {
7433 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7434 Ok(())
7435 } else {
7436 Err("MTP warm-up fallback failed after device admission")
7437 }
7438 }
7439 crate::gpu::BatchGraphOutcome::Failed => {
7440 Err("MTP warm batch graph failed after admission")
7441 }
7442 }
7443 }
7444
7445 #[cfg(not(feature = "gpu"))]
7446 fn mtp_warm_prefill_pairs(
7447 &mut self,
7448 m: &mut MtpModule,
7449 pairs: &[(&[f32], u32)],
7450 first_pos: usize,
7451 ) -> Result<(), &'static str> {
7452 for (j, (h, t)) in pairs.iter().enumerate() {
7453 self.mtp_warm(m, h, *t, first_pos + j);
7454 }
7455 Ok(())
7456 }
7457
7458 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7462 let e = self.embed_single(next_token);
7463 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7464 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7465 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7466 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7467 let mut x = vec![0.0f32; self.hidden_size];
7468 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7469 inference::rms_norm_into(
7470 &x,
7471 &m.layer.input_norm,
7472 self.rms_eps,
7473 self.norm_style,
7474 &mut self.ws.n1,
7475 );
7476 let attn = match &m.layer.attn {
7477 AttnKind::Full {
7478 wq,
7479 wk,
7480 wv,
7481 wo,
7482 q_norm,
7483 k_norm,
7484 output_gate,
7485 softplus_gate,
7486 bias,
7487 } => {
7488 let mut cfg = self.attn_cfg(position);
7489 cfg.q_norm = q_norm.as_deref();
7490 cfg.k_norm = k_norm.as_deref();
7491 cfg.output_gate = *output_gate;
7492 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7493 cfg.bias = bias
7494 .as_ref()
7495 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7496 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7497 }
7498 _ => return,
7499 };
7500 let _ = attn;
7501 }
7502
7503 #[cfg(feature = "gpu")]
7510 #[allow(clippy::too_many_arguments)]
7511 fn graph_spec_step(
7512 &mut self,
7513 m: &mut MtpModule,
7514 hidden: &[f32],
7515 t_next: u32,
7516 next_pos: usize,
7517 drafted: &mut usize,
7518 accepted: &mut usize,
7519 all_ids: &mut Vec<u32>,
7523 room: usize,
7528 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7529 #[cfg(target_os = "macos")]
7540 let metal_native = crate::gpu::q1_force();
7541 #[cfg(not(target_os = "macos"))]
7542 let metal_native = false;
7543 #[cfg(feature = "gpu")]
7544 let k_default = if metal_native {
7545 7
7548 } else if crate::gpu_wgpu::verify_i8_on() {
7549 5
7550 } else {
7551 4
7552 };
7553 #[cfg(not(feature = "gpu"))]
7554 let k_default = 4;
7555 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7556 .ok()
7557 .and_then(|v| v.parse().ok())
7558 .filter(|&v| (1..=8).contains(&v));
7559 let (k_start, k_max) = if metal_native {
7564 (7, 7)
7565 } else {
7566 (3, k_default.max(5))
7567 };
7568 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7569 let k_spec = k_full.min(room).max(1);
7570 let k_capped = k_spec < k_full;
7573 if next_pos == 0 {
7574 return None;
7575 }
7576 let t_round = std::time::Instant::now();
7577 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7593 let sub0 = subs();
7594 let cfg = self.sampler_config.clone();
7599 let penalized = !(cfg.repetition_penalty == 1.0
7600 && cfg.presence_penalty == 0.0
7601 && cfg.suppress_tokens.is_empty());
7602 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7607 let sampling = cfg.temperature >= 1e-6;
7608 let sparse = sampling && sampler::sparse_ok(&cfg);
7614 let base_len = all_ids.len();
7615 if sampling && !sparse && self.spec_q.len() < k_spec {
7616 self.spec_q.resize_with(k_spec, Vec::new);
7617 }
7618 if sparse && self.spec_qs.len() < k_spec {
7619 self.spec_qs.resize_with(k_spec, Vec::new);
7620 }
7621 let mut drafts = Vec::with_capacity(k_spec);
7626 let mut hx = hidden.to_vec();
7627 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7630 spec_stamp("pro");
7631 #[cfg(target_os = "macos")]
7637 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7638 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7639 Ok(ids) => {
7640 self.mtp_graph_mode = Some(true);
7641 drafts = ids;
7642 }
7643 Err(true) => {
7644 tracing::error!("mtp Metal draft chain failed after commit");
7645 self.clear_sequence_state();
7646 self.graph_failed
7647 .store(true, std::sync::atomic::Ordering::Relaxed);
7648 self.cancel
7649 .store(true, std::sync::atomic::Ordering::Relaxed);
7650 return None;
7651 }
7652 Err(false) => {}
7653 }
7654 }
7655 for j in drafts.len()..k_spec {
7656 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7657 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7658 if spec_dbg {
7659 let saved = self.mtp_graph_mode;
7660 self.mtp_graph_mode = Some(false);
7661 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7662 self.mtp_graph_mode = saved;
7663 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7664 return None;
7665 }
7666 m.kv.truncate_last(1);
7667 dbg_ref = Some(r);
7668 }
7669 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7670 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7671 return None;
7672 }
7673 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7674 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7675 let dl = lg
7676 .iter()
7677 .zip(&lg_cpu)
7678 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7679 let dh = hj
7680 .iter()
7681 .zip(&h_cpu)
7682 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7683 eprintln!(
7684 "spec-dbg j={j} pos {} tok_in {tok_in}: per-op draft {} graph draft {} | max|dlogit| {dl:.3} | |h_cpu| {:.2} |h_graph| {:.2} max|dh| {dh:.3} | kv rows {}",
7685 next_pos - 1 + j,
7686 sampler::argmax(&lg_cpu),
7687 sampler::argmax(&lg),
7688 n(&h_cpu),
7689 n(&hj),
7690 m.kv.seq_len
7691 );
7692 }
7693 let dj = if sparse {
7694 let mut q = std::mem::take(&mut self.spec_qs[j]);
7695 let ok = sampler::sparse_distribution_into(
7696 &lg,
7697 &cfg,
7698 all_ids,
7699 &mut self.sampler_scratch,
7700 self.pool.as_deref(),
7701 &mut q,
7702 );
7703 let d = if ok {
7704 sampler::draw_sparse(&q, &mut self.rng)
7705 } else {
7706 let t = sampler::argmax(&lg);
7708 q.clear();
7709 q.push((t, 1.0));
7710 t
7711 };
7712 self.spec_qs[j] = q;
7713 all_ids.push(d);
7714 d
7715 } else if sampling {
7716 let mut q = std::mem::take(&mut self.spec_q[j]);
7717 sampler::distribution_into(
7718 &lg,
7719 &cfg,
7720 all_ids,
7721 &mut self.sampler_scratch,
7722 self.pool.as_deref(),
7723 &mut q,
7724 );
7725 let d = sampler::draw(&q, &mut self.rng);
7726 self.spec_q[j] = q;
7727 all_ids.push(d); d
7729 } else if greedy_pen {
7730 let d = sampler::argmax_penalized(
7731 &lg,
7732 &cfg,
7733 all_ids,
7734 &mut self.sampler_scratch,
7735 self.pool.as_deref(),
7736 );
7737 all_ids.push(d);
7738 d
7739 } else {
7740 sampler::argmax(&lg)
7741 };
7742 attention::recycle_buf(&mut lg);
7743 drafts.push(dj);
7744 hx = hj;
7745 spec_stamp("d.pick");
7746 }
7747 all_ids.truncate(base_len);
7748 *drafted += k_spec;
7749 let t_draft = t_round.elapsed();
7750 let sub_draft = subs();
7751 let b = k_spec + 1;
7754 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7755 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7756 let e = self.embed_single(t);
7757 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7758 }
7759 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7760 spec_stamp("v.emb");
7761 let (lm_gw, lm_rows) = {
7762 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7763 (
7764 crate::gpu::GraphW {
7765 idx: i,
7766 kind,
7767 row_scale: rs,
7768 data: &[],
7769 prism: crate::gpu::GraphPrismOp::None,
7770 affine: false,
7771 },
7772 self.weights.lm_head.rows(),
7773 )
7774 };
7775 let mut logits = Vec::new();
7776 let final_norm = self.weights.final_norm.clone();
7777 #[cfg(target_os = "macos")]
7786 let greedy_dev = metal_native
7787 && !sampling
7788 && !greedy_pen
7789 && !self.confidence_on
7790 && self.final_softcap.is_none()
7791 && self.vocab_size == lm_rows
7797 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7798 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7799 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7800 #[cfg(not(target_os = "macos"))]
7801 let greedy_dev = false;
7802 let mut dev_ids: Vec<u32> = Vec::new();
7803 #[cfg(target_os = "macos")]
7804 let verify_outcome = if metal_native {
7805 let lm = self.weights.lm_head.q1_parts()?;
7806 let n_score = self.vocab_size.min(lm_rows);
7807 self.try_batch_graph_metal(
7808 &mut hiddens,
7809 &positions,
7810 b,
7811 Some((lm, &final_norm, &mut logits)),
7812 if greedy_dev {
7813 Some((n_score, &mut dev_ids))
7814 } else {
7815 None
7816 },
7817 )
7818 } else {
7819 self.try_batch_graph_wgpu(
7820 &mut hiddens,
7821 &positions,
7822 b,
7823 Some(crate::gpu::SpecTail {
7824 lm: lm_gw,
7825 lm_rows,
7826 final_norm: &final_norm,
7827 logits_out: &mut logits,
7828 }),
7829 )
7830 };
7831 #[cfg(not(target_os = "macos"))]
7832 let verify_outcome = self.try_batch_graph_wgpu(
7833 &mut hiddens,
7834 &positions,
7835 b,
7836 Some(crate::gpu::SpecTail {
7837 lm: lm_gw,
7838 lm_rows,
7839 final_norm: &final_norm,
7840 logits_out: &mut logits,
7841 }),
7842 );
7843 match verify_outcome {
7844 crate::gpu::BatchGraphOutcome::Completed => {}
7845 crate::gpu::BatchGraphOutcome::Declined => {
7846 m.kv.truncate_last(k_spec);
7850 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7851 self.clear_sequence_state();
7852 self.graph_failed
7853 .store(true, std::sync::atomic::Ordering::Relaxed);
7854 self.cancel
7855 .store(true, std::sync::atomic::Ordering::Relaxed);
7856 tracing::error!("MTP graph mirror rewind failed after verify decline");
7857 }
7858 return None;
7859 }
7860 crate::gpu::BatchGraphOutcome::Failed => {
7861 self.clear_sequence_state();
7865 self.graph_failed
7866 .store(true, std::sync::atomic::Ordering::Relaxed);
7867 self.cancel
7868 .store(true, std::sync::atomic::Ordering::Relaxed);
7869 tracing::error!("MTP verify batch graph failed after admission");
7870 return None;
7871 }
7872 }
7873 #[cfg(target_os = "macos")]
7879 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7880 let snap: Vec<Vec<f32>> = self
7881 .kv_cache
7882 .layers
7883 .iter()
7884 .map(|l| l.linear_state.clone())
7885 .collect();
7886 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7887 let toks: Vec<u32> = std::iter::once(t_next)
7888 .chain(drafts.iter().copied())
7889 .collect();
7890 let want_save = self.graph_want_logits;
7891 self.graph_want_logits = false;
7892 for (i, &t) in toks.iter().enumerate() {
7893 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7894 let _ = self.graph_logits.take();
7895 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7899 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7900 }
7901 let ref_lg = self.logits_from_hidden(&hi);
7902 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7903 let ra = sampler::argmax(&ref_lg);
7904 let va = sampler::argmax(row);
7905 let mut md = 0f32;
7906 let mut rms = 0f64;
7907 for j in 0..lm_rows.min(ref_lg.len()) {
7908 let d = (ref_lg[j] - row[j]).abs();
7909 md = md.max(d);
7910 rms += (d as f64) * (d as f64);
7911 }
7912 let mut hd = 0f32;
7913 for j in 0..self.hidden_size {
7914 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7915 }
7916 eprintln!(
7917 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7918 next_pos + i,
7919 if ra == va { "OK" } else { "MISMATCH" },
7920 (rms / lm_rows as f64).sqrt()
7921 );
7922 }
7923 self.graph_want_logits = want_save;
7924 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7927 if l.linear_state.len() == st.len() {
7928 l.linear_state.copy_from_slice(&st);
7929 } else {
7930 l.linear_state = st;
7931 }
7932 }
7933 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7934 let extra = l.seq_len.saturating_sub(n0);
7935 if extra > 0 {
7936 l.truncate_last(extra);
7937 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7938 }
7939 }
7940 }
7941 let t_verify = t_round.elapsed();
7942 let sub_verify = subs();
7943 let mut a = 0usize;
7948 let mut forced: Option<u32> = None;
7949 let ids: Vec<u32> = if sparse {
7950 let mut p = std::mem::take(&mut self.spec_ps);
7951 let mut res = std::mem::take(&mut self.spec_ress);
7952 while a < k_spec {
7953 let ok = sampler::sparse_distribution_into(
7954 &logits[a * lm_rows..(a + 1) * lm_rows],
7955 &cfg,
7956 all_ids,
7957 &mut self.sampler_scratch,
7958 self.pool.as_deref(),
7959 &mut p,
7960 );
7961 if !ok {
7962 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7963 p.clear();
7964 p.push((t, 1.0));
7965 }
7966 match sampler::spec_accept_or_correct_sparse(
7967 &p,
7968 &self.spec_qs[a],
7969 drafts[a],
7970 &mut self.rng,
7971 &mut res,
7972 ) {
7973 None => {
7974 all_ids.push(drafts[a]);
7975 a += 1;
7976 }
7977 Some(c) => {
7978 forced = Some(c);
7979 break;
7980 }
7981 }
7982 }
7983 all_ids.truncate(base_len);
7984 self.spec_ps = p;
7985 self.spec_ress = res;
7986 drafts.clone()
7987 } else if sampling {
7988 let mut p = std::mem::take(&mut self.spec_p);
7989 let mut res = std::mem::take(&mut self.spec_res);
7990 while a < k_spec {
7991 sampler::distribution_into(
7992 &logits[a * lm_rows..(a + 1) * lm_rows],
7993 &cfg,
7994 all_ids,
7995 &mut self.sampler_scratch,
7996 self.pool.as_deref(),
7997 &mut p,
7998 );
7999 match sampler::spec_accept_or_correct(
8000 &p,
8001 &self.spec_q[a],
8002 drafts[a],
8003 &mut self.rng,
8004 &mut res,
8005 self.pool.as_deref(),
8006 ) {
8007 None => {
8008 all_ids.push(drafts[a]);
8009 a += 1;
8010 }
8011 Some(c) => {
8012 forced = Some(c);
8013 break;
8014 }
8015 }
8016 }
8017 all_ids.truncate(base_len);
8018 self.spec_p = p;
8019 self.spec_res = res;
8020 drafts.clone()
8022 } else if greedy_pen {
8023 let mut ids: Vec<u32> = Vec::with_capacity(b);
8027 for i in 0..b {
8028 let t = sampler::argmax_penalized(
8029 &logits[i * lm_rows..(i + 1) * lm_rows],
8030 &cfg,
8031 all_ids,
8032 &mut self.sampler_scratch,
8033 self.pool.as_deref(),
8034 );
8035 ids.push(t);
8036 if i < k_spec && t == drafts[i] {
8037 all_ids.push(t);
8038 } else {
8039 break;
8040 }
8041 }
8042 all_ids.truncate(base_len);
8043 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8044 a += 1;
8045 }
8046 ids
8049 } else if greedy_dev && dev_ids.len() == b {
8050 let ids = std::mem::take(&mut dev_ids);
8051 while a < k_spec && ids[a] == drafts[a] {
8052 a += 1;
8053 }
8054 ids
8055 } else {
8056 if logits.len() < b * lm_rows {
8057 self.clear_sequence_state();
8060 self.graph_failed
8061 .store(true, std::sync::atomic::Ordering::Relaxed);
8062 self.cancel
8063 .store(true, std::sync::atomic::Ordering::Relaxed);
8064 tracing::error!("Metal verify returned neither logits nor argmax ids");
8065 return None;
8066 }
8067 let ids: Vec<u32> = (0..b)
8068 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8069 .collect();
8070 while a < k_spec && ids[a] == drafts[a] {
8071 a += 1;
8072 }
8073 ids
8074 };
8075 spec_stamp("acc");
8076 if spec_dbg {
8077 eprintln!(
8078 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8079 drafts, ids
8080 );
8081 }
8082 #[cfg(target_os = "macos")]
8086 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8087 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8088 {
8089 let snap: Vec<Vec<f32>> = self
8090 .kv_cache
8091 .layers
8092 .iter()
8093 .map(|l| l.linear_state.clone())
8094 .collect();
8095 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8096 let toks: Vec<u32> = std::iter::once(t_next)
8097 .chain(drafts.iter().copied())
8098 .collect();
8099 let want_save = self.graph_want_logits;
8100 self.graph_want_logits = false;
8101 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8102 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8103 let _ = self.graph_logits.take();
8104 }
8105 self.graph_want_logits = want_save;
8106 let plain_states: Vec<Vec<f32>> = self
8107 .kv_cache
8108 .layers
8109 .iter()
8110 .map(|l| l.linear_state.clone())
8111 .collect();
8112 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8113 let mut rows = Vec::new();
8114 for (li, (l, n0)) in self
8115 .kv_cache
8116 .layers
8117 .iter_mut()
8118 .zip(attn_lens.iter())
8119 .enumerate()
8120 {
8121 let extra = l.seq_len.saturating_sub(*n0);
8122 if extra > 0 {
8123 let mut kk = Vec::new();
8124 let mut vv = Vec::new();
8125 for g in 0..nkv {
8126 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8127 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8128 }
8129 rows.push((li, kk, vv));
8130 l.truncate_last(extra);
8131 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8132 }
8133 }
8134 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8135 if l.linear_state.len() == st.len() {
8136 l.linear_state.copy_from_slice(&st);
8137 } else {
8138 l.linear_state = st;
8139 }
8140 }
8141 Some((plain_states, rows))
8142 } else {
8143 None
8144 };
8145 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8146 #[cfg(target_os = "macos")]
8155 let mut warm_pending: Option<MetalWarmPending> = None;
8156 #[cfg(target_os = "macos")]
8157 if metal_native {
8158 m.kv.truncate_last(k_spec.saturating_sub(1));
8159 if self.mtp_graph_mode == Some(true) {
8160 crate::gpu_metal::kv_mirror_set_stored(
8163 self.mtp_kv_id(),
8164 Self::MTP_LAYER_BASE,
8165 m.kv.seq_len,
8166 );
8167 if !warm_off && a > 0 {
8168 let pairs: Vec<(&[f32], u32)> = (0..a)
8169 .map(|j| {
8170 (
8171 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8172 ids[j],
8173 )
8174 })
8175 .collect();
8176 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8177 }
8178 }
8179 spec_stamp("c.wsub");
8180 }
8181 #[cfg(target_os = "macos")]
8183 if metal_native {
8184 if !self.metal_verify_commit(a) {
8187 self.clear_sequence_state();
8188 self.graph_failed
8189 .store(true, std::sync::atomic::Ordering::Relaxed);
8190 self.cancel
8191 .store(true, std::sync::atomic::Ordering::Relaxed);
8192 tracing::error!("Metal verify state/KV handoff failed after admission");
8193 return None;
8194 }
8195 if let Some((plain_states, rows)) = commit_ref {
8196 crate::gpu_metal::queue_fence();
8197 let _ = crate::gpu_metal::wait_replay();
8200 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8201 let mut worst_s = 0f32;
8202 let mut worst_li = 0usize;
8203 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8204 if l.linear_state.len() != ps.len() || ps.is_empty() {
8205 continue;
8206 }
8207 let d = l
8208 .linear_state
8209 .iter()
8210 .zip(ps)
8211 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8212 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8213 let rel = d / n.max(1e-6);
8214 if rel > worst_s {
8215 worst_s = rel;
8216 worst_li = li;
8217 }
8218 }
8219 let mut worst_k = 0f32;
8220 for (li, kk, vv) in &rows {
8221 let l = &self.kv_cache.layers[*li];
8222 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8223 let mut ck = Vec::new();
8224 let mut cv = Vec::new();
8225 for g in 0..nkv {
8226 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8227 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8228 }
8229 if ck.len() == kk.len() {
8230 let dk = ck
8231 .iter()
8232 .zip(kk)
8233 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8234 let dv = cv
8235 .iter()
8236 .zip(vv)
8237 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8238 worst_k = worst_k.max(dk).max(dv);
8239 } else {
8240 eprintln!(
8241 "commit-check L{li}: kv row count mismatch {} vs {}",
8242 ck.len(),
8243 kk.len()
8244 );
8245 }
8246 }
8247 eprintln!(
8248 "commit-check a={a}: worst GDN state rel-max diff {worst_s:.2e} (L{worst_li}) | worst K/V row abs diff {worst_k:.4}"
8249 );
8250 }
8251 }
8252 if !metal_native && a + 1 < b {
8253 let expected_gdn_layers = self.graph_gdn_layer_count();
8254 if expected_gdn_layers > 0
8255 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8256 {
8257 self.clear_sequence_state();
8258 self.graph_failed
8259 .store(true, std::sync::atomic::Ordering::Relaxed);
8260 self.cancel
8261 .store(true, std::sync::atomic::Ordering::Relaxed);
8262 tracing::error!("GDN speculative restore failed after verify");
8263 return None;
8264 }
8265 }
8266 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8267 self.clear_sequence_state();
8272 self.graph_failed
8273 .store(true, std::sync::atomic::Ordering::Relaxed);
8274 self.cancel
8275 .store(true, std::sync::atomic::Ordering::Relaxed);
8276 tracing::error!("trunk graph KV rewind failed after speculative verify");
8277 return None;
8278 }
8279 *accepted += a;
8280 if !metal_native {
8291 m.kv.truncate_last(k_spec.saturating_sub(1));
8293 }
8294 spec_stamp("c.trunc");
8295 if !metal_native
8296 && self.mtp_graph_mode == Some(true)
8297 && !self.rewind_mtp_graph_mirror(next_pos)
8298 {
8299 self.clear_sequence_state();
8303 self.graph_failed
8304 .store(true, std::sync::atomic::Ordering::Relaxed);
8305 self.cancel
8306 .store(true, std::sync::atomic::Ordering::Relaxed);
8307 tracing::error!("MTP graph mirror rewind failed after verify commit");
8308 return None;
8309 }
8310 if !warm_off && a > 0 {
8311 let mut warmed = false;
8314 #[cfg(target_os = "macos")]
8315 if metal_native && self.mtp_graph_mode == Some(true) {
8316 warmed = match warm_pending.take() {
8320 Some(p) => self.mtp_warm_batch_finish(m, p),
8321 None => false,
8322 };
8323 if !warmed {
8324 warmed = true;
8325 for j in 0..a {
8326 let row =
8327 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8328 if self
8329 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8330 .is_none()
8331 {
8332 warmed = false;
8333 break;
8334 }
8335 }
8336 }
8337 }
8338 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8339 let rows: Vec<Vec<f32>> = (0..a)
8340 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8341 .collect();
8342 let pairs: Vec<(&[f32], u32)> = rows
8343 .iter()
8344 .zip(ids.iter())
8345 .map(|(r, &t)| (r.as_slice(), t))
8346 .collect();
8347 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8348 Ok(()) => warmed = true,
8349 Err(err) => {
8350 tracing::error!("{err}");
8356 self.clear_sequence_state();
8357 self.graph_failed
8358 .store(true, std::sync::atomic::Ordering::Relaxed);
8359 self.cancel
8360 .store(true, std::sync::atomic::Ordering::Relaxed);
8361 return None;
8362 }
8363 }
8364 }
8365 if !warmed {
8366 for j in 0..a {
8367 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8368 let row = row.to_vec();
8369 self.mtp_warm(m, &row, ids[j], next_pos + j);
8370 }
8371 }
8372 }
8373 spec_stamp("c.warm");
8377 if let Some(c) = forced {
8378 self.spec_forced = Some(c);
8379 self.graph_logits = None;
8380 } else if greedy_dev && logits.is_empty() {
8381 self.spec_forced = Some(ids[a]);
8384 self.graph_logits = None;
8385 } else {
8386 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8387 row.resize(self.vocab_size, 0.0);
8388 if let Some(c) = self.final_softcap {
8389 for l in row.iter_mut() {
8390 *l = c * (*l / c).tanh();
8391 }
8392 }
8393 self.graph_logits = Some(row);
8394 }
8395 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8396 spec_stamp("c.row");
8397 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8403 let end = subs();
8404 eprintln!(
8405 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8406 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8407 t_draft.as_secs_f64() * 1e3,
8408 sub_draft - sub0,
8409 (t_verify - t_draft).as_secs_f64() * 1e3,
8410 sub_verify - sub_draft,
8411 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8412 end - sub_verify,
8413 self.draft_full_streak,
8414 );
8415 }
8416 if k_env.is_none() && !metal_native && !k_capped {
8421 let f = a as f32 / k_spec.max(1) as f32;
8425 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8426 let mut k_next = k_spec;
8427 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8428 k_next = k_spec + 1;
8429 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8430 k_next = k_spec - 1;
8431 }
8432 if k_next != k_spec {
8433 self.spec_acc_ewma = 0.6;
8434 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8435 eprintln!("spec-k: {k_spec} → {k_next}");
8436 }
8437 }
8438 self.spec_k_adapt = Some(k_next);
8439 }
8440 spec_stamp("end");
8441 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8442 }
8443
8444 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8453 if !self.pair_supported() {
8454 return (0.0, 0.0);
8455 }
8456 if self.gdn_cfg.is_some()
8464 && self.graph_prefill_preferred()
8465 && self.automatic_gpu_prefix().is_some()
8466 {
8467 tracing::info!(
8468 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8469 );
8470 return (0.0, 0.0);
8471 }
8472 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8479 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8480 let emb1 = self.embed_single(1);
8481 let emb2 = self.embed_single(2);
8482 let pos = self.kv_cache.seq_len();
8483
8484 let t0 = std::time::Instant::now();
8485 for _ in 0..iters {
8486 let _ = self.forward_layers(&emb1, pos, None);
8487 let _ = self.forward_layers(&emb2, pos + 1, None);
8488 for l in &mut self.kv_cache.layers {
8489 l.truncate_last(2);
8490 }
8491 }
8492 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8493
8494 let t1 = std::time::Instant::now();
8495 for _ in 0..iters {
8496 let _ = self.forward_pair(&emb1, &emb2, pos);
8497 for l in &mut self.kv_cache.layers {
8498 l.truncate_last(2);
8499 }
8500 }
8501 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8502 match graph_env {
8503 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8504 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8505 }
8506 (singles_ms, pair_ms)
8507 }
8508
8509 fn pair_supported(&self) -> bool {
8517 !self.weights.layers.is_empty()
8524 && !self.per_token_stack()
8525 && !self
8526 .weights
8527 .layers
8528 .iter()
8529 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8530 }
8531
8532 fn forward_pair(
8533 &mut self,
8534 emb1: &[f32],
8535 emb2: &[f32],
8536 position: usize,
8537 ) -> (Vec<f32>, Vec<f32>) {
8538 self.mimo_moe_prepare();
8541 let mut h1 = emb1.to_vec();
8542 let mut h2 = emb2.to_vec();
8543 let (_nkv, _hd, hs, _rd, eps) = (
8544 self.num_kv_heads,
8545 self.head_dim,
8546 self.hidden_size,
8547 self.rotary_dim,
8548 self.rms_eps,
8549 );
8550 let pool = self.pool.clone();
8551 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8558
8559 for li in 0..self.num_layers {
8560 let _capacity_tail = automatic_gpu_prefix
8561 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8562 .map(|_| crate::gpu::enter_cpu_scope());
8563 crate::gpu::set_layer(li as i64);
8566 let lw = &self.weights.layers[self.phys_layer(li)];
8567 inference::rms_norm_into(
8570 &h1,
8571 &lw.input_norm,
8572 self.rms_eps,
8573 self.norm_style,
8574 &mut self.ws.n1,
8575 );
8576 inference::rms_norm_into(
8577 &h2,
8578 &lw.input_norm,
8579 self.rms_eps,
8580 self.norm_style,
8581 &mut self.ws.n2,
8582 );
8583
8584 let (a1, a2) = match &lw.attn {
8585 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8586 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8587 AttnKind::Bounded(w) => {
8588 let rope = self
8591 .bounded_rope
8592 .clone()
8593 .expect("bounded layer without an installed rotation table");
8594 let cfg = crate::bounded::BoundedAttnCfg {
8595 num_heads: self.num_heads,
8596 num_kv_heads: self.num_kv_heads,
8597 head_dim: self.head_dim,
8598 hidden_size: hs,
8599 scale: self.attn_scale,
8600 rope: &rope,
8601 pool: pool.as_deref(),
8602 };
8603 let a1 = crate::bounded::bounded_attention(
8604 &self.ws.n1,
8605 w,
8606 &mut self.kv_cache.layers[li],
8607 &cfg,
8608 );
8609 let a2 = crate::bounded::bounded_attention(
8610 &self.ws.n2,
8611 w,
8612 &mut self.kv_cache.layers[li],
8613 &cfg,
8614 );
8615 (a1, a2)
8616 }
8617 AttnKind::Linear(w) => {
8618 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8619 let layer = &mut self.kv_cache.layers[li];
8620 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8621 vmf_phase_pair(
8622 &self.ws.n1,
8623 &self.ws.n2,
8624 w,
8625 &cfg,
8626 state,
8627 scratch,
8628 self.pool.as_deref(),
8629 )
8630 }
8631 AttnKind::LinearGdn(w) => {
8632 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8633 let layer = &mut self.kv_cache.layers[li];
8634 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8635 gdn_pair(
8636 &self.ws.n1,
8637 &self.ws.n2,
8638 w,
8639 &cfg,
8640 state,
8641 scratch,
8642 self.pool.as_deref(),
8643 )
8644 }
8645 AttnKind::ShortConv(w) => {
8646 let cfg = self
8647 .short_conv_cfg
8648 .expect("short-conv layer without short_conv_cfg");
8649 let layer = &mut self.kv_cache.layers[li];
8650 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8651 short_conv_pair(
8652 &self.ws.n1,
8653 &self.ws.n2,
8654 w,
8655 &cfg,
8656 state,
8657 scratch,
8658 self.pool.as_deref(),
8659 )
8660 }
8661 AttnKind::Full {
8662 wq,
8663 wk,
8664 wv,
8665 wo,
8666 q_norm,
8667 k_norm,
8668 output_gate,
8669 softplus_gate,
8670 bias,
8671 } => {
8672 let inv_freq_l = self.layer_inv_freq(li);
8673 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8674 let cfg = QwenAttnCfg {
8675 num_heads: self.layer_num_heads(li),
8676 num_kv_heads: nkv_l,
8677 head_dim: hd_l,
8678 hidden_size: hs,
8679 position,
8680 inv_freq: &inv_freq_l,
8681 rotary_dim: rd_l,
8682 scale: self.attn_scale,
8683 softcap: self.attn_softcap,
8684 window: self.layer_window(li),
8685 v_norm: self.attn_v_norm,
8686 qk_norm_after_rope: self.qk_norm_after_rope,
8687 gate_sigmoid: self.proj_gate_sigmoid,
8688 q_norm: q_norm.as_deref(),
8689 k_norm: k_norm.as_deref(),
8690 output_gate: *output_gate,
8691 softplus_gate: softplus_gate
8692 .as_ref()
8693 .map(|(gate, per_head)| (gate, *per_head)),
8694 rope_scale: self.layer_rope_scale(li),
8695 bias: bias
8696 .as_ref()
8697 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8698 rms_eps: eps,
8699 norm_style: self.norm_style,
8700 pool: pool.as_deref(),
8701 v_head_dim: self.layer_v_dim(li),
8702 };
8703 attention::qwen_attention_pair(
8704 &self.ws.n1,
8705 &self.ws.n2,
8706 wq,
8707 wk,
8708 wv,
8709 wo,
8710 &mut self.kv_cache.layers[li],
8711 &cfg,
8712 )
8713 }
8714 };
8715 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8716 Some(w) => (
8717 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8718 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8719 ),
8720 None => (a1, a2),
8721 };
8722 for i in 0..self.hidden_size {
8723 h1[i] += a1[i];
8724 h2[i] += a2[i];
8725 }
8726 let (mut a1, mut a2) = (a1, a2);
8727 attention::recycle_buf(&mut a1);
8728 attention::recycle_buf(&mut a2);
8729
8730 let lw = &self.weights.layers[self.phys_layer(li)];
8731 inference::rms_norm_into(
8732 &h1,
8733 &lw.post_norm,
8734 self.rms_eps,
8735 self.norm_style,
8736 &mut self.ws.p1,
8737 );
8738 inference::rms_norm_into(
8739 &h2,
8740 &lw.post_norm,
8741 self.rms_eps,
8742 self.norm_style,
8743 &mut self.ws.p2,
8744 );
8745 let (f1, f2) = match &lw.ffn {
8746 FfnKind::DenseMoe(dm) => (
8749 dense_moe_ffn(
8750 dm,
8751 &self.ws.p1,
8752 &h1,
8753 self.rms_eps,
8754 self.norm_style,
8755 self.pool.as_deref(),
8756 ),
8757 dense_moe_ffn(
8758 dm,
8759 &self.ws.p2,
8760 &h2,
8761 self.rms_eps,
8762 self.norm_style,
8763 self.pool.as_deref(),
8764 ),
8765 ),
8766 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8767 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8768 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8769 ),
8770 _ => ffn_forward_pair(
8771 &lw.ffn,
8772 &self.ws.p1,
8773 &self.ws.p2,
8774 self.pool.as_deref(),
8775 None,
8776 ),
8777 };
8778 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8779 Some(w) => (
8780 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8781 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8782 ),
8783 None => (f1, f2),
8784 };
8785 for i in 0..self.hidden_size {
8786 h1[i] += f1[i];
8787 h2[i] += f2[i];
8788 }
8789 let (mut f1, mut f2) = (f1, f2);
8790 attention::recycle_buf(&mut f1);
8791 attention::recycle_buf(&mut f2);
8792 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8793 for i in 0..self.hidden_size {
8794 h1[i] *= sc;
8795 h2[i] *= sc;
8796 }
8797 }
8798 if self.is_loop_end(li) && li + 1 < self.num_layers {
8800 h1 = inference::rms_norm(
8801 &h1,
8802 &self.weights.final_norm,
8803 self.rms_eps,
8804 self.norm_style,
8805 );
8806 h2 = inference::rms_norm(
8807 &h2,
8808 &self.weights.final_norm,
8809 self.rms_eps,
8810 self.norm_style,
8811 );
8812 }
8813 }
8814 crate::gpu::set_layer(-1); if self.o1_active() {
8821 self.commit_linear_scratch();
8822 }
8823 self.o1_progress();
8824 self.swa_trim_tails();
8825 (h1, h2)
8826 }
8827
8828 fn commit_linear_scratch(&mut self) {
8830 for layer in &mut self.kv_cache.layers {
8831 if !layer.linear_scratch.is_empty() {
8832 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8833 layer.linear_scratch.clear();
8834 }
8835 }
8836 }
8837
8838 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8846 #[cfg(target_os = "macos")]
8847 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8848 if ids.is_empty() {
8849 return Err("empty id sequence".to_string());
8850 }
8851
8852 self.nll_begin()?;
8853 let result = (|| {
8854 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8855 self.nll_check_graph("encoder prefill", 0)?;
8856 let expected = ids.len() * self.hidden_size;
8857 if hidden.len() != expected {
8858 return Err(format!(
8859 "encoder prefill returned {} values for {} rows of hidden size {}",
8860 hidden.len(),
8861 ids.len(),
8862 self.hidden_size
8863 ));
8864 }
8865
8866 let mut out = vec![0.0f32; expected];
8867 for (src, dst) in hidden
8868 .chunks_exact(self.hidden_size)
8869 .zip(out.chunks_exact_mut(self.hidden_size))
8870 {
8871 inference::rms_norm_into(
8872 src,
8873 &self.weights.final_norm,
8874 self.rms_eps,
8875 self.norm_style,
8876 dst,
8877 );
8878 }
8879 Ok(out)
8880 })();
8881 self.nll_end();
8882 result
8883 }
8884
8885 pub fn forward_ids(
8888 &mut self,
8889 ids: &[u32],
8890 task_mask: Option<&TaskMask>,
8891 ) -> Result<Vec<f32>, String> {
8892 #[cfg(target_os = "macos")]
8893 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8894 if ids.is_empty() {
8895 return Err("empty id sequence".to_string());
8896 }
8897 self.clear_sequence_state();
8898 self.check_forward_graph("forward_ids setup", 0)?;
8899 if task_mask.is_none() {
8900 self.o1_begin();
8901 }
8902 let mut hidden = vec![0.0f32; self.hidden_size];
8903 let mut pos = 0usize;
8904 if let Some(b) = &mut self.dsv41 {
8905 let pool = self.pool.clone();
8906 let mut logits = Vec::new();
8907 crate::dsv41::forward_chunk(
8908 &b.0,
8909 &b.1,
8910 &b.2,
8911 &mut b.3,
8912 ids,
8913 0,
8914 pool.as_deref(),
8915 &mut logits,
8916 );
8917 if let Err(err) = self.o1_seal_checked() {
8918 self.clear_sequence_state();
8919 return Err(err);
8920 }
8921 return Ok(logits);
8922 }
8923 #[cfg(not(target_os = "macos"))]
8934 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8935 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8936 Ok(r) => r,
8937 Err(e) => {
8938 self.clear_sequence_state();
8939 return Err(e);
8940 }
8941 };
8942 if n > 0 {
8943 hidden.copy_from_slice(&last);
8944 pos = n;
8945 }
8946 }
8947 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8948 let chunk = self.prefill_chunk();
8952 let hs = self.hidden_size;
8953 while pos < ids.len() {
8954 let end = (pos + chunk).min(ids.len());
8955 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8956 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8957 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8958 pos = end;
8959 }
8960 }
8961 if task_mask.is_none()
8970 && !self.graph_prefill_preferred()
8971 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8972 && self.pair_supported()
8973 {
8974 while pos + 1 < ids.len() {
8975 let e1 = self.embed_single(ids[pos]);
8976 let e2 = self.embed_single(ids[pos + 1]);
8977 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8978 self.check_forward_graph("forward_ids pair", pos + 1)?;
8979 self.commit_linear_scratch();
8980 hidden = h2;
8981 pos += 2;
8982 }
8983 }
8984 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8987 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8988 self.graph_logits = Some(lg);
8989 hidden = vec![0.0; self.hidden_size];
8990 pos = ids.len();
8991 }
8992 }
8993 while pos < ids.len() {
8994 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8995 self.check_forward_graph("forward_ids", pos)?;
8996 pos += 1;
8997 }
8998 if let Some(logits) = self.graph_logits.take() {
8999 if let Err(err) = self.o1_seal_checked() {
9003 self.clear_sequence_state();
9004 return Err(err);
9005 }
9006 return Ok(logits);
9007 }
9008 if let Err(err) = self.o1_seal_checked() {
9012 self.clear_sequence_state();
9013 return Err(err);
9014 }
9015 let normed = inference::rms_norm(
9016 &hidden,
9017 &self.weights.final_norm,
9018 self.rms_eps,
9019 self.norm_style,
9020 );
9021 Ok(self.lm_head_forward(&normed))
9022 }
9023
9024 #[doc(hidden)]
9028 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9029 #[cfg(target_os = "macos")]
9030 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9031 if ids.is_empty() {
9032 return Err("empty id sequence".to_string());
9033 }
9034 self.clear_sequence_state();
9035 self.dsv41
9036 .as_ref()
9037 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9038 self.o1_begin();
9039 let rows = {
9040 let pool = self.pool.clone();
9041 let b = self
9042 .dsv41
9043 .as_mut()
9044 .expect("dsv41 checked above; state cannot change during forward");
9045 let mut rows = Vec::with_capacity(ids.len());
9046 for (position, &id) in ids.iter().enumerate() {
9047 let mut logits = Vec::new();
9048 crate::dsv41::forward_token(
9049 &b.0,
9050 &b.1,
9051 &b.2,
9052 &mut b.3,
9053 id,
9054 position,
9055 pool.as_deref(),
9056 &mut logits,
9057 );
9058 rows.push(logits);
9059 }
9060 rows
9061 };
9062 self.o1_seal();
9063 Ok(rows)
9064 }
9065
9066 pub fn dsv41_moe_capture(
9070 &mut self,
9071 ids: &[u32],
9072 lo: usize,
9073 hi: usize,
9074 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9075 #[cfg(target_os = "macos")]
9076 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9077 if ids.is_empty() {
9078 return Err("empty id sequence".to_string());
9079 }
9080 self.dsv41
9081 .as_ref()
9082 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9083 self.clear_sequence_state();
9084 let res = {
9085 let pool = self.pool.clone();
9086 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9087 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9088 };
9089 self.clear_sequence_state();
9090 res
9091 }
9092
9093 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9100 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9101 Ok((nll / cnt.max(1) as f64).exp())
9102 }
9103
9104 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9111 if !self.can_prefill_batched() {
9112 return self.nll_ids_from(ids, 0).map(|_| ());
9113 }
9114 self.nll_begin()?;
9115 let result: Result<(), String> = (|| {
9116 let chunk = chunk.max(1);
9117 let mut pos = 0usize;
9118 while pos < ids.len() {
9119 let end = (pos + chunk).min(ids.len());
9120 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9121 self.nll_check_graph("calibration prefill", pos)?;
9122 pos = end;
9123 }
9124 Ok(())
9125 })();
9126 self.nll_end();
9127 result
9128 }
9129
9130 pub fn calib_window_span(
9139 &mut self,
9140 ids: &[u32],
9141 input: Option<&[f32]>,
9142 from: usize,
9143 mid: usize,
9144 upto: usize,
9145 ) -> Result<Vec<f32>, String> {
9146 if !self.can_prefill_batched() {
9147 return Err("calib_window_span: this architecture has no batched prefill".into());
9148 }
9149 self.nll_begin()?;
9150 let result: Result<Vec<f32>, String> = (|| {
9151 let h = if mid > from {
9152 let inp = match input {
9153 Some(h) => PrefillIn::Hidden(h),
9154 None => PrefillIn::Ids(ids),
9155 };
9156 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9157 self.nll_check_graph("calibration span", 0)?;
9158 h
9159 } else {
9160 input.map(<[f32]>::to_vec).unwrap_or_default()
9161 };
9162 if upto > mid {
9163 let inp = if mid > from || input.is_some() {
9164 PrefillIn::Hidden(&h)
9165 } else {
9166 PrefillIn::Ids(ids)
9167 };
9168 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9169 self.nll_check_graph("calibration span", 0)?;
9170 }
9171 Ok(h)
9172 })();
9173 self.nll_end();
9174 result
9175 }
9176
9177 #[cfg(feature = "gpu")]
9181 pub fn qwen4_capture(
9182 &mut self,
9183 ids: &[u32],
9184 lo: usize,
9185 hi: usize,
9186 ) -> Result<Vec<Vec<f32>>, String> {
9187 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9188 crate::qwen4_exp::capture_window(
9189 &b.0,
9190 &b.1,
9191 &b.2,
9192 &mut b.3,
9193 ids,
9194 lo,
9195 hi,
9196 &self.inv_freq,
9197 self.pool.as_deref(),
9198 )
9199 }
9200
9201 pub fn qwen4_expert_dirs(
9203 &self,
9204 li: usize,
9205 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9206 let b = self.qwen4_exp.as_ref()?;
9207 crate::qwen4_exp::expert_dirs(&b.1, li)
9208 }
9209
9210 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9225 self.reset_mtp_probe_state();
9226 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9230 let result = if ids.len() < 2 {
9231 Err("MTP FFN probe needs at least two token ids".into())
9232 } else if ids
9233 .iter()
9234 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9235 {
9236 Err("MTP FFN probe received a token id outside the embedding table".into())
9237 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9238 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9241 } else if self.mimo_mtp.is_some() {
9242 let mut st = self
9243 .mimo_mtp
9244 .take()
9245 .expect("checked MiMo MTP before detaching it");
9246 let result = crate::gpu::cpu_scope(|| {
9247 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids)
9248 });
9249 st.reset();
9252 self.mimo_mtp = Some(st);
9253 result
9254 } else if self.mtp.is_some() {
9255 let mut mtp = self
9256 .mtp
9257 .take()
9258 .expect("checked canonical MTP before detaching it");
9259 let result = crate::gpu::cpu_scope(|| self.mtp_ffn_probe_teacher_forced(&mut mtp, ids));
9260 mtp.kv.clear();
9261 self.mtp = Some(mtp);
9262 result
9263 } else {
9264 Err("MTP FFN probe: this pipeline has no draft stack".into())
9265 };
9266 self.reset_mtp_probe_state();
9269 result
9270 }
9271
9272 fn reset_mtp_probe_state(&mut self) {
9274 self.clear_sequence_state();
9275 if let Some(mtp) = self.mtp.as_mut() {
9276 mtp.kv.clear();
9277 }
9278 if let Some(mimo) = self.mimo_mtp.as_mut() {
9279 mimo.reset();
9280 }
9281 self.mtp_graph_mode = None;
9282 }
9283
9284 fn mtp_ffn_probe_teacher_forced(
9290 &mut self,
9291 mtp: &mut MtpModule,
9292 ids: &[u32],
9293 ) -> Result<Vec<MtpFfnProbe>, String> {
9294 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9295 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9296 };
9297 let mut mass = vec![0.0f64; dense.gate_proj.rows()];
9298 let mut squared_mass = vec![0.0f64; dense.gate_proj.rows()];
9299 for position in 0..ids.len() - 1 {
9300 let hidden = self.forward_layers(&self.embed_single(ids[position]), position, None);
9301 if hidden.len() != self.hidden_size {
9302 return Err(format!(
9303 "MTP FFN probe: trunk hidden width {} != {}",
9304 hidden.len(),
9305 self.hidden_size
9306 ));
9307 }
9308 self.mtp_probe_step_ffn(
9309 mtp,
9310 &hidden,
9311 ids[position + 1],
9312 position,
9313 &mut mass,
9314 &mut squared_mass,
9315 )?;
9316 }
9317 Ok(vec![MtpFfnProbe {
9318 mass,
9319 squared_mass,
9320 observed_positions: ids.len() - 1,
9321 }])
9322 }
9323
9324 fn mtp_probe_step_ffn(
9329 &mut self,
9330 mtp: &mut MtpModule,
9331 hidden: &[f32],
9332 next_token: u32,
9333 position: usize,
9334 mass: &mut [f64],
9335 squared_mass: &mut [f64],
9336 ) -> Result<(), String> {
9337 let e = self.embed_single(next_token);
9338 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9339 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9340 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9341 inference::rms_norm_into(
9342 hidden,
9343 &mtp.hnorm,
9344 self.rms_eps,
9345 self.norm_style,
9346 cat_h,
9347 );
9348 let mut x = vec![0.0f32; self.hidden_size];
9349 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9350
9351 let lw = &mtp.layer;
9352 inference::rms_norm_into(
9353 &x,
9354 &lw.input_norm,
9355 self.rms_eps,
9356 self.norm_style,
9357 &mut self.ws.n1,
9358 );
9359 let attn = match &lw.attn {
9360 AttnKind::Full {
9361 wq,
9362 wk,
9363 wv,
9364 wo,
9365 q_norm,
9366 k_norm,
9367 output_gate,
9368 softplus_gate,
9369 bias,
9370 } => {
9371 let mut cfg = self.attn_cfg(position);
9372 cfg.q_norm = q_norm.as_deref();
9373 cfg.k_norm = k_norm.as_deref();
9374 cfg.output_gate = *output_gate;
9375 cfg.softplus_gate = softplus_gate
9376 .as_ref()
9377 .map(|(gate, per_head)| (gate, *per_head));
9378 cfg.bias = bias
9379 .as_ref()
9380 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9381 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9382 }
9383 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9384 };
9385 for (dst, src) in x.iter_mut().zip(attn) {
9386 *dst += src;
9387 }
9388 inference::rms_norm_into(
9389 &x,
9390 &lw.post_norm,
9391 self.rms_eps,
9392 self.norm_style,
9393 &mut self.ws.p1,
9394 );
9395 let FfnKind::Dense(dense) = &lw.ffn else {
9396 return Err("MTP FFN probe: draft FFN is not dense".into());
9397 };
9398 accumulate_dense_ffn_activation_mass(
9399 dense,
9400 &self.ws.p1,
9401 1,
9402 self.pool.as_deref(),
9403 mass,
9404 squared_mass,
9405 )?;
9406 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9409 for (dst, src) in x.iter_mut().zip(ffn) {
9410 *dst += src;
9411 }
9412 Ok(())
9413 }
9414
9415 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9420 self.clear_sequence_state();
9421 FFN_PROBE.with(|p| {
9422 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9423 });
9424 crate::gpu::cpu_scope(|| {
9425 for (pos, &id) in ids.iter().enumerate() {
9426 let emb = self.embed_single(id);
9427 let _ = self.forward_layers(&emb, pos, None);
9428 }
9429 });
9430 self.clear_sequence_state();
9431 FFN_PROBE
9432 .with(|p| p.borrow_mut().take())
9433 .unwrap_or_default()
9434 }
9435
9436 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9440 if let Err(err) = self.nll_begin() {
9441 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9445 self.nll_end();
9446 return Err(err);
9447 }
9448 FFN_PROBE.with(|p| {
9449 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9450 });
9451 let result: Result<(), String> = (|| {
9452 for chunk in ids.chunks(256) {
9453 if chunk.len() < 2 {
9454 continue;
9455 }
9456 self.nll_ids_masked(chunk, 0, None)?;
9457 }
9458 Ok(())
9459 })();
9460 self.nll_end();
9461 let probe = FFN_PROBE
9462 .with(|p| p.borrow_mut().take())
9463 .unwrap_or_default();
9464 match result {
9465 Ok(()) => Ok(probe),
9466 Err(err) => {
9467 drop(probe);
9468 Err(err)
9469 }
9470 }
9471 }
9472
9473 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9477 self.nll_begin()?;
9478 let result: Result<f64, String> = (|| {
9479 let mut nll = 0f64;
9480 let mut cnt = 0usize;
9481 let mut hidden = vec![0f32; self.hidden_size];
9482 for (pos, &id) in ids.iter().enumerate() {
9483 if pos > 0 {
9484 inference::rms_norm_into(
9485 &hidden,
9486 &self.weights.final_norm,
9487 self.rms_eps,
9488 self.norm_style,
9489 &mut self.ws.n1,
9490 );
9491 let mut logits = self.lm_head_forward(&self.ws.n1);
9492 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9493 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9494 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9495 nll -= p.max(1e-300).ln();
9496 cnt += 1;
9497 attention::recycle_buf(&mut logits);
9498 }
9499 let emb = self.embed_single(id);
9500 hidden = self.forward_layers(&emb, pos, Some(mask));
9501 self.nll_check_graph("masked serial forward", pos)?;
9502 let _ = self.graph_logits.take();
9506 }
9507 Ok((nll / cnt.max(1) as f64).exp())
9508 })();
9509 self.nll_end();
9510 result
9511 }
9512
9513 pub fn nll_ids_masked(
9532 &mut self,
9533 ids: &[u32],
9534 start: usize,
9535 task_mask: Option<&TaskMask>,
9536 ) -> Result<(f64, usize), String> {
9537 let task_mask = self.drop_open_mask(task_mask);
9538 self.nll_ids_inner(ids, start, task_mask)
9539 }
9540
9541 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9542 self.nll_ids_inner(ids, start, None)
9543 }
9544
9545 fn nll_ids_inner(
9546 &mut self,
9547 ids: &[u32],
9548 start: usize,
9549 task_mask: Option<&TaskMask>,
9550 ) -> Result<(f64, usize), String> {
9551 self.nll_begin()?;
9552 let result: Result<(f64, usize), String> = (|| {
9553 let mut nll = 0f64;
9554 let mut cnt = 0usize;
9555 let (graph_quality, fused_head_quality) = nll_graph_policy(
9568 task_mask.is_none(),
9569 self.graph_prefill_preferred(),
9570 crate::gpu::q1_force(),
9571 );
9572 self.graph_head_required = fused_head_quality;
9573 self.graph_want_logits = fused_head_quality;
9574 #[cfg(target_os = "macos")]
9575 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9576 match self.nll_batch_metal(ids, start) {
9577 MetalBatchNllOutcome::Completed(nll, count) => {
9578 return Ok((nll, count));
9579 }
9580 MetalBatchNllOutcome::Declined => {}
9581 MetalBatchNllOutcome::Failed(err) => return Err(err),
9582 }
9583 }
9584 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9589 if self.can_prefill_batched() && !graph_quality && !force_serial {
9590 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9599 .ok()
9600 .and_then(|v| v.parse().ok())
9601 .filter(|&c: &usize| c > 0)
9602 .unwrap_or(128);
9603 const LM_SUB: usize = 32;
9604 let n = ids.len().saturating_sub(1);
9605 let hs = self.hidden_size;
9606 let rows = self.weights.lm_head.rows();
9607 let mut pos = 0usize;
9608 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9609 while pos < n {
9610 let end = (pos + chunk_len).min(n);
9611 let bsz = end - pos;
9612 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9613 self.nll_check_graph("batched prefill", pos)?;
9614 if state_trace && end % 256 == 0 {
9615 self.trace_recurrent_state(end);
9616 }
9617 let mut k0 = 0usize;
9618 while k0 < bsz {
9619 let k1 = (k0 + LM_SUB).min(bsz);
9620 let sb = k1 - k0;
9621 if pos + k1 <= start {
9624 k0 = k1;
9625 continue;
9626 }
9627 let mut normed = vec![0.0f32; sb * hs];
9628 for k in 0..sb {
9629 let r = inference::rms_norm(
9630 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9631 &self.weights.final_norm,
9632 self.rms_eps,
9633 self.norm_style,
9634 );
9635 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9636 }
9637 let mut logits = vec![0.0f32; sb * rows];
9638 self.weights
9639 .lm_head
9640 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9641 for k in 0..sb {
9642 if pos + k0 + k < start {
9643 continue;
9644 }
9645 self.nll_check_graph("batched score row", pos + k0 + k)?;
9646 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9647 if let Some(mu) = self.logit_multiplier {
9648 for v in lg.iter_mut() {
9649 *v *= mu;
9650 }
9651 }
9652 if let Some(c) = self.final_softcap {
9656 softcap_in_place(lg, c, self.pool.as_deref());
9657 }
9658 if let Some(cm) = self.head_clusters.clone() {
9661 self.hierarchical_head_logprobs(
9662 &normed[k * hs..(k + 1) * hs],
9663 &cm,
9664 lg,
9665 );
9666 }
9667 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9668 let target = ids[pos + k0 + k + 1] as usize;
9669 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9670 let lse: f64 = lg
9671 .iter()
9672 .map(|&v| ((v - max) as f64).exp())
9673 .sum::<f64>()
9674 .ln()
9675 + max as f64;
9676 nll += lse - lg[target] as f64;
9677 cnt += 1;
9678 if std::env::var("CMF_PPL_TRACE").is_ok() {
9679 let top = lg
9680 .iter()
9681 .enumerate()
9682 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9683 .map(|(i, _)| i)
9684 .unwrap_or(0);
9685 eprintln!(
9686 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9687 pos + k0 + k,
9688 target,
9689 lse - lg[target] as f64,
9690 top,
9691 lg[target],
9692 lg[top]
9693 );
9694 }
9695 }
9696 k0 = k1;
9697 }
9698 pos = end;
9699 }
9700 return Ok((nll, cnt));
9701 }
9702 for pos in 0..ids.len().saturating_sub(1) {
9703 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9704 self.nll_check_graph("serial forward", pos)?;
9705 let out_of_band = self.graph_logits.take();
9713 if self.graph_head_required && out_of_band.is_none() {
9714 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9715 return Err(format!(
9716 "fused Metal graph head did not complete at NLL position {pos}"
9717 ));
9718 }
9719 if pos < start {
9720 continue;
9721 }
9722 let logits = match out_of_band {
9723 Some(lg) => lg,
9724 None => {
9725 let normed = inference::rms_norm(
9726 &hidden,
9727 &self.weights.final_norm,
9728 self.rms_eps,
9729 self.norm_style,
9730 );
9731 self.lm_head_forward(&normed)
9735 }
9736 };
9737 let target = ids[pos + 1] as usize;
9738 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9739 let lse: f64 = logits
9740 .iter()
9741 .map(|&v| ((v - max) as f64).exp())
9742 .sum::<f64>()
9743 .ln()
9744 + max as f64;
9745 let tok_nll = lse - logits[target] as f64;
9746 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9747 let top = logits
9748 .iter()
9749 .enumerate()
9750 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9751 .map(|(i, _)| i)
9752 .unwrap_or(0);
9753 eprintln!(
9754 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9755 logits[target], logits[top]
9756 );
9757 }
9758 nll += tok_nll;
9759 cnt += 1;
9760 }
9761 Ok((nll, cnt))
9762 })();
9763 self.nll_end();
9764 result
9765 }
9766
9767 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9772 let normed = inference::rms_norm(
9773 hidden,
9774 &self.weights.final_norm,
9775 self.rms_eps,
9776 self.norm_style,
9777 );
9778 let mut logits = self.lm_head_forward(&normed);
9781 let target = target as usize;
9782 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9783 let lse: f64 = logits
9784 .iter()
9785 .map(|&v| ((v - max) as f64).exp())
9786 .sum::<f64>()
9787 .ln()
9788 + max as f64;
9789 let tok_nll = lse - logits[target] as f64;
9790 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9791 let top = logits
9792 .iter()
9793 .enumerate()
9794 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9795 .map(|(i, _)| i)
9796 .unwrap_or(0);
9797 eprintln!(
9798 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9799 logits[target], logits[top]
9800 );
9801 }
9802 attention::recycle_buf(&mut logits);
9803 tok_nll
9804 }
9805
9806 fn trace_recurrent_state(&self, pos: usize) {
9814 let stats = |v: &[f32]| -> (f64, f64) {
9815 if v.is_empty() {
9816 return (0.0, 0.0);
9817 }
9818 let (mut ss, mut mx) = (0f64, 0f64);
9819 for &x in v {
9820 ss += (x as f64) * (x as f64);
9821 mx = mx.max((x as f64).abs());
9822 }
9823 ((ss / v.len() as f64).sqrt(), mx)
9824 };
9825 for (li, l) in self.kv_cache.layers.iter().enumerate() {
9826 let lw = &self.weights.layers[self.phys_layer(li)];
9827 let (kind, s_len) = match &lw.attn {
9828 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
9829 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
9830 AttnKind::Bounded(_) => ("bounded", 0),
9831 AttnKind::Full { .. } => ("full", 0),
9832 _ => ("other", 0),
9833 };
9834 let (rms, max) = stats(&l.linear_state);
9835 let s_part = if kind == "vmf" {
9836 &l.linear_state[..s_len.min(l.linear_state.len())]
9837 } else if kind == "gdn" {
9838 let ring = l
9839 .linear_state
9840 .len()
9841 .saturating_sub(s_len)
9842 .min(l.linear_state.len());
9843 &l.linear_state[ring..]
9844 } else {
9845 &l.linear_state[..0]
9846 };
9847 let (s_rms, s_max) = stats(s_part);
9848 let (ring_rms, ring_len) = match &l.bounded {
9849 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
9850 None => (0.0, 0),
9851 };
9852 eprintln!(
9853 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
9854 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
9855 l.linear_state.len(),
9856 l.seq_len
9857 );
9858 }
9859 }
9860
9861 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
9879 self.nll_begin()?;
9884 let requested_prefix = (prefill > 0).then_some(prefill);
9885 self.o1_begin_with_prefix(requested_prefix);
9886 let n = ids.len().saturating_sub(1);
9887 let requested_start = prefill.min(n);
9888 let exact_end = if self.o1_active() {
9893 match requested_prefix {
9894 Some(requested) => self.o1_effective_boundary(requested),
9895 None => self
9896 .o1_cfg
9897 .as_ref()
9898 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
9899 }
9900 .unwrap_or(requested_start)
9901 .min(n)
9902 } else {
9903 requested_start
9904 };
9905 let mut nll = 0f64;
9906 let mut cnt = 0usize;
9907
9908 let mut pos = 0usize;
9912 if self.can_prefill_batched() {
9913 const CHUNK: usize = 128;
9914 while pos < exact_end {
9915 let end = (pos + CHUNK).min(exact_end);
9916 let hiddens = self.prefill_batch(&ids[pos..end], pos);
9917 if self
9918 .graph_failed
9919 .swap(false, std::sync::atomic::Ordering::Relaxed)
9920 {
9921 self.cancel
9922 .store(false, std::sync::atomic::Ordering::Relaxed);
9923 self.nll_end();
9924 return Err("GPU graph failed during O(1) NLL prefix".into());
9925 }
9926 for row in 0..end - pos {
9927 let score_pos = pos + row;
9928 if score_pos >= requested_start && score_pos < n {
9929 nll += self.nll_from_hidden(
9930 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
9931 ids[score_pos + 1],
9932 score_pos,
9933 );
9934 cnt += 1;
9935 }
9936 }
9937 pos = end;
9938 }
9939 } else {
9940 while pos < exact_end {
9941 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9942 if self
9943 .graph_failed
9944 .swap(false, std::sync::atomic::Ordering::Relaxed)
9945 {
9946 self.cancel
9947 .store(false, std::sync::atomic::Ordering::Relaxed);
9948 self.nll_end();
9949 return Err("GPU graph failed during O(1) NLL prefix".into());
9950 }
9951 if pos >= requested_start && pos < n {
9952 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9953 cnt += 1;
9954 }
9955 pos += 1;
9956 }
9957 }
9958 self.o1_seal_checked().map_err(|err| {
9959 self.nll_end();
9960 err
9961 })?;
9962
9963 let batch_k = std::env::var("CMF_BATCH_K")
9972 .ok()
9973 .and_then(|v| v.parse::<usize>().ok())
9974 .unwrap_or(0);
9975 let batch_admitted = batch_k > 0
9976 && self.can_prefill_batched()
9977 && self.o1_active()
9978 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
9979 && (0..self.num_layers).all(|li| {
9980 let cache = &self.kv_cache.layers[self.phys_layer(li)];
9981 cache.o1.is_none() || cache.o1_views().is_some()
9982 });
9983 if std::env::var("CMF_GRAPH_PROF").is_ok() {
9984 eprintln!(
9985 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
9986 batch_admitted,
9987 batch_k,
9988 n.saturating_sub(exact_end),
9989 );
9990 }
9991 let mut batch_completed = false;
9992 if batch_admitted && exact_end < n {
9993 let hs = self.hidden_size;
9994 let mut batch_pos = exact_end;
9995 while batch_pos < n {
9996 let end = (batch_pos + batch_k).min(n);
9997 let bk = end - batch_pos;
9998 let mut hiddens = vec![0.0f32; bk * hs];
9999 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10000 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10001 }
10002 let positions: Vec<usize> = (batch_pos..end).collect();
10003 let t_batch = std::time::Instant::now();
10004 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10005 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10006 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10007 eprintln!(
10008 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10009 batch_pos,
10010 end.saturating_sub(1),
10011 bk as f64 / (ms / 1000.0),
10012 );
10013 }
10014 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10015 self.nll_end();
10016 return Err(err);
10017 }
10018 match outcome {
10019 crate::gpu::BatchGraphOutcome::Completed => {
10020 batch_completed = true;
10021 for row in 0..bk {
10022 nll += self.nll_from_hidden(
10023 &hiddens[row * hs..(row + 1) * hs],
10024 ids[batch_pos + row + 1],
10025 batch_pos + row,
10026 );
10027 cnt += 1;
10028 }
10029 batch_pos = end;
10030 }
10031 crate::gpu::BatchGraphOutcome::Declined => {
10032 if batch_completed {
10033 self.nll_end();
10034 return Err(format!(
10035 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10036 ));
10037 }
10038 break;
10039 }
10040 crate::gpu::BatchGraphOutcome::Failed => {
10041 self.nll_end();
10042 return Err(format!(
10043 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10044 ));
10045 }
10046 }
10047 }
10048 if batch_completed && cnt == n.saturating_sub(requested_start) {
10049 self.nll_end();
10050 return Ok((nll, cnt));
10051 }
10052 }
10053
10054 for pos in exact_end..n {
10059 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10060 if self
10061 .graph_failed
10062 .swap(false, std::sync::atomic::Ordering::Relaxed)
10063 {
10064 self.cancel
10065 .store(false, std::sync::atomic::Ordering::Relaxed);
10066 self.nll_end();
10067 return Err(format!(
10068 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10069 ));
10070 }
10071 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10072 cnt += 1;
10073 }
10074 self.nll_end();
10075 Ok((nll, cnt))
10076 }
10077
10078 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10086 self.clear_sequence_state();
10087 let n = ids.len().saturating_sub(1);
10088 let mut correct = Vec::with_capacity(n);
10089 let mut pmax = Vec::with_capacity(n);
10090 for pos in 0..n {
10091 let emb = self.embed_single(ids[pos]);
10092 let hidden = self.forward_layers(&emb, pos, None);
10093 let logits = if let Some(logits) = self.graph_logits.take() {
10094 logits
10095 } else {
10096 let normed = inference::rms_norm(
10097 &hidden,
10098 &self.weights.final_norm,
10099 self.rms_eps,
10100 self.norm_style,
10101 );
10102 self.lm_head_forward(&normed)
10106 };
10107 let target = ids[pos + 1] as usize;
10108 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10109 for (i, &v) in logits.iter().enumerate() {
10110 if v > mval {
10111 mval = v;
10112 amax = i;
10113 }
10114 }
10115 correct.push(amax == target);
10116 let row: Vec<f32> = temps
10117 .iter()
10118 .map(|&t| {
10119 let tt = t.max(1e-3);
10120 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10121 1.0 / s.max(1e-12) })
10123 .collect();
10124 pmax.push(row);
10125 }
10126 self.clear_sequence_state();
10127 (correct, pmax)
10128 }
10129
10130 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10137 if self.dyn_router.is_none() {
10138 return Ok((self.ppl_ids(ids)?, 0));
10139 }
10140 self.nll_begin()?;
10141 let saved_active = self.dyn_active;
10142 let mut router = self
10143 .dyn_router
10144 .take()
10145 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10146 router.reset();
10147 self.dyn_phi_seen = 0;
10148 let _ = self.set_active_skill(None);
10149
10150 let result: Result<(f64, usize), String> = (|| {
10151 let mut nll = 0f64;
10152 let mut cnt = 0usize;
10153 for pos in 0..ids.len().saturating_sub(1) {
10154 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10155 self.nll_check_graph("dynamic serial forward", pos)?;
10156 let out_of_band = self.graph_logits.take();
10157 let mut logits = match out_of_band {
10158 Some(lg) => lg,
10159 None => {
10160 let normed = inference::rms_norm(
10161 &hidden,
10162 &self.weights.final_norm,
10163 self.rms_eps,
10164 self.norm_style,
10165 );
10166 self.lm_head_forward(&normed)
10170 }
10171 };
10172 let target = ids[pos + 1] as usize;
10173 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10174 let lse: f64 = logits
10175 .iter()
10176 .map(|&v| ((v - max) as f64).exp())
10177 .sum::<f64>()
10178 .ln()
10179 + max as f64;
10180 let tok_nll = lse - logits[target] as f64;
10181 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10182 let top = logits
10183 .iter()
10184 .enumerate()
10185 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10186 .map(|(i, _)| i)
10187 .unwrap_or(0);
10188 eprintln!(
10189 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10190 logits[target], logits[top]
10191 );
10192 }
10193 nll += tok_nll;
10194 cnt += 1;
10195 attention::recycle_buf(&mut logits);
10196 let phi = self.dyn_phi_ema.clone();
10198 if let Some(new_active) = router.step(&phi, pos) {
10199 let _ = self.set_active_skill(new_active);
10200 }
10201 }
10202 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10203 })();
10204
10205 let _ = self.set_active_skill(saved_active);
10208 self.dyn_router = Some(router);
10209 self.nll_end();
10210 result
10211 }
10212
10213 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10215 self.clear_sequence_state();
10216 let mut acc = vec![0f32; self.hidden_size];
10217 for (pos, &id) in ids.iter().enumerate() {
10218 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10219 for (a, v) in acc.iter_mut().zip(&h) {
10220 *a += v;
10221 }
10222 }
10223 let n = ids.len().max(1) as f32;
10224 for a in acc.iter_mut() {
10225 *a /= n;
10226 }
10227 self.clear_sequence_state();
10228 acc
10229 }
10230
10231 pub fn probe_phi_span(
10245 &mut self,
10246 ids: &[u32],
10247 layer: usize,
10248 span: std::ops::Range<usize>,
10249 ) -> Vec<f32> {
10250 #[cfg(target_os = "macos")]
10251 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10252 let end = span.end.min(ids.len());
10253 let start = span.start.min(end);
10254 let reset = |p: &mut Self| p.clear_sequence_state();
10255 reset(self);
10256 let mut acc = vec![0f32; self.hidden_size];
10257 for (pos, &id) in ids[..end].iter().enumerate() {
10258 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10259 if pos >= start {
10260 for (a, v) in acc.iter_mut().zip(&h) {
10261 *a += v;
10262 }
10263 }
10264 }
10265 let n = end - start;
10266 if n > 0 {
10267 let n = n as f32;
10268 for a in acc.iter_mut() {
10269 *a /= n;
10270 }
10271 }
10272 reset(self);
10273 acc
10274 }
10275
10276 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10283 #[cfg(target_os = "macos")]
10284 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10285 self.graph_logits = None;
10286 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10287 if let Some(logits) = self.graph_logits.take() {
10288 return logits;
10289 }
10290 inference::rms_norm_into(
10291 &hidden,
10292 &self.weights.final_norm,
10293 self.rms_eps,
10294 self.norm_style,
10295 &mut self.ws.n1,
10296 );
10297 self.lm_head_forward(&self.ws.n1)
10298 }
10299
10300 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10306 self.prefill_batch_masked(ids, start_pos, None)
10307 }
10308
10309 fn prefill_batch_masked(
10315 &mut self,
10316 ids: &[u32],
10317 start_pos: usize,
10318 task_mask: Option<&TaskMask>,
10319 ) -> Vec<f32> {
10320 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10321 }
10322
10323 fn prefill_rows(
10331 &mut self,
10332 ids: &[u32],
10333 pos: usize,
10334 task_mask: Option<&TaskMask>,
10335 ) -> Result<Vec<f32>, String> {
10336 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10337 }
10338
10339 fn prefill_input_rows(
10340 &mut self,
10341 input: PrefillIn<'_>,
10342 pos: usize,
10343 task_mask: Option<&TaskMask>,
10344 ) -> Result<Vec<f32>, String> {
10345 self.mimo_moe_prepare();
10346 let hs = self.hidden_size;
10347 let bk = match input {
10348 PrefillIn::Ids(ids) => ids.len(),
10349 PrefillIn::Hidden(rows) => rows.len() / hs,
10350 };
10351 #[cfg(not(target_os = "macos"))]
10352 if task_mask.is_none()
10353 && !self.o1_active()
10354 && bk > 1
10355 && (self.batch_prefix_prefill()
10356 || (self.verify_exact_moe
10357 && crate::gpu::enabled_here()
10358 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10359 {
10360 let mut hiddens = match input {
10361 PrefillIn::Hidden(rows) => rows.to_vec(),
10362 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10363 };
10364 let positions: Vec<usize> = (pos..pos + bk).collect();
10365 let mut run = 0usize;
10366 match self.try_batch_graph_wgpu_prefix(
10367 &mut hiddens,
10368 &positions,
10369 bk,
10370 None,
10371 Some(&mut run),
10372 ) {
10373 crate::gpu::BatchGraphOutcome::Completed => {
10374 let out = if run < self.num_layers {
10375 self.prefill_batch_span(
10376 PrefillIn::Hidden(&hiddens),
10377 pos,
10378 None,
10379 run,
10380 self.num_layers,
10381 )
10382 } else {
10383 hiddens
10384 };
10385 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10386 Err("MiMo attention graph failed after admission".into())
10387 } else {
10388 Ok(out)
10389 };
10390 }
10391 crate::gpu::BatchGraphOutcome::Failed => {
10392 return Err("batched prefix prefill failed after admission".into());
10393 }
10394 crate::gpu::BatchGraphOutcome::Declined => {
10395 #[cfg(feature = "gpu")]
10397 self.pull_lagging_host_kv(0, self.num_layers, pos);
10398 }
10399 }
10400 }
10401 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10402 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10403 Err("batch tail graph failed after admission".into())
10404 } else {
10405 Ok(out)
10406 }
10407 }
10408
10409 fn prefill_batch_span(
10415 &mut self,
10416 input: PrefillIn<'_>,
10417 start_pos: usize,
10418 task_mask: Option<&TaskMask>,
10419 from: usize,
10420 upto_excl: usize,
10421 ) -> Vec<f32> {
10422 let hs = self.hidden_size;
10423 let b = match input {
10424 PrefillIn::Ids(ids) => ids.len(),
10425 PrefillIn::Hidden(hb) => hb.len() / hs,
10426 };
10427 let upto_excl = upto_excl.min(self.num_layers);
10428 let mut h: Vec<f32>;
10432 let mut h_ready;
10433 match input {
10434 PrefillIn::Ids(_) => {
10435 h = vec![0.0; b * hs];
10436 h_ready = false;
10437 }
10438 PrefillIn::Hidden(hb) => {
10439 h = hb.to_vec();
10440 h_ready = true;
10441 }
10442 }
10443 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10444 if let PrefillIn::Ids(ids) = input {
10445 for (bi, &id) in ids.iter().enumerate() {
10446 let e = me.embed_single(id);
10447 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10448 }
10449 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10450 if let Ok(t) = tp.parse::<usize>() {
10451 if t >= start_pos && t < start_pos + ids.len() {
10452 let bi = t - start_pos;
10453 let row = &h[bi * hs..(bi + 1) * hs];
10454 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10455 eprintln!(
10456 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10457 ids[bi],
10458 row[0],
10459 row[1],
10460 ids.len(),
10461 &ids[..ids.len().min(8)]
10462 );
10463 }
10464 }
10465 }
10466 }
10467 };
10468 let (_nkv, _hd, _rd, eps) = (
10469 self.num_kv_heads,
10470 self.head_dim,
10471 self.rotary_dim,
10472 self.rms_eps,
10473 );
10474 let pool = self.pool.clone();
10475 let norm_style = self.norm_style;
10476 self.mimo_moe_prepare();
10477 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10478
10479 #[cfg(target_os = "macos")]
10480 let mut chunk_skip_until = 0usize;
10481 for li in from..upto_excl {
10482 let _capacity_tail = automatic_gpu_prefix
10483 .filter(|&prefix| {
10484 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10485 })
10486 .map(|_| crate::gpu::enter_cpu_scope());
10487 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10492 let _fast_gemm = self
10493 .proj_gate_sigmoid
10494 .then(crate::gpu::enter_prefill_fast_gemm);
10495 #[cfg(target_os = "macos")]
10501 if task_mask.is_none() {
10502 if li < chunk_skip_until {
10503 continue;
10504 }
10505 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10511 fill_h(&mut h, self);
10512 h_ready = true;
10513 }
10514 let ids_for_embed = match input {
10515 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10516 PrefillIn::Hidden(_) => None,
10517 };
10518 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10519 if end > li {
10520 h_ready = true;
10521 chunk_skip_until = end;
10522 if self.is_loop_end(end - 1) && end < self.num_layers {
10525 for bi in 0..b {
10526 let normed = inference::rms_norm(
10527 &h[bi * hs..(bi + 1) * hs],
10528 &self.weights.final_norm,
10529 eps,
10530 norm_style,
10531 );
10532 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10533 }
10534 }
10535 continue;
10536 }
10537 }
10538 if !h_ready {
10539 fill_h(&mut h, self);
10540 h_ready = true;
10541 }
10542 if task_mask.is_none() && self.verify_exact_moe {
10543 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10544 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10545 crate::gpu::BatchGraphOutcome::Completed => continue,
10546 crate::gpu::BatchGraphOutcome::Failed => return h,
10547 crate::gpu::BatchGraphOutcome::Declined => {}
10548 }
10549 }
10550 #[cfg(feature = "gpu")]
10551 self.pull_lagging_host_kv(li, li + 1, start_pos);
10552 let lw = &self.weights.layers[self.phys_layer(li)];
10553 let t_attn = std::time::Instant::now();
10554 match &lw.attn {
10556 AttnKind::Kda(w) => {
10557 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10559 let mut normed = vec![0.0f32; b * hs];
10560 for bi in 0..b {
10561 inference::rms_norm_into(
10562 &h[bi * hs..(bi + 1) * hs],
10563 &lw.input_norm,
10564 eps,
10565 norm_style,
10566 &mut normed[bi * hs..(bi + 1) * hs],
10567 );
10568 }
10569 let attn = crate::linear_core::kda_forward_batch(
10570 &normed,
10571 b,
10572 w,
10573 &cfg,
10574 &mut self.kv_cache.layers[li].linear_state,
10575 pool.as_deref(),
10576 );
10577 for (dst, &a) in h.iter_mut().zip(&attn) {
10578 *dst += a;
10579 }
10580 }
10581 AttnKind::LinearGdn(w) => {
10582 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10584 let mut normed = vec![0.0f32; b * hs];
10585 for bi in 0..b {
10586 let r = inference::rms_norm(
10587 &h[bi * hs..(bi + 1) * hs],
10588 &lw.input_norm,
10589 eps,
10590 norm_style,
10591 );
10592 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10593 }
10594 let attn = crate::linear_core::gdn_forward_batch(
10595 &normed,
10596 b,
10597 w,
10598 &cfg,
10599 &mut self.kv_cache.layers[li].linear_state,
10600 pool.as_deref(),
10601 );
10602 for (dst, &a) in h.iter_mut().zip(&attn) {
10603 *dst += a;
10604 }
10605 }
10606 AttnKind::ShortConv(w) => {
10607 let cfg = self
10610 .short_conv_cfg
10611 .expect("short-conv layer without short_conv_cfg");
10612 let mut normed = vec![0.0f32; b * hs];
10613 for bi in 0..b {
10614 inference::rms_norm_into(
10615 &h[bi * hs..(bi + 1) * hs],
10616 &lw.input_norm,
10617 eps,
10618 norm_style,
10619 &mut normed[bi * hs..(bi + 1) * hs],
10620 );
10621 }
10622 let attn = short_conv_forward_batch(
10623 &normed,
10624 b,
10625 w,
10626 &cfg,
10627 &mut self.kv_cache.layers[li].linear_state,
10628 pool.as_deref(),
10629 );
10630 for (dst, &a) in h.iter_mut().zip(&attn) {
10631 *dst += a;
10632 }
10633 }
10634 AttnKind::Mla(w) => {
10635 let inv_freq_l = self.layer_inv_freq(li);
10638 let rs = self.layer_rope_scale(li);
10639 let mut normed = vec![0.0f32; hs];
10640 for bi in 0..b {
10641 inference::rms_norm_into(
10642 &h[bi * hs..(bi + 1) * hs],
10643 &lw.input_norm,
10644 eps,
10645 norm_style,
10646 &mut normed,
10647 );
10648 let ao = mla_attention(
10649 w,
10650 &normed,
10651 &mut self.kv_cache.layers[li],
10652 start_pos + bi,
10653 &inv_freq_l,
10654 rs,
10655 eps,
10656 pool.as_deref(),
10657 );
10658 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10659 *dst += a;
10660 }
10661 }
10662 }
10663 AttnKind::Full {
10664 wq,
10665 wk,
10666 wv,
10667 wo,
10668 q_norm,
10669 k_norm,
10670 output_gate,
10671 softplus_gate,
10672 bias,
10673 } => {
10674 let mut normed = vec![0.0f32; b * hs];
10678 for bi in 0..b {
10679 inference::rms_norm_into(
10680 &h[bi * hs..(bi + 1) * hs],
10681 &lw.input_norm,
10682 eps,
10683 norm_style,
10684 &mut normed[bi * hs..(bi + 1) * hs],
10685 );
10686 }
10687 let inv_freq_l = self.layer_inv_freq(li);
10688 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10689 let cfg = QwenAttnCfg {
10690 num_heads: self.layer_num_heads(li),
10691 num_kv_heads: nkv_l,
10692 head_dim: hd_l,
10693 hidden_size: hs,
10694 position: start_pos,
10695 inv_freq: &inv_freq_l,
10696 rotary_dim: rd_l,
10697 scale: self.attn_scale,
10698 softcap: self.attn_softcap,
10699 window: self.layer_window(li),
10700 v_norm: self.attn_v_norm,
10701 qk_norm_after_rope: self.qk_norm_after_rope,
10702 gate_sigmoid: self.proj_gate_sigmoid,
10703 q_norm: q_norm.as_deref(),
10704 k_norm: k_norm.as_deref(),
10705 output_gate: *output_gate,
10706 softplus_gate: softplus_gate
10707 .as_ref()
10708 .map(|(gate, per_head)| (gate, *per_head)),
10709 rope_scale: self.layer_rope_scale(li),
10710 bias: bias
10711 .as_ref()
10712 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10713 rms_eps: eps,
10714 norm_style,
10715 pool: pool.as_deref(),
10716 v_head_dim: self.layer_v_dim(li),
10717 };
10718 #[cfg(feature = "gpu")]
10719 let _mirror = self
10720 .prefill_mirror_target(li)
10721 .map(crate::gpu::enter_prefill_mirror);
10722 let mut attn = attention::qwen_attention_batch(
10723 &normed,
10724 b,
10725 wq,
10726 wk,
10727 wv,
10728 wo,
10729 &mut self.kv_cache.layers[li],
10730 &cfg,
10731 );
10732 if let Some(w) = &lw.attn_out_norm {
10733 for bi in 0..b {
10734 inference::rms_norm_into(
10735 &attn[bi * hs..(bi + 1) * hs],
10736 w,
10737 eps,
10738 norm_style,
10739 &mut normed[bi * hs..(bi + 1) * hs],
10740 );
10741 }
10742 attn.copy_from_slice(&normed);
10743 }
10744 for (dst, &a) in h.iter_mut().zip(&attn) {
10745 *dst += a;
10746 }
10747 }
10748 AttnKind::Bounded(w) => {
10749 let mut normed = vec![0.0f32; b * hs];
10752 for bi in 0..b {
10753 inference::rms_norm_into(
10754 &h[bi * hs..(bi + 1) * hs],
10755 &lw.input_norm,
10756 eps,
10757 norm_style,
10758 &mut normed[bi * hs..(bi + 1) * hs],
10759 );
10760 }
10761 let rope = self
10762 .bounded_rope
10763 .clone()
10764 .expect("bounded layer without an installed rotation table");
10765 let cfg = crate::bounded::BoundedAttnCfg {
10766 num_heads: self.num_heads,
10767 num_kv_heads: self.num_kv_heads,
10768 head_dim: self.head_dim,
10769 hidden_size: hs,
10770 scale: self.attn_scale,
10771 rope: &rope,
10772 pool: pool.as_deref(),
10773 };
10774 let mut attn = crate::bounded::bounded_attention_batch(
10775 &normed,
10776 b,
10777 w,
10778 &mut self.kv_cache.layers[li],
10779 &cfg,
10780 );
10781 if let Some(wn) = &lw.attn_out_norm {
10782 for bi in 0..b {
10783 inference::rms_norm_into(
10784 &attn[bi * hs..(bi + 1) * hs],
10785 wn,
10786 eps,
10787 norm_style,
10788 &mut normed[bi * hs..(bi + 1) * hs],
10789 );
10790 }
10791 attn.copy_from_slice(&normed);
10792 }
10793 for (dst, &a) in h.iter_mut().zip(&attn) {
10794 *dst += a;
10795 }
10796 attention::recycle_buf(&mut attn);
10797 }
10798 AttnKind::Linear(w) => {
10799 for bi in 0..b {
10800 let normed = inference::rms_norm(
10801 &h[bi * hs..(bi + 1) * hs],
10802 &lw.input_norm,
10803 eps,
10804 norm_style,
10805 );
10806 vmf_phase_forward(
10807 &normed,
10808 w,
10809 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
10810 &mut self.kv_cache.layers[li].linear_state,
10811 pool.as_deref(),
10812 )
10813 .iter()
10814 .enumerate()
10815 .for_each(|(i, &a)| h[bi * hs + i] += a);
10816 }
10817 }
10818 }
10819
10820 let lw = &self.weights.layers[self.phys_layer(li)];
10822 let mut post = vec![0.0f32; b * hs];
10823 for bi in 0..b {
10824 let r =
10825 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
10826 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10827 }
10828 let mask_row = task_mask
10831 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
10832 .and_then(|m| m.ffn_masks.get(li))
10833 .map(|v| v.as_slice());
10834 let attn_ns = t_attn.elapsed().as_nanos() as u64;
10835 let t_ffn = std::time::Instant::now();
10836 let mut ffn = match &lw.ffn {
10837 FfnKind::Dense(d) if !d.segs.is_empty() => {
10838 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
10839 }
10840 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
10841 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
10842 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
10843 }
10844 FfnKind::Moe(m) if self.verify_exact_moe => {
10845 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
10846 }
10847 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
10850 let before = m.stats.borrow().clone();
10851 let out = crate::gpu::cpu_scope(|| {
10852 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
10853 });
10854 self.mimo_moe.prime(li, m, &before);
10855 out
10856 }
10857 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
10858 FfnKind::DenseMoe(dm) => {
10861 let mut out = vec![0.0f32; b * hs];
10862 for bi in 0..b {
10863 let r = dense_moe_ffn(
10864 dm,
10865 &post[bi * hs..(bi + 1) * hs],
10866 &h[bi * hs..(bi + 1) * hs],
10867 eps,
10868 norm_style,
10869 pool.as_deref(),
10870 );
10871 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10872 }
10873 out
10874 }
10875 };
10876 if prefill_prof_on() {
10877 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
10878 PREFILL_SPLIT[1].fetch_add(
10879 t_ffn.elapsed().as_nanos() as u64,
10880 std::sync::atomic::Ordering::Relaxed,
10881 );
10882 }
10883 if let Some(w) = &lw.ffn_out_norm {
10884 for bi in 0..b {
10885 inference::rms_norm_into(
10886 &ffn[bi * hs..(bi + 1) * hs],
10887 w,
10888 eps,
10889 norm_style,
10890 &mut post[bi * hs..(bi + 1) * hs],
10891 );
10892 }
10893 ffn.copy_from_slice(&post);
10894 }
10895 for (dst, &f) in h.iter_mut().zip(&ffn) {
10896 *dst += f;
10897 }
10898 if let Some(sc) = lw.layer_scale {
10899 for v in h.iter_mut() {
10900 *v *= sc;
10901 }
10902 }
10903 if self.layer_dump.is_some() {
10905 for bi in 0..b {
10906 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
10907 }
10908 }
10909 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10910 if let Ok(t) = tp.parse::<usize>() {
10911 if t >= start_pos && t < start_pos + b {
10912 let bi = t - start_pos;
10913 let row = &h[bi * hs..(bi + 1) * hs];
10914 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10915 eprintln!(
10916 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
10917 row[0], row[1]
10918 );
10919 }
10920 }
10921 }
10922 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
10926 let row = &h[(b - 1) * hs..b * hs];
10927 let rms =
10928 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
10929 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
10930 eprintln!(
10931 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
10932 match &self.weights.layers[self.phys_layer(li)].attn {
10933 AttnKind::LinearGdn(_) => "gdn",
10934 AttnKind::Linear(_) => "vmf",
10935 AttnKind::ShortConv(_) => "conv",
10936 _ => "attn",
10937 },
10938 match &lw.ffn {
10939 FfnKind::Moe(_) => "moe",
10940 FfnKind::Dense(_) => "dense",
10941 FfnKind::DenseMoe(_) => "dense+moe",
10942 },
10943 );
10944 }
10945 if self.is_loop_end(li) && li + 1 < self.num_layers {
10947 for bi in 0..b {
10948 let normed = inference::rms_norm(
10949 &h[bi * hs..(bi + 1) * hs],
10950 &self.weights.final_norm,
10951 eps,
10952 norm_style,
10953 );
10954 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10955 }
10956 }
10957 if std::env::var("CMF_TRACE_H").is_ok() {
10958 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
10959 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
10960 eprintln!(
10961 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
10962 lw.layer_scale
10963 );
10964 }
10965 }
10966 crate::gpu::set_layer(-1); if prefill_prof_on() {
10968 let ms = |a: &std::sync::atomic::AtomicU64| {
10969 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
10970 };
10971 let sp = &attention::ATTN_SPLIT;
10972 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
10973 eprintln!(
10974 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
10975 host gate {:.1}], host loop {:.1}, attend {:.1} \
10976 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
10977 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
10978 ms(&PREFILL_SPLIT[0]),
10979 ms(&sp[0]),
10980 qkv_enq,
10981 qkv_wait,
10982 ms(&sp[6]),
10983 ms(&sp[1]),
10984 ms(&sp[2]),
10985 ms(&sp[4]),
10986 ms(&sp[5]),
10987 ca_wait,
10988 ms(&sp[3]),
10989 ms(&PREFILL_SPLIT[1]),
10990 );
10991 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
10992 eprintln!(
10993 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
10994 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
10995 fallbacks (cumulative)",
10996 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
10997 );
10998 }
10999 self.o1_progress();
11004 self.swa_trim_tails();
11006 h
11007 }
11008
11009 fn embed_single(&self, id: u32) -> Vec<f32> {
11011 let mut out = vec![0.0f32; self.hidden_size];
11012 if (id as usize) < self.weights.embed_tokens.rows() {
11013 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11014 }
11015 if self.embed_multiplier != 1.0 {
11016 for v in out.iter_mut() {
11017 *v *= self.embed_multiplier;
11018 }
11019 }
11020 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11024 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11025 v[0] = id as f32;
11026 return v;
11027 }
11028 if let Some(b) = &self.g4e {
11032 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11033 }
11034 if let Some(b) = &self.g3n {
11035 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11036 }
11037 out
11038 }
11039
11040 #[cfg(target_os = "macos")]
11046 fn chunk_run_gpu(
11047 &mut self,
11048 li0: usize,
11049 h: &mut [f32],
11050 b: usize,
11051 pos0: usize,
11052 embed_ids: Option<&[u32]>,
11053 cap: usize,
11054 ) -> usize {
11055 let gemma4 =
11062 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11063 if !crate::gpu::enabled_here()
11064 || std::env::var("CMF_GPU_CHUNK")
11065 .map(|v| v == "0")
11066 .unwrap_or(false)
11067 || b < 32
11068 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11071 || (self.global_attn.is_some() && !gemma4)
11072 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11076 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11079 || self.o1_active()
11082 || (self.attn_v_norm && !gemma4)
11083 || !self.metal_rope_scale_ok()
11084 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11085 {
11086 return li0;
11087 }
11088 let Some(model) = self.model.clone() else {
11089 return li0;
11090 };
11091 let (nh, nkv, hd, hs) = (
11092 self.num_heads,
11093 self.num_kv_heads,
11094 self.head_dim,
11095 self.hidden_size,
11096 );
11097 let loop_end = if self.loop_final_norm {
11101 ((li0 / self.physical_layers) + 1) * self.physical_layers
11102 } else {
11103 self.num_layers
11104 };
11105 let run_end = self.num_layers.min(loop_end).min(cap);
11106 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11111 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11112 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11113 let mut stored_at: Vec<usize> = Vec::new();
11114 for li in li0..run_end {
11115 let lw = &self.weights.layers[self.phys_layer(li)];
11116 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11117 && !gemma4
11118 {
11119 break;
11120 }
11121 let AttnKind::Full {
11122 wq,
11123 wk,
11124 wv,
11125 wo,
11126 q_norm,
11127 k_norm,
11128 output_gate: false,
11129 softplus_gate,
11130 bias,
11131 } = &lw.attn
11132 else {
11133 break;
11134 };
11135 let head_gate = match softplus_gate {
11137 None => None,
11138 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11139 Some((d, r, c)) if r == nh && c == hs => Some(d),
11140 _ => break,
11141 },
11142 Some(_) => break,
11143 };
11144 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11148 FfnKind::Dense(d)
11149 if (matches!(d.act, Act::Silu | Act::Gelu)
11150 || (gemma4 && d.act == Act::GeluTanh))
11151 && d.segs.is_empty() =>
11152 {
11153 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11154 }
11155 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11156 Some((router, experts, q8)) => {
11157 let e0 = &m.experts[0];
11158 let route = Box::new(move |lg: &[f32]| {
11159 let (idx, p, wsum) = moe_route(lg, m, None);
11161 let mut st = m.stats.borrow_mut();
11162 if st.len() < m.experts.len() {
11163 st.resize(m.experts.len(), 0);
11164 }
11165 for &e in &idx {
11166 st[e] += 1;
11167 }
11168 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11169 (idx, w)
11170 });
11171 (
11172 &e0.gate_proj,
11173 &e0.up_proj,
11174 &e0.down_proj,
11175 Act::Silu,
11176 Some(crate::gpu_metal::ChunkMoe {
11177 router,
11178 experts,
11179 q8,
11180 route,
11181 }),
11182 )
11183 }
11184 None => break,
11185 },
11186 _ => break,
11187 };
11188 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11194 t.q8_row_parts()
11195 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11196 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11197 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11198 }
11199 fn cf(t: &QTensor) -> &[f32] {
11200 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11201 }
11202 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11203 if moe.is_none()
11212 && !gemma4
11213 && (col.iter().any(|c| !c.is_empty())
11214 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
11215 {
11216 break;
11217 }
11218 let parts = (
11219 cw(wq),
11220 cw(wk),
11221 cw(wv),
11222 cw(wo),
11223 cw(gate_t),
11224 cw(up_t),
11225 cw(down_t),
11226 );
11227 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11228 else {
11229 break;
11230 };
11231 let layer = &self.kv_cache.layers[li];
11232 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11233 break;
11234 }
11235 debug_assert!(
11237 layer.base() == 0
11238 || self
11239 .layer_window(li)
11240 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11241 );
11242 stored_at.push(layer.head_len(0));
11243 let (nkv, hd) = if gemma4 {
11246 let (a, bb, _) = self.layer_geom(li);
11247 (a, bb)
11248 } else {
11249 (nkv, hd)
11250 };
11251 layers.push(crate::gpu_metal::ChunkLayer {
11252 model: &model,
11253 kv_id: self.graph_kv_id,
11254 layer: li,
11255 wq: pq,
11256 wk: pk,
11257 wv: pv,
11258 wo: po,
11259 gate: pg,
11260 up: pu,
11261 down: pd,
11262 input_norm: &lw.input_norm,
11263 post_norm: &lw.post_norm,
11264 bias: bias
11265 .as_ref()
11266 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11267 q_norm: q_norm.as_deref(),
11268 k_norm: k_norm.as_deref(),
11269 inv_freq: &tables[li - li0],
11270 rope_scale: self.layer_rope_scale(li),
11271 rd: self.layer_geom(li).2,
11272 nh,
11273 nkv,
11274 hd,
11275 hs,
11276 inter: gate_t.rows(),
11277 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11278 late_qk_norm: self.qk_norm_after_rope,
11279 eps: self.rms_eps as f32,
11280 window: self.layer_window(li),
11281 head_gate,
11282 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11283 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11284 v_norm: self.attn_v_norm,
11285 scale: self.attn_scale,
11286 attn_out_norm: lw.attn_out_norm.as_deref(),
11287 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11288 layer_scale: lw.layer_scale,
11289 }),
11290 col,
11291 moe,
11292 });
11293 }
11294 if layers.is_empty() {
11295 return li0;
11296 }
11297 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11299 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11300 .iter()
11301 .zip(&rows_kv)
11302 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11303 .collect();
11304 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11305 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11306 let li = layers[i].layer;
11307 let layer = &self.kv_cache.layers[li];
11308 let nkv = rows_kv[i].0;
11309 io.push(crate::gpu_metal::ChunkIo {
11310 cpu_stored: stored_at[i],
11311 cpu_gen: layer.generation(),
11312 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11313 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11314 out_k: ok,
11315 out_v: ov,
11316 imp: oi,
11317 });
11318 }
11319 let n_run = layers.len();
11320 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11321 let ep = embed_ids.and_then(|ids| {
11324 self.weights
11325 .embed_tokens
11326 .q8_row_parts()
11327 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11328 idx,
11329 rows,
11330 row_scale: rs,
11331 ids,
11332 mult: self.embed_multiplier,
11333 })
11334 });
11335 if embed_ids.is_some() && ep.is_none() {
11336 return li0;
11337 }
11338 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11339 return li0;
11340 }
11341 drop(io);
11342 drop(layers);
11343 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11346 let li = li0 + i;
11347 let row = rows_kv[i].1;
11348 let layer = &mut self.kv_cache.layers[li];
11349 for bi in 0..b {
11350 layer.append(
11351 &ok[bi * row..(bi + 1) * row],
11352 &ov[bi * row..(bi + 1) * row],
11353 &[],
11354 );
11355 }
11356 layer.accumulate_imp(oi);
11357 }
11358 last
11359 }
11360
11361 fn layer_is_local(&self, li: usize) -> bool {
11364 if let Some(layers) = &self.sliding_layers {
11365 return layers.get(li).copied().unwrap_or(false);
11366 }
11367 match self.swa {
11368 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11369 None => false,
11370 }
11371 }
11372
11373 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11376 if self.layer_is_local(li) {
11377 if let Some(f) = &self.inv_freq_local {
11378 return f.clone();
11379 }
11380 } else if let Some(f) = &self.inv_freq_global {
11381 return f.clone();
11382 }
11383 self.inv_freq.clone()
11384 }
11385
11386 fn layer_window(&self, li: usize) -> Option<usize> {
11388 self.swa
11389 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11390 }
11391
11392 fn swa_trim_tails(&mut self) {
11401 let Some((slack, align)) = self.swa_trim else {
11402 return;
11403 };
11404 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11405 return;
11406 }
11407 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11408 if let Some(w) = self.layer_window(li) {
11409 self.kv_cache.layers[li].trim_window(w, slack, align);
11410 }
11411 }
11412 }
11413
11414 fn layer_num_heads(&self, li: usize) -> usize {
11415 self.attention_heads_per_layer
11416 .as_ref()
11417 .and_then(|v| v.get(li).copied())
11418 .unwrap_or(self.num_heads)
11419 }
11420
11421 fn layer_rope_scale(&self, li: usize) -> f32 {
11422 if self.layer_is_local(li) {
11423 self.rope_scale_local
11424 } else {
11425 self.rope_scale
11426 }
11427 }
11428
11429 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11432 if !self.layer_is_local(li) {
11433 if let Some((ghd, gkv)) = self.global_attn {
11434 return (gkv, ghd, ghd);
11435 }
11436 }
11437 let rd = if self.layer_is_local(li) {
11438 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11439 } else {
11440 self.rotary_dim
11441 };
11442 let rd = if self
11444 .no_rope_layers
11445 .as_ref()
11446 .and_then(|v| v.get(li))
11447 .copied()
11448 == Some(true)
11449 {
11450 0
11451 } else {
11452 rd
11453 };
11454 (self.layer_num_kv_heads(li), self.head_dim, rd)
11455 }
11456
11457 fn layer_num_kv_heads(&self, li: usize) -> usize {
11460 self.kv_heads_per_layer
11461 .as_ref()
11462 .and_then(|v| v.get(self.phys_layer(li)).copied())
11463 .unwrap_or(self.num_kv_heads)
11464 }
11465
11466 fn layer_v_dim(&self, li: usize) -> usize {
11468 let (_, hd, _) = self.layer_geom(li);
11469 self.v_head_dim.unwrap_or(hd).min(hd)
11470 }
11471
11472 pub fn set_attn_geometry(
11481 &mut self,
11482 kv_heads_per_layer: Option<Vec<usize>>,
11483 v_head_dim: Option<usize>,
11484 ) -> Result<(), String> {
11485 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11486 if self.global_attn.is_some() {
11487 return Err(
11488 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11489 attention geometry"
11490 .into(),
11491 );
11492 }
11493 if self
11494 .weights
11495 .layers
11496 .iter()
11497 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11498 {
11499 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11500 }
11501 }
11502 if let Some(vd) = v_head_dim {
11503 if vd == 0 || vd > self.head_dim {
11504 return Err(format!(
11505 "v_head_dim {vd} must be in 1..={} (head_dim)",
11506 self.head_dim
11507 ));
11508 }
11509 }
11510 if let Some(v) = &kv_heads_per_layer {
11511 if v.len() != self.physical_layers {
11512 return Err(format!(
11513 "kv_heads_per_layer has {} entries, expected {} layers",
11514 v.len(),
11515 self.physical_layers
11516 ));
11517 }
11518 for (li, &nkv) in v.iter().enumerate() {
11519 let is_attn = matches!(
11520 self.weights.layers.get(li).map(|lw| &lw.attn),
11521 Some(AttnKind::Full { .. }) | None
11522 );
11523 if !is_attn {
11524 continue;
11525 }
11526 let nh = self
11527 .attention_heads_per_layer
11528 .as_ref()
11529 .and_then(|h| h.get(li).copied())
11530 .unwrap_or(self.num_heads);
11531 if nkv == 0 || nh % nkv != 0 {
11532 return Err(format!(
11533 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11534 ));
11535 }
11536 }
11537 }
11538 self.kv_heads_per_layer = kv_heads_per_layer;
11539 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11540 if self.kv_heads_per_layer.is_some() {
11541 for li in 0..self.kv_cache.layers.len() {
11542 let full = matches!(
11543 self.weights
11544 .layers
11545 .get(self.phys_layer(li))
11546 .map(|lw| &lw.attn),
11547 Some(AttnKind::Full { .. })
11548 );
11549 let nkv = self.layer_num_kv_heads(li);
11550 let cache = &self.kv_cache.layers[li];
11551 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11552 let sinks = cache.sinks.clone();
11553 self.kv_cache.layers[li] =
11554 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11555 self.kv_cache.layers[li].sinks = sinks;
11556 }
11557 }
11558 }
11559 Ok(())
11560 }
11561
11562 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11566 let Some(lw) = self.weights.layers.get(phys) else {
11567 return Err(format!("sinks for layer {phys}: no such layer"));
11568 };
11569 if !matches!(lw.attn, AttnKind::Full { .. }) {
11570 return Err(format!(
11571 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11572 ));
11573 }
11574 let nh = self
11575 .attention_heads_per_layer
11576 .as_ref()
11577 .and_then(|h| h.get(phys).copied())
11578 .unwrap_or(self.num_heads);
11579 if sinks.len() != nh {
11580 return Err(format!(
11581 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11582 sinks.len()
11583 ));
11584 }
11585 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11586 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11587 }
11588 for li in 0..self.kv_cache.layers.len() {
11589 if self.phys_layer(li) == phys {
11590 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11591 }
11592 }
11593 Ok(())
11594 }
11595
11596 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11606 if self.kv_heads_per_layer.is_some() {
11607 return Some("per-layer KV head counts");
11608 }
11609 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11610 return Some("V heads narrower than Q/K heads");
11611 }
11612 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11613 return Some("learned attention sinks");
11614 }
11615 if self.swa.is_some() || self.sliding_layers.is_some() {
11616 return Some("sliding-window layers");
11617 }
11618 None
11619 }
11620
11621 #[cfg(target_os = "macos")]
11629 fn metal_rope_scale_ok(&self) -> bool {
11630 !(self.qk_norm_after_rope
11631 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11632 }
11633
11634 #[cfg(target_os = "macos")]
11651 fn metal_graph_swa(&self) -> bool {
11652 (self.swa.is_some() || self.sliding_layers.is_some())
11653 && self.attention_heads_per_layer.is_none()
11654 && !self.attn_v_norm
11655 && self.attn_softcap == 0.0
11656 && self.kv_heads_per_layer.is_none()
11657 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11658 && self.global_attn.is_none()
11659 && self.inv_freq_global.is_none()
11660 && self.metal_rope_scale_ok()
11661 && !(0..self.num_layers).any(|li| {
11662 self.layer_is_local(li)
11663 && self.inv_freq_local.is_none()
11664 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11665 })
11666 }
11667
11668 #[cfg(target_os = "macos")]
11676 fn metal_gemma4(&self) -> bool {
11677 crate::gpu_metal::gemma4::enabled()
11678 && (self.global_attn.is_some() || self.attn_v_norm)
11679 && self.attention_heads_per_layer.is_none()
11680 && self.kv_heads_per_layer.is_none()
11681 && self.v_head_dim.is_none()
11682 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11683 && self.attn_softcap == 0.0
11684 && !self.qk_norm_after_rope
11685 && self.metal_rope_scale_ok()
11686 && self
11687 .weights
11688 .layers
11689 .iter()
11690 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11691 && !(0..self.num_layers).any(|li| {
11692 self.layer_is_local(li)
11693 && self.inv_freq_local.is_none()
11694 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11695 })
11696 }
11697
11698 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11705 let geometry_reason = self.graph_attn_decline_reason();
11709 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11710 if has_scaled_rope && geometry_reason.is_none() {
11711 return Some("scaled RoPE without per-layer geometry");
11712 }
11713 if has_scaled_rope && self.qk_norm_after_rope {
11718 return Some("scaled RoPE with post-RoPE Q/K norm");
11719 }
11720 geometry_reason?;
11721 if self.attention_heads_per_layer.is_some() {
11727 return Some("per-layer Q head counts with per-layer geometry");
11728 }
11729 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11733 if !gemma4
11734 && self.weights.layers.iter().any(|lw| {
11735 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11736 })
11737 {
11738 return Some("sandwich norms / layer scale with per-layer geometry");
11739 }
11740 if self.weights.layers.iter().any(|lw| {
11744 matches!(
11745 &lw.attn,
11746 AttnKind::Full {
11747 output_gate: true,
11748 ..
11749 }
11750 )
11751 }) && self.v_head_dim.is_some()
11752 {
11753 return Some("gated attention with V narrower than K");
11754 }
11755 if (0..self.num_layers).any(|li| {
11756 self.layer_is_local(li)
11757 && self.inv_freq_local.is_none()
11758 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11759 }) {
11760 return Some("local rotary width without a local RoPE table");
11761 }
11762 None
11763 }
11764
11765 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11769 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11772 return None;
11773 }
11774 if self.global_attn.is_some() {
11775 return Some("per-layer head width (Gemma-4 global layers)");
11776 }
11777 if self.attn_v_norm {
11778 return Some("V norm");
11779 }
11780 if self.weights.layers.iter().any(|lw| {
11781 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11782 }) {
11783 return Some("sandwich norms / layer scale");
11784 }
11785 None
11786 }
11787
11788 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11793 self.graph_attn_decline_reason()?;
11794 let (nkv, hd, rd) = self.layer_geom(li);
11795 let invf: &[f32] = if self.layer_is_local(li) {
11796 match &self.inv_freq_local {
11797 Some(f) => f.as_slice(),
11798 None => self.inv_freq.as_slice(),
11799 }
11800 } else {
11801 match &self.inv_freq_global {
11802 Some(f) => f.as_slice(),
11803 None => self.inv_freq.as_slice(),
11804 }
11805 };
11806 Some(crate::gpu::GraphAttnGeom {
11807 nkv,
11808 hd,
11809 dv: self.layer_v_dim(li),
11810 rd,
11811 invf,
11812 rope_scale: self.layer_rope_scale(li),
11813 window: self.layer_window(li),
11814 sink: self.kv_cache.layers[li].sinks.as_deref(),
11815 v_norm: self.attn_v_norm,
11816 })
11817 }
11818
11819 #[cfg(feature = "gpu")]
11831 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
11832 if !self.proj_gate_sigmoid
11833 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
11834 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
11835 || self.graph_refused()
11836 || self.o1_active()
11837 || self.attn_softcap > 0.0
11838 || self.wgpu_graph_attn_decline().is_some()
11839 {
11840 return None;
11841 }
11842 let g = self.graph_attn_geom(li)?;
11843 let (nkv, hd, _) = self.layer_geom(li);
11844 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
11846 return None;
11847 }
11848 Some(crate::gpu::PrefillMirror {
11849 kv_id: self.graph_kv_id,
11850 layer: li,
11851 limit: self.kv_cache.max_seq_len,
11852 })
11853 }
11854
11855 #[cfg(feature = "gpu")]
11864 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
11865 let kv_id = self.graph_kv_id;
11866 for li in from..upto.min(self.num_layers) {
11867 if !matches!(
11868 self.weights.layers[self.phys_layer(li)].attn,
11869 AttnKind::Full { .. }
11870 ) {
11871 continue;
11872 }
11873 let host = self.kv_cache.layers[li].pos_len();
11875 if host >= position {
11876 continue;
11877 }
11878 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
11879 continue;
11880 };
11881 let to = dev.min(position);
11882 if to <= host {
11883 continue;
11884 }
11885 let (nkv, hd) = {
11886 let c = &self.kv_cache.layers[li];
11887 (c.num_kv_heads, c.head_dim)
11888 };
11889 let Some((k, v, first_valid)) =
11890 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
11891 else {
11892 continue;
11893 };
11894 let need_from = match self.layer_window(li) {
11897 Some(w) => host.max((position + 1).saturating_sub(w)),
11898 None => host,
11899 };
11900 if first_valid > need_from {
11901 tracing::warn!(
11902 "layer {li}: device KV rows {host}..{to} no longer resident \
11903 (from {first_valid}); host attention will miss them"
11904 );
11905 }
11906 let row = nkv * hd;
11907 let cache = &mut self.kv_cache.layers[li];
11908 for p in 0..to - host {
11909 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
11910 }
11911 }
11912 }
11913
11914 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
11917 let mut seen = self.graph_declines.borrow_mut();
11918 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
11919 tracing::warn!("{site} declined: {reason} (CPU attention path)");
11920 seen.push((site, reason));
11921 }
11922 }
11923
11924 pub fn graph_declines(&self) -> Vec<String> {
11927 self.graph_declines
11928 .borrow()
11929 .iter()
11930 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
11931 .collect()
11932 }
11933
11934 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
11943 let Some(dir) = &self.layer_dump else {
11944 return;
11945 };
11946 let mut bytes = Vec::with_capacity(row.len() * 4);
11947 for v in row {
11948 bytes.extend_from_slice(&v.to_le_bytes());
11949 }
11950 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
11951 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
11952 use std::sync::atomic::{AtomicBool, Ordering};
11953 static SAID: AtomicBool = AtomicBool::new(false);
11954 if !SAID.swap(true, Ordering::Relaxed) {
11955 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
11956 }
11957 }
11958 }
11959
11960 fn mimo_moe_prepare(&mut self) {
11963 if !self.mimo_moe.is_undecided() {
11964 return;
11965 }
11966 let slot = {
11967 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
11968 .filter_map(
11969 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
11970 FfnKind::Moe(m) => Some((li, m)),
11971 _ => None,
11972 },
11973 )
11974 .collect();
11975 if layers.is_empty()
11978 || self.physical_layers != self.num_layers
11979 || self.gpu_plan.is_some()
11980 {
11981 crate::mimo_moe::Slot::Off
11982 } else {
11983 let graph_prefix =
11987 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
11988 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
11989 }
11990 };
11991 self.mimo_moe = slot;
11992 }
11993
11994 #[cfg(test)]
11995 pub(crate) fn test_graph_kv_id(&self) -> u64 {
11996 self.graph_kv_id
11997 }
11998
11999 pub(crate) fn mimo_graph_layer_rows(
12003 &mut self,
12004 li: usize,
12005 h: &mut [f32],
12006 positions: &[usize],
12007 ) -> crate::gpu::BatchGraphOutcome {
12008 use crate::gpu::BatchGraphOutcome as Out;
12009 let b = positions.len();
12010 if !(1..=4).contains(&b)
12011 || h.len() != b * self.hidden_size
12012 || !self.mimo_moe.is_dynamic(li, true)
12013 || !crate::gpu::enabled_here()
12014 || !crate::gpu::wgpu_active()
12015 || self.o1_active()
12016 || self.physical_layers != self.num_layers
12017 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12021 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12022 || self.wgpu_graph_attn_decline().is_some()
12023 {
12024 return Out::Declined;
12025 }
12026 let attn_started = std::time::Instant::now();
12027 let outcome = {
12028 let lw = &self.weights.layers[li];
12029 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12030 return Out::Declined;
12031 }
12032 let FfnKind::Moe(m) = &lw.ffn else {
12033 return Out::Declined;
12034 };
12035 let AttnKind::Full {
12036 wq,
12037 wk,
12038 wv,
12039 wo,
12040 q_norm,
12041 k_norm,
12042 output_gate,
12043 softplus_gate,
12044 bias,
12045 } = &lw.attn
12046 else {
12047 return Out::Declined;
12048 };
12049 if *output_gate || softplus_gate.is_some() {
12050 return Out::Declined;
12051 }
12052 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12053 m.experts
12054 .first()?
12055 .gate_proj
12056 .mapped_q4tp()
12057 .map(|(m, _)| m.clone())
12058 }) else {
12059 return Out::Declined;
12060 };
12061 fn gw<'a>(
12062 t: &'a QTensor,
12063 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12064 ) -> Option<crate::gpu::GraphW<'a>> {
12065 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12066 if m.uid() != owner.uid() || t.has_prism_contract() {
12067 return None;
12068 }
12069 return Some(crate::gpu::GraphW {
12070 idx,
12071 kind,
12072 row_scale: rs,
12073 data: &[],
12074 prism: crate::gpu::GraphPrismOp::None,
12075 affine: false,
12076 });
12077 }
12078 t.as_f32().map(|data| crate::gpu::GraphW {
12079 idx: 0,
12080 kind: 4,
12081 row_scale: &[],
12082 data,
12083 prism: crate::gpu::GraphPrismOp::None,
12084 affine: false,
12085 })
12086 }
12087 let (Some(q), Some(k), Some(v), Some(o)) = (
12088 gw(wq, &model),
12089 gw(wk, &model),
12090 gw(wv, &model),
12091 gw(wo, &model),
12092 ) else {
12093 return Out::Declined;
12094 };
12095 let layer = crate::gpu::GraphLayer {
12096 input_norm: &lw.input_norm,
12097 post_norm: &lw.post_norm,
12098 attn_out_norm: None,
12099 ffn_out_norm: None,
12100 layer_scale: None,
12101 ffn: crate::gpu::GraphFfn::AttentionOnly,
12102 attn: crate::gpu::GraphAttn::Full {
12103 wq: q,
12104 wk: k,
12105 wv: v,
12106 wo: o,
12107 q_norm: q_norm.as_deref(),
12108 k_norm: k_norm.as_deref(),
12109 late_qk_norm: self.qk_norm_after_rope,
12110 bias: bias
12111 .as_ref()
12112 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12113 output_gate: false,
12114 cpu_k: self.kv_cache.layers[li].k_heads(),
12115 cpu_v: self.kv_cache.layers[li].v_heads(),
12116 cpu_base: self.kv_cache.layers[li].base(),
12117 geom: self.graph_attn_geom(li),
12118 head_gate: None,
12119 },
12120 };
12121 let (nkv, hd, rd) = self.layer_geom(li);
12122 crate::gpu::forward_batch_graph_at(
12123 &model,
12124 self.graph_kv_id,
12125 li,
12126 &[layer],
12127 &self.inv_freq,
12128 h,
12129 self.layer_num_heads(li),
12130 nkv,
12131 hd,
12132 rd,
12133 self.hidden_size,
12134 1,
12135 positions,
12136 self.kv_cache.max_seq_len,
12137 self.norm_style == cortiq_core::NormStyle::Gemma,
12138 self.rms_eps as f32,
12139 self.attn_scale,
12140 b,
12141 &[],
12142 self.o1_epoch,
12143 None,
12144 None,
12145 )
12146 };
12147 match outcome {
12148 Out::Completed => {}
12149 Out::Declined => return Out::Declined,
12150 Out::Failed => {
12151 self.graph_failed
12152 .store(true, std::sync::atomic::Ordering::Relaxed);
12153 return Out::Failed;
12154 }
12155 }
12156 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12157 let hs = self.hidden_size;
12158 let lw = &self.weights.layers[li];
12159 let FfnKind::Moe(m) = &lw.ffn else {
12160 unreachable!()
12161 };
12162 let mut post = vec![0.0; h.len()];
12163 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12164 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12165 }
12166 let mut ffn = if b == 1 {
12167 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12168 } else {
12169 moe_ffn_banked_rows(
12170 &mut self.mimo_moe,
12171 li,
12172 m,
12173 &post,
12174 b,
12175 hs,
12176 self.pool.as_deref(),
12177 )
12178 };
12179 for (x, &f) in h.iter_mut().zip(&ffn) {
12180 *x += f;
12181 }
12182 attention::recycle_buf(&mut ffn);
12183 if self.layer_dump.is_some() {
12184 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12185 self.dump_layer_row(pos, li, row);
12186 }
12187 }
12188 crate::mimo_moe::note_attention_graph(b, attn_ns);
12189 Out::Completed
12190 }
12191
12192 fn layer_attn_plain(&self, li: usize) -> bool {
12193 self.kv_heads_per_layer.is_none()
12194 && self.v_head_dim.is_none()
12195 && self.global_attn.is_none()
12196 && self.layer_window(li).is_none()
12197 && self.kv_cache.layers[li].sinks.is_none()
12198 }
12199
12200 fn forward_layers(
12202 &mut self,
12203 hidden: &[f32],
12204 position: usize,
12205 task_mask: Option<&TaskMask>,
12206 ) -> Vec<f32> {
12207 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12208 self.o1_progress();
12209 self.swa_trim_tails();
12210 out
12211 }
12212
12213 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12221 self.embed_single(id)
12222 }
12223
12224 pub fn split_supported(&self) -> Result<(), String> {
12228 if self.dsv4.is_some() {
12229 return Err(
12230 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12231 );
12232 }
12233 if self.dsv41.is_some() {
12234 return Err(
12235 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12236 .into(),
12237 );
12238 }
12239 if self.qwen4_exp.is_some() {
12240 return Err(
12241 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12242 );
12243 }
12244 if self.per_token_stack() {
12245 return Err(
12246 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12247 );
12248 }
12249 Ok(())
12250 }
12251
12252 pub fn forward_span(
12257 &mut self,
12258 hidden: &[f32],
12259 position: usize,
12260 from: usize,
12261 upto: usize,
12262 task_mask: Option<&TaskMask>,
12263 ) -> Result<Vec<f32>, String> {
12264 self.split_supported()?;
12265 if from > upto || upto >= self.num_layers {
12266 return Err(format!(
12267 "forward_span: layer range {from}..={upto} outside 0..{}",
12268 self.num_layers
12269 ));
12270 }
12271 if hidden.len() != self.hidden_size {
12272 return Err(format!(
12273 "forward_span: hidden len {} ≠ hidden_size {}",
12274 hidden.len(),
12275 self.hidden_size
12276 ));
12277 }
12278 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12279 self.o1_progress();
12280 self.swa_trim_tails();
12281 if self
12282 .graph_failed
12283 .swap(false, std::sync::atomic::Ordering::Relaxed)
12284 {
12285 self.cancel
12286 .store(false, std::sync::atomic::Ordering::Relaxed);
12287 self.clear_sequence_state();
12288 return Err("forward_span: deferred O(1) transition failed".into());
12289 }
12290 Ok(out)
12291 }
12292
12293 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12296 let normed = inference::rms_norm(
12297 hidden,
12298 &self.weights.final_norm,
12299 self.rms_eps,
12300 self.norm_style,
12301 );
12302 self.lm_head_forward(&normed)
12303 }
12304
12305 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12307 sampler::sample_with_scratch(
12308 logits,
12309 &self.sampler_config,
12310 past_tokens,
12311 &mut self.rng,
12312 &mut self.sampler_scratch,
12313 )
12314 }
12315
12316 pub fn reset_session(&mut self) {
12318 self.clear_sequence_state();
12319 }
12320
12321 pub fn prefill_span_ids(
12327 &mut self,
12328 ids: &[u32],
12329 start_pos: usize,
12330 upto: usize,
12331 task_mask: Option<&TaskMask>,
12332 ) -> Result<Vec<f32>, String> {
12333 self.split_supported()?;
12334 if upto >= self.num_layers {
12335 return Err(format!(
12336 "prefill_span_ids: upto {upto} outside 0..{}",
12337 self.num_layers
12338 ));
12339 }
12340 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12344 let out =
12345 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12346 self.check_o1_progress_failure("prefill_span_ids")?;
12347 Ok(out)
12348 } else {
12349 let hs = self.hidden_size;
12350 let mut out = Vec::with_capacity(ids.len() * hs);
12351 for (i, &id) in ids.iter().enumerate() {
12352 let emb = self.embed_id(id);
12353 out.extend_from_slice(&self.forward_span(
12354 &emb,
12355 start_pos + i,
12356 0,
12357 upto,
12358 task_mask,
12359 )?);
12360 }
12361 Ok(out)
12362 }
12363 }
12364
12365 pub fn prefill_span_hidden(
12368 &mut self,
12369 hidden: &[f32],
12370 start_pos: usize,
12371 from: usize,
12372 upto: usize,
12373 task_mask: Option<&TaskMask>,
12374 ) -> Result<Vec<f32>, String> {
12375 self.split_supported()?;
12376 let hs = self.hidden_size;
12377 if hidden.is_empty() || hidden.len() % hs != 0 {
12378 return Err(format!(
12379 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12380 hidden.len()
12381 ));
12382 }
12383 if from > upto || upto >= self.num_layers {
12384 return Err(format!(
12385 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12386 self.num_layers
12387 ));
12388 }
12389 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12390 let out = self.prefill_batch_span(
12391 PrefillIn::Hidden(hidden),
12392 start_pos,
12393 task_mask,
12394 from,
12395 upto + 1,
12396 );
12397 self.check_o1_progress_failure("prefill_span_hidden")?;
12398 Ok(out)
12399 } else {
12400 let b = hidden.len() / hs;
12401 let mut out = Vec::with_capacity(hidden.len());
12402 for i in 0..b {
12403 let h = self.forward_span(
12404 &hidden[i * hs..(i + 1) * hs],
12405 start_pos + i,
12406 from,
12407 upto,
12408 task_mask,
12409 )?;
12410 out.extend_from_slice(&h);
12411 }
12412 Ok(out)
12413 }
12414 }
12415
12416 fn try_token_graph_wgpu(
12420 &self,
12421 hidden: &[f32],
12422 position: usize,
12423 logits_out: &mut Vec<f32>,
12424 layers_run: &mut usize,
12425 ) -> Option<Result<Vec<f32>, ()>> {
12426 self.try_token_graph_wgpu_steps(
12427 hidden,
12428 position,
12429 logits_out,
12430 1,
12431 None,
12432 Some(layers_run),
12433 0,
12434 self.num_layers,
12435 )
12436 }
12437
12438 fn try_token_graph_wgpu_span(
12442 &self,
12443 hidden: &[f32],
12444 position: usize,
12445 logits_out: &mut Vec<f32>,
12446 from: usize,
12447 upto_excl: usize,
12448 layers_run: &mut usize,
12449 ) -> Option<Result<Vec<f32>, ()>> {
12450 self.try_token_graph_wgpu_steps(
12451 hidden,
12452 position,
12453 logits_out,
12454 1,
12455 None,
12456 Some(layers_run),
12457 from,
12458 upto_excl,
12459 )
12460 }
12461
12462 #[cfg(feature = "gpu")]
12466 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12467 if !crate::minicpm_tail::minimax_pool_on()
12468 || !crate::gpu::enabled_here()
12469 || self.head_clusters.is_some()
12470 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12471 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12472 {
12473 return None;
12474 }
12475 let (model, i, kind, rs) = self
12476 .weights
12477 .lm_head
12478 .graph_weight()
12479 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12480 let name = &model.tensors[i].name;
12481 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12482 crate::gpu::GraphPrismOp::InverseEmbedding
12483 } else if crate::prism::is_forward_weight(&model, name) {
12484 crate::gpu::GraphPrismOp::Forward
12485 } else {
12486 crate::gpu::GraphPrismOp::None
12487 };
12488 let gw = crate::gpu::GraphW {
12489 idx: i,
12490 kind,
12491 row_scale: rs,
12492 data: &[],
12493 prism,
12494 affine: crate::prism::is_affine_target(&model, name),
12495 };
12496 let rows = self.weights.lm_head.rows();
12497 let mut h = hidden.to_vec();
12498 let mut lg = Vec::new();
12499 let mut layers_run = 0usize;
12500 let outcome = crate::gpu::forward_token_graph(
12501 &model,
12502 self.graph_kv_id,
12503 &[],
12504 &[],
12505 self.o1_epoch,
12506 &self.inv_freq,
12507 &mut h,
12508 self.num_heads,
12509 self.num_kv_heads,
12510 self.head_dim,
12511 self.attn_scale,
12512 self.head_dim,
12513 self.hidden_size,
12514 self.intermediate_size,
12515 0,
12516 self.kv_cache.max_seq_len,
12517 self.norm_style == cortiq_core::NormStyle::Gemma,
12518 self.rms_eps as f32,
12519 Some((&gw, rows)),
12520 &self.weights.final_norm,
12521 &mut lg,
12522 &[],
12523 1,
12524 None,
12525 None,
12526 Some(&mut layers_run),
12527 0,
12528 false,
12529 );
12530 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12531 return None;
12532 }
12533 lg.resize(self.vocab_size, 0.0);
12536 if let Some(m) = self.logit_multiplier {
12537 for l in lg.iter_mut() {
12538 *l *= m;
12539 }
12540 }
12541 if let Some(c) = self.final_softcap {
12542 softcap_in_place(&mut lg, c, self.pool.as_deref());
12543 }
12544 Some(lg)
12545 }
12546
12547 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12550 if winners.is_empty() {
12551 return;
12552 }
12553 if self.m3_n_exp == 0 {
12554 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12555 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12556 }
12557 for &e in winners {
12558 let e = e as usize;
12559 if e < self.m3_n_exp && li < self.num_layers {
12560 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12561 }
12562 }
12563 if li + 1 == self.num_layers {
12564 self.m3_tokens += 1;
12565 if self.m3_tokens % 16 == 0 {
12566 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12567 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12568 let _ = std::fs::write(&path, bytes);
12569 }
12570 }
12571 }
12572 }
12573
12574 fn m3_prewarm(&mut self) {
12579 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12580 return;
12581 };
12582 let Ok(bytes) = std::fs::read(&path) else {
12583 tracing::warn!("m3 prewarm: cannot read profile {path}");
12584 return;
12585 };
12586 let counts: Vec<u32> = bytes
12587 .chunks_exact(4)
12588 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12589 .collect();
12590 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12591 if counts.len() != self.num_layers * n_exp {
12592 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12593 return;
12594 }
12595 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12596 .ok()
12597 .and_then(|v| v.parse().ok())
12598 .unwrap_or(300);
12599 let mut ranked: Vec<(u32, usize, usize)> = counts
12600 .iter()
12601 .enumerate()
12602 .filter(|(_, c)| **c > 0)
12603 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12604 .collect();
12605 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12606 ranked.truncate(limit);
12607 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12608 for (_, li, e) in ranked {
12609 per_layer[li].push(e);
12610 }
12611 let mut admitted = 0usize;
12612 for (li, ids) in per_layer.iter().enumerate() {
12613 if ids.is_empty() {
12614 continue;
12615 }
12616 let lw = &self.weights.layers[self.phys_layer(li)];
12617 let FfnKind::Moe(m) = &lw.ffn else {
12618 continue;
12619 };
12620 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12622 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12623 admitted += ids.len();
12624 }
12625 }
12626 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12627 }
12628
12629 #[cfg(feature = "gpu")]
12640 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12641 if self.o1_active() || self.attn_softcap > 0.0 {
12642 return Ok(false);
12643 }
12644 if !self.m3_prewarmed {
12645 self.m3_prewarmed = true;
12646 self.m3_prewarm();
12647 }
12648 let frame = {
12649 let lw = &self.weights.layers[self.phys_layer(li)];
12650 let FfnKind::Moe(m) = &lw.ffn else {
12651 return Ok(false);
12652 };
12653 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12654 return Ok(false);
12655 }
12656 let t_arm = std::time::Instant::now();
12657 if self.m3_winners.len() != self.num_layers {
12658 self.m3_winners = vec![Vec::new(); self.num_layers];
12659 }
12660 let pred = std::mem::take(&mut self.m3_winners[li]);
12661 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12662 if armed.is_none() {
12663 self.m3_winners[li] = pred; }
12665 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12666 match armed {
12667 Some(frame) => frame,
12668 None => return Ok(false),
12671 }
12672 };
12673 crate::gpu_wgpu::pooled_frame_enter(frame);
12674 let want_logits = self.graph_want_logits;
12678 self.graph_want_logits = false;
12679 let mut lg = Vec::new();
12680 let mut gl = 0usize;
12681 let t_span = std::time::Instant::now();
12682 let span = crate::gpu::gpu_scope(|| {
12683 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12684 });
12685 self.graph_want_logits = want_logits;
12686 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12687 let hh = match span {
12688 Some(Ok(hh)) => hh,
12689 Some(Err(())) => return Err(()),
12690 None => {
12691 return Ok(false);
12694 }
12695 };
12696 if gl != 1 {
12697 return if gl == 0 { Ok(false) } else { Err(()) };
12700 }
12701 *h = hh;
12702 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12703 self.m3_note_route(li, &winners);
12704 if self.m3_winners.len() != self.num_layers {
12705 self.m3_winners = vec![Vec::new(); self.num_layers];
12706 }
12707 self.m3_winners[li] = winners;
12708 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12709 cold.retain(|&(_, w)| {
12712 if w.is_finite() {
12713 true
12714 } else {
12715 tracing::error!("pooled tail: non-finite cold weight from expert route");
12716 false
12717 }
12718 });
12719 if cold.is_empty() {
12720 return Ok(true);
12721 }
12722 let x = crate::gpu_wgpu::pooled_frame_take_input();
12723 if x.len() != self.hidden_size {
12724 return Err(());
12725 }
12726 let lw = &self.weights.layers[self.phys_layer(li)];
12732 let FfnKind::Moe(m) = &lw.ffn else {
12733 return Err(());
12734 };
12735 let t_cold = std::time::Instant::now();
12736 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12737 let mut out = crate::attention::take_buf(self.hidden_size);
12738 let on_device = !force_host
12739 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12740 if !on_device {
12741 crate::attention::recycle_buf(&mut out);
12742 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12743 .iter()
12744 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12745 .collect::<Result<_, ()>>()?;
12746 out = crate::gpu::cpu_scope(|| {
12747 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12748 });
12749 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12750 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12751 }
12752 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12753 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12754 if out.len() != h.len() {
12755 crate::attention::recycle_buf(&mut out);
12756 return Err(());
12757 }
12758 for (dst, src) in h.iter_mut().zip(out.iter()) {
12759 *dst += src;
12760 }
12761 crate::attention::recycle_buf(&mut out);
12762 if on_device {
12763 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12764 }
12765 Ok(true)
12766 }
12767
12768 #[cfg(not(feature = "gpu"))]
12769 fn m3_pooled_layer(
12770 &mut self,
12771 _li: usize,
12772 _position: usize,
12773 _h: &mut Vec<f32>,
12774 ) -> Result<bool, ()> {
12775 Ok(false)
12776 }
12777
12778 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12782 if self.o1_active() || self.attn_softcap > 0.0 {
12783 return None;
12784 }
12785 if let Some(reason) = self.wgpu_graph_attn_decline() {
12788 self.note_graph_decline("wgpu multi-burst", reason);
12789 return None;
12790 }
12791 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12792 if !graph_on || self.graph_refused() {
12793 return None;
12800 }
12801 let emb = self.embed_single(t_next);
12802 let mut lg = Vec::new();
12803 let mut ids = Vec::new();
12804 match self.try_token_graph_wgpu_steps(
12805 &emb,
12806 position,
12807 &mut lg,
12808 k,
12809 Some(&mut ids),
12810 None,
12811 0,
12812 self.num_layers,
12813 ) {
12814 Some(Ok(_)) => {}
12815 Some(Err(())) => {
12816 self.graph_failed
12821 .store(true, std::sync::atomic::Ordering::Relaxed);
12822 return None;
12823 }
12824 None => return None,
12825 }
12826 (ids.len() == k).then_some(ids)
12827 }
12828
12829 fn try_token_graph_wgpu_steps(
12833 &self,
12834 hidden: &[f32],
12835 position: usize,
12836 logits_out: &mut Vec<f32>,
12837 steps: usize,
12838 ids_out: Option<&mut Vec<u32>>,
12839 layers_run: Option<&mut usize>,
12840 from: usize,
12841 upto_excl: usize,
12842 ) -> Option<Result<Vec<f32>, ()>> {
12843 let upto_excl = match self.mimo_moe.graph_prefix_end() {
12846 Some(end) if end < upto_excl => {
12847 if steps != 1 || layers_run.is_none() || from >= end {
12848 return None;
12849 }
12850 end
12851 }
12852 _ => upto_excl,
12853 };
12854 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
12857 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
12858 return None;
12862 }
12863 if let Some(reason) = self.wgpu_graph_attn_decline() {
12870 self.note_graph_decline("wgpu token graph", reason);
12871 return None;
12872 }
12873 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
12878 .map(|li| {
12879 if !o1_gpu {
12880 return None;
12881 }
12882 self.kv_cache.layers[self.phys_layer(li)].o1_views()
12883 })
12884 .collect();
12885 if self.o1_active() && o1_gpu {
12886 let want: usize = (from..upto_excl)
12889 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
12890 .count();
12891 let have = o1_views.iter().filter(|v| v.is_some()).count();
12892 if want == 0 || have != want {
12893 use std::sync::atomic::{AtomicUsize, Ordering};
12903 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
12904 let code = have * 1000 + want;
12905 if LAST.swap(code, Ordering::Relaxed) != code {
12906 tracing::warn!(
12907 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
12908 );
12909 }
12910 return None;
12911 }
12912 }
12913 let nh = self.num_heads;
12914 let (nkv, hd, rd) = self.layer_geom(0);
12915 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12916 let mut layers = Vec::with_capacity(upto_excl - from);
12917 let mut model = None;
12918 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
12919 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
12920 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
12922 let name = &m.tensors[i].name;
12923 let prism = if crate::prism::is_inverse_embedding(m, name) {
12924 crate::gpu::GraphPrismOp::InverseEmbedding
12925 } else if crate::prism::is_forward_weight(m, name) {
12926 crate::gpu::GraphPrismOp::Forward
12927 } else {
12928 crate::gpu::GraphPrismOp::None
12929 };
12930 return Some(crate::gpu::GraphW {
12931 idx: i,
12932 kind,
12933 row_scale: rs,
12934 data: &[],
12935 prism,
12936 affine: crate::prism::is_affine_target(m, name),
12937 });
12938 }
12939 match t.as_f32() {
12941 Some(d) => Some(crate::gpu::GraphW {
12942 idx: 0,
12943 kind: 4,
12944 row_scale: &[],
12945 data: d,
12946 prism: crate::gpu::GraphPrismOp::None,
12947 affine: false,
12948 }),
12949 None => {
12950 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
12951 eprintln!("batch graph: weight has no graph/f32 representation");
12952 }
12953 None
12954 }
12955 }
12956 }
12957 for li in from..upto_excl {
12958 let lw = &self.weights.layers[self.phys_layer(li)];
12959 if dbg {
12960 let ak = match &lw.attn {
12961 AttnKind::Mla(_) => "Mla".into(),
12962 AttnKind::Full {
12963 output_gate, bias, ..
12964 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
12965 AttnKind::LinearGdn(_) => "LinearGdn".into(),
12966 AttnKind::Kda(_) => "Kda".into(),
12967 AttnKind::Linear(_) => "Linear".into(),
12968 AttnKind::ShortConv(_) => "ShortConv".into(),
12969 AttnKind::Bounded(_) => "Bounded".into(),
12970 };
12971 let fk = match &lw.ffn {
12972 FfnKind::Dense(_) => "Dense",
12973 FfnKind::Moe(_) => "Moe",
12974 FfnKind::DenseMoe(_) => "DenseMoe",
12975 };
12976 eprintln!("graph L{li}: attn={ak} ffn={fk}");
12977 }
12978 let gffn = match &lw.ffn {
12979 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
12983 FfnKind::Dense(d) => {
12984 let Some(act) = d.act.wgpu_graph_act() else {
12988 self.note_graph_decline(
12989 "wgpu token graph",
12990 "dense FFN activation without a graph kernel",
12991 );
12992 return None;
12993 };
12994 crate::gpu::GraphFfn::Dense {
12995 gate: gw(&d.gate_proj)?,
12996 up: gw(&d.up_proj)?,
12997 down: gw(&d.down_proj)?,
12998 act,
12999 }
13000 }
13001 FfnKind::Moe(m) => {
13002 if m.route_tau.is_some() || m.mask.is_some() {
13010 return None;
13011 }
13012 let shared = m.shared.as_ref();
13013 let inter = m.experts.first()?.gate_proj.rows();
13019 let detached_shared = match shared {
13027 Some((se, None))
13028 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13029 {
13030 if !matches!(se.act, Act::Silu)
13031 || se.gate_proj.mapped_q4tp().is_none()
13032 || se.up_proj.mapped_q4tp().is_none()
13033 || se.down_proj.mapped_q4tp().is_none()
13034 {
13035 return None;
13036 }
13037 Some(crate::gpu::GraphMoeDetachedShared {
13038 gate: gw(&se.gate_proj)?,
13039 up: gw(&se.up_proj)?,
13040 down: gw(&se.down_proj)?,
13041 width: se.gate_proj.rows(),
13042 })
13043 }
13044 Some((se, Some(_)))
13048 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13049 {
13050 return None;
13051 }
13052 _ => None,
13053 };
13054 let has_shared = shared.is_some() && detached_shared.is_none();
13055 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13056 let sgate = match shared {
13057 Some((_, Some(sg))) if has_shared => gw(sg)?,
13058 _ => gw(&m.router)?,
13062 };
13063 let router = gw(&m.router)?;
13064 if router.prism != crate::gpu::GraphPrismOp::None
13070 || sgate.prism != crate::gpu::GraphPrismOp::None
13071 || router.affine
13072 || sgate.affine
13073 {
13074 tracing::warn!(
13075 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13076 );
13077 return None;
13078 }
13079 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13080 let mut q4tp: Option<bool> = None;
13083 let mut gu_q2: Option<bool> = None;
13086 let mut q82: Option<bool> = None;
13088 let mut moe_act = None;
13092 for e in m
13093 .experts
13094 .iter()
13095 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13096 {
13097 let Some(e_act) = e.act.wgpu_graph_act() else {
13098 return None;
13099 };
13100 match moe_act {
13101 Some(existing) if existing != e_act => return None,
13102 None => moe_act = Some(e_act),
13103 _ => {}
13104 }
13105 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13106 return None;
13107 }
13108 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13113 let Some((em, ei, _, _)) = expert_weight
13114 .graph_weight()
13115 .or_else(|| expert_weight.graph_weight_descriptor())
13116 else {
13117 return None;
13118 };
13119 let name = &em.tensors[ei].name;
13120 if crate::prism::is_forward_weight(em, name)
13121 || crate::prism::is_inverse_embedding(em, name)
13122 || crate::prism::is_affine_target(em, name)
13123 {
13124 tracing::warn!(
13125 "resident MoE declined: expert Prism/affine transform is not implemented"
13126 );
13127 return None;
13128 }
13129 }
13130 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13131 Some((mm, gi)) => (
13132 mm,
13133 gi,
13134 e.up_proj.mapped_q4t()?.1,
13135 e.down_proj.mapped_q4t()?.1,
13136 false,
13137 false,
13138 false,
13139 ),
13140 None => match e.gate_proj.mapped_q2tp() {
13141 Some((mm, gi)) => (
13142 mm,
13143 gi,
13144 e.up_proj.mapped_q2tp()?.1,
13145 e.down_proj.mapped_q4tp()?.1,
13146 true,
13147 true,
13148 false,
13149 ),
13150 None => match e.gate_proj.mapped_q4tp() {
13151 Some((mm, gi)) => (
13152 mm,
13153 gi,
13154 e.up_proj.mapped_q4tp()?.1,
13155 e.down_proj.mapped_q4tp()?.1,
13156 true,
13157 false,
13158 false,
13159 ),
13160 None => {
13163 if !crate::gpu::moe_q82_graph_on() {
13164 return None;
13165 }
13166 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13167 (
13168 mm,
13169 gi,
13170 e.up_proj.mapped_q8_2f()?.1,
13171 e.down_proj.mapped_q8_2f()?.1,
13172 false,
13173 false,
13174 true,
13175 )
13176 }
13177 },
13178 },
13179 };
13180 if *q4tp.get_or_insert(is_p) != is_p
13181 || *gu_q2.get_or_insert(is_q2) != is_q2
13182 || *q82.get_or_insert(is_8) != is_8
13183 {
13184 tracing::warn!(
13190 "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."
13191 );
13192 return None;
13193 }
13194 model.get_or_insert_with(|| mm.clone());
13195 experts.push((gi, ui, di));
13196 }
13197 crate::gpu::GraphFfn::Moe {
13198 router,
13199 shared_gate: sgate,
13200 experts,
13201 n_exp: m.experts.len(),
13202 top_k: std::env::var("CMF_TOPK_PROBE")
13208 .ok()
13209 .and_then(|v| v.parse::<usize>().ok())
13210 .filter(|k| *k > 0 && *k <= m.top_k)
13211 .unwrap_or(m.top_k),
13212 inter,
13213 act: moe_act?,
13214 norm_topk: m.norm_topk_prob,
13215 q4tp: q4tp?,
13216 gu_q2: gu_q2.unwrap_or(false),
13217 q82: q82.unwrap_or(false),
13218 sigmoid: m.router_sigmoid,
13219 sigmoid_denom_eps: m.sigmoid_denom_eps,
13220 bias: m.expert_bias.as_deref(),
13221 has_shared,
13222 detached_shared,
13223 shared_gated,
13224 route_scale: m.routed_scaling,
13225 }
13226 }
13227 };
13228 let attn = match &lw.attn {
13229 AttnKind::Full {
13230 wq,
13231 wk,
13232 wv,
13233 wo,
13234 q_norm,
13235 k_norm,
13236 output_gate,
13237 softplus_gate,
13238 bias,
13239 } => {
13240 if self.attention_heads_per_layer.is_some() {
13241 return None;
13242 }
13243 let head_gate = match softplus_gate {
13247 None => None,
13248 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13249 Some(_) => {
13250 self.note_graph_decline(
13251 "wgpu token graph",
13252 "projected softplus / per-element output gate",
13253 );
13254 return None;
13255 }
13256 };
13257 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13258 model = Some(m.clone());
13259 crate::gpu::GraphAttn::Full {
13260 wq: gw(wq)?,
13261 wk: gw(wk)?,
13262 wv: gw(wv)?,
13263 wo: gw(wo)?,
13264 q_norm: q_norm.as_deref(),
13265 k_norm: k_norm.as_deref(),
13266 late_qk_norm: self.qk_norm_after_rope,
13267 bias: bias
13268 .as_ref()
13269 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13270 output_gate: *output_gate,
13271 cpu_k: self.kv_cache.layers[li].k_heads(),
13272 cpu_v: self.kv_cache.layers[li].v_heads(),
13273 cpu_base: self.kv_cache.layers[li].base(),
13274 geom: self.graph_attn_geom(li),
13275 head_gate,
13276 }
13277 }
13278 AttnKind::LinearGdn(w) => {
13279 let cfg = self.gdn_cfg?;
13280 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13281 model = Some(m.clone());
13282 crate::gpu::GraphAttn::Gdn {
13283 qkv: gw(&w.in_proj_qkv)?,
13284 z: gw(&w.in_proj_z)?,
13285 a: gw(&w.in_proj_a)?,
13286 b: gw(&w.in_proj_b)?,
13287 out: gw(&w.out_proj)?,
13288 conv1d: &w.conv1d,
13289 a_log: &w.a_log,
13290 dt_bias: &w.dt_bias,
13291 norm: &w.norm,
13292 nv: cfg.num_v_heads,
13293 nk: cfg.num_k_heads,
13294 dk: cfg.key_head_dim,
13295 dv: cfg.value_head_dim,
13296 kk: cfg.conv_kernel,
13297 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13298 }
13299 }
13300 AttnKind::ShortConv(w) => {
13301 let cfg = self.short_conv_cfg?;
13302 let (m, _, _, _) = w
13303 .in_proj
13304 .graph_weight()
13305 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13306 model = Some(m.clone());
13307 crate::gpu::GraphAttn::ShortConv {
13308 inp: gw(&w.in_proj)?,
13309 out: gw(&w.out_proj)?,
13310 taps: &w.conv,
13311 kernel: cfg.kernel,
13312 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13313 }
13314 }
13315 _ => return None,
13316 };
13317 layers.push(crate::gpu::GraphLayer {
13318 input_norm: &lw.input_norm,
13319 attn,
13320 post_norm: &lw.post_norm,
13321 attn_out_norm: lw.attn_out_norm.as_deref(),
13322 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13323 layer_scale: lw.layer_scale,
13324 ffn: gffn,
13325 });
13326 }
13327 let model = model?;
13328 let lm_gw = if upto_excl == self.num_layers
13334 && self.graph_want_logits
13335 && std::env::var("CMF_GPU_LMHEAD")
13336 .map(|v| v != "0")
13337 .unwrap_or(true)
13338 {
13339 self.weights
13340 .lm_head
13341 .graph_weight()
13342 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13343 .map(|(m, i, kind, rs)| {
13344 let name = &m.tensors[i].name;
13345 let prism = if crate::prism::is_inverse_embedding(m, name) {
13346 crate::gpu::GraphPrismOp::InverseEmbedding
13347 } else if crate::prism::is_forward_weight(m, name) {
13348 crate::gpu::GraphPrismOp::Forward
13349 } else {
13350 crate::gpu::GraphPrismOp::None
13351 };
13352 (
13353 crate::gpu::GraphW {
13354 idx: i,
13355 kind,
13356 row_scale: rs,
13357 data: &[],
13358 prism,
13359 affine: crate::prism::is_affine_target(m, name),
13360 },
13361 self.weights.lm_head.rows(),
13362 )
13363 })
13364 } else {
13365 None
13366 };
13367 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13368 let emb_gw = if steps > 1 {
13370 self.weights
13371 .embed_tokens
13372 .graph_weight()
13373 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13374 .map(|(m, i, kind, rs)| {
13375 let name = &m.tensors[i].name;
13376 let prism = if crate::prism::is_inverse_embedding(m, name) {
13377 crate::gpu::GraphPrismOp::InverseEmbedding
13378 } else if crate::prism::is_forward_weight(m, name) {
13379 crate::gpu::GraphPrismOp::Forward
13380 } else {
13381 crate::gpu::GraphPrismOp::None
13382 };
13383 (
13384 crate::gpu::GraphW {
13385 idx: i,
13386 kind,
13387 row_scale: rs,
13388 data: &[],
13389 prism,
13390 affine: crate::prism::is_affine_target(m, name),
13391 },
13392 self.weights.embed_tokens.rows(),
13393 self.embed_multiplier,
13394 )
13395 })
13396 } else {
13397 None
13398 };
13399
13400 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13406 (from..upto_excl.min(self.num_layers - 1))
13407 .filter(|&li| (li + 1) % self.physical_layers == 0)
13408 .map(|li| li - from)
13409 .collect()
13410 } else {
13411 Vec::new()
13412 };
13413 let mut h = hidden.to_vec();
13414 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13420 let outcome = crate::gpu::forward_token_graph(
13421 &model,
13422 self.graph_kv_id,
13423 &layers,
13424 &o1_views,
13425 self.o1_epoch,
13426 &self.inv_freq,
13427 &mut h,
13428 nh,
13429 nkv,
13430 hd,
13431 self.attn_scale,
13432 rd,
13433 self.hidden_size,
13434 self.intermediate_size,
13435 position,
13436 self.kv_cache.max_seq_len,
13437 gemma,
13438 self.rms_eps as f32,
13439 lm,
13440 &self.weights.final_norm,
13441 logits_out,
13442 &loop_norm_at,
13443 steps,
13444 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13445 ids_out,
13446 layers_run,
13447 from,
13448 dump_hidden,
13449 );
13450 match outcome {
13451 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13452 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13453 crate::gpu::TokenGraphOutcome::Declined => None,
13454 }
13455 }
13456
13457 #[cfg(target_os = "macos")]
13466 #[allow(clippy::type_complexity)]
13467 fn metal_rows_plan(
13468 &self,
13469 ) -> Option<(
13470 Vec<MetalRowsItem<'_>>,
13471 std::sync::Arc<cortiq_core::CmfModel>,
13472 Option<crate::gpu_metal::GdnGpuCfg>,
13473 )> {
13474 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13475 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13476 if !graph_force
13477 || !crate::gpu::enabled_here()
13478 || std::env::var("CMF_GPU_BLOCK")
13479 .map(|v| v == "0")
13480 .unwrap_or(false)
13481 || self.attn_softcap > 0.0
13482 || self.o1_active()
13483 || self.swa.is_some()
13484 || self.global_attn.is_some()
13485 || self.attention_heads_per_layer.is_some()
13486 || self.graph_attn_decline_reason().is_some()
13488 || self.attn_v_norm
13489 || self.loop_final_norm
13490 || !self.metal_rope_scale_ok()
13491 {
13492 return None;
13493 }
13494 let attend_contract = self.head_dim % 4 == 0
13495 && self.head_dim <= 256
13496 && self.rotary_dim >= 2
13497 && self.rotary_dim <= self.head_dim
13498 && (self.rotary_dim / 2) % 32 == 0
13499 && self.num_kv_heads > 0
13500 && self.num_heads % self.num_kv_heads == 0;
13501 if !attend_contract {
13502 return None;
13503 }
13504 let mut plan: Vec<MetalRowsItem> = Vec::new();
13505 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13506 for li in 0..self.num_layers {
13507 let lw = &self.weights.layers[self.phys_layer(li)];
13508 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13509 return None;
13510 }
13511 let ffn = match &lw.ffn {
13512 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13513 let (Some(g), Some(u), Some(dn)) = (
13514 d.gate_proj.metal_graph_parts(),
13515 d.up_proj.metal_graph_parts(),
13516 d.down_proj.metal_graph_parts(),
13517 ) else {
13518 return None;
13519 };
13520 MetalFfn::Dense {
13521 gate: g,
13522 up: u,
13523 down: dn,
13524 act: 0, }
13526 }
13527 _ => return None,
13528 };
13529 match &lw.attn {
13530 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13531 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13532 w.in_proj_qkv.metal_graph_parts(),
13533 w.in_proj_z.metal_graph_parts(),
13534 w.in_proj_a.f32_parts(),
13535 w.in_proj_b.f32_parts(),
13536 w.out_proj.metal_graph_parts(),
13537 ) else {
13538 return None;
13539 };
13540 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13541 model_ref.get_or_insert_with(|| model.clone());
13542 }
13543 let gl = GdnGpuLayer {
13544 attn_norm: &lw.input_norm,
13545 post_norm: &lw.post_norm,
13546 qkv,
13547 z,
13548 a,
13549 b: bb,
13550 out,
13551 ffn,
13552 conv1d: &w.conv1d,
13553 a_log: &w.a_log,
13554 dt_bias: &w.dt_bias,
13555 gnorm: &w.norm,
13556 };
13557 match plan.last_mut() {
13558 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13559 _ => plan.push(MetalRowsItem::Gdn {
13560 run: vec![gl],
13561 first: li,
13562 }),
13563 }
13564 }
13565 AttnKind::Full {
13566 wq,
13567 wk,
13568 wv,
13569 wo,
13570 q_norm,
13571 k_norm,
13572 output_gate,
13573 softplus_gate: None,
13574 bias: None,
13575 } => {
13576 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13577 wq.metal_graph_parts(),
13578 wk.metal_graph_parts(),
13579 wv.metal_graph_parts(),
13580 wo.metal_graph_parts(),
13581 ) else {
13582 return None;
13583 };
13584 if let QTensor::Mapped { model, .. } = wq {
13585 model_ref.get_or_insert_with(|| model.clone());
13586 }
13587 let cache = &self.kv_cache.layers[li];
13588 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13589 return None;
13590 }
13591 plan.push(MetalRowsItem::Attn {
13592 l: AttnGpuLayer {
13593 attn_norm: &lw.input_norm,
13594 post_norm: &lw.post_norm,
13595 wq: pq,
13596 wk: pk,
13597 wv: pv,
13598 wo: po,
13599 ffn,
13600 attn_out_norm: None,
13601 ffn_out_norm: None,
13602 layer_scale: None,
13603 },
13604 li,
13605 q_norm: q_norm.as_deref(),
13606 k_norm: k_norm.as_deref(),
13607 output_gate: *output_gate,
13608 });
13609 }
13610 _ => return None,
13611 }
13612 }
13613 let model = model_ref?;
13614 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13615 nv: cfg.num_v_heads,
13616 nk: cfg.num_k_heads,
13617 dk: cfg.key_head_dim,
13618 dv: cfg.value_head_dim,
13619 kk: cfg.conv_kernel,
13620 hidden: self.hidden_size,
13621 inter: self.intermediate_size,
13622 c_dim: cfg.conv_dim(),
13623 eps: cfg.rms_eps as f32,
13624 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13625 });
13626 Some((plan, model, gcfg))
13627 }
13628
13629 #[cfg(target_os = "macos")]
13631 #[allow(clippy::too_many_arguments)]
13632 fn metal_attn_params<'a>(
13633 li: usize,
13634 cache: &'a crate::kv_cache::LayerKvCache,
13635 q_norm: Option<&'a [f32]>,
13636 k_norm: Option<&'a [f32]>,
13637 output_gate: bool,
13638 inv_freq: &'a [f32],
13639 rope_scale: f32,
13640 geom: (usize, usize, usize, usize),
13641 pos0: usize,
13642 kv_id: u64,
13643 scale: f32,
13644 eps: f32,
13645 gemma: bool,
13646 late_qk_norm: bool,
13647 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13648 let (nh, nkv, hd, rd) = geom;
13649 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13650 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13651 let cpu_stored = cpu_k[0].len() / hd;
13652 (
13653 crate::gpu_metal::AttnDeviceParams {
13654 kv_id,
13655 layer: li,
13656 nh,
13657 nkv,
13658 hd,
13659 rd,
13660 position: pos0,
13661 scale,
13662 eps,
13663 gemma,
13664 late_qk_norm,
13665 output_gate,
13666 q_norm,
13667 k_norm,
13668 inv_freq,
13669 rope_scale,
13670 cpu_k,
13671 cpu_v,
13672 cpu_stored,
13673 cpu_gen: cache.generation(),
13674 o1: None,
13675 window: None,
13676 softmax_sink: cache.sinks.as_deref(),
13677 head_gate: None,
13678 v_norm: false,
13679 },
13680 cpu_stored,
13681 )
13682 }
13683
13684 #[cfg(target_os = "macos")]
13689 #[allow(clippy::type_complexity)]
13690 fn metal_rows_run(
13691 &mut self,
13692 hiddens: &mut [f32],
13693 pos0: usize,
13694 b: usize,
13695 prefill: bool,
13696 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13697 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13701 ) -> MetalRowsRun {
13702 use crate::gpu_metal::{GraphDims, VerifyGraph};
13703 if !crate::gpu_metal::wait_replay() {
13709 tracing::error!("Metal rows graph: the pending async replay failed");
13710 return MetalRowsRun::Failed;
13711 }
13712 spec_stamp("v.wait");
13713 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13719 if want > 0 {
13720 let phys = self.physical_layers.max(1);
13721 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13722 let is_gdn = self
13723 .weights
13724 .layers
13725 .get(li % phys)
13726 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13727 if is_gdn && l.linear_state.len() != want {
13728 l.linear_state = vec![0f32; want];
13729 }
13730 }
13731 }
13732 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13733 return MetalRowsRun::Declined;
13734 };
13735 spec_stamp("v.plan");
13736 let dims = GraphDims {
13737 hidden: self.hidden_size,
13738 eps: self.rms_eps as f32,
13739 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13740 };
13741 let Some(mut graph) = (if prefill {
13742 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13743 } else {
13744 VerifyGraph::new(&model, dims, hiddens, b)
13745 }) else {
13746 return MetalRowsRun::Declined;
13747 };
13748 let geom = (
13749 self.num_heads,
13750 self.num_kv_heads,
13751 self.head_dim,
13752 self.rotary_dim,
13753 );
13754 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13755 let eps = self.rms_eps as f32;
13756 let kv_id = self.graph_kv_id;
13757 let inv_freq = self.inv_freq.clone();
13758 for item in &plan {
13759 let ok = match item {
13760 MetalRowsItem::Gdn { run, .. } => gcfg
13761 .as_ref()
13762 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13763 .unwrap_or(false),
13764 MetalRowsItem::Attn {
13765 l,
13766 li,
13767 q_norm,
13768 k_norm,
13769 output_gate,
13770 } => {
13771 let (p, _) = Self::metal_attn_params(
13772 *li,
13773 &self.kv_cache.layers[*li],
13774 *q_norm,
13775 *k_norm,
13776 *output_gate,
13777 &inv_freq,
13778 self.layer_rope_scale(*li),
13779 geom,
13780 pos0,
13781 kv_id,
13782 self.attn_scale,
13783 eps,
13784 gemma,
13785 self.qk_norm_after_rope,
13786 );
13787 graph.attn_ok(l, &p)
13788 }
13789 };
13790 if !ok {
13791 use std::sync::atomic::{AtomicBool, Ordering};
13792 static SAID: AtomicBool = AtomicBool::new(false);
13793 if !SAID.swap(true, Ordering::Relaxed) {
13794 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13795 }
13796 return MetalRowsRun::Declined;
13797 }
13798 }
13799 let lm = match &spec {
13800 Some((lm, _, _)) => {
13801 if !graph.lm_head_ok(*lm) {
13802 return MetalRowsRun::Declined;
13803 }
13804 Some(*lm)
13805 }
13806 None => None,
13807 };
13808 let mut gdn_layers = Vec::new();
13809 let mut attn_layers = Vec::new();
13810 for item in &plan {
13811 match item {
13812 MetalRowsItem::Gdn { run, first } => {
13813 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
13814 .iter()
13815 .map(|l| l.linear_state.as_slice())
13816 .collect();
13817 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
13818 return MetalRowsRun::Declined;
13819 }
13820 gdn_layers.extend(*first..*first + run.len());
13821 }
13822 MetalRowsItem::Attn {
13823 l,
13824 li,
13825 q_norm,
13826 k_norm,
13827 output_gate,
13828 } => {
13829 let (p, cpu_stored) = Self::metal_attn_params(
13830 *li,
13831 &self.kv_cache.layers[*li],
13832 *q_norm,
13833 *k_norm,
13834 *output_gate,
13835 &inv_freq,
13836 self.layer_rope_scale(*li),
13837 geom,
13838 pos0,
13839 kv_id,
13840 self.attn_scale,
13841 eps,
13842 gemma,
13843 self.qk_norm_after_rope,
13844 );
13845 if !graph.encode_attn_b(l, &p) {
13846 return MetalRowsRun::Declined;
13847 }
13848 attn_layers.push((*li, cpu_stored));
13849 }
13850 }
13851 }
13852 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
13853 if !graph.encode_lm_head_b(final_norm, lm) {
13854 return MetalRowsRun::Declined;
13855 }
13856 if let Some((n, _)) = argmax_out.as_ref() {
13861 if !graph.encode_argmax_b(*n) {
13862 argmax_out = None;
13863 }
13864 }
13865 }
13866 spec_stamp("v.enc");
13867 if !graph.sync() {
13868 return MetalRowsRun::Failed;
13869 }
13870 spec_stamp("v.gpu");
13871 match (spec, argmax_out) {
13872 (Some(_), Some((_, ids))) => {
13873 ids.resize(b, 0);
13874 if !graph.read_argmax(ids) {
13875 return MetalRowsRun::Failed;
13876 }
13877 spec_stamp("v.am");
13878 }
13879 (Some((lm, _, logits)), None) => {
13880 logits.resize(b * lm.1, 0.0);
13881 if !graph.read_logits(logits) {
13882 return MetalRowsRun::Failed;
13883 }
13884 spec_stamp("v.lg");
13885 }
13886 (None, _) => {}
13887 }
13888 if !graph.read_hidden(hiddens) {
13889 return MetalRowsRun::Failed;
13890 }
13891 spec_stamp("v.hid");
13892 MetalRowsRun::Completed(MetalVerifyPending {
13893 graph,
13894 gdn_layers,
13895 attn_layers,
13896 })
13897 }
13898
13899 #[cfg(target_os = "macos")]
13905 fn try_batch_graph_metal(
13906 &mut self,
13907 hiddens: &mut [f32],
13908 positions: &[usize],
13909 b: usize,
13910 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13911 argmax_out: Option<(usize, &mut Vec<u32>)>,
13912 ) -> crate::gpu::BatchGraphOutcome {
13913 let _t0 = std::time::Instant::now();
13914 if positions.len() != b
13915 || positions.windows(2).any(|w| w[1] != w[0] + 1)
13916 || hiddens.len() != b * self.hidden_size
13917 {
13918 return crate::gpu::BatchGraphOutcome::Declined;
13919 }
13920 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
13921 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
13922 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
13923 MetalRowsRun::Completed(pending) => pending,
13924 };
13925 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13926 eprintln!(
13927 "metal-verify: {:.1} ms | b={b}",
13928 _t0.elapsed().as_secs_f64() * 1e3
13929 );
13930 }
13931 self.metal_verify = Some(pending);
13932 crate::gpu::BatchGraphOutcome::Completed
13933 }
13934
13935 #[cfg(target_os = "macos")]
13940 fn prefill_rows_metal(
13941 &mut self,
13942 ids: &[u32],
13943 start_pos: usize,
13944 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13945 ) -> MetalPrefillOutcome {
13946 let b = ids.len();
13947 if b == 0 || b > 512 {
13948 return MetalPrefillOutcome::Declined;
13949 }
13950 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13951 let with_head = spec.is_some();
13952 let hs = self.hidden_size;
13953 let mut hiddens = vec![0f32; b * hs];
13954 for (j, &id) in ids.iter().enumerate() {
13955 let e = self.embed_single(id);
13956 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
13957 }
13958 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
13959 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
13960 MetalRowsRun::Failed => {
13961 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13962 return MetalPrefillOutcome::Failed;
13963 }
13964 MetalRowsRun::Completed(pending) => pending,
13965 };
13966 let idxs = pending.gdn_layers.clone();
13968 let mut outs: Vec<&mut [f32]> = self
13969 .kv_cache
13970 .layers
13971 .iter_mut()
13972 .enumerate()
13973 .filter(|(i, _)| idxs.binary_search(i).is_ok())
13974 .map(|(_, l)| l.linear_state.as_mut_slice())
13975 .collect();
13976 if !pending.graph.finish_states(&mut outs) {
13977 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13978 return MetalPrefillOutcome::Failed;
13979 }
13980 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
13981 let mut rows = Vec::with_capacity(pending.attn_layers.len());
13985 for (li, cpu_stored) in &pending.attn_layers {
13986 let mut kbuf = vec![0f32; b * nkv * hd];
13987 let mut vbuf = vec![0f32; b * nkv * hd];
13988 if !crate::gpu_metal::kv_mirror_read_rows(
13989 self.graph_kv_id,
13990 *li,
13991 nkv,
13992 hd,
13993 *cpu_stored,
13994 b,
13995 &mut kbuf,
13996 &mut vbuf,
13997 ) {
13998 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13999 return MetalPrefillOutcome::Failed;
14000 }
14001 rows.push((*li, *cpu_stored, kbuf, vbuf));
14002 }
14003 for (li, cpu_stored, kbuf, vbuf) in rows {
14004 let cache = &mut self.kv_cache.layers[li];
14005 for r in 0..b {
14006 cache.append(
14007 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14008 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14009 &[],
14010 );
14011 }
14012 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14013 }
14014 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14015 if with_head {
14016 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14017 }
14018 MetalPrefillOutcome::Completed(hiddens)
14019 }
14020
14021 #[cfg(target_os = "macos")]
14022 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14023 self.prefill_rows_metal(ids, start_pos, None)
14024 }
14025
14026 #[cfg(target_os = "macos")]
14031 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14032 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14033 return MetalBatchNllOutcome::Declined;
14034 }
14035 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14036 return MetalBatchNllOutcome::Declined;
14037 };
14038 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14039 .ok()
14040 .and_then(|v| v.parse::<usize>().ok())
14041 .filter(|&v| (1..=512).contains(&v))
14042 .unwrap_or(32);
14043 let final_norm = self.weights.final_norm.clone();
14044 let mut nll = 0.0f64;
14045 let mut count = 0usize;
14046 let mut pos = 0usize;
14047 let mut completed = 0usize;
14048 while pos < ids.len() {
14049 let end = (pos + chunk).min(ids.len());
14050 let mut logits = Vec::new();
14051 let outcome =
14052 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14053 match outcome {
14054 MetalPrefillOutcome::Declined => {
14055 return if completed == 0 {
14056 MetalBatchNllOutcome::Declined
14057 } else {
14058 MetalBatchNllOutcome::Failed(format!(
14059 "ordinary Metal NLL batch declined after {completed} chunks"
14060 ))
14061 };
14062 }
14063 MetalPrefillOutcome::Failed => {
14064 return MetalBatchNllOutcome::Failed(
14065 "ordinary Metal NLL batch failed after admission".to_string(),
14066 );
14067 }
14068 MetalPrefillOutcome::Completed(_) => {}
14069 }
14070 completed += 1;
14071 let vocab = self.vocab_size.min(lm.1);
14072 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14073 return MetalBatchNllOutcome::Failed(
14074 "ordinary Metal NLL head returned an invalid shape".to_string(),
14075 );
14076 }
14077 for row in 0..(end - pos) {
14078 let absolute = pos + row;
14079 if absolute < start || absolute + 1 >= ids.len() {
14080 continue;
14081 }
14082 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14083 if let Some(mu) = self.logit_multiplier {
14084 for v in lg.iter_mut() {
14085 *v *= mu;
14086 }
14087 }
14088 if let Some(c) = self.final_softcap {
14089 softcap_in_place(lg, c, self.pool.as_deref());
14090 }
14091 let target = ids[absolute + 1] as usize;
14092 if target >= vocab {
14093 return MetalBatchNllOutcome::Failed(format!(
14094 "target token {target} exceeds Metal head rows {vocab}"
14095 ));
14096 }
14097 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14098 let lse: f64 = lg
14099 .iter()
14100 .map(|&v| ((v - max) as f64).exp())
14101 .sum::<f64>()
14102 .ln()
14103 + max as f64;
14104 nll += lse - lg[target] as f64;
14105 count += 1;
14106 }
14107 pos = end;
14108 }
14109 MetalBatchNllOutcome::Completed(nll, count)
14110 }
14111
14112 #[cfg(target_os = "macos")]
14116 fn metal_verify_commit(&mut self, a: usize) -> bool {
14117 let Some(mut pending) = self.metal_verify.take() else {
14118 return false;
14119 };
14120 let n = a + 1;
14121 let idxs = pending.gdn_layers.clone();
14123 let mut outs: Vec<&mut [f32]> = self
14124 .kv_cache
14125 .layers
14126 .iter_mut()
14127 .enumerate()
14128 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14129 .map(|(_, l)| l.linear_state.as_mut_slice())
14130 .collect();
14131 if !pending.graph.commit(n, &mut outs) {
14132 return false;
14133 }
14134 spec_stamp("c.replay");
14135 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14136 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14140 for (li, cpu_stored) in &pending.attn_layers {
14141 let mut kbuf = vec![0f32; n * nkv * hd];
14142 let mut vbuf = vec![0f32; n * nkv * hd];
14143 if !crate::gpu_metal::kv_mirror_read_rows(
14144 self.graph_kv_id,
14145 *li,
14146 nkv,
14147 hd,
14148 *cpu_stored,
14149 n,
14150 &mut kbuf,
14151 &mut vbuf,
14152 ) {
14153 return false;
14154 }
14155 rows.push((*li, *cpu_stored, kbuf, vbuf));
14156 }
14157 for (li, cpu_stored, kbuf, vbuf) in rows {
14158 let cache = &mut self.kv_cache.layers[li];
14159 for r in 0..n {
14160 cache.append(
14161 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14162 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14163 &[],
14164 );
14165 }
14166 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14167 }
14168 spec_stamp("c.kv");
14169 true
14170 }
14171
14172 #[cfg(target_os = "macos")]
14179 fn mtp_warm_batch_submit(
14180 &mut self,
14181 m: &mut MtpModule,
14182 pairs: &[(&[f32], u32)],
14183 first_pos: usize,
14184 ) -> Option<MetalWarmPending> {
14185 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14186 let b = pairs.len();
14187 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14188 return None;
14189 }
14190 let AttnKind::Full {
14191 wq,
14192 wk,
14193 wv,
14194 wo,
14195 q_norm,
14196 k_norm,
14197 output_gate,
14198 softplus_gate: None,
14199 bias: None,
14200 } = &m.layer.attn
14201 else {
14202 return None;
14203 };
14204 let FfnKind::Dense(d) = &m.layer.ffn else {
14205 return None;
14206 };
14207 if !d.segs.is_empty() {
14208 return None;
14209 }
14210 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14211 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14212 else {
14213 return None;
14214 };
14215 let (Some(g), Some(u), Some(dn)) = (
14216 d.gate_proj.q1_parts(),
14217 d.up_proj.q1_parts(),
14218 d.down_proj.q1_parts(),
14219 ) else {
14220 return None;
14221 };
14222 let Some(eh) = m.eh_proj.q1_parts() else {
14223 return None;
14224 };
14225 let QTensor::Mapped { model, .. } = wq else {
14226 return None;
14227 };
14228 let model = model.clone();
14229 let hs = self.hidden_size;
14230 let mut cat = vec![0f32; b * 2 * hs];
14232 for (j, (h, tok)) in pairs.iter().enumerate() {
14233 let e = self.embed_single(*tok);
14234 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14235 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14236 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14237 }
14238 let dims = GraphDims {
14239 hidden: hs,
14240 eps: self.rms_eps as f32,
14241 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14242 };
14243 spec_stamp("w.cat");
14244 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14245 return None;
14246 };
14247 spec_stamp("w.new");
14248 let l = AttnGpuLayer {
14249 attn_norm: &m.layer.input_norm,
14250 post_norm: &m.layer.post_norm,
14251 wq: pq,
14252 wk: pk,
14253 wv: pv,
14254 wo: po,
14255 ffn: MetalFfn::Dense {
14256 gate: g,
14257 up: u,
14258 down: dn,
14259 act: (d.act == Act::Gelu) as u8,
14260 },
14261 attn_out_norm: None,
14262 ffn_out_norm: None,
14263 layer_scale: None,
14264 };
14265 let (nh, nkv, hd, rd) = (
14266 self.num_heads,
14267 self.num_kv_heads,
14268 self.head_dim,
14269 self.rotary_dim,
14270 );
14271 let inv_freq = self.inv_freq.clone();
14272 let cpu_stored;
14273 {
14274 let cache = &m.kv;
14275 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14276 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14277 cpu_stored = cpu_k[0].len() / hd;
14278 if cpu_stored > first_pos {
14283 spec_stamp("w.decl");
14284 return None;
14285 }
14286 let p = AttnDeviceParams {
14287 kv_id: self.mtp_kv_id(),
14288 layer: Self::MTP_LAYER_BASE,
14289 nh,
14290 nkv,
14291 hd,
14292 rd,
14293 position: first_pos,
14294 scale: self.attn_scale,
14295 eps: self.rms_eps as f32,
14296 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14297 late_qk_norm: self.qk_norm_after_rope,
14298 output_gate: *output_gate,
14299 q_norm: q_norm.as_deref(),
14300 k_norm: k_norm.as_deref(),
14301 inv_freq: &inv_freq,
14302 rope_scale: self.rope_scale,
14303 cpu_k,
14304 cpu_v,
14305 cpu_stored,
14306 cpu_gen: cache.generation(),
14307 o1: None,
14308 window: None,
14309 softmax_sink: None,
14310 head_gate: None,
14311 v_norm: false,
14312 };
14313 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14314 return None;
14315 }
14316 }
14317 spec_stamp("w.enc");
14318 if !graph.submit() {
14319 return None;
14320 }
14321 spec_stamp("w.sub");
14322 Some(MetalWarmPending {
14323 graph,
14324 cpu_stored,
14325 b,
14326 })
14327 }
14328
14329 #[cfg(target_os = "macos")]
14332 fn mtp_warm_batch_metal(
14333 &mut self,
14334 m: &mut MtpModule,
14335 pairs: &[(&[f32], u32)],
14336 first_pos: usize,
14337 ) -> bool {
14338 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14339 Some(p) => self.mtp_warm_batch_finish(m, p),
14340 None => false,
14341 }
14342 }
14343
14344 #[cfg(target_os = "macos")]
14349 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14350 let MetalWarmPending {
14351 mut graph,
14352 cpu_stored,
14353 b,
14354 } = pending;
14355 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14356 if !graph.sync() {
14357 return false;
14358 }
14359 spec_stamp("w.gpu");
14360 let mut kbuf = vec![0f32; b * nkv * hd];
14361 let mut vbuf = vec![0f32; b * nkv * hd];
14362 if !crate::gpu_metal::kv_mirror_read_rows(
14363 self.mtp_kv_id(),
14364 Self::MTP_LAYER_BASE,
14365 nkv,
14366 hd,
14367 cpu_stored,
14368 b,
14369 &mut kbuf,
14370 &mut vbuf,
14371 ) {
14372 return false;
14373 }
14374 for r in 0..b {
14375 m.kv.append(
14376 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14377 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14378 &[],
14379 );
14380 }
14381 crate::gpu_metal::kv_mirror_set_stored(
14382 self.mtp_kv_id(),
14383 Self::MTP_LAYER_BASE,
14384 cpu_stored + b,
14385 );
14386 spec_stamp("w.kv");
14387 true
14388 }
14389
14390 pub(crate) fn note_draft_id(&mut self, id: u32) {
14397 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14398 if (id as usize) >= cut {
14399 self.draft_full_streak = 16;
14400 } else {
14401 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14402 }
14403 }
14404
14405 fn draft_head_rows(&self, head_rows: usize) -> usize {
14408 if self.draft_full_streak > 0 {
14409 head_rows
14410 } else {
14411 Self::draft_vocab_rows(head_rows)
14412 }
14413 }
14414
14415 fn draft_vocab_rows(head_rows: usize) -> usize {
14418 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14419 let n = *N.get_or_init(|| {
14420 std::env::var("CMF_DRAFT_VOCAB")
14421 .ok()
14422 .and_then(|v| v.parse().ok())
14423 .unwrap_or(65536)
14424 });
14425 if n == 0 { head_rows } else { n.min(head_rows) }
14426 }
14427
14428 #[cfg(target_os = "macos")]
14433 fn mtp_step_metal(
14434 &mut self,
14435 m: &mut MtpModule,
14436 hidden: &[f32],
14437 next_token: u32,
14438 position: usize,
14439 want_logits: bool,
14440 ) -> Option<(Vec<f32>, Vec<f32>)> {
14441 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14442 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14443 || !crate::gpu::q1_force()
14444 || !crate::gpu::enabled_here()
14445 || self.attn_softcap > 0.0
14446 || self.attention_heads_per_layer.is_some()
14447 || m.kv.mode != crate::kv_cache::KvMode::F32
14448 || m.kv.o1.is_some()
14449 {
14450 return None;
14451 }
14452 let AttnKind::Full {
14453 wq,
14454 wk,
14455 wv,
14456 wo,
14457 q_norm,
14458 k_norm,
14459 output_gate,
14460 softplus_gate: None,
14461 bias: None,
14462 } = &m.layer.attn
14463 else {
14464 return None;
14465 };
14466 let FfnKind::Dense(d) = &m.layer.ffn else {
14467 return None;
14468 };
14469 if d.act != Act::Silu || !d.segs.is_empty() {
14470 return None;
14471 }
14472 let (pq, pk, pv, po) = (
14473 wq.q1_parts()?,
14474 wk.q1_parts()?,
14475 wv.q1_parts()?,
14476 wo.q1_parts()?,
14477 );
14478 let (g, u, dn) = (
14479 d.gate_proj.q1_parts()?,
14480 d.up_proj.q1_parts()?,
14481 d.down_proj.q1_parts()?,
14482 );
14483 let QTensor::Mapped { model, .. } = wq else {
14484 return None;
14485 };
14486 let model = model.clone();
14487 let lm = if want_logits {
14488 Some(self.weights.lm_head.q1_parts()?)
14489 } else {
14490 None
14491 };
14492 let dims = GraphDims {
14493 hidden: self.hidden_size,
14494 eps: self.rms_eps as f32,
14495 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14496 };
14497 let hs = self.hidden_size;
14500 let mut x = vec![0f32; hs];
14501 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14508 crate::gpu_metal::DENSE_MV,
14509 );
14510 let mut graph = TokenGraph::new(&model, dims, &x)?;
14511 let mut folded = false;
14512 if let Some(eh) = m.eh_proj.q1_parts() {
14513 let e = self.embed_single(next_token);
14514 let mut cat = vec![0.0f32; 2 * hs];
14515 let (cat_e, cat_h) = cat.split_at_mut(hs);
14516 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14517 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14518 folded = graph.encode_input_proj(eh, &cat);
14519 }
14520 if !folded {
14521 x = self.mtp_block_input(m, hidden, next_token);
14522 graph = TokenGraph::new(&model, dims, &x)?;
14523 }
14524 spec_stamp("d.in");
14525 let l = AttnGpuLayer {
14526 attn_norm: &m.layer.input_norm,
14527 post_norm: &m.layer.post_norm,
14528 wq: pq,
14529 wk: pk,
14530 wv: pv,
14531 wo: po,
14532 ffn: MetalFfn::Dense {
14533 gate: g,
14534 up: u,
14535 down: dn,
14536 act: (d.act == Act::Gelu) as u8,
14537 },
14538 attn_out_norm: None,
14539 ffn_out_norm: None,
14540 layer_scale: None,
14541 };
14542 let (nh, nkv, hd, rd) = (
14543 self.num_heads,
14544 self.num_kv_heads,
14545 self.head_dim,
14546 self.rotary_dim,
14547 );
14548 let inv_freq = self.inv_freq.clone();
14549 {
14550 let cache = &m.kv;
14551 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14552 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14553 let cpu_stored = cpu_k[0].len() / hd;
14554 let p = AttnDeviceParams {
14555 kv_id: self.mtp_kv_id(),
14556 layer: Self::MTP_LAYER_BASE,
14557 nh,
14558 nkv,
14559 hd,
14560 rd,
14561 position,
14562 scale: self.attn_scale,
14563 eps: self.rms_eps as f32,
14564 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14565 late_qk_norm: self.qk_norm_after_rope,
14566 output_gate: *output_gate,
14567 q_norm: q_norm.as_deref(),
14568 k_norm: k_norm.as_deref(),
14569 inv_freq: &inv_freq,
14570 rope_scale: self.rope_scale,
14571 cpu_k,
14572 cpu_v,
14573 cpu_stored,
14574 cpu_gen: cache.generation(),
14575 o1: None,
14576 window: None,
14577 softmax_sink: None,
14578 head_gate: None,
14579 v_norm: false,
14580 };
14581 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14582 return None;
14583 }
14584 }
14585 let draft_rows = if let Some(lm) = lm {
14591 self.draft_head_rows(lm.1)
14592 } else {
14593 0
14594 };
14595 if let Some(lm) = lm {
14596 if !graph.lm_head_ok(lm) {
14597 return None;
14598 }
14599 if draft_rows < lm.1 {
14600 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14601 return None;
14602 }
14603 } else {
14604 graph.encode_lm_head(&m.final_norm, lm);
14605 }
14606 }
14607 spec_stamp("d.enc");
14608 if graph.sync_checked().is_err() {
14609 return None;
14610 }
14611 spec_stamp("d.gpu");
14612 let mut logits = Vec::new();
14613 if let Some(lm) = lm {
14614 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14615 logits = attention::take_buf(n_read);
14616 graph.read_logits(&mut logits);
14617 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14619 }
14620 graph.finish(&mut x);
14621 let mut krow = attention::take_buf(nkv * hd);
14622 let mut vrow = attention::take_buf(nkv * hd);
14623 if crate::gpu_metal::kv_mirror_read_last(
14624 self.mtp_kv_id(),
14625 Self::MTP_LAYER_BASE,
14626 nkv,
14627 hd,
14628 &mut krow,
14629 &mut vrow,
14630 ) {
14631 m.kv.append(&krow, &vrow, &[]);
14632 }
14633 attention::recycle_buf(&mut krow);
14634 attention::recycle_buf(&mut vrow);
14635 spec_stamp("d.rd");
14636 Some((logits, x))
14637 }
14638
14639 fn mtp_chain_on() -> bool {
14652 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14653 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14654 }
14655
14656 #[cfg(target_os = "macos")]
14668 fn mtp_draft_chain_metal(
14669 &mut self,
14670 m: &mut MtpModule,
14671 hidden: &[f32],
14672 t_next: u32,
14673 position: usize,
14674 k: usize,
14675 ) -> Result<Vec<u32>, bool> {
14676 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14677 if k == 0
14678 || k > 64
14679 || !Self::mtp_chain_on()
14680 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14681 || !crate::gpu::q1_force()
14682 || !crate::gpu::enabled_here()
14683 || self.attn_softcap > 0.0
14684 || self.attention_heads_per_layer.is_some()
14685 || m.kv.mode != crate::kv_cache::KvMode::F32
14686 || m.kv.o1.is_some()
14687 || self.dsv4.is_some()
14689 || self.dsv41.is_some()
14690 || self.qwen4_exp.is_some()
14691 || self.per_token_stack()
14692 {
14693 return Err(false);
14694 }
14695 let AttnKind::Full {
14696 wq,
14697 wk,
14698 wv,
14699 wo,
14700 q_norm,
14701 k_norm,
14702 output_gate,
14703 softplus_gate: None,
14704 bias: None,
14705 } = &m.layer.attn
14706 else {
14707 return Err(false);
14708 };
14709 let FfnKind::Dense(d) = &m.layer.ffn else {
14710 return Err(false);
14711 };
14712 if d.act != Act::Silu || !d.segs.is_empty() {
14713 return Err(false);
14714 }
14715 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14716 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14717 else {
14718 return Err(false);
14719 };
14720 let (Some(g), Some(u), Some(dn)) = (
14721 d.gate_proj.q1_parts(),
14722 d.up_proj.q1_parts(),
14723 d.down_proj.q1_parts(),
14724 ) else {
14725 return Err(false);
14726 };
14727 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14728 return Err(false);
14729 };
14730 let QTensor::Mapped { model, .. } = wq else {
14731 return Err(false);
14732 };
14733 let model = model.clone();
14734 let QTensor::Mapped {
14737 model: em,
14738 idx: eidx,
14739 dtype: cortiq_core::TensorDtype::Q4TiledP,
14740 ..
14741 } = &self.weights.embed_tokens
14742 else {
14743 return Err(false);
14744 };
14745 if !std::sync::Arc::ptr_eq(em, &model)
14746 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14747 {
14748 return Err(false);
14749 }
14750 let embed = (
14751 *eidx,
14752 self.weights.embed_tokens.rows(),
14753 self.weights.embed_tokens.cols(),
14754 );
14755 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14756 return Err(false);
14757 }
14758 let dims = GraphDims {
14759 hidden: self.hidden_size,
14760 eps: self.rms_eps as f32,
14761 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14762 };
14763 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14767 crate::gpu_metal::DENSE_MV,
14768 );
14769 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14770 return Err(false);
14771 };
14772 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14773 return Err(false);
14774 }
14775 let l = AttnGpuLayer {
14776 attn_norm: &m.layer.input_norm,
14777 post_norm: &m.layer.post_norm,
14778 wq: pq,
14779 wk: pk,
14780 wv: pv,
14781 wo: po,
14782 ffn: MetalFfn::Dense {
14783 gate: g,
14784 up: u,
14785 down: dn,
14786 act: (d.act == Act::Gelu) as u8,
14787 },
14788 attn_out_norm: None,
14789 ffn_out_norm: None,
14790 layer_scale: None,
14791 };
14792 let (nh, nkv, hd, rd) = (
14793 self.num_heads,
14794 self.num_kv_heads,
14795 self.head_dim,
14796 self.rotary_dim,
14797 );
14798 let inv_freq = self.inv_freq.clone();
14799 let draft_rows = self.draft_head_rows(lm.1);
14800 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
14801 if n_arg == 0 {
14802 return Err(false);
14803 }
14804 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
14811 let t_chain = std::time::Instant::now();
14812 graph.chain_ids_init(t_next, k);
14813 let cpu_stored;
14814 {
14815 let cache = &m.kv;
14816 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14817 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14818 cpu_stored = cpu_k[0].len() / hd;
14819 for j in 0..k {
14820 if !graph.encode_chain_input(
14821 embed,
14822 j as u32,
14823 &m.enorm,
14824 &m.hnorm,
14825 self.embed_multiplier,
14826 eh,
14827 ) {
14828 return Err(false);
14829 }
14830 let p = AttnDeviceParams {
14834 kv_id: self.mtp_kv_id(),
14835 layer: Self::MTP_LAYER_BASE,
14836 nh,
14837 nkv,
14838 hd,
14839 rd,
14840 position: position + j,
14841 scale: self.attn_scale,
14842 eps: self.rms_eps as f32,
14843 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14844 late_qk_norm: self.qk_norm_after_rope,
14845 output_gate: *output_gate,
14846 q_norm: q_norm.as_deref(),
14847 k_norm: k_norm.as_deref(),
14848 inv_freq: &inv_freq,
14849 rope_scale: self.rope_scale,
14850 cpu_k: cpu_k.clone(),
14851 cpu_v: cpu_v.clone(),
14852 cpu_stored: cpu_stored + j,
14853 cpu_gen: cache.generation(),
14854 o1: None,
14855 window: None,
14856 softmax_sink: None,
14857 head_gate: None,
14858 v_norm: false,
14859 };
14860 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14861 return Err(false);
14862 }
14863 if draft_rows < lm.1 {
14864 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14865 return Err(false);
14866 }
14867 } else {
14868 graph.encode_lm_head(&m.final_norm, lm);
14869 }
14870 if !graph.encode_argmax(n_arg, j as u32 + 1) {
14871 return Err(false);
14872 }
14873 if split {
14874 graph.commit();
14877 }
14878 }
14879 }
14880 let t_enc = t_chain.elapsed();
14881 if graph.sync_checked().is_err() {
14882 return Err(true);
14883 }
14884 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
14885 eprintln!(
14886 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
14887 t_enc.as_secs_f64() * 1e3,
14888 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
14889 if split { ", split" } else { "" }
14890 );
14891 }
14892 let mut ids = vec![0u32; k];
14893 if !graph.chain_ids_read(&mut ids) {
14894 return Err(true);
14895 }
14896 let mut kbuf = vec![0f32; k * nkv * hd];
14897 let mut vbuf = vec![0f32; k * nkv * hd];
14898 if !crate::gpu_metal::kv_mirror_read_rows(
14899 self.mtp_kv_id(),
14900 Self::MTP_LAYER_BASE,
14901 nkv,
14902 hd,
14903 cpu_stored,
14904 k,
14905 &mut kbuf,
14906 &mut vbuf,
14907 ) {
14908 return Err(true);
14909 }
14910 for r in 0..k {
14911 m.kv.append(
14912 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14913 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14914 &[],
14915 );
14916 }
14917 Ok(ids)
14918 }
14919
14920 fn try_batch_graph_wgpu(
14921 &self,
14922 hiddens: &mut [f32],
14923 positions: &[usize],
14924 k: usize,
14925 spec: Option<crate::gpu::SpecTail<'_>>,
14926 ) -> crate::gpu::BatchGraphOutcome {
14927 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
14928 }
14929
14930 fn try_batch_graph_wgpu_prefix(
14935 &self,
14936 hiddens: &mut [f32],
14937 positions: &[usize],
14938 k: usize,
14939 spec: Option<crate::gpu::SpecTail<'_>>,
14940 layers_run: Option<&mut usize>,
14941 ) -> crate::gpu::BatchGraphOutcome {
14942 let graph_end = match self.mimo_moe.graph_prefix_end() {
14943 Some(end) if end < self.num_layers => {
14944 if layers_run.is_none() || spec.is_some() || end == 0 {
14945 return crate::gpu::BatchGraphOutcome::Declined;
14946 }
14947 end
14948 }
14949 _ => self.num_layers,
14950 };
14951 let _tb = std::time::Instant::now();
14952 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
14953 if self.attn_softcap > 0.0 {
14954 return crate::gpu::BatchGraphOutcome::Declined; }
14956 if let Some(reason) = self
14959 .wgpu_graph_attn_decline()
14960 .or_else(|| self.batch_graph_gemma_decline())
14961 {
14962 self.note_graph_decline("wgpu batch graph", reason);
14963 return crate::gpu::BatchGraphOutcome::Declined;
14964 }
14965 let nh = self.num_heads;
14966 let (nkv, hd, rd) = self.layer_geom(0);
14967 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14968 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14969 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
14970 {
14971 let name = &m.tensors[i].name;
14972 let prism = if crate::prism::is_inverse_embedding(m, name) {
14973 crate::gpu::GraphPrismOp::InverseEmbedding
14974 } else if crate::prism::is_forward_weight(m, name) {
14975 crate::gpu::GraphPrismOp::Forward
14976 } else {
14977 crate::gpu::GraphPrismOp::None
14978 };
14979 return Some(crate::gpu::GraphW {
14980 idx: i,
14981 kind,
14982 row_scale: rs,
14983 data: &[],
14984 prism,
14985 affine: crate::prism::is_affine_target(m, name),
14986 });
14987 }
14988 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14989 eprintln!(
14990 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
14991 t.rows(),
14992 t.cols()
14993 );
14994 }
14995 t.as_f32().map(|d| crate::gpu::GraphW {
14996 idx: 0,
14997 kind: 4,
14998 row_scale: &[],
14999 data: d,
15000 prism: crate::gpu::GraphPrismOp::None,
15001 affine: false,
15002 })
15003 }
15004 let built: Option<(
15005 Vec<crate::gpu::GraphLayer<'_>>,
15006 std::sync::Arc<cortiq_core::CmfModel>,
15007 )> = (|| {
15008 let mut layers = Vec::with_capacity(graph_end);
15009 let mut model = None;
15010 for li in 0..graph_end {
15011 let lw = &self.weights.layers[self.phys_layer(li)];
15012 let gffn = match &lw.ffn {
15019 FfnKind::Dense(d) if !d.segs.is_empty() => {
15020 if batch_debug {
15021 eprintln!("batch graph: dense segmented FFN at layer {li}");
15022 }
15023 return None;
15024 }
15025 FfnKind::Dense(d) => {
15026 let Some(act) = d.act.wgpu_graph_act() else {
15027 if batch_debug {
15028 eprintln!(
15029 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15030 d.act
15031 );
15032 }
15033 return None;
15034 };
15035 crate::gpu::GraphFfn::Dense {
15036 gate: gw(&d.gate_proj)?,
15037 up: gw(&d.up_proj)?,
15038 down: gw(&d.down_proj)?,
15039 act,
15040 }
15041 }
15042 FfnKind::Moe(m) => {
15043 if m.route_tau.is_some() || m.mask.is_some() {
15050 return None;
15051 }
15052 let shared = m.shared.as_ref();
15056 let has_shared = shared.is_some();
15057 let shared_gated = matches!(shared, Some((_, Some(_))));
15058 let sgate = match shared {
15059 Some((_, Some(sg))) => gw(sg)?,
15060 _ => gw(&m.router)?,
15064 };
15065 let router = gw(&m.router)?;
15066 if router.prism != crate::gpu::GraphPrismOp::None
15072 || router.affine
15073 || sgate.prism != crate::gpu::GraphPrismOp::None
15074 || sgate.affine
15075 {
15076 return None;
15077 }
15078 let inter = m.experts.first()?.gate_proj.rows();
15079 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15080 let mut q4tp: Option<bool> = None;
15081 let mut gu_q2: Option<bool> = None;
15082 let mut q82: Option<bool> = None;
15083 let mut moe_act = None;
15087 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15088 let Some(e_act) = e.act.wgpu_graph_act() else {
15089 return None;
15090 };
15091 match moe_act {
15092 Some(existing) if existing != e_act => return None,
15093 None => moe_act = Some(e_act),
15094 _ => {}
15095 }
15096 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15097 return None;
15098 }
15099 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15103 {
15104 Some((mm, gi)) => (
15105 mm,
15106 gi,
15107 e.up_proj.mapped_q4t()?.1,
15108 e.down_proj.mapped_q4t()?.1,
15109 false,
15110 false,
15111 false,
15112 ),
15113 None => match e.gate_proj.mapped_q2tp() {
15114 Some((mm, gi)) => (
15115 mm,
15116 gi,
15117 e.up_proj.mapped_q2tp()?.1,
15118 e.down_proj.mapped_q4tp()?.1,
15119 true,
15120 true,
15121 false,
15122 ),
15123 None => match e.gate_proj.mapped_q4tp() {
15124 Some((mm, gi)) => (
15125 mm,
15126 gi,
15127 e.up_proj.mapped_q4tp()?.1,
15128 e.down_proj.mapped_q4tp()?.1,
15129 true,
15130 false,
15131 false,
15132 ),
15133 None => {
15134 if !crate::gpu::moe_q82_graph_on() {
15135 return None;
15136 }
15137 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15138 (
15139 mm,
15140 gi,
15141 e.up_proj.mapped_q8_2f()?.1,
15142 e.down_proj.mapped_q8_2f()?.1,
15143 false,
15144 false,
15145 true,
15146 )
15147 }
15148 },
15149 },
15150 };
15151 if *q4tp.get_or_insert(is_p) != is_p
15152 || *gu_q2.get_or_insert(is_q2) != is_q2
15153 || *q82.get_or_insert(is_8) != is_8
15154 {
15155 return None;
15156 }
15157 if [gi, ui, di].into_iter().any(|idx| {
15158 mm.tensors.get(idx).is_some_and(|t| {
15159 crate::prism::is_forward_weight(mm, &t.name)
15160 || crate::prism::is_affine_target(mm, &t.name)
15161 })
15162 }) {
15163 return None;
15164 }
15165 model.get_or_insert_with(|| mm.clone());
15166 experts.push((gi, ui, di));
15167 }
15168 crate::gpu::GraphFfn::Moe {
15169 router,
15170 shared_gate: sgate,
15171 experts,
15172 n_exp: m.experts.len(),
15173 top_k: m.top_k,
15174 inter,
15175 act: moe_act?,
15176 norm_topk: m.norm_topk_prob,
15177 q4tp: q4tp?,
15178 gu_q2: gu_q2.unwrap_or(false),
15179 q82: q82.unwrap_or(false),
15180 sigmoid: m.router_sigmoid,
15181 sigmoid_denom_eps: m.sigmoid_denom_eps,
15182 bias: m.expert_bias.as_deref(),
15183 has_shared,
15184 detached_shared: None,
15185 shared_gated,
15186 route_scale: m.routed_scaling,
15187 }
15188 }
15189 _ => return None,
15190 };
15191 let attn = match &lw.attn {
15192 AttnKind::Full {
15193 wq,
15194 wk,
15195 wv,
15196 wo,
15197 q_norm,
15198 k_norm,
15199 output_gate,
15200 softplus_gate,
15201 bias,
15202 } => {
15203 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15204 if batch_debug {
15205 eprintln!(
15206 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15207 softplus_gate.is_some(),
15208 self.attention_heads_per_layer.is_some()
15209 );
15210 }
15211 return None;
15212 }
15213 let (m, _, _, _) =
15214 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15215 model = Some(m.clone());
15216 crate::gpu::GraphAttn::Full {
15217 wq: gw(wq)?,
15218 wk: gw(wk)?,
15219 wv: gw(wv)?,
15220 wo: gw(wo)?,
15221 q_norm: q_norm.as_deref(),
15222 k_norm: k_norm.as_deref(),
15223 late_qk_norm: self.qk_norm_after_rope,
15224 bias: bias
15225 .as_ref()
15226 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15227 output_gate: *output_gate,
15228 cpu_k: self.kv_cache.layers[li].k_heads(),
15229 cpu_v: self.kv_cache.layers[li].v_heads(),
15230 cpu_base: self.kv_cache.layers[li].base(),
15231 geom: self.graph_attn_geom(li),
15232 head_gate: None,
15235 }
15236 }
15237 AttnKind::LinearGdn(w) => {
15238 let Some(cfg) = self.gdn_cfg else {
15239 if batch_debug {
15240 eprintln!("batch graph: no GDN config at layer {li}");
15241 }
15242 return None;
15243 };
15244 let (m, _, _, _) = w
15245 .in_proj_qkv
15246 .graph_weight()
15247 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15248 model = Some(m.clone());
15249 crate::gpu::GraphAttn::Gdn {
15250 qkv: gw(&w.in_proj_qkv)?,
15251 z: gw(&w.in_proj_z)?,
15252 a: gw(&w.in_proj_a)?,
15253 b: gw(&w.in_proj_b)?,
15254 out: gw(&w.out_proj)?,
15255 conv1d: &w.conv1d,
15256 a_log: &w.a_log,
15257 dt_bias: &w.dt_bias,
15258 norm: &w.norm,
15259 nv: cfg.num_v_heads,
15260 nk: cfg.num_k_heads,
15261 dk: cfg.key_head_dim,
15262 dv: cfg.value_head_dim,
15263 kk: cfg.conv_kernel,
15264 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15265 }
15266 }
15267 _ => return None,
15268 };
15269 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15272 layers.push(crate::gpu::GraphLayer {
15273 input_norm: &lw.input_norm,
15274 attn,
15275 post_norm: &lw.post_norm,
15276 attn_out_norm: if g4 {
15277 lw.attn_out_norm.as_deref()
15278 } else {
15279 None
15280 },
15281 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15282 layer_scale: if g4 { lw.layer_scale } else { None },
15283 ffn: gffn,
15284 });
15285 }
15286 Some((layers, model?))
15287 })();
15288 let Some((layers, model)) = built else {
15289 {
15290 use std::sync::atomic::{AtomicBool, Ordering};
15291 static SAID: AtomicBool = AtomicBool::new(false);
15292 if !SAID.swap(true, Ordering::Relaxed) {
15293 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15294 }
15295 }
15296 return crate::gpu::BatchGraphOutcome::Declined;
15297 };
15298 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15299 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15300 }
15301 crate::gpu::forward_batch_graph(
15302 &model,
15303 self.graph_kv_id,
15304 &layers,
15305 &self.inv_freq,
15306 hiddens,
15307 nh,
15308 nkv,
15309 hd,
15310 rd,
15311 self.hidden_size,
15312 self.intermediate_size,
15313 positions,
15314 self.kv_cache.max_seq_len,
15315 gemma,
15316 self.rms_eps as f32,
15317 self.attn_scale,
15318 k,
15319 &(0..graph_end)
15320 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15321 .collect::<Vec<_>>(),
15322 self.o1_epoch,
15323 spec,
15324 layers_run,
15325 )
15326 }
15327
15328 fn draft_probe() -> bool {
15332 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15333 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15334 }
15335
15336 #[cfg(feature = "gpu")]
15348 fn dsv4_spec_on() -> bool {
15349 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15350 *ON.get_or_init(|| {
15351 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15355 return v != "0";
15356 }
15357 std::env::var("CMF_DSV4_SPEC")
15364 .map(|v| v != "0")
15365 .unwrap_or_else(|_| {
15366 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15367 })
15368 })
15369 }
15370
15371 #[cfg(feature = "gpu")]
15378 fn dsv4_spec_step(
15379 &mut self,
15380 tip_token: u32,
15381 t_next: u32,
15382 next_pos: usize,
15383 max_extra: usize,
15384 drafted: &mut usize,
15385 accepted_ctr: &mut usize,
15386 ) -> Option<(Vec<u32>, usize)> {
15387 let t_all = std::time::Instant::now();
15388 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15389 thread_local! {
15390 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15391 const { std::cell::Cell::new(None) };
15392 }
15393 LAST.with(|l| {
15394 if let Some(prev) = l.get() {
15395 eprintln!(
15396 "между раундами {:.1} мс",
15397 prev.elapsed().as_secs_f64() * 1e3
15398 );
15399 }
15400 l.set(Some(std::time::Instant::now()));
15401 });
15402 }
15403 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15404 eprintln!("spec_step: вход pos={next_pos}");
15405 }
15406 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15407 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15408 if self.dspark.is_none() {
15410 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15411 if t.is_empty() {
15412 return None;
15413 }
15414 crate::dsv4::dspark_arm(&t, cfg.dim);
15415 self.dspark = Some(crate::dsv4::DsparkState::new(
15416 self.dsv4_mtp.len(),
15417 &cfg,
15418 t.len(),
15419 ));
15420 }
15421 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15422 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15423 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15424 eprintln!("spec_step: пак не построился (targets {targets:?})");
15425 }
15426 let pack = pack?;
15427 let block = crate::dsv4::dspark_block();
15428 let b_box = self.dsv4.as_mut()?;
15429 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15430 let ds = self.dspark.as_mut()?;
15431 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15434 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15435 if dbg {
15436 eprintln!("spec_step: нет захвата");
15437 }
15438 return None;
15439 }
15440 ds.have_hidden = true;
15441 let tip_pos = next_pos.checked_sub(1)?;
15442 let draft_started = std::time::Instant::now();
15443 let mut conf = Vec::new();
15444 let props = crate::dsv4::dspark_draft_gpu(
15445 g,
15446 &self.dsv4_mtp,
15447 &cfg,
15448 ds,
15449 pack,
15450 st.kv_id,
15451 tip_token,
15452 tip_pos,
15453 self.pool.as_deref(),
15454 &mut conf,
15455 );
15456 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15457 *drafted += block;
15458 if props.is_empty() || props[0] != t_next {
15459 if dbg {
15460 eprintln!(
15461 "spec_step: черновик {} (props0={:?} t_next={t_next})",
15462 if props.is_empty() {
15463 "пуст"
15464 } else {
15465 "мимо"
15466 },
15467 props.first()
15468 );
15469 }
15470 return None;
15471 }
15472 let mut k_verify = crate::dsv4::dspark_verify_k()
15479 .min(props.len())
15480 .min(max_extra.saturating_add(1));
15481 let conf_min = {
15487 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15488 *M.get_or_init(|| {
15489 std::env::var("CMF_DSPARK_CONF_MIN")
15490 .ok()
15491 .and_then(|v| v.parse().ok())
15492 .unwrap_or(0.0)
15493 })
15494 };
15495 if conf_min > 0.0 && conf.len() >= props.len() {
15496 let mut keep = 1usize;
15497 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15498 keep += 1;
15499 }
15500 k_verify = k_verify.min(keep.max(2));
15501 }
15502 if k_verify < 2 {
15503 return None;
15504 }
15505 let mut fed = Vec::with_capacity(k_verify);
15506 fed.push(t_next);
15507 fed.extend_from_slice(&props[1..k_verify]);
15508 let mut argmax = Vec::new();
15509 let mut logits_all = Vec::new();
15510 let mut walked = Vec::new();
15511 let txn = crate::dsv4::dsv4_verify_chunk(
15512 g,
15513 layers,
15514 &cfg,
15515 st,
15516 &fed,
15517 next_pos,
15518 &self.inv_freq,
15519 self.pool.as_deref(),
15520 &targets,
15521 &mut argmax,
15522 &mut logits_all,
15523 &mut walked,
15524 );
15525 if txn.is_none() && dbg {
15526 eprintln!("spec_step: verify отказал");
15527 }
15528 let txn = txn?;
15529 let spec_gpu_end = txn.gpu_end;
15530 let b = fed.len();
15531 let mut accepted = 1usize;
15532 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15533 accepted += 1;
15534 }
15535 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15540 accepted = 1;
15541 }
15542 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15543 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15544 }
15545 let t_fin = std::time::Instant::now();
15546 if !crate::dsv4::dsv4_spec_finish(
15547 g,
15548 layers,
15549 &cfg,
15550 st,
15551 txn,
15552 accepted,
15553 &fed,
15554 &self.inv_freq,
15555 self.pool.as_deref(),
15556 ) {
15557 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
15558 return None;
15559 }
15560 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15561 eprintln!(
15562 "finish(k={accepted}): {:.1} мс",
15563 t_fin.elapsed().as_secs_f64() * 1e3
15564 );
15565 }
15566 *accepted_ctr += accepted - 1;
15567 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15572 let dev_caps: Vec<usize> = targets
15577 .iter()
15578 .copied()
15579 .filter(|&t| t < spec_gpu_end)
15580 .collect();
15581 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15582 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15583 return None;
15584 }
15585 for t in 0..accepted {
15586 let tip = t + 1 == accepted;
15587 for (slot, &tl) in targets.iter().enumerate() {
15588 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15589 let lo = (di * b + t) * hc * dim;
15590 crate::dsv4::dspark_capture(
15591 &caps_all[lo..lo + hc * dim],
15592 &cfg,
15593 slot,
15594 &mut ds.main_hidden,
15595 );
15596 } else if tip
15597 && crate::dsv4::dspark_peek_slot(slot, dim, {
15598 let lo = slot * dim;
15599 &mut ds.main_hidden[lo..lo + dim]
15600 })
15601 {
15602 } else {
15607 crate::dsv4::dspark_capture(
15611 &walked[t * hc * dim..(t + 1) * hc * dim],
15612 &cfg,
15613 slot,
15614 &mut ds.main_hidden,
15615 );
15616 }
15617 }
15618 crate::dsv4::dspark_ring_append(
15619 g,
15620 &self.dsv4_mtp,
15621 &cfg,
15622 ds,
15623 next_pos + t,
15624 self.pool.as_deref(),
15625 );
15626 }
15627 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15628 self.graph_logits = Some(row);
15629 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15634 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15635 crate::dsv4::pick_tally_arm();
15636 }
15637 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15638 eprintln!(
15639 "spec_step total {:.1} мс (k={accepted})",
15640 t_all.elapsed().as_secs_f64() * 1e3
15641 );
15642 }
15643 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15644 }
15645
15646 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15647 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15648 return;
15649 }
15650 let trunk_now = crate::dsv4::pick_tally_take();
15652 crate::dsv4::trunk_freq_note(&trunk_now);
15653 if !trunk_now.is_empty() {
15654 self.dspark_trunk_picks.push(trunk_now);
15655 let keep = crate::dsv4::dspark_block();
15656 if self.dspark_trunk_picks.len() > keep {
15657 self.dspark_trunk_picks.remove(0);
15658 }
15659 }
15660 for p in std::mem::take(&mut self.dspark_pending) {
15663 let Some(i) = position.checked_sub(p.0 + 1) else {
15664 continue;
15665 };
15666 let mut p = p;
15667 if i < p.1.len() {
15668 if p.2 && p.1[i] == token_id {
15669 p.3 = i + 1;
15670 } else {
15671 p.2 = false;
15672 }
15673 if i + 1 < p.1.len() {
15674 self.dspark_pending.push(p);
15675 continue;
15676 }
15677 }
15678 self.dspark_hist.push(p.3);
15679 self.dspark_real.push(token_id);
15680 }
15681 let Some(b) = &mut self.dsv4 else { return };
15682 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15683 let n_layers = layers.len();
15684 if self.dspark.is_none() {
15685 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15686 if t.is_empty() {
15687 return;
15688 }
15689 eprintln!(
15690 "DSpark: захват со слоёв {t:?}, блок {}",
15691 crate::dsv4::dspark_block()
15692 );
15693 crate::dsv4::dspark_arm(&t, cfg.dim);
15694 self.dspark = Some(crate::dsv4::DsparkState::new(
15695 self.dsv4_mtp.len(),
15696 &cfg,
15697 t.len(),
15698 ));
15699 }
15700 let ds = self.dspark.as_mut().unwrap();
15701 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15702 return; }
15704 let mut conf = Vec::new();
15705 crate::dsv4::pick_tally_arm();
15706 let draft_started = std::time::Instant::now();
15711 #[cfg(feature = "gpu")]
15712 let gpu_draft = crate::dsv4::dspark_gpu_on();
15713 #[cfg(not(feature = "gpu"))]
15714 let gpu_draft = false;
15715 let props = if gpu_draft {
15716 #[cfg(feature = "gpu")]
15717 {
15718 let kv_id = b.3.kv_id;
15719 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15720 Some(pk) => crate::dsv4::dspark_draft_gpu(
15721 g,
15722 &self.dsv4_mtp,
15723 &cfg,
15724 ds,
15725 pk,
15726 kv_id,
15727 token_id,
15728 position,
15729 self.pool.as_deref(),
15730 &mut conf,
15731 ),
15732 None => Vec::new(),
15733 }
15734 }
15735 #[cfg(not(feature = "gpu"))]
15736 Vec::new()
15737 } else {
15738 crate::gpu::cpu_scope(|| {
15739 crate::dsv4::dspark_draft(
15740 g,
15741 &self.dsv4_mtp,
15742 &cfg,
15743 ds,
15744 token_id,
15745 position,
15746 self.pool.as_deref(),
15747 &mut conf,
15748 )
15749 })
15750 };
15751 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15752 let draft_picks = crate::dsv4::pick_tally_take();
15753 crate::dsv4::dspark_freq_note(&draft_picks);
15754 crate::dsv4::pick_tally_arm();
15757 if !props.is_empty() {
15758 let (tu, tt) = {
15762 let flat: Vec<(usize, Vec<usize>)> = self
15763 .dspark_trunk_picks
15764 .iter()
15765 .flat_map(|v| v.iter().cloned())
15766 .collect();
15767 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15769 std::collections::HashMap::new();
15770 for (li, picks) in flat {
15771 per.entry(li).or_default().extend(picks);
15772 }
15773 let n = per.len().max(1);
15774 let mut u = 0usize;
15775 let mut t = 0usize;
15776 for (_, v) in per {
15777 t += v.len();
15778 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15779 }
15780 (u / n, t / n)
15781 };
15782 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15783 self.dspark_exp.push((tu, tt, du, dt));
15784 self.dspark_pending.push((position, props, true, 0));
15785 }
15786 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15787 let n = self.dspark_hist.len() as f32;
15788 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15789 let block = crate::dsv4::dspark_block();
15790 let mut at = vec![0usize; block + 1];
15791 for &k in &self.dspark_hist {
15792 at[k] += 1;
15793 }
15794 let mut surv = Vec::with_capacity(block);
15796 for i in 1..=block {
15797 let k = at[i..].iter().sum::<usize>() as f32 / n;
15798 surv.push(format!("{k:.2}"));
15799 }
15800 let distinct = self
15801 .dspark_real
15802 .iter()
15803 .collect::<std::collections::HashSet<_>>()
15804 .len();
15805 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
15806 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
15807 });
15808 let m = self.dspark_exp.len().max(1);
15809 eprintln!(
15810 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
15811 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
15812 self.dspark_hist.len(),
15813 mean + 1.0,
15814 surv.join(" ")
15815 );
15816 eprintln!(
15817 "DSpark: разных токенов {distinct} из {} (вырожденность), \
15818 эксперты ствол {}/{} на слой за {block} токенов, \
15819 черновик {}/{} за блок, draft {:.2} мс/блок",
15820 self.dspark_real.len(),
15821 tu / m,
15822 tt / m,
15823 du / m,
15824 dt / m,
15825 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
15826 );
15827 }
15828 }
15829
15830 fn forward_layers_upto(
15831 &mut self,
15832 hidden: &[f32],
15833 position: usize,
15834 task_mask: Option<&TaskMask>,
15835 upto: Option<usize>,
15836 ) -> Vec<f32> {
15837 if let Some(plan) = self.gpu_plan.clone() {
15843 if upto.is_none() && plan.len() > 1 {
15844 let mut h = hidden.to_vec();
15845 for &(dev, from, upto_incl) in plan.iter() {
15846 h = crate::gpu::with_device(dev, || {
15847 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
15848 });
15849 }
15850 return h;
15851 }
15852 }
15853 self.forward_layers_span(hidden, position, task_mask, 0, upto)
15854 }
15855
15856 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
15861 self.set_gpu_plan_at(devices, None)
15862 }
15863
15864 pub fn set_gpu_plan_at(
15868 &mut self,
15869 devices: Option<&[usize]>,
15870 at: Option<usize>,
15871 ) -> Result<(), String> {
15872 let Some(devs) = devices.filter(|d| d.len() > 1) else {
15873 self.gpu_plan = None;
15874 return Ok(());
15875 };
15876 self.split_supported()?;
15877 let n = self.num_layers;
15878 if devs.len() > n {
15879 return Err(format!("{} devices for {n} layers", devs.len()));
15880 }
15881 if let Some(k) = at {
15882 if k == 0 || k >= n {
15883 return Err(format!("split at {k}: the model has {n} layers"));
15884 }
15885 if devs.len() == 2 {
15886 self.gpu_plan = Some(std::sync::Arc::new(vec![
15887 (devs[0], 0, k - 1),
15888 (devs[1], k, n - 1),
15889 ]));
15890 return Ok(());
15891 }
15892 return Err(format!(
15893 "an explicit split point takes exactly 2 devices, got {}",
15894 devs.len()
15895 ));
15896 }
15897 let per = n.div_ceil(devs.len());
15898 let mut plan = Vec::with_capacity(devs.len());
15899 let mut from = 0usize;
15900 for &d in devs {
15901 if from >= n {
15902 break;
15903 }
15904 let upto = (from + per - 1).min(n - 1);
15905 plan.push((d, from, upto));
15906 from = upto + 1;
15907 }
15908 self.gpu_plan = Some(std::sync::Arc::new(plan));
15909 Ok(())
15910 }
15911
15912 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
15914 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
15915 }
15916
15917 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
15923 if let Some(x) = t.as_f32() {
15924 return x.to_vec();
15925 }
15926 let mut out = vec![0.0; t.rows() * t.cols()];
15927 for r in 0..t.rows() {
15928 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
15929 }
15930 out
15931 }
15932
15933 fn embryo_resident_eligible(&self) -> bool {
15934 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
15937 || self.num_layers != self.physical_layers
15938 || self.loop_final_norm
15939 || self.weights.layers.len() != self.num_layers
15940 || self.head_clusters.is_none()
15941 || self.final_softcap.is_some()
15942 || self.logit_multiplier.is_some()
15943 || self.attn_softcap != 0.0
15944 || self.mtp.is_some()
15945 || self.per_token_stack()
15946 || self.dsv4.is_some()
15947 || self.dsv41.is_some()
15948 || self.qwen4_exp.is_some()
15949 || self.dyn_router.is_some()
15956 || self.dyn_phi_layer.is_some()
15957 || self.dyn_blend_loaded
15958 || self.o1_cfg.is_some()
15959 || self.swa.is_some()
15960 || self.sliding_layers.is_some()
15961 || self.global_attn.is_some()
15962 || self.attention_heads_per_layer.is_some()
15963 || self.attn_v_norm
15964 || self
15965 .kv_cache
15966 .layers
15967 .iter()
15968 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
15969 || self.rope_scale != 1.0
15970 || self.rope_scale_local != 1.0
15971 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
15972 || self.hidden_size == 0
15973 || self.hidden_size > 1024
15974 || self.intermediate_size > 1024
15975 || self.num_heads == 0
15976 || self.num_kv_heads == 0
15977 || self.num_heads % self.num_kv_heads != 0
15978 || self.num_heads.saturating_mul(self.head_dim) > 1024
15979 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
15980 || self.vocab_size == 0
15981 || self.kv_cache.max_seq_len == 0
15982 || self.rotary_dim == 0
15983 || self.rotary_dim > self.head_dim
15984 || self.rotary_dim % 2 != 0
15985 || self.inv_freq.len() < self.rotary_dim / 2
15986 {
15987 return false;
15988 }
15989 if self.weights.lm_head.as_f32().is_none()
16000 || self.weights.embed_tokens.as_f32().is_none()
16001 || self.weights.lm_head.rows() < self.vocab_size
16002 || self.weights.lm_head.cols() != self.hidden_size
16003 || self.weights.embed_tokens.rows() < self.vocab_size
16004 || self.weights.embed_tokens.cols() != self.hidden_size
16005 || self.weights.final_norm.len() != self.hidden_size
16006 {
16007 return false;
16008 }
16009 if let Some(cfg) = self.vmf_cfg {
16010 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16011 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16012 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16013 || cfg.state_len() == 0
16014 {
16015 return false;
16016 }
16017 }
16018 if let Some(g) = self.gdn_cfg {
16019 if g.num_v_heads == 0
16024 || g.num_k_heads == 0
16025 || g.num_v_heads % g.num_k_heads != 0
16026 || g.key_head_dim == 0
16027 || g.key_head_dim > 128
16028 || g.value_head_dim == 0
16029 || g.value_head_dim > 256
16030 || g.value_head_dim % 4 != 0
16031 || g.conv_kernel == 0
16032 || g.num_v_heads > 512
16033 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16034 || g.conv_dim() > 2048
16035 || g.conv_dim() % 4 != 0
16036 || g.hidden_size != self.hidden_size
16037 || g.output_gate_sigmoid
16038 || g.rms_eps != self.rms_eps
16039 || g.state_len() == 0
16040 {
16041 return false;
16042 }
16043 }
16044 let mut full_seen = false;
16045 for lw in &self.weights.layers {
16046 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16047 return false;
16048 }
16049 match &lw.attn {
16050 AttnKind::LinearGdn(w) => {
16051 let Some(g) = self.gdn_cfg else {
16052 return false;
16053 };
16054 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16055 if w.in_proj_qkv.rows() != g.conv_dim()
16056 || w.in_proj_qkv.cols() != self.hidden_size
16057 || w.in_proj_qkv.as_f32().is_none()
16058 || w.in_proj_z.rows() != nv * dv
16059 || w.in_proj_z.cols() != self.hidden_size
16060 || w.in_proj_z.as_f32().is_none()
16061 || w.in_proj_a.rows() != nv
16062 || w.in_proj_a.cols() != self.hidden_size
16063 || w.in_proj_a.as_f32().is_none()
16064 || w.in_proj_b.rows() != nv
16065 || w.in_proj_b.cols() != self.hidden_size
16066 || w.in_proj_b.as_f32().is_none()
16067 || w.conv1d.len() != g.conv_dim() * kk
16068 || w.a_log.len() != nv
16069 || w.dt_bias.len() != nv
16070 || w.norm.len() != dv
16071 || w.out_proj.rows() != self.hidden_size
16072 || w.out_proj.cols() != nv * dv
16073 || w.out_proj.as_f32().is_none()
16074 {
16075 return false;
16076 }
16077 }
16078 AttnKind::Linear(w) => {
16079 let Some(cfg) = self.vmf_cfg else {
16080 return false;
16081 };
16082 if w.thq.rows() != cfg.num_heads * cfg.nphase
16083 || w.thq.cols() != self.hidden_size
16084 || w.thq.as_f32().is_none()
16085 || w.thk.rows() != cfg.num_heads * cfg.nphase
16086 || w.thk.cols() != self.hidden_size
16087 || w.thk.as_f32().is_none()
16088 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16089 || w.v_proj.cols() != self.hidden_size
16090 || w.v_proj.as_f32().is_none()
16091 || w.out_proj.rows() != self.hidden_size
16092 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16093 || w.out_proj.as_f32().is_none()
16094 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16095 {
16096 return false;
16097 }
16098 if let Some((kg, kb)) = &w.k_gate {
16099 if kg.rows() != cfg.num_heads
16100 || kg.cols() != self.hidden_size
16101 || kg.as_f32().is_none()
16102 || kb.len() != cfg.num_heads
16103 {
16104 return false;
16105 }
16106 }
16107 if let Some(conv) = &w.conv {
16108 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16109 return false;
16110 }
16111 }
16112 }
16113 AttnKind::Full {
16114 wq,
16115 wk,
16116 wv,
16117 wo,
16118 q_norm,
16119 k_norm,
16120 output_gate,
16121 softplus_gate,
16122 bias,
16123 } => {
16124 if full_seen
16125 || q_norm.is_some()
16126 || k_norm.is_some()
16127 || *output_gate
16128 || softplus_gate.is_some()
16129 || bias.is_some()
16130 || wq.as_f32().is_none()
16131 || wk.as_f32().is_none()
16132 || wv.as_f32().is_none()
16133 || wo.as_f32().is_none()
16134 || wq.rows() != self.num_heads * self.head_dim
16135 || wk.rows() != self.num_kv_heads * self.head_dim
16136 || wv.rows() != self.num_kv_heads * self.head_dim
16137 || wq.cols() != self.hidden_size
16138 || wk.cols() != self.hidden_size
16139 || wv.cols() != self.hidden_size
16140 || wo.rows() != self.hidden_size
16141 || wo.cols() != self.num_heads * self.head_dim
16142 {
16143 return false;
16144 }
16145 full_seen = true;
16146 }
16147 AttnKind::Bounded(w) => {
16148 let Some(ac) = self.anchor_core.as_ref() else {
16151 return false;
16152 };
16153 if self.bounded_rope.is_none()
16154 || w.window != ac.window
16155 || w.sink != ac.sink
16156 || w.window == 0
16157 || w.window + w.sink > 256
16158 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16159 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16160 || w.wq.as_f32().is_none()
16161 || w.wk.as_f32().is_none()
16162 || w.wv.as_f32().is_none()
16163 || w.wo.as_f32().is_none()
16164 || w.wq.rows() != self.num_heads * self.head_dim
16165 || w.wk.rows() != self.num_kv_heads * self.head_dim
16166 || w.wv.rows() != self.num_kv_heads * self.head_dim
16167 || w.wq.cols() != self.hidden_size
16168 || w.wk.cols() != self.hidden_size
16169 || w.wv.cols() != self.hidden_size
16170 || w.wo.rows() != self.hidden_size
16171 || w.wo.cols() != self.num_heads * self.head_dim
16172 {
16173 return false;
16174 }
16175 }
16176 _ => return false,
16177 }
16178 match &lw.ffn {
16179 FfnKind::Dense(d) => {
16180 if d.act != Act::Silu
16181 || !d.segs.is_empty()
16182 || d.gate_proj.as_f32().is_none()
16183 || d.up_proj.as_f32().is_none()
16184 || d.down_proj.as_f32().is_none()
16185 || d.gate_proj.rows() != self.intermediate_size
16186 || d.gate_proj.cols() != self.hidden_size
16187 || d.up_proj.rows() != self.intermediate_size
16188 || d.up_proj.cols() != self.hidden_size
16189 || d.down_proj.rows() != self.hidden_size
16190 || d.down_proj.cols() != self.intermediate_size
16191 {
16192 return false;
16193 }
16194 }
16195 FfnKind::Moe(m) => {
16196 if m.resonance.is_none()
16197 || m.top_k != 1
16198 || m.router_sigmoid
16199 || !m.norm_topk_prob
16200 || m.expert_bias.is_some()
16201 || m.routed_scaling != 1.0
16202 || m.route_tau.is_some()
16203 || m.shared.is_none()
16204 || m.mask.is_some()
16205 || m.per_expert_scale.is_some()
16206 || m.router_input_norm
16207 || m.experts.is_empty()
16208 || m.experts.len() > 8
16209 {
16210 return false;
16211 }
16212 let r = m.resonance.as_ref().unwrap();
16213 if r.mu.len() != m.experts.len() * self.hidden_size
16214 || r.bias.len() != m.experts.len()
16215 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16216 || r.k > 128
16217 {
16218 return false;
16219 }
16220 let Some((shared, gate)) = &m.shared else {
16221 return false;
16222 };
16223 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16224 return false;
16225 }
16226 if shared.gate_proj.as_f32().is_none()
16227 || shared.up_proj.as_f32().is_none()
16228 || shared.down_proj.as_f32().is_none()
16229 || shared.gate_proj.rows() != self.intermediate_size
16230 || shared.gate_proj.cols() != self.hidden_size
16231 || shared.up_proj.rows() != self.intermediate_size
16232 || shared.up_proj.cols() != self.hidden_size
16233 || shared.down_proj.rows() != self.hidden_size
16234 || shared.down_proj.cols() != self.intermediate_size
16235 {
16236 return false;
16237 }
16238 for e in &m.experts {
16239 if e.act != Act::Silu
16240 || !e.segs.is_empty()
16241 || e.gate_proj.as_f32().is_none()
16242 || e.up_proj.as_f32().is_none()
16243 || e.down_proj.as_f32().is_none()
16244 || e.gate_proj.rows() != self.intermediate_size
16245 || e.gate_proj.cols() != self.hidden_size
16246 || e.up_proj.rows() != self.intermediate_size
16247 || e.up_proj.cols() != self.hidden_size
16248 || e.down_proj.rows() != self.hidden_size
16249 || e.down_proj.cols() != self.intermediate_size
16250 {
16251 return false;
16252 }
16253 }
16254 }
16255 FfnKind::DenseMoe(_) => return false,
16256 }
16257 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16258 return false;
16259 }
16260 }
16261 if full_seen && self.anchor_core.is_some() {
16262 return false;
16263 }
16264 full_seen || self.num_layers > 0
16265 }
16266
16267 fn embryo_resident_wanted(&self) -> bool {
16272 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16273 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16274 && matches!(
16275 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16276 Ok("1") | Ok("parallel")
16277 )
16278 && crate::gpu::enabled_here()
16279 && !self.graph_refused()
16280 && self.embryo_resident_eligible()
16281 }
16282
16283 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16291 if ids.len() < 2
16292 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16293 || !self.embryo_resident_wanted()
16294 {
16295 return None;
16296 }
16297 let model = self.ensure_embryo_graph()?;
16298 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16299 .ok()
16300 .and_then(|v| v.parse::<usize>().ok())
16301 .filter(|&v| v >= 1)
16302 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16303 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16304 let hs = self.hidden_size;
16305 let n = ids.len();
16306 let mut pos = start;
16307 let mut last = None;
16308 let mut rows = Vec::with_capacity(cmax * hs);
16309 while pos < n {
16310 let end = (pos + cmax).min(n);
16311 rows.clear();
16312 for &id in &ids[pos..end] {
16313 rows.extend_from_slice(&self.embed_single(id));
16314 }
16315 let mut lg = Vec::new();
16316 if !crate::gpu::forward_embryo_graph_chunk(
16317 &model,
16318 self.graph_kv_id,
16319 &rows,
16320 pos,
16321 end - pos,
16322 &mut lg,
16323 ) {
16324 if pos == start {
16325 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16326 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16327 }
16328 return None;
16329 }
16330 self.kv_cache.clear();
16334 self.clear_history();
16335 crate::gpu::graph_kv_reset(self.graph_kv_id);
16336 panic!(
16337 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16338 );
16339 }
16340 last = Some(lg);
16341 pos = end;
16342 }
16343 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16344 eprintln!(
16345 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16346 n - start,
16347 (n - start).div_ceil(cmax)
16348 );
16349 }
16350 last
16351 }
16352
16353 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16354 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16355 const UMAX: u32 = u32::MAX;
16356 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16357 const REC: usize = 64;
16358 struct Pack {
16359 data: Vec<f32>,
16360 }
16361 impl Pack {
16362 fn put(&mut self, x: &[f32]) -> u32 {
16363 if x.is_empty() {
16364 return u32::MAX;
16365 }
16366 let off = self.data.len();
16367 self.data.extend_from_slice(x);
16368 off as u32
16369 }
16370 }
16371 let vmf = self.vmf_cfg;
16375 let gdn = self.gdn_cfg;
16376 let mut pack = Pack { data: Vec::new() };
16377 let mut meta = vec![0u32; HEADER];
16378 meta[0] = self.hidden_size as u32;
16379 meta[1] = self.intermediate_size as u32;
16380 meta[2] = self.vocab_size as u32;
16381 meta[3] = self.num_layers as u32;
16382 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16383 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16384 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16385 if let Some(g) = gdn {
16386 meta[24] = g.num_v_heads as u32;
16387 meta[25] = g.num_k_heads as u32;
16388 meta[26] = g.key_head_dim as u32;
16389 meta[27] = g.value_head_dim as u32;
16390 meta[28] = g.conv_kernel as u32;
16391 meta[29] = g.conv_dim() as u32;
16392 }
16393 meta[7] = self.num_heads as u32;
16394 meta[8] = self.num_kv_heads as u32;
16395 meta[9] = self.head_dim as u32;
16396 meta[10] = self.kv_cache.max_seq_len as u32;
16397 let clusters = self.head_clusters.as_ref().unwrap();
16398 let cluster_count = clusters.len() / self.hidden_size;
16399 if clusters.len() % self.hidden_size != 0
16400 || cluster_count == 0
16401 || cluster_count > 1024
16402 || self.vocab_size % cluster_count != 0
16403 || self.weights.lm_head.rows() < self.vocab_size
16404 || self.weights.final_norm.len() != self.hidden_size
16405 {
16406 return None;
16407 }
16408 meta[11] = cluster_count as u32;
16409 meta[12] = (self.vocab_size / cluster_count) as u32;
16410 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16411 meta[16] = self.rotary_dim as u32;
16412 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16413 meta[19] = (self.rms_eps as f32).to_bits();
16414 let max_conv = self
16415 .weights
16416 .layers
16417 .iter()
16418 .filter_map(|lw| match &lw.attn {
16419 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16420 _ => None,
16421 })
16422 .max()
16423 .unwrap_or(1);
16424 let phase_stride = vmf
16430 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16431 .unwrap_or(0);
16432 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16433 let state_stride = phase_stride.max(gdn_stride);
16434 let bounded = self.anchor_core.clone();
16438 let (anchor_window, anchor_sink) = bounded
16439 .as_ref()
16440 .map(|ac| (ac.window, ac.sink))
16441 .unwrap_or((0, 0));
16442 let kv_stride = if bounded.is_some() {
16443 2usize
16444 .saturating_mul(self.num_kv_heads)
16445 .saturating_mul(anchor_window)
16446 .saturating_mul(self.head_dim)
16447 } else {
16448 2usize
16449 .saturating_mul(self.num_kv_heads)
16450 .saturating_mul(self.kv_cache.max_seq_len)
16451 .saturating_mul(self.head_dim)
16452 };
16453 meta[14] = state_stride as u32;
16454 meta[15] = kv_stride as u32;
16455 meta[18] = anchor_window as u32;
16456 meta[20] = anchor_sink as u32;
16457 meta[21] = match &self.bounded_rope {
16458 Some(rope) => {
16459 let off = pack.put(&rope.cos);
16461 let _ = pack.put(&rope.sin);
16462 off
16463 }
16464 None => UMAX,
16465 };
16466 let mut full_seen = false;
16467 let mut bounded_seen = 0usize;
16468 let mut phase_seen = 0usize;
16472 let mut gdn_seen = 0usize;
16473 for (li, lw) in self.weights.layers.iter().enumerate() {
16474 let base = meta.len();
16475 meta.resize(base + REC, UMAX);
16476 meta[base] = match &lw.attn {
16477 AttnKind::Linear(w) if w.phase_delta => 1,
16478 AttnKind::Linear(_) => 0,
16479 AttnKind::Full { .. } => 2,
16480 AttnKind::Bounded(_) => 3,
16481 AttnKind::LinearGdn(_) => 4,
16482 _ => UMAX,
16483 };
16484 meta[base + 1] = pack.put(&lw.input_norm);
16485 meta[base + 2] = pack.put(&lw.post_norm);
16486 meta[base + 25] = match &lw.attn {
16487 AttnKind::Linear(_) => {
16488 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16489 phase_seen += 1;
16490 off
16491 }
16492 AttnKind::LinearGdn(_) => {
16493 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16494 gdn_seen += 1;
16495 off
16496 }
16497 _ => UMAX,
16498 };
16499 match &lw.attn {
16500 AttnKind::LinearGdn(w) => {
16501 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16504 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16505 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16506 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16507 meta[base + 60] = pack.put(&w.conv1d);
16508 meta[base + 61] = pack.put(&w.a_log);
16509 meta[base + 62] = pack.put(&w.dt_bias);
16510 meta[base + 63] = pack.put(&w.norm);
16511 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16512 meta[base + 24] = 0;
16513 }
16514 AttnKind::Linear(w) => {
16515 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16516 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16517 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16518 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16519 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16520 meta[base + 7] = pack.put(&decay);
16521 if let Some((kg, kb)) = &w.k_gate {
16522 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16523 meta[base + 9] = pack.put(kb);
16524 }
16525 if let Some(conv) = &w.conv {
16526 meta[base + 10] = pack.put(conv);
16527 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16528 } else {
16529 meta[base + 24] = 0;
16530 }
16531 }
16532 AttnKind::Full { wq, wk, wv, wo, .. } => {
16533 full_seen = true;
16534 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16535 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16536 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16537 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16538 meta[base + 26] = (li * kv_stride) as u32;
16539 }
16540 AttnKind::Bounded(w) => {
16541 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16542 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16543 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16544 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16545 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16547 meta[base + 27] = pack.put(&w.sink_k);
16548 meta[base + 28] = pack.put(&w.sink_v);
16549 bounded_seen += 1;
16550 }
16551 _ => return None,
16552 }
16553 match &lw.ffn {
16554 FfnKind::Dense(d) => {
16555 meta[base + 15] = 0;
16556 meta[base + 16] = 0;
16557 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16558 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16559 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16560 }
16561 FfnKind::Moe(m) => {
16562 let r = m.resonance.as_ref().unwrap();
16563 let (shared, _) = m.shared.as_ref().unwrap();
16564 meta[base + 15] = 1;
16565 meta[base + 16] = m.experts.len() as u32;
16566 meta[base + 17] = pack.put(&r.mu);
16567 meta[base + 18] = pack.put(&r.u);
16568 meta[base + 19] = pack.put(&r.bias);
16569 meta[base + 20] = r.k as u32;
16570 let mut shell = r.effective_shell(m.experts.len());
16579 shell.push(f32::NEG_INFINITY);
16580 meta[base + 30] = pack.put(&shell);
16581 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16582 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16583 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16584 for (e, ex) in m.experts.iter().enumerate() {
16585 meta[base + 32 + e * 3] =
16586 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16587 meta[base + 33 + e * 3] =
16588 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16589 meta[base + 34 + e * 3] =
16590 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16591 }
16592 }
16593 FfnKind::DenseMoe(_) => return None,
16594 }
16595 }
16596 if !full_seen && self.num_layers == 0 {
16597 return None;
16598 }
16599 let id = {
16600 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16601 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16602 };
16603 let model = crate::gpu::EmbryoGraphModel {
16604 id,
16605 hidden: self.hidden_size,
16606 intermediate: self.intermediate_size,
16607 vocab: self.vocab_size,
16608 layers: self.num_layers,
16609 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16610 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16611 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16612 anchor_q_heads: self.num_heads,
16613 anchor_kv_heads: self.num_kv_heads,
16614 anchor_head_dim: self.head_dim,
16615 rotary_dim: self.rotary_dim,
16616 max_seq: self.kv_cache.max_seq_len,
16617 cluster_count,
16618 cluster_size: self.vocab_size / cluster_count,
16619 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16620 state_stride,
16621 kv_stride,
16622 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16623 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16624 weights: pack.data,
16625 meta,
16626 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16627 clusters: clusters.as_ref().clone(),
16628 final_norm: self.weights.final_norm.clone(),
16629 inv_freq: self.inv_freq.as_ref().clone(),
16630 bounded: bounded.is_some(),
16631 kv_layers: if bounded.is_some() {
16632 bounded_seen
16633 } else {
16634 self.num_layers
16635 },
16636 state_layers: phase_seen + gdn_seen,
16637 anchor_window,
16638 anchor_sink,
16639 phase_layers: phase_seen,
16640 gdn_layers: gdn_seen,
16641 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16642 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16643 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16644 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16645 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16646 };
16647 self.embryo_graph = Some(std::sync::Arc::new(model));
16648 }
16649 self.embryo_graph.clone()
16650 }
16651
16652 fn forward_layers_span(
16653 &mut self,
16654 hidden: &[f32],
16655 position: usize,
16656 task_mask: Option<&TaskMask>,
16657 from: usize,
16658 upto: Option<usize>,
16659 ) -> Vec<f32> {
16660 #[cfg(target_os = "macos")]
16661 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16662 position,
16663 from,
16664 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16665 );
16666 debug_assert!(
16667 from == 0
16668 || (self.dsv4.is_none()
16669 && self.dsv41.is_none()
16670 && self.qwen4_exp.is_none()
16671 && !self.per_token_stack())
16672 );
16673 #[cfg(target_os = "macos")]
16679 if !crate::gpu_metal::wait_replay() {
16680 self.fail_metal_graph("the pending async replay failed before a plain forward");
16681 return vec![0.0; self.hidden_size];
16682 }
16683 if let Some(b) = &mut self.qwen4_exp {
16684 let _ = (task_mask, upto);
16685 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16686 let mut logits = Vec::new();
16687 crate::qwen4_exp::forward_token(
16688 &b.0,
16689 &b.1,
16690 &b.2,
16691 &mut b.3,
16692 token_id,
16693 position,
16694 &self.inv_freq,
16695 self.pool.as_deref(),
16696 &mut logits,
16697 true,
16698 );
16699 self.graph_logits = Some(logits);
16700 return vec![0.0; self.hidden_size];
16701 }
16702 if let Some(b) = &mut self.dsv4 {
16708 let _ = (task_mask, upto);
16709 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16710 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16711 st.pos = position;
16712 let mut logits = Vec::new();
16713 crate::dsv4::forward_token(
16714 g,
16715 layers,
16716 &cfg,
16717 st,
16718 token_id,
16719 &self.inv_freq,
16720 self.pool.as_deref(),
16721 &mut logits,
16722 );
16723 self.graph_logits = Some(logits);
16724 self.dspark_probe(position, token_id);
16725 return vec![0.0; self.hidden_size];
16728 }
16729 if let Some(b) = &mut self.dsv41 {
16731 let _ = (task_mask, upto);
16732 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16733 let mut logits = Vec::new();
16734 crate::dsv41::forward_token(
16735 &b.0,
16736 &b.1,
16737 &b.2,
16738 &mut b.3,
16739 token_id,
16740 position,
16741 self.pool.as_deref(),
16742 &mut logits,
16743 );
16744 self.graph_logits = Some(logits);
16745 return vec![0.0; self.hidden_size];
16746 }
16747 if let Some(b) = &self.g4e {
16753 let _ = (task_mask, upto);
16754 return crate::g4e::g4e_forward(
16755 &b.0,
16756 &b.1,
16757 hidden,
16758 position,
16759 &mut self.kv_cache.layers,
16760 self.num_heads,
16761 self.num_kv_heads,
16762 self.pool.as_deref(),
16763 );
16764 }
16765 if let Some(b) = &self.g3n {
16766 let _ = (task_mask, upto);
16767 return crate::g3n::g3n_forward(
16768 &b.0,
16769 &b.1,
16770 hidden,
16771 position,
16772 &mut self.kv_cache.layers,
16773 self.num_heads,
16774 self.num_kv_heads,
16775 self.head_dim,
16776 self.pool.as_deref(),
16777 );
16778 }
16779 if from == 0
16786 && upto.is_none()
16787 && task_mask.is_none()
16788 && self.anchor_core.is_some()
16789 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16790 {
16791 static ONCE: std::sync::Once = std::sync::Once::new();
16792 ONCE.call_once(|| {
16793 eprintln!(
16794 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16795 unsupported={} eligible={}",
16796 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
16797 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
16798 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
16799 crate::gpu::enabled_here(),
16800 self.graph_refused(),
16801 self.embryo_resident_eligible(),
16802 );
16803 });
16804 }
16805 if from == 0
16806 && upto.is_none()
16807 && task_mask.is_none()
16808 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16812 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16813 && matches!(
16817 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16818 Ok("1") | Ok("parallel")
16819 )
16820 && crate::gpu::enabled_here()
16821 && !self.graph_refused()
16822 && (position == 0 || self.device_sequence_position().is_some())
16828 && self.embryo_resident_eligible()
16829 && let Some(model) = self.ensure_embryo_graph()
16830 {
16831 let mut lg = Vec::new();
16832 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
16833 {
16834 self.graph_logits = Some(lg);
16835 return vec![0.0; self.hidden_size];
16836 }
16837 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16838 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
16839 }
16840 self.mark_graph_refused();
16846 if position != 0 {
16847 self.kv_cache.clear();
16851 self.clear_history();
16852 crate::gpu::graph_kv_reset(self.graph_kv_id);
16853 panic!(
16854 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
16855 );
16856 }
16857 }
16858 let mut h = hidden.to_vec();
16859 self.mimo_moe_prepare();
16862 let _mimo_q8 = self
16863 .mimo_moe
16864 .is_on()
16865 .then(crate::qtensor::enter_full_gpu_q8_scope);
16866 let (nh, _nkv, _hd, hs, _rd, eps) = (
16869 self.num_heads,
16870 self.num_kv_heads,
16871 self.head_dim,
16872 self.hidden_size,
16873 self.rotary_dim,
16874 self.rms_eps,
16875 );
16876 let pool = self.pool.clone();
16877 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
16889 let graph_on = match graph_env.as_deref() {
16890 Some("0") => false,
16891 Some("prefill") => false, Some(_) => true,
16893 None => crate::gpu::wgpu_graph_default(),
16899 };
16900 let graph_trusted =
16901 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
16902 let race_eligible =
16903 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
16904 let mut tail_start = 0usize;
16905 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
16906 let t_graph = std::time::Instant::now();
16907 let mut lg = Vec::new();
16908 let mut gl = 0usize;
16909 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
16910 let declined = built.is_none();
16911 let built = match built {
16912 Some(Ok(hh)) => Some(hh),
16913 Some(Err(())) => {
16914 self.clear_sequence_state();
16918 self.graph_failed
16919 .store(true, std::sync::atomic::Ordering::Relaxed);
16920 self.cancel
16921 .store(true, std::sync::atomic::Ordering::Relaxed);
16922 tracing::error!("token graph failed after admission; sequence state cleared");
16923 return vec![0.0; self.hidden_size];
16924 }
16925 None => None,
16926 };
16927 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
16932 self.mark_graph_refused();
16933 }
16934 graph_note(built.is_some(), gl, self.num_layers);
16935 if let Some(hh) = built {
16936 let dur = t_graph.elapsed();
16937 if std::env::var("CMF_GRAPH_PROF").is_ok() {
16938 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
16939 }
16940 if gl > 0 && gl < self.num_layers {
16941 h = hh;
16947 tail_start = gl;
16948 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
16949 if !graph_trusted {
16950 crate::gpu::graph_race_record(true, dur);
16951 }
16952 if !lg.is_empty() {
16953 lg.resize(self.vocab_size, 0.0);
16956 if let Some(c) = self.final_softcap {
16957 softcap_in_place(&mut lg, c, self.pool.as_deref());
16958 }
16959 self.graph_logits = Some(lg);
16960 }
16961 return hh;
16962 }
16963 }
16969 }
16970 let span = from > 0 || upto.is_some();
16994 if span && graph_on && task_mask.is_none() && graph_trusted {
16995 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
16996 let mut lg = Vec::new();
16997 let mut gl = 0usize;
16998 let span_res =
16999 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17000 let span_res = match span_res {
17001 Some(Ok(hh)) => Some(hh),
17002 Some(Err(())) => {
17003 self.clear_sequence_state();
17004 self.graph_failed
17005 .store(true, std::sync::atomic::Ordering::Relaxed);
17006 self.cancel
17007 .store(true, std::sync::atomic::Ordering::Relaxed);
17008 tracing::error!(
17009 "span token graph failed after admission; sequence state cleared"
17010 );
17011 return vec![0.0; self.hidden_size];
17012 }
17013 None => None,
17014 };
17015 graph_note(span_res.is_some(), gl, upto_excl - from);
17016 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17017 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17021 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17022 eprintln!(
17023 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17024 upto_excl - from,
17025 span_res.is_some()
17026 );
17027 }
17028 }
17029 if let Some(hh) = span_res {
17030 if gl == upto_excl - from {
17031 if !lg.is_empty() {
17032 lg.resize(self.vocab_size, 0.0);
17033 if let Some(c) = self.final_softcap {
17034 softcap_in_place(&mut lg, c, self.pool.as_deref());
17035 }
17036 self.graph_logits = Some(lg);
17037 }
17038 crate::gpu::set_layer(-1);
17039 return hh;
17040 }
17041 h = hh;
17043 tail_start = from + gl;
17044 }
17045 }
17046 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17051
17052 let host_tail = tail_start > from;
17062 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17063 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17064
17065 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17066 #[cfg(target_os = "macos")]
17067 let mut gpu_skip_until = 0usize;
17068 for li in tail_start.max(from)..self.num_layers {
17069 let _capacity_tail = automatic_gpu_prefix
17070 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17071 .map(|_| crate::gpu::enter_cpu_scope());
17072 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17074 if li > u {
17075 break;
17076 }
17077 }
17078 if let Some(mask) = task_mask {
17079 if !mask.layer_alive(li) {
17080 continue; }
17082 }
17083 if host_tail && task_mask.is_none() {
17088 match self.m3_pooled_layer(li, position, &mut h) {
17089 Ok(true) => {
17090 crate::gpu::set_layer(-1);
17091 continue;
17092 }
17093 Ok(false) => {}
17094 Err(()) => {
17095 self.clear_sequence_state();
17096 self.graph_failed
17097 .store(true, std::sync::atomic::Ordering::Relaxed);
17098 self.cancel
17099 .store(true, std::sync::atomic::Ordering::Relaxed);
17100 tracing::error!(
17101 "pooled tail token graph failed after admission; sequence state cleared"
17102 );
17103 return vec![0.0; self.hidden_size];
17104 }
17105 }
17106 }
17107 #[cfg(target_os = "macos")]
17111 {
17112 if li < gpu_skip_until {
17113 continue;
17114 }
17115 if task_mask.is_none() {
17116 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17117 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17118 return vec![0.0; self.hidden_size];
17122 }
17123 if end > li {
17124 gpu_skip_until = end;
17125 if self.is_loop_end(end - 1) && end < self.num_layers {
17128 h = inference::rms_norm(
17129 &h,
17130 &self.weights.final_norm,
17131 self.rms_eps,
17132 self.norm_style,
17133 );
17134 }
17135 continue;
17136 }
17137 }
17138 }
17139
17140 if task_mask.is_none() {
17141 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17142 crate::gpu::BatchGraphOutcome::Completed => continue,
17143 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17144 crate::gpu::BatchGraphOutcome::Declined => {}
17145 }
17146 }
17147 #[cfg(feature = "gpu")]
17148 self.pull_lagging_host_kv(li, li + 1, position);
17149 let lw = &self.weights.layers[self.phys_layer(li)];
17150 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17151 if tp.parse::<usize>().ok() == Some(position) {
17152 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17153 eprintln!(
17154 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17155 h[0], h[1]
17156 );
17157 }
17158 }
17159 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17162 inference::rms_norm_into(
17163 &h,
17164 &lw.input_norm,
17165 self.rms_eps,
17166 self.norm_style,
17167 &mut self.ws.n1,
17168 );
17169 drop(prof);
17170
17171 let attn_out = match &lw.attn {
17172 AttnKind::Mla(w) => {
17173 let inv_freq_l = self.layer_inv_freq(li);
17174 let rs = self.layer_rope_scale(li);
17175 let eps = self.rms_eps;
17176 let pool = self.pool.clone();
17177 mla_attention(
17178 w,
17179 &self.ws.n1,
17180 &mut self.kv_cache.layers[li],
17181 position,
17182 &inv_freq_l,
17183 rs,
17184 eps,
17185 pool.as_deref(),
17186 )
17187 }
17188 AttnKind::Linear(w) => {
17189 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17190 vmf_phase_forward(
17191 &self.ws.n1,
17192 w,
17193 &cfg,
17194 &mut self.kv_cache.layers[li].linear_state,
17195 self.pool.as_deref(),
17196 )
17197 }
17198 AttnKind::Kda(w) => {
17199 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17200 crate::linear_core::kda_forward(
17201 &self.ws.n1,
17202 w,
17203 &cfg,
17204 &mut self.kv_cache.layers[li].linear_state,
17205 self.pool.as_deref(),
17206 )
17207 }
17208 AttnKind::LinearGdn(w) => {
17209 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17210 gdn_forward(
17211 &self.ws.n1,
17212 w,
17213 &cfg,
17214 &mut self.kv_cache.layers[li].linear_state,
17215 self.pool.as_deref(),
17216 )
17217 }
17218 AttnKind::ShortConv(w) => {
17219 let cfg = self
17220 .short_conv_cfg
17221 .expect("short-conv layer without short_conv_cfg");
17222 short_conv_forward(
17223 &self.ws.n1,
17224 w,
17225 &cfg,
17226 &mut self.kv_cache.layers[li].linear_state,
17227 self.pool.as_deref(),
17228 )
17229 }
17230 AttnKind::Bounded(w) => {
17231 let rope = self
17234 .bounded_rope
17235 .clone()
17236 .expect("bounded layer without an installed rotation table");
17237 let cfg = crate::bounded::BoundedAttnCfg {
17238 num_heads: self.num_heads,
17239 num_kv_heads: self.num_kv_heads,
17240 head_dim: self.head_dim,
17241 hidden_size: hs,
17242 scale: self.attn_scale,
17243 rope: &rope,
17244 pool: pool.as_deref(),
17245 };
17246 crate::bounded::bounded_attention(
17247 &self.ws.n1,
17248 w,
17249 &mut self.kv_cache.layers[li],
17250 &cfg,
17251 )
17252 }
17253 AttnKind::Full {
17254 wq,
17255 wk,
17256 wv,
17257 wo,
17258 q_norm,
17259 k_norm,
17260 output_gate,
17261 softplus_gate,
17262 bias,
17263 } if self.kv_cache.layers[li].o1_sealed() => {
17264 let inv_freq_l = self.layer_inv_freq(li);
17267 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17268 let cfg = QwenAttnCfg {
17269 num_heads: self.layer_num_heads(li),
17270 num_kv_heads: nkv_l,
17271 head_dim: hd_l,
17272 hidden_size: hs,
17273 position,
17274 inv_freq: &inv_freq_l,
17275 rotary_dim: rd_l,
17276 scale: self.attn_scale,
17277 softcap: self.attn_softcap,
17278 window: None,
17279 v_norm: self.attn_v_norm,
17280 qk_norm_after_rope: self.qk_norm_after_rope,
17281 gate_sigmoid: self.proj_gate_sigmoid,
17282 q_norm: q_norm.as_deref(),
17283 k_norm: k_norm.as_deref(),
17284 output_gate: *output_gate,
17285 softplus_gate: softplus_gate
17286 .as_ref()
17287 .map(|(gate, per_head)| (gate, *per_head)),
17288 rope_scale: self.layer_rope_scale(li),
17289 bias: bias
17290 .as_ref()
17291 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17292 rms_eps: eps,
17293 norm_style: self.norm_style,
17294 pool: pool.as_deref(),
17295 v_head_dim: self.layer_v_dim(li),
17296 };
17297 attention::qwen_attention_nystrom(
17298 &self.ws.n1,
17299 wq,
17300 wk,
17301 wv,
17302 wo,
17303 &mut self.kv_cache.layers[li],
17304 &cfg,
17305 )
17306 }
17307 AttnKind::Full {
17308 wq,
17309 wk,
17310 wv,
17311 wo,
17312 q_norm,
17313 k_norm,
17314 output_gate,
17315 softplus_gate,
17316 bias,
17317 } => 'attn: {
17318 let dropin_reason =
17323 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17324 if let Some(reason) = dropin_reason {
17325 self.note_graph_decline("wgpu attn dropin", reason);
17326 }
17327 if graph_on
17328 && dropin_reason.is_none()
17329 && !*output_gate
17330 && softplus_gate.is_none()
17331 && self.attention_heads_per_layer.is_none()
17332 && bias.is_none()
17333 && task_mask.is_none()
17334 {
17335 let inv_freq_l = self.layer_inv_freq(li);
17336 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17337 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17338 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17339 wq.mapped_q1(),
17340 wk.mapped_q1(),
17341 wv.mapped_q1(),
17342 wo.mapped_q1(),
17343 ) {
17344 let gm = gm.clone();
17345 let mut out = vec![0f32; hs];
17346 let cache = &self.kv_cache.layers[li];
17347 if crate::gpu::attn_dropin(
17348 &gm,
17349 self.graph_kv_id,
17350 li,
17351 &self.ws.n1,
17352 qi,
17353 ki,
17354 vi,
17355 oi,
17356 q_norm.as_deref(),
17357 k_norm.as_deref(),
17358 self.qk_norm_after_rope,
17359 &inv_freq_l,
17360 nh,
17361 nkv_l,
17362 hd_l,
17363 rd_l,
17364 hs,
17365 position,
17366 self.kv_cache.max_seq_len,
17367 gemma,
17368 eps as f32,
17369 cache.k_heads(),
17370 cache.v_heads(),
17371 &mut out,
17372 ) {
17373 break 'attn out;
17374 }
17375 }
17376 }
17377 let masked = task_mask
17378 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17379 .unwrap_or(false);
17380 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17381 let plain = self.layer_attn_plain(li);
17384 match (masked, f32_view) {
17385 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17388 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17389 attention::multi_head_attention(
17390 &self.ws.n1,
17391 q,
17392 k,
17393 v,
17394 o,
17395 &mut self.kv_cache.layers[li],
17396 self.num_heads,
17397 self.num_kv_heads,
17398 self.head_dim,
17399 self.hidden_size,
17400 position,
17401 &active_heads,
17402 &self.inv_freq,
17403 )
17404 }
17405 (masked, _) => {
17406 if masked {
17407 tracing::warn!(
17408 "layer {li}: head mask on quantized weights or on a \
17409 window/sink/per-layer-geometry layer not supported \
17410 yet — executing dense"
17411 );
17412 }
17413 let inv_freq_l = self.layer_inv_freq(li);
17414 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17415 let cfg = QwenAttnCfg {
17416 num_heads: self.layer_num_heads(li),
17417 num_kv_heads: nkv_l,
17418 head_dim: hd_l,
17419 hidden_size: hs,
17420 position,
17421 inv_freq: &inv_freq_l,
17422 rotary_dim: rd_l,
17423 scale: self.attn_scale,
17424 softcap: self.attn_softcap,
17425 window: self.layer_window(li),
17426 v_norm: self.attn_v_norm,
17427 qk_norm_after_rope: self.qk_norm_after_rope,
17428 gate_sigmoid: self.proj_gate_sigmoid,
17429 q_norm: q_norm.as_deref(),
17430 k_norm: k_norm.as_deref(),
17431 output_gate: *output_gate,
17432 softplus_gate: softplus_gate
17433 .as_ref()
17434 .map(|(gate, per_head)| (gate, *per_head)),
17435 rope_scale: self.layer_rope_scale(li),
17436 bias: bias
17437 .as_ref()
17438 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17439 rms_eps: eps,
17440 norm_style: self.norm_style,
17441 pool: pool.as_deref(),
17442 v_head_dim: self.layer_v_dim(li),
17443 };
17444 attention::qwen_attention(
17445 &self.ws.n1,
17446 wq,
17447 wk,
17448 wv,
17449 wo,
17450 &mut self.kv_cache.layers[li],
17451 &cfg,
17452 )
17453 }
17454 }
17455 }
17456 };
17457 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17460 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17461 None => attn_out,
17462 };
17463 let lw = &self.weights.layers[self.phys_layer(li)];
17464 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17465 inference::add_rmsnorm_fused_into(
17466 &mut h,
17467 &attn_out,
17468 &lw.post_norm,
17469 self.rms_eps,
17470 self.norm_style,
17471 &mut self.ws.p1,
17472 );
17473 drop(prof);
17474 let mut attn_out = attn_out;
17475 attention::recycle_buf(&mut attn_out);
17476 let post_normed = &self.ws.p1;
17477
17478 let ffn_masked = task_mask
17479 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17480 .unwrap_or(false);
17481 let ffn_out = match (ffn_masked, &lw.ffn) {
17493 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17497 let row = task_mask
17498 .and_then(|tm| tm.ffn_masks.get(li))
17499 .map(|v| v.as_slice());
17500 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17501 }
17502 (true, FfnKind::Dense(d)) => {
17503 let tm = task_mask.unwrap();
17504 let alive = tm.ffn_active_count(li);
17505 let deep = alive * 2 <= self.intermediate_size;
17506 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17507 let active = tm.ffn_active_indices(li);
17508 sparse_ffn_quant(
17509 d,
17510 post_normed,
17511 &active,
17512 self.hidden_size,
17513 self.pool.as_deref(),
17514 )
17515 } else if deep
17516 && let (Some(g), Some(u), Some(dn)) = (
17517 d.gate_proj.as_f32(),
17518 d.up_proj.as_f32(),
17519 d.down_proj.as_f32(),
17520 )
17521 {
17522 let active = tm.ffn_active_indices(li);
17523 inference::sparse_ffn_forward(
17524 post_normed,
17525 g,
17526 u,
17527 dn,
17528 self.hidden_size,
17529 self.intermediate_size,
17530 &active,
17531 self.pool.as_deref(),
17532 )
17533 } else {
17534 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17535 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17536 }
17537 }
17538 (true, FfnKind::Moe(m)) => {
17539 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17543 ffn_forward(
17544 &lw.ffn,
17545 post_normed,
17546 self.pool.as_deref(),
17547 allowed.as_deref(),
17548 )
17549 }
17550 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17551 dm,
17552 post_normed,
17553 &h,
17554 self.rms_eps,
17555 self.norm_style,
17556 self.pool.as_deref(),
17557 ),
17558 (false, _) => match &lw.ffn {
17559 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17560 dm,
17561 post_normed,
17562 &h,
17563 self.rms_eps,
17564 self.norm_style,
17565 self.pool.as_deref(),
17566 ),
17567 FfnKind::Moe(m)
17568 if task_mask.is_none()
17569 && host_tail
17570 && li >= tail_start
17571 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17572 {
17573 moe_ffn_minicpm_tail(
17574 &mut self.minicpm_tail,
17575 li,
17576 self.num_layers,
17577 m,
17578 post_normed,
17579 self.pool.as_deref(),
17580 )
17581 }
17582 FfnKind::Moe(m)
17583 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17584 {
17585 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17586 }
17587 _ => {
17588 let allowed = match (&lw.ffn, task_mask) {
17589 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17590 _ => None,
17591 };
17592 ffn_forward(
17593 &lw.ffn,
17594 post_normed,
17595 self.pool.as_deref(),
17596 allowed.as_deref(),
17597 )
17598 }
17599 },
17600 };
17601 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17602 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17603 None => ffn_out,
17604 };
17605 for (i, &f) in ffn_out.iter().enumerate() {
17606 h[i] += f;
17607 }
17608 let mut ffn_out = ffn_out;
17609 attention::recycle_buf(&mut ffn_out);
17610
17611 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17613 for v in h.iter_mut() {
17614 *v *= sc;
17615 }
17616 }
17617 if self.layer_dump.is_some() {
17619 self.dump_layer_row(position, li, &h);
17620 }
17621
17622 if self.is_loop_end(li) && li + 1 < self.num_layers {
17625 h = inference::rms_norm(
17626 &h,
17627 &self.weights.final_norm,
17628 self.rms_eps,
17629 self.norm_style,
17630 );
17631 }
17632
17633 if self.dyn_phi_layer == Some(li) {
17637 self.update_dyn_phi(&h);
17638 }
17639 }
17640 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17642 crate::gpu::graph_race_record(false, t.elapsed());
17643 }
17644
17645 h
17646 }
17647
17648 fn update_dyn_phi(&mut self, h: &[f32]) {
17651 const A: f32 = 0.2;
17652 if self.dyn_phi_ema.len() != h.len() {
17653 self.dyn_phi_ema = vec![0.0; h.len()];
17654 self.dyn_phi_seen = 0;
17655 }
17656 if self.dyn_phi_seen == 0 {
17657 self.dyn_phi_ema.copy_from_slice(h);
17658 } else {
17659 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17660 *e = (1.0 - A) * *e + A * v;
17661 }
17662 }
17663 self.dyn_phi_seen += 1;
17664 }
17665
17666 pub fn dyn_phi(&self) -> &[f32] {
17668 &self.dyn_phi_ema
17669 }
17670
17671 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17673 self.dyn_phi_layer = layer;
17674 self.dyn_phi_ema.clear();
17675 self.dyn_phi_seen = 0;
17676 }
17677
17678 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17680 let Some(model) = &self.model else {
17681 return Vec::new();
17682 };
17683 model
17684 .header
17685 .skills
17686 .iter()
17687 .enumerate()
17688 .filter_map(|(i, sk)| {
17689 if sk.is_v2() {
17693 return None;
17694 }
17695 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17696 let sel = sk.selection.as_ref()?;
17697 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17698 })
17699 .collect()
17700 }
17701
17702 pub fn active_skill(&self) -> Option<usize> {
17704 self.dyn_active
17705 }
17706
17707 pub fn enable_dynamic_routing(&mut self) -> usize {
17712 use crate::swarm::{DynRouter, RoutableSkill};
17713 let Some(model) = self.model.clone() else {
17714 return 0;
17715 };
17716 if let Some(r) = &model.header.router {
17723 tracing::warn!(
17724 "dynamic routing disabled: this file declares router policy '{}' with \
17725 granularity \"{}\" — the request-level decision applies instead",
17726 r.policy,
17727 r.granularity
17728 );
17729 return 0;
17730 }
17731 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17737 || model.header.skills.iter().any(|s| s.is_v2())
17738 {
17739 tracing::warn!(
17740 "dynamic routing disabled: this file carries format-v2 skill records \
17741 (SKILLS_V2) — they route per request through a router policy only"
17742 );
17743 return 0;
17744 }
17745 if self.dyn_blend_loaded {
17748 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17749 return 0;
17750 }
17751 if let Some(a) = self.dyn_active {
17755 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17756 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17757 return 0;
17758 }
17759 }
17760 let hidden = self.hidden_size;
17761 let mut skills = Vec::new();
17762 for (idx, id, _phi) in self.dynamic_skills() {
17763 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17764 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17765 skills.push(rs);
17766 }
17767 }
17768 }
17769 if skills.is_empty() {
17770 return 0;
17771 }
17772 let phi = skills[0].phi_layer;
17774 if skills.iter().any(|s| s.phi_layer != phi) {
17775 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17776 }
17777 let n = skills.len();
17778 self.set_dyn_phi_layer(Some(phi));
17779 self.dyn_router = Some(DynRouter::new(skills));
17780 n
17781 }
17782
17783 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17785 self.dyn_router
17786 .as_ref()
17787 .map(|r| r.switches.clone())
17788 .unwrap_or_default()
17789 }
17790
17791 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17794 let _mimo_q8 = self
17795 .mimo_moe
17796 .is_on()
17797 .then(crate::qtensor::enter_full_gpu_q8_scope);
17798 let rows = self.weights.lm_head.rows();
17799 let mut logits = attention::take_buf(rows.min(self.vocab_size));
17800 let served = self.mimo_moe.is_on()
17804 && crate::gpu::mimo_q8_short_enabled()
17805 && rows == self.vocab_size
17806 && !self.weights.lm_head.has_prism_contract()
17807 && self
17808 .weights
17809 .lm_head
17810 .graph_weight()
17811 .is_some_and(|(model, idx, kind, _)| {
17812 kind == 7
17813 && crate::gpu::q82_short_rows(
17814 model,
17815 idx,
17816 hidden,
17817 1,
17818 rows,
17819 self.hidden_size,
17820 &mut logits,
17821 )
17822 });
17823 if !served {
17824 self.weights
17825 .lm_head
17826 .matvec(hidden, &mut logits, self.pool.as_deref());
17827 }
17828 logits.resize(self.vocab_size, 0.0);
17829 if let Some(m) = self.logit_multiplier {
17830 for l in logits.iter_mut() {
17831 *l *= m;
17832 }
17833 }
17834 if let Some(c) = self.final_softcap {
17835 softcap_in_place(&mut logits, c, self.pool.as_deref());
17836 }
17837 if let Some(cm) = self.head_clusters.as_ref() {
17838 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
17839 }
17840 logits
17841 }
17842
17843 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
17846 let h = hidden.len();
17847 let ncl = cm.len() / h.max(1);
17848 if ncl == 0 || logits.len() % ncl != 0 {
17849 return;
17850 }
17851 let cs = logits.len() / ncl;
17852 let mut lc = vec![0.0f32; ncl];
17854 for c in 0..ncl {
17855 let row = &cm[c * h..(c + 1) * h];
17856 let mut s = 0.0f32;
17857 for j in 0..h {
17858 s += row[j] * hidden[j];
17859 }
17860 lc[c] = s;
17861 }
17862 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17863 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
17864 for c in 0..ncl {
17865 let blk = &mut logits[c * cs..(c + 1) * cs];
17866 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
17867 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
17868 let add = lc[c] - lse - bl;
17869 for v in blk.iter_mut() {
17870 *v += add;
17871 }
17872 }
17873 }
17874
17875 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
17880 #[cfg(target_os = "macos")]
17881 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
17882 self.clear_sequence_state();
17883 crate::gpu::graph_race_begin_generation();
17887 if task_mask.is_none() {
17888 self.o1_begin();
17889 }
17890 let mut hidden = vec![0.0f32; self.hidden_size];
17891 for (pos, &id) in ids.iter().enumerate() {
17892 let emb = self.embed_single(id);
17893 hidden = self.forward_layers(&emb, pos, task_mask);
17894 }
17895 if let Err(err) = self.o1_seal_checked() {
17896 self.o1_fail(err);
17897 }
17898 if let Some(logits) = self.graph_logits.take() {
17901 return logits;
17902 }
17903 inference::rms_norm_into(
17904 &hidden,
17905 &self.weights.final_norm,
17906 self.rms_eps,
17907 self.norm_style,
17908 &mut self.ws.n1,
17909 );
17910 self.lm_head_forward(&self.ws.n1)
17911 }
17912}
17913
17914pub fn create_test_pipeline(
17916 hidden_size: usize,
17917 intermediate_size: usize,
17918 num_heads: usize,
17919 num_kv_heads: usize,
17920 head_dim: usize,
17921 num_layers: usize,
17922 vocab_size: usize,
17923) -> Pipeline {
17924 let synth = |n: usize, salt: usize| -> Vec<f32> {
17927 (0..n)
17928 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
17929 .collect()
17930 };
17931 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
17932 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
17933 };
17934 let layer_weights: Vec<LayerWeights> = (0..num_layers)
17935 .map(|li| LayerWeights {
17936 input_norm: vec![1.0; hidden_size],
17937 post_norm: vec![1.0; hidden_size],
17938 attn_out_norm: None,
17939 ffn_out_norm: None,
17940 layer_scale: None,
17941 ffn: FfnKind::Dense(DenseFfn {
17942 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
17943 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
17944 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
17945 act: Act::Silu,
17946 down_t: None,
17947 segs: Vec::new(),
17948 }),
17949 attn: AttnKind::Full {
17950 bias: None,
17951 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
17952 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
17953 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
17954 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
17955 q_norm: None,
17956 k_norm: None,
17957 output_gate: false,
17958 softplus_gate: None,
17959 },
17960 })
17961 .collect();
17962
17963 Pipeline::new(
17964 Tokenizer::byte_level(),
17965 PipelineWeights {
17966 embed_tokens: qt(vocab_size, hidden_size, 100),
17967 layers: layer_weights,
17968 lm_head: qt(vocab_size, hidden_size, 200),
17969 final_norm: vec![1.0; hidden_size],
17970 },
17971 hidden_size,
17972 intermediate_size,
17973 num_heads,
17974 num_kv_heads,
17975 head_dim,
17976 num_layers,
17977 num_layers, false, vocab_size,
17980 1e-6,
17981 10_000.0,
17982 NormStyle::Qwen,
17983 4096,
17984 SamplerConfig {
17985 seed: Some(42),
17986 ..Default::default()
17987 },
17988 )
17989}
17990
17991#[inline]
17996fn mask_bit(row: &[u8], j: usize) -> bool {
17997 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
17998}
17999
18000fn mask_gain() -> f32 {
18011 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18012 *G.get_or_init(|| {
18013 std::env::var("CMF_FFN_MASK_GAIN")
18014 .ok()
18015 .and_then(|v| v.parse().ok())
18016 .unwrap_or(1.0)
18017 })
18018}
18019
18020fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18021 let fill = meanfill().and_then(|(i, v)| {
18024 let li = crate::gpu::cur_layer();
18025 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18026 });
18027 for r in 0..rows {
18028 let base = r * inter;
18029 for (bi, &byte) in row.iter().enumerate() {
18030 if byte == 0xFF {
18031 continue;
18032 }
18033 let j0 = bi * 8;
18034 for bit in 0..8 {
18035 let j = j0 + bit;
18036 if j < inter && byte & (1 << bit) == 0 {
18037 g[base + j] = fill.map_or(0.0, |f| f[j]);
18038 }
18039 }
18040 }
18041 }
18042 let gain = mask_gain();
18043 if gain != 1.0 {
18044 for v in g[..rows * inter].iter_mut() {
18045 *v *= gain;
18046 }
18047 }
18048}
18049
18050#[inline]
18052fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18053 row.is_none_or(|r| mask_bit(r, i))
18054}
18055
18056fn all_bits_on(row: &[u8], n: usize) -> bool {
18059 (0..n).all(|i| mask_bit(row, i))
18060}
18061
18062fn tube_topk() -> usize {
18070 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18071 *K.get_or_init(|| {
18072 std::env::var("CMF_TUBE_TOPK")
18073 .ok()
18074 .and_then(|v| v.parse().ok())
18075 .unwrap_or(0)
18076 })
18077}
18078
18079fn tube_score_oracle() -> bool {
18080 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18081 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18082}
18083
18084fn tube_ffn_routed(
18091 d: &DenseFfn,
18092 xs: &[f32],
18093 b: usize,
18094 pool: Option<&Pool>,
18095 mask_row: Option<&[u8]>,
18096 k: usize,
18097) -> Vec<f32> {
18098 let hidden = d.down_proj.rows();
18099 let core = d.gate_proj.rows();
18100 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18101 let mut out = match (b, core_full, mask_row) {
18102 (1, true, _) => dense_ffn(d, xs, pool),
18103 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18104 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18105 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18106 };
18107 let cand: Vec<usize> = (0..d.segs.len())
18108 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18109 .collect();
18110 if cand.is_empty() {
18111 return out;
18112 }
18113 let oracle = tube_score_oracle();
18117 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18118 let mut scores = vec![0f32; b * cand.len()];
18119 for (ci, &i) in cand.iter().enumerate() {
18120 let seg = &d.segs[i];
18121 let w = seg.width;
18122 let mut g = vec![0.0f32; b * w];
18123 if b == 1 {
18124 seg.gate.matvec(xs, &mut g, pool);
18125 } else {
18126 seg.gate.matmat(xs, b, &mut g, pool);
18127 }
18128 for v in g.iter_mut() {
18129 *v = Act::Silu.combine(*v, 1.0);
18130 }
18131 if !oracle {
18132 for t in 0..b {
18133 scores[t * cand.len() + ci] =
18134 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18135 }
18136 }
18137 if oracle || b > 1 {
18138 let mut u = vec![0.0f32; b * w];
18139 if b == 1 {
18140 seg.up.matvec(xs, &mut u, pool);
18141 } else {
18142 seg.up.matmat(xs, b, &mut u, pool);
18143 }
18144 for (a, &v) in g.iter_mut().zip(u.iter()) {
18145 *a *= v;
18146 }
18147 if oracle {
18148 for t in 0..b {
18149 scores[t * cand.len() + ci] =
18150 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18151 }
18152 }
18153 }
18154 acts.push(g);
18155 }
18156 let keep = k.min(cand.len());
18158 let mut scratch: Vec<f32> = Vec::new();
18159 for t in 0..b {
18160 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18161 .map(|ci| (scores[t * cand.len() + ci], ci))
18162 .collect();
18163 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18164 let mut alive = vec![false; cand.len()];
18165 for &(_, ci) in sc.iter().take(keep) {
18166 alive[ci] = true;
18167 }
18168 if b > 1 {
18169 for (ci, a) in acts.iter_mut().enumerate() {
18170 if !alive[ci] {
18171 let w = d.segs[cand[ci]].width;
18172 a[t * w..(t + 1) * w].fill(0.0);
18173 }
18174 }
18175 } else {
18176 for (ci, &i) in cand.iter().enumerate() {
18180 if !alive[ci] {
18181 continue;
18182 }
18183 let seg = &d.segs[i];
18184 let w = seg.width;
18185 let g = &mut acts[ci];
18186 if !tube_score_oracle() {
18187 scratch.clear();
18188 scratch.resize(w, 0.0);
18189 seg.up.matvec(xs, &mut scratch, pool);
18190 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18191 *a *= v;
18192 }
18193 }
18194 let mut acc = vec![0.0f32; hidden];
18195 seg.down.matvec(g, &mut acc, pool);
18196 for (o, a) in out.iter_mut().zip(&acc) {
18197 *o += *a;
18198 }
18199 }
18200 }
18201 }
18202 if b > 1 {
18203 for (ci, &i) in cand.iter().enumerate() {
18204 let seg = &d.segs[i];
18205 let mut acc = vec![0.0f32; b * hidden];
18206 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18207 for (o, a) in out.iter_mut().zip(&acc) {
18208 *o += *a;
18209 }
18210 }
18211 }
18212 out
18213}
18214
18215fn tube_ffn(
18221 d: &DenseFfn,
18222 xs: &[f32],
18223 b: usize,
18224 pool: Option<&Pool>,
18225 mask_row: Option<&[u8]>,
18226) -> Vec<f32> {
18227 if tube_topk() > 0 {
18228 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18229 }
18230 let hidden = d.down_proj.rows();
18231 let core = d.gate_proj.rows();
18232 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18233 let mut out = match (b, core_full, mask_row) {
18234 (1, true, _) => dense_ffn(d, xs, pool),
18235 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18236 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18237 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18238 };
18239 TUBE_SCRATCH.with(|sc| {
18240 let mut sc = sc.borrow_mut();
18241 let [g, u, acc] = &mut *sc;
18242 for seg in &d.segs {
18243 if !tube_bit(mask_row, seg.start) {
18244 continue;
18245 }
18246 let w = seg.width;
18247 g.resize(b * w, 0.0);
18248 if b == 1
18249 && d.act == Act::Silu
18250 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18251 {
18252 } else {
18254 u.resize(b * w, 0.0);
18255 if b == 1 {
18256 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18257 } else {
18258 seg.gate.matmat(xs, b, g, pool);
18259 seg.up.matmat(xs, b, u, pool);
18260 }
18261 for i in 0..b * w {
18262 g[i] = d.act.combine(g[i], u[i]);
18263 }
18264 }
18265 acc.resize(b * hidden, 0.0);
18266 acc.fill(0.0);
18267 if b == 1 {
18268 seg.down.matvec(g, acc, pool);
18269 } else {
18270 seg.down.matmat(g, b, acc, pool);
18271 }
18272 for (o, a) in out.iter_mut().zip(acc.iter()) {
18273 *o += *a;
18274 }
18275 }
18276 out
18277 })
18278}
18279
18280thread_local! {
18281 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18285 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18286}
18287
18288static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18291 std::sync::atomic::AtomicU64::new(0),
18292 std::sync::atomic::AtomicU64::new(0),
18293];
18294
18295fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18299 #[cfg(feature = "gpu")]
18300 {
18301 use std::sync::atomic::Ordering::Relaxed;
18302 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18303 (
18304 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18305 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18306 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18307 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18308 )
18309 }
18310 #[cfg(not(feature = "gpu"))]
18311 (0.0, 0.0, 0.0, 0)
18312}
18313
18314fn wgpu_mirror_counters() -> [u64; 6] {
18317 #[cfg(feature = "gpu")]
18318 {
18319 crate::gpu_wgpu::MIRROR_EVENTS
18320 .each_ref()
18321 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18322 }
18323 #[cfg(not(feature = "gpu"))]
18324 [0; 6]
18325}
18326
18327fn prefill_prof_on() -> bool {
18328 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18329 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18330}
18331
18332pub(crate) fn accumulate_dense_ffn_activation_mass(
18341 d: &DenseFfn,
18342 xs: &[f32],
18343 b: usize,
18344 pool: Option<&Pool>,
18345 mass: &mut [f64],
18346 squared_mass: &mut [f64],
18347) -> Result<(), String> {
18348 let inter = d.gate_proj.rows();
18349 let hidden = d.gate_proj.cols();
18350 if mass.len() != inter || squared_mass.len() != inter {
18351 return Err(format!(
18352 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18353 mass.len(),
18354 squared_mass.len()
18355 ));
18356 }
18357 if d.up_proj.rows() != inter
18358 || d.up_proj.cols() != hidden
18359 || d.down_proj.cols() != inter
18360 || d.down_proj.rows() != hidden
18361 {
18362 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18363 }
18364 if !d.segs.is_empty() {
18365 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18366 }
18367 if xs.len() != b.saturating_mul(hidden) {
18368 return Err(format!(
18369 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18370 xs.len()
18371 ));
18372 }
18373 if b == 0 {
18374 return Ok(());
18375 }
18376 let mut gate = vec![0.0f32; b * inter];
18377 let mut up = vec![0.0f32; b * inter];
18378 d.gate_proj.matmat(xs, b, &mut gate, pool);
18379 d.up_proj.matmat(xs, b, &mut up, pool);
18380 for row in 0..b {
18381 for i in 0..inter {
18382 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18383 let a = a as f64;
18384 mass[i] += a.abs();
18385 squared_mass[i] += a * a;
18386 }
18387 }
18388 Ok(())
18389}
18390
18391fn dense_ffn_batch(
18392 d: &DenseFfn,
18393 xs: &[f32],
18394 b: usize,
18395 pool: Option<&Pool>,
18396 mask_row: Option<&[u8]>,
18397) -> Vec<f32> {
18398 let inter = d.gate_proj.rows();
18399 let hidden = d.down_proj.rows();
18400 let fused_act = d.act.wgpu_graph_act();
18412 if mask_row.is_none()
18413 && fused_act.is_some()
18414 && b >= 32
18415 && crate::gpu::enabled_here()
18416 && !crate::gpu::mm_killed()
18417 && refit_dir().is_none()
18422 && !ffn_probe_active()
18427 && !crate::gptq_capture::capturing()
18431 {
18432 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18433 d.gate_proj.mapped_q4t(),
18434 d.up_proj.mapped_q4t(),
18435 d.down_proj.mapped_q4t(),
18436 ) {
18437 let mut out = vec![0.0f32; b * hidden];
18438 let act = fused_act.expect("checked above");
18439 if crate::gpu::q4_ffn_act(
18440 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18441 ) {
18442 return out;
18443 }
18444 }
18445 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18450 d.gate_proj.mapped_q4tp(),
18451 d.up_proj.mapped_q4tp(),
18452 d.down_proj.mapped_q4tp(),
18453 ) {
18454 let mut out = vec![0.0f32; b * hidden];
18455 let act = fused_act.expect("checked above");
18456 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18457 {
18458 return out;
18459 }
18460 }
18461 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18467 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18468 d.gate_proj.mapped_device_gemm(),
18469 d.up_proj.mapped_device_gemm(),
18470 d.down_proj.mapped_device_gemm(),
18471 ) {
18472 let mut out = vec![0.0f32; b * hidden];
18473 let act = fused_act.expect("checked above");
18474 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18475 return out;
18476 }
18477 }
18478 }
18479 let mut g = vec![0.0f32; b * inter];
18480 d.gate_proj.matmat(xs, b, &mut g, pool);
18481 let mut u = vec![0.0f32; b * inter];
18482 d.up_proj.matmat(xs, b, &mut u, pool);
18483 if gate_topk() > 0 && d.act == Act::Silu {
18484 for t in 0..b {
18485 let row = &mut g[t * inter..(t + 1) * inter];
18486 for v in row.iter_mut() {
18487 *v = Act::Silu.combine(*v, 1.0);
18488 }
18489 keep_top_k(row, gate_topk());
18490 }
18491 for i in 0..b * inter {
18492 g[i] *= u[i];
18493 }
18494 } else {
18495 for i in 0..b * inter {
18496 g[i] = d.act.combine(g[i], u[i]);
18497 }
18498 }
18499 if let Some(row) = mask_row {
18500 zero_masked_cols(&mut g, b, inter, row);
18501 }
18502 if oracle_topk() > 0 {
18503 for t in 0..b {
18504 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18505 }
18506 }
18507 let mut out = vec![0.0f32; b * hidden];
18508 d.down_proj.matmat(&g, b, &mut out, pool);
18509 if refit_dir().is_some() {
18510 let li = crate::gpu::cur_layer();
18511 if li >= 0 {
18512 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18513 }
18514 }
18515 FFN_PROBE.with(|pr| {
18519 if let Some(acc) = pr.borrow_mut().as_mut() {
18520 let li = crate::gpu::cur_layer();
18521 if li < 0 {
18522 return;
18523 }
18524 let Some(row) = acc.get_mut(li as usize) else {
18525 return;
18526 };
18527 let sq = probe_sq();
18528 for t in 0..b {
18529 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18530 *a += if sq {
18531 (v as f64) * (v as f64)
18532 } else {
18533 (v as f64).abs()
18534 };
18535 }
18536 }
18537 }
18538 });
18539 out
18540}
18541
18542fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18547 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18548 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18549 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18550 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18551 if (!on && !dump) || b == 0 {
18552 return;
18553 }
18554 let hidden = xs.len() / b;
18555 if on {
18556 let mut acc = m.act_sq.borrow_mut();
18557 if acc.len() < hidden {
18558 acc.resize(hidden, 0.0);
18559 }
18560 for t in 0..b {
18561 let row = &xs[t * hidden..(t + 1) * hidden];
18562 for (a, &v) in acc.iter_mut().zip(row) {
18563 *a += (v as f64) * (v as f64);
18564 }
18565 }
18566 }
18567 if dump {
18568 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18571 .ok()
18572 .and_then(|v| v.parse().ok())
18573 .unwrap_or(4096);
18574 let mut rows = m.act_rows.borrow_mut();
18575 if rows.len() < cap * hidden {
18576 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18577 rows.extend_from_slice(&xs[..take * hidden]);
18578 }
18579 }
18580}
18581
18582#[derive(Clone, Copy)]
18585struct SendVecs(*mut Vec<f32>);
18586unsafe impl Send for SendVecs {}
18587unsafe impl Sync for SendVecs {}
18588impl SendVecs {
18589 #[inline]
18590 fn at(self, i: usize) -> *mut Vec<f32> {
18591 unsafe { self.0.add(i) }
18592 }
18593}
18594
18595fn moe_ffn_batch(
18596 m: &MoeFfn,
18597 xs: &[f32],
18598 b: usize,
18599 hidden: usize,
18600 pool: Option<&Pool>,
18601 allowed: Option<&[bool]>,
18602) -> Vec<f32> {
18603 accumulate_act(m, xs, b);
18604 let ne = m.experts.len();
18605 let mut logits = vec![0.0f32; b * ne];
18606 match &m.resonance {
18607 Some(r) => {
18608 let hdim = xs.len() / b.max(1);
18609 for bi in 0..b {
18610 r.scores(
18611 &xs[bi * hdim..(bi + 1) * hdim],
18612 &mut logits[bi * ne..(bi + 1) * ne],
18613 );
18614 }
18615 }
18616 None => m.router.matmat(xs, b, &mut logits, pool),
18617 }
18618
18619 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18622 {
18623 let mut st = m.stats.borrow_mut();
18624 if st.len() < ne {
18625 st.resize(ne, 0);
18626 }
18627 for bi in 0..b {
18628 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18629 for &e in &idx {
18630 st[e] += 1;
18631 assign[e].push((bi, p[e] / wsum));
18632 }
18633 }
18634 }
18635
18636 let mut out = vec![0.0f32; b * hidden];
18637 let cols = m.experts[0].gate_proj.cols();
18638 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18639 let sb = list.len();
18640 let mut sub = vec![0.0f32; sb * cols];
18641 for (k, &(bi, _)) in list.iter().enumerate() {
18642 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18643 }
18644 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18645 for (k, &(bi, w)) in list.iter().enumerate() {
18646 for i in 0..hidden {
18647 out[bi * hidden + i] += w * eo[k * hidden + i];
18648 }
18649 }
18650 };
18651 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18657 if pool.is_some() && active.len() >= 8 {
18658 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18659 {
18660 let panel_ptr = SendVecs(panels.as_mut_ptr());
18661 let experts = &m.experts;
18664 let (active_r, assign_r) = (&active, &assign);
18665 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18666 let run = |start: usize, end: usize| {
18667 let _cpu_scope = inherit_cpu();
18668 #[cfg(target_os = "macos")]
18676 let _metal_cpu = crate::gpu::enter_cpu_scope();
18677 for ai in start..end {
18678 let e = active_r[ai];
18679 let list = &assign_r[e];
18680 let sb = list.len();
18681 let mut sub = vec![0.0f32; sb * cols];
18682 for (k, &(bi, _)) in list.iter().enumerate() {
18683 sub[k * cols..(k + 1) * cols]
18684 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18685 }
18686 unsafe {
18688 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18689 }
18690 }
18691 };
18692 match pool {
18693 Some(p) => p.run_rows(active.len(), &run),
18694 None => run(0, active.len()),
18695 }
18696 }
18697 for (ai, &e) in active.iter().enumerate() {
18698 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18699 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18700 for i in 0..hidden {
18701 out[bi * hidden + i] += w * eo[i];
18702 }
18703 }
18704 }
18705 } else {
18706 for &e in &active {
18707 run_expert(&m.experts[e], &assign[e], &mut out);
18708 }
18709 }
18710 if let Some((se, gate)) = &m.shared {
18711 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18712 let mut gl = vec![0.0f32; b];
18713 gate.matmat(xs, b, &mut gl, pool);
18714 (0..b)
18715 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18716 .collect()
18717 } else {
18718 (0..b).map(|bi| (bi, 1.0)).collect()
18719 };
18720 run_expert(se, &all, &mut out);
18721 }
18722 out
18723}
18724
18725fn moe_ffn_rows_exact(
18737 m: &MoeFfn,
18738 xs: &[f32],
18739 b: usize,
18740 hidden: usize,
18741 pool: Option<&Pool>,
18742) -> Vec<f32> {
18743 let mut out = vec![0.0f32; b * hidden];
18744 let per_row = |out: &mut [f32]| {
18745 for r in 0..b {
18746 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18747 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18748 }
18749 };
18750 let covered = !crate::gpu::enabled_here()
18751 && moe_batch_enabled()
18752 && m.shared.is_none()
18753 && m.resonance.is_none()
18754 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18755 && m.experts.iter().all(|d| d.act == Act::Silu);
18756 if !covered {
18757 per_row(&mut out);
18758 return out;
18759 }
18760 let ne = m.experts.len();
18761 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18763 for r in 0..b {
18764 let x = &xs[r * hidden..(r + 1) * hidden];
18765 accumulate_act(m, x, 1);
18766 let mut logits = vec![0.0f32; ne];
18767 m.router.matvec(x, &mut logits, pool);
18768 let (idx, p, wsum) = moe_route(&logits, m, None);
18769 {
18770 let mut st = m.stats.borrow_mut();
18771 if st.len() < ne {
18772 st.resize(ne, 0);
18773 }
18774 for &e in &idx {
18775 st[e] += 1;
18776 }
18777 }
18778 let w: Vec<f32> = idx
18779 .iter()
18780 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18781 .collect();
18782 routes.push((idx, w));
18783 }
18784 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18785 per_row(&mut out);
18786 return out;
18787 }
18788 let mut experts: Vec<usize> = Vec::new();
18790 let mut groups: Vec<Vec<usize>> = Vec::new();
18791 for (r, (idx, _)) in routes.iter().enumerate() {
18792 for &e in idx {
18793 match experts.iter().position(|&x| x == e) {
18794 Some(g) => groups[g].push(r),
18795 None => {
18796 experts.push(e);
18797 groups.push(vec![r]);
18798 }
18799 }
18800 }
18801 }
18802 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
18803 let inter = m.experts[experts[0]].gate_proj.rows();
18804 let pairs: Vec<(&QTensor, &QTensor)> = experts
18805 .iter()
18806 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
18807 .collect();
18808 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
18809 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
18810 per_row(&mut out);
18811 return out;
18812 }
18813 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
18814 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
18815 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
18816 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
18817 per_row(&mut out);
18818 return out;
18819 }
18820 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
18822 let mut p = 0usize;
18823 for (g, &e) in experts.iter().enumerate() {
18824 for &r in &groups[g] {
18825 slot.insert((r, e), p);
18826 p += 1;
18827 }
18828 }
18829 for (r, (idx, w)) in routes.iter().enumerate() {
18830 let terms: Vec<(&[f32], f32)> = idx
18831 .iter()
18832 .zip(w)
18833 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
18834 .collect();
18835 let row = &mut out[r * hidden..(r + 1) * hidden];
18836 for (i, dst) in row.iter_mut().enumerate() {
18837 let mut acc = 0f32;
18839 for (d, we) in &terms {
18840 acc += we * d[i];
18841 }
18842 *dst = acc;
18843 }
18844 }
18845 out
18846}
18847
18848pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
18852 match pool {
18853 Some(p) if v.len() >= 1 << 15 => {
18854 let base = v.as_mut_ptr() as usize;
18855 p.run_rows(v.len(), &|s, e| {
18856 let sl =
18859 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
18860 for x in sl {
18861 *x = c * (*x / c).tanh();
18862 }
18863 });
18864 }
18865 _ => {
18866 for x in v.iter_mut() {
18867 *x = c * (*x / c).tanh();
18868 }
18869 }
18870 }
18871}
18872
18873thread_local! {
18874 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
18878 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
18879}
18880
18881fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18883 if gate_topk() > 0
18886 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
18887 {
18888 return out;
18889 }
18890 let prism_body = d.gate_proj.has_prism_contract()
18906 || d.up_proj.has_prism_contract()
18907 || d.down_proj.has_prism_contract();
18908 if !prism_body
18909 && crate::gpu::enabled_here()
18910 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
18911 {
18912 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
18913 crate::gpu::ProbeArm::Gpu
18914 } else {
18915 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
18916 };
18917 match arm {
18918 crate::gpu::ProbeArm::Gpu => {
18919 let t0 = std::time::Instant::now();
18920 if let Some(out) = dense_ffn_gpu(d, x, pool) {
18921 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18922 return out;
18923 }
18924 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
18928 }
18929 crate::gpu::ProbeArm::CpuTimed => {
18930 let t0 = std::time::Instant::now();
18931 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18932 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18933 return out;
18934 }
18935 crate::gpu::ProbeArm::Cpu => {
18936 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
18937 }
18938 }
18939 }
18940 dense_ffn_cpu(d, x, pool)
18941}
18942
18943fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
18945 let inter = d.gate_proj.rows();
18946 FFN_SCRATCH.with(|s| {
18947 let mut s = s.borrow_mut();
18948 let [g, u, ..] = &mut *s;
18949 g.resize(inter, 0.0);
18950 if gate_topk() > 0 {
18953 u.resize(inter, 0.0);
18957 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18958 for i in 0..inter {
18959 g[i] = Act::Silu.combine(g[i], 1.0);
18960 }
18961 keep_top_k(g, gate_topk());
18962 for i in 0..inter {
18963 g[i] *= u[i];
18964 }
18965 } else if d.act == Act::Silu && {
18966 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18967 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
18968 } {
18969 } else {
18971 u.resize(inter, 0.0);
18972 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
18974 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18975 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
18976 for i in 0..inter {
18977 g[i] = d.act.combine(g[i], u[i]);
18978 }
18979 }
18980 FFN_PROBE.with(|pr| {
18988 if let Some(acc) = pr.borrow_mut().as_mut() {
18989 let li = crate::gpu::cur_layer();
18990 if li >= 0 {
18991 if let Some(row) = acc.get_mut(li as usize) {
18992 match probe_topk() {
18993 0 if probe_sq() => {
18994 for (a, &v) in row.iter_mut().zip(g.iter()) {
18995 *a += (v as f64) * (v as f64);
18996 }
18997 }
18998 0 if probe_signed() => {
18999 for (a, &v) in row.iter_mut().zip(g.iter()) {
19000 *a += v as f64;
19001 }
19002 }
19003 0 => {
19004 for (a, &v) in row.iter_mut().zip(g.iter()) {
19005 *a += (v as f64).abs();
19006 }
19007 }
19008 k => {
19009 let n = g.len();
19010 let k = k.min(n);
19011 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19012 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19013 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19014 });
19015 let thr = *kth;
19016 for (a, &v) in row.iter_mut().zip(g.iter()) {
19017 if v.abs() >= thr {
19018 *a += 1.0;
19019 }
19020 }
19021 }
19022 }
19023 }
19024 }
19025 }
19026 });
19027 if oracle_topk() > 0 {
19028 keep_top_k(g, oracle_topk());
19029 }
19030 {
19031 let li = crate::gpu::cur_layer();
19032 if li >= 0 {
19033 adump_row(li as usize, g);
19034 }
19035 }
19036 let mut out = attention::take_buf(d.down_proj.rows());
19037 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19038 d.down_proj.matvec(g, &mut out, pool);
19039 out
19040 })
19041}
19042
19043pub struct RefitAcc {
19056 pub support: Vec<u32>,
19057 pub gss: Vec<f32>,
19058 pub ya: Vec<f32>,
19059 pub hidden: usize,
19060 pub tokens: u64,
19061 pub buf_g: Vec<f32>,
19067 pub buf_o: Vec<f32>,
19068 pub buf_t: usize,
19069}
19070
19071type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19075
19076static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19077 std::sync::OnceLock::new();
19078
19079fn ffn_probe_active() -> bool {
19082 FFN_PROBE.with(|p| p.borrow().is_some())
19083}
19084
19085fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19086 REFIT
19087 .get_or_init(|| {
19088 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19089 (
19090 d,
19091 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19092 )
19093 })
19094 })
19095 .as_ref()
19096}
19097
19098fn refit_accumulate(
19100 li: usize,
19101 g: &[f32],
19102 b: usize,
19103 inter: usize,
19104 out: &[f32],
19105 hidden: usize,
19106 pool: Option<&Pool>,
19107) {
19108 let Some((dir, map)) = refit_dir() else {
19109 return;
19110 };
19111 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19112 let (from, to) = *SPAN.get_or_init(|| {
19113 let g = |k: &str, d: usize| {
19114 std::env::var(k)
19115 .ok()
19116 .and_then(|v| v.parse().ok())
19117 .unwrap_or(d)
19118 };
19119 (
19120 g("CMF_FFN_REFIT_FROM", 0),
19121 g("CMF_FFN_REFIT_TO", usize::MAX),
19122 )
19123 });
19124 if li < from || li > to {
19125 return;
19126 }
19127 let mut guard = map.lock().unwrap();
19128 let (map, shared) = &mut *guard;
19129 let acc = match map.entry(li) {
19130 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19131 std::collections::hash_map::Entry::Vacant(e) => {
19132 let path = format!("{dir}/support.{li}.u32");
19133 let Ok(bytes) = std::fs::read(&path) else {
19134 eprintln!("refit: no {path} — layer {li} skipped");
19135 return;
19136 };
19137 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19138 let support: Vec<u32> = bytes[4..4 + n * 4]
19139 .chunks_exact(4)
19140 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19141 .collect();
19142 eprintln!(
19143 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19144 (n * n + hidden * n) as f64 * 4.0 / 1e6
19145 );
19146 e.insert(RefitAcc {
19147 gss: vec![0.0; n * n],
19148 ya: vec![0.0; hidden * n],
19149 buf_g: Vec::new(),
19150 buf_o: Vec::new(),
19151 buf_t: 0,
19152 support,
19153 hidden,
19154 tokens: 0,
19155 })
19156 }
19157 };
19158 let ns = acc.support.len();
19159 let cap = refit_batch();
19161 if acc.buf_g.is_empty() {
19162 acc.buf_g = vec![0.0; ns * cap];
19163 acc.buf_o = vec![0.0; hidden * cap];
19164 }
19165 let take = b.min(cap - acc.buf_t);
19166 for t in 0..take {
19167 let col = acc.buf_t + t;
19168 for (j, &n) in acc.support.iter().enumerate() {
19169 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19170 }
19171 for h in 0..hidden {
19172 acc.buf_o[h * cap + col] = out[t * hidden + h];
19173 }
19174 }
19175 acc.buf_t += take;
19176 acc.tokens += take as u64;
19177 if acc.buf_t < cap {
19178 return;
19179 }
19180 let bt = acc.buf_t;
19181 acc.buf_t = 0;
19182 let RefitAcc {
19192 gss,
19193 ya,
19194 buf_g,
19195 buf_o,
19196 ..
19197 } = acc;
19198 let need = (ns * ns).max(hidden * ns);
19199 if shared.len() < need {
19200 shared.resize(need, 0.0);
19201 }
19202 let scratch = &mut shared[..];
19203 let _ = bt;
19204 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19205 add_into(gss, &scratch[..ns * ns], pool);
19206 if crate::gpu::gemm_nt_f32_transient(
19207 buf_o,
19208 buf_g,
19209 &mut scratch[..hidden * ns],
19210 hidden,
19211 cap,
19212 ns,
19213 ) {
19214 add_into(ya, &scratch[..hidden * ns], pool);
19215 } else {
19216 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19217 }
19218 } else {
19219 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19220 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19221 }
19222 }
19226
19227fn refit_batch() -> usize {
19229 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19230 *B.get_or_init(|| {
19231 std::env::var("CMF_FFN_REFIT_BATCH")
19232 .ok()
19233 .and_then(|v| v.parse().ok())
19234 .unwrap_or(4096)
19235 })
19236}
19237
19238fn accum_outer_t(
19241 c: &mut [f32],
19242 m: usize,
19243 n: usize,
19244 b: usize,
19245 left: &[f32],
19246 right: &[f32],
19247 pool: Option<&Pool>,
19248) {
19249 let ptr = SendMut(c.as_mut_ptr());
19250 let body = |i: usize| {
19251 let ptr = &ptr;
19252 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19253 for t in 0..b {
19254 let a = left[i * b + t];
19255 if a == 0.0 {
19256 continue;
19257 }
19258 for (j, o) in row.iter_mut().enumerate() {
19259 *o += a * right[j * b + t];
19260 }
19261 }
19262 };
19263 match pool {
19264 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19265 for i in s..e {
19266 body(i);
19267 }
19268 }),
19269 _ => {
19270 for i in 0..m {
19271 body(i);
19272 }
19273 }
19274 }
19275}
19276
19277fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19280 let n = dst.len().min(src.len());
19281 match pool {
19282 Some(p) if n >= 1 << 16 => {
19283 let ptr = SendMut(dst.as_mut_ptr());
19284 let f = |s: usize, e: usize| {
19285 let ptr = &ptr;
19286 for blk in s..e {
19287 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19288 for i in a..b {
19289 unsafe { *ptr.0.add(i) += src[i] };
19290 }
19291 }
19292 };
19293 p.run_rows(n.div_ceil(4096), &f);
19294 }
19295 _ => {
19296 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19297 *d += *v;
19298 }
19299 }
19300 }
19301}
19302
19303fn accum_outer(
19308 c: &mut [f32],
19309 m: usize,
19310 n: usize,
19311 b: usize,
19312 left: &[f32],
19313 right: &[f32],
19314 pool: Option<&Pool>,
19315) {
19316 const TILE: usize = 32;
19317 let tiles = m.div_ceil(TILE);
19318 let cp = SendMut(c.as_mut_ptr());
19319 let body = |ti: usize| {
19320 let cp = &cp;
19321 let i0 = ti * TILE;
19322 let i1 = (i0 + TILE).min(m);
19323 for t in 0..b {
19324 let r = &right[t * n..t * n + n];
19325 for i in i0..i1 {
19326 let a = left[i * b + t];
19327 if a == 0.0 {
19328 continue;
19329 }
19330 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19332 for (o, v) in row.iter_mut().zip(r) {
19333 *o += a * *v;
19334 }
19335 }
19336 }
19337 };
19338 match pool {
19339 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19340 for ti in s..e {
19341 body(ti);
19342 }
19343 }),
19344 _ => {
19345 for ti in 0..tiles {
19346 body(ti);
19347 }
19348 }
19349 }
19350}
19351
19352pub fn refit_flush() -> usize {
19354 let Some((dir, map)) = refit_dir() else {
19355 return 0;
19356 };
19357 let guard = map.lock().unwrap();
19358 let mut n = 0;
19359 for (li, acc) in guard.0.iter() {
19360 let w = |name: &str, v: &[f32]| {
19363 let path = format!("{dir}/{name}.{li}.f32");
19364 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19365 match std::fs::write(&path, &bytes) {
19366 Ok(()) => {}
19367 Err(e) => eprintln!(
19368 "refit: FAILED to write {path} ({} MB): {e}",
19369 bytes.len() / 1_000_000
19370 ),
19371 }
19372 };
19373 w("gss", &acc.gss);
19374 w("ya", &acc.ya);
19375 println!(
19376 "refit L{li}: {} support, {} tokens, hidden {}",
19377 acc.support.len(),
19378 acc.tokens,
19379 acc.hidden
19380 );
19381 n += 1;
19382 }
19383 n
19384}
19385
19386fn adump_row(li: usize, g: &[f32]) {
19391 use std::io::Write as _;
19392 static FILES: std::sync::OnceLock<
19393 Option<(
19394 String,
19395 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19396 )>,
19397 > = std::sync::OnceLock::new();
19398 let Some((prefix, map)) = FILES
19399 .get_or_init(|| {
19400 std::env::var("CMF_FFN_ADUMP")
19401 .ok()
19402 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19403 })
19404 .as_ref()
19405 else {
19406 return;
19407 };
19408 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19411 let (from, to) = *SPAN.get_or_init(|| {
19412 let g = |k: &str, d: usize| {
19413 std::env::var(k)
19414 .ok()
19415 .and_then(|v| v.parse().ok())
19416 .unwrap_or(d)
19417 };
19418 (
19419 g("CMF_FFN_ADUMP_FROM", 0),
19420 g("CMF_FFN_ADUMP_TO", usize::MAX),
19421 )
19422 });
19423 if li < from || li > to {
19424 return;
19425 }
19426 let mut map = map.lock().unwrap();
19427 let f = map.entry(li).or_insert_with(|| {
19428 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19429 });
19430 let mut bytes = Vec::with_capacity(g.len() * 2);
19431 for v in g {
19432 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19433 }
19434 let _ = f.write_all(&bytes);
19435}
19436
19437fn oracle_topk() -> usize {
19443 if exact_mtp_calibration_active() {
19444 return 0;
19445 }
19446 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19447 *K.get_or_init(|| {
19448 std::env::var("CMF_FFN_ORACLE_TOPK")
19449 .ok()
19450 .and_then(|v| v.parse().ok())
19451 .unwrap_or(0)
19452 })
19453}
19454
19455fn gate_topk() -> usize {
19461 if exact_mtp_calibration_active() {
19462 return 0;
19463 }
19464 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19465 *K.get_or_init(|| {
19466 std::env::var("CMF_FFN_GATE_TOPK")
19467 .ok()
19468 .and_then(|v| v.parse().ok())
19469 .unwrap_or(0)
19470 })
19471}
19472
19473fn gate_block() -> usize {
19480 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19481 *B.get_or_init(|| {
19482 std::env::var("CMF_FFN_GATE_BLOCK")
19483 .ok()
19484 .and_then(|v| v.parse().ok())
19485 .unwrap_or(1)
19486 })
19487}
19488
19489fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19491 let n = g.len();
19492 let nb = n.div_ceil(block);
19493 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19494 if kb >= nb {
19495 return;
19496 }
19497 let mut score: Vec<f32> = (0..nb)
19498 .map(|b| {
19499 g[b * block..((b + 1) * block).min(n)]
19500 .iter()
19501 .map(|v| v * v)
19502 .sum::<f32>()
19503 })
19504 .collect();
19505 let mut ord = score.clone();
19506 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19507 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19508 });
19509 let thr = *kth;
19510 for b in 0..nb {
19511 if score[b] < thr {
19512 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19513 }
19514 }
19515 score.clear();
19516}
19517
19518fn keep_top_k(g: &mut [f32], k: usize) {
19520 if gate_block() > 1 {
19521 return keep_top_blocks(g, k, gate_block());
19522 }
19523 let n = g.len();
19524 if k == 0 || k >= n {
19525 return;
19526 }
19527 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19528 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19529 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19530 });
19531 let thr = *kth;
19532 for v in g.iter_mut() {
19533 if v.abs() < thr {
19534 *v = 0.0;
19535 }
19536 }
19537}
19538
19539fn probe_sq() -> bool {
19543 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19544 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19545}
19546
19547fn probe_signed() -> bool {
19551 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19552 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19553}
19554
19555fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19563 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19564 M.get_or_init(|| {
19565 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19566 let b = std::fs::read(&p).ok()?;
19567 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19568 let vals: Vec<f32> = b[8..]
19569 .chunks_exact(4)
19570 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19571 .collect();
19572 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19573 Some((inter, vals))
19574 })
19575 .as_ref()
19576}
19577
19578fn probe_topk() -> usize {
19581 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19582 *K.get_or_init(|| {
19583 std::env::var("CMF_FFN_PROBE_TOPK")
19584 .ok()
19585 .and_then(|v| v.parse().ok())
19586 .unwrap_or(0)
19587 })
19588}
19589
19590thread_local! {
19591 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19594 const { std::cell::RefCell::new(None) };
19595}
19596
19597fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19610 if d.gate_proj.has_prism_contract()
19615 || d.up_proj.has_prism_contract()
19616 || d.down_proj.has_prism_contract()
19617 {
19618 return None;
19619 }
19620 let dt = d.down_t.as_ref()?;
19621 let inter = d.gate_proj.rows();
19622 let hidden = dt.cols();
19623 if k == 0 || k >= inter || d.act != Act::Silu {
19624 return None;
19625 }
19626 DYN_SCRATCH.with(|sc| {
19627 let mut sc = sc.borrow_mut();
19628 let DynScratch {
19629 g,
19630 mag,
19631 live,
19632 parts,
19633 } = &mut *sc;
19634 g.resize(inter, 0.0);
19635 d.gate_proj.matvec(x, g, pool);
19636 for v in g.iter_mut() {
19637 *v = inference::silu(*v);
19638 }
19639 mag.clear();
19642 mag.extend(g.iter().map(|v| v.abs()));
19643 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19644 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19645 });
19646 let thr = *kth;
19647 live.clear();
19648 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19649 let mut out = vec![0.0f32; hidden];
19650 match pool {
19651 Some(p) if live.len() >= 64 => {
19652 let nw = p.n_workers() + 1;
19653 parts.clear();
19654 parts.resize(nw * hidden, 0.0);
19655 let ptr = SendMut(parts.as_mut_ptr());
19656 let n = live.len();
19657 let live_ref: &[u32] = live;
19658 let g_ref: &[f32] = g;
19659 p.run(&|w, workers| {
19660 let chunk = n.div_ceil(workers);
19661 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19662 if s >= e {
19663 return;
19664 }
19665 WORKER_SCRATCH.with(|ws| {
19666 let mut ws = ws.borrow_mut();
19667 let [scratch, acc] = &mut *ws;
19668 scratch.resize(hidden.max(x.len()), 0.0);
19669 acc.clear();
19670 acc.resize(hidden, 0.0);
19671 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19672 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19675 d.up_proj.prefetch_row(nx as usize);
19676 dt.prefetch_row(nx as usize);
19677 }
19678 let idx = nrm as usize;
19679 let up = d.up_proj.row_dot(idx, x, scratch);
19680 let a = g_ref[idx] * up;
19681 if a != 0.0 {
19682 dt.add_row_scaled(idx, a, acc, scratch);
19683 }
19684 }
19685 for (j, v) in acc.iter().enumerate() {
19686 unsafe { *ptr.at(w * hidden + j) = *v };
19687 }
19688 });
19689 });
19690 for w in 0..nw {
19691 for (j, o) in out.iter_mut().enumerate() {
19692 *o += parts[w * hidden + j];
19693 }
19694 }
19695 }
19696 _ => {
19697 WORKER_SCRATCH.with(|ws| {
19698 let mut ws = ws.borrow_mut();
19699 let [scratch, _acc] = &mut *ws;
19700 scratch.resize(hidden.max(x.len()), 0.0);
19701 for &nrm in live.iter() {
19702 let idx = nrm as usize;
19703 let up = d.up_proj.row_dot(idx, x, scratch);
19704 let a = g[idx] * up;
19705 if a != 0.0 {
19706 dt.add_row_scaled(idx, a, &mut out, scratch);
19707 }
19708 }
19709 });
19710 }
19711 }
19712 Some(out)
19713 })
19714}
19715
19716struct DynScratch {
19719 g: Vec<f32>,
19720 mag: Vec<f32>,
19721 live: Vec<u32>,
19722 parts: Vec<f32>,
19723}
19724
19725thread_local! {
19726 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19727 std::cell::RefCell::new(DynScratch {
19728 g: Vec::new(),
19729 mag: Vec::new(),
19730 live: Vec::new(),
19731 parts: Vec::new(),
19732 })
19733 };
19734 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19736 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19737}
19738
19739fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19744 let inter = d.gate_proj.rows();
19745 FFN_SCRATCH.with(|s| {
19746 let mut s = s.borrow_mut();
19747 let [g, u, ..] = &mut *s;
19748 g.resize(inter, 0.0);
19749 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19750 } else {
19752 u.resize(inter, 0.0);
19753 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19754 for i in 0..inter {
19755 g[i] = d.act.combine(g[i], u[i]);
19756 }
19757 }
19758 zero_masked_cols(g, 1, inter, mask_row);
19759 let mut out = attention::take_buf(d.down_proj.rows());
19760 d.down_proj.matvec(g, &mut out, pool);
19761 out
19762 })
19763}
19764
19765fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19771 if d.gate_proj.has_prism_contract()
19772 || d.up_proj.has_prism_contract()
19773 || d.down_proj.has_prism_contract()
19774 {
19775 return None;
19776 }
19777 d.act.moe_job_graph_contract()?;
19780 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19783 return None;
19784 }
19785 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19786 let mut model_ref = None;
19787 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19788 let model = model_ref?;
19789 let hidden = jobs[0].down.1;
19790 let mut out = attention::take_buf(hidden);
19791 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19792 Some(out)
19793 } else {
19794 let mut out = out;
19795 attention::recycle_buf(&mut out);
19796 None
19797 }
19798}
19799
19800#[allow(clippy::type_complexity)]
19805#[allow(clippy::type_complexity)]
19806pub(crate) fn moe_parts(
19807 t: &QTensor,
19808) -> Option<(
19809 &std::sync::Arc<cortiq_core::CmfModel>,
19810 usize,
19811 usize,
19812 usize,
19813 &[f32],
19814 &[f32],
19815 bool,
19816 bool,
19817 bool,
19818)> {
19819 match t {
19820 QTensor::Mapped {
19821 model,
19822 idx,
19823 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
19824 rows,
19825 cols,
19826 row_scale,
19827 col_field,
19828 ..
19829 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
19830 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
19831 )),
19832 QTensor::Mapped {
19834 model,
19835 idx,
19836 dtype: cortiq_core::TensorDtype::Q1,
19837 rows,
19838 cols,
19839 ..
19840 } => Some((
19841 model,
19842 *idx,
19843 *rows,
19844 *cols,
19845 &[][..],
19846 &[][..],
19847 true,
19848 false,
19849 false,
19850 )),
19851 QTensor::Mapped {
19853 model,
19854 idx,
19855 dtype: cortiq_core::TensorDtype::Q4Tiled,
19856 rows,
19857 cols,
19858 ..
19859 } => Some((
19860 model,
19861 *idx,
19862 *rows,
19863 *cols,
19864 &[][..],
19865 &[][..],
19866 false,
19867 true,
19868 false,
19869 )),
19870 QTensor::Mapped {
19872 model,
19873 idx,
19874 dtype: cortiq_core::TensorDtype::Q4TiledP,
19875 rows,
19876 cols,
19877 ..
19878 } => Some((
19879 model,
19880 *idx,
19881 *rows,
19882 *cols,
19883 &[][..],
19884 &[][..],
19885 false,
19886 true,
19887 false,
19888 )),
19889 QTensor::Mapped {
19893 model,
19894 idx,
19895 dtype: cortiq_core::TensorDtype::Q2TiledP,
19896 rows,
19897 cols,
19898 ..
19899 } => Some((
19900 model,
19901 *idx,
19902 *rows,
19903 *cols,
19904 &[][..],
19905 &[][..],
19906 false,
19907 true,
19908 true,
19909 )),
19910 _ => None,
19911 }
19912}
19913
19914#[cfg(target_os = "macos")]
19921#[allow(clippy::type_complexity)]
19922fn chunk_moe_parts(
19923 m: &MoeFfn,
19924 hidden: usize,
19925) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
19926 if m.shared.is_some()
19927 || m.per_expert_scale.is_some()
19928 || m.resonance.is_some()
19929 || m.route_tau.is_some()
19930 || m.router_input_norm
19931 || m.experts.is_empty()
19932 || m.top_k == 0
19933 || std::env::var("CMF_RMS_TRACE").is_ok()
19934 || std::env::var("CMF_ACT_DUMP").is_ok()
19935 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
19936 {
19937 return None;
19938 }
19939 let (rf, rr, rc) = m.router.f32_parts()?;
19940 if rr != m.experts.len() || rc != hidden {
19941 return None;
19942 }
19943 let inter = m.experts[0].gate_proj.rows();
19944 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
19947 let experts = m
19948 .experts
19949 .iter()
19950 .map(|e| {
19951 if e.act != Act::Silu
19952 || !e.segs.is_empty()
19953 || e.gate_proj.rows() != inter
19954 || e.up_proj.rows() != inter
19955 || e.down_proj.cols() != inter
19956 || e.gate_proj.cols() != hidden
19957 || e.down_proj.rows() != hidden
19958 {
19959 return None;
19960 }
19961 if q8 {
19962 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
19963 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
19964 }
19965 Some((
19966 e.gate_proj.mapped_q4tp()?.1,
19967 e.up_proj.mapped_q4tp()?.1,
19968 e.down_proj.mapped_q4tp()?.1,
19969 ))
19970 })
19971 .collect::<Option<Vec<_>>>()?;
19972 Some((rf, experts, q8))
19973}
19974
19975#[cfg(target_os = "macos")]
19988fn metal_flint_moe_graph_parts(
19989 m: &MoeFfn,
19990 hidden: usize,
19991 scratch_key: usize,
19992) -> Option<crate::gpu::GpuFlintMoe<'_>> {
19993 use std::sync::Arc;
19994
19995 const HIDDEN: usize = 2_048;
19996 const ROUTED_INTER: usize = 512;
19997 const SHARED_INTER: usize = 1_024;
19998 const EXPERTS: usize = 256;
19999 const TOP_K: usize = 6;
20000
20001 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20002 return None;
20003 }
20004 let routed0 = m.experts.first()?;
20005 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20006 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20007 if router_rows != EXPERTS || router_cols != HIDDEN {
20008 return None;
20009 }
20010 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20011 return None;
20012 }
20013 let same_q2 = |t: &QTensor| -> Option<usize> {
20014 let (owner, idx) = t.mapped_q2tp()?;
20015 Arc::ptr_eq(&owner, &model).then_some(idx)
20016 };
20017 let same_q4 = |t: &QTensor| -> Option<usize> {
20018 let (owner, idx) = t.mapped_q4tp()?;
20019 Arc::ptr_eq(&owner, &model).then_some(idx)
20020 };
20021 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20022 if e.act != Act::Silu
20023 || !e.segs.is_empty()
20024 || e.gate_proj.rows() != ROUTED_INTER
20025 || e.gate_proj.cols() != HIDDEN
20026 || e.up_proj.rows() != ROUTED_INTER
20027 || e.up_proj.cols() != HIDDEN
20028 || e.down_proj.rows() != HIDDEN
20029 || e.down_proj.cols() != ROUTED_INTER
20030 {
20031 return None;
20032 }
20033 Some((
20034 same_q2(&e.gate_proj)?,
20035 same_q2(&e.up_proj)?,
20036 same_q4(&e.down_proj)?,
20037 ))
20038 };
20039 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20040 if e.act != Act::Silu
20041 || !e.segs.is_empty()
20042 || e.gate_proj.rows() != SHARED_INTER
20043 || e.gate_proj.cols() != HIDDEN
20044 || e.up_proj.rows() != SHARED_INTER
20045 || e.up_proj.cols() != HIDDEN
20046 || e.down_proj.rows() != HIDDEN
20047 || e.down_proj.cols() != SHARED_INTER
20048 {
20049 return None;
20050 }
20051 Some((
20052 same_q4(&e.gate_proj)?,
20053 same_q4(&e.up_proj)?,
20054 same_q4(&e.down_proj)?,
20055 ))
20056 };
20057 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20058 if experts.len() != EXPERTS {
20059 return None;
20060 }
20061 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20062 if shared_gate.is_some() {
20063 return None;
20064 }
20065 Some(crate::gpu::GpuFlintMoe {
20066 router,
20067 bias: m.expert_bias.as_deref(),
20068 experts,
20069 scratch_key,
20070 shared: shared(shared_ffn)?,
20071 top_k: m.top_k,
20072 norm_topk: m.norm_topk_prob,
20073 route_scale: m.routed_scaling,
20074 sigmoid: m.router_sigmoid,
20075 sigmoid_denom_eps: m.sigmoid_denom_eps,
20076 })
20077}
20078
20079#[cfg(target_os = "macos")]
20080fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20081 if m.router_input_norm
20082 || m.route_tau.is_some()
20083 || m.mask.is_some()
20084 || m.per_expert_scale.is_some()
20085 || m.experts.is_empty()
20086 || m.top_k == 0
20087 || m.resonance.is_some()
20088 {
20089 return None;
20090 }
20091 let (sh, sg) = match &m.shared {
20096 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20097 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20098 None => return None,
20099 };
20100 let (rf, rr, rc) = m.router.f32_parts()?;
20101 if rr != m.experts.len() || rc != hidden {
20102 return None;
20103 }
20104 let shared_gated = sg.is_some();
20105 let sf = match sg {
20106 Some(sg) => {
20107 let (sf, sr, sc) = sg.f32_parts()?;
20108 if sr * sc != hidden {
20109 return None;
20110 }
20111 sf
20112 }
20113 None => &rf[..hidden],
20116 };
20117 if let Some(b) = &m.expert_bias {
20118 if b.len() != m.experts.len() {
20119 return None;
20120 }
20121 }
20122 let inter = m.experts[0].gate_proj.rows();
20123 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20126 let q8 = crate::gpu_metal::moe_q8::enabled()
20130 && m.shared.is_none()
20131 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20132 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20133 if e.act != Act::Silu
20134 || e.gate_proj.rows() != inter
20135 || e.gate_proj.cols() != hidden
20136 || e.up_proj.rows() != inter
20137 || e.up_proj.cols() != hidden
20138 || e.down_proj.rows() != hidden
20139 || e.down_proj.cols() != inter
20140 {
20141 return None;
20142 }
20143 if q8 {
20144 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20145 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20146 }
20147 let pick = |t: &QTensor| -> Option<usize> {
20148 if gu_q2 {
20149 t.mapped_q2tp().map(|(_, i)| i)
20150 } else {
20151 t.mapped_q4tp().map(|(_, i)| i)
20152 }
20153 };
20154 Some((
20155 pick(&e.gate_proj)?,
20156 pick(&e.up_proj)?,
20157 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20158 ))
20159 };
20160 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20161 let shared = match sh {
20162 Some(sh) => Some(trio(sh)?),
20163 None => None,
20164 };
20165 Some(crate::gpu::GpuMoe {
20166 router: rf,
20167 sgate: sf,
20168 experts,
20169 shared,
20170 n_exp: m.experts.len(),
20171 top_k: m.top_k,
20172 inter,
20173 norm_topk: m.norm_topk_prob,
20174 route_scale: m.routed_scaling,
20175 gu_q2,
20176 q8,
20177 sigmoid: m.router_sigmoid,
20178 sigmoid_denom_eps: m.sigmoid_denom_eps,
20179 bias: m.expert_bias.as_deref(),
20180 shared_gated,
20181 })
20182}
20183
20184fn forced_ids_at(step: usize) -> Option<u32> {
20190 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20191 IDS.get_or_init(|| {
20192 std::env::var("CMF_FORCE_IDS")
20193 .ok()
20194 .and_then(|p| std::fs::read_to_string(p).ok())
20195 .map(|s| {
20196 s.split_whitespace()
20197 .filter_map(|t| t.parse().ok())
20198 .collect()
20199 })
20200 .unwrap_or_default()
20201 })
20202 .get(step)
20203 .copied()
20204}
20205
20206pub(crate) fn moe_push_job_parts<'a>(
20210 gate: &'a QTensor,
20211 up: &'a QTensor,
20212 down: &'a QTensor,
20213 x: &[f32],
20214 w: f32,
20215 swiglu_limit: f32,
20216 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20217 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20218) -> Option<()> {
20219 use crate::qtensor::prescale;
20220 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20221 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20222 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20223 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20224 return None; }
20226 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20229 return None;
20230 }
20231 if !gq2 && dq2 {
20232 return None;
20233 }
20234 model_ref.get_or_insert_with(|| gm.clone());
20235 let dt = |cf: &[f32]| {
20236 if cf.is_empty() {
20237 cortiq_core::TensorDtype::Q8Row
20238 } else {
20239 cortiq_core::TensorDtype::Q8_2f
20240 }
20241 };
20242 jobs.push(crate::gpu::MoeJob {
20243 gate: (gi, gr, gc, grs),
20244 up: (ui, ur, uc, urs),
20245 down: (di, dr, dc, drs),
20246 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20247 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20248 down_col: dcf,
20249 w,
20250 q1: gq1,
20251 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20252 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20253 gu_q2: gq2,
20254 swiglu_limit,
20255 act: 0,
20256 });
20257 Some(())
20258}
20259
20260fn moe_push_job<'a>(
20262 d: &'a DenseFfn,
20263 x: &[f32],
20264 w: f32,
20265 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20266 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20267) -> Option<()> {
20268 use crate::qtensor::prescale;
20269 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20274 let act = graph_act.code();
20275 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20276 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20277 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20278 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20279 return None; }
20281 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20282 return None;
20283 }
20284 if !gq2 && dq2 {
20285 return None;
20286 }
20287 model_ref.get_or_insert_with(|| gm.clone());
20288 let gdt = if gcf.is_empty() {
20289 cortiq_core::TensorDtype::Q8Row
20290 } else {
20291 cortiq_core::TensorDtype::Q8_2f
20292 };
20293 let udt = if ucf.is_empty() {
20294 cortiq_core::TensorDtype::Q8Row
20295 } else {
20296 cortiq_core::TensorDtype::Q8_2f
20297 };
20298 jobs.push(crate::gpu::MoeJob {
20299 gate: (gi, gr, gc, grs),
20300 up: (ui, ur, uc, urs),
20301 down: (di, dr, dc, drs),
20302 xs_gate: prescale(x, gcf, gdt).into_owned(),
20303 xs_up: prescale(x, ucf, udt).into_owned(),
20304 down_col: dcf,
20305 w,
20306 q1: gq1,
20307 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20308 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20309 gu_q2: gq2,
20310 swiglu_limit,
20311 act,
20312 });
20313 Some(())
20314}
20315
20316fn sparse_ffn_quant(
20323 d: &DenseFfn,
20324 x: &[f32],
20325 active: &[u16],
20326 hidden: usize,
20327 pool: Option<&Pool>,
20328) -> Vec<f32> {
20329 let n = active.len();
20330 let inter = d.gate_proj.rows();
20331 let mut act = vec![0.0f32; n];
20332 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20335 let compute = |ai: usize| -> f32 {
20336 let idx = active[ai] as usize;
20337 if idx >= inter {
20338 return 0.0; }
20340 let mut s = if need_scratch {
20341 vec![0.0f32; hidden]
20342 } else {
20343 Vec::new()
20344 };
20345 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20346 let up = d.up_proj.row_dot(idx, x, &mut s);
20347 d.act.combine(gate, up)
20348 };
20349 match pool {
20350 Some(p) if n >= 256 => {
20351 let ptr = SendMut(act.as_mut_ptr());
20352 p.run(&|widx, nw| {
20353 let chunk = n.div_ceil(nw);
20354 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20355 for ai in s..e {
20356 unsafe { *ptr.at(ai) = compute(ai) };
20357 }
20358 });
20359 }
20360 _ => {
20361 for (ai, a) in act.iter_mut().enumerate() {
20362 *a = compute(ai);
20363 }
20364 }
20365 }
20366 let mut out = vec![0.0f32; hidden];
20368 for (ai, &idx) in active.iter().enumerate() {
20369 let w = act[ai];
20370 if w.abs() >= 1e-12 && (idx as usize) < inter {
20371 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20372 }
20373 }
20374 out
20375}
20376
20377#[doc(hidden)]
20379pub fn sparse_ffn_quant_for_test(
20380 d: &DenseFfn,
20381 x: &[f32],
20382 active: &[u16],
20383 hidden: usize,
20384) -> Vec<f32> {
20385 sparse_ffn_quant(d, x, active, hidden, None)
20386}
20387
20388fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20392 let deq = |t: &QTensor| -> Vec<f32> {
20393 let (rows, cols) = (t.rows(), t.cols());
20394 let mut out = vec![0.0f32; rows * cols];
20395 for r in 0..rows {
20396 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20397 }
20398 out
20399 };
20400 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20401}
20402
20403struct SendMut(*mut f32);
20405unsafe impl Send for SendMut {}
20406unsafe impl Sync for SendMut {}
20407impl SendMut {
20408 #[inline]
20409 #[allow(clippy::mut_from_ref)]
20412 unsafe fn at(&self, i: usize) -> &mut f32 {
20413 unsafe { &mut *self.0.add(i) }
20414 }
20415}
20416
20417pub(crate) fn moe_route(
20429 logits: &[f32],
20430 m: &MoeFfn,
20431 allowed: Option<&[bool]>,
20432) -> (Vec<usize>, Vec<f32>, f32) {
20433 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20434}
20435
20436pub(crate) fn moe_route_with_eps(
20446 logits: &[f32],
20447 m: &MoeFfn,
20448 allowed: Option<&[bool]>,
20449 sigmoid_denom_eps: f32,
20450) -> (Vec<usize>, Vec<f32>, f32) {
20451 let ne = logits.len();
20452 let admit = |e: usize| {
20458 m.mask.as_ref().is_none_or(|mk| mk[e])
20459 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20460 };
20461 if m.resonance.is_some() && m.top_k == 1 {
20474 let mut best: Option<usize> = None;
20475 for e in (0..ne).filter(|&e| admit(e)) {
20476 let l = logits[e];
20477 if l == f32::NEG_INFINITY || l.is_nan() {
20478 continue;
20479 }
20480 if best.is_none_or(|b| l > logits[b]) {
20481 best = Some(e);
20482 }
20483 }
20484 if let Some(b) = best {
20485 let mut p = vec![0.0f32; ne];
20486 p[b] = 1.0;
20487 return (vec![b], p, 1.0 / m.routed_scaling);
20488 }
20489 }
20490 let p: Vec<f32> = if m.router_sigmoid {
20496 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20497 } else {
20498 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20499 if mx == f32::NEG_INFINITY {
20500 vec![1.0 / ne.max(1) as f32; ne]
20501 } else {
20502 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20503 let s: f32 = e.iter().sum();
20504 for v in &mut e {
20505 *v /= s;
20506 }
20507 e
20508 }
20509 };
20510 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20511 match &m.expert_bias {
20513 Some(b) => idx.sort_unstable_by(|&x, &y| {
20514 (p[y] + b[y])
20515 .partial_cmp(&(p[x] + b[x]))
20516 .unwrap()
20517 .then(x.cmp(&y))
20518 }),
20519 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20520 }
20521 idx.truncate(m.top_k);
20522 if let Some(tau) = m.route_tau {
20526 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20527 if total > 0.0 {
20528 let mut acc = 0.0f32;
20529 let mut keep = idx.len();
20530 for (i, &e) in idx.iter().enumerate() {
20531 acc += p[e];
20532 if acc >= tau * total {
20533 keep = i + 1;
20534 break;
20535 }
20536 }
20537 idx.truncate(keep);
20538 }
20539 }
20540 let wsum: f32 = if m.norm_topk_prob {
20541 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20542 (if m.router_sigmoid {
20546 s + sigmoid_denom_eps
20547 } else {
20548 s
20549 }) / m.routed_scaling
20550 } else {
20551 1.0 / m.routed_scaling
20552 };
20553 (idx, p, wsum)
20554}
20555
20556fn moe_trace(idx: &[usize]) {
20558 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20559}
20560
20561pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20564 use std::io::Write;
20565 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20566 std::sync::OnceLock::new();
20567 let Some(f) = F.get_or_init(|| {
20568 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20569 Some(std::sync::Mutex::new(
20570 std::fs::OpenOptions::new()
20571 .create(true)
20572 .append(true)
20573 .open(p)
20574 .ok()?,
20575 ))
20576 }) else {
20577 return;
20578 };
20579 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20580 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20581}
20582
20583pub(crate) fn moe_ffn(
20586 m: &MoeFfn,
20587 x: &[f32],
20588 pool: Option<&Pool>,
20589 allowed: Option<&[bool]>,
20590) -> Vec<f32> {
20591 let r = moe_ffn_route(m, x, pool, allowed);
20592 moe_ffn_experts(m, x, &r, pool)
20593}
20594
20595pub(crate) struct MoeRoute {
20599 pub idx: Vec<usize>,
20600 pub p: Vec<f32>,
20601 pub wsum: f32,
20602 pub logits: Vec<f32>,
20603}
20604
20605pub(crate) fn moe_ffn_route(
20611 m: &MoeFfn,
20612 x: &[f32],
20613 pool: Option<&Pool>,
20614 allowed: Option<&[bool]>,
20615) -> MoeRoute {
20616 accumulate_act(m, x, 1);
20617 let ne = m.experts.len();
20618 let mut logits = vec![0.0f32; ne];
20619 match &m.resonance {
20620 Some(r) => r.scores(x, &mut logits),
20621 None => m.router.matvec(x, &mut logits, pool),
20622 }
20623 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20624 {
20625 let mut st = m.stats.borrow_mut();
20626 if st.len() < ne {
20627 st.resize(ne, 0);
20628 }
20629 for &e in &idx {
20630 st[e] += 1;
20631 }
20632 }
20633 moe_trace(&idx);
20639 MoeRoute {
20640 idx,
20641 p,
20642 wsum,
20643 logits,
20644 }
20645}
20646
20647#[cfg(target_os = "macos")]
20656fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20657 const HIDDEN: usize = 2_048;
20658 const ROUTED_INTER: usize = 512;
20659 const SHARED_INTER: usize = 1_024;
20660 const EXPERTS: usize = 256;
20661 const TOP_K: usize = 6;
20662
20663 let Some(routed) = m.experts.first() else {
20664 return false;
20665 };
20666 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20667 return false;
20668 };
20669 let Some((shared, shared_gate)) = &m.shared else {
20670 return false;
20671 };
20672 if shared_gate.is_some()
20673 || model.header.arch.arch_name != "flint"
20674 || model.header.arch.prism_hadamard.is_some()
20675 || model.header.arch.hidden_size != HIDDEN
20676 || m.experts.len() != EXPERTS
20677 || m.top_k != TOP_K
20678 || !m.router_sigmoid
20679 || !m.norm_topk_prob
20680 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20681 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20682 || m.route_tau.is_some()
20683 || m.mask.is_some()
20684 || m.per_expert_scale.is_some()
20685 || m.router_input_norm
20686 || m.resonance.is_some()
20687 || !m.grown.is_empty()
20688 {
20689 return false;
20690 }
20691
20692 let routed_q2 = routed.act == Act::Silu
20693 && routed.segs.is_empty()
20694 && routed.gate_proj.rows() == ROUTED_INTER
20695 && routed.gate_proj.cols() == HIDDEN
20696 && routed.up_proj.rows() == ROUTED_INTER
20697 && routed.up_proj.cols() == HIDDEN
20698 && routed.down_proj.rows() == HIDDEN
20699 && routed.down_proj.cols() == ROUTED_INTER
20700 && routed.up_proj.mapped_q2tp().is_some()
20701 && routed.down_proj.mapped_q4tp().is_some();
20702 let shared_q4 = shared.act == Act::Silu
20703 && shared.segs.is_empty()
20704 && shared.gate_proj.rows() == SHARED_INTER
20705 && shared.gate_proj.cols() == HIDDEN
20706 && shared.up_proj.rows() == SHARED_INTER
20707 && shared.up_proj.cols() == HIDDEN
20708 && shared.down_proj.rows() == HIDDEN
20709 && shared.down_proj.cols() == SHARED_INTER
20710 && shared.gate_proj.mapped_q4tp().is_some()
20711 && shared.up_proj.mapped_q4tp().is_some()
20712 && shared.down_proj.mapped_q4tp().is_some();
20713 routed_q2 && shared_q4
20714}
20715
20716pub(crate) fn moe_ffn_experts(
20719 m: &MoeFfn,
20720 x: &[f32],
20721 r: &MoeRoute,
20722 pool: Option<&Pool>,
20723) -> Vec<f32> {
20724 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20725 if crate::gpu::enabled_here() {
20730 #[cfg(target_os = "macos")]
20731 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20732 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20738 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20739 });
20740 }
20741 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20742 crate::gpu::ProbeArm::Gpu => {
20743 let t0 = std::time::Instant::now();
20744 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20745 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20746 return out;
20747 }
20748 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20749 }
20750 crate::gpu::ProbeArm::CpuTimed => {
20751 let t0 = std::time::Instant::now();
20752 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20753 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20754 return out;
20755 }
20756 crate::gpu::ProbeArm::Cpu => {
20757 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20758 }
20759 }
20760 }
20761 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20762}
20763
20764fn moe_ffn_minicpm_tail(
20768 slot: &mut crate::minicpm_tail::MiniCpmTail,
20769 li: usize,
20770 total_layers: usize,
20771 m: &MoeFfn,
20772 x: &[f32],
20773 pool: Option<&Pool>,
20774) -> Vec<f32> {
20775 let route = moe_ffn_route(m, x, pool, None);
20776 match slot.forward(li, m, x, &route, pool, total_layers) {
20777 Some(out) => out,
20778 None => moe_ffn_experts(m, x, &route, pool),
20783 }
20784}
20785
20786fn moe_ffn_banked(
20790 slot: &mut crate::mimo_moe::Slot,
20791 li: usize,
20792 m: &MoeFfn,
20793 x: &[f32],
20794 pool: Option<&Pool>,
20795) -> Vec<f32> {
20796 let t0 = std::time::Instant::now();
20797 let r = moe_ffn_route(m, x, pool, None);
20798 slot.note_route(t0.elapsed().as_nanos() as u64);
20799 match slot.forward(li, m, x, &r, pool) {
20800 Some(out) => out,
20801 None => crate::qtensor::float_activations_scope(|| {
20802 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
20803 }),
20804 }
20805}
20806
20807fn moe_ffn_banked_rows(
20809 slot: &mut crate::mimo_moe::Slot,
20810 li: usize,
20811 m: &MoeFfn,
20812 xs: &[f32],
20813 b: usize,
20814 hidden: usize,
20815 pool: Option<&Pool>,
20816) -> Vec<f32> {
20817 let t0 = std::time::Instant::now();
20818 let routes: Vec<_> = xs
20819 .chunks_exact(hidden)
20820 .map(|x| moe_ffn_route(m, x, pool, None))
20821 .collect();
20822 slot.note_route(t0.elapsed().as_nanos() as u64);
20823 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
20824 return out;
20825 }
20826 let mut out = Vec::with_capacity(b * hidden);
20827 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
20828 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
20829 crate::qtensor::float_activations_scope(|| {
20831 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
20832 })
20833 });
20834 out.extend(row);
20835 }
20836 out
20837}
20838
20839fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
20844 use std::sync::atomic::{AtomicBool, Ordering};
20845 if built {
20846 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
20847 if total_layers > 0 && layers_run < total_layers {
20848 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
20849 } else {
20850 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
20851 }
20852 } else {
20853 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
20854 }
20855 static SAID: AtomicBool = AtomicBool::new(false);
20856 if !SAID.swap(true, Ordering::Relaxed) {
20857 if built {
20858 tracing::info!("wgpu whole-token graph: ACTIVE");
20859 } else {
20860 tracing::warn!("wgpu whole-token graph refused — per-op path");
20861 }
20862 }
20863}
20864
20865pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20869pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20870pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20874pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20876
20877pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20881pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20882pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
20883 std::sync::atomic::AtomicU64::new(0);
20884pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20885pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20886pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
20890 std::sync::atomic::AtomicU64::new(0);
20891pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20892pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
20893 std::sync::atomic::AtomicU64::new(0);
20894pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
20895 std::sync::atomic::AtomicU64::new(0);
20896
20897fn moe_batch_enabled() -> bool {
20900 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20901 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
20902}
20903
20904fn moe_ffn_cpu_batched(
20910 m: &MoeFfn,
20911 x: &[f32],
20912 idx: &[usize],
20913 p: &[f32],
20914 wsum: f32,
20915 pool: Option<&Pool>,
20916) -> Option<Vec<f32>> {
20917 if idx.is_empty() || !moe_batch_enabled() {
20918 return None;
20919 }
20920 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
20924 return None;
20925 }
20926 let n = idx.len() + usize::from(m.shared.is_some());
20927 let mut pairs = Vec::with_capacity(n);
20928 let mut downs = Vec::with_capacity(n);
20929 let mut ws = Vec::with_capacity(n);
20930 for &e in idx {
20931 let d = &m.experts[e];
20932 if d.act != Act::Silu {
20933 return None;
20934 }
20935 pairs.push((&d.gate_proj, &d.up_proj));
20936 downs.push(&d.down_proj);
20937 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
20938 }
20939 if let Some((se, gate)) = &m.shared {
20942 if se.act != Act::Silu {
20943 return None;
20944 }
20945 let g = gate.as_ref().map_or(1.0, |gate| {
20946 let mut gl = [0.0f32; 1];
20947 gate.matvec(x, &mut gl, pool);
20948 1.0 / (1.0 + (-gl[0]).exp())
20949 });
20950 pairs.push((&se.gate_proj, &se.up_proj));
20951 downs.push(&se.down_proj);
20952 ws.push(g);
20953 }
20954 let inter = pairs[0].0.rows();
20955 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
20956 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
20957 return None;
20958 }
20959 let mut out = attention::take_buf(x.len());
20960 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
20961 attention::recycle_buf(&mut out);
20962 return None;
20963 }
20964 Some(out)
20965}
20966
20967pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20975pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20976pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20977pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20978pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
20979
20980pub(crate) fn moe_cold_experts_cpu(
20981 experts: &[(&DenseFfn, f32)],
20982 x: &[f32],
20983 pool: Option<&Pool>,
20984) -> Vec<f32> {
20985 let mut out = attention::take_buf(x.len());
20986 if experts.is_empty() {
20987 return out;
20988 }
20989 let pairs: Vec<_> = experts
20990 .iter()
20991 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
20992 .collect();
20993 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
20994 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
20995 let inter = experts[0].0.gate_proj.rows();
20996 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
20997 let silu_only = experts
21005 .iter()
21006 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21007 if silu_only
21008 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21009 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21010 {
21011 return out;
21012 }
21013 out.fill(0.0);
21014 for &(expert, weight) in experts {
21015 let mut one = dense_ffn(expert, x, pool);
21016 for (o, v) in out.iter_mut().zip(&one) {
21017 *o += weight * v;
21018 }
21019 attention::recycle_buf(&mut one);
21020 }
21021 out
21022}
21023
21024pub(crate) fn moe_cold_experts_rows_cpu(
21028 jobs: &[Vec<(&DenseFfn, f32)>],
21029 xs: &[f32],
21030 hidden: usize,
21031 pool: Option<&Pool>,
21032) -> Vec<f32> {
21033 let mut out = vec![0.0; xs.len()];
21034 let mut experts: Vec<&DenseFfn> = Vec::new();
21035 let mut groups: Vec<Vec<usize>> = Vec::new();
21036 let mut terms = vec![Vec::new(); jobs.len()];
21037 for (r, row) in jobs.iter().enumerate() {
21038 for &(e, w) in row {
21039 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21040 Some(g) => g,
21041 None => {
21042 experts.push(e);
21043 groups.push(Vec::new());
21044 groups.len() - 1
21045 }
21046 };
21047 terms[r].push((g, groups[g].len(), w));
21048 groups[g].push(r);
21049 }
21050 }
21051 if experts.is_empty() {
21052 return out;
21053 }
21054 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21055 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21056 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21057 let count: usize = lens.iter().sum();
21058 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21059 let mut ds = vec![vec![0.0; hidden]; count];
21060 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21061 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21062 {
21063 let mut offset = 0;
21064 let offsets: Vec<_> = lens
21065 .iter()
21066 .map(|&n| {
21067 let start = offset;
21068 offset += n;
21069 start
21070 })
21071 .collect();
21072 for (r, terms) in terms.iter().enumerate() {
21073 for &(g, slot, w) in terms {
21074 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21075 .iter_mut()
21076 .zip(&ds[offsets[g] + slot])
21077 {
21078 *o += w * v;
21079 }
21080 }
21081 }
21082 } else {
21083 for (r, jobs) in jobs.iter().enumerate() {
21084 if !jobs.is_empty() {
21085 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21086 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21087 attention::recycle_buf(&mut row);
21088 }
21089 }
21090 }
21091 out
21092}
21093
21094fn moe_ffn_cpu(
21096 m: &MoeFfn,
21097 x: &[f32],
21098 idx: &[usize],
21099 p: &[f32],
21100 wsum: f32,
21101 pool: Option<&Pool>,
21102) -> Vec<f32> {
21103 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21104 return out;
21105 }
21106 let mut out = attention::take_buf(x.len());
21107 for &e in idx {
21108 let mut eo = dense_ffn(&m.experts[e], x, pool);
21109 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21110 for i in 0..out.len() {
21111 out[i] += w * eo[i];
21112 }
21113 attention::recycle_buf(&mut eo);
21114 }
21115 if let Some((se, gate)) = &m.shared {
21116 let mut so = dense_ffn(se, x, pool);
21117 let g = gate.as_ref().map_or(1.0, |gate| {
21118 let mut gl = [0.0f32; 1];
21119 gate.matvec(x, &mut gl, pool);
21120 1.0 / (1.0 + (-gl[0]).exp())
21121 });
21122 for i in 0..out.len() {
21123 out[i] += g * so[i];
21124 }
21125 attention::recycle_buf(&mut so);
21126 }
21127 out
21128}
21129
21130#[allow(clippy::too_many_arguments)]
21138pub(crate) fn mla_attention(
21139 w: &MlaWeights,
21140 normed: &[f32],
21141 cache: &mut crate::kv_cache::LayerKvCache,
21142 position: usize,
21143 inv_freq: &[f32],
21144 rope_scale: f32,
21145 eps: f64,
21146 pool: Option<&Pool>,
21147) -> Vec<f32> {
21148 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21149 let hd = dr + dn;
21150 let mut q = vec![0.0f32; nh * hd];
21151 match (&w.q_a, &w.q_a_norm) {
21152 (Some(qa), Some(qn)) => {
21153 let mut t = vec![0.0f32; qa.rows()];
21154 qa.matvec(normed, &mut t, pool);
21155 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21156 w.q_proj.matvec(&tn, &mut q, pool);
21157 }
21158 _ => w.q_proj.matvec(normed, &mut q, pool),
21159 }
21160 let mut ca = vec![0.0f32; lora + dr];
21161 w.kv_a.matvec(normed, &mut ca, pool);
21162 let (c_lat, k_rope) = ca.split_at_mut(lora);
21163 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21164 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21165 w.kv_b.matvec(&latn, &mut kvb, pool);
21166 if !w.nope {
21167 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21168 }
21169 for h in 0..nh {
21170 if !w.nope {
21171 attention::rope_rotate_scaled(
21172 &mut q[h * hd..h * hd + dr],
21173 position,
21174 inv_freq,
21175 rope_scale,
21176 );
21177 }
21178 }
21179 let mut k = vec![0.0f32; nh * hd];
21180 let mut v = vec![0.0f32; nh * hd];
21181 for h in 0..nh {
21182 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21183 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21184 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21185 }
21186 cache.append(&k, &v, &vec![true; nh]);
21187 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21188 attention::recycle_buf(&mut imp);
21189 let mut ov = vec![0.0f32; nh * dv];
21190 for h in 0..nh {
21191 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21192 }
21193 let mut out = vec![0.0f32; w.o_proj.rows()];
21194 w.o_proj.matvec(&ov, &mut out, pool);
21195 out
21196}
21197
21198fn dense_moe_ffn(
21205 dm: &DenseMoeFfn,
21206 x_normed: &[f32],
21207 h_raw: &[f32],
21208 eps: f64,
21209 norm_style: NormStyle,
21210 pool: Option<&Pool>,
21211) -> Vec<f32> {
21212 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21213 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21214 let m = &dm.moe;
21215 let ne = m.experts.len();
21216 let mut logits = vec![0.0f32; ne];
21217 if m.router_input_norm {
21218 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21219 let inv = 1.0 / (ss + eps as f32).sqrt();
21220 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21221 m.router.matvec(&xr, &mut logits, pool);
21222 } else {
21223 m.router.matvec(h_raw, &mut logits, pool);
21224 }
21225 let (idx, p, wsum) = moe_route(&logits, m, None);
21226 {
21227 let mut st = m.stats.borrow_mut();
21228 if st.len() < ne {
21229 st.resize(ne, 0);
21230 }
21231 for &e in &idx {
21232 st[e] += 1;
21233 }
21234 }
21235 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21236 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21237 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21238 for (di, mi) in d.iter_mut().zip(&mo) {
21239 *di += mi;
21240 }
21241 d
21242}
21243
21244fn moe_gpu_refused(why: &'static str) {
21251 use std::sync::atomic::{AtomicBool, Ordering};
21252 static SAID: AtomicBool = AtomicBool::new(false);
21253 if !SAID.swap(true, Ordering::Relaxed) {
21254 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21255 }
21256}
21257
21258fn moe_ffn_gpu(
21259 m: &MoeFfn,
21260 x: &[f32],
21261 idx: &[usize],
21262 p: &[f32],
21263 wsum: f32,
21264 pool: Option<&Pool>,
21265) -> Option<Vec<f32>> {
21266 use crate::gpu::MoeJob;
21267
21268 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21269 let mut model_ref = None;
21270 for &e in idx {
21271 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21272 moe_gpu_refused("push_job(expert)");
21273 return None;
21274 }
21275 }
21276 if let Some((se, gate)) = &m.shared {
21277 let g = gate.as_ref().map_or(1.0, |gate| {
21278 let mut gl = [0.0f32; 1];
21279 gate.matvec(x, &mut gl, pool);
21280 1.0 / (1.0 + (-gl[0]).exp())
21281 });
21282 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21283 moe_gpu_refused("push_job(shared)");
21284 return None;
21285 }
21286 }
21287 let Some(model) = model_ref else {
21288 moe_gpu_refused("no model_ref");
21289 return None;
21290 };
21291 let hidden = jobs[0].down.1;
21292 let mut out = vec![0.0f32; hidden];
21293 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21294 Some(out)
21295 } else {
21296 moe_gpu_refused("gpu::moe_block");
21297 None
21298 }
21299}
21300
21301fn ffn_forward(
21303 ffn: &FfnKind,
21304 x: &[f32],
21305 pool: Option<&Pool>,
21306 experts_allowed: Option<&[bool]>,
21307) -> Vec<f32> {
21308 match ffn {
21309 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21310 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21311 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21312 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21316 }
21317}
21318
21319fn ffn_forward_pair(
21323 ffn: &FfnKind,
21324 x1: &[f32],
21325 x2: &[f32],
21326 pool: Option<&Pool>,
21327 experts_allowed: Option<&[bool]>,
21328) -> (Vec<f32>, Vec<f32>) {
21329 let d = match ffn {
21330 FfnKind::Dense(d) if !d.segs.is_empty() => {
21333 return (
21334 tube_ffn(d, x1, 1, pool, None),
21335 tube_ffn(d, x2, 1, pool, None),
21336 );
21337 }
21338 FfnKind::Dense(d) => d,
21339 FfnKind::Moe(m) => {
21340 return (
21341 moe_ffn(m, x1, pool, experts_allowed),
21342 moe_ffn(m, x2, pool, experts_allowed),
21343 );
21344 }
21345 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21346 };
21347 let inter = d.gate_proj.rows();
21348 FFN_SCRATCH.with(|s| {
21349 let mut s = s.borrow_mut();
21350 let [g1, g2, u1, u2] = &mut *s;
21351 g1.resize(inter, 0.0);
21352 g2.resize(inter, 0.0);
21353 u1.resize(inter, 0.0);
21354 u2.resize(inter, 0.0);
21355 QTensor::matvec2_many(
21358 [&d.gate_proj, &d.up_proj],
21359 x1,
21360 x2,
21361 [g1.as_mut_slice(), u1.as_mut_slice()],
21362 [g2.as_mut_slice(), u2.as_mut_slice()],
21363 pool,
21364 );
21365 for i in 0..inter {
21366 g1[i] = d.act.combine(g1[i], u1[i]);
21367 g2[i] = d.act.combine(g2[i], u2[i]);
21368 }
21369 let mut o1 = attention::take_buf(d.down_proj.rows());
21370 let mut o2 = attention::take_buf(d.down_proj.rows());
21371 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21372 (o1, o2)
21373 })
21374}
21375
21376#[cfg(test)]
21377mod tests {
21378 use std::ffi::OsString;
21379 use std::sync::Mutex;
21380
21381 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21384
21385 struct ScopedTestEnv {
21386 previous: Vec<(&'static str, Option<OsString>)>,
21387 }
21388
21389 impl ScopedTestEnv {
21390 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21391 let previous = vars
21392 .iter()
21393 .map(|(name, _)| (*name, std::env::var_os(name)))
21394 .collect();
21395 for &(name, value) in vars {
21396 unsafe {
21397 match value {
21398 Some(value) => std::env::set_var(name, value),
21399 None => std::env::remove_var(name),
21400 }
21401 }
21402 }
21403 Self { previous }
21404 }
21405 }
21406
21407 impl Drop for ScopedTestEnv {
21408 fn drop(&mut self) {
21409 for (name, value) in self.previous.drain(..).rev() {
21410 unsafe {
21411 match value {
21412 Some(value) => std::env::set_var(name, value),
21413 None => std::env::remove_var(name),
21414 }
21415 }
21416 }
21417 }
21418 }
21419
21420 #[test]
21421 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21422 use super::pad_mtp_draft_logits;
21423
21424 let mut logits = vec![-3.0, -1.0, -2.0];
21425 pad_mtp_draft_logits(&mut logits, 6);
21426 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21427 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21428 assert_eq!(
21429 logits
21430 .iter()
21431 .enumerate()
21432 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21433 .map(|(i, _)| i),
21434 Some(1),
21435 "an unevaluated row cannot win an all-negative shortlist"
21436 );
21437 }
21438
21439 #[test]
21440 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21441 use super::{Pipeline, SpecMon, SpecTrial};
21442
21443 let mut mon = SpecMon::default();
21444 let start = std::time::Instant::now();
21445 mon.round(1.0, 1);
21446 let first = Pipeline::spec_trial_round(
21447 SpecTrial::Spec {
21448 t0: start,
21449 gen0: 0,
21450 rounds: 0,
21451 },
21452 &mut mon,
21453 1,
21454 );
21455 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21456
21457 mon.round(1.0, 1);
21458 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21459 assert!(
21460 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21461 "two verifier-tip-only rounds must stop the losing MTP trial"
21462 );
21463
21464 let mut viable = SpecMon::default();
21467 viable.round(1.0, 1);
21468 viable.round(1.0, 2);
21469 let continued = Pipeline::spec_trial_round(
21470 SpecTrial::Spec {
21471 t0: start,
21472 gen0: 0,
21473 rounds: 1,
21474 },
21475 &mut viable,
21476 3,
21477 );
21478 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21479 }
21480
21481 #[test]
21482 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21483 let mut p = spark_test_pipeline(None);
21484 let head_rows = 248_070;
21485 p.draft_full_streak = 7;
21486 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21487
21488 p.reset_fresh_mtp_draft_state();
21489 assert_eq!(
21490 p.draft_head_rows(head_rows),
21491 super::Pipeline::draft_vocab_rows(head_rows),
21492 "a fresh request returns to its configured shortlist"
21493 );
21494
21495 p.note_draft_id(u32::MAX);
21498 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21499 }
21500
21501 #[test]
21502 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21503 use super::graph_spec_wanted_by_policy;
21504
21505 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21509 assert!(!graph_spec_wanted_by_policy(
21510 Some("anything-but-1"),
21511 true,
21512 false,
21513 false,
21514 true,
21515 ));
21516 assert!(graph_spec_wanted_by_policy(
21517 Some("1"),
21518 false,
21519 true,
21520 false,
21521 true,
21522 ));
21523 assert!(!graph_spec_wanted_by_policy(
21524 Some("0"),
21525 true,
21526 false,
21527 false,
21528 true,
21529 ));
21530
21531 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21534 assert!(graph_spec_wanted_by_policy(
21535 Some("legacy-force"),
21536 false,
21537 true,
21538 true,
21539 false,
21540 ));
21541 }
21542
21543 #[test]
21544 fn encoder_hidden_states_are_normalized_and_reusable() {
21545 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21546 let ids = [1, 7, 11, 19];
21547
21548 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21549 let second = p
21550 .encode_hidden_states(&ids)
21551 .expect("reusable encoder forward");
21552 assert_eq!(first.len(), ids.len() * 8);
21553 assert!(first.iter().all(|v| v.is_finite()));
21554 assert_eq!(
21555 first, second,
21556 "encoder forward must start from a fresh state"
21557 );
21558
21559 assert!(p.forward_ids(&ids, None).is_ok());
21562 }
21563
21564 #[test]
21569 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21570 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21571 let dense_card = ChunkStackFacts {
21572 plain_dense: true,
21573 discrete: true,
21574 gpu_on: true,
21575 ..Default::default()
21576 };
21577 assert!(dense_card.dense_on_discrete());
21578 assert_eq!(
21580 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21581 DISCRETE_DENSE_PREFILL_CHUNK
21582 );
21583 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21584 for (label, facts) in [
21585 (
21586 "GDN hybrid / MoE / DeepSeek stack",
21587 ChunkStackFacts {
21588 plain_dense: false,
21589 ..dense_card
21590 },
21591 ),
21592 (
21593 "integrated GPU",
21594 ChunkStackFacts {
21595 discrete: false,
21596 ..dense_card
21597 },
21598 ),
21599 (
21600 "CPU only",
21601 ChunkStackFacts {
21602 gpu_on: false,
21603 discrete: false,
21604 ..dense_card
21605 },
21606 ),
21607 (
21608 "capacity split",
21609 ChunkStackFacts {
21610 capacity_split: true,
21611 ..dense_card
21612 },
21613 ),
21614 (
21615 "multi-GPU plan",
21616 ChunkStackFacts {
21617 multi_gpu: true,
21618 ..dense_card
21619 },
21620 ),
21621 (
21622 "O(1) layers",
21623 ChunkStackFacts {
21624 o1: true,
21625 ..dense_card
21626 },
21627 ),
21628 ] {
21629 assert!(!facts.dense_on_discrete(), "{label}");
21630 assert_eq!(
21631 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21632 48,
21633 "{label} keeps the historical x86 chunk"
21634 );
21635 }
21636 for dense in [false, true] {
21638 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21639 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21640 }
21641 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21643 for dense in [false, true] {
21644 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21645 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21646 }
21647 }
21648 }
21649
21650 #[test]
21651 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21652 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21653 let full = |host_rows, device_rows| ReuseLayer {
21654 full: true,
21655 host_rows,
21656 device_rows,
21657 device_state: false,
21658 };
21659 assert_eq!(
21662 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21663 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21664 );
21665 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21667 assert_eq!(
21669 kv_reuse_plan(339, &[full(339, Some(345))]),
21670 ReusePlan::Ready
21671 );
21672 assert_eq!(
21674 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21675 ReusePlan::Pull(vec![(0, 300, 339)])
21676 );
21677 assert_eq!(
21679 kv_reuse_plan(339, &[full(300, Some(320))]),
21680 ReusePlan::Fresh
21681 );
21682 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21683 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21684 let conv = |device_state| ReuseLayer {
21687 full: false,
21688 host_rows: 0,
21689 device_rows: None,
21690 device_state,
21691 };
21692 assert_eq!(
21693 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21694 ReusePlan::Fresh
21695 );
21696 assert_eq!(
21697 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21698 ReusePlan::Ready
21699 );
21700 }
21701
21702 #[test]
21703 fn nll_graph_policy_scopes_only_the_fused_head() {
21704 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21705 ("vulkan graph", true, true, false, true, false),
21707 ("native Metal graph", true, true, true, true, true),
21709 ("masked", false, true, false, false, false),
21711 ("graph disabled", true, false, true, false, false),
21712 ] {
21713 let (graph_quality, graph_head_required) =
21714 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21715 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21716 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21717 }
21718 }
21719
21720 #[test]
21721 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21722 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21723 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21724 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21725 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21726 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21727 }
21728
21729 #[test]
21730 fn cancel_flag_stops_generation() {
21731 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21732 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21735 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21736 assert_eq!(r.finish_reason, "cancelled");
21737 assert!(
21738 r.token_ids.is_empty(),
21739 "no tokens after cancel: {:?}",
21740 r.token_ids
21741 );
21742 assert_eq!(p.kv_cache.seq_len(), 0);
21743 assert!(p.kv_history.is_empty());
21744 assert!(!p.graph_want_logits);
21745 assert!(p.graph_logits.is_none());
21746 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21748 assert_ne!(r2.finish_reason, "cancelled");
21749 }
21750 use super::*;
21751
21752 #[test]
21753 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21754 let arch = |arch_name: &str, hidden_act: &str| {
21758 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21759 "arch_name": arch_name,
21760 "hidden_size": 8,
21761 "intermediate_size": 16,
21762 "num_layers": 1,
21763 "num_attention_heads": 1,
21764 "num_kv_heads": 1,
21765 "head_dim": 8,
21766 "vocab_size": 16,
21767 "layer_types": ["FullAttention"],
21768 "rms_norm_eps": 1e-6,
21769 "max_position_embeddings": 16,
21770 "hidden_act": hidden_act
21771 }))
21772 .expect("minimal architecture")
21773 };
21774 let minimax = arch("minimax_m3_vl", "swigluoai");
21775 assert_eq!(
21776 Act::from_arch_full(&minimax),
21777 Act::SwiGluOai {
21778 alpha: 1.702,
21779 limit: 7.0,
21780 }
21781 );
21782
21783 let unrelated = arch("qwen2", "swigluoai");
21786 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21787
21788 let non_oai = arch("minimax_m3_vl", "gelu");
21792 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21793 }
21794
21795 #[test]
21796 fn minimax_swiglu_oai_act_matches_scalar_contract() {
21797 let act = Act::swiglu_oai(1.702, 7.0);
21798 let got = act.combine(10.0, -10.0);
21799 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
21800 assert!((got - expected).abs() < 1e-6);
21801 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
21802 assert_eq!(
21803 act.wgpu_graph_act(),
21804 Some(crate::gpu::GraphAct::SwiGluOaiM3),
21805 "the WGPU-only arm accepts only the frozen M3 constants"
21806 );
21807 assert_eq!(
21808 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
21809 None,
21810 "nearby OAI constants must not borrow MiniMax's kernel"
21811 );
21812 assert_eq!(
21813 act.moe_job_graph_contract(),
21814 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
21815 "the direct MoE path carries the frozen clamp with the marker"
21816 );
21817 assert_eq!(
21818 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
21819 None,
21820 "a nearby clamp must not borrow MiniMax's device contract"
21821 );
21822
21823 let gate_only = act.apply(-10.0);
21826 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
21827 assert!((gate_only - expected_gate).abs() < 1e-10);
21828 }
21829
21830 #[test]
21831 fn explicit_sliding_schedule_selects_local_rope_profile() {
21832 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
21837 p.swa = Some((1024, usize::MAX));
21838 p.sliding_layers = Some(vec![true, true, true, false]);
21839 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
21840 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
21841 p.rope_scale = 1.277_258_9;
21842 p.rope_scale_local = 1.0;
21843
21844 for li in 0..3 {
21845 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
21846 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
21847 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
21848 }
21849 assert_eq!(p.layer_window(3), None);
21850 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
21851 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
21852 }
21853
21854 #[test]
21862 fn dynamic_ffn_equals_the_zeroing_arm() {
21863 let (hidden, inter) = (8usize, 32usize);
21864 let synth = |n: usize, salt: usize| -> Vec<f32> {
21865 (0..n)
21866 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
21867 .collect()
21868 };
21869 let down = synth(hidden * inter, 3);
21870 let mut down_t = vec![0.0f32; inter * hidden];
21871 for r in 0..hidden {
21872 for c in 0..inter {
21873 down_t[c * hidden + r] = down[r * inter + c];
21874 }
21875 }
21876 let d = DenseFfn {
21877 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21878 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21879 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
21880 act: Act::Silu,
21881 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
21882 segs: Vec::new(),
21883 };
21884 let x = synth(hidden, 11);
21885 let k = 12usize;
21886 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
21887 let mut g = vec![0.0f32; inter];
21889 d.gate_proj.matvec(&x, &mut g, None);
21890 let mut u = vec![0.0f32; inter];
21891 d.up_proj.matvec(&x, &mut u, None);
21892 for v in g.iter_mut() {
21893 *v = inference::silu(*v);
21894 }
21895 keep_top_k(&mut g, k);
21896 for i in 0..inter {
21897 g[i] *= u[i];
21898 }
21899 let mut want = vec![0.0f32; hidden];
21900 d.down_proj.matvec(&g, &mut want, None);
21901 for (a, b) in want.iter().zip(&got) {
21902 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
21903 }
21904 }
21905
21906 #[test]
21912 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
21913 let (hidden, core, tube) = (8usize, 12usize, 8usize);
21914 let inter = core + tube;
21915 let synth = |n: usize, salt: usize| -> Vec<f32> {
21916 (0..n)
21917 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
21918 .collect()
21919 };
21920 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
21921 let d_all = synth(hidden * inter, 3);
21922 let dense = DenseFfn {
21924 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
21925 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
21926 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
21927 act: Act::Silu,
21928 down_t: None,
21929 segs: Vec::new(),
21930 };
21931 let rows =
21932 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
21933 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
21934 let mut o = Vec::with_capacity(hidden * (b - a));
21935 for r in 0..hidden {
21936 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
21937 }
21938 o
21939 };
21940 let tubed = DenseFfn {
21941 down_t: None,
21942 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
21943 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
21944 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
21945 act: Act::Silu,
21946 segs: vec![FfnSeg {
21947 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
21948 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
21949 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
21950 start: core,
21951 width: tube,
21952 }],
21953 };
21954 let x = synth(hidden, 7);
21955 let want = dense_ffn(&dense, &x, None);
21956 let got = tube_ffn(&tubed, &x, 1, None, None);
21957 for (a, b) in want.iter().zip(&got) {
21958 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
21959 }
21960 let mut bits = vec![0u8; inter.div_ceil(8)];
21962 for n in 0..core {
21963 bits[n / 8] |= 1 << (n % 8);
21964 }
21965 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21966 let masked = dense_ffn_masked(&dense, &x, None, &bits);
21967 for (a, b) in masked.iter().zip(&closed) {
21968 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
21969 }
21970 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
21972 for (a, b) in closed.iter().zip(&batch) {
21973 assert_eq!(a, b, "batch arm disagrees with decode arm");
21974 }
21975 }
21976
21977 #[test]
21979 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
21980 let (hidden, inter) = (16usize, 40usize);
21981 let synth = |n: usize, salt: usize| -> Vec<f32> {
21982 (0..n)
21983 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
21984 .collect()
21985 };
21986 let d = DenseFfn {
21987 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
21988 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
21989 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
21990 act: Act::Silu,
21991 down_t: None,
21992 segs: Vec::new(),
21993 };
21994 let x = synth(hidden, 9);
21995 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
21997
21998 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
21999
22000 let mut g = vec![0.0f32; inter];
22002 d.gate_proj.matvec(&x, &mut g, None);
22003 let mut u = vec![0.0f32; inter];
22004 d.up_proj.matvec(&x, &mut u, None);
22005 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22006 for i in 0..inter {
22007 g[i] = if act_set.contains(&(i as u16)) {
22008 inference::silu(g[i]) * u[i]
22009 } else {
22010 0.0
22011 };
22012 }
22013 let mut reference = vec![0.0f32; hidden];
22014 d.down_proj.matvec(&g, &mut reference, None);
22015
22016 let max_d = sparse
22017 .iter()
22018 .zip(&reference)
22019 .map(|(a, b)| (a - b).abs())
22020 .fold(0.0f32, f32::max);
22021 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22022 }
22023
22024 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22027 let (h, inter, heads, kv, hd) = (
22028 p.hidden_size,
22029 inter,
22030 p.num_heads,
22031 p.num_kv_heads,
22032 p.head_dim,
22033 );
22034 let synth = |n: usize, salt: usize| -> Vec<f32> {
22035 (0..n)
22036 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22037 .collect()
22038 };
22039 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22040 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22041 };
22042 p.mtp = Some(MtpModule {
22043 enorm: vec![1.0; h],
22044 hnorm: vec![1.0; h],
22045 eh_proj: qt(h, 2 * h, 301),
22046 layer: LayerWeights {
22047 input_norm: vec![1.0; h],
22048 post_norm: vec![1.0; h],
22049 attn_out_norm: None,
22050 ffn_out_norm: None,
22051 layer_scale: None,
22052 ffn: FfnKind::Dense(DenseFfn {
22053 gate_proj: qt(inter, h, 315),
22054 up_proj: qt(inter, h, 316),
22055 down_proj: qt(h, inter, 317),
22056 act: Act::Silu,
22057 down_t: None,
22058 segs: Vec::new(),
22059 }),
22060 attn: AttnKind::Full {
22061 bias: None,
22062 wq: qt(heads * hd, h, 311),
22063 wk: qt(kv * hd, h, 312),
22064 wv: qt(kv * hd, h, 313),
22065 wo: qt(h, heads * hd, 314),
22066 q_norm: None,
22067 k_norm: None,
22068 output_gate: false,
22069 softplus_gate: None,
22070 },
22071 },
22072 final_norm: vec![1.0; h],
22073 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22074 });
22075 }
22076
22077 fn attach_test_mtp(p: &mut Pipeline) {
22079 attach_test_mtp_width(p, p.intermediate_size);
22080 }
22081
22082 #[test]
22083 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22084 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22085 attach_test_mtp_width(&mut p, 11);
22088 let ids = [3, 9, 4, 12, 7, 2];
22089 let trunk = p.probe_ffn_mass(&ids);
22090 assert_eq!(trunk[0].len(), p.intermediate_size);
22091
22092 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22093 assert_eq!(got.len(), 1);
22094 assert_eq!(got[0].mass.len(), 11);
22095 assert_eq!(got[0].squared_mass.len(), 11);
22096 assert_eq!(got[0].observed_positions, ids.len() - 1);
22097 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22098 assert!(got[0]
22099 .mass
22100 .iter()
22101 .zip(&got[0].squared_mass)
22102 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22103 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22104 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22105
22106 let h = p.hidden_size;
22110 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22111 panic!("synthetic MTP must be dense")
22112 };
22113 dense.segs.push(FfnSeg {
22114 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22115 up: QTensor::from_f32(vec![0.0; h], 1, h),
22116 down: QTensor::from_f32(vec![0.0; h], h, 1),
22117 start: dense.gate_proj.rows(),
22118 width: 1,
22119 });
22120 let err = p.probe_mtp_ffn(&ids);
22121 assert!(err
22122 .unwrap_err()
22123 .contains("tube-segmented draft FFNs are not supported"));
22124 assert_eq!(p.kv_cache.seq_len(), 0);
22125 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22126 }
22127
22128 #[test]
22129 fn speculative_equals_vanilla_greedy() {
22130 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22131 let _env = ScopedTestEnv::set(&[
22134 ("CMF_GPU", Some("0")),
22135 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22136 ("CMF_MTP_PAIR", Some("1")),
22137 ]);
22138 let run = |spec: bool| {
22139 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22140 p.sampler_config.temperature = 0.0;
22141 attach_test_mtp(&mut p);
22142 p.speculative = spec;
22143 let r = p.generate("abcdef", 12, None, None).unwrap();
22144 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22145 };
22146 let (vanilla, d0, _) = run(false);
22147 let (spec, d1, a1) = run(true);
22148 assert_eq!(d0, 0, "vanilla path must not draft");
22149 assert!(d1 > 0, "speculative path must draft");
22150 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22151 assert_eq!(
22152 vanilla, spec,
22153 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22154 );
22155 }
22156
22157 #[test]
22158 fn speculative_accepts_constant_oracle() {
22159 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22160 let _env = ScopedTestEnv::set(&[
22161 ("CMF_GPU", Some("0")),
22162 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22163 ("CMF_MTP_PAIR", Some("1")),
22164 ]);
22165 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22166 p.sampler_config.temperature = 0.0;
22167 p.sampler_config.repetition_penalty = 1.0;
22168 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22171 attach_test_mtp(&mut p);
22172 p.speculative = true;
22173 let r = p.generate("abcd", 10, None, None).unwrap();
22174 assert!(r.mtp_drafted > 0);
22175 assert_eq!(
22176 r.mtp_accepted, r.mtp_drafted,
22177 "constant logits → every draft accepted"
22178 );
22179 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22182 }
22183
22184 #[test]
22185 fn host_fused_mtp_pair_is_opt_in() {
22186 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22187 let _env = ScopedTestEnv::set(&[
22188 ("CMF_GPU", Some("0")),
22189 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22190 ("CMF_MTP_PAIR", None),
22191 ]);
22192 let run = |spec: bool| {
22193 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22194 p.sampler_config.temperature = 0.0;
22195 attach_test_mtp(&mut p);
22196 p.speculative = spec;
22197 let r = p.generate("abcdef", 12, None, None).unwrap();
22198 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22199 };
22200 let (plain, d0, _) = run(false);
22201 let (guarded, d1, a1) = run(true);
22202 assert_eq!(d0, 0);
22203 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22204 assert_eq!(a1, 0);
22205 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22206 }
22207
22208 #[test]
22209 fn empty_prompt_is_an_error_not_a_panic() {
22210 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22211 let r = p.generate("", 4, None, None);
22212 assert!(r.is_err(), "empty prompt must be a clean error");
22213 }
22214
22215 #[test]
22216 fn every_token_enters_kv_exactly_once() {
22217 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22218 p.sampler_config.temperature = 0.0;
22220 let r = p.generate("abc", 2, None, None).unwrap();
22221 assert_eq!(r.prompt_tokens, 3);
22222 assert_eq!(
22226 p.kv_cache.seq_len(),
22227 3 + r.tokens_generated - 1,
22228 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22229 );
22230 }
22231
22232 #[test]
22233 fn generation_is_reproducible_with_seed() {
22234 let run = || {
22235 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22236 p.generate("hello", 8, None, None).unwrap().token_ids
22237 };
22238 assert_eq!(run(), run());
22239 }
22240
22241 #[test]
22242 fn resetting_sampler_restarts_the_seeded_stream() {
22243 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22244 let config = SamplerConfig {
22245 seed: Some(1234),
22246 ..SamplerConfig::default()
22247 };
22248 p.set_sampler_config(config.clone());
22249 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22250 p.set_sampler_config(config);
22251 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22252 assert_eq!(first, second);
22253 }
22254
22255 #[test]
22256 fn eviction_bounds_the_cache() {
22257 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22258 p.kv_cache.max_seq_len = 6;
22259 p.sampler_config.temperature = 0.0;
22260 let _ = p.generate("abcd", 12, None, None).unwrap();
22261 assert!(
22262 p.kv_cache.seq_len() <= 6 + 1,
22263 "cache must stay bounded by max_seq_len (got {})",
22264 p.kv_cache.seq_len()
22265 );
22266 }
22267
22268 #[test]
22269 fn confidence_matches_tokens_and_is_a_probability() {
22270 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22271 p.sampler_config.temperature = 0.0;
22272 p.sampler_config.repetition_penalty = 1.0;
22273 let r = p.generate("abcd", 10, None, None).unwrap();
22274 assert_eq!(
22275 r.token_confidence.len(),
22276 r.token_ids.len(),
22277 "one confidence per emitted token"
22278 );
22279 for &c in &r.token_confidence {
22280 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22281 }
22282 let logits = [1.0f32, 3.0, 0.5, 3.0];
22284 let p0 = top1_prob_t(&logits, 1, 1.0);
22285 let p1 = top1_prob_t(&logits, 3, 1.0);
22286 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22287 assert!(p0 > 0.0 && p0 < 1.0);
22288 let sharp = top1_prob_t(&logits, 1, 1.0);
22290 let soft = top1_prob_t(&logits, 1, 2.0);
22291 assert!(soft < sharp, "higher temperature lowers peak confidence");
22292 }
22293
22294 #[test]
22295 fn trace_is_opt_in_and_parallels_the_output() {
22296 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22298 p.sampler_config.temperature = 0.0;
22299 p.sampler_config.repetition_penalty = 1.0;
22300 let r = p.generate("abcd", 10, None, None).unwrap();
22301 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22302
22303 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22305 p.sampler_config.temperature = 0.0;
22306 p.sampler_config.repetition_penalty = 1.0;
22307 p.set_trace(true);
22308 let r = p.generate("abcd", 10, None, None).unwrap();
22309 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22310 for (i, tr) in r.traces.iter().enumerate() {
22311 assert_eq!(tr.t, i, "trace index is sequential");
22312 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22313 assert_eq!(
22314 tr.confidence, r.token_confidence[i],
22315 "trace confidence matches the confidence channel"
22316 );
22317 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22319 }
22320 }
22321
22322 #[test]
22323 fn explain_prefill_logits_match_greedy_first_token() {
22324 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22328 p.sampler_config.temperature = 0.0;
22329 p.sampler_config.repetition_penalty = 1.0;
22330 let ids = p.tokenizer.encode("abcd");
22331 let logits = p.prefill_next_logits(&ids, None);
22332 let argmax = logits
22333 .iter()
22334 .enumerate()
22335 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22336 .unwrap()
22337 .0 as u32;
22338 let r = p.generate("abcd", 1, None, None).unwrap();
22339 assert_eq!(
22340 argmax, r.token_ids[0],
22341 "explain preview must match greedy emit"
22342 );
22343 }
22344
22345 #[test]
22346 fn laguna_shared_expert_is_unconditionally_added() {
22347 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22348 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22349 let zero_dense = || DenseFfn {
22350 gate_proj: matrix(vec![0.0; 4]),
22351 up_proj: matrix(vec![0.0; 4]),
22352 down_proj: matrix(vec![0.0; 4]),
22353 act: Act::Silu,
22354 down_t: None,
22355 segs: Vec::new(),
22356 };
22357 let shared = DenseFfn {
22358 gate_proj: identity(),
22359 up_proj: identity(),
22360 down_proj: identity(),
22361 act: Act::Silu,
22362 down_t: None,
22363 segs: Vec::new(),
22364 };
22365 let x = [1.0, 2.0];
22366 let expected = dense_ffn(&shared, &x, None);
22367 let moe = MoeFfn {
22368 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22369 experts: vec![zero_dense()],
22370 top_k: 1,
22371 norm_topk_prob: true,
22372 router_sigmoid: true,
22373 sigmoid_denom_eps: 1e-6,
22374 expert_bias: None,
22375 routed_scaling: 1.0,
22376 route_tau: None,
22377 shared: Some((shared, None)),
22378 stats: std::cell::RefCell::new(Vec::new()),
22379 act_sq: std::cell::RefCell::new(Vec::new()),
22380 act_rows: std::cell::RefCell::new(Vec::new()),
22381 mask: None,
22382 per_expert_scale: None,
22383 router_input_norm: false,
22384 resonance: None,
22385 grown: Vec::new(),
22386 };
22387 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22388 for (actual, expected) in actual.iter().zip(expected) {
22389 assert!((actual - expected).abs() < 1e-6);
22390 }
22391 }
22392
22393 fn mimo_test_pipeline() -> Pipeline {
22402 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22403 let kvh = [1usize, 2, 2, 1];
22404 let synth = |n: usize, salt: usize| -> Vec<f32> {
22405 (0..n)
22406 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22407 .collect()
22408 };
22409 let qt = |rows: usize, cols: usize, salt: usize| {
22410 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22411 };
22412 let dense = |inter: usize, salt: usize| DenseFfn {
22413 gate_proj: qt(inter, hs, salt),
22414 up_proj: qt(inter, hs, salt + 1),
22415 down_proj: qt(hs, inter, salt + 2),
22416 act: Act::Silu,
22417 down_t: None,
22418 segs: Vec::new(),
22419 };
22420 let layers: Vec<LayerWeights> = (0..4)
22421 .map(|li| LayerWeights {
22422 input_norm: vec![1.0; hs],
22423 post_norm: vec![1.0; hs],
22424 attn_out_norm: None,
22425 ffn_out_norm: None,
22426 layer_scale: None,
22427 ffn: if li == 0 {
22428 FfnKind::Dense(dense(inter, 50))
22429 } else {
22430 FfnKind::Moe(MoeFfn {
22431 router: qt(4, hs, 60 + li),
22432 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22433 top_k: 2,
22434 norm_topk_prob: true,
22435 router_sigmoid: true,
22436 sigmoid_denom_eps: 1e-6,
22437 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22438 routed_scaling: 1.0,
22439 route_tau: None,
22440 shared: None,
22441 stats: std::cell::RefCell::new(Vec::new()),
22442 act_sq: std::cell::RefCell::new(Vec::new()),
22443 act_rows: std::cell::RefCell::new(Vec::new()),
22444 mask: None,
22445 per_expert_scale: None,
22446 router_input_norm: false,
22447 resonance: None,
22448 grown: Vec::new(),
22449 })
22450 },
22451 attn: AttnKind::Full {
22452 wq: qt(nh * hd, hs, li * 10 + 1),
22453 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22454 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22455 wo: qt(hs, nh * vd, li * 10 + 4),
22456 q_norm: None,
22457 k_norm: None,
22458 output_gate: false,
22459 softplus_gate: None,
22460 bias: None,
22461 },
22462 })
22463 .collect();
22464 let mut p = Pipeline::new(
22465 Tokenizer::byte_level(),
22466 PipelineWeights {
22467 embed_tokens: qt(vocab, hs, 100),
22468 layers,
22469 lm_head: qt(vocab, hs, 200),
22470 final_norm: vec![1.0; hs],
22471 },
22472 hs,
22473 inter,
22474 nh,
22475 1, hd,
22477 4,
22478 4,
22479 false,
22480 vocab,
22481 1e-6,
22482 1e7,
22483 NormStyle::Qwen,
22484 4096,
22485 SamplerConfig {
22486 seed: Some(7),
22487 ..Default::default()
22488 },
22489 );
22490 p.layer_dump = None;
22492 p.set_rotary(4, 1e7);
22493 p.sliding_layers = Some(vec![false, true, true, false]);
22494 p.swa = Some((3, usize::MAX));
22495 p.rotary_dim_local = Some(4);
22496 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22497 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22498 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22499 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22500 p
22501 }
22502
22503 #[test]
22504 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22505 let mut p = mimo_test_pipeline();
22506 p.speculative = false;
22507 p.ignore_eos = true;
22508 p.sampler_config.temperature = 0.0;
22509 p.sampler_config.repetition_penalty = 1.0;
22510 let a = vec![3, 5, 7, 9, 11, 13];
22511 let b = vec![4, 8, 12, 16, 20, 24];
22512 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22513 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22514 let actual = p
22518 .generate_from_embeds(&a, &rows, 8, None, None)
22519 .unwrap()
22520 .token_ids;
22521 assert_eq!(actual, expected);
22522 assert!(p.kv_history.is_empty());
22523 let mut extended = a.clone();
22524 extended.push(17);
22525 let after_media = p
22526 .generate_from_ids(&extended, 8, None, None)
22527 .unwrap()
22528 .token_ids;
22529 p.reset_session();
22530 let fresh = p
22531 .generate_from_ids(&extended, 8, None, None)
22532 .unwrap()
22533 .token_ids;
22534 assert_eq!(after_media, fresh);
22535 assert!(
22536 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22537 .is_err()
22538 );
22539 p.reset_session();
22543 p.generate_from_ids(&a, 1, None, None).unwrap();
22544 let mut media_ids = p.kv_history.clone();
22545 assert!(!media_ids.is_empty());
22546 media_ids.extend_from_slice(&[19, 21, 23]);
22547 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22548 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22549 let mut oracle = mimo_test_pipeline();
22550 oracle.speculative = false;
22551 oracle.ignore_eos = true;
22552 oracle.sampler_config.temperature = 0.0;
22553 oracle.sampler_config.repetition_penalty = 1.0;
22554 let expected = oracle
22555 .generate_from_ids(&source_ids, 8, None, None)
22556 .unwrap()
22557 .token_ids;
22558 assert_eq!(
22559 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22560 .unwrap()
22561 .token_ids,
22562 expected
22563 );
22564 assert!(p.kv_history.is_empty());
22565 let mut bad = rows;
22566 bad[0] = f32::NAN;
22567 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22568 }
22569
22570 fn f32_bits(v: &[f32]) -> Vec<u32> {
22571 v.iter().map(|x| x.to_bits()).collect()
22572 }
22573
22574 #[test]
22581 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22582 let mut p = mimo_test_pipeline();
22583 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22584 assert_eq!(kv, vec![1, 2, 2, 1]);
22585 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22586 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22587 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22588 let hs = p.hidden_size;
22589 let mut decode = Vec::new();
22590 for (pos, &id) in ids.iter().enumerate() {
22591 let e = p.embed_single(id);
22592 let h = p.forward_layers(&e, pos, None);
22593 decode.push(p.logits_from_hidden(&h));
22594 }
22595 for l in &p.kv_cache.layers {
22596 assert_eq!(l.seq_len, 12);
22597 assert_eq!(l.head_values(0).len(), 12 * 8);
22599 }
22600 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22601
22602 p.clear_sequence_state();
22603 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22604 for pos in 0..ids.len() {
22605 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22606 assert_eq!(
22607 f32_bits(&decode[pos]),
22608 f32_bits(&lg),
22609 "whole prompt, pos {pos}"
22610 );
22611 }
22612
22613 p.clear_sequence_state();
22614 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22615 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22616 for pos in 0..ids.len() {
22617 let row = if pos < 5 {
22618 &a[pos * hs..(pos + 1) * hs]
22619 } else {
22620 &b[(pos - 5) * hs..(pos - 4) * hs]
22621 };
22622 let lg = p.logits_from_hidden(row);
22623 assert_eq!(
22624 f32_bits(&decode[pos]),
22625 f32_bits(&lg),
22626 "two chunks, pos {pos}"
22627 );
22628 }
22629
22630 let last = |p: &mut Pipeline| {
22633 p.clear_sequence_state();
22634 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22635 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22636 };
22637 let base = last(&mut p);
22638 let mut no_sinks = mimo_test_pipeline();
22639 for l in &mut no_sinks.kv_cache.layers {
22640 l.sinks = None;
22641 }
22642 assert_ne!(
22643 f32_bits(&last(&mut no_sinks)),
22644 f32_bits(&base),
22645 "sinks are live"
22646 );
22647 let mut wide = mimo_test_pipeline();
22648 wide.swa = Some((64, usize::MAX));
22649 assert_ne!(
22650 f32_bits(&last(&mut wide)),
22651 f32_bits(&base),
22652 "window is live"
22653 );
22654
22655 p.clear_sequence_state();
22657 p.ignore_eos = true;
22658 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22659 assert_eq!(r.token_ids.len(), 4);
22660 }
22661
22662 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22665 let (hs, nh) = (16usize, 4usize);
22666 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22667 p.layer_dump = None;
22668 p.swa = Some((6, 4));
22669 p.proj_gate_sigmoid = true;
22670 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22671 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22672 let g: Vec<f32> = (0..nh * hs)
22673 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22674 .collect();
22675 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22676 }
22677 }
22678 p.swa_trim = trim;
22679 p
22680 }
22681
22682 #[test]
22688 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22689 let mut a = spark_test_pipeline(None);
22690 let mut b = spark_test_pipeline(Some((2, 4)));
22691 assert_eq!(
22692 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22693 vec![Some(6), Some(6), Some(6), None]
22694 );
22695 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22696 let mut pos = 0usize;
22697 for &n in [5usize, 13, 7, 16].iter().cycle() {
22698 if pos >= ids.len() {
22699 break;
22700 }
22701 let end = (pos + n).min(ids.len());
22702 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22703 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22704 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22705 pos = end;
22706 }
22707 for _ in 0..23 {
22708 let e = a.embed_single(((pos * 7) % 64) as u32);
22709 let ha = a.forward_layers(&e, pos, None);
22710 let hb = b.forward_layers(&e, pos, None);
22711 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22712 for li in 0..3 {
22713 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22714 }
22715 pos += 1;
22716 }
22717 for round in 0..9 {
22718 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22719 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22720 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22721 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22722 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22723 if round % 2 == 1 {
22724 for p in [&mut a, &mut b] {
22726 for l in &mut p.kv_cache.layers {
22727 l.truncate_last(2);
22728 }
22729 }
22730 } else {
22731 pos += 2;
22732 }
22733 }
22734 let e = a.embed_single(9);
22735 let (ha, hb) = (
22736 a.forward_layers(&e, pos, None),
22737 b.forward_layers(&e, pos, None),
22738 );
22739 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22740 for li in 0..3 {
22741 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22742 assert!(lb.base() > 0, "layer {li} trimmed");
22743 assert_eq!(la.base(), 0);
22744 assert_eq!(lb.pos_len(), la.seq_len);
22745 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22746 }
22747 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22748 assert_eq!(
22749 (gb.base(), gb.seq_len),
22750 (0, ga.seq_len),
22751 "the global layer keeps all"
22752 );
22753 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22754 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22755 }
22756
22757 #[test]
22762 fn swa_trim_wire_handoff_continues_bitwise() {
22763 let mut a = spark_test_pipeline(None);
22764 let mut b = spark_test_pipeline(Some((2, 4)));
22765 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22766 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22767 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22768 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22769 let mut pos = ids.len();
22770 for _ in 0..5 {
22771 let e = a.embed_single(((pos * 5) % 64) as u32);
22772 assert_eq!(
22773 f32_bits(&a.forward_layers(&e, pos, None)),
22774 f32_bits(&b.forward_layers(&e, pos, None))
22775 );
22776 pos += 1;
22777 }
22778 let mut c = spark_test_pipeline(Some((2, 4)));
22779 for li in 0..4 {
22780 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22781 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22782 }
22783 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22784 assert_eq!(c.kv_cache.seq_len(), pos);
22785 for step in 0..17 {
22786 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22787 let ha = a.forward_layers(&e, pos, None);
22788 let hc = c.forward_layers(&e, pos, None);
22789 assert_eq!(
22790 f32_bits(&ha),
22791 f32_bits(&hc),
22792 "after the hand-off, step {step}"
22793 );
22794 pos += 1;
22795 }
22796 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
22797 }
22798
22799 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
22804 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
22805 let synth = |len: usize, salt: usize| -> Vec<f32> {
22806 (0..len)
22807 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
22808 .collect()
22809 };
22810 let qt = |rows: usize, cols: usize, salt: usize| {
22811 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22812 };
22813 let layers = widths
22814 .iter()
22815 .copied()
22816 .enumerate()
22817 .map(|(k, inter)| {
22818 let s = 500 + k * 40;
22819 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
22820 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
22821 MtpModule {
22822 enorm: vec![1.0; hs],
22823 hnorm: vec![1.0; hs],
22824 eh_proj: qt(hs, 2 * hs, s),
22825 layer: LayerWeights {
22826 input_norm: vec![1.0; hs],
22827 post_norm: vec![1.0; hs],
22828 attn_out_norm: None,
22829 ffn_out_norm: None,
22830 layer_scale: None,
22831 attn: AttnKind::Full {
22832 wq: qt(nh * hd, hs, s + 1),
22833 wk: qt(nkv * hd, hs, s + 2),
22834 wv: qt(nkv * vd, hs, s + 3),
22835 wo: qt(hs, nh * vd, s + 4),
22836 q_norm: None,
22837 k_norm: None,
22838 output_gate: false,
22839 softplus_gate: None,
22840 bias: None,
22841 },
22842 ffn: FfnKind::Dense(DenseFfn {
22843 gate_proj: qt(inter, hs, s + 5),
22844 up_proj: qt(inter, hs, s + 6),
22845 down_proj: qt(hs, inter, s + 7),
22846 act: Act::Silu,
22847 down_t: None,
22848 segs: Vec::new(),
22849 }),
22850 },
22851 final_norm: vec![1.0; hs],
22852 kv,
22853 }
22854 })
22855 .collect();
22856 mimo_mtp::MimoMtp::from_layers(layers)
22857 }
22858
22859 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
22860 mimo_test_mtp_widths(&vec![24; n], gain)
22861 }
22862
22863 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
22864 p.clear_sequence_state();
22865 p.speculative = spec;
22866 p.ignore_eos = true;
22867 p.sampler_config.temperature = 0.0;
22868 p.generate_from_ids(ids, n, None, None).unwrap()
22869 }
22870
22871 #[test]
22872 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
22873 let mut p = mimo_test_pipeline();
22874 let widths = [7usize, 13, 29];
22875 assert_ne!(widths[0], p.intermediate_size);
22876 assert_ne!(widths[1], p.intermediate_size);
22877 assert_ne!(widths[2], p.intermediate_size);
22878 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
22879 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
22880
22881 let trunk = p.probe_ffn_mass(&ids);
22882 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
22883 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
22884 assert_eq!(got.len(), widths.len());
22885 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
22886 assert_eq!(stat.mass.len(), width, "draft layer {k}");
22887 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
22888 assert_eq!(stat.observed_positions, ids.len() - k - 1);
22889 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
22890 assert!(stat
22891 .mass
22892 .iter()
22893 .zip(&stat.squared_mass)
22894 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22895 }
22896 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
22897 let st = p.mimo_mtp.as_ref().unwrap();
22898 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
22899
22900 let h = p.hidden_size;
22903 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
22904 panic!("synthetic MiMo MTP must be dense")
22905 };
22906 dense.segs.push(FfnSeg {
22907 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22908 up: QTensor::from_f32(vec![0.0; h], 1, h),
22909 down: QTensor::from_f32(vec![0.0; h], h, 1),
22910 start: dense.gate_proj.rows(),
22911 width: 1,
22912 });
22913 assert!(p
22914 .probe_mtp_ffn(&ids)
22915 .unwrap_err()
22916 .contains("tube-segmented draft FFNs are not supported"));
22917 assert_eq!(p.kv_cache.seq_len(), 0);
22918 assert!(p
22919 .mimo_mtp
22920 .as_ref()
22921 .unwrap()
22922 .layers
22923 .iter()
22924 .all(|m| m.kv.seq_len == 0));
22925 }
22926
22927 #[test]
22928 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
22929 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
22930 p.clear_sequence_state();
22931 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
22932 let _exact = ExactMtpCalibrationGuard::enter();
22933 let got = crate::gpu::cpu_scope(|| {
22934 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, rows)
22935 })
22936 .expect("teacher-forced MiMo probe");
22937 st.reset();
22938 p.mimo_mtp = Some(st);
22939 p.reset_mtp_probe_state();
22940 got
22941 }
22942
22943 for post_norm_hidden in [false, true] {
22944 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
22945 let make = || {
22946 let mut p = mimo_test_pipeline();
22947 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
22948 st.post_norm_hidden = post_norm_hidden;
22949 p.mimo_mtp = Some(st);
22950 p
22951 };
22952 let mut full = make();
22953 let expected = collect(&mut full, &ids, usize::MAX);
22954 for rows in [1usize, 7, 128] {
22955 let mut chunked = make();
22956 let got = collect(&mut chunked, &ids, rows);
22957 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
22958 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
22959 assert_eq!(
22960 got.observed_positions, expected.observed_positions,
22961 "post={post_norm_hidden}, rows={rows}, layer={layer}"
22962 );
22963 assert_eq!(
22964 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
22965 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
22966 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
22967 );
22968 assert_eq!(
22969 got.squared_mass
22970 .iter()
22971 .map(|v| v.to_bits())
22972 .collect::<Vec<_>>(),
22973 expected
22974 .squared_mass
22975 .iter()
22976 .map(|v| v.to_bits())
22977 .collect::<Vec<_>>(),
22978 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
22979 );
22980 }
22981 }
22982 }
22983 }
22984
22985 #[test]
22991 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
22992 for post in [false, true] {
22995 let mut p = mimo_test_pipeline();
22996 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
22998 let mut st0 = mimo_test_mtp(3, 1.0);
22999 st0.post_norm_hidden = post;
23000 p.mimo_mtp = Some(st0);
23001 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23002 let hs = p.hidden_size;
23003 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23004 p.mimo_note_rows(&hb, 0);
23005 let mut st = p.mimo_mtp.take().unwrap();
23006 let k = 3;
23009 let mut inc = Vec::new();
23010 for t in 0..ids.len() - k - 1 {
23011 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23012 }
23013 let s = ids.len();
23016 let mut reference = vec![vec![0u32; k]; s - k - 1];
23017 let mut fresh = mimo_test_mtp(3, 1.0);
23018 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23019 let n = s - layer - 1;
23020 let mut cats = vec![0.0f32; n * 2 * hs];
23021 for j in 0..n {
23022 let e = p.embed_single(ids[j + layer + 1]);
23023 let raw = &hb[j * hs..(j + 1) * hs];
23024 let g = if post {
23025 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23026 } else {
23027 raw.to_vec()
23028 };
23029 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23030 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23031 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23032 }
23033 let mut x = vec![0.0f32; n * hs];
23034 m.eh_proj.matmat(&cats, n, &mut x, None);
23035 p.mimo_mtp_block(m, &mut x, n, 0);
23036 for (t, row) in reference.iter_mut().enumerate() {
23037 let y = inference::rms_norm(
23038 &x[t * hs..(t + 1) * hs],
23039 &m.final_norm,
23040 p.rms_eps,
23041 p.norm_style,
23042 );
23043 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23044 }
23045 }
23046 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23047 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23049 assert!(distinct.len() > 3, "{inc:?}");
23050 let last_t = ids.len() - k - 2;
23052 for m in &st.layers {
23053 assert_eq!(m.kv.seq_len, last_t + 1);
23054 }
23055 }
23056 }
23057
23058 #[test]
23065 fn mimo_speculative_greedy_equals_plain_greedy() {
23066 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23067 let _env = ScopedTestEnv::set(&[
23068 ("CMF_GPU", Some("0")),
23069 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23070 ]);
23071 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23072 let n = 24;
23073 let mut p = mimo_test_pipeline();
23074 let plain = mimo_greedy(&mut p, &ids, n, false);
23075 assert_eq!(plain.mtp_drafted, 0);
23076 assert_eq!(plain.token_ids.len(), n);
23077 let plain_kv = p.kv_cache.layers[0].seq_len;
23078
23079 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23081 let spec = mimo_greedy(&mut p, &ids, n, true);
23082 assert!(spec.mtp_drafted > 0, "the round must draft");
23083 assert_eq!(spec.token_ids, plain.token_ids);
23084 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23085
23086 let mut truth: Vec<u32> = ids.clone();
23089 truth.extend(&plain.token_ids);
23090 let mut noisy = truth.clone();
23091 for (i, t) in noisy.iter_mut().enumerate() {
23092 if i % 5 == 0 {
23093 *t = (*t + 1) % 64;
23094 }
23095 }
23096 let mut st = mimo_test_mtp(3, 1.0);
23097 st.draft_override = Some(noisy);
23098 p.mimo_mtp = Some(st);
23099 let spec = mimo_greedy(&mut p, &ids, n, true);
23100 assert_eq!(spec.token_ids, plain.token_ids);
23101 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23102 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23103 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23104 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23105 assert!(
23106 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23107 "{:?}",
23108 stats.accept_hist
23109 );
23110 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23111
23112 let mut st = mimo_test_mtp(3, 1.0);
23115 st.draft_override = Some(truth);
23116 p.mimo_mtp = Some(st);
23117 let spec = mimo_greedy(&mut p, &ids, n, true);
23118 assert_eq!(spec.token_ids, plain.token_ids);
23119 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23120 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23121
23122 let off = mimo_greedy(&mut p, &ids, n, false);
23124 assert_eq!(off.token_ids, plain.token_ids);
23125 assert_eq!(off.mtp_drafted, 0);
23126 }
23127
23128 #[test]
23137 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23138 let p = mimo_test_pipeline();
23139 assert_eq!(
23140 p.graph_attn_decline_reason(),
23141 Some("per-layer KV head counts")
23142 );
23143 assert_eq!(p.wgpu_graph_attn_decline(), None);
23144 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23145 assert_eq!(
23146 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23147 (1, 4, 4, None, false)
23148 );
23149 assert_eq!(g0.invf, p.inv_freq.as_slice());
23150 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23151 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23152 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23153 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23154 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23155 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23156 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23157
23158 let emb = p.embed_single(3);
23161 let mut lg = Vec::new();
23162 assert!(
23163 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23164 .is_none()
23165 );
23166 let mut hid = emb.clone();
23167 assert_eq!(
23168 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23169 crate::gpu::BatchGraphOutcome::Declined
23170 );
23171 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23172 assert!(p.try_multi_burst(3, 0, 4).is_none());
23173 assert!(
23174 p.graph_declines().is_empty(),
23175 "no attention decline logged: {:?}",
23176 p.graph_declines()
23177 );
23178 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23183 assert_eq!(plain().graph_attn_decline_reason(), None);
23184 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23185 assert!(
23186 plain().graph_attn_geom(0).is_none(),
23187 "uniform models keep the historical arms"
23188 );
23189 let mut q = plain();
23190 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23191 assert_eq!(
23192 q.graph_attn_decline_reason(),
23193 Some("learned attention sinks")
23194 );
23195 assert_eq!(
23196 q.graph_attn_geom(1).unwrap().sink,
23197 Some(&[0.25f32, -0.25][..])
23198 );
23199 let mut q = plain();
23200 q.set_attn_geometry(None, Some(2)).unwrap();
23201 assert_eq!(
23202 q.graph_attn_decline_reason(),
23203 Some("V heads narrower than Q/K heads")
23204 );
23205 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23206 let mut q = plain();
23207 q.sliding_layers = Some(vec![true, false]);
23208 q.swa = Some((4, usize::MAX));
23209 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23210 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23211 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23212
23213 let mut q = mimo_test_pipeline();
23217 q.rope_scale = 2.0;
23218 q.rope_scale_local = 1.25;
23219 assert_eq!(q.wgpu_graph_attn_decline(), None);
23220 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23221 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23222
23223 let mut q = plain();
23226 q.rope_scale = 2.0;
23227 assert_eq!(
23228 q.wgpu_graph_attn_decline(),
23229 Some("scaled RoPE without per-layer geometry")
23230 );
23231 }
23232
23233 #[cfg(target_os = "macos")]
23239 #[test]
23240 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23241 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23245 p.swa = Some((16, usize::MAX));
23246 p.sliding_layers = Some(vec![true, true]);
23247 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23248 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23249
23250 assert_eq!(
23251 p.graph_attn_decline_reason(),
23252 Some("learned attention sinks")
23253 );
23254 let swa_graph = p.metal_graph_swa();
23255 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23256 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23257 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23258 assert!(
23259 native_sink_attend,
23260 "the q1 sink admission must follow the SWA door"
23261 );
23262 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23265 }
23266
23267 #[test]
23268 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23269 let mut p = mimo_test_pipeline();
23270 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23271 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23272 for _ in 0..if li == 0 { 2 } else { 12 } {
23273 layer.append(&row, &row, &[]);
23274 }
23275 }
23276 p.mimo_verify_rewind(9).unwrap();
23277 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23278 for layer in &p.kv_cache.layers[1..] {
23279 assert_eq!(layer.seq_len, 9);
23280 }
23281 }
23282
23283 #[test]
23287 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23288 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23289 let _ = std::fs::remove_dir_all(&dir);
23290 let mut p = mimo_test_pipeline();
23291 let hs = p.hidden_size;
23292 let ids = [5u32, 9, 11, 2, 40];
23293 p.layer_dump = Some(dir.join("decode"));
23294 for (pos, &id) in ids.iter().enumerate() {
23295 let e = p.embed_single(id);
23296 let _ = p.forward_layers(&e, pos, None);
23297 }
23298 p.clear_sequence_state();
23299 p.layer_dump = Some(dir.join("prefill"));
23300 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23301 for pos in 0..ids.len() {
23302 for li in 0..p.num_layers {
23303 let name = format!("p{pos:06}_l{li:02}.f32");
23304 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23305 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23306 assert_eq!(a.len(), hs * 4, "{name}");
23307 assert_eq!(a, b, "{name}");
23308 }
23309 }
23310 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23311 let vals: Vec<f32> = last
23312 .chunks(4)
23313 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23314 .collect();
23315 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23316 let _ = std::fs::remove_dir_all(&dir);
23317 }
23318
23319 #[test]
23320 fn attn_geometry_and_sinks_are_validated() {
23321 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23322 assert!(
23323 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23324 "one entry per layer"
23325 );
23326 assert!(
23327 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23328 "3 does not divide 4"
23329 );
23330 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23331 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23332 assert!(
23333 p.set_attn_geometry(None, Some(5)).is_err(),
23334 "V wider than the head"
23335 );
23336 p.set_attn_geometry(None, Some(4)).unwrap();
23337 assert_eq!(
23338 p.v_head_dim, None,
23339 "v_head_dim == head_dim is the uniform case"
23340 );
23341 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23342 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23343 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23344 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23345 assert!(
23346 p.kv_cache.layers[1].sinks.is_some(),
23347 "a reshape keeps the layer's sinks"
23348 );
23349 assert_eq!(p.layer_geom(1).0, 4);
23350 assert!(
23351 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23352 "one sink per Q head"
23353 );
23354 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23355 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23356 }
23357
23358 #[test]
23361 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23362 let cfg = || {
23363 Some(crate::nystrom::O1Cfg {
23364 layers: crate::nystrom::O1Layers::All,
23365 m: 4,
23366 w: 8,
23367 sink: 2,
23368 rect: crate::nystrom::O1Rect::Aggregate,
23369 })
23370 };
23371 let mut p = mimo_test_pipeline();
23372 p.set_o1(cfg());
23373 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23374 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23375 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23376 q.sliding_layers = Some(vec![false, false, true]);
23377 q.swa = Some((4, usize::MAX));
23378 q.set_o1(cfg());
23379 assert_eq!(q.o1_flags, vec![true, false, false]);
23380 }
23381
23382 #[test]
23383 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23384 const B: usize = 19;
23385 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23386 p.set_o1(Some(crate::nystrom::O1Cfg {
23387 layers: crate::nystrom::O1Layers::All,
23388 m: 4,
23389 w: 8,
23390 sink: 2,
23391 rect: crate::nystrom::O1Rect::Aggregate,
23392 }));
23393 p.o1_begin_with_prefix(Some(B));
23394 let ids: Vec<u32> = (0..B as u32).collect();
23395 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23396
23397 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23398 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23399 let next = p.embed_single(B as u32);
23400 let _ = p.forward_layers(&next, B, None);
23401 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23402 }
23403
23404 #[test]
23405 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23406 const B: usize = 19;
23407 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23408 let gdn_cfg = crate::linear_core::GdnCfg {
23412 num_v_heads: 2,
23413 num_k_heads: 1,
23414 key_head_dim: 2,
23415 value_head_dim: 4,
23416 conv_kernel: 3,
23417 hidden_size: 8,
23418 rms_eps: 1e-6,
23419 output_gate_sigmoid: false,
23420 };
23421 let synth = |n: usize, salt: usize| -> Vec<f32> {
23422 (0..n)
23423 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23424 .collect()
23425 };
23426 let qt = |rows: usize, cols: usize, salt: usize| {
23427 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23428 };
23429 let c_dim = gdn_cfg.conv_dim();
23430 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23431 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23432 in_proj_qkv: qt(c_dim, 8, 1),
23433 in_proj_z: qt(vd, 8, 2),
23434 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23435 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23436 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23437 a_log: vec![0.2, 0.5],
23438 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23439 norm: vec![1.0; gdn_cfg.value_head_dim],
23440 out_proj: qt(8, vd, 7),
23441 });
23442 p.gdn_cfg = Some(gdn_cfg);
23443 p.set_o1(Some(crate::nystrom::O1Cfg {
23444 layers: crate::nystrom::O1Layers::All,
23445 m: 4,
23446 w: 8,
23447 sink: 2,
23448 rect: crate::nystrom::O1Rect::Aggregate,
23449 }));
23450 p.o1_begin_with_prefix(Some(B));
23451 for pos in 0..B - 2 {
23452 let emb = p.embed_single(pos as u32);
23453 let _ = p.forward_layers(&emb, pos, None);
23454 }
23455 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23456
23457 let e1 = p.embed_single((B - 2) as u32);
23458 let e2 = p.embed_single((B - 1) as u32);
23459 let _ = p.forward_pair(&e1, &e2, B - 2);
23460
23461 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23462 assert!(
23463 p.kv_cache
23464 .layers
23465 .iter()
23466 .enumerate()
23467 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23468 );
23469 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23470 assert_ne!(
23471 p.kv_cache.layers[0].linear_state, lane1_state,
23472 "real pair must commit GDN lane 2 before returning"
23473 );
23474 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23475 let next = p.embed_single(B as u32);
23476 let _ = p.forward_layers(&next, B, None);
23477 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23478 }
23479
23480 #[test]
23481 fn o1_error_observation_stays_terminal_until_reset() {
23482 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23483 p.set_o1(Some(crate::nystrom::O1Cfg {
23484 layers: crate::nystrom::O1Layers::All,
23485 m: 4,
23486 w: 8,
23487 sink: 2,
23488 rect: crate::nystrom::O1Rect::Aggregate,
23489 }));
23490 p.o1_begin();
23491 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23492
23493 assert!(p.o1_seal_checked().is_err());
23494 assert!(
23495 p.o1_seal_checked().is_err(),
23496 "retry must see the sticky error"
23497 );
23498 let k = vec![0.2f32; 4];
23499 let v = vec![0.3f32; 4];
23500 p.kv_cache.layers[0].append(&k, &v, &[]);
23501 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23502
23503 p.reset_session();
23504 p.o1_begin();
23505 p.kv_cache.layers[0].append(&k, &v, &[]);
23506 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23507 }
23508
23509 #[test]
23510 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23511 let ids = vec![1u32, 2, 3, 4, 5, 6];
23512 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23513 p.graph_logits = Some(vec![123.0]);
23514 p.graph_want_logits = true;
23515 p.graph_failed
23516 .store(true, std::sync::atomic::Ordering::Relaxed);
23517 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23518 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23519 assert!(err.contains("before NLL"));
23520 assert!(p.graph_logits.is_none());
23521 assert!(!p.graph_want_logits);
23522 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23523 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23524
23525 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23526 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23527 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23528 assert_eq!(actual.1, expected.1);
23529 assert!((actual.0 - expected.0).abs() < 1e-9);
23530 }
23531
23532 #[test]
23533 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23534 let ids = vec![1u32, 2, 3, 4, 5, 6];
23535 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23536 p.nll_test_fail_at = Some(1);
23537 let err = p
23538 .nll_ids_from(&ids, 0)
23539 .expect_err("one-shot forward failure");
23540 assert!(err.contains("forward") || err.contains("score row"));
23541 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23542 assert!(!p.graph_want_logits);
23543 assert!(p.graph_logits.is_none());
23544 assert!(p.kv_history.is_empty());
23545
23546 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23547 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23548 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23549 assert_eq!(actual.1, expected.1);
23550 assert!((actual.0 - expected.0).abs() < 1e-9);
23551 }
23552
23553 #[test]
23554 fn nll_serial_failure_before_first_row_is_reported() {
23555 let ids = vec![1u32, 2, 3, 4];
23556 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23557 p.nll_test_force_serial = true;
23558 p.nll_test_fail_at = Some(0);
23559 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23560 assert!(err.contains("serial forward"));
23561 assert!(p.kv_history.is_empty());
23562 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23563 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23564 }
23565
23566 #[test]
23567 fn ffn_probe_failure_discards_recorder_and_state() {
23568 let ids = vec![1u32, 2, 3, 4];
23569 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23570 p.nll_test_fail_at = Some(0);
23571 let err = p
23572 .probe_ffn_mass_batch(&ids)
23573 .expect_err("probe forward failure");
23574 assert!(err.contains("NLL"));
23575 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23576 assert!(p.kv_history.is_empty());
23577 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23578 }
23579
23580 #[test]
23581 fn nll_test_controls_are_pipeline_scoped() {
23582 let ids = vec![1u32, 2, 3, 4];
23583 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23584 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23585 failing.nll_test_force_serial = true;
23586 failing.nll_test_fail_at = Some(0);
23587
23588 assert!(!failing.can_prefill_batched());
23589 assert!(unaffected.can_prefill_batched());
23590 let expected = unaffected
23591 .nll_ids_from(&ids, 0)
23592 .expect("unaffected pipeline remains usable");
23593 let err = failing
23594 .nll_ids_from(&ids, 0)
23595 .expect_err("failure injection belongs to failing pipeline");
23596 assert!(err.contains("serial forward"));
23597 assert!(failing.nll_test_fail_at.is_none());
23598 assert!(unaffected.can_prefill_batched());
23599 let actual = unaffected
23600 .nll_ids_from(&ids, 0)
23601 .expect("unaffected pipeline remains reusable");
23602 assert_eq!(actual.1, expected.1);
23603 assert!((actual.0 - expected.0).abs() < 1e-9);
23604 }
23605
23606 #[test]
23607 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23608 let ids = vec![1u32, 2, 3, 4, 5, 6];
23609 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23610 p.graph_logits = Some(vec![123.0]);
23611 p.graph_want_logits = true;
23612 p.graph_failed
23613 .store(true, std::sync::atomic::Ordering::Relaxed);
23614 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23615
23616 let err = p
23617 .forward_ids(&ids, None)
23618 .expect_err("a failed forward must not become a valid head result");
23619 assert!(err.contains("forward_ids setup"));
23620 assert!(p.graph_logits.is_none());
23621 assert!(!p.graph_want_logits);
23622 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23623 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23624 assert_eq!(p.kv_cache.seq_len(), 0);
23625
23626 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23627 .forward_ids(&ids, None)
23628 .expect("fresh forward_ids");
23629 let actual = p
23630 .forward_ids(&ids, None)
23631 .expect("pipeline remains reusable after a failed forward");
23632 assert_eq!(actual.len(), expected.len());
23633 assert!(
23634 actual
23635 .iter()
23636 .zip(expected)
23637 .all(|(a, b)| (a - b).abs() < 1e-9)
23638 );
23639 assert_eq!(p.kv_cache.seq_len(), ids.len());
23640 }
23641
23642 #[test]
23643 fn sigmoid_router_floor_is_explicit_per_architecture() {
23644 let zero = || DenseFfn {
23650 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23651 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23652 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23653 act: Act::Silu,
23654 down_t: None,
23655 segs: Vec::new(),
23656 };
23657 let m = MoeFfn {
23658 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23659 experts: vec![zero(), zero()],
23660 top_k: 1,
23661 norm_topk_prob: true,
23662 router_sigmoid: true,
23663 sigmoid_denom_eps: 1e-20,
23664 expert_bias: None,
23665 routed_scaling: 2.5,
23666 route_tau: None,
23667 shared: None,
23668 stats: std::cell::RefCell::new(Vec::new()),
23669 act_sq: std::cell::RefCell::new(Vec::new()),
23670 act_rows: std::cell::RefCell::new(Vec::new()),
23671 mask: None,
23672 per_expert_scale: None,
23673 router_input_norm: false,
23674 resonance: None,
23675 grown: Vec::new(),
23676 };
23677 let logits = [-20.0f32, -20.0];
23678 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23679 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23680 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23681 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23682 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23683 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23684 assert!(legacy_wsum > minimax_wsum * 100.0);
23685 }
23686
23687 #[test]
23688 fn resonance_scores_match_formula_and_stable_tie() {
23689 let r = Resonance {
23690 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23692 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23693 k: 1,
23694 bias: vec![1.5, 0.5, 0.0],
23695 shell: Vec::new(),
23696 };
23697 let x = [1.0f32, 1.0];
23698 let mut got = vec![0.0; 3];
23699 r.scores(&x, &mut got);
23700 assert!((got[0] - 0.5).abs() < 1e-6);
23704 assert!((got[1] - 0.5).abs() < 1e-6);
23705 assert!(got[2].abs() < 1e-6);
23706 let best = got
23707 .iter()
23708 .enumerate()
23709 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23710 .map(|(i, _)| i);
23711 assert_eq!(best, Some(0));
23712 assert!(got.iter().all(|v| v.is_finite()));
23713 }
23714
23715 #[test]
23720 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23721 let plain = Resonance {
23727 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23728 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23729 k: 1,
23730 bias: vec![1.5, 0.5, 0.0, 0.0],
23731 shell: Vec::new(),
23732 };
23733 let shelled = Resonance {
23734 mu: plain.mu.clone(),
23735 u: plain.u.clone(),
23736 k: 1,
23737 bias: plain.bias.clone(),
23738 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23739 };
23740 assert!(!plain.has_shell());
23741 assert!(shelled.has_shell());
23742 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23743 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23744 set_growth_shell(Some(true));
23745 assert!(growth_shell_enabled());
23746 let x = [1.0f32, 1.0];
23749 plain.scores(&x, &mut a);
23750 shelled.scores(&x, &mut b);
23751 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23752 assert!(a[2] == 0.0 && a[3] == 0.0);
23753 let xo = [3.0f32, 0.0];
23756 plain.scores(&xo, &mut a);
23757 shelled.scores(&xo, &mut b);
23758 assert_eq!(a[2], -6.0);
23759 assert_eq!(a[3], -6.0);
23760 assert_eq!(
23761 bits(&a[..3]),
23762 bits(&b[..3]),
23763 "trunk rows + the boundary row"
23764 );
23765 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23766 assert_eq!(shelled.effective_shell(4), shelled.shell);
23767 set_growth_shell(Some(false));
23770 assert!(!growth_shell_enabled());
23771 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
23772 shelled.scores(&xo, &mut b);
23773 assert_eq!(bits(&a), bits(&b));
23774 set_growth_shell(None);
23775 let short = Resonance {
23778 shell: vec![f32::INFINITY, f32::INFINITY],
23779 ..shelled
23780 };
23781 set_growth_shell(Some(true));
23782 short.scores(&xo, &mut b);
23783 assert_eq!(bits(&a), bits(&b));
23784 set_growth_shell(None);
23785 }
23786
23787 #[test]
23791 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
23792 let zero = || DenseFfn {
23793 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23794 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23795 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23796 act: Act::Silu,
23797 down_t: None,
23798 segs: Vec::new(),
23799 };
23800 let moe = |sigmoid: bool| MoeFfn {
23801 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
23802 experts: vec![zero(), zero(), zero(), zero()],
23803 top_k: 1,
23804 norm_topk_prob: true,
23805 router_sigmoid: sigmoid,
23806 sigmoid_denom_eps: 1e-6,
23807 expert_bias: None,
23808 routed_scaling: 1.0,
23809 route_tau: None,
23810 shared: None,
23811 stats: std::cell::RefCell::new(Vec::new()),
23812 act_sq: std::cell::RefCell::new(Vec::new()),
23813 act_rows: std::cell::RefCell::new(Vec::new()),
23814 mask: None,
23815 per_expert_scale: None,
23816 router_input_norm: false,
23817 resonance: None,
23818 grown: Vec::new(),
23819 };
23820 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
23821 for sigmoid in [false, true] {
23822 let m = moe(sigmoid);
23823 let (idx, p, wsum) = moe_route(&logits, &m, None);
23824 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
23825 assert_eq!(p[1], 0.0);
23826 assert_eq!(p[3], 0.0);
23827 assert!(p[2] > p[0] && p[0] > 0.0);
23828 assert!(p.iter().all(|v| v.is_finite()));
23829 let w = p[2] / wsum;
23830 if sigmoid {
23831 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
23833 } else {
23834 assert_eq!(
23835 w, 1.0,
23836 "softmax: the renormalized top-1 weight is exactly 1"
23837 );
23838 }
23839 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
23842 assert_eq!(idx, vec![2]);
23843 }
23844 let m = moe(false);
23846 let (idx, _, _) = moe_route(
23847 &[
23848 f32::NEG_INFINITY,
23849 f32::NEG_INFINITY,
23850 f32::NEG_INFINITY,
23851 -9.0,
23852 ],
23853 &m,
23854 None,
23855 );
23856 assert_eq!(idx, vec![3]);
23857 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
23860 assert_eq!(idx, vec![0]);
23861 assert!(p.iter().all(|&v| v == 0.25));
23862 assert!(wsum.is_finite() && wsum > 0.0);
23863 }
23864
23865 #[cfg(feature = "gpu")]
23871 #[test]
23872 fn minimax_device_route_matches_host_sigmoid_contract() {
23873 match crate::gpu_wgpu::selected_and_up() {
23874 None => {
23875 crate::gpu_wgpu::skip_or_fail(module_path!());
23876 return;
23877 }
23878 Some(false) => {
23886 eprintln!(
23887 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23888 module_path!()
23889 );
23890 return;
23891 }
23892 Some(true) => {}
23893 }
23894 let n = 128usize;
23895 let top_k = 4usize;
23896 let logits: Vec<f32> = (0..n)
23897 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23898 .collect();
23899 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23900 let m = MoeFfn {
23901 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23902 experts: Vec::new(),
23903 top_k,
23904 norm_topk_prob: true,
23905 router_sigmoid: true,
23906 sigmoid_denom_eps: 1e-20,
23907 expert_bias: Some(bias.clone()),
23908 routed_scaling: 2.0,
23909 route_tau: None,
23910 shared: None,
23911 stats: std::cell::RefCell::new(Vec::new()),
23912 act_sq: std::cell::RefCell::new(Vec::new()),
23913 act_rows: std::cell::RefCell::new(Vec::new()),
23914 mask: None,
23915 per_expert_scale: None,
23916 router_input_norm: false,
23917 resonance: None,
23918 grown: Vec::new(),
23919 };
23920 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23921 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
23922
23923 for shared in [Some(n as u32), None] {
23924 let mut gi = Vec::new();
23925 let mut gw = Vec::new();
23926 assert!(
23927 crate::gpu_wgpu::minimax_moe_route_for_test(
23928 &logits,
23929 Some(&bias),
23930 top_k,
23931 2.0,
23932 shared,
23933 &mut gi,
23934 &mut gw,
23935 ),
23936 "устройство не поднялось — тест не проверил ничего"
23937 );
23938 let routed = if shared.is_some() { top_k } else { gi.len() };
23939 assert_eq!(
23940 &gi[..routed],
23941 &ci[..],
23942 "разные эксперты или порядок (shared={shared:?})"
23943 );
23944 let d = cw
23945 .iter()
23946 .zip(&gw[..routed])
23947 .map(|(a, b)| (a - b).abs())
23948 .fold(0.0f32, f32::max);
23949 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
23950 assert!(d < 1e-6, "веса разошлись на {d:.3e}");
23951 if let Some(s) = shared {
23952 assert_eq!(gi[top_k], s as usize, "shared pin slot");
23953 assert_eq!(gw[top_k], 1.0, "shared weight");
23954 }
23955 }
23956 }
23957
23958 #[cfg(feature = "gpu")]
23963 #[test]
23964 fn minimax_device_cold_weights_match_host_contract() {
23965 match crate::gpu_wgpu::selected_and_up() {
23966 None => {
23967 crate::gpu_wgpu::skip_or_fail(module_path!());
23968 return;
23969 }
23970 Some(false) => {
23978 eprintln!(
23979 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
23980 module_path!()
23981 );
23982 return;
23983 }
23984 Some(true) => {}
23985 }
23986 let n = 128usize;
23987 let top_k = 4usize;
23988 let logits: Vec<f32> = (0..n)
23989 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
23990 .collect();
23991 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
23992 let m = MoeFfn {
23993 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23994 experts: Vec::new(),
23995 top_k,
23996 norm_topk_prob: true,
23997 router_sigmoid: true,
23998 sigmoid_denom_eps: 1e-20,
23999 expert_bias: Some(bias.clone()),
24000 routed_scaling: 2.0,
24001 route_tau: None,
24002 shared: None,
24003 stats: std::cell::RefCell::new(Vec::new()),
24004 act_sq: std::cell::RefCell::new(Vec::new()),
24005 act_rows: std::cell::RefCell::new(Vec::new()),
24006 mask: None,
24007 per_expert_scale: None,
24008 router_input_norm: false,
24009 resonance: None,
24010 grown: Vec::new(),
24011 };
24012 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24013 assert_eq!(ci.len(), top_k);
24014
24015 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24017 let mut cold = Vec::new();
24018 let mut idx = Vec::new();
24019 let mut dw = Vec::new();
24020 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24021 &logits,
24022 Some(&bias),
24023 top_k,
24024 2.0,
24025 &resident,
24026 n as u32,
24027 &mut cold,
24028 &mut idx,
24029 &mut dw,
24030 ));
24031 assert!(cold.is_empty(), "полностью резидентный банк: {cold:?}");
24032 for (r, &e) in ci.iter().enumerate() {
24037 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24038 let host = p[e] / wsum;
24039 let d = (dw[r] - host).abs();
24040 assert!(
24041 d <= 1e-6 * host.abs().max(1.0),
24042 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24043 dw[r]
24044 );
24045 }
24046 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24047 assert_eq!(dw[top_k], 1.0, "shared weight");
24048
24049 let mut remap = resident.clone();
24051 remap[ci[0]] = u32::MAX;
24052 remap[ci[2]] = u32::MAX;
24053 let mut cold = Vec::new();
24054 let mut idx2 = Vec::new();
24055 let mut dw2 = Vec::new();
24056 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24057 &logits,
24058 Some(&bias),
24059 top_k,
24060 2.0,
24061 &remap,
24062 n as u32,
24063 &mut cold,
24064 &mut idx2,
24065 &mut dw2,
24066 ));
24067 let mut resident_weight_checked = 0usize;
24075 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24076 if dw2[r] == 0.0 {
24077 continue; }
24079 let e = slot as usize;
24080 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24081 let host = p[e] / wsum;
24082 let d = (dw2[r] - host).abs();
24083 assert!(
24084 d <= 1e-6 * host.abs().max(1.0),
24085 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24086 );
24087 resident_weight_checked += 1;
24088 }
24089 assert_eq!(resident_weight_checked, 2, "ровно два резидентных пика из четырёх");
24090 println!("partial bank: checked {resident_weight_checked} resident weights");
24091 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24092 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24093 ids.sort_unstable();
24094 let mut want = vec![ci[0], ci[2]];
24095 want.sort_unstable();
24096 assert_eq!(ids, want, "холодные эксперты разошлись: {cold:?} против {want:?}");
24097 for &(e, w) in &cold {
24098 let host = p[e] / wsum;
24099 let d = (w - host).abs();
24100 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24101 assert!(d <= 1e-6 * host.abs().max(1.0), "cold вес эксперта {e}: Δ={d:.3e}");
24102 }
24103 }
24104
24105 #[test]
24110 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24111 let zero = || DenseFfn {
24112 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24113 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24114 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24115 act: Act::Silu,
24116 down_t: None,
24117 segs: Vec::new(),
24118 };
24119 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24120 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24121 experts: vec![zero(), zero(), zero()],
24122 top_k: 1,
24123 norm_topk_prob: norm_topk,
24124 router_sigmoid: false,
24125 sigmoid_denom_eps: 1e-6,
24126 expert_bias: None,
24127 routed_scaling: 1.0,
24128 route_tau: None,
24129 shared: None,
24130 stats: std::cell::RefCell::new(Vec::new()),
24131 act_sq: std::cell::RefCell::new(Vec::new()),
24132 act_rows: std::cell::RefCell::new(Vec::new()),
24133 mask: None,
24134 per_expert_scale: None,
24135 router_input_norm: false,
24136 resonance: resonant.then(|| Resonance {
24137 mu: vec![0.0; 6],
24138 u: Vec::new(),
24139 k: 0,
24140 bias: vec![0.0; 3],
24141 shell: Vec::new(),
24142 }),
24143 grown: Vec::new(),
24144 };
24145 let lo = -0.1f32;
24148 let hi = f32::from_bits(lo.to_bits() - 1);
24149 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24150 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24151 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24153 assert_eq!(
24154 idx,
24155 vec![0],
24156 "softmax tie → lower index (the gated contract)"
24157 );
24158 for norm in [true, false] {
24161 let m = moe(true, norm);
24162 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24163 assert_eq!(idx, vec![1], "norm_topk {norm}");
24164 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24165 assert_eq!(p[1] / wsum, 1.0);
24166 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24169 assert_eq!(idx, vec![0]);
24170 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24172 assert_eq!(idx, vec![2]);
24173 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24174 assert_eq!(idx, vec![0]);
24175 assert_eq!(p[0] / wsum, 1.0);
24176 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24179 assert_eq!(idx, vec![0]);
24180 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24181 }
24182 }
24183}