1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18use crate::kv_cache::KvCache;
19use crate::linear_core::{
20 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
21 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
22 vmf_phase_pair,
23};
24use crate::pool::Pool;
25use crate::qtensor::QTensor;
26use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
27use crate::tokenizer::Tokenizer;
28use cortiq_core::mask::TaskMask;
29use cortiq_core::types::NormStyle;
30
31pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
32 std::sync::atomic::AtomicBool::new(false);
33
34#[derive(Clone)]
41struct GraphMoeBank {
42 experts: Vec<(usize, usize, usize)>,
43 q4tp: Option<bool>,
44 gu_q2: Option<bool>,
45 q82: Option<bool>,
46 act: Option<crate::gpu::GraphAct>,
47 model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
48}
49
50type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
56
57thread_local! {
62 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
63}
64
65struct ExactMtpCalibrationGuard;
66
67impl ExactMtpCalibrationGuard {
68 fn enter() -> Self {
69 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
70 Self
71 }
72}
73
74impl Drop for ExactMtpCalibrationGuard {
75 fn drop(&mut self) {
76 EXACT_MTP_CALIBRATION.with(|depth| {
77 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
78 depth.set(depth.get().saturating_sub(1));
79 });
80 }
81}
82
83fn exact_mtp_calibration_active() -> bool {
84 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
85}
86
87fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
91 logits.resize(vocab_size, f32::NEG_INFINITY);
92}
93
94struct ForwardScratch {
98 n1: Vec<f32>,
99 n2: Vec<f32>,
100 p1: Vec<f32>,
101 p2: Vec<f32>,
102}
103
104impl ForwardScratch {
105 fn new(hidden: usize) -> Self {
106 Self {
107 n1: vec![0.0; hidden],
108 n2: vec![0.0; hidden],
109 p1: vec![0.0; hidden],
110 p2: vec![0.0; hidden],
111 }
112 }
113}
114
115pub struct Pipeline {
117 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
122 pub tokenizer: std::sync::Arc<Tokenizer>,
125 pub kv_cache: KvCache,
126 pub sampler_config: SamplerConfig,
127 pub weights: PipelineWeights,
128 pub hidden_size: usize,
129 pub intermediate_size: usize,
130 pub num_heads: usize,
131 pub num_kv_heads: usize,
132 pub head_dim: usize,
133 pub num_layers: usize,
135 pub physical_layers: usize,
137 pub loop_final_norm: bool,
139 pub vocab_size: usize,
140 pub rms_eps: f64,
141 pub rope_base: f32,
142 pub norm_style: NormStyle,
143 pub rotary_dim: usize,
145 pub attention_heads_per_layer: Option<Vec<usize>>,
147 pub kv_heads_per_layer: Option<Vec<usize>>,
152 pub v_head_dim: Option<usize>,
157 pub layer_dump: Option<std::path::PathBuf>,
170 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
173 pub(crate) mimo_moe: crate::mimo_moe::Slot,
176 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
181 pub vmf_cfg: Option<VmfPhaseCfg>,
183 pub gdn_cfg: Option<GdnCfg>,
185 pub logit_multiplier: Option<f32>,
187 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
192 graph_failed: std::sync::atomic::AtomicBool,
197 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
200 pub kv_history: Vec<u32>,
205 pub kv_history_device: bool,
208 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
210 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
213 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
216 pub dsv4: Option<
220 Box<(
221 crate::dsv4::Dsv4Globals,
222 Vec<crate::dsv4::Dsv4Layer>,
223 crate::dsv4::Dsv4Cfg,
224 crate::dsv4::Dsv4State,
225 )>,
226 >,
227 pub dsv41: Option<
231 Box<(
232 crate::dsv41::Dsv41Globals,
233 Vec<crate::dsv41::Dsv41Layer>,
234 crate::dsv41::Dsv41Cfg,
235 crate::dsv41::Dsv41State,
236 )>,
237 >,
238 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
240 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
242 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
247 clover_stats: clover::CloverStats,
248 pub qwen4_exp: Option<
249 Box<(
250 crate::qwen4_exp::Globals,
251 Vec<crate::qwen4_exp::Layer>,
252 crate::qwen4_exp::Cfg,
253 crate::qwen4_exp::State,
254 )>,
255 >,
256 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
260 pub dspark: Option<crate::dsv4::DsparkState>,
262 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
265 pub dspark_hist: Vec<usize>,
267 pub dspark_real: Vec<u32>,
271 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
275 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
277 pub dspark_draft_ns: u128,
281 m3_winners: Vec<Vec<u32>>,
283 m3_counts: Vec<u32>,
285 m3_n_exp: usize,
286 m3_tokens: u64,
287 m3_prewarmed: bool,
288 pub short_conv_cfg: Option<ShortConvCfg>,
291 pub mtp: Option<MtpModule>,
293 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
297 verify_exact_moe: bool,
300 pub speculative: bool,
302 pub ignore_eos: bool,
308 pub draft_full_streak: u32,
314 pub spec_k_adapt: Option<usize>,
322 pub spec_acc_ewma: f32,
324 rng: SplitMix64,
325 sampler_scratch: SamplerScratch,
326 spec_forced: Option<u32>,
332 spec_q: Vec<Vec<f32>>,
333 spec_p: Vec<f32>,
334 spec_res: Vec<f32>,
335 spec_qs: Vec<sampler::Sparse>,
337 spec_ps: sampler::Sparse,
338 spec_ress: sampler::Sparse,
339 mtp_graph_mode: Option<bool>,
346 #[cfg(target_os = "macos")]
349 metal_verify: Option<MetalVerifyPending>,
350 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
354 ws: ForwardScratch,
358 pool: Option<std::sync::Arc<Pool>>,
360 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
364 pub(crate) dyn_force_f32: bool,
366 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
371 pub(crate) dyn_active: Option<usize>,
377 pub(crate) dyn_blend_loaded: bool,
381 pub(crate) dyn_phi_layer: Option<usize>,
384 dyn_phi_ema: Vec<f32>,
386 dyn_phi_seen: usize,
387 pub dyn_router: Option<crate::swarm::DynRouter>,
390 o1_cfg: Option<crate::nystrom::O1Cfg>,
393 o1_epoch: u64,
396 o1_flags: Vec<bool>,
398 trace: bool,
401 calib_temp: f32,
404 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
406 graph_kv_id: u64,
407 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
410 graph_want_logits: bool,
411 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
415 graph_head_required: bool,
416 graph_logits: Option<Vec<f32>>,
419 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
422 graph_refused: std::sync::atomic::AtomicBool,
428 pub embed_multiplier: f32,
430 pub attn_scale: f32,
433 pub swa: Option<(usize, usize)>,
436 pub swa_trim: Option<(usize, usize)>,
443 swa_trim_anchor: usize,
447 pub chunk_min_rows: usize,
451 pub sliding_layers: Option<Vec<bool>>,
454 pub no_rope_layers: Option<Vec<bool>>,
459 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
465 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
468 pub kv_prefix: KvPrefix,
472 pub last_prefill_tokens: usize,
475 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
478 pub rotary_dim_local: Option<usize>,
479 pub rope_scale: f32,
480 pub rope_scale_local: f32,
481 pub global_attn: Option<(usize, usize)>,
484 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
487 pub attn_v_norm: bool,
489 pub qk_norm_after_rope: bool,
491 pub proj_gate_sigmoid: bool,
494 pub final_softcap: Option<f32>,
496 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
500 pub attn_softcap: f32,
502 confidence_on: bool,
506 #[cfg(test)]
509 nll_test_fail_at: Option<usize>,
510 #[cfg(test)]
513 nll_test_force_serial: bool,
514}
515
516#[cfg(target_os = "macos")]
517impl Drop for Pipeline {
518 fn drop(&mut self) {
519 let _ = crate::gpu_metal::wait_replay();
521 crate::gpu::kv_mirror_drop(self.graph_kv_id);
522 }
523}
524
525#[cfg(not(target_os = "macos"))]
526impl Drop for Pipeline {
527 fn drop(&mut self) {
528 crate::gpu::graph_kv_reset(self.graph_kv_id);
532 }
533}
534
535pub struct PipelineWeights {
540 pub embed_tokens: QTensor,
542 pub layers: Vec<LayerWeights>,
544 pub lm_head: QTensor,
546 pub final_norm: Vec<f32>,
548}
549
550pub struct LayerWeights {
552 pub input_norm: Vec<f32>,
553 pub post_norm: Vec<f32>,
556 pub attn_out_norm: Option<Vec<f32>>,
559 pub layer_scale: Option<f32>,
561 pub ffn_out_norm: Option<Vec<f32>>,
564 pub ffn: FfnKind,
565 pub attn: AttnKind,
566}
567
568#[derive(Clone, Copy, PartialEq, Debug, Default)]
572pub enum Act {
573 #[default]
574 Silu,
575 GeluTanh,
576 Gelu,
578 Situ {
581 beta: f32,
582 linear_beta: f32,
583 },
584 SwiGluOai {
589 alpha: f32,
590 limit: f32,
591 },
592}
593
594impl Act {
595 pub fn from_arch(name: &str) -> Self {
596 if name == "gelu_tanh" {
597 Self::GeluTanh
598 } else if name == "gelu" {
599 Self::Gelu
600 } else {
601 Self::Silu
602 }
603 }
604
605 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
607 match arch.hidden_act.as_str() {
608 "situ" => Self::Situ {
609 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
610 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
611 },
612 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
617 alpha: 1.702,
618 limit: 7.0,
619 },
620 other => Self::from_arch(other),
621 }
622 }
623
624 #[inline]
629 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
630 Self::SwiGluOai { alpha, limit }
631 }
632
633 #[inline]
634 pub fn apply(self, x: f32) -> f32 {
635 match self {
636 Self::Silu => inference::silu(x),
637 Self::GeluTanh => inference::gelu_tanh(x),
638 Self::Gelu => inference::gelu_erf(x),
639 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
640 Self::SwiGluOai { alpha, limit } => {
641 let gate = x.min(limit);
642 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
643 }
644 }
645 }
646
647 #[inline]
650 pub fn combine(self, g: f32, u: f32) -> f32 {
651 match self {
652 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
653 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
654 }
655 Self::SwiGluOai { alpha, limit } => {
656 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
657 }
658 _ => self.apply(g) * u,
659 }
660 }
661
662 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
666 match self {
667 Self::Silu => Some(crate::gpu::GraphAct::Silu),
668 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
669 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
670 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
671 }
672 }
673
674 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
680 self.moe_job_graph_contract().map(|(act, _)| act)
681 }
682
683 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
689 match self {
690 Self::SwiGluOai { alpha, limit }
691 if alpha.to_bits() == 1.702f32.to_bits()
692 && limit.to_bits() == 7.0f32.to_bits() =>
693 {
694 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
695 }
696 _ => self.graph_act().map(|act| (act, 0.0)),
697 }
698 }
699}
700
701pub struct DenseFfn {
703 pub gate_proj: QTensor,
704 pub up_proj: QTensor,
705 pub down_proj: QTensor,
706 pub act: Act,
708 pub down_t: Option<QTensor>,
714 pub segs: Vec<FfnSeg>,
721}
722
723pub struct FfnSeg {
728 pub gate: QTensor,
729 pub up: QTensor,
730 pub down: QTensor,
731 pub start: usize,
732 pub width: usize,
733}
734
735pub enum FfnKind {
738 Dense(DenseFfn),
739 Moe(MoeFfn),
743 DenseMoe(Box<DenseMoeFfn>),
750}
751
752pub struct DenseMoeFfn {
754 pub dense: DenseFfn,
755 pub moe: MoeFfn,
756 pub post_norm_1: Vec<f32>,
758 pub pre_norm_2: Vec<f32>,
761 pub post_norm_2: Vec<f32>,
763}
764
765pub struct MoeFfn {
766 pub router: QTensor,
768 pub experts: Vec<DenseFfn>,
769 pub top_k: usize,
770 pub norm_topk_prob: bool,
771 pub router_sigmoid: bool,
774 pub sigmoid_denom_eps: f32,
780 pub expert_bias: Option<Vec<f32>>,
784 pub routed_scaling: f32,
787 pub route_tau: Option<f32>,
793 pub shared: Option<(DenseFfn, Option<QTensor>)>,
796 pub stats: std::cell::RefCell<Vec<u64>>,
800 pub act_sq: std::cell::RefCell<Vec<f64>>,
807 pub act_rows: std::cell::RefCell<Vec<f32>>,
813 pub mask: Option<Vec<bool>>,
818 pub per_expert_scale: Option<Vec<f32>>,
821 pub router_input_norm: bool,
825 pub resonance: Option<Resonance>,
829 pub grown: Vec<GrownExpert>,
835}
836
837#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct GrownExpert {
840 pub record: String,
842 pub record_index: usize,
844 pub layer: usize,
845 pub expert: usize,
849}
850
851pub struct Resonance {
853 pub mu: Vec<f32>,
855 pub u: Vec<f32>,
857 pub k: usize,
858 pub bias: Vec<f32>,
860 pub shell: Vec<f32>,
866}
867
868static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
871
872pub fn growth_shell_enabled() -> bool {
877 use std::sync::atomic::Ordering;
878 match GROWTH_SHELL.load(Ordering::Relaxed) {
879 1 => true,
880 2 => false,
881 _ => {
882 let off = std::env::var("CMF_GROWTH_SHELL")
883 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
884 .unwrap_or(false);
885 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
886 !off
887 }
888 }
889}
890
891pub fn set_growth_shell(on: Option<bool>) {
896 GROWTH_SHELL.store(
897 match on {
898 Some(true) => 1,
899 Some(false) => 2,
900 None => 0,
901 },
902 std::sync::atomic::Ordering::Relaxed,
903 );
904}
905
906impl Resonance {
907 pub fn has_shell(&self) -> bool {
909 self.shell.iter().any(|s| s.is_finite())
910 }
911
912 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
915 let mut out = vec![f32::INFINITY; ne];
916 if growth_shell_enabled() {
917 for (o, s) in out.iter_mut().zip(&self.shell) {
918 *o = *s;
919 }
920 }
921 out
922 }
923
924 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
929 let h = x.len();
930 let ne = out.len();
931 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
932 for e in 0..ne {
933 let mu = &self.mu[e * h..(e + 1) * h];
934 let mut d2 = 0.0f32;
935 for j in 0..h {
936 let d = x[j] - mu[j];
937 d2 += d * d;
938 }
939 let mut proj = 0.0f32;
940 for i in 0..self.k {
941 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
942 let mut p = 0.0f32;
943 for j in 0..h {
944 p += (x[j] - mu[j]) * u[j];
945 }
946 proj += p * p;
947 }
948 let err = d2 - proj;
949 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
950 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
951 out[e] = f32::NEG_INFINITY;
952 }
953 }
954 }
955}
956
957pub enum AttnKind {
960 Full {
962 wq: QTensor,
963 wk: QTensor,
964 wv: QTensor,
965 wo: QTensor,
966 q_norm: Option<Vec<f32>>,
967 k_norm: Option<Vec<f32>>,
968 output_gate: bool,
969 softplus_gate: Option<(QTensor, bool)>,
973 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
975 },
976 Linear(VmfPhaseWeights),
978 LinearGdn(GdnWeights),
980 ShortConv(ShortConvWeights),
983 Mla(Box<MlaWeights>),
991 Kda(Box<crate::linear_core::KdaWeights>),
995 Bounded(Box<crate::bounded::BoundedWeights>),
1000}
1001
1002pub struct MlaWeights {
1004 pub q_proj: QTensor,
1008 pub q_a: Option<QTensor>,
1011 pub q_a_norm: Option<Vec<f32>>,
1012 pub kv_a: QTensor,
1014 pub kv_a_norm: Vec<f32>,
1016 pub kv_b: QTensor,
1018 pub o_proj: QTensor,
1020 pub nh: usize,
1021 pub qk_rope: usize,
1022 pub qk_nope: usize,
1023 pub v_dim: usize,
1024 pub lora: usize,
1025 pub scale: f32,
1027 pub nope: bool,
1029}
1030
1031pub struct MtpModule {
1036 pub enorm: Vec<f32>,
1037 pub hnorm: Vec<f32>,
1038 pub eh_proj: QTensor,
1040 pub layer: LayerWeights,
1041 pub final_norm: Vec<f32>,
1042 pub kv: crate::kv_cache::LayerKvCache,
1043}
1044
1045const MTP_PROBE_FFN_ROWS: usize = 128;
1054
1055#[derive(Clone, Debug, PartialEq)]
1060pub struct MtpFfnProbe {
1061 pub mass: Vec<f64>,
1062 pub squared_mass: Vec<f64>,
1063 pub observed_positions: usize,
1064}
1065
1066#[cfg(target_os = "macos")]
1073enum MetalRowsItem<'a> {
1074 Gdn {
1075 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1076 first: usize,
1077 },
1078 Attn {
1079 l: crate::gpu_metal::AttnGpuLayer<'a>,
1080 li: usize,
1081 q_norm: Option<&'a [f32]>,
1082 k_norm: Option<&'a [f32]>,
1083 output_gate: bool,
1084 },
1085}
1086
1087#[cfg(target_os = "macos")]
1088struct MetalVerifyPending {
1089 graph: crate::gpu_metal::VerifyGraph,
1090 gdn_layers: Vec<usize>,
1091 attn_layers: Vec<(usize, usize)>,
1092}
1093
1094#[cfg(target_os = "macos")]
1098struct MetalWarmPending {
1099 graph: crate::gpu_metal::VerifyGraph,
1100 cpu_stored: usize,
1101 b: usize,
1102}
1103
1104#[cfg(target_os = "macos")]
1105enum MetalRowsRun {
1106 Declined,
1108 Failed,
1111 Completed(MetalVerifyPending),
1112}
1113
1114#[cfg(target_os = "macos")]
1115enum MetalPrefillOutcome {
1116 Declined,
1117 Failed,
1118 Completed(Vec<f32>),
1119}
1120
1121#[cfg(target_os = "macos")]
1122enum MetalBatchNllOutcome {
1123 Declined,
1124 Failed(String),
1125 Completed(f64, usize),
1126}
1127
1128#[derive(Clone, Copy)]
1134enum SpecTrial {
1135 Spec {
1136 t0: std::time::Instant,
1137 gen0: usize,
1138 rounds: usize,
1139 },
1140 Plain {
1141 t0: std::time::Instant,
1142 gen0: usize,
1143 },
1144 Decided {
1145 spec: bool,
1146 recheck_at: usize,
1147 },
1148}
1149
1150pub(crate) fn spec_time_level() -> u8 {
1154 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1155 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1156 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1157 Err(_) => 0,
1158 })
1159}
1160
1161fn graph_moe_bank_on() -> bool {
1164 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1165 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1166}
1167
1168fn graph_spec_wanted_by_policy(
1176 spec_env: Option<&str>,
1177 spec_default_ok: bool,
1178 penalized: bool,
1179 metal_wgpu: bool,
1180 native_metal: bool,
1181) -> bool {
1182 match spec_env {
1183 Some("0") => false,
1184 Some("1") => true,
1189 Some(_) => !native_metal,
1190 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1193 }
1194}
1195
1196struct SpecStampLog {
1202 t_last: std::time::Instant,
1203 items: Vec<(&'static str, f32)>,
1204}
1205
1206static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1207
1208pub(crate) fn spec_stamp(name: &'static str) {
1209 if spec_time_level() == 0 {
1210 return;
1211 }
1212 if let Ok(mut g) = SPEC_STAMPS.lock() {
1213 if let Some(log) = g.as_mut() {
1214 let now = std::time::Instant::now();
1215 log.items
1216 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1217 log.t_last = now;
1218 }
1219 }
1220}
1221
1222fn spec_stamps_begin() {
1223 if spec_time_level() == 0 {
1224 return;
1225 }
1226 if let Ok(mut g) = SPEC_STAMPS.lock() {
1227 *g = Some(SpecStampLog {
1228 t_last: std::time::Instant::now(),
1229 items: Vec::with_capacity(64),
1230 });
1231 }
1232}
1233
1234fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1235 SPEC_STAMPS
1236 .lock()
1237 .ok()
1238 .and_then(|mut g| g.take())
1239 .map(|l| l.items)
1240 .unwrap_or_default()
1241}
1242
1243fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1246 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1247 for &(n, ms) in items {
1248 match agg.iter_mut().find(|e| e.0 == n) {
1249 Some(e) => {
1250 e.1 += ms;
1251 e.2 += 1;
1252 }
1253 None => agg.push((n, ms, 1)),
1254 }
1255 }
1256 let mut s = String::with_capacity(agg.len() * 16);
1257 for (n, ms, k) in agg {
1258 if k > 1 {
1259 s.push_str(&format!("{n} {ms:.1}/{k} "));
1260 } else {
1261 s.push_str(&format!("{n} {ms:.1} "));
1262 }
1263 }
1264 s
1265}
1266
1267#[derive(Default, Clone, Copy)]
1289struct SpecMon {
1290 round_ms: f64,
1291 tokens: f64,
1292 plain_ms: f64,
1293 n: u32,
1294 fails: u32,
1295 zero_accept_rounds: u8,
1300 metal: bool,
1301}
1302
1303const SPEC_PROXY_TOKENS: f64 = 3.5;
1306const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1309
1310impl SpecMon {
1311 fn round(&mut self, dt_ms: f64, produced: usize) {
1312 if produced <= 1 {
1313 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1314 } else {
1315 self.zero_accept_rounds = 0;
1316 }
1317 self.n += 1;
1318 if self.n == 1 {
1319 return; }
1321 let a = if self.n == 2 { 1.0 } else { 0.3 };
1322 self.round_ms += a * (dt_ms - self.round_ms);
1323 self.tokens += a * (produced as f64 - self.tokens);
1324 }
1325 fn pays(&self) -> bool {
1326 if self.plain_ms > 0.0 {
1327 self.tokens * self.plain_ms > self.round_ms * 1.03
1328 } else {
1329 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1330 }
1331 }
1332 fn repeated_zero_acceptance(&self) -> bool {
1333 self.zero_accept_rounds >= 2
1334 }
1335 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1337 let n = generated.saturating_sub(gen0);
1338 if n >= 8 {
1339 return true;
1340 }
1341 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1342 }
1343}
1344
1345pub const KV_PREFIX_TAIL: usize = 128;
1348
1349#[derive(Debug, Clone, Default)]
1355pub struct KvPrefix {
1356 len: usize,
1357 hash: u64,
1358 tail: Vec<u32>,
1359 device: bool,
1364}
1365
1366impl KvPrefix {
1367 #[inline]
1368 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1369 for &id in ids {
1370 h ^= id as u64;
1371 h = h.wrapping_mul(0x100000001b3);
1372 h ^= h >> 29;
1373 }
1374 h
1375 }
1376
1377 pub fn clear(&mut self) {
1378 self.len = 0;
1379 self.hash = 0xcbf29ce484222325;
1380 self.tail.clear();
1381 self.device = false;
1382 }
1383
1384 pub fn on_device(&self) -> bool {
1386 self.device
1387 }
1388
1389 pub fn set_on_device(&mut self, device: bool) {
1391 self.device = device;
1392 }
1393
1394 pub fn len(&self) -> usize {
1396 self.len
1397 }
1398
1399 pub fn is_empty(&self) -> bool {
1400 self.len == 0
1401 }
1402
1403 pub fn tail_len(&self) -> usize {
1405 self.tail.len()
1406 }
1407
1408 pub fn set(&mut self, ids: &[u32]) {
1410 self.clear();
1411 self.extend(ids);
1412 }
1413
1414 pub fn extend(&mut self, more: &[u32]) {
1416 if self.len == 0 && self.hash == 0 {
1417 self.hash = 0xcbf29ce484222325;
1418 }
1419 self.hash = Self::fold(self.hash, more);
1420 self.len += more.len();
1421 if more.len() >= KV_PREFIX_TAIL {
1422 self.tail.clear();
1423 self.tail
1424 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1425 } else {
1426 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1427 self.tail.drain(..drop);
1428 self.tail.extend_from_slice(more);
1429 }
1430 }
1431
1432 pub fn extension(&self, ids: &[u32]) -> usize {
1436 if self.len == 0 || ids.len() <= self.len {
1437 return 0;
1438 }
1439 let t = self.tail.len();
1440 if ids[self.len - t..self.len] != self.tail[..] {
1441 return 0;
1442 }
1443 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1444 return 0;
1445 }
1446 self.len
1447 }
1448}
1449
1450pub struct GenerateResult {
1452 pub text: String,
1453 pub token_ids: Vec<u32>,
1454 pub prompt_tokens: usize,
1455 pub tokens_generated: usize,
1456 pub finish_reason: String,
1457 pub mtp_drafted: usize,
1459 pub mtp_accepted: usize,
1460 pub token_confidence: Vec<f32>,
1465 pub traces: Vec<TokenTrace>,
1468}
1469
1470#[derive(Clone, Debug)]
1475pub struct TokenTrace {
1476 pub t: usize,
1478 pub token_id: u32,
1480 pub confidence: f32,
1482 pub active_skill: Option<String>,
1484 pub recon: Option<f32>,
1488 pub switched: bool,
1491}
1492
1493#[cfg_attr(not(test), allow(dead_code))]
1498fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1499 let t = if temp > 1e-3 { temp } else { 1.0 };
1500 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1501 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1502 if sum > 0.0 {
1503 (((logits[id as usize] - max) / t).exp()) / sum
1504 } else {
1505 0.0
1506 }
1507}
1508
1509fn prefill_batched() -> bool {
1512 std::env::var("CMF_PREFILL")
1513 .map(|v| v != "seq")
1514 .unwrap_or(true)
1515}
1516
1517#[inline]
1521fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1522 let graph_quality = unmasked && prefer_graph;
1523 let fused_head_quality = graph_quality && native_metal;
1524 (graph_quality, fused_head_quality)
1525}
1526
1527#[derive(Clone, Copy)]
1531enum PrefillIn<'a> {
1532 Ids(&'a [u32]),
1533 Hidden(&'a [f32]),
1534}
1535
1536impl Pipeline {
1543 pub(crate) fn can_prefill_batched(&self) -> bool {
1544 #[cfg(test)]
1545 let force_serial = self.nll_test_force_serial;
1546 #[cfg(not(test))]
1547 let force_serial = false;
1548 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1549 }
1550
1551 fn automatic_gpu_prefix(&self) -> Option<usize> {
1554 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1555 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1556 }
1557
1558 pub fn prefill_chunk(&self) -> usize {
1562 let env = env_prefill_chunk();
1563 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1564 return prefill_chunk_rule(env, ChunkHost::here(), false);
1565 }
1566 prefill_chunk_rule(
1567 None,
1568 ChunkHost::Other,
1569 self.chunk_stack_facts().dense_on_discrete(),
1570 )
1571 }
1572
1573 fn per_token_stack(&self) -> bool {
1577 self.g3n.is_some() || self.g4e.is_some()
1578 }
1579
1580 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1581 let plain_dense = !self.weights.layers.is_empty()
1582 && !self.per_token_stack()
1583 && self.dsv4.is_none()
1584 && self.dsv41.is_none()
1585 && self.qwen4_exp.is_none()
1586 && self.weights.layers.iter().all(|lw| {
1587 matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1592 && matches!(lw.ffn, FfnKind::Dense(_))
1593 });
1594 let gpu_on = crate::gpu::enabled();
1595 ChunkStackFacts {
1596 plain_dense,
1597 discrete: gpu_on && crate::gpu::discrete(),
1598 gpu_on,
1599 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1602 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1603 multi_gpu: self.gpu_plan.is_some(),
1604 o1: self.o1_active(),
1605 }
1606 }
1607}
1608
1609pub fn prefill_chunk() -> usize {
1618 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1619}
1620
1621fn env_prefill_chunk() -> Option<usize> {
1622 std::env::var("CMF_PREFILL_CHUNK")
1623 .ok()
1624 .and_then(|v| v.parse::<usize>().ok())
1625}
1626
1627#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1629enum ChunkHost {
1630 Macos,
1631 Aarch64,
1633 Other,
1635}
1636
1637impl ChunkHost {
1638 fn here() -> Self {
1639 if cfg!(target_os = "macos") {
1640 ChunkHost::Macos
1641 } else if cfg!(target_arch = "aarch64") {
1642 ChunkHost::Aarch64
1643 } else {
1644 ChunkHost::Other
1645 }
1646 }
1647}
1648
1649const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1656
1657fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1663 if let Some(n) = env {
1664 return n.max(1);
1665 }
1666 match host {
1667 ChunkHost::Macos => 512,
1668 ChunkHost::Aarch64 => 256,
1671 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1672 ChunkHost::Other => 48,
1673 }
1674}
1675
1676#[derive(Clone, Copy, Debug, Default)]
1678struct ChunkStackFacts {
1679 plain_dense: bool,
1683 discrete: bool,
1685 gpu_on: bool,
1687 capacity_split: bool,
1689 multi_gpu: bool,
1691 o1: bool,
1693}
1694
1695impl ChunkStackFacts {
1696 fn dense_on_discrete(self) -> bool {
1697 self.plain_dense
1698 && self.discrete
1699 && self.gpu_on
1700 && !self.capacity_split
1701 && !self.multi_gpu
1702 && !self.o1
1703 }
1704}
1705
1706#[inline]
1712fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1713 if end <= start || start >= input_len {
1714 return 0;
1715 }
1716 let rows = (end.min(input_len) - start).min(input_len - start);
1717 if end < input_len {
1718 rows
1719 } else {
1720 rows.saturating_sub(1)
1721 }
1722}
1723
1724pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1726
1727#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1729pub(crate) struct ReuseLayer {
1730 pub full: bool,
1733 pub host_rows: usize,
1736 pub device_rows: Option<usize>,
1738 pub device_state: bool,
1740}
1741
1742#[derive(Debug, Clone, PartialEq, Eq)]
1744pub(crate) enum ReusePlan {
1745 Ready,
1747 Pull(Vec<(usize, usize, usize)>),
1750 Fresh,
1752}
1753
1754pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1764 let mut pulls = Vec::new();
1765 for (li, l) in layers.iter().enumerate() {
1766 if !l.full {
1767 if l.device_state {
1768 return ReusePlan::Fresh;
1769 }
1770 continue;
1771 }
1772 if l.host_rows == reuse_from {
1773 continue;
1774 }
1775 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1776 pulls.push((li, l.host_rows, reuse_from));
1777 continue;
1778 }
1779 return ReusePlan::Fresh;
1780 }
1781 if pulls.is_empty() {
1782 ReusePlan::Ready
1783 } else {
1784 ReusePlan::Pull(pulls)
1785 }
1786}
1787
1788impl Pipeline {
1789 fn reset_fresh_mtp_draft_state(&mut self) {
1792 self.draft_full_streak = 0;
1793 }
1794
1795 fn clear_sequence_state(&mut self) {
1803 #[cfg(target_os = "macos")]
1806 let _ = crate::gpu_metal::wait_replay();
1807 self.kv_cache.clear();
1808 self.clear_history();
1811 self.graph_logits = None;
1812 if let Some(b) = &mut self.dsv41 {
1813 b.3.clear();
1814 }
1815 crate::gpu::graph_kv_reset(self.graph_kv_id);
1816 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1821 }
1822
1823 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1830 if self.graph_prefill_preferred() {
1831 return true;
1832 }
1833 let kv_id = self.graph_kv_id;
1834 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1835 .map(|li| {
1836 let full = matches!(
1837 self.weights.layers[self.phys_layer(li)].attn,
1838 AttnKind::Full { .. }
1839 );
1840 ReuseLayer {
1841 full,
1842 host_rows: self.kv_cache.layers[li].pos_len(),
1845 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1846 device_state: crate::gpu::graph_state_resident(kv_id, li),
1847 }
1848 })
1849 .collect();
1850 if layers
1854 .iter()
1855 .all(|l| l.device_rows.is_none() && !l.device_state)
1856 {
1857 return true;
1858 }
1859 let plan = kv_reuse_plan(reuse_from, &layers);
1860 let (what, rows, n) = match &plan {
1861 ReusePlan::Ready => ("host ready", 0, 0),
1862 ReusePlan::Fresh => ("fresh", 0, 0),
1863 ReusePlan::Pull(p) => (
1864 "pull",
1865 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1866 p.len(),
1867 ),
1868 };
1869 let t0 = std::time::Instant::now();
1870 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1871 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1872 eprintln!(
1873 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1874 if ok { "" } else { " (failed → fresh)" },
1875 t0.elapsed().as_secs_f64() * 1e3
1876 );
1877 }
1878 ok
1879 }
1880
1881 fn apply_kv_reuse_plan(
1882 &mut self,
1883 reuse_from: usize,
1884 plan: ReusePlan,
1885 layers: &[ReuseLayer],
1886 ) -> bool {
1887 let kv_id = self.graph_kv_id;
1888 match plan {
1889 ReusePlan::Fresh => return false,
1890 ReusePlan::Ready => {}
1891 ReusePlan::Pull(pulls) => {
1892 let (nkv, hd) = {
1897 let c = &self.kv_cache.layers[pulls[0].0];
1898 (c.num_kv_heads, c.head_dim)
1899 };
1900 let uniform = pulls.iter().all(|&(li, _, _)| {
1901 let c = &self.kv_cache.layers[li];
1902 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1903 });
1904 let batched = if uniform {
1905 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1906 } else {
1907 None
1908 };
1909 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1910 Some(rows) => rows,
1911 None => {
1912 let mut rows = Vec::with_capacity(pulls.len());
1913 for &(li, from, to) in &pulls {
1914 let (lnkv, lhd) = {
1915 let c = &self.kv_cache.layers[li];
1916 (c.num_kv_heads, c.head_dim)
1917 };
1918 let Some((k, v, first_valid)) =
1919 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1920 else {
1921 return false;
1922 };
1923 let need_from = match self.layer_window(li) {
1927 Some(w) => from.max((to + 1).saturating_sub(w)),
1928 None => from,
1929 };
1930 if first_valid > need_from {
1931 return false;
1932 }
1933 rows.push((k, v));
1934 }
1935 rows
1936 }
1937 };
1938 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1939 let cache = &mut self.kv_cache.layers[li];
1940 let row = cache.num_kv_heads * cache.head_dim;
1941 for p in 0..to - from {
1942 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1943 }
1944 if cache.pos_len() != to {
1945 return false;
1946 }
1947 }
1948 }
1949 }
1950 for (li, l) in layers.iter().enumerate() {
1954 if l.full
1955 && l.device_rows.is_some_and(|d| d > reuse_from)
1956 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1957 {
1958 return false;
1959 }
1960 }
1961 true
1962 }
1963
1964 fn finish_generation(
1970 &mut self,
1971 mtp: &mut Option<MtpModule>,
1972 router: &mut Option<crate::swarm::DynRouter>,
1973 clear_sequence: bool,
1974 ) {
1975 if router.is_some() {
1979 let _ = self.set_active_skill(None);
1980 }
1981 #[cfg(target_os = "macos")]
1988 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1989 if clear_sequence {
1990 self.clear_sequence_state();
1991 if let Some(m) = mtp.as_mut() {
1992 m.kv.clear();
1998 }
1999 if let Some(m) = self.mtp.as_mut() {
2000 m.kv.clear();
2004 }
2005 }
2006 self.graph_want_logits = false;
2007 self.graph_head_required = false;
2008 self.graph_logits = None;
2009 self.graph_failed
2010 .store(false, std::sync::atomic::Ordering::Relaxed);
2011 self.cancel
2012 .store(false, std::sync::atomic::Ordering::Relaxed);
2013 self.dyn_router = router.take().or(self.dyn_router.take());
2014 self.mtp = mtp.take().or(self.mtp.take());
2015 self.mtp_graph_mode = None;
2016 self.spec_forced = None;
2017 }
2018
2019 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2023 if self
2024 .graph_failed
2025 .swap(false, std::sync::atomic::Ordering::Relaxed)
2026 {
2027 self.cancel
2028 .store(false, std::sync::atomic::Ordering::Relaxed);
2029 self.clear_sequence_state();
2030 self.graph_logits = None;
2031 self.graph_want_logits = false;
2032 self.graph_head_required = false;
2033 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2034 }
2035 Ok(())
2036 }
2037
2038 #[cfg(target_os = "macos")]
2039 fn fail_metal_graph(&mut self, reason: &str) {
2040 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2041 self.clear_sequence_state();
2042 self.graph_logits = None;
2043 self.graph_failed
2044 .store(true, std::sync::atomic::Ordering::Relaxed);
2045 self.cancel
2046 .store(true, std::sync::atomic::Ordering::Relaxed);
2047 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2048 }
2049
2050 fn nll_begin(&mut self) -> Result<(), String> {
2055 if self
2056 .graph_failed
2057 .swap(false, std::sync::atomic::Ordering::Relaxed)
2058 {
2059 self.cancel
2060 .store(false, std::sync::atomic::Ordering::Relaxed);
2061 self.clear_sequence_state();
2062 self.graph_logits = None;
2063 self.graph_want_logits = false;
2064 self.graph_head_required = false;
2065 return Err("GPU graph failed before NLL scoring".to_string());
2066 }
2067 self.clear_sequence_state();
2068 self.graph_logits = None;
2069 self.graph_want_logits = false;
2070 self.graph_head_required = false;
2071 Ok(())
2072 }
2073
2074 fn nll_end(&mut self) {
2078 self.clear_sequence_state();
2079 self.graph_logits = None;
2080 self.graph_want_logits = false;
2081 self.graph_head_required = false;
2082 self.graph_failed
2083 .store(false, std::sync::atomic::Ordering::Relaxed);
2084 }
2085
2086 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2089 #[cfg(test)]
2090 if self.nll_test_fail_at == Some(pos) {
2091 self.nll_test_fail_at = None;
2092 self.graph_failed
2093 .store(true, std::sync::atomic::Ordering::Relaxed);
2094 self.cancel
2095 .store(true, std::sync::atomic::Ordering::Relaxed);
2096 }
2097 if self
2098 .graph_failed
2099 .swap(false, std::sync::atomic::Ordering::Relaxed)
2100 {
2101 self.cancel
2102 .store(false, std::sync::atomic::Ordering::Relaxed);
2103 self.clear_sequence_state();
2104 self.graph_logits = None;
2105 self.graph_want_logits = false;
2106 return Err(format!(
2107 "GPU graph failed during NLL {phase} at position {pos}"
2108 ));
2109 }
2110 Ok(())
2111 }
2112
2113 #[inline]
2117 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2118 virtual_idx % self.physical_layers
2119 }
2120
2121 #[inline]
2124 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2125 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2126 }
2127
2128 #[allow(clippy::too_many_arguments)]
2130
2131 #[cfg(target_os = "macos")]
2150 fn graph_prefill_preferred(&self) -> bool {
2151 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2152 if !crate::gpu::enabled_here()
2153 || !graph_force
2154 || std::env::var("CMF_GPU_BLOCK")
2155 .map(|v| v == "0")
2156 .unwrap_or(false)
2157 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2160 {
2161 return false;
2162 }
2163 self.weights
2164 .layers
2165 .iter()
2166 .any(|lw| {
2167 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2168 })
2169 }
2170
2171 #[cfg(not(target_os = "macos"))]
2178 fn batch_prefix_prefill(&self) -> bool {
2179 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2180 Ok("0") => return false,
2181 Ok("1") => true,
2182 _ => false,
2183 };
2184 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2185 && crate::gpu::enabled_here()
2186 && !self.graph_refused()
2187 && (forced || self.graph_attn_decline_reason().is_some())
2188 && self.wgpu_graph_attn_decline().is_none()
2189 && self.attn_softcap == 0.0
2190 && self
2191 .weights
2192 .layers
2193 .iter()
2194 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2195 && self.automatic_gpu_prefix().is_some()
2196 }
2197
2198 fn g4_batch_prefill(&self) -> bool {
2204 #[cfg(target_os = "macos")]
2205 {
2206 false
2207 }
2208 #[cfg(not(target_os = "macos"))]
2209 {
2210 !self.weights.layers.is_empty()
2214 && !self.per_token_stack()
2215 && (self.global_attn.is_some() || self.attn_v_norm)
2216 && crate::gpu::g4_batch_on()
2217 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2218 && crate::gpu::enabled_here()
2219 && crate::gpu::discrete()
2220 && !self.graph_refused()
2221 && !self.o1_active()
2222 && self.attn_softcap == 0.0
2223 && self.wgpu_graph_attn_decline().is_none()
2224 && self.weights.layers.iter().all(|lw| {
2225 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2226 })
2227 && self.automatic_gpu_prefix().is_none()
2228 }
2229 }
2230
2231 #[cfg(not(target_os = "macos"))]
2238 fn g4_batch_ingest(
2239 &mut self,
2240 ids: &[u32],
2241 pos0: usize,
2242 keep_all: bool,
2243 ) -> Result<(usize, Vec<f32>), String> {
2244 let hs = self.hidden_size;
2245 let chunk = self.generation_batch_k().max(1);
2246 let end_all = pos0 + ids.len();
2247 let mut pos = pos0;
2248 let mut out = Vec::new();
2249 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2250 let end = (pos + chunk).min(end_all);
2251 let bk = end - pos;
2252 let mut hiddens = vec![0f32; bk * hs];
2253 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2254 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2255 }
2256 let positions: Vec<usize> = (pos..end).collect();
2257 let t_chunk = std::time::Instant::now();
2258 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2259 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2260 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2261 eprintln!(
2262 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2263 bk as f64 / (ms / 1000.0)
2264 );
2265 }
2266 match outcome {
2267 crate::gpu::BatchGraphOutcome::Completed => {
2268 if keep_all {
2269 out.extend_from_slice(&hiddens);
2270 } else {
2271 out = hiddens[(bk - 1) * hs..].to_vec();
2272 }
2273 pos = end;
2274 }
2275 crate::gpu::BatchGraphOutcome::Failed => {
2276 return Err("Gemma-4 batched prefill failed after admission".into());
2277 }
2278 crate::gpu::BatchGraphOutcome::Declined => {
2279 #[cfg(feature = "gpu")]
2280 if pos > pos0 {
2281 self.pull_lagging_host_kv(0, self.num_layers, pos);
2282 }
2283 break;
2284 }
2285 }
2286 }
2287 Ok((pos - pos0, out))
2288 }
2289
2290 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2295 #[cfg(not(target_os = "macos"))]
2296 {
2297 if ids.is_empty() || !self.g4_batch_prefill() {
2298 return None;
2299 }
2300 self.clear_sequence_state();
2301 match self.g4_batch_ingest(ids, 0, true) {
2302 Ok((n, rows)) if n == ids.len() => Some(rows),
2303 _ => {
2304 self.clear_sequence_state();
2305 None
2306 }
2307 }
2308 }
2309 #[cfg(target_os = "macos")]
2310 {
2311 let _ = ids;
2312 None
2313 }
2314 }
2315
2316 #[cfg(not(target_os = "macos"))]
2317 fn graph_prefill_preferred(&self) -> bool {
2318 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2326 if !graph_on || !crate::gpu::enabled_here() {
2327 return false;
2328 }
2329 if self.embryo_resident_eligible() {
2333 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2338 }
2339 if self.o1_active() {
2353 return false;
2354 }
2355 if self.wgpu_graph_attn_decline().is_some() {
2359 return false;
2360 }
2361 if self
2362 .weights
2363 .layers
2364 .iter()
2365 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2366 {
2367 return true;
2368 }
2369 self.weights
2380 .layers
2381 .iter()
2382 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2383 && self.automatic_gpu_prefix().is_none()
2384 }
2385
2386 #[cfg(target_os = "macos")]
2387 fn q1_graph_gpu(
2388 &mut self,
2389 start: usize,
2390 upto: Option<usize>,
2391 position: usize,
2392 h: &mut [f32],
2393 ) -> usize {
2394 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2396 const MOE_WORKING_SET_REASON: &str =
2397 "model weights exceed Metal working set for native MoE graph";
2398 if self
2402 .graph_declines
2403 .borrow()
2404 .iter()
2405 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2406 {
2407 return start;
2408 }
2409 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2410 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2412 || !crate::gpu::enabled_here()
2413 || !graph_force
2414 || std::env::var("CMF_GPU_BLOCK")
2415 .map(|v| v == "0")
2416 .unwrap_or(false)
2417 {
2418 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2419 eprintln!(
2420 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2421 self.attn_softcap > 0.0,
2422 crate::gpu::enabled_here(),
2423 graph_force,
2424 );
2425 }
2426 if self.graph_head_required {
2427 self.fail_metal_graph("native graph front gate refused");
2428 }
2429 return start;
2430 }
2431 let gemma4 = self.metal_gemma4();
2441 let swa_graph = self.metal_graph_swa() || gemma4;
2442 let graph_attn_reason = self.graph_attn_decline_reason();
2449 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2450 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2451 if (self.swa.is_some() && !swa_graph)
2452 || (self.global_attn.is_some() && !gemma4)
2453 || self.attention_heads_per_layer.is_some()
2454 || (self.attn_v_norm && !gemma4)
2455 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2458 || self.weights.layers.iter().any(|lw| {
2459 ((lw.attn_out_norm.is_some()
2460 || lw.ffn_out_norm.is_some()
2461 || lw.layer_scale.is_some())
2462 && !gemma4)
2463 || matches!(&lw.ffn, FfnKind::Dense(d)
2464 if !(matches!(d.act, Act::Silu | Act::Gelu)
2465 || (gemma4 && d.act == Act::GeluTanh)))
2466 })
2467 {
2468 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2471 self.note_graph_decline("metal block graph", reason);
2472 }
2473 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2474 eprintln!(
2475 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2476 self.swa.is_some(),
2477 self.global_attn.is_some(),
2478 self.attention_heads_per_layer.is_some(),
2479 self.attn_v_norm,
2480 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2481 );
2482 }
2483 if self.graph_head_required {
2484 self.fail_metal_graph("native graph architecture gate refused");
2485 }
2486 return start;
2487 }
2488 let limit = upto
2491 .map(|u| u + 1)
2492 .unwrap_or(self.num_layers)
2493 .min(self.num_layers);
2494
2495 enum Item<'a> {
2496 Gdn {
2497 run: Vec<GdnGpuLayer<'a>>,
2498 first: usize,
2499 },
2500 Attn {
2501 l: AttnGpuLayer<'a>,
2502 li: usize,
2503 q_norm: Option<&'a [f32]>,
2504 k_norm: Option<&'a [f32]>,
2505 output_gate: bool,
2506 proj_gate: Option<(&'a QTensor, bool)>,
2510 head_gate_w: Option<&'a [f32]>,
2513 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2514 full_gpu: bool,
2517 },
2518 }
2519
2520 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2527 let attend_contract = attend_mode != "0"
2528 && attend_mode != "off"
2529 && self.head_dim % 4 == 0
2530 && self.head_dim <= 256
2531 && (self.rotary_dim == 0
2535 || (self.rotary_dim >= 2
2536 && self.rotary_dim <= self.head_dim
2537 && (self.rotary_dim / 2) % 32 == 0))
2538 && self.num_kv_heads > 0
2539 && self.num_heads % self.num_kv_heads == 0;
2540
2541 let mut plan: Vec<Item> = Vec::new();
2542 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2543 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2545 let mut scan = start;
2546 while scan < limit {
2547 let lw = &self.weights.layers[self.phys_layer(scan)];
2548 let ffn = match &lw.ffn {
2549 FfnKind::Dense(d) if d.segs.is_empty() => {
2550 let (Some(g), Some(u), Some(dn)) = (
2551 d.gate_proj.metal_graph_parts(),
2552 d.up_proj.metal_graph_parts(),
2553 d.down_proj.metal_graph_parts(),
2554 ) else {
2555 if block_diag {
2556 eprintln!(
2557 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2558 );
2559 }
2560 break;
2561 };
2562 MetalFfn::Dense {
2563 gate: g,
2564 up: u,
2565 down: dn,
2566 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2569 }
2570 }
2571 FfnKind::Moe(m) => {
2572 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2578 {
2579 MetalFfn::FlintMoe(flint)
2580 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2581 MetalFfn::Moe(moe)
2582 } else {
2583 if block_diag {
2584 eprintln!(
2585 "block-graph: L{scan} MoE outside the graph contract — run ends"
2586 );
2587 }
2588 break;
2589 };
2590 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2591 model_ref.get_or_insert_with(|| model.clone());
2592 }
2593 ffn
2594 }
2595 _ => {
2596 if block_diag {
2597 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2598 }
2599 break;
2600 }
2601 };
2602 match &lw.attn {
2603 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2604 let parts = (
2605 w.in_proj_qkv.metal_graph_parts(),
2606 w.in_proj_z.metal_graph_parts(),
2607 w.in_proj_a.f32_parts(),
2608 w.in_proj_b.f32_parts(),
2609 w.out_proj.metal_graph_parts(),
2610 );
2611 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2612 if block_diag {
2613 eprintln!(
2614 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2615 w.in_proj_qkv.metal_graph_parts().is_some(),
2616 w.in_proj_z.metal_graph_parts().is_some(),
2617 w.in_proj_a.f32_parts().is_some(),
2618 w.in_proj_b.f32_parts().is_some(),
2619 w.out_proj.metal_graph_parts().is_some(),
2620 );
2621 }
2622 break;
2623 };
2624 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2625 model_ref.get_or_insert_with(|| model.clone());
2626 }
2627 let gl = GdnGpuLayer {
2628 attn_norm: &lw.input_norm,
2629 post_norm: &lw.post_norm,
2630 qkv,
2631 z,
2632 a,
2633 b,
2634 out,
2635 ffn,
2636 conv1d: &w.conv1d,
2637 a_log: &w.a_log,
2638 dt_bias: &w.dt_bias,
2639 gnorm: &w.norm,
2640 };
2641 match plan.last_mut() {
2642 Some(Item::Gdn { run, .. }) => run.push(gl),
2643 _ => plan.push(Item::Gdn {
2644 run: vec![gl],
2645 first: scan,
2646 }),
2647 }
2648 }
2649 AttnKind::Full {
2650 wq,
2651 wk,
2652 wv,
2653 wo,
2654 q_norm,
2655 k_norm,
2656 output_gate,
2657 softplus_gate,
2658 bias,
2659 } if (!self.kv_cache.layers[scan].o1_sealed()
2660 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2665 && softplus_gate
2669 .as_ref()
2670 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2671 {
2672 let parts = (
2673 wq.metal_graph_parts(),
2674 wk.metal_graph_parts(),
2675 wv.metal_graph_parts(),
2676 wo.metal_graph_parts(),
2677 );
2678 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2679 break;
2680 };
2681 if let QTensor::Mapped { model, .. } = wq {
2682 model_ref.get_or_insert_with(|| model.clone());
2683 }
2684 let cache = &self.kv_cache.layers[scan];
2685 let o1_metal = cache.o1.is_some()
2689 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2690 && cache.o1_views().is_some();
2691 let head_gate_w = softplus_gate
2695 .as_ref()
2696 .and_then(|(g, _)| g.f32_parts())
2697 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2698 .map(|(d, _, _)| d);
2699 let (nkv_g, hd_g) = if gemma4 {
2702 let (a, b, _) = self.layer_geom(scan);
2703 (a, b)
2704 } else {
2705 (self.num_kv_heads, self.head_dim)
2706 };
2707 let full_gpu = attend_contract
2708 && softplus_gate.is_none() == head_gate_w.is_none()
2709 && cache.mode == crate::kv_cache::KvMode::F32
2710 && (cache.o1.is_none() || o1_metal)
2711 && bias.is_none()
2712 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2713 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2714 && pk.1 == nkv_g * hd_g
2715 && pv.1 == nkv_g * hd_g
2716 && po.2 == self.num_heads * hd_g;
2717 plan.push(Item::Attn {
2718 l: AttnGpuLayer {
2719 attn_norm: &lw.input_norm,
2720 post_norm: &lw.post_norm,
2721 wq: pq,
2722 wk: pk,
2723 wv: pv,
2724 wo: po,
2725 ffn,
2726 attn_out_norm: lw.attn_out_norm.as_deref(),
2727 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2728 layer_scale: lw.layer_scale,
2729 },
2730 li: scan,
2731 q_norm: q_norm.as_deref(),
2732 k_norm: k_norm.as_deref(),
2733 output_gate: *output_gate,
2734 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2735 head_gate_w,
2736 bias: bias
2737 .as_ref()
2738 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2739 full_gpu,
2740 });
2741 }
2742 _ => break,
2743 }
2744 scan += 1;
2745 }
2746 let Some(model) = model_ref else {
2747 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2748 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2749 }
2750 if self.graph_head_required {
2751 self.fail_metal_graph("native graph has no mapped model reference");
2752 }
2753 return start;
2754 };
2755 if plan.is_empty() {
2756 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2757 eprintln!("q1-graph: empty plan at layer {start}");
2758 }
2759 if self.graph_head_required {
2760 self.fail_metal_graph("native graph plan is empty");
2761 }
2762 return start;
2763 }
2764 let has_moe = plan.iter().any(|it| match it {
2765 Item::Gdn { run, .. } => run
2766 .iter()
2767 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2768 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2769 });
2770 let has_flint_moe = plan.iter().any(|it| match it {
2771 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2772 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2773 });
2774 let metal_working_set = crate::gpu_metal::working_set_bytes();
2775 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2776 let model_bytes = model.primary_bytes().len() as u64;
2777 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2783 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2784 let pager_cap = if !has_moe {
2793 None
2794 } else if has_flint_moe {
2795 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2799 } else {
2800 crate::gpu_metal::moe_pager_cap_bytes()
2801 };
2802 let pager_eligible =
2803 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2804 let mut pager_span: Option<(usize, usize)> = None;
2805 if pager_eligible {
2806 let cap = pager_cap.expect("checked above");
2807 let tail_span = (self.graph_want_logits
2811 && upto.is_none()
2812 && scan == self.num_layers
2813 && std::env::var("CMF_GPU_LMHEAD")
2814 .map(|v| v != "0")
2815 .unwrap_or(true))
2816 .then(|| self.weights.lm_head.metal_graph_parts())
2817 .flatten()
2818 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2819 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2820 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2821 Some(match span {
2822 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2823 None => (lo, hi),
2824 })
2825 };
2826 let page_span = |span: (usize, usize), include_tail: bool| {
2827 let (mut lo, mut hi) = span;
2828 if include_tail {
2829 if let Some((tail_lo, tail_hi)) = tail_span {
2830 lo = lo.min(tail_lo);
2831 hi = hi.max(tail_hi);
2832 }
2833 }
2834 crate::gpu_metal::page_round_span(lo, hi)
2835 };
2836 let mut selected = 0usize;
2837 let mut selected_span: Option<(usize, usize)> = None;
2838 let mut gdn_prefix: Option<(usize, usize)> = None;
2846 'items: for (i, item) in plan.iter().enumerate() {
2847 match item {
2848 Item::Attn { li, .. } => {
2849 let Some(span) = add_layer_span(selected_span, *li) else {
2850 break;
2851 };
2852 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2853 break;
2854 };
2855 if page_hi - page_lo > cap {
2856 break;
2857 }
2858 selected = i + 1;
2859 selected_span = Some((page_lo, page_hi));
2860 }
2861 Item::Gdn { first, run } => {
2862 let mut whole = None;
2869 for take in 1..=run.len() {
2870 whole = add_layer_span(whole, *first + take - 1);
2871 if whole.is_none() {
2872 break;
2873 }
2874 }
2875 let whole_fits_empty_page = whole
2876 .and_then(|span| page_span(span, false))
2877 .is_some_and(|(lo, hi)| hi - lo <= cap);
2878 if whole_fits_empty_page {
2879 let mut span = selected_span;
2880 for take in 1..=run.len() {
2881 let Some(next) = add_layer_span(span, *first + take - 1) else {
2882 break 'items;
2883 };
2884 span = Some(next);
2885 }
2886 let Some(span) = span else {
2887 break;
2888 };
2889 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2890 else {
2891 break;
2892 };
2893 if page_hi - page_lo > cap {
2894 break;
2895 }
2896 selected = i + 1;
2897 selected_span = Some((page_lo, page_hi));
2898 continue;
2899 }
2900 if selected != 0 {
2901 break;
2902 }
2903 let mut span = None;
2904 let mut best: Option<(usize, (usize, usize))> = None;
2905 for take in 1..=run.len() {
2906 let Some(next) = add_layer_span(span, *first + take - 1) else {
2907 break;
2908 };
2909 let Some((page_lo, page_hi)) = page_span(next, false) else {
2912 break;
2913 };
2914 if page_hi - page_lo > cap {
2915 break;
2916 }
2917 span = Some((page_lo, page_hi));
2918 best = Some((take, (page_lo, page_hi)));
2919 }
2920 let Some((take, page)) = best else {
2921 break;
2922 };
2923 selected = i + 1;
2924 selected_span = Some(page);
2925 if take != run.len() {
2926 gdn_prefix = Some((i, take));
2927 break 'items;
2928 }
2929 }
2930 }
2931 }
2932 if selected != 0 {
2933 if let Some((item, take)) = gdn_prefix {
2934 match &mut plan[item] {
2935 Item::Gdn { run, .. } => run.truncate(take),
2936 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2937 }
2938 }
2939 let (page_lo, page_hi) = selected_span.expect("selected page span");
2940 tracing::info!(
2941 page_start = page_lo,
2942 page_end = page_hi,
2943 page_mb = (page_hi - page_lo) / (1024 * 1024),
2944 cap_mb = cap / (1024 * 1024),
2945 graph_items = selected,
2946 "native Metal MoE graph uses transient bounded weight page"
2947 );
2948 plan.truncate(selected);
2949 pager_span = Some((page_lo, page_hi));
2950 } else {
2951 tracing::warn!(
2952 cap_mb = cap / (1024 * 1024),
2953 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2954 );
2955 }
2956 }
2957 if has_moe && needs_bounded_pager && pager_span.is_none() {
2966 tracing::info!(
2967 model_bytes,
2968 metal_working_set,
2969 "native Metal MoE graph declined before allocation"
2970 );
2971 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2972 if self.graph_head_required {
2973 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2974 }
2975 return start;
2976 }
2977 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2978 let dev_attend = attend_contract
2979 && (self.head_dim <= 128
2980 || has_moe
2981 || (self.head_dim <= 256 && has_gdn)
2987 || (self.head_dim <= 256 && swa_graph)
2991 || attend_mode == "force"
2992 || attend_mode == "256");
2993 if !dev_attend {
2994 for it in &mut plan {
2995 if let Item::Attn { li, full_gpu, .. } = it {
2996 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2999 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3000 if !keep_o1 {
3001 *full_gpu = false;
3002 }
3003 }
3004 }
3005 }
3006 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3007 use std::sync::atomic::{AtomicBool, Ordering};
3008 static SAID: AtomicBool = AtomicBool::new(false);
3009 if !SAID.swap(true, Ordering::Relaxed) {
3010 let fg = plan
3011 .iter()
3012 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3013 .count();
3014 let att = plan
3015 .iter()
3016 .filter(|it| matches!(it, Item::Attn { .. }))
3017 .count();
3018 eprintln!(
3019 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3020 plan.len(),
3021 self.head_dim,
3022 self.rotary_dim,
3023 self.num_kv_heads,
3024 self.num_heads,
3025 );
3026 }
3027 }
3028 let dims = GraphDims {
3029 hidden: self.hidden_size,
3030 eps: self.rms_eps as f32,
3031 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3032 };
3033 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3037 crate::gpu_metal::MoePagerPageScope::new(
3038 _mt0,
3039 position,
3040 start,
3041 page_lo,
3042 page_hi,
3043 pager_cap.unwrap_or(0),
3047 )
3048 });
3049 let graph_new = match pager_span {
3050 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3051 None => TokenGraph::new(&model, dims, h),
3052 };
3053 let Some(mut graph) = graph_new else {
3054 if self.graph_head_required {
3055 self.fail_metal_graph("native TokenGraph allocation refused");
3056 }
3057 return start;
3058 };
3059 if swa_graph && self.head_dim > 128 {
3060 graph.set_attend_blk(1, 32);
3070 }
3071 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3072 nv: cfg.num_v_heads,
3073 nk: cfg.num_k_heads,
3074 dk: cfg.key_head_dim,
3075 dv: cfg.value_head_dim,
3076 kk: cfg.conv_kernel,
3077 hidden: self.hidden_size,
3078 inter: self.intermediate_size,
3079 c_dim: cfg.conv_dim(),
3080 eps: cfg.rms_eps as f32,
3081 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3082 });
3083 let mut valid = 0usize;
3087 let mut end = start;
3088 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3090 static ONCE: std::sync::Once = std::sync::Once::new();
3091 ONCE.call_once(|| {
3092 for it in &plan {
3093 match it {
3094 Item::Gdn { first, run } => {
3095 eprintln!("plan: Gdn first={first} len={}", run.len())
3096 }
3097 Item::Attn { li, full_gpu, .. } => {
3098 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3099 }
3100 }
3101 }
3102 });
3103 }
3104 for item in &plan {
3105 let ok = match item {
3106 Item::Gdn { run, .. } => gcfg
3107 .as_ref()
3108 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3109 .unwrap_or(false),
3110 Item::Attn { l, .. } => graph.attn_ok(l),
3111 };
3112 if !ok {
3113 if block_diag {
3114 eprintln!(
3115 "block-graph: plan item {} ({}) failed graph preflight",
3116 valid,
3117 match item {
3118 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3119 Item::Attn { li, .. } => format!("Attn L{li}"),
3120 }
3121 );
3122 }
3123 break;
3124 }
3125 valid += 1;
3126 end += match item {
3127 Item::Gdn { run, .. } => run.len(),
3128 Item::Attn { .. } => 1,
3129 };
3130 }
3131 plan.truncate(valid);
3132 if plan.is_empty() {
3133 if self.graph_head_required {
3134 self.fail_metal_graph("native graph preflight produced no valid items");
3135 }
3136 return start;
3137 }
3138
3139 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3140 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3141 return start;
3142 }
3143
3144 let one_pass = |t: (usize, usize, usize)| {
3153 use cortiq_core::TensorDtype as D;
3154 matches!(
3155 model.tensors[t.0].dtype,
3156 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3157 )
3158 };
3159 let dense_fast = plan.iter().all(|it| match it {
3160 Item::Attn {
3161 l, li, full_gpu, ..
3162 } => {
3163 *full_gpu
3164 && self.kv_cache.layers[*li].o1.is_none()
3165 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3166 && match l.ffn {
3167 MetalFfn::Dense { gate, up, down, .. } => {
3168 one_pass(gate) && one_pass(up) && one_pass(down)
3169 }
3170 _ => false,
3171 }
3172 }
3173 Item::Gdn { .. } => false,
3174 });
3175 let moe_plan = !dense_fast
3179 && plan.iter().all(|it| match it {
3180 Item::Attn {
3181 l, li, full_gpu, ..
3182 } => {
3183 *full_gpu
3184 && self.kv_cache.layers[*li].o1.is_none()
3185 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3186 }
3187 Item::Gdn { .. } => false,
3188 });
3189 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3190 if block_diag {
3191 use std::sync::atomic::{AtomicBool, Ordering};
3192 static SAID_AB: AtomicBool = AtomicBool::new(false);
3193 if !SAID_AB.swap(true, Ordering::Relaxed) {
3194 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3195 }
3196 }
3197 let _mv_fast = match ab {
3198 Some((bits, _)) => {
3199 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3200 crate::gpu_metal::MvFastGuard::set_raw(bits)
3201 }
3202 None if moe_plan => {
3203 graph.set_dense_concurrent(
3204 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3205 );
3206 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3207 }
3208 None => {
3209 graph.set_dense_concurrent(dense_fast);
3210 let q8r4 = if swa_graph {
3213 crate::gpu_metal::DENSE_Q8R4
3214 } else {
3215 0
3216 };
3217 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3218 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3219 } else {
3220 0
3221 })
3222 }
3223 };
3224
3225 let pool = self.pool.clone();
3228 let (nh, nkv, hd, hs, eps) = (
3229 self.num_heads,
3230 self.num_kv_heads,
3231 self.head_dim,
3232 self.hidden_size,
3233 self.rms_eps,
3234 );
3235 let norm_style = self.norm_style;
3236 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3237 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3238 let kv_id = self.graph_kv_id;
3239 let mut pending: Vec<(usize, usize)> = Vec::new();
3242 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3245 for item in &plan {
3246 let _xt0 = std::time::Instant::now();
3247 let _xkind: u32 = match item {
3248 Item::Gdn { .. } => 2,
3249 Item::Attn { .. } => 3,
3250 };
3251 if self.loop_final_norm {
3253 let item_start = match item {
3254 Item::Gdn { first, .. } => *first,
3255 Item::Attn { li, .. } => *li,
3256 };
3257 if item_start > start && self.is_loop_end(item_start - 1) {
3258 graph.encode_loop_norm(&self.weights.final_norm);
3259 }
3260 }
3261 match item {
3262 Item::Gdn { run, first } => {
3263 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3264 if l.linear_state.len() != want {
3265 l.linear_state = vec![0f32; want];
3266 }
3267 }
3268 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3269 .iter()
3270 .map(|l| l.linear_state.as_slice())
3271 .collect();
3272 let _ig = std::time::Instant::now();
3273 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3274 tracing::error!("q1 graph: GDN run refused after validation");
3276 return start;
3277 }
3278 graph.commit_kind = 2;
3281 graph.commit();
3282 crate::gpu::stageprof(0, _ig.elapsed());
3283 pending.push((*first, run.len()));
3284 }
3285 Item::Attn {
3286 l,
3287 li,
3288 q_norm,
3289 k_norm,
3290 output_gate,
3291 proj_gate,
3292 head_gate_w,
3293 bias,
3294 full_gpu,
3295 } => {
3296 let _ia = std::time::Instant::now();
3297 let inv_freq_l = self.layer_inv_freq(*li);
3303 let rd_l = self.layer_geom(*li).2;
3304 let window_l = self.layer_window(*li);
3305 let head_gate_w = *head_gate_w;
3306 let (nkv, hd) = if gemma4 {
3309 let (a, b, _) = self.layer_geom(*li);
3310 (a, b)
3311 } else {
3312 (nkv, hd)
3313 };
3314 if *full_gpu {
3316 let cache = &self.kv_cache.layers[*li];
3317 let o1p = if cache.o1.is_some() {
3318 match cache.o1_views() {
3319 Some(views) => Some(crate::gpu::O1AttnParams {
3320 views,
3321 epoch: self.o1_epoch,
3322 }),
3323 None => None,
3325 }
3326 } else {
3327 None
3328 };
3329 let o1_layer = cache.o1.is_some();
3330 if o1_layer && o1p.is_none() {
3331 }
3333 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3334 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3335 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3336 debug_assert!(
3339 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3340 );
3341 let p = crate::gpu::AttnDeviceParams {
3342 kv_id,
3343 layer: *li,
3344 nh,
3345 nkv,
3346 hd,
3347 rd: rd_l,
3348 position,
3349 scale: self.attn_scale,
3350 eps: eps as f32,
3351 gemma,
3352 late_qk_norm: self.qk_norm_after_rope,
3353 output_gate: *output_gate,
3354 q_norm: *q_norm,
3355 k_norm: *k_norm,
3356 inv_freq: &inv_freq_l,
3357 rope_scale: self.layer_rope_scale(*li),
3358 cpu_k,
3359 cpu_v,
3360 cpu_stored,
3361 cpu_gen: cache.generation(),
3362 o1: o1p,
3363 window: window_l,
3364 softmax_sink: cache.sinks.as_deref(),
3365 head_gate: head_gate_w,
3366 v_norm: self.attn_v_norm,
3367 };
3368 let o1_bad = o1_layer && p.o1.is_none();
3369 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3370 {
3371 if p.o1.is_none() {
3373 dev_attn.push((*li, nkv, hd));
3374 }
3375 graph.commit_kind = 3;
3376 graph.commit();
3377 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3381 continue;
3382 }
3383 }
3385 graph.encode_attn_prefix(l);
3386 if let Err(err) = graph.sync_checked() {
3387 self.fail_metal_graph(&err);
3388 return start;
3389 }
3390 if !pending.is_empty() {
3391 let idxs: Vec<usize> =
3392 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3393 let mut outs: Vec<&mut [f32]> = self
3394 .kv_cache
3395 .layers
3396 .iter_mut()
3397 .enumerate()
3398 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3399 .map(|(_, s)| s.linear_state.as_mut_slice())
3400 .collect();
3401 graph.read_states(&mut outs);
3402 }
3403 let mut q_raw = attention::take_buf(l.wq.1);
3404 let mut k = attention::take_buf(l.wk.1);
3405 let mut v = attention::take_buf(l.wv.1);
3406 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3407 let mut gate_raw = proj_gate.map(|(gp, _)| {
3410 let mut normed = attention::take_buf(hs);
3411 graph.read_normed(&mut normed);
3412 let mut raw = attention::take_buf(gp.rows());
3413 gp.matvec(&normed, &mut raw, pool.as_deref());
3414 attention::recycle_buf(&mut normed);
3415 raw
3416 });
3417 let cfg = QwenAttnCfg {
3418 num_heads: nh,
3419 num_kv_heads: nkv,
3420 head_dim: hd,
3421 hidden_size: hs,
3422 position,
3423 inv_freq: &inv_freq_l,
3424 rotary_dim: rd_l,
3425 scale: self.attn_scale,
3426 softcap: self.attn_softcap,
3427 window: window_l,
3428 v_norm: self.attn_v_norm,
3431 qk_norm_after_rope: self.qk_norm_after_rope,
3432 gate_sigmoid: self.proj_gate_sigmoid,
3433 q_norm: *q_norm,
3434 k_norm: *k_norm,
3435 output_gate: *output_gate,
3436 softplus_gate: None,
3437 rope_scale: self.layer_rope_scale(*li),
3438 bias: *bias,
3439 rms_eps: eps,
3440 norm_style,
3441 pool: pool.as_deref(),
3442 v_head_dim: hd,
3443 };
3444 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3447 || std::env::var("CMF_ATTN_DUMP").is_ok();
3448 let _ = full_gpu;
3449 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3450 let mut ao = attention::qwen_attention_core(
3451 q_raw,
3452 k,
3453 v,
3454 &mut self.kv_cache.layers[*li],
3455 &cfg,
3456 );
3457 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3461 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3462 let (cq, _cg, _ck, _cv) =
3463 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3464 let cache = &self.kv_cache.layers[*li];
3465 let n = cache.head_keys(0).len() / hd;
3466 let mut bytes: Vec<u8> = Vec::new();
3467 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3468 bytes.extend_from_slice(&v.to_le_bytes());
3469 }
3470 for v in &cq {
3471 bytes.extend_from_slice(&v.to_le_bytes());
3472 }
3473 for g in 0..nkv {
3474 for v in cache.head_keys(g) {
3475 bytes.extend_from_slice(&v.to_le_bytes());
3476 }
3477 }
3478 for g in 0..nkv {
3479 for v in cache.head_values(g) {
3480 bytes.extend_from_slice(&v.to_le_bytes());
3481 }
3482 }
3483 let _ =
3484 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3485 }
3486 }
3487 if let Some((qr0, k0, v0)) =
3488 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3489 {
3490 let (cq, _cg, ck, cv) =
3491 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3492 let mut h_now = vec![0f32; hs];
3493 graph.read_h(&mut h_now);
3494 let cache = &self.kv_cache.layers[*li];
3495 let n_after = cache.head_keys(0).len() / hd;
3496 let stored = n_after.saturating_sub(1);
3500 let cpu_k: Vec<&[f32]> = (0..nkv)
3501 .map(|g| &cache.head_keys(g)[..stored * hd])
3502 .collect();
3503 let cpu_v: Vec<&[f32]> = (0..nkv)
3504 .map(|g| &cache.head_values(g)[..stored * hd])
3505 .collect();
3506 let p = crate::gpu::AttnDeviceParams {
3507 kv_id,
3508 layer: *li,
3509 nh,
3510 nkv,
3511 hd,
3512 rd: rd_l,
3516 position,
3517 scale: self.attn_scale,
3518 eps: eps as f32,
3519 gemma,
3520 late_qk_norm: self.qk_norm_after_rope,
3521 output_gate: *output_gate,
3522 q_norm: *q_norm,
3523 k_norm: *k_norm,
3524 inv_freq: &inv_freq_l,
3525 rope_scale: self.layer_rope_scale(*li),
3526 cpu_k,
3527 cpu_v,
3528 cpu_stored: stored,
3529 cpu_gen: cache.generation(),
3530 o1: None,
3531 window: window_l,
3532 softmax_sink: cache.sinks.as_deref(),
3533 head_gate: None,
3534 v_norm: self.attn_v_norm,
3535 };
3536 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3537 let md = |a: &[f32], b: &[f32]| {
3538 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3539 };
3540 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3541 eprintln!(
3542 "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}",
3543 nn(&cq),
3544 md(&cq, &dq),
3545 nn(&ck),
3546 md(&ck, &dk),
3547 nn(&cv),
3548 md(&cv, &dv),
3549 nn(&ao),
3550 md(&ao, &dao)
3551 );
3552 } else {
3553 eprintln!("attn-oracle L{li}: device probe declined");
3554 }
3555 }
3556 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3557 attention::apply_projected_gate(
3560 &mut ao,
3561 raw,
3562 per_head,
3563 hd,
3564 self.proj_gate_sigmoid,
3565 );
3566 }
3567 if let Some(mut raw) = gate_raw.take() {
3568 attention::recycle_buf(&mut raw);
3569 }
3570 graph.encode_attn_suffix(l, &ao);
3571 graph.commit();
3574 attention::recycle_buf(&mut ao);
3575 }
3576 }
3577
3578 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3579 }
3580 let mut lm_rows = None;
3585 if self.graph_want_logits
3586 && upto.is_none()
3587 && end == self.num_layers
3588 && std::env::var("CMF_GPU_LMHEAD")
3589 .map(|v| v != "0")
3590 .unwrap_or(true)
3591 {
3592 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3593 if graph.lm_head_ok(lm) {
3594 graph.encode_lm_head(&self.weights.final_norm, lm);
3595 lm_rows = Some(lm.1);
3596 }
3597 }
3598 }
3599 if self.graph_head_required && lm_rows.is_none() {
3600 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3601 self.fail_metal_graph("fused graph head was requested but not encodable");
3602 return start;
3603 }
3604 let _sy0 = std::time::Instant::now();
3605 if let Err(err) = graph.sync_checked() {
3606 if let Some(profile) = pager_profile_page.as_mut() {
3607 profile.finish(start, graph.pager_page_stats(), "sync-error");
3608 }
3609 self.fail_metal_graph(&err);
3610 return start;
3611 }
3612 let _rs0 = std::time::Instant::now();
3613 if !pending.is_empty() {
3614 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3615 let mut outs: Vec<&mut [f32]> = self
3616 .kv_cache
3617 .layers
3618 .iter_mut()
3619 .enumerate()
3620 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3621 .map(|(_, s)| s.linear_state.as_mut_slice())
3622 .collect();
3623 graph.read_states(&mut outs);
3624 }
3625 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3626 use std::sync::atomic::{AtomicU64, Ordering};
3627 static SY: AtomicU64 = AtomicU64::new(0);
3628 static RS: AtomicU64 = AtomicU64::new(0);
3629 static N: AtomicU64 = AtomicU64::new(0);
3630 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3631 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3632 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3633 if n % 100 == 0 {
3634 eprintln!(
3635 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3636 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3637 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3638 );
3639 }
3640 }
3641 if let Some(rows) = lm_rows {
3642 crate::gpu::hostprof_encode_done(_mt0);
3643 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3644 graph.read_logits(&mut lg);
3645 crate::gpu::hostprof_total(_mt0);
3646 lg.resize(self.vocab_size, 0.0);
3647 if let Some(c) = self.final_softcap {
3648 softcap_in_place(&mut lg, c, self.pool.as_deref());
3649 }
3650 self.graph_logits = Some(lg);
3651 }
3652 graph.read_h(h);
3653 if self.graph_head_required && self.graph_logits.is_none() {
3654 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3655 self.fail_metal_graph("fused graph head completed without logits readback");
3656 return start;
3657 }
3658 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3659 METAL_GRAPH_LAYERS.fetch_add(
3660 end.saturating_sub(start) as u64,
3661 std::sync::atomic::Ordering::Relaxed,
3662 );
3663 if self.graph_head_required {
3664 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3665 }
3666 for (li, nkv, hd) in dev_attn {
3670 let mut krow = attention::take_buf(nkv * hd);
3671 let mut vrow = attention::take_buf(nkv * hd);
3672 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3673 let cache = &mut self.kv_cache.layers[li];
3674 cache.append(&krow, &vrow, &[]);
3675 let n = cache.seq_len;
3676 let mut imp = attention::take_buf(n);
3677 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3678 cache.accumulate_imp(&imp);
3679 attention::recycle_buf(&mut imp);
3680 }
3681 attention::recycle_buf(&mut krow);
3682 attention::recycle_buf(&mut vrow);
3683 }
3684 if let Some(profile) = pager_profile_page.as_mut() {
3685 profile.finish(end, graph.pager_page_stats(), "page-complete");
3686 }
3687 if let Some((_, arm)) = ab {
3688 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3689 }
3690 end
3691 }
3692
3693 pub fn new(
3694 tokenizer: Tokenizer,
3695 weights: PipelineWeights,
3696 hidden_size: usize,
3697 intermediate_size: usize,
3698 num_heads: usize,
3699 num_kv_heads: usize,
3700 head_dim: usize,
3701 num_layers: usize,
3702 physical_layers: usize,
3703 loop_final_norm: bool,
3704 vocab_size: usize,
3705 rms_eps: f64,
3706 rope_base: f32,
3707 norm_style: NormStyle,
3708 max_seq_len: usize,
3709 sampler_config: SamplerConfig,
3710 ) -> Self {
3711 let rng = match sampler_config.seed {
3712 Some(s) => SplitMix64::new(s),
3713 None => SplitMix64::from_entropy(),
3714 };
3715 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3716 let pool = Pool::from_env();
3717 if let Some(p) = &pool {
3718 tracing::info!("worker pool: {} threads", p.n_workers());
3719 if let Some(model) = weights
3721 .lm_head
3722 .model_arc()
3723 .or_else(|| weights.embed_tokens.model_arc())
3724 {
3725 let regions: Vec<&[u8]> =
3726 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3727 p.bind_numa(®ions);
3728 }
3729 }
3730 Self {
3731 gpu_plan: None,
3732 tokenizer: std::sync::Arc::new(tokenizer),
3733 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3734 sampler_config,
3735 weights,
3736 hidden_size,
3737 intermediate_size,
3738 num_heads,
3739 num_kv_heads,
3740 head_dim,
3741 num_layers,
3742 physical_layers,
3743 loop_final_norm,
3744 vocab_size,
3745 rms_eps,
3746 rope_base,
3747 norm_style,
3748 rotary_dim: head_dim,
3749 attention_heads_per_layer: None,
3750 kv_heads_per_layer: None,
3751 v_head_dim: None,
3752 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3753 .filter(|v| !v.is_empty())
3754 .map(std::path::PathBuf::from),
3755 graph_declines: std::cell::RefCell::new(Vec::new()),
3756 mimo_moe: Default::default(),
3757 minicpm_tail: Default::default(),
3758 vmf_cfg: None,
3759 gdn_cfg: None,
3760 kda_cfg: None,
3761 g3n: None,
3762 g4e: None,
3763 dsv4: None,
3764 dsv41: None,
3765 dsv41_vision: None,
3766 dsv41_prefill: None,
3767 qwen4_exp: None,
3768 clover: None,
3769 clover_stats: clover::CloverStats::default(),
3770 dsv4_mtp: Vec::new(),
3771 dspark: None,
3772 dspark_pending: Vec::new(),
3773 dspark_hist: Vec::new(),
3774 dspark_real: Vec::new(),
3775 dspark_trunk_picks: Vec::new(),
3776 dspark_exp: Vec::new(),
3777 dspark_draft_ns: 0,
3778 m3_winners: Vec::new(),
3779 m3_counts: Vec::new(),
3780 m3_n_exp: 0,
3781 m3_tokens: 0,
3782 m3_prewarmed: false,
3783 logit_multiplier: None,
3784 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3785 graph_failed: std::sync::atomic::AtomicBool::new(false),
3786 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3787 kv_history: Vec::new(),
3788 kv_history_device: false,
3789 short_conv_cfg: None,
3790 mtp: None,
3791 mimo_mtp: None,
3792 verify_exact_moe: false,
3793 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3794 ignore_eos: false,
3795 draft_full_streak: 0,
3796 spec_k_adapt: None,
3797 spec_acc_ewma: 0.7,
3798 rng,
3799 sampler_scratch: SamplerScratch::default(),
3800 spec_forced: None,
3801 spec_q: Vec::new(),
3802 spec_p: Vec::new(),
3803 spec_res: Vec::new(),
3804 spec_qs: Vec::new(),
3805 spec_ps: Vec::new(),
3806 spec_ress: Vec::new(),
3807 mtp_graph_mode: None,
3808 #[cfg(target_os = "macos")]
3809 metal_verify: None,
3810 inv_freq,
3811 ws: ForwardScratch::new(hidden_size),
3812 pool,
3813 model: None,
3814 dyn_force_f32: false,
3815 dyn_skill_layers: Vec::new(),
3816 dyn_active: None,
3817 dyn_blend_loaded: false,
3818 dyn_phi_layer: None,
3819 dyn_phi_ema: Vec::new(),
3820 dyn_phi_seen: 0,
3821 dyn_router: None,
3822 o1_cfg: None,
3823 o1_epoch: 0,
3824 o1_flags: Vec::new(),
3825 trace: false,
3826 calib_temp: 1.0,
3827 confidence_on: true,
3828 embed_multiplier: 1.0,
3829 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3830 swa: None,
3831 swa_trim: None,
3832 swa_trim_anchor: usize::MAX,
3833 chunk_min_rows: 32,
3834 sliding_layers: None,
3835 no_rope_layers: None,
3836 anchor_core: None,
3837 bounded_rope: None,
3838 kv_prefix: KvPrefix::default(),
3839 last_prefill_tokens: 0,
3840 inv_freq_local: None,
3841 rotary_dim_local: None,
3842 rope_scale: 1.0,
3843 rope_scale_local: 1.0,
3844 global_attn: None,
3845 inv_freq_global: None,
3846 attn_v_norm: false,
3847 qk_norm_after_rope: false,
3848 proj_gate_sigmoid: false,
3849 final_softcap: None,
3850 head_clusters: None,
3851 attn_softcap: 0.0,
3852 graph_want_logits: false,
3853 graph_head_required: false,
3854 graph_logits: None,
3855 embryo_graph: None,
3856 graph_refused: std::sync::atomic::AtomicBool::new(false),
3857 graph_kv_id: {
3858 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3859 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3860 },
3861 #[cfg(test)]
3862 nll_test_fail_at: None,
3863 #[cfg(test)]
3864 nll_test_force_serial: false,
3865 }
3866 }
3867
3868 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3876 if let Err(e) = self.try_set_o1(cfg) {
3877 tracing::error!("{e}");
3878 }
3879 }
3880
3881 pub fn bounded_native(&self) -> bool {
3885 self.anchor_core.is_some()
3886 }
3887
3888 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3891 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3892 }
3893
3894 pub fn o1_refusal(&self) -> Option<String> {
3896 self.anchor_core.as_ref().map(|ac| {
3897 format!(
3898 "--o1 / CMF_O1 refused: the anchor is native bounded \
3899 (anchor_core kind={} window={} sink={}); the file's operator \
3900 is executed as-is and no post-hoc Nyström overlay applies",
3901 ac.kind, ac.window, ac.sink
3902 )
3903 })
3904 }
3905
3906 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3908 if let Some(c) = &cfg {
3909 if let Some(why) = self.o1_refusal() {
3910 self.o1_flags = Vec::new();
3911 self.o1_cfg = None;
3912 return Err(why);
3913 }
3914 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3915 self.o1_flags.clear();
3916 self.o1_cfg = None;
3917 return Err(format!(
3918 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3919 c.w, c.sink
3920 ));
3921 }
3922 }
3923 self.o1_flags = match &cfg {
3924 Some(c) => {
3925 let mut flags = c.layer_flags(self.num_layers);
3926 for (li, f) in flags.iter_mut().enumerate() {
3927 if *f
3933 && (!matches!(
3934 self.weights.layers[self.phys_layer(li)].attn,
3935 AttnKind::Full { .. }
3936 ) || self.layer_window(li).is_some()
3937 || self.kv_cache.layers[li].sinks.is_some()
3938 || self.layer_v_dim(li) != self.layer_geom(li).1)
3939 {
3940 *f = false;
3941 }
3942 }
3943 flags
3944 }
3945 None => Vec::new(),
3946 };
3947 if let Some(c) = &cfg {
3948 let n = self.o1_flags.iter().filter(|&&f| f).count();
3949 tracing::info!(
3950 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3951 self.num_layers,
3952 c.m,
3953 c.w,
3954 c.sink,
3955 c.rect
3956 );
3957 }
3958 self.o1_cfg = cfg;
3959 Ok(())
3960 }
3961
3962 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3968 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3969 return Err(format!(
3970 "anchor_core kind '{}' is not executable by this runtime",
3971 cfg.kind
3972 ));
3973 }
3974 if cfg.window == 0 {
3975 return Err("anchor_core.window must be >= 1".into());
3976 }
3977 let mut n = 0usize;
3978 for li in 0..self.num_layers {
3979 let pl = self.phys_layer(li);
3980 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3981 if w.window != cfg.window || w.sink != cfg.sink {
3982 return Err(format!(
3983 "layer {li}: bounded weights (window {} sink {}) disagree with \
3984 anchor_core (window {} sink {})",
3985 w.window, w.sink, cfg.window, cfg.sink
3986 ));
3987 }
3988 self.kv_cache.layers[li].install_bounded(cfg.window);
3989 n += 1;
3990 }
3991 }
3992 if n == 0 {
3993 return Err("anchor_core is present but no layer executes it".into());
3994 }
3995 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3996 self.bounded_rope = Some(std::sync::Arc::new(rope));
3997 self.anchor_core = Some(cfg.clone());
3998 self.embryo_graph = None;
3999 tracing::info!(
4000 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4001 cfg.kind,
4002 cfg.window,
4003 cfg.sink,
4004 self.kv_cache
4005 .layers
4006 .iter()
4007 .map(|l| l.bounded_state_bytes())
4008 .max()
4009 .unwrap_or(0)
4010 );
4011 Ok(())
4012 }
4013
4014 pub fn install_wire_identity(&mut self, identity: u64) {
4018 for li in 0..self.kv_cache.layers.len() {
4019 let pl = self.phys_layer(li);
4020 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4021 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4022 Some(AttnKind::Linear(_))
4023 | Some(AttnKind::LinearGdn(_))
4024 | Some(AttnKind::ShortConv(_))
4025 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4026 _ => crate::kv_cache::WireKind::Full,
4027 };
4028 let l = &mut self.kv_cache.layers[li];
4029 l.wire_kind = kind;
4030 l.wire_identity = identity;
4031 l.wire_layer = li as u32;
4034 }
4035 }
4036
4037 pub fn clear_history(&mut self) {
4039 self.kv_history.clear();
4040 self.kv_history_device = false;
4041 self.kv_prefix.clear();
4042 }
4043
4044 pub fn graph_refused(&self) -> bool {
4048 self.graph_refused
4049 .load(std::sync::atomic::Ordering::Relaxed)
4050 }
4051
4052 pub fn mark_graph_refused(&self) {
4054 if !self
4055 .graph_refused
4056 .swap(true, std::sync::atomic::Ordering::Relaxed)
4057 {
4058 tracing::info!(
4059 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4060 self.graph_kv_id
4061 );
4062 }
4063 }
4064
4065 pub fn device_sequence_position(&self) -> Option<usize> {
4069 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4070 }
4071
4072 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4077 let dev = self.device_sequence_position();
4078 if recorded_on_device {
4079 dev == Some(n) && self.embryo_resident_wanted()
4080 } else {
4081 dev.is_none()
4082 }
4083 }
4084
4085 pub(crate) fn invalidate_for_weight_change(&mut self) {
4093 self.clear_sequence_state();
4094 self.embryo_graph = None;
4095 }
4096
4097 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4103 let (n, on_device) = if self.bounded_native() {
4104 (
4105 self.kv_prefix.extension(input_ids),
4106 self.kv_prefix.on_device(),
4107 )
4108 } else {
4109 let h = &self.kv_history;
4110 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4111 (h.len(), self.kv_history_device)
4112 } else if !h.is_empty() && !self.kv_history_device {
4113 (self.rollback_prefix_len(input_ids), false)
4114 } else {
4115 (0, false)
4116 }
4117 };
4118 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4121 tracing::warn!(
4122 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4123 the device now holds {:?} — re-prefilling from zero",
4124 if on_device { "resident device" } else { "host" },
4125 self.device_sequence_position()
4126 );
4127 return 0;
4128 }
4129 n
4130 }
4131
4132 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4143 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4144 || self.device_sequence_position().is_some()
4145 {
4146 return 0;
4147 }
4148 let h = &self.kv_history;
4149 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4150 let n = lcp.min(input_ids.len().saturating_sub(1));
4151 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4152 return 0;
4153 }
4154 let kv_id = self.graph_kv_id;
4155 for li in 0..self.num_layers {
4156 let l = &self.kv_cache.layers[li];
4157 let tail_ok = l.base() == 0
4158 || self
4159 .layer_window(li)
4160 .is_some_and(|w| l.base() + w <= n + 1);
4161 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4162 || l.o1.is_some()
4163 || l.bounded.is_some()
4164 || !l.linear_state.is_empty()
4165 || l.mode != crate::kv_cache::KvMode::F32
4166 || l.pos_len() != h.len()
4167 || !tail_ok
4168 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4169 || crate::gpu::graph_state_resident(kv_id, li)
4170 {
4171 return 0;
4172 }
4173 }
4174 n
4175 }
4176
4177 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4180 self.cached_prefix_len(input_ids)
4181 }
4182
4183 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4189 let on_device = self.device_sequence_position().is_some();
4190 if self.bounded_native() {
4191 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4192 let prev_device = self.kv_prefix.on_device();
4193 self.kv_history.clear();
4194 self.kv_history_device = false;
4195 if keep && prev_device == on_device {
4196 self.kv_prefix.extend(&consumed[reused..]);
4197 } else {
4198 self.kv_prefix.set(consumed);
4199 }
4200 self.kv_prefix.set_on_device(on_device);
4201 } else {
4202 self.kv_history = consumed.to_vec();
4203 self.kv_history_device = on_device;
4204 }
4205 }
4206
4207 pub fn o1_active(&self) -> bool {
4209 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4210 }
4211
4212 pub fn generation_batch_k(&self) -> usize {
4230 if let Some(k) = std::env::var("CMF_BATCH_K")
4231 .ok()
4232 .and_then(|v| v.parse::<usize>().ok())
4233 {
4234 return k;
4235 }
4236 if self.g4_batch_prefill() {
4239 return 128;
4240 }
4241 #[cfg(not(target_os = "macos"))]
4242 if self.graph_prefill_preferred() && !self.o1_active() {
4243 let moe = self
4247 .weights
4248 .layers
4249 .iter()
4250 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4251 let q82 = crate::gpu::moe_q82_graph_on()
4257 && self.weights.layers.iter().any(|lw| {
4258 matches!(&lw.ffn, FfnKind::Moe(m)
4259 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4260 });
4261 let q4tp_moe = self.weights.layers.iter().any(|lw| {
4268 matches!(&lw.ffn, FfnKind::Moe(m)
4269 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4270 });
4271 return if q82 {
4272 128
4273 } else if q4tp_moe {
4274 256
4275 } else if moe {
4276 32
4277 } else {
4278 128
4279 };
4280 }
4281 0
4282 }
4283
4284 pub fn generation_graph_prefill(&self) -> bool {
4285 let graph = self.graph_prefill_preferred();
4286 #[cfg(not(target_os = "macos"))]
4297 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4298 return false;
4299 }
4300 graph
4301 }
4302
4303 pub fn o1_device_stats(&self) -> (usize, u64) {
4308 crate::gpu::o1_device_stats(self.graph_kv_id)
4309 }
4310
4311 pub fn o1_begin(&mut self) {
4316 self.o1_begin_with_prefix(None);
4317 }
4318
4319 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4323 if let Some(c) = &self.o1_cfg {
4324 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4325 let boundary = requested_prefix.map(|p| {
4326 p.max(
4327 crate::nystrom::o1_deferred_boundary(w, sink)
4328 .expect("o1 config boundary validated in set_o1"),
4329 )
4330 });
4331 for (li, &f) in self.o1_flags.iter().enumerate() {
4332 if f {
4333 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4334 }
4335 }
4336 }
4337 }
4338
4339 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4341 self.o1_cfg.as_ref().and_then(|c| {
4342 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4343 .map(|floor| requested_prefix.max(floor))
4344 })
4345 }
4346
4347 fn o1_note_transition(&mut self) {
4348 let mut transitioned = false;
4352 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4353 if flagged {
4354 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4355 }
4356 }
4357 if transitioned {
4358 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4359 }
4360 }
4361
4362 fn o1_pending(&self) -> bool {
4363 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4364 f && self.kv_cache.layers[li].seq_len > 0
4365 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4366 })
4367 }
4368
4369 fn o1_fail(&mut self, err: String) {
4370 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4371 self.clear_sequence_state();
4372 self.graph_failed
4373 .store(true, std::sync::atomic::Ordering::Relaxed);
4374 self.cancel
4375 .store(true, std::sync::atomic::Ordering::Relaxed);
4376 }
4377
4378 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4383 if self.o1_cfg.is_none() {
4384 return Ok(false);
4385 }
4386 let mut participating = false;
4387 for li in 0..self.num_layers {
4388 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4389 continue;
4390 }
4391 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4392 return Err(err);
4393 }
4394 if self.kv_cache.layers[li].seq_len == 0 {
4395 continue;
4396 }
4397 participating = true;
4398 let num_heads = self.layer_num_heads(li);
4399 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4400 }
4401 self.o1_note_transition();
4402 for li in 0..self.num_layers {
4403 if self.o1_flags.get(li).copied().unwrap_or(false) {
4404 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4405 return Err(err);
4406 }
4407 }
4408 }
4409 Ok(participating
4410 && (0..self.num_layers).all(|li| {
4411 !self.o1_flags.get(li).copied().unwrap_or(false)
4412 || self.kv_cache.layers[li].seq_len == 0
4413 || self.kv_cache.layers[li].o1_sealed()
4414 }))
4415 }
4416
4417 fn o1_progress(&mut self) {
4420 if !self.o1_active() {
4421 return;
4422 }
4423 for li in 0..self.num_layers {
4424 if self.o1_flags.get(li).copied().unwrap_or(false) {
4425 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4426 self.o1_fail(err);
4427 return;
4428 }
4429 }
4430 }
4431 self.o1_note_transition();
4435 if !self.o1_pending() {
4436 return;
4437 }
4438 if let Err(err) = self.o1_seal_checked() {
4439 self.o1_fail(err);
4440 }
4441 }
4442
4443 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4448 if self
4449 .graph_failed
4450 .swap(false, std::sync::atomic::Ordering::Relaxed)
4451 {
4452 self.cancel
4453 .store(false, std::sync::atomic::Ordering::Relaxed);
4454 self.clear_sequence_state();
4455 return Err(format!("{phase}: deferred O(1) transition failed"));
4456 }
4457 Ok(())
4458 }
4459
4460 pub fn o1_seal(&mut self) {
4464 if let Err(err) = self.o1_seal_checked() {
4465 self.o1_fail(err);
4466 }
4467 }
4468
4469 pub fn set_trace(&mut self, on: bool) {
4471 self.trace = on;
4472 }
4473
4474 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4477 self.rng = match config.seed {
4478 Some(seed) => SplitMix64::new(seed),
4479 None => SplitMix64::from_entropy(),
4480 };
4481 self.sampler_config = config;
4482 }
4483
4484 pub fn set_confidence(&mut self, on: bool) {
4489 self.confidence_on = on;
4490 }
4491
4492 pub fn set_calib_temp(&mut self, t: f32) {
4495 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4496 }
4497
4498 pub fn calib_temp(&self) -> f32 {
4500 self.calib_temp
4501 }
4502
4503 pub fn rope_inv_freq(&self) -> &[f32] {
4507 self.inv_freq.as_slice()
4508 }
4509
4510 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4513 self.rotary_dim = rotary_dim.min(self.head_dim);
4514 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4515 self.embryo_graph = None;
4519 }
4520
4521 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4522 QwenAttnCfg {
4523 num_heads: self.num_heads,
4524 num_kv_heads: self.num_kv_heads,
4525 head_dim: self.head_dim,
4526 hidden_size: self.hidden_size,
4527 position,
4528 inv_freq: &self.inv_freq,
4529 rotary_dim: self.rotary_dim,
4530 scale: self.attn_scale,
4531 softcap: self.attn_softcap,
4532 window: None,
4533 v_norm: false,
4534 qk_norm_after_rope: self.qk_norm_after_rope,
4535 gate_sigmoid: self.proj_gate_sigmoid,
4536 q_norm: None,
4537 k_norm: None,
4538 output_gate: false,
4539 softplus_gate: None,
4540 rope_scale: self.rope_scale,
4541 bias: None,
4542 rms_eps: self.rms_eps,
4543 norm_style: self.norm_style,
4544 pool: self.pool.as_deref(),
4545 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4546 }
4547 }
4548
4549 pub fn generate(
4551 &mut self,
4552 prompt: &str,
4553 max_tokens: usize,
4554 task_mask: Option<&TaskMask>,
4555 on_token: Option<TokenCallback>,
4556 ) -> Result<GenerateResult, String> {
4557 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4558 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4559 }
4560
4561 pub fn generate_from_vl(
4564 &mut self,
4565 input: &crate::dsv41_vision::PreparedVlInputs,
4566 max_tokens: usize,
4567 task_mask: Option<&TaskMask>,
4568 on_token: Option<TokenCallback>,
4569 ) -> Result<GenerateResult, String> {
4570 let Some(dsv41) = &self.dsv41 else {
4571 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4572 };
4573 if input.token_ids.is_empty() {
4574 return Err("empty V4.1 multimodal prompt".into());
4575 }
4576 if input.token_types.len() != input.token_ids.len() {
4577 return Err(format!(
4578 "V4.1 token type count {} != token count {}",
4579 input.token_types.len(),
4580 input.token_ids.len()
4581 ));
4582 }
4583 let dim = dsv41.2.dim;
4584 let mut embeddings = vec![None; input.token_ids.len()];
4585 let mut participates = vec![true; input.token_ids.len()];
4586 if !input.images.is_empty() {
4587 let vision = self
4588 .dsv41_vision
4589 .as_ref()
4590 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4591 for image in &input.images {
4592 let end = image.start.saturating_add(image.types.len());
4593 if end > input.token_ids.len() {
4594 return Err(format!(
4595 "V4.1 image span {}..{} exceeds prompt length {}",
4596 image.start,
4597 end,
4598 input.token_ids.len()
4599 ));
4600 }
4601 let mut span = vec![0.0f32; image.types.len() * dim];
4602 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4603 for (offset, &kind) in image.types.iter().enumerate() {
4604 let pos = image.start + offset;
4605 if input.token_types[pos] != kind {
4606 return Err(format!(
4607 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4608 input.token_types[pos]
4609 ));
4610 }
4611 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4612 participates[pos] = false;
4613 }
4614 }
4615 }
4616 for (pos, &kind) in input.token_types.iter().enumerate() {
4617 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4618 return Err(format!("V4.1 text position {pos} has an image embedding"));
4619 }
4620 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4621 return Err(format!("V4.1 image position {pos} has no image embedding"));
4622 }
4623 }
4624 self.dsv41_prefill = Some((embeddings, participates));
4625 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4626 self.dsv41_prefill = None;
4627 result
4628 }
4629
4630 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4632 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4633 }
4634
4635 pub fn generate_from_ids(
4643 &mut self,
4644 input_ids: &[u32],
4645 max_tokens: usize,
4646 task_mask: Option<&TaskMask>,
4647 on_token: Option<TokenCallback>,
4648 ) -> Result<GenerateResult, String> {
4649 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4650 }
4651
4652 pub fn generate_from_embeds(
4658 &mut self,
4659 input_ids: &[u32],
4660 prompt_rows: &[f32],
4661 max_tokens: usize,
4662 task_mask: Option<&TaskMask>,
4663 on_token: Option<TokenCallback>,
4664 ) -> Result<GenerateResult, String> {
4665 if input_ids.is_empty()
4666 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4667 {
4668 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4669 }
4670 if prompt_rows.iter().any(|x| !x.is_finite()) {
4671 return Err("embedded prompt contains non-finite values".into());
4672 }
4673 if !self.can_prefill_batched()
4674 || self.dyn_router.is_some()
4675 || self.o1_active()
4676 || self.mtp.is_some()
4677 || self.gpu_plan.is_some()
4678 {
4679 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4680 }
4681 self.generate_with_prompt_rows(
4682 input_ids,
4683 Some(prompt_rows),
4684 max_tokens,
4685 task_mask,
4686 on_token,
4687 )
4688 }
4689
4690 fn generate_with_prompt_rows(
4691 &mut self,
4692 input_ids: &[u32],
4693 prompt_rows: Option<&[f32]>,
4694 max_tokens: usize,
4695 task_mask: Option<&TaskMask>,
4696 mut on_token: Option<TokenCallback>,
4697 ) -> Result<GenerateResult, String> {
4698 #[cfg(target_os = "macos")]
4699 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4700 if std::env::var("CMF_TRACE_H").is_ok() {
4701 eprintln!("input_ids: {input_ids:?}");
4702 }
4703 if input_ids.is_empty() {
4704 return Err("empty prompt: nothing to generate from".to_string());
4705 }
4706 self.graph_failed
4710 .store(false, std::sync::atomic::Ordering::Relaxed);
4711 let task_mask = self.drop_open_mask(task_mask);
4716 if self.clover.is_some() {
4719 if prompt_rows.is_some() || task_mask.is_some() {
4720 return Err("block-diffusion generation takes token prompts without task masks".into());
4721 }
4722 return self.clover_generate(input_ids, max_tokens, on_token);
4723 }
4724
4725 let mut reuse_from = {
4733 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4734 if on
4735 && prompt_rows.is_none()
4736 && task_mask.is_none()
4737 && self.mtp.is_none()
4738 && !(self.mimo_mtp.is_some() && self.speculative)
4739 && self.o1_cfg.is_none()
4740 && self.dsv41.is_none()
4741 {
4742 self.cached_prefix_len(input_ids)
4743 } else {
4744 0
4745 }
4746 };
4747 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4750 reuse_from = 0;
4751 }
4752 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4755 for l in &mut self.kv_cache.layers {
4756 let d = l.pos_len() - reuse_from;
4757 l.truncate_last(d);
4758 }
4759 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4760 eprintln!(
4761 "kv-reuse: rolled back {} answer position(s) to the common prefix",
4762 self.kv_history.len() - reuse_from
4763 );
4764 }
4765 self.kv_history.truncate(reuse_from);
4766 self.graph_logits = None;
4767 }
4768 self.swa_trim_anchor = input_ids.len();
4769 self.last_prefill_tokens = input_ids.len() - reuse_from;
4770 let bounded_native = self.bounded_native();
4771 if reuse_from == 0 {
4772 self.reset_fresh_mtp_draft_state();
4777 self.clear_sequence_state();
4778 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4779 eprintln!(
4780 "kv-reuse: {} of {} prompt positions already cached",
4781 reuse_from,
4782 input_ids.len()
4783 );
4784 }
4785 crate::gpu::graph_race_begin_generation();
4786 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4790 std::env::var("CMF_O1_PREFILL")
4791 .ok()
4792 .and_then(|v| v.parse::<usize>().ok())
4793 .filter(|&p| p > 0)
4794 } else {
4795 None
4796 };
4797 if task_mask.is_none() {
4798 self.o1_begin_with_prefix(o1_prefill);
4799 }
4800
4801 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4807 #[cfg(target_os = "macos")]
4845 let metal_graph = crate::gpu::q1_force()
4846 && crate::gpu::enabled_here()
4847 && std::env::var("CMF_GPU_BLOCK")
4848 .map(|v| v != "0")
4849 .unwrap_or(true);
4850 #[cfg(target_os = "macos")]
4856 let native_metal_backend = crate::gpu::q1_force();
4857 #[cfg(not(target_os = "macos"))]
4858 let metal_graph = false;
4859 #[cfg(not(target_os = "macos"))]
4860 let native_metal_backend = false;
4861 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4862 let spec_cheap_round =
4866 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4867 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4868 || match spec_sample_env.as_deref() {
4869 Some("1") => true,
4870 Some(_) => false,
4871 None => metal_graph && spec_cheap_round,
4872 };
4873 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4889 for lw in &self.weights.layers {
4890 if let FfnKind::Dense(d) = &lw.ffn {
4891 dense_n += 1;
4892 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4893 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4894 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4895 {
4896 dense_q4tp += 1;
4897 }
4898 }
4899 }
4900 #[cfg(feature = "gpu")]
4909 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
4910 #[cfg(not(feature = "gpu"))]
4911 let mtp_on_graph = true;
4912 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
4913 let penalized = !metal_graph
4925 && (self.sampler_config.repetition_penalty != 1.0
4926 || self.sampler_config.presence_penalty != 0.0
4927 || !self.sampler_config.suppress_tokens.is_empty());
4928 #[cfg(feature = "gpu")]
4933 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4934 #[cfg(not(feature = "gpu"))]
4935 let metal_wgpu = false;
4936 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4937 let spec_wanted = graph_spec_wanted_by_policy(
4938 spec_env.as_deref(),
4939 spec_default_ok,
4940 penalized,
4941 metal_wgpu,
4942 native_metal_backend,
4943 );
4944 if spec_wanted && metal_wgpu {
4945 tracing::warn!(
4946 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4947 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4948 );
4949 }
4950 let graph_spec = self.speculative
4954 && (graph_on || metal_graph)
4955 && self.mtp.is_some()
4956 && task_mask.is_none()
4957 && !self.o1_active()
4958 && spec_sampling_ok
4959 && spec_wanted;
4960 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4973 let pair_pays = host_pair_opt_in
4974 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4975 #[cfg(target_os = "macos")]
4979 if metal_graph {
4980 static SAID: std::sync::Once = std::sync::Once::new();
4981 SAID.call_once(|| {
4982 let spec = if graph_spec {
4983 let k = std::env::var("CMF_GRAPH_SPEC_K")
4984 .ok()
4985 .and_then(|v| v.parse::<usize>().ok())
4986 .filter(|&v| (1..=8).contains(&v))
4987 .unwrap_or(7);
4988 let arm = if self.sampler_config.temperature < 1e-6 {
4989 "greedy"
4990 } else {
4991 "sampling"
4992 };
4993 format!(
4994 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4995 Self::draft_vocab_rows(usize::MAX)
4996 )
4997 } else if !self.speculative {
4998 "spec off (CMF_MTP=0)".to_string()
4999 } else if self.mtp.is_none() {
5000 "spec off (no MTP head)".to_string()
5001 } else if !spec_sampling_ok {
5002 if spec_cheap_round {
5003 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5004 } else {
5005 "spec off (sampling without a top-k: the dense chain \
5006 costs more than it saves)"
5007 .to_string()
5008 }
5009 } else if !spec_wanted {
5010 if spec_env.is_none() {
5011 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5012 } else {
5013 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5014 }
5015 } else if task_mask.is_some() {
5016 "spec off (task mask)".to_string()
5017 } else if !graph_spec && !pair_pays {
5018 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5019 .to_string()
5020 } else {
5021 "spec off (O(1) attention)".to_string()
5022 };
5023 let on = |var: &str| {
5024 if std::env::var(var).as_deref() == Ok("0") {
5025 "off"
5026 } else {
5027 "on"
5028 }
5029 };
5030 tracing::info!(
5031 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5032 MTP graph {}, attend {}, probe {}",
5033 if crate::gpu_metal::state4_on() {
5034 "on"
5035 } else {
5036 "off"
5037 },
5038 if crate::gpu_metal::async_replay_on() {
5039 "on"
5040 } else {
5041 "off"
5042 },
5043 on("CMF_METAL_PREFILL"),
5044 on("CMF_MTP_GRAPH"),
5045 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5046 if crate::gpu::probe_enabled() {
5047 "bypassed (q1 force)"
5048 } else {
5049 "off"
5050 },
5051 );
5052 });
5053 }
5054 let spec_active = self.speculative
5055 && self.mtp.is_some()
5056 && task_mask.is_none()
5057 && !self.o1_active()
5058 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5059 let mut mtp = if spec_active { self.mtp.take() } else { None };
5062 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5063 eprintln!(
5064 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5065 mtp.is_some(),
5066 self.speculative,
5067 self.sampler_config.temperature < 1e-6,
5068 );
5069 }
5070 if let Some(m) = &mut mtp {
5071 m.kv.clear();
5072 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5074 self.mtp_graph_mode = None;
5075 }
5076 let mimo_spec = self.speculative
5080 && self.mimo_mtp.is_some()
5081 && task_mask.is_none()
5082 && !self.o1_active()
5083 && self.dyn_router.is_none()
5084 && self.sampler_config.temperature < 1e-6
5085 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5086 if let Some(st) = self.mimo_mtp.as_mut() {
5087 st.reset();
5088 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5089 Self::mimo_mtp_hist_cap(st, input_ids.len());
5090 }
5091 }
5092 let mut router = if mtp.is_none() {
5096 self.dyn_router.take()
5097 } else {
5098 None
5099 };
5100 let mut reuse_from = reuse_from;
5101 if let Some(r) = &mut router {
5102 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5105 let _ = self.set_active_skill(None);
5108 reuse_from = 0;
5109 self.last_prefill_tokens = input_ids.len();
5110 }
5111 }
5112
5113 let mut all_ids = input_ids.to_vec();
5114 let mut generated = 0usize;
5115 let mut finish_reason = "max_tokens".to_string();
5116 let mut drafted = 0usize;
5117 let mut accepted = 0usize;
5118 let mut dsv4_spec_bad = 0usize;
5125 let mut dsv4_spec_retry_at = 0usize;
5126 let mut confidence: Vec<f32> = Vec::new();
5127 let trace_on = self.trace;
5128 let calib_temp = self.calib_temp;
5129 let mut traces: Vec<TokenTrace> = Vec::new();
5130
5131 let mut hidden = vec![0.0f32; self.hidden_size];
5137 let mut pos = reuse_from;
5138 let fuse_lm = mtp.is_none()
5147 && router.is_none()
5148 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5149 self.graph_logits = None;
5150 self.graph_want_logits = false;
5151 let _tpf = std::time::Instant::now();
5152 let batch_k = self.generation_batch_k();
5153 if let Some(rows) = prompt_rows {
5154 let hs = self.hidden_size;
5155 let chunk = self.prefill_chunk().max(1);
5156 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5157 let end = (pos + chunk).min(input_ids.len());
5158 let hb = match self.prefill_input_rows(
5159 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5160 pos,
5161 task_mask,
5162 ) {
5163 Ok(hb) => hb,
5164 Err(err) => {
5165 self.finish_generation(&mut mtp, &mut router, true);
5166 return Err(err);
5167 }
5168 };
5169 if mimo_spec {
5170 self.mimo_note_rows(&hb, pos);
5171 }
5172 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5173 pos = end;
5174 }
5175 }
5176 while self.qwen4_exp.is_some()
5187 && mtp.is_none()
5188 && pos < input_ids.len()
5189 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5190 {
5191 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5194 let want_logits = end == input_ids.len();
5195 let mut lg = Vec::new();
5196 if let Some(b) = &mut self.qwen4_exp {
5197 crate::qwen4_exp::forward_tokens(
5198 &b.0,
5199 &b.1,
5200 &b.2,
5201 &mut b.3,
5202 &input_ids[pos..end],
5203 pos,
5204 &self.inv_freq,
5205 self.pool.as_deref(),
5206 &mut lg,
5207 want_logits,
5208 );
5209 }
5210 if want_logits {
5211 self.graph_logits = Some(lg);
5212 }
5213 pos = end;
5214 hidden.fill(0.0);
5215 }
5216 while self.dsv4.is_some()
5217 && mtp.is_none()
5218 && pos < input_ids.len()
5219 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5220 {
5221 let end = (pos + prefill_chunk()).min(input_ids.len());
5222 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5223 let mut lg = Vec::new();
5224 if let Some(b) = &mut self.dsv4 {
5225 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5226 crate::dsv4::forward_chunk(
5227 g,
5228 layers,
5229 &cfg,
5230 st,
5231 &ids,
5232 pos,
5233 &self.inv_freq,
5234 self.pool.as_deref(),
5235 &mut lg,
5236 end == input_ids.len(),
5237 );
5238 }
5239 if end == input_ids.len() {
5240 self.graph_logits = Some(lg);
5241 }
5242 pos = end;
5243 hidden = vec![0.0; self.hidden_size];
5244 }
5245 let dsv41_prefill = self.dsv41_prefill.take();
5246 while self.dsv41.is_some()
5247 && mtp.is_none()
5248 && pos < input_ids.len()
5249 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5250 {
5251 let end = (pos + prefill_chunk()).min(input_ids.len());
5252 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5253 let mut lg = Vec::new();
5254 if let Some(b) = &mut self.dsv41 {
5255 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5256 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5257 crate::dsv41::forward_chunk_masked_with_embeddings(
5258 g,
5259 layers,
5260 cfg,
5261 st,
5262 &ids,
5263 pos,
5264 &embeddings[pos..end],
5265 &participates[pos..end],
5266 self.pool.as_deref(),
5267 &mut lg,
5268 );
5269 } else {
5270 crate::dsv41::forward_chunk(
5271 g,
5272 layers,
5273 cfg,
5274 st,
5275 &ids,
5276 pos,
5277 self.pool.as_deref(),
5278 &mut lg,
5279 );
5280 }
5281 }
5282 if end == input_ids.len() {
5283 self.graph_logits = Some(lg);
5284 }
5285 pos = end;
5286 hidden = vec![0.0; self.hidden_size];
5287 }
5288 let dyn_prefill = router.is_some();
5293 let o1_prefill_limit = o1_prefill
5301 .and_then(|requested| self.o1_effective_boundary(requested))
5302 .map(|boundary| boundary.min(input_ids.len()));
5303 let mut o1_sealed = false;
5304 if let Some(limit) = o1_prefill_limit {
5305 if self.can_prefill_batched() && limit > 2 {
5308 let chunk = self.prefill_chunk();
5309 let hs = self.hidden_size;
5310 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5311 let end = (pos + chunk).min(limit);
5312 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5313 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5314 pos = end;
5315 }
5316 } else {
5317 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5318 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5319 pos += 1;
5320 }
5321 }
5322 if pos >= limit {
5323 o1_sealed = match self.o1_seal_checked() {
5324 Ok(sealed) => sealed,
5325 Err(err) => {
5326 self.finish_generation(&mut mtp, &mut router, true);
5327 return Err(err);
5328 }
5329 };
5330 tracing::info!(
5331 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5332 o1_prefill.unwrap_or(0),
5333 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5334 .unwrap_or(limit),
5335 limit,
5336 input_ids.len()
5337 );
5338 }
5339 }
5340 let graph_prefill = self.graph_prefill_preferred();
5346 #[cfg(target_os = "macos")]
5354 if task_mask.is_none()
5355 && !dyn_prefill
5356 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5357 && crate::gpu::enabled_here()
5358 && self.gdn_cfg.is_some()
5359 && !self.per_token_stack()
5360 && input_ids.len() > 8
5361 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5362 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5363 {
5364 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5365 .ok()
5366 .and_then(|v| v.parse().ok())
5367 .filter(|&v| (16..=512).contains(&v))
5368 .unwrap_or(256);
5369 let hs = self.hidden_size;
5370 let _tp = std::time::Instant::now();
5371 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5372 let end = (pos + chunk).min(input_ids.len());
5373 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5374 MetalPrefillOutcome::Completed(hb) => hb,
5375 MetalPrefillOutcome::Declined => break,
5376 MetalPrefillOutcome::Failed => {
5377 self.finish_generation(&mut mtp, &mut router, true);
5378 return Err("ordinary Metal prefill failed after admission".into());
5379 }
5380 };
5381 if let Some(m) = &mut mtp {
5382 let n_pairs = if end < input_ids.len() {
5383 end - pos
5384 } else {
5385 end - pos - 1
5386 };
5387 if n_pairs > 0 {
5388 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5389 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5390 .collect();
5391 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5392 for (j, (h, t)) in pairs.iter().enumerate() {
5393 let h = h.to_vec();
5394 let _ = self.mtp_step(m, &h, *t, pos + j);
5395 }
5396 }
5397 }
5398 }
5399 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5400 pos = end;
5401 }
5402 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5403 eprintln!(
5404 "metal-prefill: {} of {} tokens in {:.1} ms",
5405 pos,
5406 input_ids.len(),
5407 _tp.elapsed().as_secs_f64() * 1e3
5408 );
5409 }
5410 }
5411 #[cfg(not(target_os = "macos"))]
5416 if task_mask.is_none()
5417 && !dyn_prefill
5418 && mtp.is_none()
5419 && o1_prefill.is_none()
5420 && batch_k > 0
5421 && pos + 1 < input_ids.len()
5422 && self.g4_batch_prefill()
5423 {
5424 let t_g4 = std::time::Instant::now();
5425 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5426 Ok((n, last)) => {
5427 if n > 0 {
5428 hidden.copy_from_slice(&last);
5429 }
5430 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5431 eprintln!(
5432 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5433 input_ids.len() - pos,
5434 t_g4.elapsed().as_secs_f64() * 1e3
5435 );
5436 }
5437 pos += n;
5438 }
5439 Err(e) => {
5440 self.finish_generation(&mut mtp, &mut router, true);
5441 return Err(e);
5442 }
5443 }
5444 }
5445 self.mimo_moe_prepare();
5446 #[cfg(not(target_os = "macos"))]
5452 if task_mask.is_none()
5453 && !dyn_prefill
5454 && !graph_prefill
5455 && mtp.is_none()
5456 && o1_prefill.is_none()
5457 && !self.o1_active()
5458 && input_ids.len() > 2
5459 && self.batch_prefix_prefill()
5460 {
5461 let chunk = self.prefill_chunk().max(1);
5462 let hs = self.hidden_size;
5463 let t_bp = std::time::Instant::now();
5464 let pos0 = pos;
5465 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5466 let end = (pos + chunk).min(input_ids.len());
5467 let bk = end - pos;
5468 let mut hiddens = vec![0f32; bk * hs];
5469 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5470 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5471 }
5472 let positions: Vec<usize> = (pos..end).collect();
5473 let mut run = 0usize;
5474 let outcome = self.try_batch_graph_wgpu_prefix(
5475 &mut hiddens,
5476 &positions,
5477 bk,
5478 None,
5479 Some(&mut run),
5480 );
5481 match outcome {
5482 crate::gpu::BatchGraphOutcome::Completed => {
5483 let hb = if run < self.num_layers {
5484 self.prefill_batch_span(
5485 PrefillIn::Hidden(&hiddens),
5486 pos,
5487 None,
5488 run,
5489 self.num_layers,
5490 )
5491 } else {
5492 hiddens
5493 };
5494 if mimo_spec {
5495 self.mimo_note_rows(&hb, pos);
5496 }
5497 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5498 pos = end;
5499 }
5500 crate::gpu::BatchGraphOutcome::Failed => {
5501 self.finish_generation(&mut mtp, &mut router, true);
5502 return Err("batched prefix prefill failed after admission".into());
5503 }
5504 crate::gpu::BatchGraphOutcome::Declined => {
5505 #[cfg(feature = "gpu")]
5508 if pos > pos0 {
5509 self.pull_lagging_host_kv(0, self.num_layers, pos);
5510 }
5511 break;
5512 }
5513 }
5514 }
5515 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5516 eprintln!(
5517 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5518 pos - pos0,
5519 input_ids.len(),
5520 t_bp.elapsed().as_secs_f64() * 1e3
5521 );
5522 }
5523 }
5524 if task_mask.is_none()
5525 && !dyn_prefill
5526 && !graph_prefill
5527 && self.can_prefill_batched()
5528 && !self.per_token_stack()
5529 && o1_prefill.is_none()
5530 && input_ids.len() > 2
5531 {
5532 let chunk = self.prefill_chunk();
5538 let hs = self.hidden_size;
5539 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5540 let end = (pos + chunk).min(input_ids.len());
5541 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5542 if mimo_spec {
5543 self.mimo_note_rows(&hb, pos);
5544 }
5545 if let Some(m) = &mut mtp {
5546 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5547 .ok()
5548 .and_then(|v| v.parse().ok())
5549 .unwrap_or(0);
5550 for p in pos..end {
5551 if p + 1 < input_ids.len() {
5552 if probe >= 1 && p + 2 < input_ids.len() {
5553 let (d1, mut hx) = self.mtp_step_h(
5557 m,
5558 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5559 input_ids[p + 1],
5560 p,
5561 );
5562 let mut ok = d1 == input_ids[p + 2];
5563 Self::chain_probe_note(0, ok);
5564 let mut d_prev = d1;
5565 let mut extra = 0usize;
5566 for j in 1..probe {
5567 if p + 2 + j >= input_ids.len() {
5568 break;
5569 }
5570 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5571 extra += 1;
5572 ok = ok && dj == input_ids[p + 2 + j];
5573 Self::chain_probe_note(j, ok);
5574 d_prev = dj;
5575 hx = hj;
5576 }
5577 m.kv.truncate_last(extra);
5578 } else {
5579 let _ = self.mtp_step(
5580 m,
5581 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5582 input_ids[p + 1],
5583 p,
5584 );
5585 }
5586 }
5587 }
5588 }
5589 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5590 pos = end;
5591 }
5592 }
5593 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5594 if task_mask.is_none()
5595 && !dyn_prefill
5596 && !graph_prefill
5597 && !pair_off
5598 && self.pair_supported()
5599 && o1_prefill.is_none()
5600 {
5601 while pos + 1 < input_ids.len()
5602 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5603 {
5604 let e1 = self.embed_single(input_ids[pos]);
5605 let e2 = self.embed_single(input_ids[pos + 1]);
5606 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5607 if mimo_spec {
5608 self.mimo_note_rows(&h1, pos);
5609 self.mimo_note_rows(&h2, pos + 1);
5610 }
5611 self.commit_linear_scratch();
5613 if let Some(m) = &mut mtp {
5614 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5615 if pos + 2 < input_ids.len() {
5616 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5617 .ok()
5618 .and_then(|v| v.parse().ok())
5619 .unwrap_or(0);
5620 if probe >= 1 && pos + 3 < input_ids.len() {
5621 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5625 let mut ok = d1 == input_ids[pos + 3];
5626 Self::chain_probe_note(0, ok);
5627 let mut d_prev = d1;
5628 let mut extra = 0usize;
5629 for j in 1..probe {
5630 if pos + 3 + j >= input_ids.len() {
5631 break;
5632 }
5633 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5634 extra += 1;
5635 ok = ok && dj == input_ids[pos + 3 + j];
5636 Self::chain_probe_note(j, ok);
5637 d_prev = dj;
5638 hx = hj;
5639 }
5640 m.kv.truncate_last(extra);
5641 } else {
5642 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5643 }
5644 }
5645 }
5646 hidden = h2;
5647 pos += 2;
5648 }
5649 }
5650 let o1_batch_ready = o1_sealed
5663 && o1_prefill.is_some()
5664 && mtp.is_none()
5665 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5666 && (0..self.num_layers).all(|li| {
5667 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5668 cache.o1.is_none() || cache.o1_views().is_some()
5669 });
5670 let mtp_batch_prefill = mtp.is_some()
5675 && graph_prefill
5676 && task_mask.is_none()
5677 && !dyn_prefill
5678 && !self.o1_active()
5679 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5680 if batch_k > 0
5681 && (graph_prefill || o1_batch_ready)
5682 && task_mask.is_none()
5683 && (!self.o1_active() || o1_batch_ready)
5684 && (mtp.is_none() || mtp_batch_prefill)
5685 && !dyn_prefill
5686 && pos + 1 < input_ids.len()
5687 {
5688 let hs = self.hidden_size;
5689 let chunk = batch_k;
5690 while pos < input_ids.len() {
5691 let end = (pos + chunk).min(input_ids.len());
5692 let bk = end - pos;
5693 let mut hiddens = vec![0f32; bk * hs];
5694 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5695 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5696 }
5697 let positions: Vec<usize> = (pos..end).collect();
5698 let t_chunk = std::time::Instant::now();
5699 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5700 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5701 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5702 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5703 eprintln!(
5704 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5705 if o1_batch_ready {
5706 "o1"
5707 } else if mtp_batch_prefill {
5708 "ordinary_mtp"
5709 } else {
5710 "ordinary"
5711 },
5712 bk as f64 / (ms / 1000.0)
5713 );
5714 }
5715 {
5716 use std::sync::atomic::{AtomicBool, Ordering};
5717 static SAID: AtomicBool = AtomicBool::new(false);
5718 if !SAID.swap(true, Ordering::Relaxed) {
5719 if ok_b {
5720 tracing::info!(
5721 "batched prefill: ACTIVE mode={} (k={bk})",
5722 if o1_batch_ready {
5723 "o1"
5724 } else if mtp_batch_prefill {
5725 "ordinary_mtp"
5726 } else {
5727 "ordinary"
5728 }
5729 );
5730 } else {
5731 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5732 }
5733 }
5734 }
5735 if ok_b {
5736 if mimo_spec {
5737 self.mimo_note_rows(&hiddens, pos);
5738 }
5739 if mtp_batch_prefill {
5740 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5741 if n_pairs > 0 {
5742 let rows: Vec<Vec<f32>> = (0..n_pairs)
5748 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5749 .collect();
5750 let pairs: Vec<(&[f32], u32)> = rows
5751 .iter()
5752 .enumerate()
5753 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5754 .collect();
5755 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5756 eprintln!(
5757 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5758 pos,
5759 n_pairs,
5760 pos + n_pairs - 1,
5761 );
5762 }
5763 let warm_error = if let Some(m) = mtp.as_mut() {
5764 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5765 } else {
5766 None
5767 };
5768 if let Some(err) = warm_error {
5769 self.finish_generation(&mut mtp, &mut router, true);
5774 return Err(err.to_string());
5775 }
5776 }
5777 }
5778 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5779 pos = end;
5780 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5781 self.finish_generation(&mut mtp, &mut router, true);
5786 return Err(if o1_batch_ready {
5787 "sealed O(1) batch graph failed after admission".to_string()
5788 } else {
5789 "ordinary recurrent batch graph failed after admission".to_string()
5790 });
5791 } else {
5792 break; }
5794 }
5795 }
5796 if graph_prefill
5800 && task_mask.is_none()
5801 && mtp.is_none()
5802 && !dyn_prefill
5803 && pos == 0
5804 && input_ids.len() > 1
5805 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5806 {
5807 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5808 self.graph_logits = Some(lg);
5809 hidden = vec![0.0; self.hidden_size];
5810 pos = input_ids.len();
5811 }
5812 }
5813 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5814 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5815 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5816 if mimo_spec {
5817 self.mimo_note_rows(&hidden, pos);
5818 }
5819 if let Some(m) = &mut mtp {
5820 if pos + 1 < input_ids.len() {
5821 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5827 .ok()
5828 .and_then(|v| v.parse().ok())
5829 .unwrap_or(0);
5830 if probe >= 1 && pos + 2 < input_ids.len() {
5831 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5832 let mut ok = d1 == input_ids[pos + 2];
5833 Self::chain_probe_note(0, ok);
5834 let mut d_prev = d1;
5835 let mut extra = 0usize;
5836 for j in 1..probe {
5837 if pos + 2 + j >= input_ids.len() {
5838 break;
5839 }
5840 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5841 extra += 1;
5842 ok = ok && dj == input_ids[pos + 2 + j];
5843 Self::chain_probe_note(j, ok);
5844 d_prev = dj;
5845 hx = hj;
5846 }
5847 m.kv.truncate_last(extra);
5850 } else {
5851 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5852 }
5853 }
5854 }
5855 pos += 1;
5856 }
5857 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5858 eprintln!(
5859 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5860 input_ids.len(),
5861 _tpf.elapsed().as_secs_f64() * 1000.0
5862 );
5863 }
5864 if self
5865 .graph_failed
5866 .swap(false, std::sync::atomic::Ordering::Relaxed)
5867 {
5868 self.finish_generation(&mut mtp, &mut router, true);
5873 return Err("GPU token graph failed during prefill".to_string());
5874 }
5875 if self
5878 .cancel
5879 .swap(false, std::sync::atomic::Ordering::Relaxed)
5880 {
5881 self.finish_generation(&mut mtp, &mut router, true);
5885 return Ok(GenerateResult {
5886 text: String::new(),
5887 token_ids: Vec::new(),
5888 prompt_tokens: input_ids.len(),
5889 tokens_generated: 0,
5890 finish_reason: "cancelled".to_string(),
5891 mtp_drafted: 0,
5892 mtp_accepted: 0,
5893 token_confidence: Vec::new(),
5894 traces: Vec::new(),
5895 });
5896 }
5897
5898 if !o1_sealed {
5901 match self.o1_seal_checked() {
5902 Ok(_) => {}
5903 Err(err) => {
5904 self.finish_generation(&mut mtp, &mut router, true);
5905 return Err(err);
5906 }
5907 }
5908 }
5909
5910 macro_rules! commit {
5912 ($id:expr) => {{
5913 all_ids.push($id);
5914 generated += 1;
5915 self.note_draft_id($id);
5916 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5917 finish_reason = "stop".to_string();
5918 false
5919 } else {
5920 let token_text = self.tokenizer.decode_token($id);
5921 let mut go = true;
5922 if let Some(ref mut cb) = on_token {
5923 if !cb(&token_text) {
5924 finish_reason = "cancelled".to_string();
5925 go = false;
5926 }
5927 }
5928 go
5929 }
5930 }};
5931 }
5932
5933 let mut spec_trial = SpecTrial::Spec {
5944 t0: std::time::Instant::now(),
5945 gen0: generated,
5946 rounds: 0,
5947 };
5948 let mut spec_mon = SpecMon {
5954 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5955 ..SpecMon::default()
5956 };
5957 let mut spec_watchdog_off = false;
5958 let mut spec_walls: Vec<f32> = Vec::new();
5961 let mut spec_round_end: Option<std::time::Instant> = None;
5964 if mimo_spec {
5965 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5966 if let Some(mut st) = self.mimo_mtp.take() {
5967 self.mimo_mtp_probe(&mut st, input_ids, &path);
5968 self.mimo_mtp = Some(st);
5969 }
5970 }
5971 }
5972 let mut next_pos = input_ids.len();
5974 'decode: while generated < max_tokens {
5975 if self
5976 .graph_failed
5977 .swap(false, std::sync::atomic::Ordering::Relaxed)
5978 {
5979 self.finish_generation(&mut mtp, &mut router, true);
5984 return Err("GPU token graph failed during decode".to_string());
5985 }
5986 if self
5987 .cancel
5988 .swap(false, std::sync::atomic::Ordering::Relaxed)
5989 {
5990 finish_reason = "cancelled".to_string();
5991 break 'decode;
5992 }
5993 if mimo_spec && next_pos > 0 {
5998 self.mimo_note_rows(&hidden, next_pos - 1);
6001 }
6002 let forced = self.spec_forced.take();
6003 let mut logits = match (forced, self.graph_logits.take()) {
6004 (Some(_), _) => Vec::new(),
6005 (None, Some(lg)) => lg,
6006 (None, None) => {
6007 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6008 #[cfg(feature = "gpu")]
6013 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6014 lg
6015 } else {
6016 inference::rms_norm_into(
6017 &hidden,
6018 &self.weights.final_norm,
6019 self.rms_eps,
6020 self.norm_style,
6021 &mut self.ws.n1,
6022 );
6023 self.lm_head_forward(&self.ws.n1)
6024 };
6025 #[cfg(not(feature = "gpu"))]
6026 let logits_out = {
6027 inference::rms_norm_into(
6028 &hidden,
6029 &self.weights.final_norm,
6030 self.rms_eps,
6031 self.norm_style,
6032 &mut self.ws.n1,
6033 );
6034 self.lm_head_forward(&self.ws.n1)
6035 };
6036 logits_out
6037 }
6038 };
6039 if generated
6042 == std::env::var("CMF_LOGIT_DUMP_STEP")
6043 .ok()
6044 .and_then(|v| v.parse().ok())
6045 .unwrap_or(0)
6046 {
6047 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6048 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6049 for v in hidden.iter().chain(logits.iter()) {
6050 bytes.extend_from_slice(&v.to_le_bytes());
6051 }
6052 if let Err(e) = std::fs::write(&path, &bytes) {
6053 eprintln!("logit dump: failed to write {path}: {e}");
6054 self.finish_generation(&mut mtp, &mut router, true);
6055 return Err(format!("logit dump write failed: {e}"));
6056 }
6057 }
6058 }
6059 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6063 if !logits.is_empty() {
6064 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6065 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6066 if let Err(e) =
6067 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6068 {
6069 eprintln!("logit dump: failed to write {}: {e}", path.display());
6070 }
6071 }
6072 }
6073 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6074 Some(c) => c,
6075 None => {
6076 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6077 sampler::sample_with_scratch_pool(
6078 &logits,
6079 &self.sampler_config,
6080 self.sampler_config.penalty_past(&all_ids, bounded_native),
6081 &mut self.rng,
6082 &mut self.sampler_scratch,
6083 self.pool.as_deref(),
6084 )
6085 }
6086 };
6087 if self.confidence_on {
6088 confidence.push(if logits.is_empty() {
6089 0.0
6090 } else {
6091 sampler::top1_prob_pool(
6092 self.pool.as_deref(),
6093 &mut self.sampler_scratch,
6094 &logits,
6095 t_next,
6096 calib_temp,
6097 )
6098 });
6099 }
6100 if !logits.is_empty() {
6101 attention::recycle_buf(&mut logits);
6102 }
6103 if trace_on {
6104 let skill = router.as_ref().and_then(|r| r.active_id());
6108 traces.push(TokenTrace {
6109 t: generated,
6110 token_id: t_next,
6111 confidence: confidence.last().copied().unwrap_or(0.0),
6112 active_skill: skill,
6113 recon: None,
6114 switched: false,
6115 });
6116 }
6117 if !commit!(t_next) {
6118 break 'decode;
6119 }
6120 if generated >= max_tokens {
6121 break 'decode;
6122 }
6123
6124 self.swa_trim_tails();
6127 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6128 static SAID: std::sync::Once = std::sync::Once::new();
6134 SAID.call_once(|| {
6135 tracing::warn!(
6136 "KV cache full at {} positions — evicting half; quality \
6137 will degrade. Raise CMF_MAX_SEQ.",
6138 self.kv_cache.max_seq_len,
6139 );
6140 });
6141 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6142 self.kv_cache.evict(keep);
6143 }
6144
6145 if graph_spec {
6148 match spec_trial {
6149 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6150 spec_mon.plain_ms =
6151 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6152 let keep = spec_mon.pays();
6153 tracing::info!(
6154 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6155 spec_mon.tokens,
6156 spec_mon.round_ms,
6157 spec_mon.plain_ms,
6158 if keep { "speculating" } else { "plain" }
6159 );
6160 spec_mon.fails = 0;
6161 spec_trial = SpecTrial::Decided {
6162 spec: keep,
6163 recheck_at: if keep { usize::MAX } else { generated + 128 },
6164 };
6165 }
6166 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6167 spec_mon.n = 0;
6168 spec_mon.zero_accept_rounds = 0;
6169 spec_trial = SpecTrial::Spec {
6170 t0: std::time::Instant::now(),
6171 gen0: generated,
6172 rounds: 0,
6173 };
6174 }
6175 _ => {}
6176 }
6177 spec_watchdog_off = matches!(
6178 spec_trial,
6179 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6180 );
6181 }
6182 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6184 let budget = max_tokens - generated - 1;
6185 if let Some(mut st) = self.mimo_mtp.take() {
6186 let k = st.depth.min(budget);
6187 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6188 self.mimo_mtp = Some(st);
6189 let r = match r {
6190 Ok(r) => r,
6191 Err(err) => {
6192 self.finish_generation(&mut mtp, &mut router, true);
6193 return Err(err);
6194 }
6195 };
6196 if let Some(r) = r {
6197 drafted += r.drafted;
6198 accepted += r.accepted.len();
6199 let mut stopped = false;
6200 for &id in &r.accepted {
6201 if self.confidence_on {
6202 confidence.push(0.0);
6203 }
6204 if !commit!(id) {
6205 stopped = true;
6206 break;
6207 }
6208 }
6209 if stopped {
6210 break 'decode;
6211 }
6212 next_pos += r.accepted.len() + 1;
6213 hidden = r.hidden;
6214 self.graph_logits = Some(r.logits);
6217 continue 'decode;
6218 }
6219 }
6220 }
6221 #[cfg(feature = "gpu")]
6224 if self.speculative
6225 && self.qwen4_exp.is_some()
6226 && task_mask.is_none()
6227 && self.sampler_config.temperature < 1e-6
6228 && generated + 1 < max_tokens
6229 && next_pos > 0
6230 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6231 {
6232 let r = match &mut self.qwen4_exp {
6233 Some(b) => crate::qwen4_exp::spec_round(
6234 &b.0,
6235 &b.1,
6236 &b.2,
6237 &mut b.3,
6238 next_pos,
6239 &all_ids,
6240 &self.inv_freq,
6241 self.pool.as_deref(),
6242 ),
6243 None => None,
6244 };
6245 if let Some(r) = r {
6246 drafted += r.drafted;
6247 accepted += r.accepted.len();
6248 let mut stopped = false;
6249 for &id in &r.accepted {
6250 if self.confidence_on {
6251 confidence.push(0.0);
6252 }
6253 if !commit!(id) {
6254 stopped = true;
6255 break;
6256 }
6257 }
6258 if stopped {
6259 break 'decode;
6260 }
6261 next_pos += r.accepted.len() + 1;
6262 hidden.fill(0.0);
6263 self.graph_logits = Some(r.logits);
6264 continue 'decode;
6265 }
6266 }
6267 match &mut mtp {
6268 #[cfg(feature = "gpu")]
6270 Some(m)
6271 if graph_spec
6272 && !spec_watchdog_off
6273 && generated + 1 < max_tokens
6274 && next_pos > 0 =>
6275 {
6276 let t_round = std::time::Instant::now();
6277 if spec_time_level() >= 2 {
6278 if let Some(t) = spec_round_end.take() {
6279 eprintln!(
6280 "spec-gap {:.2} ms (host between rounds)",
6281 t.elapsed().as_secs_f64() * 1e3
6282 );
6283 }
6284 }
6285 spec_stamps_begin();
6286 #[cfg(target_os = "macos")]
6291 let allocs0 =
6292 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6293 #[cfg(not(target_os = "macos"))]
6294 let allocs0 = 0u64;
6295 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6296 m,
6297 &hidden,
6298 t_next,
6299 next_pos,
6300 &mut drafted,
6301 &mut accepted,
6302 &mut all_ids,
6303 max_tokens - generated,
6304 ) {
6305 next_pos = n_pos;
6306 hidden = new_h;
6307 let level = spec_time_level();
6308 if level > 0 {
6309 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6310 let stamps = spec_stamps_take();
6311 let median = if spec_walls.len() >= 3 {
6314 let mut s = spec_walls.clone();
6315 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6316 Some(s[s.len() / 2])
6317 } else {
6318 None
6319 };
6320 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6321 #[cfg(target_os = "macos")]
6322 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6323 .load(std::sync::atomic::Ordering::Relaxed)
6324 - allocs0;
6325 #[cfg(not(target_os = "macos"))]
6326 let allocs = allocs0;
6327 eprintln!(
6328 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6329 extra.len() + 1,
6330 if allocs > 0 {
6331 format!(" [{allocs} new device buffers]")
6332 } else {
6333 String::new()
6334 },
6335 match (outlier, median) {
6336 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6337 _ => String::new(),
6338 }
6339 );
6340 if level >= 2 || outlier {
6341 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6342 eprintln!(
6343 "spec-stamps: {}| untracked {:.1}",
6344 spec_stamps_format(&stamps),
6345 wall - sum
6346 );
6347 }
6348 if spec_mon.n >= 1 {
6349 spec_walls.push(wall);
6350 }
6351 }
6352 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6356 spec_trial = Self::spec_trial_round(
6359 spec_trial,
6360 &mut spec_mon,
6361 generated + extra.len() + 1,
6362 );
6363 let mut stopped = false;
6364 for &id in &extra {
6365 if self.confidence_on {
6366 confidence.push(0.0);
6367 }
6368 if trace_on {
6381 traces.push(TokenTrace {
6382 t: generated,
6383 token_id: id,
6384 confidence: confidence.last().copied().unwrap_or(0.0),
6385 active_skill: None,
6386 recon: None,
6387 switched: false,
6388 });
6389 }
6390 if !commit!(id) {
6391 stopped = true;
6392 break;
6393 }
6394 }
6395 if stopped {
6396 break 'decode;
6397 }
6398 if spec_time_level() >= 2 {
6399 spec_round_end = Some(std::time::Instant::now());
6400 }
6401 continue 'decode;
6402 }
6403 if self
6404 .graph_failed
6405 .swap(false, std::sync::atomic::Ordering::Relaxed)
6406 {
6407 self.finish_generation(&mut mtp, &mut router, true);
6413 return Err("GPU MTP graph failed during speculative decode".to_string());
6414 }
6415 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6426 spec_mon.tokens = 0.0;
6427 spec_mon.fails = 3;
6428 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6429 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6430 next_pos += 1;
6431 continue 'decode;
6432 }
6433 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6435 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6436 drafted += 1;
6437 let emb1 = self.embed_single(t_next);
6438 let emb2 = self.embed_single(draft);
6439 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6440
6441 inference::rms_norm_into(
6442 &h1,
6443 &self.weights.final_norm,
6444 self.rms_eps,
6445 self.norm_style,
6446 &mut self.ws.n1,
6447 );
6448 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6449 let t_after = sampler::sample_with_scratch_pool(
6450 &logits1,
6451 &self.sampler_config,
6452 self.sampler_config.penalty_past(&all_ids, bounded_native),
6453 &mut self.rng,
6454 &mut self.sampler_scratch,
6455 self.pool.as_deref(),
6456 );
6457 if self.confidence_on {
6458 confidence.push(sampler::top1_prob_pool(
6459 self.pool.as_deref(),
6460 &mut self.sampler_scratch,
6461 &logits1,
6462 t_after,
6463 calib_temp,
6464 ));
6465 }
6466 attention::recycle_buf(&mut logits1);
6467 if trace_on {
6468 traces.push(TokenTrace {
6471 t: generated,
6472 token_id: t_after,
6473 confidence: confidence.last().copied().unwrap_or(0.0),
6474 active_skill: None,
6475 recon: None,
6476 switched: false,
6477 });
6478 }
6479 let stop = !commit!(t_after);
6480
6481 if t_after == draft {
6482 accepted += 1;
6483 self.commit_linear_scratch();
6484 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6485 hidden = h2;
6486 next_pos += 2;
6487 } else {
6488 for layer in &mut self.kv_cache.layers {
6490 layer.truncate_last(1);
6491 }
6492 if !stop {
6493 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6494 hidden = self.forward_layers(
6495 &self.embed_single(t_after),
6496 next_pos + 1,
6497 None,
6498 );
6499 }
6500 next_pos += 2;
6501 }
6502 if stop {
6503 break 'decode;
6504 }
6505 }
6506 _ => {
6508 #[cfg(feature = "gpu")]
6513 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6514 static SAID: std::sync::Once = std::sync::Once::new();
6515 SAID.call_once(|| {
6516 eprintln!(
6517 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6518 !self.dsv4_mtp.is_empty(),
6519 task_mask.is_none(),
6520 router.is_none(),
6521 !trace_on,
6522 self.sampler_config.temperature < 1e-6,
6523 self.sampler_config.repetition_penalty == 1.0,
6524 );
6525 });
6526 }
6527 #[cfg(feature = "gpu")]
6528 if Self::dsv4_spec_on()
6529 && self.dsv4.is_some()
6530 && !self.dsv4_mtp.is_empty()
6531 && task_mask.is_none()
6532 && router.is_none()
6533 && !trace_on
6534 && self.sampler_config.temperature < 1e-6
6535 && self.sampler_config.repetition_penalty == 1.0
6536 && generated + 1 < max_tokens
6537 && all_ids.len() >= 2
6538 && generated >= dsv4_spec_retry_at
6539 {
6540 let tip_token = all_ids[all_ids.len() - 2];
6541 let drafted0 = drafted;
6542 let round = self.dsv4_spec_step(
6543 tip_token,
6544 t_next,
6545 next_pos,
6546 max_tokens.saturating_sub(generated),
6547 &mut drafted,
6548 &mut accepted,
6549 );
6550 if drafted > drafted0 {
6551 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6552 if useful {
6553 dsv4_spec_bad = 0;
6554 } else {
6555 dsv4_spec_bad += 1;
6556 if dsv4_spec_bad >= 2 {
6557 dsv4_spec_bad = 0;
6558 dsv4_spec_retry_at = generated.saturating_add(32);
6559 tracing::info!(
6560 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6561 );
6562 }
6563 }
6564 }
6565 if let Some((extra, n_pos)) = round {
6566 next_pos = n_pos;
6567 let mut stopped = false;
6568 for &id in &extra {
6569 if self.confidence_on {
6570 confidence.push(0.0);
6571 }
6572 if !commit!(id) {
6573 stopped = true;
6574 break;
6575 }
6576 }
6577 if stopped {
6578 break 'decode;
6579 }
6580 continue 'decode;
6581 }
6582 }
6583 self.graph_want_logits = fuse_lm;
6584 let mut t_fwd = t_next;
6590 let pure_greedy = self.sampler_config.temperature < 1e-6
6591 && self.sampler_config.repetition_penalty == 1.0
6592 && self.sampler_config.presence_penalty == 0.0
6593 && self.sampler_config.suppress_tokens.is_empty();
6594 let burst_k = std::env::var("CMF_MULTISTEP")
6599 .ok()
6600 .and_then(|v| v.parse::<usize>().ok())
6601 .unwrap_or(0);
6602 if pure_greedy
6603 && burst_k >= 1
6604 && fuse_lm
6605 && task_mask.is_none()
6606 && router.is_none()
6607 && !trace_on
6608 && !self.confidence_on
6609 && std::env::var_os("CMF_FORCE_IDS").is_none()
6614 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6615 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6616 {
6617 let mut stopped = false;
6618 loop {
6619 if self
6624 .cancel
6625 .swap(false, std::sync::atomic::Ordering::Relaxed)
6626 {
6627 finish_reason = "cancelled".to_string();
6628 stopped = true;
6629 break;
6630 }
6631 let room = max_tokens.saturating_sub(generated);
6632 if room <= 2 {
6633 break;
6634 }
6635 let k = burst_k.min(room - 1);
6636 if k < 1 {
6637 break;
6638 }
6639 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6640 if self
6641 .graph_failed
6642 .swap(false, std::sync::atomic::Ordering::Relaxed)
6643 {
6644 self.finish_generation(&mut mtp, &mut router, true);
6645 return Err(
6646 "GPU token graph failed during greedy burst".to_string()
6647 );
6648 }
6649 break;
6650 };
6651 next_pos += k;
6652 for &id in &ids {
6653 if !commit!(id) {
6654 stopped = true;
6655 break;
6656 }
6657 }
6658 if stopped {
6659 break;
6660 }
6661 t_fwd = *ids.last().unwrap();
6662 }
6663 if stopped {
6664 break 'decode;
6665 }
6666 }
6667 #[cfg(target_os = "macos")]
6677 if graph_spec
6678 && spec_watchdog_off
6679 && next_pos > 0
6680 && self.mtp_graph_mode == Some(true)
6681 && crate::gpu::q1_force()
6682 {
6683 if let Some(m) = mtp.as_mut() {
6684 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6685 }
6686 }
6687 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6688 next_pos += 1;
6689 if let Some(r) = &mut router {
6692 let phi = self.dyn_phi_ema.clone();
6693 let decision = r.step(&phi, generated);
6694 if let Some(new_active) = decision {
6695 let _ = self.set_active_skill(new_active);
6696 }
6697 if trace_on {
6700 if let Some(last) = traces.last_mut() {
6701 let e = r.last_best_e();
6702 last.recon = e.is_finite().then_some(e);
6703 last.switched = decision.is_some();
6704 }
6705 }
6706 }
6707 }
6708 }
6709 }
6710
6711 let cancelled = finish_reason == "cancelled";
6712 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6716 if mimo_spec {
6717 if let Some(st) = self.mimo_mtp.as_ref() {
6718 let line = st.stats.line();
6719 tracing::info!("{line}");
6720 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6721 eprintln!("{line}");
6722 }
6723 }
6724 }
6725 self.finish_generation(&mut mtp, &mut router, cancelled);
6726
6727 let output_ids = &all_ids[input_ids.len()..];
6728 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6732 let consumed = std::mem::take(&mut all_ids);
6737 if dyn_switched {
6738 self.clear_sequence_state();
6739 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6740 self.clear_history();
6741 } else {
6742 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6743 }
6744 all_ids = consumed;
6745 let output_ids = &all_ids[input_ids.len()..];
6746 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6748 Ok(GenerateResult {
6749 text: self.tokenizer.decode(output_ids),
6750 token_ids: output_ids.to_vec(),
6751 prompt_tokens: input_ids.len(),
6752 tokens_generated: generated,
6753 finish_reason,
6754 mtp_drafted: drafted,
6755 mtp_accepted: accepted,
6756 token_confidence: confidence,
6757 traces,
6758 })
6759 }
6760
6761 fn mtp_step(
6765 &mut self,
6766 m: &mut MtpModule,
6767 hidden: &[f32],
6768 next_token: u32,
6769 position: usize,
6770 ) -> u32 {
6771 self.mtp_step_h(m, hidden, next_token, position).0
6772 }
6773
6774 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6778 use std::sync::Mutex;
6779 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6780 let mut t = T.lock().unwrap();
6781 if t.len() <= depth {
6782 t.resize(depth + 1, (0, 0));
6783 }
6784 t[depth].0 += 1;
6785 t[depth].1 += prefix_ok as u64;
6786 if depth == 0 && t[0].0 % 128 == 0 {
6787 let line: Vec<String> = t
6788 .iter()
6789 .enumerate()
6790 .map(|(d, (n, k))| {
6791 format!(
6792 "d{}={:.0}%({n})",
6793 d + 1,
6794 100.0 * *k as f64 / (*n).max(1) as f64
6795 )
6796 })
6797 .collect();
6798 eprintln!("mtp-chain: {}", line.join(" "));
6799 }
6800 }
6801
6802 fn mtp_step_hl(
6810 &mut self,
6811 m: &mut MtpModule,
6812 hidden: &[f32],
6813 next_token: u32,
6814 position: usize,
6815 ) -> (Vec<f32>, Vec<f32>) {
6816 #[cfg(target_os = "macos")]
6821 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6822 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6823 self.mtp_graph_mode = Some(true);
6824 return r;
6825 }
6826 if self.mtp_graph_mode == Some(true) {
6827 tracing::error!("mtp Metal graph failed after admission");
6828 self.clear_sequence_state();
6829 self.graph_failed
6830 .store(true, std::sync::atomic::Ordering::Relaxed);
6831 self.cancel
6832 .store(true, std::sync::atomic::Ordering::Relaxed);
6833 return (Vec::new(), Vec::new());
6834 }
6835 self.mtp_graph_mode = Some(false);
6836 }
6837 #[cfg(feature = "gpu")]
6838 if self.mtp_graph_mode != Some(false) {
6839 if !self.mtp_graph_ok(m) {
6840 if self.mtp_graph_mode == Some(true) {
6841 tracing::error!("mtp graph became unavailable after admission");
6846 self.clear_sequence_state();
6847 self.graph_failed
6848 .store(true, std::sync::atomic::Ordering::Relaxed);
6849 self.cancel
6850 .store(true, std::sync::atomic::Ordering::Relaxed);
6851 return (Vec::new(), Vec::new());
6852 }
6853 self.mtp_graph_mode = Some(false);
6854 } else {
6855 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6856 self.mtp_graph_mode = Some(true);
6857 return r;
6858 }
6859 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6860 return (Vec::new(), Vec::new());
6867 }
6868 tracing::error!("mtp graph failed or declined after admission");
6872 self.clear_sequence_state();
6873 self.graph_failed
6874 .store(true, std::sync::atomic::Ordering::Relaxed);
6875 self.cancel
6876 .store(true, std::sync::atomic::Ordering::Relaxed);
6877 return (Vec::new(), Vec::new());
6878 }
6879 }
6880 let e = self.embed_single(next_token);
6884 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6885 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6886 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6887 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6888 let mut x = vec![0.0f32; self.hidden_size];
6889 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6890
6891 let lw = &m.layer;
6893 inference::rms_norm_into(
6894 &x,
6895 &lw.input_norm,
6896 self.rms_eps,
6897 self.norm_style,
6898 &mut self.ws.n1,
6899 );
6900 let attn = match &lw.attn {
6901 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6903 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6904 AttnKind::Full {
6905 wq,
6906 wk,
6907 wv,
6908 wo,
6909 q_norm,
6910 k_norm,
6911 output_gate,
6912 softplus_gate,
6913 bias,
6914 } => {
6915 let mut cfg = self.attn_cfg(position);
6916 cfg.q_norm = q_norm.as_deref();
6917 cfg.k_norm = k_norm.as_deref();
6918 cfg.output_gate = *output_gate;
6919 cfg.softplus_gate = softplus_gate
6920 .as_ref()
6921 .map(|(gate, per_head)| (gate, *per_head));
6922 cfg.bias = bias
6923 .as_ref()
6924 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6925 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6926 }
6927 AttnKind::Linear(_)
6928 | AttnKind::LinearGdn(_)
6929 | AttnKind::ShortConv(_)
6930 | AttnKind::Bounded(_) => {
6931 unreachable!("MTP block is full attention")
6932 }
6933 };
6934 for (i, &a) in attn.iter().enumerate() {
6935 x[i] += a;
6936 }
6937 inference::rms_norm_into(
6938 &x,
6939 &lw.post_norm,
6940 self.rms_eps,
6941 self.norm_style,
6942 &mut self.ws.p1,
6943 );
6944 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6945 for (i, &f) in ffn.iter().enumerate() {
6946 x[i] += f;
6947 }
6948
6949 inference::rms_norm_into(
6950 &x,
6951 &m.final_norm,
6952 self.rms_eps,
6953 self.norm_style,
6954 &mut self.ws.n1,
6955 );
6956 let lg = self.lm_head_forward(&self.ws.n1);
6957 (lg, x)
6958 }
6959
6960 fn mtp_step_h(
6962 &mut self,
6963 m: &mut MtpModule,
6964 hidden: &[f32],
6965 next_token: u32,
6966 position: usize,
6967 ) -> (u32, Vec<f32>) {
6968 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6969 let draft = sampler::argmax(&lg);
6970 attention::recycle_buf(&mut lg);
6971 (draft, x)
6972 }
6973
6974 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6980 match trial {
6981 SpecTrial::Spec { t0, gen0, rounds } => {
6982 let rounds = rounds + 1;
6983 if mon.repeated_zero_acceptance() {
6991 tracing::info!(
6992 "speculation trial: no accepted drafts in two rounds — timing plain"
6993 );
6994 return SpecTrial::Plain {
6995 t0: std::time::Instant::now(),
6996 gen0: generated,
6997 };
6998 }
6999 if rounds >= 5 {
7000 if mon.plain_ms > 0.0 {
7001 let keep = mon.pays();
7002 mon.fails = 0;
7003 tracing::info!(
7004 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7005 mon.tokens,
7006 mon.round_ms,
7007 mon.plain_ms,
7008 if keep { "speculating" } else { "plain" }
7009 );
7010 SpecTrial::Decided {
7011 spec: keep,
7012 recheck_at: if keep { usize::MAX } else { generated + 128 },
7013 }
7014 } else if mon.pays() {
7015 mon.fails = 0;
7020 tracing::info!(
7021 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7022 mon.tokens,
7023 mon.round_ms,
7024 );
7025 SpecTrial::Decided {
7026 spec: true,
7027 recheck_at: usize::MAX,
7028 }
7029 } else {
7030 SpecTrial::Plain {
7031 t0: std::time::Instant::now(),
7032 gen0: generated,
7033 }
7034 }
7035 } else {
7036 SpecTrial::Spec { t0, gen0, rounds }
7037 }
7038 }
7039 SpecTrial::Decided { spec: true, .. } => {
7040 if mon.pays() {
7041 mon.fails = 0;
7042 trial
7043 } else {
7044 mon.fails += 1;
7045 if mon.fails >= 4 {
7046 if mon.plain_ms <= 0.0 {
7047 tracing::info!(
7051 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7052 mon.tokens,
7053 mon.round_ms,
7054 );
7055 return SpecTrial::Plain {
7056 t0: std::time::Instant::now(),
7057 gen0: generated,
7058 };
7059 }
7060 tracing::info!(
7061 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7062 mon.tokens,
7063 mon.round_ms,
7064 mon.plain_ms
7065 );
7066 SpecTrial::Decided {
7067 spec: false,
7068 recheck_at: generated + 128,
7069 }
7070 } else {
7071 trial
7072 }
7073 }
7074 }
7075 other => other,
7076 }
7077 }
7078
7079 fn mtp_kv_id(&self) -> u64 {
7082 self.graph_kv_id | (1u64 << 40)
7083 }
7084
7085 const MTP_LAYER_BASE: usize = 0;
7090
7091 #[cfg(feature = "gpu")]
7098 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7099 self.mtp_graph_mode != Some(true)
7100 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7101 }
7102
7103 #[cfg(feature = "gpu")]
7109 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7110 let mut ok = true;
7111 let mut expected = false;
7112 for li in 0..self.num_layers {
7113 if matches!(
7114 self.weights.layers[self.phys_layer(li)].attn,
7115 AttnKind::Full { .. }
7116 ) {
7117 expected = true;
7118 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7119 }
7120 }
7121 !expected || ok
7122 }
7123
7124 fn graph_gdn_layer_count(&self) -> usize {
7128 (0..self.num_layers)
7129 .filter(|&li| {
7130 matches!(
7131 &self.weights.layers[self.phys_layer(li)].attn,
7132 AttnKind::LinearGdn(_)
7133 )
7134 })
7135 .count()
7136 }
7137
7138 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7141 let e = self.embed_single(next_token);
7142 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7143 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7144 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7145 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7146 let mut x = vec![0.0f32; self.hidden_size];
7147 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7148 x
7149 }
7150
7151 #[cfg(feature = "gpu")]
7154 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7155 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7156 return false;
7157 }
7158 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7159 || !crate::gpu::enabled_here()
7160 || self.attn_softcap > 0.0
7161 || self.attention_heads_per_layer.is_some()
7162 || self.v_head_dim.is_some()
7165 {
7166 return false;
7167 }
7168 matches!(
7169 &m.layer.attn,
7170 AttnKind::Full {
7171 softplus_gate: None,
7172 ..
7173 }
7174 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7175 }
7176
7177 #[cfg(feature = "gpu")]
7181 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7182 if !self.mtp_block_graph_ok(m) {
7183 return false;
7184 }
7185 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7186 return false;
7187 };
7188 let FfnKind::Dense(d) = &m.layer.ffn else {
7189 return false;
7190 };
7191 d.segs.is_empty()
7192 && wq.graph_weight().is_some()
7193 && wk.graph_weight().is_some()
7194 && wv.graph_weight().is_some()
7195 && wo.graph_weight().is_some()
7196 && d.gate_proj.graph_weight().is_some()
7197 && d.up_proj.graph_weight().is_some()
7198 && d.down_proj.graph_weight().is_some()
7199 && self.weights.lm_head.graph_weight().is_some()
7200 }
7201
7202 #[cfg(feature = "gpu")]
7208 fn mtp_step_graph(
7209 &mut self,
7210 m: &mut MtpModule,
7211 hidden: &[f32],
7212 next_token: u32,
7213 position: usize,
7214 ) -> Option<(Vec<f32>, Vec<f32>)> {
7215 if !self.mtp_graph_ok(m) {
7216 return None;
7217 }
7218 let lw = &m.layer;
7219 let AttnKind::Full {
7220 wq,
7221 wk,
7222 wv,
7223 wo,
7224 q_norm,
7225 k_norm,
7226 output_gate,
7227 softplus_gate,
7228 bias,
7229 } = &lw.attn
7230 else {
7231 return None;
7232 };
7233 if softplus_gate.is_some() {
7234 return None;
7235 }
7236 let FfnKind::Dense(d) = &lw.ffn else {
7237 return None;
7238 };
7239 if !d.segs.is_empty() {
7240 return None; }
7242 let mut x = self.mtp_block_input(m, hidden, next_token);
7245 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7246 let (_, i, kind, rs) = t.graph_weight()?;
7247 Some(crate::gpu::GraphW {
7248 idx: i,
7249 kind,
7250 row_scale: rs,
7251 data: &[],
7252 prism: crate::gpu::GraphPrismOp::None,
7253 affine: false,
7254 })
7255 }
7256 let (model, _, _, _) = wq.graph_weight()?;
7257 let model = model.clone();
7258 let (lm_gw, lm_rows) = {
7259 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7260 let rows = if kind == 6 {
7264 self.draft_head_rows(self.weights.lm_head.rows())
7265 } else {
7266 self.weights.lm_head.rows()
7267 };
7268 (
7269 crate::gpu::GraphW {
7270 idx: i,
7271 kind,
7272 row_scale: rs,
7273 data: &[],
7274 prism: crate::gpu::GraphPrismOp::None,
7275 affine: false,
7276 },
7277 rows,
7278 )
7279 };
7280 let layer = crate::gpu::GraphLayer {
7281 input_norm: &lw.input_norm,
7282 attn: crate::gpu::GraphAttn::Full {
7283 wq: gw(wq)?,
7284 wk: gw(wk)?,
7285 wv: gw(wv)?,
7286 wo: gw(wo)?,
7287 q_norm: q_norm.as_deref(),
7288 k_norm: k_norm.as_deref(),
7289 late_qk_norm: self.qk_norm_after_rope,
7290 bias: bias
7291 .as_ref()
7292 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7293 output_gate: *output_gate,
7294 cpu_k: m.kv.k_heads(),
7295 cpu_v: m.kv.v_heads(),
7296 cpu_base: m.kv.base(),
7297 geom: None,
7298 head_gate: None,
7299 },
7300 post_norm: &lw.post_norm,
7301 attn_out_norm: None,
7302 ffn_out_norm: None,
7303 layer_scale: None,
7304 ffn: crate::gpu::GraphFfn::Dense {
7305 gate: gw(&d.gate_proj)?,
7306 up: gw(&d.up_proj)?,
7307 down: gw(&d.down_proj)?,
7308 act: d.act.graph_act()?,
7309 },
7310 };
7311 let nh = self.num_heads;
7312 let (nkv, hd, rd) = self.layer_geom(0);
7313 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7314 let mut logits = Vec::new();
7315 let ok = crate::gpu::forward_token_graph(
7316 &model,
7317 self.mtp_kv_id(),
7318 std::slice::from_ref(&layer),
7319 &[None],
7320 self.o1_epoch,
7321 &self.inv_freq,
7322 &mut x,
7323 nh,
7324 nkv,
7325 hd,
7326 self.attn_scale,
7327 rd,
7328 self.hidden_size,
7329 d.gate_proj.rows(),
7333 position,
7334 self.kv_cache.max_seq_len,
7335 gemma,
7336 self.rms_eps as f32,
7337 Some((&lm_gw, lm_rows)),
7338 &m.final_norm,
7339 &mut logits,
7340 &[],
7341 1,
7342 None,
7343 None,
7344 None,
7345 Self::MTP_LAYER_BASE,
7346 true,
7347 );
7348 match ok {
7349 crate::gpu::TokenGraphOutcome::Completed => {}
7350 crate::gpu::TokenGraphOutcome::Declined => return None,
7351 crate::gpu::TokenGraphOutcome::Failed => {
7352 self.clear_sequence_state();
7356 self.graph_failed
7357 .store(true, std::sync::atomic::Ordering::Relaxed);
7358 self.cancel
7359 .store(true, std::sync::atomic::Ordering::Relaxed);
7360 return None;
7361 }
7362 }
7363 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7367 Some((logits, x))
7368 }
7369
7370 #[cfg(feature = "gpu")]
7378 fn mtp_warm_graph(
7379 &mut self,
7380 m: &mut MtpModule,
7381 pairs: &[(&[f32], u32)],
7382 first_pos: usize,
7383 ) -> crate::gpu::BatchGraphOutcome {
7384 if pairs.is_empty() {
7385 return crate::gpu::BatchGraphOutcome::Completed;
7386 }
7387 if !self.mtp_block_graph_ok(m) {
7388 return crate::gpu::BatchGraphOutcome::Declined;
7389 }
7390 let hs = self.hidden_size;
7391 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7394 for (h, t) in pairs {
7395 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7396 }
7397 let lw = &m.layer;
7398 let AttnKind::Full {
7399 wq,
7400 wk,
7401 wv,
7402 wo,
7403 q_norm,
7404 k_norm,
7405 output_gate,
7406 bias,
7407 ..
7408 } = &lw.attn
7409 else {
7410 return crate::gpu::BatchGraphOutcome::Declined;
7411 };
7412 let FfnKind::Dense(d) = &lw.ffn else {
7413 return crate::gpu::BatchGraphOutcome::Declined;
7414 };
7415 if !d.segs.is_empty() {
7416 return crate::gpu::BatchGraphOutcome::Declined; }
7418 let (
7421 AttnKind::Full {
7422 softplus_gate: None,
7423 ..
7424 },
7425 Some(gact),
7426 ) = (&lw.attn, d.act.graph_act())
7427 else {
7428 return crate::gpu::BatchGraphOutcome::Declined;
7429 };
7430 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7431 let (_, i, kind, rs) = t.graph_weight()?;
7432 Some(crate::gpu::GraphW {
7433 idx: i,
7434 kind,
7435 row_scale: rs,
7436 data: &[],
7437 prism: crate::gpu::GraphPrismOp::None,
7438 affine: false,
7439 })
7440 }
7441 let Some((model, _, _, _)) = wq.graph_weight() else {
7442 return crate::gpu::BatchGraphOutcome::Declined;
7443 };
7444 let model = model.clone();
7445 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7446 gw(wq),
7447 gw(wk),
7448 gw(wv),
7449 gw(wo),
7450 gw(&d.gate_proj),
7451 gw(&d.up_proj),
7452 gw(&d.down_proj),
7453 ) else {
7454 return crate::gpu::BatchGraphOutcome::Declined;
7455 };
7456 let layer = crate::gpu::GraphLayer {
7457 input_norm: &lw.input_norm,
7458 attn: crate::gpu::GraphAttn::Full {
7459 wq: gwq,
7460 wk: gwk,
7461 wv: gwv,
7462 wo: gwo,
7463 q_norm: q_norm.as_deref(),
7464 k_norm: k_norm.as_deref(),
7465 late_qk_norm: self.qk_norm_after_rope,
7466 bias: bias
7467 .as_ref()
7468 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7469 output_gate: *output_gate,
7470 cpu_k: m.kv.k_heads(),
7471 cpu_v: m.kv.v_heads(),
7472 cpu_base: m.kv.base(),
7473 geom: None,
7474 head_gate: None,
7475 },
7476 post_norm: &lw.post_norm,
7477 attn_out_norm: None,
7478 ffn_out_norm: None,
7479 layer_scale: None,
7480 ffn: crate::gpu::GraphFfn::Dense {
7481 gate: gg,
7482 up: gu,
7483 down: gd,
7484 act: gact,
7485 },
7486 };
7487 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7488 let nh = self.num_heads;
7489 let (nkv, hd, rd) = self.layer_geom(0);
7490 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7491 crate::gpu::forward_batch_graph(
7492 &model,
7493 self.mtp_kv_id(),
7494 std::slice::from_ref(&layer),
7495 &self.inv_freq,
7496 &mut hiddens,
7497 nh,
7498 nkv,
7499 hd,
7500 rd,
7501 hs,
7502 d.gate_proj.rows(),
7505 &positions,
7506 self.kv_cache.max_seq_len,
7507 gemma,
7508 self.rms_eps as f32,
7509 self.attn_scale,
7510 pairs.len(),
7511 &[],
7512 0,
7513 None,
7514 None,
7515 )
7516 }
7517
7518 #[cfg(feature = "gpu")]
7525 fn mtp_warm_graph_fallback(
7526 &mut self,
7527 m: &mut MtpModule,
7528 pairs: &[(&[f32], u32)],
7529 first_pos: usize,
7530 ) -> bool {
7531 if pairs.is_empty() {
7532 return true;
7533 }
7534 let graphable = self.mtp_block_graph_ok(m);
7535 if !graphable {
7536 if self.mtp_graph_mode == Some(true) {
7540 return false;
7541 }
7542 self.mtp_graph_mode = Some(false);
7543 for (j, (h, t)) in pairs.iter().enumerate() {
7544 self.mtp_warm(m, h, *t, first_pos + j);
7545 }
7546 return true;
7547 }
7548
7549 for (j, (h, t)) in pairs.iter().enumerate() {
7554 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7555 return false;
7556 }
7557 }
7558 self.mtp_graph_mode = Some(true);
7559 true
7560 }
7561
7562 #[cfg(feature = "gpu")]
7567 fn mtp_warm_prefill_pairs(
7568 &mut self,
7569 m: &mut MtpModule,
7570 pairs: &[(&[f32], u32)],
7571 first_pos: usize,
7572 ) -> Result<(), &'static str> {
7573 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7578 if self.mtp_graph_mode == Some(true) {
7579 return Err("MTP token graph became unavailable after admission");
7580 }
7581 self.mtp_graph_mode = Some(false);
7582 for (j, (h, t)) in pairs.iter().enumerate() {
7583 self.mtp_warm(m, h, *t, first_pos + j);
7584 }
7585 return Ok(());
7586 }
7587 match self.mtp_warm_graph(m, pairs, first_pos) {
7588 crate::gpu::BatchGraphOutcome::Completed => {
7589 if !pairs.is_empty() {
7590 self.mtp_graph_mode = Some(true);
7591 }
7592 Ok(())
7593 }
7594 crate::gpu::BatchGraphOutcome::Declined => {
7595 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7596 Ok(())
7597 } else {
7598 Err("MTP warm-up fallback failed after device admission")
7599 }
7600 }
7601 crate::gpu::BatchGraphOutcome::Failed => {
7602 Err("MTP warm batch graph failed after admission")
7603 }
7604 }
7605 }
7606
7607 #[cfg(not(feature = "gpu"))]
7608 fn mtp_warm_prefill_pairs(
7609 &mut self,
7610 m: &mut MtpModule,
7611 pairs: &[(&[f32], u32)],
7612 first_pos: usize,
7613 ) -> Result<(), &'static str> {
7614 for (j, (h, t)) in pairs.iter().enumerate() {
7615 self.mtp_warm(m, h, *t, first_pos + j);
7616 }
7617 Ok(())
7618 }
7619
7620 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7624 let e = self.embed_single(next_token);
7625 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7626 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7627 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7628 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7629 let mut x = vec![0.0f32; self.hidden_size];
7630 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7631 inference::rms_norm_into(
7632 &x,
7633 &m.layer.input_norm,
7634 self.rms_eps,
7635 self.norm_style,
7636 &mut self.ws.n1,
7637 );
7638 let attn = match &m.layer.attn {
7639 AttnKind::Full {
7640 wq,
7641 wk,
7642 wv,
7643 wo,
7644 q_norm,
7645 k_norm,
7646 output_gate,
7647 softplus_gate,
7648 bias,
7649 } => {
7650 let mut cfg = self.attn_cfg(position);
7651 cfg.q_norm = q_norm.as_deref();
7652 cfg.k_norm = k_norm.as_deref();
7653 cfg.output_gate = *output_gate;
7654 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7655 cfg.bias = bias
7656 .as_ref()
7657 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7658 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7659 }
7660 _ => return,
7661 };
7662 let _ = attn;
7663 }
7664
7665 #[cfg(feature = "gpu")]
7672 #[allow(clippy::too_many_arguments)]
7673 fn graph_spec_step(
7674 &mut self,
7675 m: &mut MtpModule,
7676 hidden: &[f32],
7677 t_next: u32,
7678 next_pos: usize,
7679 drafted: &mut usize,
7680 accepted: &mut usize,
7681 all_ids: &mut Vec<u32>,
7685 room: usize,
7690 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7691 #[cfg(target_os = "macos")]
7702 let metal_native = crate::gpu::q1_force();
7703 #[cfg(not(target_os = "macos"))]
7704 let metal_native = false;
7705 #[cfg(feature = "gpu")]
7706 let k_default = if metal_native {
7707 7
7710 } else if crate::gpu_wgpu::verify_i8_on() {
7711 5
7712 } else {
7713 4
7714 };
7715 #[cfg(not(feature = "gpu"))]
7716 let k_default = 4;
7717 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7718 .ok()
7719 .and_then(|v| v.parse().ok())
7720 .filter(|&v| (1..=8).contains(&v));
7721 let (k_start, k_max) = if metal_native {
7726 (7, 7)
7727 } else {
7728 (3, k_default.max(5))
7729 };
7730 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7731 let k_spec = k_full.min(room).max(1);
7732 let k_capped = k_spec < k_full;
7735 if next_pos == 0 {
7736 return None;
7737 }
7738 let t_round = std::time::Instant::now();
7739 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7755 let sub0 = subs();
7756 let cfg = self.sampler_config.clone();
7761 let penalized = !(cfg.repetition_penalty == 1.0
7762 && cfg.presence_penalty == 0.0
7763 && cfg.suppress_tokens.is_empty());
7764 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7769 let sampling = cfg.temperature >= 1e-6;
7770 let sparse = sampling && sampler::sparse_ok(&cfg);
7776 let base_len = all_ids.len();
7777 if sampling && !sparse && self.spec_q.len() < k_spec {
7778 self.spec_q.resize_with(k_spec, Vec::new);
7779 }
7780 if sparse && self.spec_qs.len() < k_spec {
7781 self.spec_qs.resize_with(k_spec, Vec::new);
7782 }
7783 let mut drafts = Vec::with_capacity(k_spec);
7788 let mut hx = hidden.to_vec();
7789 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7792 spec_stamp("pro");
7793 #[cfg(target_os = "macos")]
7799 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7800 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7801 Ok(ids) => {
7802 self.mtp_graph_mode = Some(true);
7803 drafts = ids;
7804 }
7805 Err(true) => {
7806 tracing::error!("mtp Metal draft chain failed after commit");
7807 self.clear_sequence_state();
7808 self.graph_failed
7809 .store(true, std::sync::atomic::Ordering::Relaxed);
7810 self.cancel
7811 .store(true, std::sync::atomic::Ordering::Relaxed);
7812 return None;
7813 }
7814 Err(false) => {}
7815 }
7816 }
7817 for j in drafts.len()..k_spec {
7818 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7819 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7820 if spec_dbg {
7821 let saved = self.mtp_graph_mode;
7822 self.mtp_graph_mode = Some(false);
7823 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7824 self.mtp_graph_mode = saved;
7825 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7826 return None;
7827 }
7828 m.kv.truncate_last(1);
7829 dbg_ref = Some(r);
7830 }
7831 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7832 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7833 return None;
7834 }
7835 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7836 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7837 let dl = lg
7838 .iter()
7839 .zip(&lg_cpu)
7840 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7841 let dh = hj
7842 .iter()
7843 .zip(&h_cpu)
7844 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7845 eprintln!(
7846 "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 {}",
7847 next_pos - 1 + j,
7848 sampler::argmax(&lg_cpu),
7849 sampler::argmax(&lg),
7850 n(&h_cpu),
7851 n(&hj),
7852 m.kv.seq_len
7853 );
7854 }
7855 let dj = if sparse {
7856 let mut q = std::mem::take(&mut self.spec_qs[j]);
7857 let ok = sampler::sparse_distribution_into(
7858 &lg,
7859 &cfg,
7860 all_ids,
7861 &mut self.sampler_scratch,
7862 self.pool.as_deref(),
7863 &mut q,
7864 );
7865 let d = if ok {
7866 sampler::draw_sparse(&q, &mut self.rng)
7867 } else {
7868 let t = sampler::argmax(&lg);
7870 q.clear();
7871 q.push((t, 1.0));
7872 t
7873 };
7874 self.spec_qs[j] = q;
7875 all_ids.push(d);
7876 d
7877 } else if sampling {
7878 let mut q = std::mem::take(&mut self.spec_q[j]);
7879 sampler::distribution_into(
7880 &lg,
7881 &cfg,
7882 all_ids,
7883 &mut self.sampler_scratch,
7884 self.pool.as_deref(),
7885 &mut q,
7886 );
7887 let d = sampler::draw(&q, &mut self.rng);
7888 self.spec_q[j] = q;
7889 all_ids.push(d); d
7891 } else if greedy_pen {
7892 let d = sampler::argmax_penalized(
7893 &lg,
7894 &cfg,
7895 all_ids,
7896 &mut self.sampler_scratch,
7897 self.pool.as_deref(),
7898 );
7899 all_ids.push(d);
7900 d
7901 } else {
7902 sampler::argmax(&lg)
7903 };
7904 attention::recycle_buf(&mut lg);
7905 drafts.push(dj);
7906 hx = hj;
7907 spec_stamp("d.pick");
7908 }
7909 all_ids.truncate(base_len);
7910 *drafted += k_spec;
7911 let t_draft = t_round.elapsed();
7912 let sub_draft = subs();
7913 let b = k_spec + 1;
7916 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7917 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7918 let e = self.embed_single(t);
7919 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7920 }
7921 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7922 spec_stamp("v.emb");
7923 let (lm_gw, lm_rows) = {
7924 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7925 (
7926 crate::gpu::GraphW {
7927 idx: i,
7928 kind,
7929 row_scale: rs,
7930 data: &[],
7931 prism: crate::gpu::GraphPrismOp::None,
7932 affine: false,
7933 },
7934 self.weights.lm_head.rows(),
7935 )
7936 };
7937 let mut logits = Vec::new();
7938 let final_norm = self.weights.final_norm.clone();
7939 #[cfg(target_os = "macos")]
7948 let greedy_dev = metal_native
7949 && !sampling
7950 && !greedy_pen
7951 && !self.confidence_on
7952 && self.final_softcap.is_none()
7953 && self.vocab_size == lm_rows
7959 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7960 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7961 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7962 #[cfg(not(target_os = "macos"))]
7963 let greedy_dev = false;
7964 let mut dev_ids: Vec<u32> = Vec::new();
7965 #[cfg(target_os = "macos")]
7966 let verify_outcome = if metal_native {
7967 let lm = self.weights.lm_head.q1_parts()?;
7968 let n_score = self.vocab_size.min(lm_rows);
7969 self.try_batch_graph_metal(
7970 &mut hiddens,
7971 &positions,
7972 b,
7973 Some((lm, &final_norm, &mut logits)),
7974 if greedy_dev {
7975 Some((n_score, &mut dev_ids))
7976 } else {
7977 None
7978 },
7979 )
7980 } else {
7981 self.try_batch_graph_wgpu(
7982 &mut hiddens,
7983 &positions,
7984 b,
7985 Some(crate::gpu::SpecTail {
7986 lm: lm_gw,
7987 lm_rows,
7988 final_norm: &final_norm,
7989 logits_out: &mut logits,
7990 pick: None,
7991 }),
7992 )
7993 };
7994 #[cfg(not(target_os = "macos"))]
7995 let verify_outcome = self.try_batch_graph_wgpu(
7996 &mut hiddens,
7997 &positions,
7998 b,
7999 Some(crate::gpu::SpecTail {
8000 lm: lm_gw,
8001 lm_rows,
8002 final_norm: &final_norm,
8003 logits_out: &mut logits,
8004 pick: None,
8005 }),
8006 );
8007 match verify_outcome {
8008 crate::gpu::BatchGraphOutcome::Completed => {}
8009 crate::gpu::BatchGraphOutcome::Declined => {
8010 m.kv.truncate_last(k_spec);
8014 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8015 self.clear_sequence_state();
8016 self.graph_failed
8017 .store(true, std::sync::atomic::Ordering::Relaxed);
8018 self.cancel
8019 .store(true, std::sync::atomic::Ordering::Relaxed);
8020 tracing::error!("MTP graph mirror rewind failed after verify decline");
8021 }
8022 return None;
8023 }
8024 crate::gpu::BatchGraphOutcome::Failed => {
8025 self.clear_sequence_state();
8029 self.graph_failed
8030 .store(true, std::sync::atomic::Ordering::Relaxed);
8031 self.cancel
8032 .store(true, std::sync::atomic::Ordering::Relaxed);
8033 tracing::error!("MTP verify batch graph failed after admission");
8034 return None;
8035 }
8036 }
8037 #[cfg(target_os = "macos")]
8043 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8044 let snap: Vec<Vec<f32>> = self
8045 .kv_cache
8046 .layers
8047 .iter()
8048 .map(|l| l.linear_state.clone())
8049 .collect();
8050 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8051 let toks: Vec<u32> = std::iter::once(t_next)
8052 .chain(drafts.iter().copied())
8053 .collect();
8054 let want_save = self.graph_want_logits;
8055 self.graph_want_logits = false;
8056 for (i, &t) in toks.iter().enumerate() {
8057 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8058 let _ = self.graph_logits.take();
8059 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8063 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8064 }
8065 let ref_lg = self.logits_from_hidden(&hi);
8066 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8067 let ra = sampler::argmax(&ref_lg);
8068 let va = sampler::argmax(row);
8069 let mut md = 0f32;
8070 let mut rms = 0f64;
8071 for j in 0..lm_rows.min(ref_lg.len()) {
8072 let d = (ref_lg[j] - row[j]).abs();
8073 md = md.max(d);
8074 rms += (d as f64) * (d as f64);
8075 }
8076 let mut hd = 0f32;
8077 for j in 0..self.hidden_size {
8078 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8079 }
8080 eprintln!(
8081 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8082 next_pos + i,
8083 if ra == va { "OK" } else { "MISMATCH" },
8084 (rms / lm_rows as f64).sqrt()
8085 );
8086 }
8087 self.graph_want_logits = want_save;
8088 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8091 if l.linear_state.len() == st.len() {
8092 l.linear_state.copy_from_slice(&st);
8093 } else {
8094 l.linear_state = st;
8095 }
8096 }
8097 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8098 let extra = l.seq_len.saturating_sub(n0);
8099 if extra > 0 {
8100 l.truncate_last(extra);
8101 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8102 }
8103 }
8104 }
8105 let t_verify = t_round.elapsed();
8106 let sub_verify = subs();
8107 let mut a = 0usize;
8112 let mut forced: Option<u32> = None;
8113 let ids: Vec<u32> = if sparse {
8114 let mut p = std::mem::take(&mut self.spec_ps);
8115 let mut res = std::mem::take(&mut self.spec_ress);
8116 while a < k_spec {
8117 let ok = sampler::sparse_distribution_into(
8118 &logits[a * lm_rows..(a + 1) * lm_rows],
8119 &cfg,
8120 all_ids,
8121 &mut self.sampler_scratch,
8122 self.pool.as_deref(),
8123 &mut p,
8124 );
8125 if !ok {
8126 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8127 p.clear();
8128 p.push((t, 1.0));
8129 }
8130 match sampler::spec_accept_or_correct_sparse(
8131 &p,
8132 &self.spec_qs[a],
8133 drafts[a],
8134 &mut self.rng,
8135 &mut res,
8136 ) {
8137 None => {
8138 all_ids.push(drafts[a]);
8139 a += 1;
8140 }
8141 Some(c) => {
8142 forced = Some(c);
8143 break;
8144 }
8145 }
8146 }
8147 all_ids.truncate(base_len);
8148 self.spec_ps = p;
8149 self.spec_ress = res;
8150 drafts.clone()
8151 } else if sampling {
8152 let mut p = std::mem::take(&mut self.spec_p);
8153 let mut res = std::mem::take(&mut self.spec_res);
8154 while a < k_spec {
8155 sampler::distribution_into(
8156 &logits[a * lm_rows..(a + 1) * lm_rows],
8157 &cfg,
8158 all_ids,
8159 &mut self.sampler_scratch,
8160 self.pool.as_deref(),
8161 &mut p,
8162 );
8163 match sampler::spec_accept_or_correct(
8164 &p,
8165 &self.spec_q[a],
8166 drafts[a],
8167 &mut self.rng,
8168 &mut res,
8169 self.pool.as_deref(),
8170 ) {
8171 None => {
8172 all_ids.push(drafts[a]);
8173 a += 1;
8174 }
8175 Some(c) => {
8176 forced = Some(c);
8177 break;
8178 }
8179 }
8180 }
8181 all_ids.truncate(base_len);
8182 self.spec_p = p;
8183 self.spec_res = res;
8184 drafts.clone()
8186 } else if greedy_pen {
8187 let mut ids: Vec<u32> = Vec::with_capacity(b);
8191 for i in 0..b {
8192 let t = sampler::argmax_penalized(
8193 &logits[i * lm_rows..(i + 1) * lm_rows],
8194 &cfg,
8195 all_ids,
8196 &mut self.sampler_scratch,
8197 self.pool.as_deref(),
8198 );
8199 ids.push(t);
8200 if i < k_spec && t == drafts[i] {
8201 all_ids.push(t);
8202 } else {
8203 break;
8204 }
8205 }
8206 all_ids.truncate(base_len);
8207 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8208 a += 1;
8209 }
8210 ids
8213 } else if greedy_dev && dev_ids.len() == b {
8214 let ids = std::mem::take(&mut dev_ids);
8215 while a < k_spec && ids[a] == drafts[a] {
8216 a += 1;
8217 }
8218 ids
8219 } else {
8220 if logits.len() < b * lm_rows {
8221 self.clear_sequence_state();
8224 self.graph_failed
8225 .store(true, std::sync::atomic::Ordering::Relaxed);
8226 self.cancel
8227 .store(true, std::sync::atomic::Ordering::Relaxed);
8228 tracing::error!("Metal verify returned neither logits nor argmax ids");
8229 return None;
8230 }
8231 let ids: Vec<u32> = (0..b)
8232 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8233 .collect();
8234 while a < k_spec && ids[a] == drafts[a] {
8235 a += 1;
8236 }
8237 ids
8238 };
8239 spec_stamp("acc");
8240 if spec_dbg {
8241 eprintln!(
8242 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8243 drafts, ids
8244 );
8245 }
8246 #[cfg(target_os = "macos")]
8250 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8251 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8252 {
8253 let snap: Vec<Vec<f32>> = self
8254 .kv_cache
8255 .layers
8256 .iter()
8257 .map(|l| l.linear_state.clone())
8258 .collect();
8259 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8260 let toks: Vec<u32> = std::iter::once(t_next)
8261 .chain(drafts.iter().copied())
8262 .collect();
8263 let want_save = self.graph_want_logits;
8264 self.graph_want_logits = false;
8265 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8266 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8267 let _ = self.graph_logits.take();
8268 }
8269 self.graph_want_logits = want_save;
8270 let plain_states: Vec<Vec<f32>> = self
8271 .kv_cache
8272 .layers
8273 .iter()
8274 .map(|l| l.linear_state.clone())
8275 .collect();
8276 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8277 let mut rows = Vec::new();
8278 for (li, (l, n0)) in self
8279 .kv_cache
8280 .layers
8281 .iter_mut()
8282 .zip(attn_lens.iter())
8283 .enumerate()
8284 {
8285 let extra = l.seq_len.saturating_sub(*n0);
8286 if extra > 0 {
8287 let mut kk = Vec::new();
8288 let mut vv = Vec::new();
8289 for g in 0..nkv {
8290 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8291 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8292 }
8293 rows.push((li, kk, vv));
8294 l.truncate_last(extra);
8295 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8296 }
8297 }
8298 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8299 if l.linear_state.len() == st.len() {
8300 l.linear_state.copy_from_slice(&st);
8301 } else {
8302 l.linear_state = st;
8303 }
8304 }
8305 Some((plain_states, rows))
8306 } else {
8307 None
8308 };
8309 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8310 #[cfg(target_os = "macos")]
8319 let mut warm_pending: Option<MetalWarmPending> = None;
8320 #[cfg(target_os = "macos")]
8321 if metal_native {
8322 m.kv.truncate_last(k_spec.saturating_sub(1));
8323 if self.mtp_graph_mode == Some(true) {
8324 crate::gpu_metal::kv_mirror_set_stored(
8327 self.mtp_kv_id(),
8328 Self::MTP_LAYER_BASE,
8329 m.kv.seq_len,
8330 );
8331 if !warm_off && a > 0 {
8332 let pairs: Vec<(&[f32], u32)> = (0..a)
8333 .map(|j| {
8334 (
8335 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8336 ids[j],
8337 )
8338 })
8339 .collect();
8340 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8341 }
8342 }
8343 spec_stamp("c.wsub");
8344 }
8345 #[cfg(target_os = "macos")]
8347 if metal_native {
8348 if !self.metal_verify_commit(a) {
8351 self.clear_sequence_state();
8352 self.graph_failed
8353 .store(true, std::sync::atomic::Ordering::Relaxed);
8354 self.cancel
8355 .store(true, std::sync::atomic::Ordering::Relaxed);
8356 tracing::error!("Metal verify state/KV handoff failed after admission");
8357 return None;
8358 }
8359 if let Some((plain_states, rows)) = commit_ref {
8360 crate::gpu_metal::queue_fence();
8361 let _ = crate::gpu_metal::wait_replay();
8364 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8365 let mut worst_s = 0f32;
8366 let mut worst_li = 0usize;
8367 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8368 if l.linear_state.len() != ps.len() || ps.is_empty() {
8369 continue;
8370 }
8371 let d = l
8372 .linear_state
8373 .iter()
8374 .zip(ps)
8375 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8376 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8377 let rel = d / n.max(1e-6);
8378 if rel > worst_s {
8379 worst_s = rel;
8380 worst_li = li;
8381 }
8382 }
8383 let mut worst_k = 0f32;
8384 for (li, kk, vv) in &rows {
8385 let l = &self.kv_cache.layers[*li];
8386 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8387 let mut ck = Vec::new();
8388 let mut cv = Vec::new();
8389 for g in 0..nkv {
8390 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8391 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8392 }
8393 if ck.len() == kk.len() {
8394 let dk = ck
8395 .iter()
8396 .zip(kk)
8397 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8398 let dv = cv
8399 .iter()
8400 .zip(vv)
8401 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8402 worst_k = worst_k.max(dk).max(dv);
8403 } else {
8404 eprintln!(
8405 "commit-check L{li}: kv row count mismatch {} vs {}",
8406 ck.len(),
8407 kk.len()
8408 );
8409 }
8410 }
8411 eprintln!(
8412 "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}"
8413 );
8414 }
8415 }
8416 if !metal_native && a + 1 < b {
8417 let expected_gdn_layers = self.graph_gdn_layer_count();
8418 if expected_gdn_layers > 0
8419 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8420 {
8421 self.clear_sequence_state();
8422 self.graph_failed
8423 .store(true, std::sync::atomic::Ordering::Relaxed);
8424 self.cancel
8425 .store(true, std::sync::atomic::Ordering::Relaxed);
8426 tracing::error!("GDN speculative restore failed after verify");
8427 return None;
8428 }
8429 }
8430 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8431 self.clear_sequence_state();
8436 self.graph_failed
8437 .store(true, std::sync::atomic::Ordering::Relaxed);
8438 self.cancel
8439 .store(true, std::sync::atomic::Ordering::Relaxed);
8440 tracing::error!("trunk graph KV rewind failed after speculative verify");
8441 return None;
8442 }
8443 *accepted += a;
8444 if !metal_native {
8455 m.kv.truncate_last(k_spec.saturating_sub(1));
8457 }
8458 spec_stamp("c.trunc");
8459 if !metal_native
8460 && self.mtp_graph_mode == Some(true)
8461 && !self.rewind_mtp_graph_mirror(next_pos)
8462 {
8463 self.clear_sequence_state();
8467 self.graph_failed
8468 .store(true, std::sync::atomic::Ordering::Relaxed);
8469 self.cancel
8470 .store(true, std::sync::atomic::Ordering::Relaxed);
8471 tracing::error!("MTP graph mirror rewind failed after verify commit");
8472 return None;
8473 }
8474 if !warm_off && a > 0 {
8475 let mut warmed = false;
8478 #[cfg(target_os = "macos")]
8479 if metal_native && self.mtp_graph_mode == Some(true) {
8480 warmed = match warm_pending.take() {
8484 Some(p) => self.mtp_warm_batch_finish(m, p),
8485 None => false,
8486 };
8487 if !warmed {
8488 warmed = true;
8489 for j in 0..a {
8490 let row =
8491 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8492 if self
8493 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8494 .is_none()
8495 {
8496 warmed = false;
8497 break;
8498 }
8499 }
8500 }
8501 }
8502 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8503 let rows: Vec<Vec<f32>> = (0..a)
8504 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8505 .collect();
8506 let pairs: Vec<(&[f32], u32)> = rows
8507 .iter()
8508 .zip(ids.iter())
8509 .map(|(r, &t)| (r.as_slice(), t))
8510 .collect();
8511 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8512 Ok(()) => warmed = true,
8513 Err(err) => {
8514 tracing::error!("{err}");
8520 self.clear_sequence_state();
8521 self.graph_failed
8522 .store(true, std::sync::atomic::Ordering::Relaxed);
8523 self.cancel
8524 .store(true, std::sync::atomic::Ordering::Relaxed);
8525 return None;
8526 }
8527 }
8528 }
8529 if !warmed {
8530 for j in 0..a {
8531 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8532 let row = row.to_vec();
8533 self.mtp_warm(m, &row, ids[j], next_pos + j);
8534 }
8535 }
8536 }
8537 spec_stamp("c.warm");
8541 if let Some(c) = forced {
8542 self.spec_forced = Some(c);
8543 self.graph_logits = None;
8544 } else if greedy_dev && logits.is_empty() {
8545 self.spec_forced = Some(ids[a]);
8548 self.graph_logits = None;
8549 } else {
8550 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8551 row.resize(self.vocab_size, 0.0);
8552 if let Some(c) = self.final_softcap {
8553 for l in row.iter_mut() {
8554 *l = c * (*l / c).tanh();
8555 }
8556 }
8557 self.graph_logits = Some(row);
8558 }
8559 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8560 spec_stamp("c.row");
8561 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8567 let end = subs();
8568 eprintln!(
8569 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8570 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8571 t_draft.as_secs_f64() * 1e3,
8572 sub_draft - sub0,
8573 (t_verify - t_draft).as_secs_f64() * 1e3,
8574 sub_verify - sub_draft,
8575 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8576 end - sub_verify,
8577 self.draft_full_streak,
8578 );
8579 }
8580 if k_env.is_none() && !metal_native && !k_capped {
8585 let f = a as f32 / k_spec.max(1) as f32;
8589 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8590 let mut k_next = k_spec;
8591 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8592 k_next = k_spec + 1;
8593 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8594 k_next = k_spec - 1;
8595 }
8596 if k_next != k_spec {
8597 self.spec_acc_ewma = 0.6;
8598 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8599 eprintln!("spec-k: {k_spec} → {k_next}");
8600 }
8601 }
8602 self.spec_k_adapt = Some(k_next);
8603 }
8604 spec_stamp("end");
8605 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8606 }
8607
8608 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8617 if !self.pair_supported() {
8618 return (0.0, 0.0);
8619 }
8620 if self.gdn_cfg.is_some()
8628 && self.graph_prefill_preferred()
8629 && self.automatic_gpu_prefix().is_some()
8630 {
8631 tracing::info!(
8632 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8633 );
8634 return (0.0, 0.0);
8635 }
8636 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8643 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8644 let emb1 = self.embed_single(1);
8645 let emb2 = self.embed_single(2);
8646 let pos = self.kv_cache.seq_len();
8647
8648 let t0 = std::time::Instant::now();
8649 for _ in 0..iters {
8650 let _ = self.forward_layers(&emb1, pos, None);
8651 let _ = self.forward_layers(&emb2, pos + 1, None);
8652 for l in &mut self.kv_cache.layers {
8653 l.truncate_last(2);
8654 }
8655 }
8656 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8657
8658 let t1 = std::time::Instant::now();
8659 for _ in 0..iters {
8660 let _ = self.forward_pair(&emb1, &emb2, pos);
8661 for l in &mut self.kv_cache.layers {
8662 l.truncate_last(2);
8663 }
8664 }
8665 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8666 match graph_env {
8667 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8668 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8669 }
8670 (singles_ms, pair_ms)
8671 }
8672
8673 fn pair_supported(&self) -> bool {
8681 !self.weights.layers.is_empty()
8688 && !self.per_token_stack()
8689 && !self
8690 .weights
8691 .layers
8692 .iter()
8693 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8694 }
8695
8696 fn forward_pair(
8697 &mut self,
8698 emb1: &[f32],
8699 emb2: &[f32],
8700 position: usize,
8701 ) -> (Vec<f32>, Vec<f32>) {
8702 self.mimo_moe_prepare();
8705 let mut h1 = emb1.to_vec();
8706 let mut h2 = emb2.to_vec();
8707 let (_nkv, _hd, hs, _rd, eps) = (
8708 self.num_kv_heads,
8709 self.head_dim,
8710 self.hidden_size,
8711 self.rotary_dim,
8712 self.rms_eps,
8713 );
8714 let pool = self.pool.clone();
8715 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8722
8723 for li in 0..self.num_layers {
8724 let _capacity_tail = automatic_gpu_prefix
8725 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8726 .map(|_| crate::gpu::enter_cpu_scope());
8727 crate::gpu::set_layer(li as i64);
8730 let lw = &self.weights.layers[self.phys_layer(li)];
8731 inference::rms_norm_into(
8734 &h1,
8735 &lw.input_norm,
8736 self.rms_eps,
8737 self.norm_style,
8738 &mut self.ws.n1,
8739 );
8740 inference::rms_norm_into(
8741 &h2,
8742 &lw.input_norm,
8743 self.rms_eps,
8744 self.norm_style,
8745 &mut self.ws.n2,
8746 );
8747
8748 let (a1, a2) = match &lw.attn {
8749 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8750 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8751 AttnKind::Bounded(w) => {
8752 let rope = self
8755 .bounded_rope
8756 .clone()
8757 .expect("bounded layer without an installed rotation table");
8758 let cfg = crate::bounded::BoundedAttnCfg {
8759 num_heads: self.num_heads,
8760 num_kv_heads: self.num_kv_heads,
8761 head_dim: self.head_dim,
8762 hidden_size: hs,
8763 scale: self.attn_scale,
8764 rope: &rope,
8765 pool: pool.as_deref(),
8766 };
8767 let a1 = crate::bounded::bounded_attention(
8768 &self.ws.n1,
8769 w,
8770 &mut self.kv_cache.layers[li],
8771 &cfg,
8772 );
8773 let a2 = crate::bounded::bounded_attention(
8774 &self.ws.n2,
8775 w,
8776 &mut self.kv_cache.layers[li],
8777 &cfg,
8778 );
8779 (a1, a2)
8780 }
8781 AttnKind::Linear(w) => {
8782 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8783 let layer = &mut self.kv_cache.layers[li];
8784 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8785 vmf_phase_pair(
8786 &self.ws.n1,
8787 &self.ws.n2,
8788 w,
8789 &cfg,
8790 state,
8791 scratch,
8792 self.pool.as_deref(),
8793 )
8794 }
8795 AttnKind::LinearGdn(w) => {
8796 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8797 let layer = &mut self.kv_cache.layers[li];
8798 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8799 gdn_pair(
8800 &self.ws.n1,
8801 &self.ws.n2,
8802 w,
8803 &cfg,
8804 state,
8805 scratch,
8806 self.pool.as_deref(),
8807 )
8808 }
8809 AttnKind::ShortConv(w) => {
8810 let cfg = self
8811 .short_conv_cfg
8812 .expect("short-conv layer without short_conv_cfg");
8813 let layer = &mut self.kv_cache.layers[li];
8814 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8815 short_conv_pair(
8816 &self.ws.n1,
8817 &self.ws.n2,
8818 w,
8819 &cfg,
8820 state,
8821 scratch,
8822 self.pool.as_deref(),
8823 )
8824 }
8825 AttnKind::Full {
8826 wq,
8827 wk,
8828 wv,
8829 wo,
8830 q_norm,
8831 k_norm,
8832 output_gate,
8833 softplus_gate,
8834 bias,
8835 } => {
8836 let inv_freq_l = self.layer_inv_freq(li);
8837 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8838 let cfg = QwenAttnCfg {
8839 num_heads: self.layer_num_heads(li),
8840 num_kv_heads: nkv_l,
8841 head_dim: hd_l,
8842 hidden_size: hs,
8843 position,
8844 inv_freq: &inv_freq_l,
8845 rotary_dim: rd_l,
8846 scale: self.attn_scale,
8847 softcap: self.attn_softcap,
8848 window: self.layer_window(li),
8849 v_norm: self.attn_v_norm,
8850 qk_norm_after_rope: self.qk_norm_after_rope,
8851 gate_sigmoid: self.proj_gate_sigmoid,
8852 q_norm: q_norm.as_deref(),
8853 k_norm: k_norm.as_deref(),
8854 output_gate: *output_gate,
8855 softplus_gate: softplus_gate
8856 .as_ref()
8857 .map(|(gate, per_head)| (gate, *per_head)),
8858 rope_scale: self.layer_rope_scale(li),
8859 bias: bias
8860 .as_ref()
8861 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8862 rms_eps: eps,
8863 norm_style: self.norm_style,
8864 pool: pool.as_deref(),
8865 v_head_dim: self.layer_v_dim(li),
8866 };
8867 attention::qwen_attention_pair(
8868 &self.ws.n1,
8869 &self.ws.n2,
8870 wq,
8871 wk,
8872 wv,
8873 wo,
8874 &mut self.kv_cache.layers[li],
8875 &cfg,
8876 )
8877 }
8878 };
8879 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8880 Some(w) => (
8881 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8882 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8883 ),
8884 None => (a1, a2),
8885 };
8886 for i in 0..self.hidden_size {
8887 h1[i] += a1[i];
8888 h2[i] += a2[i];
8889 }
8890 let (mut a1, mut a2) = (a1, a2);
8891 attention::recycle_buf(&mut a1);
8892 attention::recycle_buf(&mut a2);
8893
8894 let lw = &self.weights.layers[self.phys_layer(li)];
8895 inference::rms_norm_into(
8896 &h1,
8897 &lw.post_norm,
8898 self.rms_eps,
8899 self.norm_style,
8900 &mut self.ws.p1,
8901 );
8902 inference::rms_norm_into(
8903 &h2,
8904 &lw.post_norm,
8905 self.rms_eps,
8906 self.norm_style,
8907 &mut self.ws.p2,
8908 );
8909 let (f1, f2) = match &lw.ffn {
8910 FfnKind::DenseMoe(dm) => (
8913 dense_moe_ffn(
8914 dm,
8915 &self.ws.p1,
8916 &h1,
8917 self.rms_eps,
8918 self.norm_style,
8919 self.pool.as_deref(),
8920 ),
8921 dense_moe_ffn(
8922 dm,
8923 &self.ws.p2,
8924 &h2,
8925 self.rms_eps,
8926 self.norm_style,
8927 self.pool.as_deref(),
8928 ),
8929 ),
8930 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8931 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8932 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8933 ),
8934 _ => ffn_forward_pair(
8935 &lw.ffn,
8936 &self.ws.p1,
8937 &self.ws.p2,
8938 self.pool.as_deref(),
8939 None,
8940 ),
8941 };
8942 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8943 Some(w) => (
8944 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8945 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8946 ),
8947 None => (f1, f2),
8948 };
8949 for i in 0..self.hidden_size {
8950 h1[i] += f1[i];
8951 h2[i] += f2[i];
8952 }
8953 let (mut f1, mut f2) = (f1, f2);
8954 attention::recycle_buf(&mut f1);
8955 attention::recycle_buf(&mut f2);
8956 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8957 for i in 0..self.hidden_size {
8958 h1[i] *= sc;
8959 h2[i] *= sc;
8960 }
8961 }
8962 if self.is_loop_end(li) && li + 1 < self.num_layers {
8964 h1 = inference::rms_norm(
8965 &h1,
8966 &self.weights.final_norm,
8967 self.rms_eps,
8968 self.norm_style,
8969 );
8970 h2 = inference::rms_norm(
8971 &h2,
8972 &self.weights.final_norm,
8973 self.rms_eps,
8974 self.norm_style,
8975 );
8976 }
8977 }
8978 crate::gpu::set_layer(-1); if self.o1_active() {
8985 self.commit_linear_scratch();
8986 }
8987 self.o1_progress();
8988 self.swa_trim_tails();
8989 (h1, h2)
8990 }
8991
8992 fn commit_linear_scratch(&mut self) {
8994 for layer in &mut self.kv_cache.layers {
8995 if !layer.linear_scratch.is_empty() {
8996 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8997 layer.linear_scratch.clear();
8998 }
8999 }
9000 }
9001
9002 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9010 #[cfg(target_os = "macos")]
9011 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9012 if ids.is_empty() {
9013 return Err("empty id sequence".to_string());
9014 }
9015
9016 self.nll_begin()?;
9017 let result = (|| {
9018 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9019 self.nll_check_graph("encoder prefill", 0)?;
9020 let expected = ids.len() * self.hidden_size;
9021 if hidden.len() != expected {
9022 return Err(format!(
9023 "encoder prefill returned {} values for {} rows of hidden size {}",
9024 hidden.len(),
9025 ids.len(),
9026 self.hidden_size
9027 ));
9028 }
9029
9030 let mut out = vec![0.0f32; expected];
9031 for (src, dst) in hidden
9032 .chunks_exact(self.hidden_size)
9033 .zip(out.chunks_exact_mut(self.hidden_size))
9034 {
9035 inference::rms_norm_into(
9036 src,
9037 &self.weights.final_norm,
9038 self.rms_eps,
9039 self.norm_style,
9040 dst,
9041 );
9042 }
9043 Ok(out)
9044 })();
9045 self.nll_end();
9046 result
9047 }
9048
9049 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9054 #[cfg(target_os = "macos")]
9055 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9056 if ids.is_empty() {
9057 return Err("empty id sequence".to_string());
9058 }
9059 self.nll_begin()?;
9060 let result = (|| {
9061 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9062 self.nll_check_graph("hidden prefill", 0)?;
9063 if hidden.len() != ids.len() * self.hidden_size {
9064 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9065 }
9066 Ok(hidden)
9067 })();
9068 self.nll_end();
9069 result
9070 }
9071
9072 pub fn forward_ids(
9075 &mut self,
9076 ids: &[u32],
9077 task_mask: Option<&TaskMask>,
9078 ) -> Result<Vec<f32>, String> {
9079 #[cfg(target_os = "macos")]
9080 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9081 if ids.is_empty() {
9082 return Err("empty id sequence".to_string());
9083 }
9084 self.clear_sequence_state();
9085 self.check_forward_graph("forward_ids setup", 0)?;
9086 if task_mask.is_none() {
9087 self.o1_begin();
9088 }
9089 let mut hidden = vec![0.0f32; self.hidden_size];
9090 let mut pos = 0usize;
9091 if let Some(b) = &mut self.dsv41 {
9092 let pool = self.pool.clone();
9093 let mut logits = Vec::new();
9094 crate::dsv41::forward_chunk(
9095 &b.0,
9096 &b.1,
9097 &b.2,
9098 &mut b.3,
9099 ids,
9100 0,
9101 pool.as_deref(),
9102 &mut logits,
9103 );
9104 if let Err(err) = self.o1_seal_checked() {
9105 self.clear_sequence_state();
9106 return Err(err);
9107 }
9108 return Ok(logits);
9109 }
9110 #[cfg(not(target_os = "macos"))]
9121 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9122 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9123 Ok(r) => r,
9124 Err(e) => {
9125 self.clear_sequence_state();
9126 return Err(e);
9127 }
9128 };
9129 if n > 0 {
9130 hidden.copy_from_slice(&last);
9131 pos = n;
9132 }
9133 }
9134 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9135 let chunk = self.prefill_chunk();
9139 let hs = self.hidden_size;
9140 while pos < ids.len() {
9141 let end = (pos + chunk).min(ids.len());
9142 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9143 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9144 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9145 pos = end;
9146 }
9147 }
9148 if task_mask.is_none()
9157 && !self.graph_prefill_preferred()
9158 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9159 && self.pair_supported()
9160 {
9161 while pos + 1 < ids.len() {
9162 let e1 = self.embed_single(ids[pos]);
9163 let e2 = self.embed_single(ids[pos + 1]);
9164 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9165 self.check_forward_graph("forward_ids pair", pos + 1)?;
9166 self.commit_linear_scratch();
9167 hidden = h2;
9168 pos += 2;
9169 }
9170 }
9171 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9174 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9175 self.graph_logits = Some(lg);
9176 hidden = vec![0.0; self.hidden_size];
9177 pos = ids.len();
9178 }
9179 }
9180 while pos < ids.len() {
9181 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9182 self.check_forward_graph("forward_ids", pos)?;
9183 pos += 1;
9184 }
9185 if let Some(logits) = self.graph_logits.take() {
9186 if let Err(err) = self.o1_seal_checked() {
9190 self.clear_sequence_state();
9191 return Err(err);
9192 }
9193 return Ok(logits);
9194 }
9195 if let Err(err) = self.o1_seal_checked() {
9199 self.clear_sequence_state();
9200 return Err(err);
9201 }
9202 let normed = inference::rms_norm(
9203 &hidden,
9204 &self.weights.final_norm,
9205 self.rms_eps,
9206 self.norm_style,
9207 );
9208 Ok(self.lm_head_forward(&normed))
9209 }
9210
9211 #[doc(hidden)]
9215 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9216 #[cfg(target_os = "macos")]
9217 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9218 if ids.is_empty() {
9219 return Err("empty id sequence".to_string());
9220 }
9221 self.clear_sequence_state();
9222 self.dsv41
9223 .as_ref()
9224 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9225 self.o1_begin();
9226 let rows = {
9227 let pool = self.pool.clone();
9228 let b = self
9229 .dsv41
9230 .as_mut()
9231 .expect("dsv41 checked above; state cannot change during forward");
9232 let mut rows = Vec::with_capacity(ids.len());
9233 for (position, &id) in ids.iter().enumerate() {
9234 let mut logits = Vec::new();
9235 crate::dsv41::forward_token(
9236 &b.0,
9237 &b.1,
9238 &b.2,
9239 &mut b.3,
9240 id,
9241 position,
9242 pool.as_deref(),
9243 &mut logits,
9244 );
9245 rows.push(logits);
9246 }
9247 rows
9248 };
9249 self.o1_seal();
9250 Ok(rows)
9251 }
9252
9253 pub fn dsv41_moe_capture(
9257 &mut self,
9258 ids: &[u32],
9259 lo: usize,
9260 hi: usize,
9261 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9262 #[cfg(target_os = "macos")]
9263 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9264 if ids.is_empty() {
9265 return Err("empty id sequence".to_string());
9266 }
9267 self.dsv41
9268 .as_ref()
9269 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9270 self.clear_sequence_state();
9271 let res = {
9272 let pool = self.pool.clone();
9273 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9274 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9275 };
9276 self.clear_sequence_state();
9277 res
9278 }
9279
9280 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9287 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9288 Ok((nll / cnt.max(1) as f64).exp())
9289 }
9290
9291 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9298 if !self.can_prefill_batched() {
9299 return self.nll_ids_from(ids, 0).map(|_| ());
9300 }
9301 self.nll_begin()?;
9302 let result: Result<(), String> = (|| {
9303 let chunk = chunk.max(1);
9304 let mut pos = 0usize;
9305 while pos < ids.len() {
9306 let end = (pos + chunk).min(ids.len());
9307 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9308 self.nll_check_graph("calibration prefill", pos)?;
9309 pos = end;
9310 }
9311 Ok(())
9312 })();
9313 self.nll_end();
9314 result
9315 }
9316
9317 pub fn calib_window_span(
9326 &mut self,
9327 ids: &[u32],
9328 input: Option<&[f32]>,
9329 from: usize,
9330 mid: usize,
9331 upto: usize,
9332 ) -> Result<Vec<f32>, String> {
9333 if !self.can_prefill_batched() {
9334 return Err("calib_window_span: this architecture has no batched prefill".into());
9335 }
9336 self.nll_begin()?;
9337 let result: Result<Vec<f32>, String> = (|| {
9338 let h = if mid > from {
9339 let inp = match input {
9340 Some(h) => PrefillIn::Hidden(h),
9341 None => PrefillIn::Ids(ids),
9342 };
9343 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9344 self.nll_check_graph("calibration span", 0)?;
9345 h
9346 } else {
9347 input.map(<[f32]>::to_vec).unwrap_or_default()
9348 };
9349 if upto > mid {
9350 let inp = if mid > from || input.is_some() {
9351 PrefillIn::Hidden(&h)
9352 } else {
9353 PrefillIn::Ids(ids)
9354 };
9355 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9356 self.nll_check_graph("calibration span", 0)?;
9357 }
9358 Ok(h)
9359 })();
9360 self.nll_end();
9361 result
9362 }
9363
9364 #[cfg(feature = "gpu")]
9368 pub fn qwen4_capture(
9369 &mut self,
9370 ids: &[u32],
9371 lo: usize,
9372 hi: usize,
9373 ) -> Result<Vec<Vec<f32>>, String> {
9374 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9375 crate::qwen4_exp::capture_window(
9376 &b.0,
9377 &b.1,
9378 &b.2,
9379 &mut b.3,
9380 ids,
9381 lo,
9382 hi,
9383 &self.inv_freq,
9384 self.pool.as_deref(),
9385 )
9386 }
9387
9388 #[cfg(feature = "gpu")]
9394 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9395 use crate::gpu_wgpu::clover as cap;
9396 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9397 let ne = (lo..hi)
9398 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9399 FfnKind::Moe(m) => Some(m.experts.len()),
9400 _ => None,
9401 })
9402 .ok_or("no MoE layer in the capture range")?;
9403 let chunk = self.prefill_chunk().max(1);
9404 self.clear_sequence_state();
9405 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9406 return Err("capture unavailable (no wgpu device)".into());
9407 }
9408 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9409 let res = (|| {
9410 for (ci, part) in ids.chunks(chunk).enumerate() {
9411 let mut hiddens = Vec::with_capacity(part.len() * hs);
9412 for &id in part {
9413 hiddens.extend_from_slice(&self.embed_single(id));
9414 }
9415 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9416 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9417 crate::gpu::BatchGraphOutcome::Completed => {}
9418 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9419 }
9420 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9421 if written < hi - lo {
9422 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9423 }
9424 for (o, r) in out.iter_mut().zip(rows) {
9425 o.extend_from_slice(&r);
9426 }
9427 }
9428 Ok(())
9429 })();
9430 cap::moe_capture_end();
9431 self.clear_sequence_state();
9432 res.map(|_| out)
9433 }
9434
9435 pub fn moe_expert_dirs(
9438 &self,
9439 li: usize,
9440 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9441 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9442 return None;
9443 };
9444 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9445 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9446 let shared = match &m.shared {
9447 Some((se, _)) => Some(dirs(se)?),
9448 None => None,
9449 };
9450 Some((routed, shared))
9451 }
9452
9453 pub fn qwen4_expert_dirs(
9455 &self,
9456 li: usize,
9457 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9458 let b = self.qwen4_exp.as_ref()?;
9459 crate::qwen4_exp::expert_dirs(&b.1, li)
9460 }
9461
9462 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9477 self.reset_mtp_probe_state();
9478 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9484 let result = if ids.len() < 2 {
9485 Err("MTP FFN probe needs at least two token ids".into())
9486 } else if ids
9487 .iter()
9488 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9489 {
9490 Err("MTP FFN probe received a token id outside the embedding table".into())
9491 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9492 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9495 } else if self.mimo_mtp.is_some() {
9496 let mut st = self
9497 .mimo_mtp
9498 .take()
9499 .expect("checked MiMo MTP before detaching it");
9500 let result = match self.probe_trunk_rows(ids) {
9501 Ok(trunk) => crate::gpu::cpu_scope(|| {
9502 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9503 }),
9504 Err(e) => Err(e),
9505 };
9506 st.reset();
9509 self.mimo_mtp = Some(st);
9510 result
9511 } else if self.mtp.is_some() {
9512 let mut mtp = self
9513 .mtp
9514 .take()
9515 .expect("checked canonical MTP before detaching it");
9516 let result = match self.probe_trunk_rows(ids) {
9517 Ok(trunk) => crate::gpu::cpu_scope(|| {
9518 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9519 }),
9520 Err(e) => Err(e),
9521 };
9522 mtp.kv.clear();
9523 self.mtp = Some(mtp);
9524 result
9525 } else {
9526 Err("MTP FFN probe: this pipeline has no draft stack".into())
9527 };
9528 self.reset_mtp_probe_state();
9531 result
9532 }
9533
9534 fn reset_mtp_probe_state(&mut self) {
9536 self.clear_sequence_state();
9537 if let Some(mtp) = self.mtp.as_mut() {
9538 mtp.kv.clear();
9539 }
9540 if let Some(mimo) = self.mimo_mtp.as_mut() {
9541 mimo.reset();
9542 }
9543 self.mtp_graph_mode = None;
9544 }
9545
9546 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9552 if ids.len() < 2 {
9553 return Err("MTP FFN probe needs at least two token ids".into());
9554 }
9555 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9556 self.clear_sequence_state();
9557 Ok(rows)
9558 }
9559
9560 fn mtp_ffn_probe_teacher_forced(
9566 &mut self,
9567 mtp: &mut MtpModule,
9568 ids: &[u32],
9569 trunk: &[f32],
9570 ) -> Result<Vec<MtpFfnProbe>, String> {
9571 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9572 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9573 };
9574 let inter = dense.gate_proj.rows();
9575 let (hs, n) = (self.hidden_size, ids.len() - 1);
9576 if trunk.len() != n * hs {
9577 return Err(format!(
9578 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9579 trunk.len()
9580 ));
9581 }
9582 let mut mass = vec![0.0f64; inter];
9583 let mut squared_mass = vec![0.0f64; inter];
9584 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9585 for position in 0..n {
9586 self.mtp_probe_ffn_input(
9587 mtp,
9588 &trunk[position * hs..(position + 1) * hs],
9589 ids[position + 1],
9590 position,
9591 )?;
9592 inputs.extend_from_slice(&self.ws.p1[..hs]);
9593 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9594 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9595 unreachable!("checked dense above");
9596 };
9597 accumulate_dense_ffn_activation_mass(
9598 dense,
9599 &inputs,
9600 inputs.len() / hs,
9601 self.pool.as_deref(),
9602 &mut mass,
9603 &mut squared_mass,
9604 )?;
9605 inputs.clear();
9606 }
9607 }
9608 Ok(vec![MtpFfnProbe {
9609 mass,
9610 squared_mass,
9611 observed_positions: n,
9612 }])
9613 }
9614
9615 fn mtp_probe_ffn_input(
9621 &mut self,
9622 mtp: &mut MtpModule,
9623 hidden: &[f32],
9624 next_token: u32,
9625 position: usize,
9626 ) -> Result<(), String> {
9627 let e = self.embed_single(next_token);
9628 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9629 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9630 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9631 inference::rms_norm_into(
9632 hidden,
9633 &mtp.hnorm,
9634 self.rms_eps,
9635 self.norm_style,
9636 cat_h,
9637 );
9638 let mut x = vec![0.0f32; self.hidden_size];
9639 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9640
9641 let lw = &mtp.layer;
9642 inference::rms_norm_into(
9643 &x,
9644 &lw.input_norm,
9645 self.rms_eps,
9646 self.norm_style,
9647 &mut self.ws.n1,
9648 );
9649 let attn = match &lw.attn {
9650 AttnKind::Full {
9651 wq,
9652 wk,
9653 wv,
9654 wo,
9655 q_norm,
9656 k_norm,
9657 output_gate,
9658 softplus_gate,
9659 bias,
9660 } => {
9661 let mut cfg = self.attn_cfg(position);
9662 cfg.q_norm = q_norm.as_deref();
9663 cfg.k_norm = k_norm.as_deref();
9664 cfg.output_gate = *output_gate;
9665 cfg.softplus_gate = softplus_gate
9666 .as_ref()
9667 .map(|(gate, per_head)| (gate, *per_head));
9668 cfg.bias = bias
9669 .as_ref()
9670 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9671 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9672 }
9673 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9674 };
9675 for (dst, src) in x.iter_mut().zip(attn) {
9676 *dst += src;
9677 }
9678 inference::rms_norm_into(
9679 &x,
9680 &lw.post_norm,
9681 self.rms_eps,
9682 self.norm_style,
9683 &mut self.ws.p1,
9684 );
9685 Ok(())
9686 }
9687
9688 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9693 self.clear_sequence_state();
9694 FFN_PROBE.with(|p| {
9695 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9696 });
9697 crate::gpu::cpu_scope(|| {
9698 for (pos, &id) in ids.iter().enumerate() {
9699 let emb = self.embed_single(id);
9700 let _ = self.forward_layers(&emb, pos, None);
9701 }
9702 });
9703 self.clear_sequence_state();
9704 FFN_PROBE
9705 .with(|p| p.borrow_mut().take())
9706 .unwrap_or_default()
9707 }
9708
9709 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9713 if let Err(err) = self.nll_begin() {
9714 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9718 self.nll_end();
9719 return Err(err);
9720 }
9721 FFN_PROBE.with(|p| {
9722 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9723 });
9724 let result: Result<(), String> = (|| {
9725 for chunk in ids.chunks(256) {
9726 if chunk.len() < 2 {
9727 continue;
9728 }
9729 self.nll_ids_masked(chunk, 0, None)?;
9730 }
9731 Ok(())
9732 })();
9733 self.nll_end();
9734 let probe = FFN_PROBE
9735 .with(|p| p.borrow_mut().take())
9736 .unwrap_or_default();
9737 match result {
9738 Ok(()) => Ok(probe),
9739 Err(err) => {
9740 drop(probe);
9741 Err(err)
9742 }
9743 }
9744 }
9745
9746 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9750 self.nll_begin()?;
9751 let result: Result<f64, String> = (|| {
9752 let mut nll = 0f64;
9753 let mut cnt = 0usize;
9754 let mut hidden = vec![0f32; self.hidden_size];
9755 for (pos, &id) in ids.iter().enumerate() {
9756 if pos > 0 {
9757 inference::rms_norm_into(
9758 &hidden,
9759 &self.weights.final_norm,
9760 self.rms_eps,
9761 self.norm_style,
9762 &mut self.ws.n1,
9763 );
9764 let mut logits = self.lm_head_forward(&self.ws.n1);
9765 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9766 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9767 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9768 nll -= p.max(1e-300).ln();
9769 cnt += 1;
9770 attention::recycle_buf(&mut logits);
9771 }
9772 let emb = self.embed_single(id);
9773 hidden = self.forward_layers(&emb, pos, Some(mask));
9774 self.nll_check_graph("masked serial forward", pos)?;
9775 let _ = self.graph_logits.take();
9779 }
9780 Ok((nll / cnt.max(1) as f64).exp())
9781 })();
9782 self.nll_end();
9783 result
9784 }
9785
9786 pub fn nll_ids_masked(
9805 &mut self,
9806 ids: &[u32],
9807 start: usize,
9808 task_mask: Option<&TaskMask>,
9809 ) -> Result<(f64, usize), String> {
9810 let task_mask = self.drop_open_mask(task_mask);
9811 self.nll_ids_inner(ids, start, task_mask)
9812 }
9813
9814 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9815 self.nll_ids_inner(ids, start, None)
9816 }
9817
9818 fn nll_ids_inner(
9819 &mut self,
9820 ids: &[u32],
9821 start: usize,
9822 task_mask: Option<&TaskMask>,
9823 ) -> Result<(f64, usize), String> {
9824 self.nll_begin()?;
9825 #[cfg(target_os = "macos")]
9827 if task_mask.is_none() && self.is_block_diffusion() {
9828 if let Some(r) = self.clover_metal_nll(ids, start) {
9829 self.nll_end();
9830 return r;
9831 }
9832 }
9833 let result: Result<(f64, usize), String> = (|| {
9834 let mut nll = 0f64;
9835 let mut cnt = 0usize;
9836 let (graph_quality, fused_head_quality) = nll_graph_policy(
9849 task_mask.is_none(),
9850 self.graph_prefill_preferred(),
9851 crate::gpu::q1_force(),
9852 );
9853 self.graph_head_required = fused_head_quality;
9854 self.graph_want_logits = fused_head_quality;
9855 #[cfg(target_os = "macos")]
9856 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9857 match self.nll_batch_metal(ids, start) {
9858 MetalBatchNllOutcome::Completed(nll, count) => {
9859 return Ok((nll, count));
9860 }
9861 MetalBatchNllOutcome::Declined => {}
9862 MetalBatchNllOutcome::Failed(err) => return Err(err),
9863 }
9864 }
9865 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9870 if self.can_prefill_batched() && !graph_quality && !force_serial {
9871 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9880 .ok()
9881 .and_then(|v| v.parse().ok())
9882 .filter(|&c: &usize| c > 0)
9883 .unwrap_or(128);
9884 const LM_SUB: usize = 32;
9885 let n = ids.len().saturating_sub(1);
9886 let hs = self.hidden_size;
9887 let rows = self.weights.lm_head.rows();
9888 let mut pos = 0usize;
9889 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9890 while pos < n {
9891 let end = (pos + chunk_len).min(n);
9892 let bsz = end - pos;
9893 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9894 self.nll_check_graph("batched prefill", pos)?;
9895 if state_trace && end % 256 == 0 {
9896 self.trace_recurrent_state(end);
9897 }
9898 let mut k0 = 0usize;
9899 while k0 < bsz {
9900 let k1 = (k0 + LM_SUB).min(bsz);
9901 let sb = k1 - k0;
9902 if pos + k1 <= start {
9905 k0 = k1;
9906 continue;
9907 }
9908 let mut normed = vec![0.0f32; sb * hs];
9909 for k in 0..sb {
9910 let r = inference::rms_norm(
9911 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9912 &self.weights.final_norm,
9913 self.rms_eps,
9914 self.norm_style,
9915 );
9916 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9917 }
9918 let mut logits = vec![0.0f32; sb * rows];
9919 self.weights
9920 .lm_head
9921 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9922 for k in 0..sb {
9923 if pos + k0 + k < start {
9924 continue;
9925 }
9926 self.nll_check_graph("batched score row", pos + k0 + k)?;
9927 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9928 if let Some(mu) = self.logit_multiplier {
9929 for v in lg.iter_mut() {
9930 *v *= mu;
9931 }
9932 }
9933 if let Some(c) = self.final_softcap {
9937 softcap_in_place(lg, c, self.pool.as_deref());
9938 }
9939 if let Some(cm) = self.head_clusters.clone() {
9942 self.hierarchical_head_logprobs(
9943 &normed[k * hs..(k + 1) * hs],
9944 &cm,
9945 lg,
9946 );
9947 }
9948 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9949 let target = ids[pos + k0 + k + 1] as usize;
9950 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9951 let lse: f64 = lg
9952 .iter()
9953 .map(|&v| ((v - max) as f64).exp())
9954 .sum::<f64>()
9955 .ln()
9956 + max as f64;
9957 nll += lse - lg[target] as f64;
9958 cnt += 1;
9959 if std::env::var("CMF_PPL_TRACE").is_ok() {
9960 let top = lg
9961 .iter()
9962 .enumerate()
9963 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9964 .map(|(i, _)| i)
9965 .unwrap_or(0);
9966 eprintln!(
9967 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9968 pos + k0 + k,
9969 target,
9970 lse - lg[target] as f64,
9971 top,
9972 lg[target],
9973 lg[top]
9974 );
9975 }
9976 }
9977 k0 = k1;
9978 }
9979 pos = end;
9980 }
9981 return Ok((nll, cnt));
9982 }
9983 for pos in 0..ids.len().saturating_sub(1) {
9984 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9985 self.nll_check_graph("serial forward", pos)?;
9986 let out_of_band = self.graph_logits.take();
9994 if self.graph_head_required && out_of_band.is_none() {
9995 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9996 return Err(format!(
9997 "fused Metal graph head did not complete at NLL position {pos}"
9998 ));
9999 }
10000 if pos < start {
10001 continue;
10002 }
10003 let logits = match out_of_band {
10004 Some(lg) => lg,
10005 None => {
10006 let normed = inference::rms_norm(
10007 &hidden,
10008 &self.weights.final_norm,
10009 self.rms_eps,
10010 self.norm_style,
10011 );
10012 self.lm_head_forward(&normed)
10016 }
10017 };
10018 let target = ids[pos + 1] as usize;
10019 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10020 let lse: f64 = logits
10021 .iter()
10022 .map(|&v| ((v - max) as f64).exp())
10023 .sum::<f64>()
10024 .ln()
10025 + max as f64;
10026 let tok_nll = lse - logits[target] as f64;
10027 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10028 let top = logits
10029 .iter()
10030 .enumerate()
10031 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10032 .map(|(i, _)| i)
10033 .unwrap_or(0);
10034 eprintln!(
10035 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10036 logits[target], logits[top]
10037 );
10038 }
10039 nll += tok_nll;
10040 cnt += 1;
10041 }
10042 Ok((nll, cnt))
10043 })();
10044 self.nll_end();
10045 result
10046 }
10047
10048 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
10053 let normed = inference::rms_norm(
10054 hidden,
10055 &self.weights.final_norm,
10056 self.rms_eps,
10057 self.norm_style,
10058 );
10059 let mut logits = self.lm_head_forward(&normed);
10062 let target = target as usize;
10063 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10064 let lse: f64 = logits
10065 .iter()
10066 .map(|&v| ((v - max) as f64).exp())
10067 .sum::<f64>()
10068 .ln()
10069 + max as f64;
10070 let tok_nll = lse - logits[target] as f64;
10071 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10072 let top = logits
10073 .iter()
10074 .enumerate()
10075 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10076 .map(|(i, _)| i)
10077 .unwrap_or(0);
10078 eprintln!(
10079 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10080 logits[target], logits[top]
10081 );
10082 }
10083 attention::recycle_buf(&mut logits);
10084 tok_nll
10085 }
10086
10087 fn trace_recurrent_state(&self, pos: usize) {
10095 let stats = |v: &[f32]| -> (f64, f64) {
10096 if v.is_empty() {
10097 return (0.0, 0.0);
10098 }
10099 let (mut ss, mut mx) = (0f64, 0f64);
10100 for &x in v {
10101 ss += (x as f64) * (x as f64);
10102 mx = mx.max((x as f64).abs());
10103 }
10104 ((ss / v.len() as f64).sqrt(), mx)
10105 };
10106 for (li, l) in self.kv_cache.layers.iter().enumerate() {
10107 let lw = &self.weights.layers[self.phys_layer(li)];
10108 let (kind, s_len) = match &lw.attn {
10109 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10110 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10111 AttnKind::Bounded(_) => ("bounded", 0),
10112 AttnKind::Full { .. } => ("full", 0),
10113 _ => ("other", 0),
10114 };
10115 let (rms, max) = stats(&l.linear_state);
10116 let s_part = if kind == "vmf" {
10117 &l.linear_state[..s_len.min(l.linear_state.len())]
10118 } else if kind == "gdn" {
10119 let ring = l
10120 .linear_state
10121 .len()
10122 .saturating_sub(s_len)
10123 .min(l.linear_state.len());
10124 &l.linear_state[ring..]
10125 } else {
10126 &l.linear_state[..0]
10127 };
10128 let (s_rms, s_max) = stats(s_part);
10129 let (ring_rms, ring_len) = match &l.bounded {
10130 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10131 None => (0.0, 0),
10132 };
10133 eprintln!(
10134 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10135 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10136 l.linear_state.len(),
10137 l.seq_len
10138 );
10139 }
10140 }
10141
10142 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10160 self.nll_begin()?;
10165 let requested_prefix = (prefill > 0).then_some(prefill);
10166 self.o1_begin_with_prefix(requested_prefix);
10167 let n = ids.len().saturating_sub(1);
10168 let requested_start = prefill.min(n);
10169 let exact_end = if self.o1_active() {
10174 match requested_prefix {
10175 Some(requested) => self.o1_effective_boundary(requested),
10176 None => self
10177 .o1_cfg
10178 .as_ref()
10179 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10180 }
10181 .unwrap_or(requested_start)
10182 .min(n)
10183 } else {
10184 requested_start
10185 };
10186 let mut nll = 0f64;
10187 let mut cnt = 0usize;
10188
10189 let mut pos = 0usize;
10193 if self.can_prefill_batched() {
10194 const CHUNK: usize = 128;
10195 while pos < exact_end {
10196 let end = (pos + CHUNK).min(exact_end);
10197 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10198 if self
10199 .graph_failed
10200 .swap(false, std::sync::atomic::Ordering::Relaxed)
10201 {
10202 self.cancel
10203 .store(false, std::sync::atomic::Ordering::Relaxed);
10204 self.nll_end();
10205 return Err("GPU graph failed during O(1) NLL prefix".into());
10206 }
10207 for row in 0..end - pos {
10208 let score_pos = pos + row;
10209 if score_pos >= requested_start && score_pos < n {
10210 nll += self.nll_from_hidden(
10211 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10212 ids[score_pos + 1],
10213 score_pos,
10214 );
10215 cnt += 1;
10216 }
10217 }
10218 pos = end;
10219 }
10220 } else {
10221 while pos < exact_end {
10222 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10223 if self
10224 .graph_failed
10225 .swap(false, std::sync::atomic::Ordering::Relaxed)
10226 {
10227 self.cancel
10228 .store(false, std::sync::atomic::Ordering::Relaxed);
10229 self.nll_end();
10230 return Err("GPU graph failed during O(1) NLL prefix".into());
10231 }
10232 if pos >= requested_start && pos < n {
10233 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10234 cnt += 1;
10235 }
10236 pos += 1;
10237 }
10238 }
10239 self.o1_seal_checked().map_err(|err| {
10240 self.nll_end();
10241 err
10242 })?;
10243
10244 let batch_k = std::env::var("CMF_BATCH_K")
10253 .ok()
10254 .and_then(|v| v.parse::<usize>().ok())
10255 .unwrap_or(0);
10256 let batch_admitted = batch_k > 0
10257 && self.can_prefill_batched()
10258 && self.o1_active()
10259 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10260 && (0..self.num_layers).all(|li| {
10261 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10262 cache.o1.is_none() || cache.o1_views().is_some()
10263 });
10264 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10265 eprintln!(
10266 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10267 batch_admitted,
10268 batch_k,
10269 n.saturating_sub(exact_end),
10270 );
10271 }
10272 let mut batch_completed = false;
10273 if batch_admitted && exact_end < n {
10274 let hs = self.hidden_size;
10275 let mut batch_pos = exact_end;
10276 while batch_pos < n {
10277 let end = (batch_pos + batch_k).min(n);
10278 let bk = end - batch_pos;
10279 let mut hiddens = vec![0.0f32; bk * hs];
10280 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10281 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10282 }
10283 let positions: Vec<usize> = (batch_pos..end).collect();
10284 let t_batch = std::time::Instant::now();
10285 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10286 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10287 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10288 eprintln!(
10289 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10290 batch_pos,
10291 end.saturating_sub(1),
10292 bk as f64 / (ms / 1000.0),
10293 );
10294 }
10295 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10296 self.nll_end();
10297 return Err(err);
10298 }
10299 match outcome {
10300 crate::gpu::BatchGraphOutcome::Completed => {
10301 batch_completed = true;
10302 for row in 0..bk {
10303 nll += self.nll_from_hidden(
10304 &hiddens[row * hs..(row + 1) * hs],
10305 ids[batch_pos + row + 1],
10306 batch_pos + row,
10307 );
10308 cnt += 1;
10309 }
10310 batch_pos = end;
10311 }
10312 crate::gpu::BatchGraphOutcome::Declined => {
10313 if batch_completed {
10314 self.nll_end();
10315 return Err(format!(
10316 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10317 ));
10318 }
10319 break;
10320 }
10321 crate::gpu::BatchGraphOutcome::Failed => {
10322 self.nll_end();
10323 return Err(format!(
10324 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10325 ));
10326 }
10327 }
10328 }
10329 if batch_completed && cnt == n.saturating_sub(requested_start) {
10330 self.nll_end();
10331 return Ok((nll, cnt));
10332 }
10333 }
10334
10335 for pos in exact_end..n {
10340 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10341 if self
10342 .graph_failed
10343 .swap(false, std::sync::atomic::Ordering::Relaxed)
10344 {
10345 self.cancel
10346 .store(false, std::sync::atomic::Ordering::Relaxed);
10347 self.nll_end();
10348 return Err(format!(
10349 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10350 ));
10351 }
10352 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10353 cnt += 1;
10354 }
10355 self.nll_end();
10356 Ok((nll, cnt))
10357 }
10358
10359 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10367 self.clear_sequence_state();
10368 let n = ids.len().saturating_sub(1);
10369 let mut correct = Vec::with_capacity(n);
10370 let mut pmax = Vec::with_capacity(n);
10371 for pos in 0..n {
10372 let emb = self.embed_single(ids[pos]);
10373 let hidden = self.forward_layers(&emb, pos, None);
10374 let logits = if let Some(logits) = self.graph_logits.take() {
10375 logits
10376 } else {
10377 let normed = inference::rms_norm(
10378 &hidden,
10379 &self.weights.final_norm,
10380 self.rms_eps,
10381 self.norm_style,
10382 );
10383 self.lm_head_forward(&normed)
10387 };
10388 let target = ids[pos + 1] as usize;
10389 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10390 for (i, &v) in logits.iter().enumerate() {
10391 if v > mval {
10392 mval = v;
10393 amax = i;
10394 }
10395 }
10396 correct.push(amax == target);
10397 let row: Vec<f32> = temps
10398 .iter()
10399 .map(|&t| {
10400 let tt = t.max(1e-3);
10401 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10402 1.0 / s.max(1e-12) })
10404 .collect();
10405 pmax.push(row);
10406 }
10407 self.clear_sequence_state();
10408 (correct, pmax)
10409 }
10410
10411 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10418 if self.dyn_router.is_none() {
10419 return Ok((self.ppl_ids(ids)?, 0));
10420 }
10421 self.nll_begin()?;
10422 let saved_active = self.dyn_active;
10423 let mut router = self
10424 .dyn_router
10425 .take()
10426 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10427 router.reset();
10428 self.dyn_phi_seen = 0;
10429 let _ = self.set_active_skill(None);
10430
10431 let result: Result<(f64, usize), String> = (|| {
10432 let mut nll = 0f64;
10433 let mut cnt = 0usize;
10434 for pos in 0..ids.len().saturating_sub(1) {
10435 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10436 self.nll_check_graph("dynamic serial forward", pos)?;
10437 let out_of_band = self.graph_logits.take();
10438 let mut logits = match out_of_band {
10439 Some(lg) => lg,
10440 None => {
10441 let normed = inference::rms_norm(
10442 &hidden,
10443 &self.weights.final_norm,
10444 self.rms_eps,
10445 self.norm_style,
10446 );
10447 self.lm_head_forward(&normed)
10451 }
10452 };
10453 let target = ids[pos + 1] as usize;
10454 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10455 let lse: f64 = logits
10456 .iter()
10457 .map(|&v| ((v - max) as f64).exp())
10458 .sum::<f64>()
10459 .ln()
10460 + max as f64;
10461 let tok_nll = lse - logits[target] as f64;
10462 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10463 let top = logits
10464 .iter()
10465 .enumerate()
10466 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10467 .map(|(i, _)| i)
10468 .unwrap_or(0);
10469 eprintln!(
10470 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10471 logits[target], logits[top]
10472 );
10473 }
10474 nll += tok_nll;
10475 cnt += 1;
10476 attention::recycle_buf(&mut logits);
10477 let phi = self.dyn_phi_ema.clone();
10479 if let Some(new_active) = router.step(&phi, pos) {
10480 let _ = self.set_active_skill(new_active);
10481 }
10482 }
10483 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10484 })();
10485
10486 let _ = self.set_active_skill(saved_active);
10489 self.dyn_router = Some(router);
10490 self.nll_end();
10491 result
10492 }
10493
10494 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10496 self.clear_sequence_state();
10497 let mut acc = vec![0f32; self.hidden_size];
10498 for (pos, &id) in ids.iter().enumerate() {
10499 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10500 for (a, v) in acc.iter_mut().zip(&h) {
10501 *a += v;
10502 }
10503 }
10504 let n = ids.len().max(1) as f32;
10505 for a in acc.iter_mut() {
10506 *a /= n;
10507 }
10508 self.clear_sequence_state();
10509 acc
10510 }
10511
10512 pub fn probe_phi_span(
10526 &mut self,
10527 ids: &[u32],
10528 layer: usize,
10529 span: std::ops::Range<usize>,
10530 ) -> Vec<f32> {
10531 #[cfg(target_os = "macos")]
10532 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10533 let end = span.end.min(ids.len());
10534 let start = span.start.min(end);
10535 let reset = |p: &mut Self| p.clear_sequence_state();
10536 reset(self);
10537 let mut acc = vec![0f32; self.hidden_size];
10538 for (pos, &id) in ids[..end].iter().enumerate() {
10539 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10540 if pos >= start {
10541 for (a, v) in acc.iter_mut().zip(&h) {
10542 *a += v;
10543 }
10544 }
10545 }
10546 let n = end - start;
10547 if n > 0 {
10548 let n = n as f32;
10549 for a in acc.iter_mut() {
10550 *a /= n;
10551 }
10552 }
10553 reset(self);
10554 acc
10555 }
10556
10557 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10564 #[cfg(target_os = "macos")]
10565 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10566 self.graph_logits = None;
10567 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10568 if let Some(logits) = self.graph_logits.take() {
10569 return logits;
10570 }
10571 inference::rms_norm_into(
10572 &hidden,
10573 &self.weights.final_norm,
10574 self.rms_eps,
10575 self.norm_style,
10576 &mut self.ws.n1,
10577 );
10578 self.lm_head_forward(&self.ws.n1)
10579 }
10580
10581 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10587 self.prefill_batch_masked(ids, start_pos, None)
10588 }
10589
10590 fn prefill_batch_masked(
10596 &mut self,
10597 ids: &[u32],
10598 start_pos: usize,
10599 task_mask: Option<&TaskMask>,
10600 ) -> Vec<f32> {
10601 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10602 }
10603
10604 fn prefill_rows(
10612 &mut self,
10613 ids: &[u32],
10614 pos: usize,
10615 task_mask: Option<&TaskMask>,
10616 ) -> Result<Vec<f32>, String> {
10617 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10618 }
10619
10620 fn prefill_input_rows(
10621 &mut self,
10622 input: PrefillIn<'_>,
10623 pos: usize,
10624 task_mask: Option<&TaskMask>,
10625 ) -> Result<Vec<f32>, String> {
10626 self.mimo_moe_prepare();
10627 let hs = self.hidden_size;
10628 let bk = match input {
10629 PrefillIn::Ids(ids) => ids.len(),
10630 PrefillIn::Hidden(rows) => rows.len() / hs,
10631 };
10632 #[cfg(not(target_os = "macos"))]
10633 if task_mask.is_none()
10634 && !self.o1_active()
10635 && bk > 1
10636 && (self.batch_prefix_prefill()
10637 || (self.verify_exact_moe
10638 && crate::gpu::enabled_here()
10639 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10640 {
10641 let mut hiddens = match input {
10642 PrefillIn::Hidden(rows) => rows.to_vec(),
10643 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10644 };
10645 let positions: Vec<usize> = (pos..pos + bk).collect();
10646 let mut run = 0usize;
10647 match self.try_batch_graph_wgpu_prefix(
10648 &mut hiddens,
10649 &positions,
10650 bk,
10651 None,
10652 Some(&mut run),
10653 ) {
10654 crate::gpu::BatchGraphOutcome::Completed => {
10655 let out = if run < self.num_layers {
10656 self.prefill_batch_span(
10657 PrefillIn::Hidden(&hiddens),
10658 pos,
10659 None,
10660 run,
10661 self.num_layers,
10662 )
10663 } else {
10664 hiddens
10665 };
10666 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10667 Err("MiMo attention graph failed after admission".into())
10668 } else {
10669 Ok(out)
10670 };
10671 }
10672 crate::gpu::BatchGraphOutcome::Failed => {
10673 return Err("batched prefix prefill failed after admission".into());
10674 }
10675 crate::gpu::BatchGraphOutcome::Declined => {
10676 #[cfg(feature = "gpu")]
10678 self.pull_lagging_host_kv(0, self.num_layers, pos);
10679 }
10680 }
10681 }
10682 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10683 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10684 Err("batch tail graph failed after admission".into())
10685 } else {
10686 Ok(out)
10687 }
10688 }
10689
10690 fn prefill_batch_span(
10696 &mut self,
10697 input: PrefillIn<'_>,
10698 start_pos: usize,
10699 task_mask: Option<&TaskMask>,
10700 from: usize,
10701 upto_excl: usize,
10702 ) -> Vec<f32> {
10703 let hs = self.hidden_size;
10704 let b = match input {
10705 PrefillIn::Ids(ids) => ids.len(),
10706 PrefillIn::Hidden(hb) => hb.len() / hs,
10707 };
10708 let upto_excl = upto_excl.min(self.num_layers);
10709 let mut h: Vec<f32>;
10713 let mut h_ready;
10714 match input {
10715 PrefillIn::Ids(_) => {
10716 h = vec![0.0; b * hs];
10717 h_ready = false;
10718 }
10719 PrefillIn::Hidden(hb) => {
10720 h = hb.to_vec();
10721 h_ready = true;
10722 }
10723 }
10724 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10725 if let PrefillIn::Ids(ids) = input {
10726 for (bi, &id) in ids.iter().enumerate() {
10727 let e = me.embed_single(id);
10728 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10729 }
10730 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10731 if let Ok(t) = tp.parse::<usize>() {
10732 if t >= start_pos && t < start_pos + ids.len() {
10733 let bi = t - start_pos;
10734 let row = &h[bi * hs..(bi + 1) * hs];
10735 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10736 eprintln!(
10737 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10738 ids[bi],
10739 row[0],
10740 row[1],
10741 ids.len(),
10742 &ids[..ids.len().min(8)]
10743 );
10744 }
10745 }
10746 }
10747 }
10748 };
10749 let (_nkv, _hd, _rd, eps) = (
10750 self.num_kv_heads,
10751 self.head_dim,
10752 self.rotary_dim,
10753 self.rms_eps,
10754 );
10755 let pool = self.pool.clone();
10756 let norm_style = self.norm_style;
10757 self.mimo_moe_prepare();
10758 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10759
10760 #[cfg(target_os = "macos")]
10761 let mut chunk_skip_until = 0usize;
10762 for li in from..upto_excl {
10763 let _capacity_tail = automatic_gpu_prefix
10764 .filter(|&prefix| {
10765 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10766 })
10767 .map(|_| crate::gpu::enter_cpu_scope());
10768 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10773 let _fast_gemm = self
10774 .proj_gate_sigmoid
10775 .then(crate::gpu::enter_prefill_fast_gemm);
10776 #[cfg(target_os = "macos")]
10782 if task_mask.is_none() {
10783 if li < chunk_skip_until {
10784 continue;
10785 }
10786 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10792 fill_h(&mut h, self);
10793 h_ready = true;
10794 }
10795 let ids_for_embed = match input {
10796 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10797 PrefillIn::Hidden(_) => None,
10798 };
10799 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10800 if end > li {
10801 h_ready = true;
10802 chunk_skip_until = end;
10803 if self.is_loop_end(end - 1) && end < self.num_layers {
10806 for bi in 0..b {
10807 let normed = inference::rms_norm(
10808 &h[bi * hs..(bi + 1) * hs],
10809 &self.weights.final_norm,
10810 eps,
10811 norm_style,
10812 );
10813 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10814 }
10815 }
10816 continue;
10817 }
10818 }
10819 if !h_ready {
10820 fill_h(&mut h, self);
10821 h_ready = true;
10822 }
10823 if task_mask.is_none() && self.verify_exact_moe {
10824 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10825 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10826 crate::gpu::BatchGraphOutcome::Completed => continue,
10827 crate::gpu::BatchGraphOutcome::Failed => return h,
10828 crate::gpu::BatchGraphOutcome::Declined => {}
10829 }
10830 }
10831 #[cfg(feature = "gpu")]
10832 self.pull_lagging_host_kv(li, li + 1, start_pos);
10833 let lw = &self.weights.layers[self.phys_layer(li)];
10834 let t_attn = std::time::Instant::now();
10835 match &lw.attn {
10837 AttnKind::Kda(w) => {
10838 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10840 let mut normed = vec![0.0f32; b * hs];
10841 for bi in 0..b {
10842 inference::rms_norm_into(
10843 &h[bi * hs..(bi + 1) * hs],
10844 &lw.input_norm,
10845 eps,
10846 norm_style,
10847 &mut normed[bi * hs..(bi + 1) * hs],
10848 );
10849 }
10850 let attn = crate::linear_core::kda_forward_batch(
10851 &normed,
10852 b,
10853 w,
10854 &cfg,
10855 &mut self.kv_cache.layers[li].linear_state,
10856 pool.as_deref(),
10857 );
10858 for (dst, &a) in h.iter_mut().zip(&attn) {
10859 *dst += a;
10860 }
10861 }
10862 AttnKind::LinearGdn(w) => {
10863 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10865 let mut normed = vec![0.0f32; b * hs];
10866 for bi in 0..b {
10867 let r = inference::rms_norm(
10868 &h[bi * hs..(bi + 1) * hs],
10869 &lw.input_norm,
10870 eps,
10871 norm_style,
10872 );
10873 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10874 }
10875 let attn = crate::linear_core::gdn_forward_batch(
10876 &normed,
10877 b,
10878 w,
10879 &cfg,
10880 &mut self.kv_cache.layers[li].linear_state,
10881 pool.as_deref(),
10882 );
10883 for (dst, &a) in h.iter_mut().zip(&attn) {
10884 *dst += a;
10885 }
10886 }
10887 AttnKind::ShortConv(w) => {
10888 let cfg = self
10891 .short_conv_cfg
10892 .expect("short-conv layer without short_conv_cfg");
10893 let mut normed = vec![0.0f32; b * hs];
10894 for bi in 0..b {
10895 inference::rms_norm_into(
10896 &h[bi * hs..(bi + 1) * hs],
10897 &lw.input_norm,
10898 eps,
10899 norm_style,
10900 &mut normed[bi * hs..(bi + 1) * hs],
10901 );
10902 }
10903 let mut attn = short_conv_forward_batch(
10904 &normed,
10905 b,
10906 w,
10907 &cfg,
10908 &mut self.kv_cache.layers[li].linear_state,
10909 pool.as_deref(),
10910 );
10911 if let Some(wn) = &lw.attn_out_norm {
10914 for bi in 0..b {
10915 inference::rms_norm_into(
10916 &attn[bi * hs..(bi + 1) * hs],
10917 wn,
10918 eps,
10919 norm_style,
10920 &mut normed[bi * hs..(bi + 1) * hs],
10921 );
10922 }
10923 attn.copy_from_slice(&normed);
10924 }
10925 for (dst, &a) in h.iter_mut().zip(&attn) {
10926 *dst += a;
10927 }
10928 }
10929 AttnKind::Mla(w) => {
10930 let inv_freq_l = self.layer_inv_freq(li);
10933 let rs = self.layer_rope_scale(li);
10934 let mut normed = vec![0.0f32; hs];
10935 for bi in 0..b {
10936 inference::rms_norm_into(
10937 &h[bi * hs..(bi + 1) * hs],
10938 &lw.input_norm,
10939 eps,
10940 norm_style,
10941 &mut normed,
10942 );
10943 let ao = mla_attention(
10944 w,
10945 &normed,
10946 &mut self.kv_cache.layers[li],
10947 start_pos + bi,
10948 &inv_freq_l,
10949 rs,
10950 eps,
10951 pool.as_deref(),
10952 );
10953 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10954 *dst += a;
10955 }
10956 }
10957 }
10958 AttnKind::Full {
10959 wq,
10960 wk,
10961 wv,
10962 wo,
10963 q_norm,
10964 k_norm,
10965 output_gate,
10966 softplus_gate,
10967 bias,
10968 } => {
10969 let mut normed = vec![0.0f32; b * hs];
10973 for bi in 0..b {
10974 inference::rms_norm_into(
10975 &h[bi * hs..(bi + 1) * hs],
10976 &lw.input_norm,
10977 eps,
10978 norm_style,
10979 &mut normed[bi * hs..(bi + 1) * hs],
10980 );
10981 }
10982 let inv_freq_l = self.layer_inv_freq(li);
10983 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10984 let cfg = QwenAttnCfg {
10985 num_heads: self.layer_num_heads(li),
10986 num_kv_heads: nkv_l,
10987 head_dim: hd_l,
10988 hidden_size: hs,
10989 position: start_pos,
10990 inv_freq: &inv_freq_l,
10991 rotary_dim: rd_l,
10992 scale: self.attn_scale,
10993 softcap: self.attn_softcap,
10994 window: self.layer_window(li),
10995 v_norm: self.attn_v_norm,
10996 qk_norm_after_rope: self.qk_norm_after_rope,
10997 gate_sigmoid: self.proj_gate_sigmoid,
10998 q_norm: q_norm.as_deref(),
10999 k_norm: k_norm.as_deref(),
11000 output_gate: *output_gate,
11001 softplus_gate: softplus_gate
11002 .as_ref()
11003 .map(|(gate, per_head)| (gate, *per_head)),
11004 rope_scale: self.layer_rope_scale(li),
11005 bias: bias
11006 .as_ref()
11007 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11008 rms_eps: eps,
11009 norm_style,
11010 pool: pool.as_deref(),
11011 v_head_dim: self.layer_v_dim(li),
11012 };
11013 #[cfg(feature = "gpu")]
11014 let _mirror = self
11015 .prefill_mirror_target(li)
11016 .map(crate::gpu::enter_prefill_mirror);
11017 let mut attn = attention::qwen_attention_batch(
11018 &normed,
11019 b,
11020 wq,
11021 wk,
11022 wv,
11023 wo,
11024 &mut self.kv_cache.layers[li],
11025 &cfg,
11026 );
11027 if let Some(w) = &lw.attn_out_norm {
11028 for bi in 0..b {
11029 inference::rms_norm_into(
11030 &attn[bi * hs..(bi + 1) * hs],
11031 w,
11032 eps,
11033 norm_style,
11034 &mut normed[bi * hs..(bi + 1) * hs],
11035 );
11036 }
11037 attn.copy_from_slice(&normed);
11038 }
11039 for (dst, &a) in h.iter_mut().zip(&attn) {
11040 *dst += a;
11041 }
11042 }
11043 AttnKind::Bounded(w) => {
11044 let mut normed = vec![0.0f32; b * hs];
11047 for bi in 0..b {
11048 inference::rms_norm_into(
11049 &h[bi * hs..(bi + 1) * hs],
11050 &lw.input_norm,
11051 eps,
11052 norm_style,
11053 &mut normed[bi * hs..(bi + 1) * hs],
11054 );
11055 }
11056 let rope = self
11057 .bounded_rope
11058 .clone()
11059 .expect("bounded layer without an installed rotation table");
11060 let cfg = crate::bounded::BoundedAttnCfg {
11061 num_heads: self.num_heads,
11062 num_kv_heads: self.num_kv_heads,
11063 head_dim: self.head_dim,
11064 hidden_size: hs,
11065 scale: self.attn_scale,
11066 rope: &rope,
11067 pool: pool.as_deref(),
11068 };
11069 let mut attn = crate::bounded::bounded_attention_batch(
11070 &normed,
11071 b,
11072 w,
11073 &mut self.kv_cache.layers[li],
11074 &cfg,
11075 );
11076 if let Some(wn) = &lw.attn_out_norm {
11077 for bi in 0..b {
11078 inference::rms_norm_into(
11079 &attn[bi * hs..(bi + 1) * hs],
11080 wn,
11081 eps,
11082 norm_style,
11083 &mut normed[bi * hs..(bi + 1) * hs],
11084 );
11085 }
11086 attn.copy_from_slice(&normed);
11087 }
11088 for (dst, &a) in h.iter_mut().zip(&attn) {
11089 *dst += a;
11090 }
11091 attention::recycle_buf(&mut attn);
11092 }
11093 AttnKind::Linear(w) => {
11094 for bi in 0..b {
11095 let normed = inference::rms_norm(
11096 &h[bi * hs..(bi + 1) * hs],
11097 &lw.input_norm,
11098 eps,
11099 norm_style,
11100 );
11101 vmf_phase_forward(
11102 &normed,
11103 w,
11104 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11105 &mut self.kv_cache.layers[li].linear_state,
11106 pool.as_deref(),
11107 )
11108 .iter()
11109 .enumerate()
11110 .for_each(|(i, &a)| h[bi * hs + i] += a);
11111 }
11112 }
11113 }
11114
11115 let lw = &self.weights.layers[self.phys_layer(li)];
11117 let mut post = vec![0.0f32; b * hs];
11118 for bi in 0..b {
11119 let r =
11120 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11121 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11122 }
11123 let mask_row = task_mask
11126 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11127 .and_then(|m| m.ffn_masks.get(li))
11128 .map(|v| v.as_slice());
11129 let attn_ns = t_attn.elapsed().as_nanos() as u64;
11130 let t_ffn = std::time::Instant::now();
11131 let mut ffn = match &lw.ffn {
11132 FfnKind::Dense(d) if !d.segs.is_empty() => {
11133 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11134 }
11135 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11136 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11137 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11138 }
11139 FfnKind::Moe(m) if self.verify_exact_moe => {
11140 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11141 }
11142 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11145 let before = m.stats.borrow().clone();
11146 let out = crate::gpu::cpu_scope(|| {
11147 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11148 });
11149 self.mimo_moe.prime(li, m, &before);
11150 out
11151 }
11152 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11153 FfnKind::DenseMoe(dm) => {
11156 let mut out = vec![0.0f32; b * hs];
11157 for bi in 0..b {
11158 let r = dense_moe_ffn(
11159 dm,
11160 &post[bi * hs..(bi + 1) * hs],
11161 &h[bi * hs..(bi + 1) * hs],
11162 eps,
11163 norm_style,
11164 pool.as_deref(),
11165 );
11166 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11167 }
11168 out
11169 }
11170 };
11171 if prefill_prof_on() {
11172 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11173 PREFILL_SPLIT[1].fetch_add(
11174 t_ffn.elapsed().as_nanos() as u64,
11175 std::sync::atomic::Ordering::Relaxed,
11176 );
11177 }
11178 if let Some(w) = &lw.ffn_out_norm {
11179 for bi in 0..b {
11180 inference::rms_norm_into(
11181 &ffn[bi * hs..(bi + 1) * hs],
11182 w,
11183 eps,
11184 norm_style,
11185 &mut post[bi * hs..(bi + 1) * hs],
11186 );
11187 }
11188 ffn.copy_from_slice(&post);
11189 }
11190 for (dst, &f) in h.iter_mut().zip(&ffn) {
11191 *dst += f;
11192 }
11193 if let Some(sc) = lw.layer_scale {
11194 for v in h.iter_mut() {
11195 *v *= sc;
11196 }
11197 }
11198 if self.layer_dump.is_some() {
11200 for bi in 0..b {
11201 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11202 }
11203 }
11204 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11205 if let Ok(t) = tp.parse::<usize>() {
11206 if t >= start_pos && t < start_pos + b {
11207 let bi = t - start_pos;
11208 let row = &h[bi * hs..(bi + 1) * hs];
11209 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11210 eprintln!(
11211 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11212 row[0], row[1]
11213 );
11214 }
11215 }
11216 }
11217 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11221 let row = &h[(b - 1) * hs..b * hs];
11222 let rms =
11223 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11224 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11225 eprintln!(
11226 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11227 match &self.weights.layers[self.phys_layer(li)].attn {
11228 AttnKind::LinearGdn(_) => "gdn",
11229 AttnKind::Linear(_) => "vmf",
11230 AttnKind::ShortConv(_) => "conv",
11231 _ => "attn",
11232 },
11233 match &lw.ffn {
11234 FfnKind::Moe(_) => "moe",
11235 FfnKind::Dense(_) => "dense",
11236 FfnKind::DenseMoe(_) => "dense+moe",
11237 },
11238 );
11239 }
11240 if self.is_loop_end(li) && li + 1 < self.num_layers {
11242 for bi in 0..b {
11243 let normed = inference::rms_norm(
11244 &h[bi * hs..(bi + 1) * hs],
11245 &self.weights.final_norm,
11246 eps,
11247 norm_style,
11248 );
11249 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11250 }
11251 }
11252 if std::env::var("CMF_TRACE_H").is_ok() {
11253 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11254 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11255 eprintln!(
11256 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11257 lw.layer_scale
11258 );
11259 }
11260 }
11261 crate::gpu::set_layer(-1); if prefill_prof_on() {
11263 let ms = |a: &std::sync::atomic::AtomicU64| {
11264 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11265 };
11266 let sp = &attention::ATTN_SPLIT;
11267 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11268 eprintln!(
11269 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11270 host gate {:.1}], host loop {:.1}, attend {:.1} \
11271 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11272 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11273 ms(&PREFILL_SPLIT[0]),
11274 ms(&sp[0]),
11275 qkv_enq,
11276 qkv_wait,
11277 ms(&sp[6]),
11278 ms(&sp[1]),
11279 ms(&sp[2]),
11280 ms(&sp[4]),
11281 ms(&sp[5]),
11282 ca_wait,
11283 ms(&sp[3]),
11284 ms(&PREFILL_SPLIT[1]),
11285 );
11286 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11287 eprintln!(
11288 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11289 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11290 fallbacks (cumulative)",
11291 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11292 );
11293 }
11294 self.o1_progress();
11299 self.swa_trim_tails();
11301 h
11302 }
11303
11304 fn embed_single(&self, id: u32) -> Vec<f32> {
11306 let mut out = vec![0.0f32; self.hidden_size];
11307 if (id as usize) < self.weights.embed_tokens.rows() {
11308 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11309 }
11310 if self.embed_multiplier != 1.0 {
11311 for v in out.iter_mut() {
11312 *v *= self.embed_multiplier;
11313 }
11314 }
11315 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11319 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11320 v[0] = id as f32;
11321 return v;
11322 }
11323 if let Some(b) = &self.g4e {
11327 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11328 }
11329 if let Some(b) = &self.g3n {
11330 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11331 }
11332 out
11333 }
11334
11335 #[cfg(target_os = "macos")]
11341 fn chunk_run_gpu(
11342 &mut self,
11343 li0: usize,
11344 h: &mut [f32],
11345 b: usize,
11346 pos0: usize,
11347 embed_ids: Option<&[u32]>,
11348 cap: usize,
11349 ) -> usize {
11350 let gemma4 =
11357 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11358 if !crate::gpu::enabled_here()
11359 || std::env::var("CMF_GPU_CHUNK")
11360 .map(|v| v == "0")
11361 .unwrap_or(false)
11362 || b < self.chunk_min_rows
11363 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11366 || (self.global_attn.is_some() && !gemma4)
11367 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11371 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11374 || self.o1_active()
11377 || (self.attn_v_norm && !gemma4)
11378 || !self.metal_rope_scale_ok()
11379 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11380 {
11381 return li0;
11382 }
11383 let Some(model) = self.model.clone() else {
11384 return li0;
11385 };
11386 let (nh, nkv, hd, hs) = (
11387 self.num_heads,
11388 self.num_kv_heads,
11389 self.head_dim,
11390 self.hidden_size,
11391 );
11392 let loop_end = if self.loop_final_norm {
11396 ((li0 / self.physical_layers) + 1) * self.physical_layers
11397 } else {
11398 self.num_layers
11399 };
11400 let run_end = self.num_layers.min(loop_end).min(cap);
11401 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11406 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11407 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11408 let mut stored_at: Vec<usize> = Vec::new();
11409 for li in li0..run_end {
11410 let lw = &self.weights.layers[self.phys_layer(li)];
11411 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11412 && !gemma4
11413 {
11414 break;
11415 }
11416 let AttnKind::Full {
11417 wq,
11418 wk,
11419 wv,
11420 wo,
11421 q_norm,
11422 k_norm,
11423 output_gate: false,
11424 softplus_gate,
11425 bias,
11426 } = &lw.attn
11427 else {
11428 break;
11429 };
11430 let head_gate = match softplus_gate {
11432 None => None,
11433 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11434 Some((d, r, c)) if r == nh && c == hs => Some(d),
11435 _ => break,
11436 },
11437 Some(_) => break,
11438 };
11439 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11443 FfnKind::Dense(d)
11444 if (matches!(d.act, Act::Silu | Act::Gelu)
11445 || (gemma4 && d.act == Act::GeluTanh))
11446 && d.segs.is_empty() =>
11447 {
11448 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11449 }
11450 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11451 Some((router, experts, q8)) => {
11452 let e0 = &m.experts[0];
11453 let route = Box::new(move |lg: &[f32]| {
11454 let (idx, p, wsum) = moe_route(lg, m, None);
11456 let mut st = m.stats.borrow_mut();
11457 if st.len() < m.experts.len() {
11458 st.resize(m.experts.len(), 0);
11459 }
11460 for &e in &idx {
11461 st[e] += 1;
11462 }
11463 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11464 (idx, w)
11465 });
11466 (
11467 &e0.gate_proj,
11468 &e0.up_proj,
11469 &e0.down_proj,
11470 Act::Silu,
11471 Some(crate::gpu_metal::ChunkMoe {
11472 router,
11473 experts,
11474 q8,
11475 route,
11476 }),
11477 )
11478 }
11479 None => break,
11480 },
11481 _ => break,
11482 };
11483 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11489 t.q8_row_parts()
11490 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11491 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11492 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11493 }
11494 fn cf(t: &QTensor) -> &[f32] {
11495 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11496 }
11497 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11498 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
11499 if moe.is_none()
11509 && !gemma4
11510 && ffn_act != Act::Gelu
11511 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
11512 {
11513 break;
11514 }
11515 let parts = (
11516 cw(wq),
11517 cw(wk),
11518 cw(wv),
11519 cw(wo),
11520 cw(gate_t),
11521 cw(up_t),
11522 cw(down_t),
11523 );
11524 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11525 else {
11526 break;
11527 };
11528 let layer = &self.kv_cache.layers[li];
11529 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11530 break;
11531 }
11532 debug_assert!(
11534 layer.base() == 0
11535 || self
11536 .layer_window(li)
11537 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11538 );
11539 stored_at.push(layer.head_len(0));
11540 let (nkv, hd) = if gemma4 {
11543 let (a, bb, _) = self.layer_geom(li);
11544 (a, bb)
11545 } else {
11546 (nkv, hd)
11547 };
11548 layers.push(crate::gpu_metal::ChunkLayer {
11549 model: &model,
11550 kv_id: self.graph_kv_id,
11551 layer: li,
11552 wq: pq,
11553 wk: pk,
11554 wv: pv,
11555 wo: po,
11556 gate: pg,
11557 up: pu,
11558 down: pd,
11559 input_norm: &lw.input_norm,
11560 post_norm: &lw.post_norm,
11561 bias: bias
11562 .as_ref()
11563 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11564 q_norm: q_norm.as_deref(),
11565 k_norm: k_norm.as_deref(),
11566 inv_freq: &tables[li - li0],
11567 rope_scale: self.layer_rope_scale(li),
11568 rd: self.layer_geom(li).2,
11569 nh,
11570 nkv,
11571 hd,
11572 hs,
11573 inter: gate_t.rows(),
11574 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11575 late_qk_norm: self.qk_norm_after_rope,
11576 eps: self.rms_eps as f32,
11577 window: self.layer_window(li),
11578 head_gate,
11579 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11580 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11581 v_norm: self.attn_v_norm,
11582 scale: self.attn_scale,
11583 attn_out_norm: lw.attn_out_norm.as_deref(),
11584 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11585 layer_scale: lw.layer_scale,
11586 }),
11587 col,
11588 ffn_col: if moe.is_none() && !gemma4 {
11591 ffn_col
11592 } else {
11593 [&[][..]; 3]
11594 },
11595 moe,
11596 });
11597 }
11598 if layers.is_empty() {
11599 return li0;
11600 }
11601 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11603 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11604 .iter()
11605 .zip(&rows_kv)
11606 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11607 .collect();
11608 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11609 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11610 let li = layers[i].layer;
11611 let layer = &self.kv_cache.layers[li];
11612 let nkv = rows_kv[i].0;
11613 io.push(crate::gpu_metal::ChunkIo {
11614 cpu_stored: stored_at[i],
11615 cpu_gen: layer.generation(),
11616 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11617 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11618 out_k: ok,
11619 out_v: ov,
11620 imp: oi,
11621 });
11622 }
11623 let n_run = layers.len();
11624 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11625 let ep = embed_ids.and_then(|ids| {
11628 self.weights
11629 .embed_tokens
11630 .q8_row_parts()
11631 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11632 idx,
11633 rows,
11634 row_scale: rs,
11635 ids,
11636 mult: self.embed_multiplier,
11637 })
11638 });
11639 if embed_ids.is_some() && ep.is_none() {
11640 return li0;
11641 }
11642 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11643 return li0;
11644 }
11645 drop(io);
11646 drop(layers);
11647 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11650 let li = li0 + i;
11651 let row = rows_kv[i].1;
11652 let layer = &mut self.kv_cache.layers[li];
11653 for bi in 0..b {
11654 layer.append(
11655 &ok[bi * row..(bi + 1) * row],
11656 &ov[bi * row..(bi + 1) * row],
11657 &[],
11658 );
11659 }
11660 layer.accumulate_imp(oi);
11661 }
11662 last
11663 }
11664
11665 fn layer_is_local(&self, li: usize) -> bool {
11668 if let Some(layers) = &self.sliding_layers {
11669 return layers.get(li).copied().unwrap_or(false);
11670 }
11671 match self.swa {
11672 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11673 None => false,
11674 }
11675 }
11676
11677 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11680 if self.layer_is_local(li) {
11681 if let Some(f) = &self.inv_freq_local {
11682 return f.clone();
11683 }
11684 } else if let Some(f) = &self.inv_freq_global {
11685 return f.clone();
11686 }
11687 self.inv_freq.clone()
11688 }
11689
11690 fn layer_window(&self, li: usize) -> Option<usize> {
11692 self.swa
11693 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11694 }
11695
11696 fn swa_trim_tails(&mut self) {
11705 let Some((slack, align)) = self.swa_trim else {
11706 return;
11707 };
11708 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11709 return;
11710 }
11711 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11712 if let Some(w) = self.layer_window(li) {
11713 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
11714 }
11715 }
11716 }
11717
11718 fn layer_num_heads(&self, li: usize) -> usize {
11719 self.attention_heads_per_layer
11720 .as_ref()
11721 .and_then(|v| v.get(li).copied())
11722 .unwrap_or(self.num_heads)
11723 }
11724
11725 fn layer_rope_scale(&self, li: usize) -> f32 {
11726 if self.layer_is_local(li) {
11727 self.rope_scale_local
11728 } else {
11729 self.rope_scale
11730 }
11731 }
11732
11733 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11736 if !self.layer_is_local(li) {
11737 if let Some((ghd, gkv)) = self.global_attn {
11738 return (gkv, ghd, ghd);
11739 }
11740 }
11741 let rd = if self.layer_is_local(li) {
11742 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11743 } else {
11744 self.rotary_dim
11745 };
11746 let rd = if self
11748 .no_rope_layers
11749 .as_ref()
11750 .and_then(|v| v.get(li))
11751 .copied()
11752 == Some(true)
11753 {
11754 0
11755 } else {
11756 rd
11757 };
11758 (self.layer_num_kv_heads(li), self.head_dim, rd)
11759 }
11760
11761 fn layer_num_kv_heads(&self, li: usize) -> usize {
11764 self.kv_heads_per_layer
11765 .as_ref()
11766 .and_then(|v| v.get(self.phys_layer(li)).copied())
11767 .unwrap_or(self.num_kv_heads)
11768 }
11769
11770 fn layer_v_dim(&self, li: usize) -> usize {
11772 let (_, hd, _) = self.layer_geom(li);
11773 self.v_head_dim.unwrap_or(hd).min(hd)
11774 }
11775
11776 pub fn set_attn_geometry(
11785 &mut self,
11786 kv_heads_per_layer: Option<Vec<usize>>,
11787 v_head_dim: Option<usize>,
11788 ) -> Result<(), String> {
11789 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11790 if self.global_attn.is_some() {
11791 return Err(
11792 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11793 attention geometry"
11794 .into(),
11795 );
11796 }
11797 if self
11798 .weights
11799 .layers
11800 .iter()
11801 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11802 {
11803 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11804 }
11805 }
11806 if let Some(vd) = v_head_dim {
11807 if vd == 0 || vd > self.head_dim {
11808 return Err(format!(
11809 "v_head_dim {vd} must be in 1..={} (head_dim)",
11810 self.head_dim
11811 ));
11812 }
11813 }
11814 if let Some(v) = &kv_heads_per_layer {
11815 if v.len() != self.physical_layers {
11816 return Err(format!(
11817 "kv_heads_per_layer has {} entries, expected {} layers",
11818 v.len(),
11819 self.physical_layers
11820 ));
11821 }
11822 for (li, &nkv) in v.iter().enumerate() {
11823 let is_attn = matches!(
11824 self.weights.layers.get(li).map(|lw| &lw.attn),
11825 Some(AttnKind::Full { .. }) | None
11826 );
11827 if !is_attn {
11828 continue;
11829 }
11830 let nh = self
11831 .attention_heads_per_layer
11832 .as_ref()
11833 .and_then(|h| h.get(li).copied())
11834 .unwrap_or(self.num_heads);
11835 if nkv == 0 || nh % nkv != 0 {
11836 return Err(format!(
11837 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11838 ));
11839 }
11840 }
11841 }
11842 self.kv_heads_per_layer = kv_heads_per_layer;
11843 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11844 if self.kv_heads_per_layer.is_some() {
11845 for li in 0..self.kv_cache.layers.len() {
11846 let full = matches!(
11847 self.weights
11848 .layers
11849 .get(self.phys_layer(li))
11850 .map(|lw| &lw.attn),
11851 Some(AttnKind::Full { .. })
11852 );
11853 let nkv = self.layer_num_kv_heads(li);
11854 let cache = &self.kv_cache.layers[li];
11855 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11856 let sinks = cache.sinks.clone();
11857 self.kv_cache.layers[li] =
11858 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11859 self.kv_cache.layers[li].sinks = sinks;
11860 }
11861 }
11862 }
11863 Ok(())
11864 }
11865
11866 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11870 let Some(lw) = self.weights.layers.get(phys) else {
11871 return Err(format!("sinks for layer {phys}: no such layer"));
11872 };
11873 if !matches!(lw.attn, AttnKind::Full { .. }) {
11874 return Err(format!(
11875 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11876 ));
11877 }
11878 let nh = self
11879 .attention_heads_per_layer
11880 .as_ref()
11881 .and_then(|h| h.get(phys).copied())
11882 .unwrap_or(self.num_heads);
11883 if sinks.len() != nh {
11884 return Err(format!(
11885 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11886 sinks.len()
11887 ));
11888 }
11889 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11890 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11891 }
11892 for li in 0..self.kv_cache.layers.len() {
11893 if self.phys_layer(li) == phys {
11894 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11895 }
11896 }
11897 Ok(())
11898 }
11899
11900 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11910 if self.kv_heads_per_layer.is_some() {
11911 return Some("per-layer KV head counts");
11912 }
11913 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11914 return Some("V heads narrower than Q/K heads");
11915 }
11916 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11917 return Some("learned attention sinks");
11918 }
11919 if self.swa.is_some() || self.sliding_layers.is_some() {
11920 return Some("sliding-window layers");
11921 }
11922 None
11923 }
11924
11925 #[cfg(target_os = "macos")]
11933 fn metal_rope_scale_ok(&self) -> bool {
11934 !(self.qk_norm_after_rope
11935 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11936 }
11937
11938 #[cfg(target_os = "macos")]
11955 fn metal_graph_swa(&self) -> bool {
11956 (self.swa.is_some() || self.sliding_layers.is_some())
11957 && self.attention_heads_per_layer.is_none()
11958 && !self.attn_v_norm
11959 && self.attn_softcap == 0.0
11960 && self.kv_heads_per_layer.is_none()
11961 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11962 && self.global_attn.is_none()
11963 && self.inv_freq_global.is_none()
11964 && self.metal_rope_scale_ok()
11965 && !(0..self.num_layers).any(|li| {
11966 self.layer_is_local(li)
11967 && self.inv_freq_local.is_none()
11968 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11969 })
11970 }
11971
11972 #[cfg(target_os = "macos")]
11980 fn metal_gemma4(&self) -> bool {
11981 crate::gpu_metal::gemma4::enabled()
11982 && (self.global_attn.is_some() || self.attn_v_norm)
11983 && self.attention_heads_per_layer.is_none()
11984 && self.kv_heads_per_layer.is_none()
11985 && self.v_head_dim.is_none()
11986 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11987 && self.attn_softcap == 0.0
11988 && !self.qk_norm_after_rope
11989 && self.metal_rope_scale_ok()
11990 && self
11991 .weights
11992 .layers
11993 .iter()
11994 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11995 && !(0..self.num_layers).any(|li| {
11996 self.layer_is_local(li)
11997 && self.inv_freq_local.is_none()
11998 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11999 })
12000 }
12001
12002 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
12009 let geometry_reason = self.graph_attn_decline_reason();
12013 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
12014 if has_scaled_rope && geometry_reason.is_none() {
12015 return Some("scaled RoPE without per-layer geometry");
12016 }
12017 if has_scaled_rope && self.qk_norm_after_rope {
12022 return Some("scaled RoPE with post-RoPE Q/K norm");
12023 }
12024 geometry_reason?;
12025 if self.attention_heads_per_layer.is_some() {
12031 return Some("per-layer Q head counts with per-layer geometry");
12032 }
12033 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
12038 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
12039 if !gemma4
12040 && self.weights.layers.iter().any(|lw| {
12041 lw.layer_scale.is_some()
12042 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
12043 })
12044 {
12045 return Some("sandwich norms / layer scale with per-layer geometry");
12046 }
12047 if self.weights.layers.iter().any(|lw| {
12051 matches!(
12052 &lw.attn,
12053 AttnKind::Full {
12054 output_gate: true,
12055 ..
12056 }
12057 )
12058 }) && self.v_head_dim.is_some()
12059 {
12060 return Some("gated attention with V narrower than K");
12061 }
12062 if (0..self.num_layers).any(|li| {
12063 self.layer_is_local(li)
12064 && self.inv_freq_local.is_none()
12065 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12066 }) {
12067 return Some("local rotary width without a local RoPE table");
12068 }
12069 None
12070 }
12071
12072 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
12076 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
12079 return None;
12080 }
12081 if self.global_attn.is_some() {
12082 return Some("per-layer head width (Gemma-4 global layers)");
12083 }
12084 if self.attn_v_norm {
12085 return Some("V norm");
12086 }
12087 if self.weights.layers.iter().any(|lw| {
12088 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12089 }) {
12090 return Some("sandwich norms / layer scale");
12091 }
12092 None
12093 }
12094
12095 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
12100 self.graph_attn_decline_reason()?;
12101 let (nkv, hd, rd) = self.layer_geom(li);
12102 let invf: &[f32] = if self.layer_is_local(li) {
12103 match &self.inv_freq_local {
12104 Some(f) => f.as_slice(),
12105 None => self.inv_freq.as_slice(),
12106 }
12107 } else {
12108 match &self.inv_freq_global {
12109 Some(f) => f.as_slice(),
12110 None => self.inv_freq.as_slice(),
12111 }
12112 };
12113 Some(crate::gpu::GraphAttnGeom {
12114 nkv,
12115 hd,
12116 dv: self.layer_v_dim(li),
12117 rd,
12118 invf,
12119 rope_scale: self.layer_rope_scale(li),
12120 window: self.layer_window(li),
12121 sink: self.kv_cache.layers[li].sinks.as_deref(),
12122 v_norm: self.attn_v_norm,
12123 })
12124 }
12125
12126 #[cfg(feature = "gpu")]
12138 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12139 if !self.proj_gate_sigmoid
12140 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12141 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12142 || self.graph_refused()
12143 || self.o1_active()
12144 || self.attn_softcap > 0.0
12145 || self.wgpu_graph_attn_decline().is_some()
12146 {
12147 return None;
12148 }
12149 let g = self.graph_attn_geom(li)?;
12150 let (nkv, hd, _) = self.layer_geom(li);
12151 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12153 return None;
12154 }
12155 Some(crate::gpu::PrefillMirror {
12156 kv_id: self.graph_kv_id,
12157 layer: li,
12158 limit: self.kv_cache.max_seq_len,
12159 })
12160 }
12161
12162 #[cfg(feature = "gpu")]
12171 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12172 let kv_id = self.graph_kv_id;
12173 for li in from..upto.min(self.num_layers) {
12174 if !matches!(
12175 self.weights.layers[self.phys_layer(li)].attn,
12176 AttnKind::Full { .. }
12177 ) {
12178 continue;
12179 }
12180 let host = self.kv_cache.layers[li].pos_len();
12182 if host >= position {
12183 continue;
12184 }
12185 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12186 continue;
12187 };
12188 let to = dev.min(position);
12189 if to <= host {
12190 continue;
12191 }
12192 let (nkv, hd) = {
12193 let c = &self.kv_cache.layers[li];
12194 (c.num_kv_heads, c.head_dim)
12195 };
12196 let Some((k, v, first_valid)) =
12197 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
12198 else {
12199 continue;
12200 };
12201 let need_from = match self.layer_window(li) {
12204 Some(w) => host.max((position + 1).saturating_sub(w)),
12205 None => host,
12206 };
12207 if first_valid > need_from {
12208 tracing::warn!(
12209 "layer {li}: device KV rows {host}..{to} no longer resident \
12210 (from {first_valid}); host attention will miss them"
12211 );
12212 }
12213 let row = nkv * hd;
12214 let cache = &mut self.kv_cache.layers[li];
12215 for p in 0..to - host {
12216 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12217 }
12218 }
12219 }
12220
12221 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12224 let mut seen = self.graph_declines.borrow_mut();
12225 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12226 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12227 seen.push((site, reason));
12228 }
12229 }
12230
12231 pub fn graph_declines(&self) -> Vec<String> {
12234 self.graph_declines
12235 .borrow()
12236 .iter()
12237 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12238 .collect()
12239 }
12240
12241 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12250 let Some(dir) = &self.layer_dump else {
12251 return;
12252 };
12253 let mut bytes = Vec::with_capacity(row.len() * 4);
12254 for v in row {
12255 bytes.extend_from_slice(&v.to_le_bytes());
12256 }
12257 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12258 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12259 use std::sync::atomic::{AtomicBool, Ordering};
12260 static SAID: AtomicBool = AtomicBool::new(false);
12261 if !SAID.swap(true, Ordering::Relaxed) {
12262 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12263 }
12264 }
12265 }
12266
12267 fn mimo_moe_prepare(&mut self) {
12270 if !self.mimo_moe.is_undecided() {
12271 return;
12272 }
12273 let slot = {
12274 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12275 .filter_map(
12276 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12277 FfnKind::Moe(m) => Some((li, m)),
12278 _ => None,
12279 },
12280 )
12281 .collect();
12282 if layers.is_empty()
12285 || self.physical_layers != self.num_layers
12286 || self.gpu_plan.is_some()
12287 {
12288 crate::mimo_moe::Slot::Off
12289 } else {
12290 let graph_prefix =
12294 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12295 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12296 }
12297 };
12298 self.mimo_moe = slot;
12299 }
12300
12301 #[cfg(test)]
12302 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12303 self.graph_kv_id
12304 }
12305
12306 pub(crate) fn mimo_graph_layer_rows(
12310 &mut self,
12311 li: usize,
12312 h: &mut [f32],
12313 positions: &[usize],
12314 ) -> crate::gpu::BatchGraphOutcome {
12315 use crate::gpu::BatchGraphOutcome as Out;
12316 let b = positions.len();
12317 if !(1..=4).contains(&b)
12318 || h.len() != b * self.hidden_size
12319 || !self.mimo_moe.is_dynamic(li, true)
12320 || !crate::gpu::enabled_here()
12321 || !crate::gpu::wgpu_active()
12322 || self.o1_active()
12323 || self.physical_layers != self.num_layers
12324 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12328 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12329 || self.wgpu_graph_attn_decline().is_some()
12330 {
12331 return Out::Declined;
12332 }
12333 let attn_started = std::time::Instant::now();
12334 let outcome = {
12335 let lw = &self.weights.layers[li];
12336 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12337 return Out::Declined;
12338 }
12339 let FfnKind::Moe(m) = &lw.ffn else {
12340 return Out::Declined;
12341 };
12342 let AttnKind::Full {
12343 wq,
12344 wk,
12345 wv,
12346 wo,
12347 q_norm,
12348 k_norm,
12349 output_gate,
12350 softplus_gate,
12351 bias,
12352 } = &lw.attn
12353 else {
12354 return Out::Declined;
12355 };
12356 if *output_gate || softplus_gate.is_some() {
12357 return Out::Declined;
12358 }
12359 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12360 m.experts
12361 .first()?
12362 .gate_proj
12363 .mapped_q4tp()
12364 .map(|(m, _)| m.clone())
12365 }) else {
12366 return Out::Declined;
12367 };
12368 fn gw<'a>(
12369 t: &'a QTensor,
12370 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12371 ) -> Option<crate::gpu::GraphW<'a>> {
12372 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12373 if m.uid() != owner.uid() || t.has_prism_contract() {
12374 return None;
12375 }
12376 return Some(crate::gpu::GraphW {
12377 idx,
12378 kind,
12379 row_scale: rs,
12380 data: &[],
12381 prism: crate::gpu::GraphPrismOp::None,
12382 affine: false,
12383 });
12384 }
12385 t.as_f32().map(|data| crate::gpu::GraphW {
12386 idx: 0,
12387 kind: 4,
12388 row_scale: &[],
12389 data,
12390 prism: crate::gpu::GraphPrismOp::None,
12391 affine: false,
12392 })
12393 }
12394 let (Some(q), Some(k), Some(v), Some(o)) = (
12395 gw(wq, &model),
12396 gw(wk, &model),
12397 gw(wv, &model),
12398 gw(wo, &model),
12399 ) else {
12400 return Out::Declined;
12401 };
12402 let layer = crate::gpu::GraphLayer {
12403 input_norm: &lw.input_norm,
12404 post_norm: &lw.post_norm,
12405 attn_out_norm: None,
12406 ffn_out_norm: None,
12407 layer_scale: None,
12408 ffn: crate::gpu::GraphFfn::AttentionOnly,
12409 attn: crate::gpu::GraphAttn::Full {
12410 wq: q,
12411 wk: k,
12412 wv: v,
12413 wo: o,
12414 q_norm: q_norm.as_deref(),
12415 k_norm: k_norm.as_deref(),
12416 late_qk_norm: self.qk_norm_after_rope,
12417 bias: bias
12418 .as_ref()
12419 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12420 output_gate: false,
12421 cpu_k: self.kv_cache.layers[li].k_heads(),
12422 cpu_v: self.kv_cache.layers[li].v_heads(),
12423 cpu_base: self.kv_cache.layers[li].base(),
12424 geom: self.graph_attn_geom(li),
12425 head_gate: None,
12426 },
12427 };
12428 let (nkv, hd, rd) = self.layer_geom(li);
12429 crate::gpu::forward_batch_graph_at(
12430 &model,
12431 self.graph_kv_id,
12432 li,
12433 &[layer],
12434 &self.inv_freq,
12435 h,
12436 self.layer_num_heads(li),
12437 nkv,
12438 hd,
12439 rd,
12440 self.hidden_size,
12441 1,
12442 positions,
12443 self.kv_cache.max_seq_len,
12444 self.norm_style == cortiq_core::NormStyle::Gemma,
12445 self.rms_eps as f32,
12446 self.attn_scale,
12447 b,
12448 &[],
12449 self.o1_epoch,
12450 None,
12451 None,
12452 )
12453 };
12454 match outcome {
12455 Out::Completed => {}
12456 Out::Declined => return Out::Declined,
12457 Out::Failed => {
12458 self.graph_failed
12459 .store(true, std::sync::atomic::Ordering::Relaxed);
12460 return Out::Failed;
12461 }
12462 }
12463 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12464 let hs = self.hidden_size;
12465 let lw = &self.weights.layers[li];
12466 let FfnKind::Moe(m) = &lw.ffn else {
12467 unreachable!()
12468 };
12469 let mut post = vec![0.0; h.len()];
12470 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12471 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12472 }
12473 let mut ffn = if b == 1 {
12474 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12475 } else {
12476 moe_ffn_banked_rows(
12477 &mut self.mimo_moe,
12478 li,
12479 m,
12480 &post,
12481 b,
12482 hs,
12483 self.pool.as_deref(),
12484 )
12485 };
12486 for (x, &f) in h.iter_mut().zip(&ffn) {
12487 *x += f;
12488 }
12489 attention::recycle_buf(&mut ffn);
12490 if self.layer_dump.is_some() {
12491 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12492 self.dump_layer_row(pos, li, row);
12493 }
12494 }
12495 crate::mimo_moe::note_attention_graph(b, attn_ns);
12496 Out::Completed
12497 }
12498
12499 fn layer_attn_plain(&self, li: usize) -> bool {
12500 self.kv_heads_per_layer.is_none()
12501 && self.v_head_dim.is_none()
12502 && self.global_attn.is_none()
12503 && self.layer_window(li).is_none()
12504 && self.kv_cache.layers[li].sinks.is_none()
12505 }
12506
12507 fn forward_layers(
12509 &mut self,
12510 hidden: &[f32],
12511 position: usize,
12512 task_mask: Option<&TaskMask>,
12513 ) -> Vec<f32> {
12514 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12515 self.o1_progress();
12516 self.swa_trim_tails();
12517 out
12518 }
12519
12520 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12528 self.embed_single(id)
12529 }
12530
12531 pub fn split_supported(&self) -> Result<(), String> {
12535 if self.dsv4.is_some() {
12536 return Err(
12537 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12538 );
12539 }
12540 if self.dsv41.is_some() {
12541 return Err(
12542 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12543 .into(),
12544 );
12545 }
12546 if self.qwen4_exp.is_some() {
12547 return Err(
12548 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12549 );
12550 }
12551 if self.per_token_stack() {
12552 return Err(
12553 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12554 );
12555 }
12556 Ok(())
12557 }
12558
12559 pub fn forward_span(
12564 &mut self,
12565 hidden: &[f32],
12566 position: usize,
12567 from: usize,
12568 upto: usize,
12569 task_mask: Option<&TaskMask>,
12570 ) -> Result<Vec<f32>, String> {
12571 self.split_supported()?;
12572 if from > upto || upto >= self.num_layers {
12573 return Err(format!(
12574 "forward_span: layer range {from}..={upto} outside 0..{}",
12575 self.num_layers
12576 ));
12577 }
12578 if hidden.len() != self.hidden_size {
12579 return Err(format!(
12580 "forward_span: hidden len {} ≠ hidden_size {}",
12581 hidden.len(),
12582 self.hidden_size
12583 ));
12584 }
12585 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12586 self.o1_progress();
12587 self.swa_trim_tails();
12588 if self
12589 .graph_failed
12590 .swap(false, std::sync::atomic::Ordering::Relaxed)
12591 {
12592 self.cancel
12593 .store(false, std::sync::atomic::Ordering::Relaxed);
12594 self.clear_sequence_state();
12595 return Err("forward_span: deferred O(1) transition failed".into());
12596 }
12597 Ok(out)
12598 }
12599
12600 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12603 let normed = inference::rms_norm(
12604 hidden,
12605 &self.weights.final_norm,
12606 self.rms_eps,
12607 self.norm_style,
12608 );
12609 self.lm_head_forward(&normed)
12610 }
12611
12612 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12614 sampler::sample_with_scratch(
12615 logits,
12616 &self.sampler_config,
12617 past_tokens,
12618 &mut self.rng,
12619 &mut self.sampler_scratch,
12620 )
12621 }
12622
12623 pub fn reset_session(&mut self) {
12625 self.clear_sequence_state();
12626 }
12627
12628 pub fn prefill_span_ids(
12634 &mut self,
12635 ids: &[u32],
12636 start_pos: usize,
12637 upto: usize,
12638 task_mask: Option<&TaskMask>,
12639 ) -> Result<Vec<f32>, String> {
12640 self.split_supported()?;
12641 if upto >= self.num_layers {
12642 return Err(format!(
12643 "prefill_span_ids: upto {upto} outside 0..{}",
12644 self.num_layers
12645 ));
12646 }
12647 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12651 let out =
12652 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12653 self.check_o1_progress_failure("prefill_span_ids")?;
12654 Ok(out)
12655 } else {
12656 let hs = self.hidden_size;
12657 let mut out = Vec::with_capacity(ids.len() * hs);
12658 for (i, &id) in ids.iter().enumerate() {
12659 let emb = self.embed_id(id);
12660 out.extend_from_slice(&self.forward_span(
12661 &emb,
12662 start_pos + i,
12663 0,
12664 upto,
12665 task_mask,
12666 )?);
12667 }
12668 Ok(out)
12669 }
12670 }
12671
12672 pub fn prefill_span_hidden(
12675 &mut self,
12676 hidden: &[f32],
12677 start_pos: usize,
12678 from: usize,
12679 upto: usize,
12680 task_mask: Option<&TaskMask>,
12681 ) -> Result<Vec<f32>, String> {
12682 self.split_supported()?;
12683 let hs = self.hidden_size;
12684 if hidden.is_empty() || hidden.len() % hs != 0 {
12685 return Err(format!(
12686 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12687 hidden.len()
12688 ));
12689 }
12690 if from > upto || upto >= self.num_layers {
12691 return Err(format!(
12692 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12693 self.num_layers
12694 ));
12695 }
12696 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12697 let out = self.prefill_batch_span(
12698 PrefillIn::Hidden(hidden),
12699 start_pos,
12700 task_mask,
12701 from,
12702 upto + 1,
12703 );
12704 self.check_o1_progress_failure("prefill_span_hidden")?;
12705 Ok(out)
12706 } else {
12707 let b = hidden.len() / hs;
12708 let mut out = Vec::with_capacity(hidden.len());
12709 for i in 0..b {
12710 let h = self.forward_span(
12711 &hidden[i * hs..(i + 1) * hs],
12712 start_pos + i,
12713 from,
12714 upto,
12715 task_mask,
12716 )?;
12717 out.extend_from_slice(&h);
12718 }
12719 Ok(out)
12720 }
12721 }
12722
12723 fn try_token_graph_wgpu(
12727 &self,
12728 hidden: &[f32],
12729 position: usize,
12730 logits_out: &mut Vec<f32>,
12731 layers_run: &mut usize,
12732 ) -> Option<Result<Vec<f32>, ()>> {
12733 self.try_token_graph_wgpu_steps(
12734 hidden,
12735 position,
12736 logits_out,
12737 1,
12738 None,
12739 Some(layers_run),
12740 0,
12741 self.num_layers,
12742 )
12743 }
12744
12745 fn try_token_graph_wgpu_span(
12749 &self,
12750 hidden: &[f32],
12751 position: usize,
12752 logits_out: &mut Vec<f32>,
12753 from: usize,
12754 upto_excl: usize,
12755 layers_run: &mut usize,
12756 ) -> Option<Result<Vec<f32>, ()>> {
12757 self.try_token_graph_wgpu_steps(
12758 hidden,
12759 position,
12760 logits_out,
12761 1,
12762 None,
12763 Some(layers_run),
12764 from,
12765 upto_excl,
12766 )
12767 }
12768
12769 #[cfg(feature = "gpu")]
12773 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12774 if !crate::minicpm_tail::minimax_pool_on()
12775 || !crate::gpu::enabled_here()
12776 || self.head_clusters.is_some()
12777 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12778 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12779 {
12780 return None;
12781 }
12782 let (model, i, kind, rs) = self
12783 .weights
12784 .lm_head
12785 .graph_weight()
12786 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12787 let name = &model.tensors[i].name;
12788 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12789 crate::gpu::GraphPrismOp::InverseEmbedding
12790 } else if crate::prism::is_forward_weight(&model, name) {
12791 crate::gpu::GraphPrismOp::Forward
12792 } else {
12793 crate::gpu::GraphPrismOp::None
12794 };
12795 let gw = crate::gpu::GraphW {
12796 idx: i,
12797 kind,
12798 row_scale: rs,
12799 data: &[],
12800 prism,
12801 affine: crate::prism::is_affine_target(&model, name),
12802 };
12803 let rows = self.weights.lm_head.rows();
12804 let mut h = hidden.to_vec();
12805 let mut lg = Vec::new();
12806 let mut layers_run = 0usize;
12807 let outcome = crate::gpu::forward_token_graph(
12808 &model,
12809 self.graph_kv_id,
12810 &[],
12811 &[],
12812 self.o1_epoch,
12813 &self.inv_freq,
12814 &mut h,
12815 self.num_heads,
12816 self.num_kv_heads,
12817 self.head_dim,
12818 self.attn_scale,
12819 self.head_dim,
12820 self.hidden_size,
12821 self.intermediate_size,
12822 0,
12823 self.kv_cache.max_seq_len,
12824 self.norm_style == cortiq_core::NormStyle::Gemma,
12825 self.rms_eps as f32,
12826 Some((&gw, rows)),
12827 &self.weights.final_norm,
12828 &mut lg,
12829 &[],
12830 1,
12831 None,
12832 None,
12833 Some(&mut layers_run),
12834 0,
12835 false,
12836 );
12837 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12838 return None;
12839 }
12840 lg.resize(self.vocab_size, 0.0);
12843 if let Some(m) = self.logit_multiplier {
12844 for l in lg.iter_mut() {
12845 *l *= m;
12846 }
12847 }
12848 if let Some(c) = self.final_softcap {
12849 softcap_in_place(&mut lg, c, self.pool.as_deref());
12850 }
12851 Some(lg)
12852 }
12853
12854 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12857 if winners.is_empty() {
12858 return;
12859 }
12860 if self.m3_n_exp == 0 {
12861 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12862 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12863 }
12864 for &e in winners {
12865 let e = e as usize;
12866 if e < self.m3_n_exp && li < self.num_layers {
12867 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12868 }
12869 }
12870 if li + 1 == self.num_layers {
12871 self.m3_tokens += 1;
12872 if self.m3_tokens % 16 == 0 {
12873 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12874 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12875 let _ = std::fs::write(&path, bytes);
12876 }
12877 }
12878 }
12879 }
12880
12881 fn m3_prewarm(&mut self) {
12886 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12887 return;
12888 };
12889 let Ok(bytes) = std::fs::read(&path) else {
12890 tracing::warn!("m3 prewarm: cannot read profile {path}");
12891 return;
12892 };
12893 let counts: Vec<u32> = bytes
12894 .chunks_exact(4)
12895 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12896 .collect();
12897 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12898 if counts.len() != self.num_layers * n_exp {
12899 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12900 return;
12901 }
12902 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12903 .ok()
12904 .and_then(|v| v.parse().ok())
12905 .unwrap_or(300);
12906 let mut ranked: Vec<(u32, usize, usize)> = counts
12907 .iter()
12908 .enumerate()
12909 .filter(|(_, c)| **c > 0)
12910 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12911 .collect();
12912 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12913 ranked.truncate(limit);
12914 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12915 for (_, li, e) in ranked {
12916 per_layer[li].push(e);
12917 }
12918 let mut admitted = 0usize;
12919 for (li, ids) in per_layer.iter().enumerate() {
12920 if ids.is_empty() {
12921 continue;
12922 }
12923 let lw = &self.weights.layers[self.phys_layer(li)];
12924 let FfnKind::Moe(m) = &lw.ffn else {
12925 continue;
12926 };
12927 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12929 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12930 admitted += ids.len();
12931 }
12932 }
12933 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12934 }
12935
12936 #[cfg(feature = "gpu")]
12947 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12948 if self.o1_active() || self.attn_softcap > 0.0 {
12949 return Ok(false);
12950 }
12951 if !self.m3_prewarmed {
12952 self.m3_prewarmed = true;
12953 self.m3_prewarm();
12954 }
12955 let frame = {
12956 let lw = &self.weights.layers[self.phys_layer(li)];
12957 let FfnKind::Moe(m) = &lw.ffn else {
12958 return Ok(false);
12959 };
12960 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12961 return Ok(false);
12962 }
12963 let t_arm = std::time::Instant::now();
12964 if self.m3_winners.len() != self.num_layers {
12965 self.m3_winners = vec![Vec::new(); self.num_layers];
12966 }
12967 let pred = std::mem::take(&mut self.m3_winners[li]);
12968 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12969 if armed.is_none() {
12970 self.m3_winners[li] = pred; }
12972 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12973 match armed {
12974 Some(frame) => frame,
12975 None => return Ok(false),
12978 }
12979 };
12980 crate::gpu_wgpu::pooled_frame_enter(frame);
12981 let want_logits = self.graph_want_logits;
12985 self.graph_want_logits = false;
12986 let mut lg = Vec::new();
12987 let mut gl = 0usize;
12988 let t_span = std::time::Instant::now();
12989 let span = crate::gpu::gpu_scope(|| {
12990 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12991 });
12992 self.graph_want_logits = want_logits;
12993 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12994 let hh = match span {
12995 Some(Ok(hh)) => hh,
12996 Some(Err(())) => return Err(()),
12997 None => {
12998 return Ok(false);
13001 }
13002 };
13003 if gl != 1 {
13004 return if gl == 0 { Ok(false) } else { Err(()) };
13007 }
13008 *h = hh;
13009 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
13010 self.m3_note_route(li, &winners);
13011 if self.m3_winners.len() != self.num_layers {
13012 self.m3_winners = vec![Vec::new(); self.num_layers];
13013 }
13014 self.m3_winners[li] = winners;
13015 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
13016 cold.retain(|&(_, w)| {
13019 if w.is_finite() {
13020 true
13021 } else {
13022 tracing::error!("pooled tail: non-finite cold weight from expert route");
13023 false
13024 }
13025 });
13026 if cold.is_empty() {
13027 return Ok(true);
13028 }
13029 let x = crate::gpu_wgpu::pooled_frame_take_input();
13030 if x.len() != self.hidden_size {
13031 return Err(());
13032 }
13033 let lw = &self.weights.layers[self.phys_layer(li)];
13039 let FfnKind::Moe(m) = &lw.ffn else {
13040 return Err(());
13041 };
13042 let t_cold = std::time::Instant::now();
13043 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
13044 let mut out = crate::attention::take_buf(self.hidden_size);
13045 let on_device = !force_host
13046 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
13047 if !on_device {
13048 crate::attention::recycle_buf(&mut out);
13049 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
13050 .iter()
13051 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
13052 .collect::<Result<_, ()>>()?;
13053 out = crate::gpu::cpu_scope(|| {
13054 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
13055 });
13056 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
13057 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
13058 }
13059 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13060 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13061 if out.len() != h.len() {
13062 crate::attention::recycle_buf(&mut out);
13063 return Err(());
13064 }
13065 for (dst, src) in h.iter_mut().zip(out.iter()) {
13066 *dst += src;
13067 }
13068 crate::attention::recycle_buf(&mut out);
13069 if on_device {
13070 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13071 }
13072 Ok(true)
13073 }
13074
13075 #[cfg(not(feature = "gpu"))]
13076 fn m3_pooled_layer(
13077 &mut self,
13078 _li: usize,
13079 _position: usize,
13080 _h: &mut Vec<f32>,
13081 ) -> Result<bool, ()> {
13082 Ok(false)
13083 }
13084
13085 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
13089 if self.o1_active() || self.attn_softcap > 0.0 {
13090 return None;
13091 }
13092 if let Some(reason) = self.wgpu_graph_attn_decline() {
13095 self.note_graph_decline("wgpu multi-burst", reason);
13096 return None;
13097 }
13098 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
13099 if !graph_on || self.graph_refused() {
13100 return None;
13107 }
13108 let emb = self.embed_single(t_next);
13109 let mut lg = Vec::new();
13110 let mut ids = Vec::new();
13111 match self.try_token_graph_wgpu_steps(
13112 &emb,
13113 position,
13114 &mut lg,
13115 k,
13116 Some(&mut ids),
13117 None,
13118 0,
13119 self.num_layers,
13120 ) {
13121 Some(Ok(_)) => {}
13122 Some(Err(())) => {
13123 self.graph_failed
13128 .store(true, std::sync::atomic::Ordering::Relaxed);
13129 return None;
13130 }
13131 None => return None,
13132 }
13133 (ids.len() == k).then_some(ids)
13134 }
13135
13136 fn try_token_graph_wgpu_steps(
13140 &self,
13141 hidden: &[f32],
13142 position: usize,
13143 logits_out: &mut Vec<f32>,
13144 steps: usize,
13145 ids_out: Option<&mut Vec<u32>>,
13146 layers_run: Option<&mut usize>,
13147 from: usize,
13148 upto_excl: usize,
13149 ) -> Option<Result<Vec<f32>, ()>> {
13150 let upto_excl = match self.mimo_moe.graph_prefix_end() {
13153 Some(end) if end < upto_excl => {
13154 if steps != 1 || layers_run.is_none() || from >= end {
13155 return None;
13156 }
13157 end
13158 }
13159 _ => upto_excl,
13160 };
13161 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13164 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13165 return None;
13169 }
13170 if let Some(reason) = self.wgpu_graph_attn_decline() {
13177 self.note_graph_decline("wgpu token graph", reason);
13178 return None;
13179 }
13180 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13185 .map(|li| {
13186 if !o1_gpu {
13187 return None;
13188 }
13189 self.kv_cache.layers[self.phys_layer(li)].o1_views()
13190 })
13191 .collect();
13192 if self.o1_active() && o1_gpu {
13193 let want: usize = (from..upto_excl)
13196 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
13197 .count();
13198 let have = o1_views.iter().filter(|v| v.is_some()).count();
13199 if want == 0 || have != want {
13200 use std::sync::atomic::{AtomicUsize, Ordering};
13210 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13211 let code = have * 1000 + want;
13212 if LAST.swap(code, Ordering::Relaxed) != code {
13213 tracing::warn!(
13214 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13215 );
13216 }
13217 return None;
13218 }
13219 }
13220 let nh = self.num_heads;
13221 let (nkv, hd, rd) = self.layer_geom(0);
13222 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13223 let mut layers = Vec::with_capacity(upto_excl - from);
13224 let mut model = None;
13225 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13226 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13227 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13229 let name = &m.tensors[i].name;
13230 let prism = if crate::prism::is_inverse_embedding(m, name) {
13231 crate::gpu::GraphPrismOp::InverseEmbedding
13232 } else if crate::prism::is_forward_weight(m, name) {
13233 crate::gpu::GraphPrismOp::Forward
13234 } else {
13235 crate::gpu::GraphPrismOp::None
13236 };
13237 return Some(crate::gpu::GraphW {
13238 idx: i,
13239 kind,
13240 row_scale: rs,
13241 data: &[],
13242 prism,
13243 affine: crate::prism::is_affine_target(m, name),
13244 });
13245 }
13246 match t.as_f32() {
13248 Some(d) => Some(crate::gpu::GraphW {
13249 idx: 0,
13250 kind: 4,
13251 row_scale: &[],
13252 data: d,
13253 prism: crate::gpu::GraphPrismOp::None,
13254 affine: false,
13255 }),
13256 None => {
13257 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13258 eprintln!("batch graph: weight has no graph/f32 representation");
13259 }
13260 None
13261 }
13262 }
13263 }
13264 for li in from..upto_excl {
13265 let lw = &self.weights.layers[self.phys_layer(li)];
13266 if dbg {
13267 let ak = match &lw.attn {
13268 AttnKind::Mla(_) => "Mla".into(),
13269 AttnKind::Full {
13270 output_gate, bias, ..
13271 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13272 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13273 AttnKind::Kda(_) => "Kda".into(),
13274 AttnKind::Linear(_) => "Linear".into(),
13275 AttnKind::ShortConv(_) => "ShortConv".into(),
13276 AttnKind::Bounded(_) => "Bounded".into(),
13277 };
13278 let fk = match &lw.ffn {
13279 FfnKind::Dense(_) => "Dense",
13280 FfnKind::Moe(_) => "Moe",
13281 FfnKind::DenseMoe(_) => "DenseMoe",
13282 };
13283 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13284 }
13285 let gffn = match &lw.ffn {
13286 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13290 FfnKind::Dense(d) => {
13291 let Some(act) = d.act.wgpu_graph_act() else {
13295 self.note_graph_decline(
13296 "wgpu token graph",
13297 "dense FFN activation without a graph kernel",
13298 );
13299 return None;
13300 };
13301 crate::gpu::GraphFfn::Dense {
13302 gate: gw(&d.gate_proj)?,
13303 up: gw(&d.up_proj)?,
13304 down: gw(&d.down_proj)?,
13305 act,
13306 }
13307 }
13308 FfnKind::Moe(m) => {
13309 if m.route_tau.is_some() || m.mask.is_some() {
13317 return None;
13318 }
13319 let shared = m.shared.as_ref();
13320 let inter = m.experts.first()?.gate_proj.rows();
13326 let detached_shared = match shared {
13334 Some((se, None))
13335 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13336 {
13337 if !matches!(se.act, Act::Silu)
13338 || se.gate_proj.mapped_q4tp().is_none()
13339 || se.up_proj.mapped_q4tp().is_none()
13340 || se.down_proj.mapped_q4tp().is_none()
13341 {
13342 return None;
13343 }
13344 Some(crate::gpu::GraphMoeDetachedShared {
13345 gate: gw(&se.gate_proj)?,
13346 up: gw(&se.up_proj)?,
13347 down: gw(&se.down_proj)?,
13348 width: se.gate_proj.rows(),
13349 })
13350 }
13351 Some((se, Some(_)))
13355 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13356 {
13357 return None;
13358 }
13359 _ => None,
13360 };
13361 let has_shared = shared.is_some() && detached_shared.is_none();
13362 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13363 let sgate = match shared {
13364 Some((_, Some(sg))) if has_shared => gw(sg)?,
13365 _ => gw(&m.router)?,
13369 };
13370 let router = gw(&m.router)?;
13371 if router.prism != crate::gpu::GraphPrismOp::None
13377 || sgate.prism != crate::gpu::GraphPrismOp::None
13378 || router.affine
13379 || sgate.affine
13380 {
13381 tracing::warn!(
13382 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13383 );
13384 return None;
13385 }
13386 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
13387 let edge = |t: &QTensor| {
13388 t.graph_weight()
13389 .or_else(|| t.graph_weight_descriptor())
13390 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
13391 };
13392 let bank_key: Option<GraphMoeKey> = match (
13393 m.experts.first().and_then(|e| edge(&e.gate_proj)),
13394 m.experts.last().and_then(|e| edge(&e.down_proj)),
13395 ) {
13396 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
13397 self.phys_layer(li),
13398 m.experts.as_ptr() as usize,
13399 m.experts.len(),
13400 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
13401 fm,
13402 fi,
13403 di,
13404 )),
13405 _ => None,
13406 };
13407 let cached =
13408 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
13409 let bank = if let Some(b) = cached {
13410 b
13411 } else {
13412 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13413 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13414 let mut q4tp: Option<bool> = None;
13417 let mut gu_q2: Option<bool> = None;
13420 let mut q82: Option<bool> = None;
13422 let mut moe_act = None;
13426 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
13427 let Some(e_act) = e.act.wgpu_graph_act() else {
13428 return None;
13429 };
13430 match moe_act {
13431 Some(existing) if existing != e_act => return None,
13432 None => moe_act = Some(e_act),
13433 _ => {}
13434 }
13435 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13436 return None;
13437 }
13438 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13443 let Some((em, ei, _, _)) = expert_weight
13444 .graph_weight()
13445 .or_else(|| expert_weight.graph_weight_descriptor())
13446 else {
13447 return None;
13448 };
13449 let name = &em.tensors[ei].name;
13450 if crate::prism::is_forward_weight(em, name)
13451 || crate::prism::is_inverse_embedding(em, name)
13452 || crate::prism::is_affine_target(em, name)
13453 {
13454 tracing::warn!(
13455 "resident MoE declined: expert Prism/affine transform is not implemented"
13456 );
13457 return None;
13458 }
13459 }
13460 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13461 Some((mm, gi)) => (
13462 mm,
13463 gi,
13464 e.up_proj.mapped_q4t()?.1,
13465 e.down_proj.mapped_q4t()?.1,
13466 false,
13467 false,
13468 false,
13469 ),
13470 None => match e.gate_proj.mapped_q2tp() {
13471 Some((mm, gi)) => (
13472 mm,
13473 gi,
13474 e.up_proj.mapped_q2tp()?.1,
13475 e.down_proj.mapped_q4tp()?.1,
13476 true,
13477 true,
13478 false,
13479 ),
13480 None => match e.gate_proj.mapped_q4tp() {
13481 Some((mm, gi)) => (
13482 mm,
13483 gi,
13484 e.up_proj.mapped_q4tp()?.1,
13485 e.down_proj.mapped_q4tp()?.1,
13486 true,
13487 false,
13488 false,
13489 ),
13490 None => {
13493 if !crate::gpu::moe_q82_graph_on() {
13494 return None;
13495 }
13496 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13497 (
13498 mm,
13499 gi,
13500 e.up_proj.mapped_q8_2f()?.1,
13501 e.down_proj.mapped_q8_2f()?.1,
13502 false,
13503 false,
13504 true,
13505 )
13506 }
13507 },
13508 },
13509 };
13510 if *q4tp.get_or_insert(is_p) != is_p
13511 || *gu_q2.get_or_insert(is_q2) != is_q2
13512 || *q82.get_or_insert(is_8) != is_8
13513 {
13514 tracing::warn!(
13520 "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."
13521 );
13522 return None;
13523 }
13524 bank_model.get_or_insert_with(|| mm.clone());
13525 experts.push((gi, ui, di));
13526 }
13527 let b = GraphMoeBank {
13528 experts,
13529 q4tp,
13530 gu_q2,
13531 q82,
13532 act: moe_act,
13533 model: bank_model,
13534 };
13535 if let Some(k) = bank_key {
13536 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
13537 }
13538 b
13539 };
13540 if let Some(mm) = &bank.model {
13541 model.get_or_insert_with(|| mm.clone());
13542 }
13543 crate::gpu::GraphFfn::Moe {
13544 router,
13545 shared_gate: sgate,
13546 experts: bank.experts,
13547 n_exp: m.experts.len(),
13548 top_k: std::env::var("CMF_TOPK_PROBE")
13554 .ok()
13555 .and_then(|v| v.parse::<usize>().ok())
13556 .filter(|k| *k > 0 && *k <= m.top_k)
13557 .unwrap_or(m.top_k),
13558 inter,
13559 act: bank.act?,
13560 norm_topk: m.norm_topk_prob,
13561 q4tp: bank.q4tp?,
13562 gu_q2: bank.gu_q2.unwrap_or(false),
13563 q82: bank.q82.unwrap_or(false),
13564 sigmoid: m.router_sigmoid,
13565 sigmoid_denom_eps: m.sigmoid_denom_eps,
13566 bias: m.expert_bias.as_deref(),
13567 has_shared,
13568 detached_shared,
13569 shared_gated,
13570 route_scale: m.routed_scaling,
13571 }
13572 }
13573 };
13574 let attn = match &lw.attn {
13575 AttnKind::Full {
13576 wq,
13577 wk,
13578 wv,
13579 wo,
13580 q_norm,
13581 k_norm,
13582 output_gate,
13583 softplus_gate,
13584 bias,
13585 } => {
13586 if self.attention_heads_per_layer.is_some() {
13587 return None;
13588 }
13589 let head_gate = match softplus_gate {
13593 None => None,
13594 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13595 Some(_) => {
13596 self.note_graph_decline(
13597 "wgpu token graph",
13598 "projected softplus / per-element output gate",
13599 );
13600 return None;
13601 }
13602 };
13603 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13604 model = Some(m.clone());
13605 crate::gpu::GraphAttn::Full {
13606 wq: gw(wq)?,
13607 wk: gw(wk)?,
13608 wv: gw(wv)?,
13609 wo: gw(wo)?,
13610 q_norm: q_norm.as_deref(),
13611 k_norm: k_norm.as_deref(),
13612 late_qk_norm: self.qk_norm_after_rope,
13613 bias: bias
13614 .as_ref()
13615 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13616 output_gate: *output_gate,
13617 cpu_k: self.kv_cache.layers[li].k_heads(),
13618 cpu_v: self.kv_cache.layers[li].v_heads(),
13619 cpu_base: self.kv_cache.layers[li].base(),
13620 geom: self.graph_attn_geom(li),
13621 head_gate,
13622 }
13623 }
13624 AttnKind::LinearGdn(w) => {
13625 let cfg = self.gdn_cfg?;
13626 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13627 model = Some(m.clone());
13628 crate::gpu::GraphAttn::Gdn {
13629 qkv: gw(&w.in_proj_qkv)?,
13630 z: gw(&w.in_proj_z)?,
13631 a: gw(&w.in_proj_a)?,
13632 b: gw(&w.in_proj_b)?,
13633 out: gw(&w.out_proj)?,
13634 conv1d: &w.conv1d,
13635 a_log: &w.a_log,
13636 dt_bias: &w.dt_bias,
13637 norm: &w.norm,
13638 nv: cfg.num_v_heads,
13639 nk: cfg.num_k_heads,
13640 dk: cfg.key_head_dim,
13641 dv: cfg.value_head_dim,
13642 kk: cfg.conv_kernel,
13643 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13644 preview: None,
13645 }
13646 }
13647 AttnKind::ShortConv(w) => {
13648 let cfg = self.short_conv_cfg?;
13649 let (m, _, _, _) = w
13650 .in_proj
13651 .graph_weight()
13652 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13653 model = Some(m.clone());
13654 crate::gpu::GraphAttn::ShortConv {
13655 inp: gw(&w.in_proj)?,
13656 out: gw(&w.out_proj)?,
13657 taps: &w.conv,
13658 kernel: cfg.kernel,
13659 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13660 }
13661 }
13662 _ => return None,
13663 };
13664 layers.push(crate::gpu::GraphLayer {
13665 input_norm: &lw.input_norm,
13666 attn,
13667 post_norm: &lw.post_norm,
13668 attn_out_norm: lw.attn_out_norm.as_deref(),
13669 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13670 layer_scale: lw.layer_scale,
13671 ffn: gffn,
13672 });
13673 }
13674 let model = model?;
13675 let lm_gw = if upto_excl == self.num_layers
13681 && self.graph_want_logits
13682 && std::env::var("CMF_GPU_LMHEAD")
13683 .map(|v| v != "0")
13684 .unwrap_or(true)
13685 {
13686 self.weights
13687 .lm_head
13688 .graph_weight()
13689 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13690 .map(|(m, i, kind, rs)| {
13691 let name = &m.tensors[i].name;
13692 let prism = if crate::prism::is_inverse_embedding(m, name) {
13693 crate::gpu::GraphPrismOp::InverseEmbedding
13694 } else if crate::prism::is_forward_weight(m, name) {
13695 crate::gpu::GraphPrismOp::Forward
13696 } else {
13697 crate::gpu::GraphPrismOp::None
13698 };
13699 (
13700 crate::gpu::GraphW {
13701 idx: i,
13702 kind,
13703 row_scale: rs,
13704 data: &[],
13705 prism,
13706 affine: crate::prism::is_affine_target(m, name),
13707 },
13708 self.weights.lm_head.rows(),
13709 )
13710 })
13711 } else {
13712 None
13713 };
13714 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13715 let emb_gw = if steps > 1 {
13717 self.weights
13718 .embed_tokens
13719 .graph_weight()
13720 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13721 .map(|(m, i, kind, rs)| {
13722 let name = &m.tensors[i].name;
13723 let prism = if crate::prism::is_inverse_embedding(m, name) {
13724 crate::gpu::GraphPrismOp::InverseEmbedding
13725 } else if crate::prism::is_forward_weight(m, name) {
13726 crate::gpu::GraphPrismOp::Forward
13727 } else {
13728 crate::gpu::GraphPrismOp::None
13729 };
13730 (
13731 crate::gpu::GraphW {
13732 idx: i,
13733 kind,
13734 row_scale: rs,
13735 data: &[],
13736 prism,
13737 affine: crate::prism::is_affine_target(m, name),
13738 },
13739 self.weights.embed_tokens.rows(),
13740 self.embed_multiplier,
13741 )
13742 })
13743 } else {
13744 None
13745 };
13746
13747 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13753 (from..upto_excl.min(self.num_layers - 1))
13754 .filter(|&li| (li + 1) % self.physical_layers == 0)
13755 .map(|li| li - from)
13756 .collect()
13757 } else {
13758 Vec::new()
13759 };
13760 let mut h = hidden.to_vec();
13761 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13767 let outcome = crate::gpu::forward_token_graph(
13768 &model,
13769 self.graph_kv_id,
13770 &layers,
13771 &o1_views,
13772 self.o1_epoch,
13773 &self.inv_freq,
13774 &mut h,
13775 nh,
13776 nkv,
13777 hd,
13778 self.attn_scale,
13779 rd,
13780 self.hidden_size,
13781 self.intermediate_size,
13782 position,
13783 self.kv_cache.max_seq_len,
13784 gemma,
13785 self.rms_eps as f32,
13786 lm,
13787 &self.weights.final_norm,
13788 logits_out,
13789 &loop_norm_at,
13790 steps,
13791 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13792 ids_out,
13793 layers_run,
13794 from,
13795 dump_hidden,
13796 );
13797 match outcome {
13798 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13799 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13800 crate::gpu::TokenGraphOutcome::Declined => None,
13801 }
13802 }
13803
13804 #[cfg(target_os = "macos")]
13813 #[allow(clippy::type_complexity)]
13814 fn metal_rows_plan(
13815 &self,
13816 ) -> Option<(
13817 Vec<MetalRowsItem<'_>>,
13818 std::sync::Arc<cortiq_core::CmfModel>,
13819 Option<crate::gpu_metal::GdnGpuCfg>,
13820 )> {
13821 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13822 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13823 if !graph_force
13824 || !crate::gpu::enabled_here()
13825 || std::env::var("CMF_GPU_BLOCK")
13826 .map(|v| v == "0")
13827 .unwrap_or(false)
13828 || self.attn_softcap > 0.0
13829 || self.o1_active()
13830 || self.swa.is_some()
13831 || self.global_attn.is_some()
13832 || self.attention_heads_per_layer.is_some()
13833 || self.graph_attn_decline_reason().is_some()
13835 || self.attn_v_norm
13836 || self.loop_final_norm
13837 || !self.metal_rope_scale_ok()
13838 {
13839 return None;
13840 }
13841 let attend_contract = self.head_dim % 4 == 0
13842 && self.head_dim <= 256
13843 && self.rotary_dim >= 2
13844 && self.rotary_dim <= self.head_dim
13845 && (self.rotary_dim / 2) % 32 == 0
13846 && self.num_kv_heads > 0
13847 && self.num_heads % self.num_kv_heads == 0;
13848 if !attend_contract {
13849 return None;
13850 }
13851 let mut plan: Vec<MetalRowsItem> = Vec::new();
13852 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13853 for li in 0..self.num_layers {
13854 let lw = &self.weights.layers[self.phys_layer(li)];
13855 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13856 return None;
13857 }
13858 let ffn = match &lw.ffn {
13859 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13860 let (Some(g), Some(u), Some(dn)) = (
13861 d.gate_proj.metal_graph_parts(),
13862 d.up_proj.metal_graph_parts(),
13863 d.down_proj.metal_graph_parts(),
13864 ) else {
13865 return None;
13866 };
13867 MetalFfn::Dense {
13868 gate: g,
13869 up: u,
13870 down: dn,
13871 act: 0, }
13873 }
13874 _ => return None,
13875 };
13876 match &lw.attn {
13877 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13878 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13879 w.in_proj_qkv.metal_graph_parts(),
13880 w.in_proj_z.metal_graph_parts(),
13881 w.in_proj_a.f32_parts(),
13882 w.in_proj_b.f32_parts(),
13883 w.out_proj.metal_graph_parts(),
13884 ) else {
13885 return None;
13886 };
13887 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13888 model_ref.get_or_insert_with(|| model.clone());
13889 }
13890 let gl = GdnGpuLayer {
13891 attn_norm: &lw.input_norm,
13892 post_norm: &lw.post_norm,
13893 qkv,
13894 z,
13895 a,
13896 b: bb,
13897 out,
13898 ffn,
13899 conv1d: &w.conv1d,
13900 a_log: &w.a_log,
13901 dt_bias: &w.dt_bias,
13902 gnorm: &w.norm,
13903 };
13904 match plan.last_mut() {
13905 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13906 _ => plan.push(MetalRowsItem::Gdn {
13907 run: vec![gl],
13908 first: li,
13909 }),
13910 }
13911 }
13912 AttnKind::Full {
13913 wq,
13914 wk,
13915 wv,
13916 wo,
13917 q_norm,
13918 k_norm,
13919 output_gate,
13920 softplus_gate: None,
13921 bias: None,
13922 } => {
13923 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13924 wq.metal_graph_parts(),
13925 wk.metal_graph_parts(),
13926 wv.metal_graph_parts(),
13927 wo.metal_graph_parts(),
13928 ) else {
13929 return None;
13930 };
13931 if let QTensor::Mapped { model, .. } = wq {
13932 model_ref.get_or_insert_with(|| model.clone());
13933 }
13934 let cache = &self.kv_cache.layers[li];
13935 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13936 return None;
13937 }
13938 plan.push(MetalRowsItem::Attn {
13939 l: AttnGpuLayer {
13940 attn_norm: &lw.input_norm,
13941 post_norm: &lw.post_norm,
13942 wq: pq,
13943 wk: pk,
13944 wv: pv,
13945 wo: po,
13946 ffn,
13947 attn_out_norm: None,
13948 ffn_out_norm: None,
13949 layer_scale: None,
13950 },
13951 li,
13952 q_norm: q_norm.as_deref(),
13953 k_norm: k_norm.as_deref(),
13954 output_gate: *output_gate,
13955 });
13956 }
13957 _ => return None,
13958 }
13959 }
13960 let model = model_ref?;
13961 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13962 nv: cfg.num_v_heads,
13963 nk: cfg.num_k_heads,
13964 dk: cfg.key_head_dim,
13965 dv: cfg.value_head_dim,
13966 kk: cfg.conv_kernel,
13967 hidden: self.hidden_size,
13968 inter: self.intermediate_size,
13969 c_dim: cfg.conv_dim(),
13970 eps: cfg.rms_eps as f32,
13971 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13972 });
13973 Some((plan, model, gcfg))
13974 }
13975
13976 #[cfg(target_os = "macos")]
13978 #[allow(clippy::too_many_arguments)]
13979 fn metal_attn_params<'a>(
13980 li: usize,
13981 cache: &'a crate::kv_cache::LayerKvCache,
13982 q_norm: Option<&'a [f32]>,
13983 k_norm: Option<&'a [f32]>,
13984 output_gate: bool,
13985 inv_freq: &'a [f32],
13986 rope_scale: f32,
13987 geom: (usize, usize, usize, usize),
13988 pos0: usize,
13989 kv_id: u64,
13990 scale: f32,
13991 eps: f32,
13992 gemma: bool,
13993 late_qk_norm: bool,
13994 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13995 let (nh, nkv, hd, rd) = geom;
13996 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13997 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13998 let cpu_stored = cpu_k[0].len() / hd;
13999 (
14000 crate::gpu_metal::AttnDeviceParams {
14001 kv_id,
14002 layer: li,
14003 nh,
14004 nkv,
14005 hd,
14006 rd,
14007 position: pos0,
14008 scale,
14009 eps,
14010 gemma,
14011 late_qk_norm,
14012 output_gate,
14013 q_norm,
14014 k_norm,
14015 inv_freq,
14016 rope_scale,
14017 cpu_k,
14018 cpu_v,
14019 cpu_stored,
14020 cpu_gen: cache.generation(),
14021 o1: None,
14022 window: None,
14023 softmax_sink: cache.sinks.as_deref(),
14024 head_gate: None,
14025 v_norm: false,
14026 },
14027 cpu_stored,
14028 )
14029 }
14030
14031 #[cfg(target_os = "macos")]
14036 #[allow(clippy::type_complexity)]
14037 fn metal_rows_run(
14038 &mut self,
14039 hiddens: &mut [f32],
14040 pos0: usize,
14041 b: usize,
14042 prefill: bool,
14043 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14044 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
14048 ) -> MetalRowsRun {
14049 use crate::gpu_metal::{GraphDims, VerifyGraph};
14050 if !crate::gpu_metal::wait_replay() {
14056 tracing::error!("Metal rows graph: the pending async replay failed");
14057 return MetalRowsRun::Failed;
14058 }
14059 spec_stamp("v.wait");
14060 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
14066 if want > 0 {
14067 let phys = self.physical_layers.max(1);
14068 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
14069 let is_gdn = self
14070 .weights
14071 .layers
14072 .get(li % phys)
14073 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
14074 if is_gdn && l.linear_state.len() != want {
14075 l.linear_state = vec![0f32; want];
14076 }
14077 }
14078 }
14079 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
14080 return MetalRowsRun::Declined;
14081 };
14082 spec_stamp("v.plan");
14083 let dims = GraphDims {
14084 hidden: self.hidden_size,
14085 eps: self.rms_eps as f32,
14086 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14087 };
14088 let Some(mut graph) = (if prefill {
14089 VerifyGraph::new_prefill(&model, dims, hiddens, b)
14090 } else {
14091 VerifyGraph::new(&model, dims, hiddens, b)
14092 }) else {
14093 return MetalRowsRun::Declined;
14094 };
14095 let geom = (
14096 self.num_heads,
14097 self.num_kv_heads,
14098 self.head_dim,
14099 self.rotary_dim,
14100 );
14101 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14102 let eps = self.rms_eps as f32;
14103 let kv_id = self.graph_kv_id;
14104 let inv_freq = self.inv_freq.clone();
14105 for item in &plan {
14106 let ok = match item {
14107 MetalRowsItem::Gdn { run, .. } => gcfg
14108 .as_ref()
14109 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
14110 .unwrap_or(false),
14111 MetalRowsItem::Attn {
14112 l,
14113 li,
14114 q_norm,
14115 k_norm,
14116 output_gate,
14117 } => {
14118 let (p, _) = Self::metal_attn_params(
14119 *li,
14120 &self.kv_cache.layers[*li],
14121 *q_norm,
14122 *k_norm,
14123 *output_gate,
14124 &inv_freq,
14125 self.layer_rope_scale(*li),
14126 geom,
14127 pos0,
14128 kv_id,
14129 self.attn_scale,
14130 eps,
14131 gemma,
14132 self.qk_norm_after_rope,
14133 );
14134 graph.attn_ok(l, &p)
14135 }
14136 };
14137 if !ok {
14138 use std::sync::atomic::{AtomicBool, Ordering};
14139 static SAID: AtomicBool = AtomicBool::new(false);
14140 if !SAID.swap(true, Ordering::Relaxed) {
14141 tracing::warn!("metal rows graph: a layer failed preflight — declining");
14142 }
14143 return MetalRowsRun::Declined;
14144 }
14145 }
14146 let lm = match &spec {
14147 Some((lm, _, _)) => {
14148 if !graph.lm_head_ok(*lm) {
14149 return MetalRowsRun::Declined;
14150 }
14151 Some(*lm)
14152 }
14153 None => None,
14154 };
14155 let mut gdn_layers = Vec::new();
14156 let mut attn_layers = Vec::new();
14157 for item in &plan {
14158 match item {
14159 MetalRowsItem::Gdn { run, first } => {
14160 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14161 .iter()
14162 .map(|l| l.linear_state.as_slice())
14163 .collect();
14164 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14165 return MetalRowsRun::Declined;
14166 }
14167 gdn_layers.extend(*first..*first + run.len());
14168 }
14169 MetalRowsItem::Attn {
14170 l,
14171 li,
14172 q_norm,
14173 k_norm,
14174 output_gate,
14175 } => {
14176 let (p, cpu_stored) = Self::metal_attn_params(
14177 *li,
14178 &self.kv_cache.layers[*li],
14179 *q_norm,
14180 *k_norm,
14181 *output_gate,
14182 &inv_freq,
14183 self.layer_rope_scale(*li),
14184 geom,
14185 pos0,
14186 kv_id,
14187 self.attn_scale,
14188 eps,
14189 gemma,
14190 self.qk_norm_after_rope,
14191 );
14192 if !graph.encode_attn_b(l, &p) {
14193 return MetalRowsRun::Declined;
14194 }
14195 attn_layers.push((*li, cpu_stored));
14196 }
14197 }
14198 }
14199 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
14200 if !graph.encode_lm_head_b(final_norm, lm) {
14201 return MetalRowsRun::Declined;
14202 }
14203 if let Some((n, _)) = argmax_out.as_ref() {
14208 if !graph.encode_argmax_b(*n) {
14209 argmax_out = None;
14210 }
14211 }
14212 }
14213 spec_stamp("v.enc");
14214 if !graph.sync() {
14215 return MetalRowsRun::Failed;
14216 }
14217 spec_stamp("v.gpu");
14218 match (spec, argmax_out) {
14219 (Some(_), Some((_, ids))) => {
14220 ids.resize(b, 0);
14221 if !graph.read_argmax(ids) {
14222 return MetalRowsRun::Failed;
14223 }
14224 spec_stamp("v.am");
14225 }
14226 (Some((lm, _, logits)), None) => {
14227 logits.resize(b * lm.1, 0.0);
14228 if !graph.read_logits(logits) {
14229 return MetalRowsRun::Failed;
14230 }
14231 spec_stamp("v.lg");
14232 }
14233 (None, _) => {}
14234 }
14235 if !graph.read_hidden(hiddens) {
14236 return MetalRowsRun::Failed;
14237 }
14238 spec_stamp("v.hid");
14239 MetalRowsRun::Completed(MetalVerifyPending {
14240 graph,
14241 gdn_layers,
14242 attn_layers,
14243 })
14244 }
14245
14246 #[cfg(target_os = "macos")]
14252 fn try_batch_graph_metal(
14253 &mut self,
14254 hiddens: &mut [f32],
14255 positions: &[usize],
14256 b: usize,
14257 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14258 argmax_out: Option<(usize, &mut Vec<u32>)>,
14259 ) -> crate::gpu::BatchGraphOutcome {
14260 let _t0 = std::time::Instant::now();
14261 if positions.len() != b
14262 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14263 || hiddens.len() != b * self.hidden_size
14264 {
14265 return crate::gpu::BatchGraphOutcome::Declined;
14266 }
14267 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14268 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14269 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14270 MetalRowsRun::Completed(pending) => pending,
14271 };
14272 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14273 eprintln!(
14274 "metal-verify: {:.1} ms | b={b}",
14275 _t0.elapsed().as_secs_f64() * 1e3
14276 );
14277 }
14278 self.metal_verify = Some(pending);
14279 crate::gpu::BatchGraphOutcome::Completed
14280 }
14281
14282 #[cfg(target_os = "macos")]
14287 fn prefill_rows_metal(
14288 &mut self,
14289 ids: &[u32],
14290 start_pos: usize,
14291 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14292 ) -> MetalPrefillOutcome {
14293 let b = ids.len();
14294 if b == 0 || b > 512 {
14295 return MetalPrefillOutcome::Declined;
14296 }
14297 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14298 let with_head = spec.is_some();
14299 let hs = self.hidden_size;
14300 let mut hiddens = vec![0f32; b * hs];
14301 for (j, &id) in ids.iter().enumerate() {
14302 let e = self.embed_single(id);
14303 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14304 }
14305 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14306 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14307 MetalRowsRun::Failed => {
14308 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14309 return MetalPrefillOutcome::Failed;
14310 }
14311 MetalRowsRun::Completed(pending) => pending,
14312 };
14313 let idxs = pending.gdn_layers.clone();
14315 let mut outs: Vec<&mut [f32]> = self
14316 .kv_cache
14317 .layers
14318 .iter_mut()
14319 .enumerate()
14320 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14321 .map(|(_, l)| l.linear_state.as_mut_slice())
14322 .collect();
14323 if !pending.graph.finish_states(&mut outs) {
14324 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14325 return MetalPrefillOutcome::Failed;
14326 }
14327 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14328 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14332 for (li, cpu_stored) in &pending.attn_layers {
14333 let mut kbuf = vec![0f32; b * nkv * hd];
14334 let mut vbuf = vec![0f32; b * nkv * hd];
14335 if !crate::gpu_metal::kv_mirror_read_rows(
14336 self.graph_kv_id,
14337 *li,
14338 nkv,
14339 hd,
14340 *cpu_stored,
14341 b,
14342 &mut kbuf,
14343 &mut vbuf,
14344 ) {
14345 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14346 return MetalPrefillOutcome::Failed;
14347 }
14348 rows.push((*li, *cpu_stored, kbuf, vbuf));
14349 }
14350 for (li, cpu_stored, kbuf, vbuf) in rows {
14351 let cache = &mut self.kv_cache.layers[li];
14352 for r in 0..b {
14353 cache.append(
14354 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14355 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14356 &[],
14357 );
14358 }
14359 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14360 }
14361 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14362 if with_head {
14363 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14364 }
14365 MetalPrefillOutcome::Completed(hiddens)
14366 }
14367
14368 #[cfg(target_os = "macos")]
14369 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14370 self.prefill_rows_metal(ids, start_pos, None)
14371 }
14372
14373 #[cfg(target_os = "macos")]
14378 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14379 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14380 return MetalBatchNllOutcome::Declined;
14381 }
14382 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14383 return MetalBatchNllOutcome::Declined;
14384 };
14385 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14386 .ok()
14387 .and_then(|v| v.parse::<usize>().ok())
14388 .filter(|&v| (1..=512).contains(&v))
14389 .unwrap_or(32);
14390 let final_norm = self.weights.final_norm.clone();
14391 let mut nll = 0.0f64;
14392 let mut count = 0usize;
14393 let mut pos = 0usize;
14394 let mut completed = 0usize;
14395 while pos < ids.len() {
14396 let end = (pos + chunk).min(ids.len());
14397 let mut logits = Vec::new();
14398 let outcome =
14399 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14400 match outcome {
14401 MetalPrefillOutcome::Declined => {
14402 return if completed == 0 {
14403 MetalBatchNllOutcome::Declined
14404 } else {
14405 MetalBatchNllOutcome::Failed(format!(
14406 "ordinary Metal NLL batch declined after {completed} chunks"
14407 ))
14408 };
14409 }
14410 MetalPrefillOutcome::Failed => {
14411 return MetalBatchNllOutcome::Failed(
14412 "ordinary Metal NLL batch failed after admission".to_string(),
14413 );
14414 }
14415 MetalPrefillOutcome::Completed(_) => {}
14416 }
14417 completed += 1;
14418 let vocab = self.vocab_size.min(lm.1);
14419 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14420 return MetalBatchNllOutcome::Failed(
14421 "ordinary Metal NLL head returned an invalid shape".to_string(),
14422 );
14423 }
14424 for row in 0..(end - pos) {
14425 let absolute = pos + row;
14426 if absolute < start || absolute + 1 >= ids.len() {
14427 continue;
14428 }
14429 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14430 if let Some(mu) = self.logit_multiplier {
14431 for v in lg.iter_mut() {
14432 *v *= mu;
14433 }
14434 }
14435 if let Some(c) = self.final_softcap {
14436 softcap_in_place(lg, c, self.pool.as_deref());
14437 }
14438 let target = ids[absolute + 1] as usize;
14439 if target >= vocab {
14440 return MetalBatchNllOutcome::Failed(format!(
14441 "target token {target} exceeds Metal head rows {vocab}"
14442 ));
14443 }
14444 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14445 let lse: f64 = lg
14446 .iter()
14447 .map(|&v| ((v - max) as f64).exp())
14448 .sum::<f64>()
14449 .ln()
14450 + max as f64;
14451 nll += lse - lg[target] as f64;
14452 count += 1;
14453 }
14454 pos = end;
14455 }
14456 MetalBatchNllOutcome::Completed(nll, count)
14457 }
14458
14459 #[cfg(target_os = "macos")]
14463 fn metal_verify_commit(&mut self, a: usize) -> bool {
14464 let Some(mut pending) = self.metal_verify.take() else {
14465 return false;
14466 };
14467 let n = a + 1;
14468 let idxs = pending.gdn_layers.clone();
14470 let mut outs: Vec<&mut [f32]> = self
14471 .kv_cache
14472 .layers
14473 .iter_mut()
14474 .enumerate()
14475 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14476 .map(|(_, l)| l.linear_state.as_mut_slice())
14477 .collect();
14478 if !pending.graph.commit(n, &mut outs) {
14479 return false;
14480 }
14481 spec_stamp("c.replay");
14482 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14483 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14487 for (li, cpu_stored) in &pending.attn_layers {
14488 let mut kbuf = vec![0f32; n * nkv * hd];
14489 let mut vbuf = vec![0f32; n * nkv * hd];
14490 if !crate::gpu_metal::kv_mirror_read_rows(
14491 self.graph_kv_id,
14492 *li,
14493 nkv,
14494 hd,
14495 *cpu_stored,
14496 n,
14497 &mut kbuf,
14498 &mut vbuf,
14499 ) {
14500 return false;
14501 }
14502 rows.push((*li, *cpu_stored, kbuf, vbuf));
14503 }
14504 for (li, cpu_stored, kbuf, vbuf) in rows {
14505 let cache = &mut self.kv_cache.layers[li];
14506 for r in 0..n {
14507 cache.append(
14508 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14509 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14510 &[],
14511 );
14512 }
14513 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14514 }
14515 spec_stamp("c.kv");
14516 true
14517 }
14518
14519 #[cfg(target_os = "macos")]
14526 fn mtp_warm_batch_submit(
14527 &mut self,
14528 m: &mut MtpModule,
14529 pairs: &[(&[f32], u32)],
14530 first_pos: usize,
14531 ) -> Option<MetalWarmPending> {
14532 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14533 let b = pairs.len();
14534 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14535 return None;
14536 }
14537 let AttnKind::Full {
14538 wq,
14539 wk,
14540 wv,
14541 wo,
14542 q_norm,
14543 k_norm,
14544 output_gate,
14545 softplus_gate: None,
14546 bias: None,
14547 } = &m.layer.attn
14548 else {
14549 return None;
14550 };
14551 let FfnKind::Dense(d) = &m.layer.ffn else {
14552 return None;
14553 };
14554 if !d.segs.is_empty() {
14555 return None;
14556 }
14557 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14558 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14559 else {
14560 return None;
14561 };
14562 let (Some(g), Some(u), Some(dn)) = (
14563 d.gate_proj.q1_parts(),
14564 d.up_proj.q1_parts(),
14565 d.down_proj.q1_parts(),
14566 ) else {
14567 return None;
14568 };
14569 let Some(eh) = m.eh_proj.q1_parts() else {
14570 return None;
14571 };
14572 let QTensor::Mapped { model, .. } = wq else {
14573 return None;
14574 };
14575 let model = model.clone();
14576 let hs = self.hidden_size;
14577 let mut cat = vec![0f32; b * 2 * hs];
14579 for (j, (h, tok)) in pairs.iter().enumerate() {
14580 let e = self.embed_single(*tok);
14581 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14582 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14583 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14584 }
14585 let dims = GraphDims {
14586 hidden: hs,
14587 eps: self.rms_eps as f32,
14588 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14589 };
14590 spec_stamp("w.cat");
14591 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14592 return None;
14593 };
14594 spec_stamp("w.new");
14595 let l = AttnGpuLayer {
14596 attn_norm: &m.layer.input_norm,
14597 post_norm: &m.layer.post_norm,
14598 wq: pq,
14599 wk: pk,
14600 wv: pv,
14601 wo: po,
14602 ffn: MetalFfn::Dense {
14603 gate: g,
14604 up: u,
14605 down: dn,
14606 act: (d.act == Act::Gelu) as u8,
14607 },
14608 attn_out_norm: None,
14609 ffn_out_norm: None,
14610 layer_scale: None,
14611 };
14612 let (nh, nkv, hd, rd) = (
14613 self.num_heads,
14614 self.num_kv_heads,
14615 self.head_dim,
14616 self.rotary_dim,
14617 );
14618 let inv_freq = self.inv_freq.clone();
14619 let cpu_stored;
14620 {
14621 let cache = &m.kv;
14622 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14623 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14624 cpu_stored = cpu_k[0].len() / hd;
14625 if cpu_stored > first_pos {
14630 spec_stamp("w.decl");
14631 return None;
14632 }
14633 let p = AttnDeviceParams {
14634 kv_id: self.mtp_kv_id(),
14635 layer: Self::MTP_LAYER_BASE,
14636 nh,
14637 nkv,
14638 hd,
14639 rd,
14640 position: first_pos,
14641 scale: self.attn_scale,
14642 eps: self.rms_eps as f32,
14643 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14644 late_qk_norm: self.qk_norm_after_rope,
14645 output_gate: *output_gate,
14646 q_norm: q_norm.as_deref(),
14647 k_norm: k_norm.as_deref(),
14648 inv_freq: &inv_freq,
14649 rope_scale: self.rope_scale,
14650 cpu_k,
14651 cpu_v,
14652 cpu_stored,
14653 cpu_gen: cache.generation(),
14654 o1: None,
14655 window: None,
14656 softmax_sink: None,
14657 head_gate: None,
14658 v_norm: false,
14659 };
14660 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14661 return None;
14662 }
14663 }
14664 spec_stamp("w.enc");
14665 if !graph.submit() {
14666 return None;
14667 }
14668 spec_stamp("w.sub");
14669 Some(MetalWarmPending {
14670 graph,
14671 cpu_stored,
14672 b,
14673 })
14674 }
14675
14676 #[cfg(target_os = "macos")]
14679 fn mtp_warm_batch_metal(
14680 &mut self,
14681 m: &mut MtpModule,
14682 pairs: &[(&[f32], u32)],
14683 first_pos: usize,
14684 ) -> bool {
14685 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14686 Some(p) => self.mtp_warm_batch_finish(m, p),
14687 None => false,
14688 }
14689 }
14690
14691 #[cfg(target_os = "macos")]
14696 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14697 let MetalWarmPending {
14698 mut graph,
14699 cpu_stored,
14700 b,
14701 } = pending;
14702 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14703 if !graph.sync() {
14704 return false;
14705 }
14706 spec_stamp("w.gpu");
14707 let mut kbuf = vec![0f32; b * nkv * hd];
14708 let mut vbuf = vec![0f32; b * nkv * hd];
14709 if !crate::gpu_metal::kv_mirror_read_rows(
14710 self.mtp_kv_id(),
14711 Self::MTP_LAYER_BASE,
14712 nkv,
14713 hd,
14714 cpu_stored,
14715 b,
14716 &mut kbuf,
14717 &mut vbuf,
14718 ) {
14719 return false;
14720 }
14721 for r in 0..b {
14722 m.kv.append(
14723 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14724 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14725 &[],
14726 );
14727 }
14728 crate::gpu_metal::kv_mirror_set_stored(
14729 self.mtp_kv_id(),
14730 Self::MTP_LAYER_BASE,
14731 cpu_stored + b,
14732 );
14733 spec_stamp("w.kv");
14734 true
14735 }
14736
14737 pub(crate) fn note_draft_id(&mut self, id: u32) {
14744 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14745 if (id as usize) >= cut {
14746 self.draft_full_streak = 16;
14747 } else {
14748 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14749 }
14750 }
14751
14752 fn draft_head_rows(&self, head_rows: usize) -> usize {
14755 if self.draft_full_streak > 0 {
14756 head_rows
14757 } else {
14758 Self::draft_vocab_rows(head_rows)
14759 }
14760 }
14761
14762 fn draft_vocab_rows(head_rows: usize) -> usize {
14765 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14766 let n = *N.get_or_init(|| {
14767 std::env::var("CMF_DRAFT_VOCAB")
14768 .ok()
14769 .and_then(|v| v.parse().ok())
14770 .unwrap_or(65536)
14771 });
14772 if n == 0 { head_rows } else { n.min(head_rows) }
14773 }
14774
14775 #[cfg(target_os = "macos")]
14780 fn mtp_step_metal(
14781 &mut self,
14782 m: &mut MtpModule,
14783 hidden: &[f32],
14784 next_token: u32,
14785 position: usize,
14786 want_logits: bool,
14787 ) -> Option<(Vec<f32>, Vec<f32>)> {
14788 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14789 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14790 || !crate::gpu::q1_force()
14791 || !crate::gpu::enabled_here()
14792 || self.attn_softcap > 0.0
14793 || self.attention_heads_per_layer.is_some()
14794 || m.kv.mode != crate::kv_cache::KvMode::F32
14795 || m.kv.o1.is_some()
14796 {
14797 return None;
14798 }
14799 let AttnKind::Full {
14800 wq,
14801 wk,
14802 wv,
14803 wo,
14804 q_norm,
14805 k_norm,
14806 output_gate,
14807 softplus_gate: None,
14808 bias: None,
14809 } = &m.layer.attn
14810 else {
14811 return None;
14812 };
14813 let FfnKind::Dense(d) = &m.layer.ffn else {
14814 return None;
14815 };
14816 if d.act != Act::Silu || !d.segs.is_empty() {
14817 return None;
14818 }
14819 let (pq, pk, pv, po) = (
14820 wq.q1_parts()?,
14821 wk.q1_parts()?,
14822 wv.q1_parts()?,
14823 wo.q1_parts()?,
14824 );
14825 let (g, u, dn) = (
14826 d.gate_proj.q1_parts()?,
14827 d.up_proj.q1_parts()?,
14828 d.down_proj.q1_parts()?,
14829 );
14830 let QTensor::Mapped { model, .. } = wq else {
14831 return None;
14832 };
14833 let model = model.clone();
14834 let lm = if want_logits {
14835 Some(self.weights.lm_head.q1_parts()?)
14836 } else {
14837 None
14838 };
14839 let dims = GraphDims {
14840 hidden: self.hidden_size,
14841 eps: self.rms_eps as f32,
14842 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14843 };
14844 let hs = self.hidden_size;
14847 let mut x = vec![0f32; hs];
14848 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14855 crate::gpu_metal::DENSE_MV,
14856 );
14857 let mut graph = TokenGraph::new(&model, dims, &x)?;
14858 let mut folded = false;
14859 if let Some(eh) = m.eh_proj.q1_parts() {
14860 let e = self.embed_single(next_token);
14861 let mut cat = vec![0.0f32; 2 * hs];
14862 let (cat_e, cat_h) = cat.split_at_mut(hs);
14863 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14864 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14865 folded = graph.encode_input_proj(eh, &cat);
14866 }
14867 if !folded {
14868 x = self.mtp_block_input(m, hidden, next_token);
14869 graph = TokenGraph::new(&model, dims, &x)?;
14870 }
14871 spec_stamp("d.in");
14872 let l = AttnGpuLayer {
14873 attn_norm: &m.layer.input_norm,
14874 post_norm: &m.layer.post_norm,
14875 wq: pq,
14876 wk: pk,
14877 wv: pv,
14878 wo: po,
14879 ffn: MetalFfn::Dense {
14880 gate: g,
14881 up: u,
14882 down: dn,
14883 act: (d.act == Act::Gelu) as u8,
14884 },
14885 attn_out_norm: None,
14886 ffn_out_norm: None,
14887 layer_scale: None,
14888 };
14889 let (nh, nkv, hd, rd) = (
14890 self.num_heads,
14891 self.num_kv_heads,
14892 self.head_dim,
14893 self.rotary_dim,
14894 );
14895 let inv_freq = self.inv_freq.clone();
14896 {
14897 let cache = &m.kv;
14898 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14899 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14900 let cpu_stored = cpu_k[0].len() / hd;
14901 let p = AttnDeviceParams {
14902 kv_id: self.mtp_kv_id(),
14903 layer: Self::MTP_LAYER_BASE,
14904 nh,
14905 nkv,
14906 hd,
14907 rd,
14908 position,
14909 scale: self.attn_scale,
14910 eps: self.rms_eps as f32,
14911 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14912 late_qk_norm: self.qk_norm_after_rope,
14913 output_gate: *output_gate,
14914 q_norm: q_norm.as_deref(),
14915 k_norm: k_norm.as_deref(),
14916 inv_freq: &inv_freq,
14917 rope_scale: self.rope_scale,
14918 cpu_k,
14919 cpu_v,
14920 cpu_stored,
14921 cpu_gen: cache.generation(),
14922 o1: None,
14923 window: None,
14924 softmax_sink: None,
14925 head_gate: None,
14926 v_norm: false,
14927 };
14928 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14929 return None;
14930 }
14931 }
14932 let draft_rows = if let Some(lm) = lm {
14938 self.draft_head_rows(lm.1)
14939 } else {
14940 0
14941 };
14942 if let Some(lm) = lm {
14943 if !graph.lm_head_ok(lm) {
14944 return None;
14945 }
14946 if draft_rows < lm.1 {
14947 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14948 return None;
14949 }
14950 } else {
14951 graph.encode_lm_head(&m.final_norm, lm);
14952 }
14953 }
14954 spec_stamp("d.enc");
14955 if graph.sync_checked().is_err() {
14956 return None;
14957 }
14958 spec_stamp("d.gpu");
14959 let mut logits = Vec::new();
14960 if let Some(lm) = lm {
14961 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14962 logits = attention::take_buf(n_read);
14963 graph.read_logits(&mut logits);
14964 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14966 }
14967 graph.finish(&mut x);
14968 let mut krow = attention::take_buf(nkv * hd);
14969 let mut vrow = attention::take_buf(nkv * hd);
14970 if crate::gpu_metal::kv_mirror_read_last(
14971 self.mtp_kv_id(),
14972 Self::MTP_LAYER_BASE,
14973 nkv,
14974 hd,
14975 &mut krow,
14976 &mut vrow,
14977 ) {
14978 m.kv.append(&krow, &vrow, &[]);
14979 }
14980 attention::recycle_buf(&mut krow);
14981 attention::recycle_buf(&mut vrow);
14982 spec_stamp("d.rd");
14983 Some((logits, x))
14984 }
14985
14986 fn mtp_chain_on() -> bool {
14999 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15000 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
15001 }
15002
15003 #[cfg(target_os = "macos")]
15015 fn mtp_draft_chain_metal(
15016 &mut self,
15017 m: &mut MtpModule,
15018 hidden: &[f32],
15019 t_next: u32,
15020 position: usize,
15021 k: usize,
15022 ) -> Result<Vec<u32>, bool> {
15023 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15024 if k == 0
15025 || k > 64
15026 || !Self::mtp_chain_on()
15027 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15028 || !crate::gpu::q1_force()
15029 || !crate::gpu::enabled_here()
15030 || self.attn_softcap > 0.0
15031 || self.attention_heads_per_layer.is_some()
15032 || m.kv.mode != crate::kv_cache::KvMode::F32
15033 || m.kv.o1.is_some()
15034 || self.dsv4.is_some()
15036 || self.dsv41.is_some()
15037 || self.qwen4_exp.is_some()
15038 || self.per_token_stack()
15039 {
15040 return Err(false);
15041 }
15042 let AttnKind::Full {
15043 wq,
15044 wk,
15045 wv,
15046 wo,
15047 q_norm,
15048 k_norm,
15049 output_gate,
15050 softplus_gate: None,
15051 bias: None,
15052 } = &m.layer.attn
15053 else {
15054 return Err(false);
15055 };
15056 let FfnKind::Dense(d) = &m.layer.ffn else {
15057 return Err(false);
15058 };
15059 if d.act != Act::Silu || !d.segs.is_empty() {
15060 return Err(false);
15061 }
15062 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15063 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15064 else {
15065 return Err(false);
15066 };
15067 let (Some(g), Some(u), Some(dn)) = (
15068 d.gate_proj.q1_parts(),
15069 d.up_proj.q1_parts(),
15070 d.down_proj.q1_parts(),
15071 ) else {
15072 return Err(false);
15073 };
15074 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
15075 return Err(false);
15076 };
15077 let QTensor::Mapped { model, .. } = wq else {
15078 return Err(false);
15079 };
15080 let model = model.clone();
15081 let QTensor::Mapped {
15084 model: em,
15085 idx: eidx,
15086 dtype: cortiq_core::TensorDtype::Q4TiledP,
15087 ..
15088 } = &self.weights.embed_tokens
15089 else {
15090 return Err(false);
15091 };
15092 if !std::sync::Arc::ptr_eq(em, &model)
15093 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
15094 {
15095 return Err(false);
15096 }
15097 let embed = (
15098 *eidx,
15099 self.weights.embed_tokens.rows(),
15100 self.weights.embed_tokens.cols(),
15101 );
15102 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
15103 return Err(false);
15104 }
15105 let dims = GraphDims {
15106 hidden: self.hidden_size,
15107 eps: self.rms_eps as f32,
15108 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15109 };
15110 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15114 crate::gpu_metal::DENSE_MV,
15115 );
15116 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
15117 return Err(false);
15118 };
15119 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
15120 return Err(false);
15121 }
15122 let l = AttnGpuLayer {
15123 attn_norm: &m.layer.input_norm,
15124 post_norm: &m.layer.post_norm,
15125 wq: pq,
15126 wk: pk,
15127 wv: pv,
15128 wo: po,
15129 ffn: MetalFfn::Dense {
15130 gate: g,
15131 up: u,
15132 down: dn,
15133 act: (d.act == Act::Gelu) as u8,
15134 },
15135 attn_out_norm: None,
15136 ffn_out_norm: None,
15137 layer_scale: None,
15138 };
15139 let (nh, nkv, hd, rd) = (
15140 self.num_heads,
15141 self.num_kv_heads,
15142 self.head_dim,
15143 self.rotary_dim,
15144 );
15145 let inv_freq = self.inv_freq.clone();
15146 let draft_rows = self.draft_head_rows(lm.1);
15147 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15148 if n_arg == 0 {
15149 return Err(false);
15150 }
15151 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15158 let t_chain = std::time::Instant::now();
15159 graph.chain_ids_init(t_next, k);
15160 let cpu_stored;
15161 {
15162 let cache = &m.kv;
15163 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15164 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15165 cpu_stored = cpu_k[0].len() / hd;
15166 for j in 0..k {
15167 if !graph.encode_chain_input(
15168 embed,
15169 j as u32,
15170 &m.enorm,
15171 &m.hnorm,
15172 self.embed_multiplier,
15173 eh,
15174 ) {
15175 return Err(false);
15176 }
15177 let p = AttnDeviceParams {
15181 kv_id: self.mtp_kv_id(),
15182 layer: Self::MTP_LAYER_BASE,
15183 nh,
15184 nkv,
15185 hd,
15186 rd,
15187 position: position + j,
15188 scale: self.attn_scale,
15189 eps: self.rms_eps as f32,
15190 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15191 late_qk_norm: self.qk_norm_after_rope,
15192 output_gate: *output_gate,
15193 q_norm: q_norm.as_deref(),
15194 k_norm: k_norm.as_deref(),
15195 inv_freq: &inv_freq,
15196 rope_scale: self.rope_scale,
15197 cpu_k: cpu_k.clone(),
15198 cpu_v: cpu_v.clone(),
15199 cpu_stored: cpu_stored + j,
15200 cpu_gen: cache.generation(),
15201 o1: None,
15202 window: None,
15203 softmax_sink: None,
15204 head_gate: None,
15205 v_norm: false,
15206 };
15207 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15208 return Err(false);
15209 }
15210 if draft_rows < lm.1 {
15211 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15212 return Err(false);
15213 }
15214 } else {
15215 graph.encode_lm_head(&m.final_norm, lm);
15216 }
15217 if !graph.encode_argmax(n_arg, j as u32 + 1) {
15218 return Err(false);
15219 }
15220 if split {
15221 graph.commit();
15224 }
15225 }
15226 }
15227 let t_enc = t_chain.elapsed();
15228 if graph.sync_checked().is_err() {
15229 return Err(true);
15230 }
15231 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
15232 eprintln!(
15233 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
15234 t_enc.as_secs_f64() * 1e3,
15235 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
15236 if split { ", split" } else { "" }
15237 );
15238 }
15239 let mut ids = vec![0u32; k];
15240 if !graph.chain_ids_read(&mut ids) {
15241 return Err(true);
15242 }
15243 let mut kbuf = vec![0f32; k * nkv * hd];
15244 let mut vbuf = vec![0f32; k * nkv * hd];
15245 if !crate::gpu_metal::kv_mirror_read_rows(
15246 self.mtp_kv_id(),
15247 Self::MTP_LAYER_BASE,
15248 nkv,
15249 hd,
15250 cpu_stored,
15251 k,
15252 &mut kbuf,
15253 &mut vbuf,
15254 ) {
15255 return Err(true);
15256 }
15257 for r in 0..k {
15258 m.kv.append(
15259 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15260 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15261 &[],
15262 );
15263 }
15264 Ok(ids)
15265 }
15266
15267 fn try_batch_graph_wgpu(
15268 &self,
15269 hiddens: &mut [f32],
15270 positions: &[usize],
15271 k: usize,
15272 spec: Option<crate::gpu::SpecTail<'_>>,
15273 ) -> crate::gpu::BatchGraphOutcome {
15274 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15275 }
15276
15277 fn try_batch_graph_wgpu_prefix(
15282 &self,
15283 hiddens: &mut [f32],
15284 positions: &[usize],
15285 k: usize,
15286 spec: Option<crate::gpu::SpecTail<'_>>,
15287 layers_run: Option<&mut usize>,
15288 ) -> crate::gpu::BatchGraphOutcome {
15289 let graph_end = match self.mimo_moe.graph_prefix_end() {
15290 Some(end) if end < self.num_layers => {
15291 if layers_run.is_none() || spec.is_some() || end == 0 {
15292 return crate::gpu::BatchGraphOutcome::Declined;
15293 }
15294 end
15295 }
15296 _ => self.num_layers,
15297 };
15298 let _tb = std::time::Instant::now();
15299 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15300 if self.attn_softcap > 0.0 {
15301 return crate::gpu::BatchGraphOutcome::Declined; }
15303 if let Some(reason) = self
15306 .wgpu_graph_attn_decline()
15307 .or_else(|| self.batch_graph_gemma_decline())
15308 {
15309 self.note_graph_decline("wgpu batch graph", reason);
15310 return crate::gpu::BatchGraphOutcome::Declined;
15311 }
15312 let nh = self.num_heads;
15313 let (nkv, hd, rd) = self.layer_geom(0);
15314 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15315 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15316 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15317 {
15318 let name = &m.tensors[i].name;
15319 let prism = if crate::prism::is_inverse_embedding(m, name) {
15320 crate::gpu::GraphPrismOp::InverseEmbedding
15321 } else if crate::prism::is_forward_weight(m, name) {
15322 crate::gpu::GraphPrismOp::Forward
15323 } else {
15324 crate::gpu::GraphPrismOp::None
15325 };
15326 return Some(crate::gpu::GraphW {
15327 idx: i,
15328 kind,
15329 row_scale: rs,
15330 data: &[],
15331 prism,
15332 affine: crate::prism::is_affine_target(m, name),
15333 });
15334 }
15335 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15336 eprintln!(
15337 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15338 t.rows(),
15339 t.cols()
15340 );
15341 }
15342 t.as_f32().map(|d| crate::gpu::GraphW {
15343 idx: 0,
15344 kind: 4,
15345 row_scale: &[],
15346 data: d,
15347 prism: crate::gpu::GraphPrismOp::None,
15348 affine: false,
15349 })
15350 }
15351 let built: Option<(
15352 Vec<crate::gpu::GraphLayer<'_>>,
15353 std::sync::Arc<cortiq_core::CmfModel>,
15354 )> = (|| {
15355 let mut layers = Vec::with_capacity(graph_end);
15356 let mut model = None;
15357 for li in 0..graph_end {
15358 let lw = &self.weights.layers[self.phys_layer(li)];
15359 let gffn = match &lw.ffn {
15366 FfnKind::Dense(d) if !d.segs.is_empty() => {
15367 if batch_debug {
15368 eprintln!("batch graph: dense segmented FFN at layer {li}");
15369 }
15370 return None;
15371 }
15372 FfnKind::Dense(d) => {
15373 let Some(act) = d.act.wgpu_graph_act() else {
15374 if batch_debug {
15375 eprintln!(
15376 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15377 d.act
15378 );
15379 }
15380 return None;
15381 };
15382 crate::gpu::GraphFfn::Dense {
15383 gate: gw(&d.gate_proj)?,
15384 up: gw(&d.up_proj)?,
15385 down: gw(&d.down_proj)?,
15386 act,
15387 }
15388 }
15389 FfnKind::Moe(m) => {
15390 if m.route_tau.is_some() || m.mask.is_some() {
15397 return None;
15398 }
15399 let shared = m.shared.as_ref();
15403 let has_shared = shared.is_some();
15404 let shared_gated = matches!(shared, Some((_, Some(_))));
15405 let sgate = match shared {
15406 Some((_, Some(sg))) => gw(sg)?,
15407 _ => gw(&m.router)?,
15411 };
15412 let router = gw(&m.router)?;
15413 if router.prism != crate::gpu::GraphPrismOp::None
15419 || router.affine
15420 || sgate.prism != crate::gpu::GraphPrismOp::None
15421 || sgate.affine
15422 {
15423 return None;
15424 }
15425 let inter = m.experts.first()?.gate_proj.rows();
15426 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15427 let mut q4tp: Option<bool> = None;
15428 let mut gu_q2: Option<bool> = None;
15429 let mut q82: Option<bool> = None;
15430 let mut moe_act = None;
15434 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15435 let Some(e_act) = e.act.wgpu_graph_act() else {
15436 return None;
15437 };
15438 match moe_act {
15439 Some(existing) if existing != e_act => return None,
15440 None => moe_act = Some(e_act),
15441 _ => {}
15442 }
15443 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15444 return None;
15445 }
15446 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15450 {
15451 Some((mm, gi)) => (
15452 mm,
15453 gi,
15454 e.up_proj.mapped_q4t()?.1,
15455 e.down_proj.mapped_q4t()?.1,
15456 false,
15457 false,
15458 false,
15459 ),
15460 None => match e.gate_proj.mapped_q2tp() {
15461 Some((mm, gi)) => (
15462 mm,
15463 gi,
15464 e.up_proj.mapped_q2tp()?.1,
15465 e.down_proj.mapped_q4tp()?.1,
15466 true,
15467 true,
15468 false,
15469 ),
15470 None => match e.gate_proj.mapped_q4tp() {
15471 Some((mm, gi)) => (
15472 mm,
15473 gi,
15474 e.up_proj.mapped_q4tp()?.1,
15475 e.down_proj.mapped_q4tp()?.1,
15476 true,
15477 false,
15478 false,
15479 ),
15480 None => {
15481 if !crate::gpu::moe_q82_graph_on() {
15482 return None;
15483 }
15484 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15485 (
15486 mm,
15487 gi,
15488 e.up_proj.mapped_q8_2f()?.1,
15489 e.down_proj.mapped_q8_2f()?.1,
15490 false,
15491 false,
15492 true,
15493 )
15494 }
15495 },
15496 },
15497 };
15498 if *q4tp.get_or_insert(is_p) != is_p
15499 || *gu_q2.get_or_insert(is_q2) != is_q2
15500 || *q82.get_or_insert(is_8) != is_8
15501 {
15502 return None;
15503 }
15504 if [gi, ui, di].into_iter().any(|idx| {
15505 mm.tensors.get(idx).is_some_and(|t| {
15506 crate::prism::is_forward_weight(mm, &t.name)
15507 || crate::prism::is_affine_target(mm, &t.name)
15508 })
15509 }) {
15510 return None;
15511 }
15512 model.get_or_insert_with(|| mm.clone());
15513 experts.push((gi, ui, di));
15514 }
15515 crate::gpu::GraphFfn::Moe {
15516 router,
15517 shared_gate: sgate,
15518 experts,
15519 n_exp: m.experts.len(),
15520 top_k: m.top_k,
15521 inter,
15522 act: moe_act?,
15523 norm_topk: m.norm_topk_prob,
15524 q4tp: q4tp?,
15525 gu_q2: gu_q2.unwrap_or(false),
15526 q82: q82.unwrap_or(false),
15527 sigmoid: m.router_sigmoid,
15528 sigmoid_denom_eps: m.sigmoid_denom_eps,
15529 bias: m.expert_bias.as_deref(),
15530 has_shared,
15531 detached_shared: None,
15532 shared_gated,
15533 route_scale: m.routed_scaling,
15534 }
15535 }
15536 _ => return None,
15537 };
15538 let attn = match &lw.attn {
15539 AttnKind::Full {
15540 wq,
15541 wk,
15542 wv,
15543 wo,
15544 q_norm,
15545 k_norm,
15546 output_gate,
15547 softplus_gate,
15548 bias,
15549 } => {
15550 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15551 if batch_debug {
15552 eprintln!(
15553 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15554 softplus_gate.is_some(),
15555 self.attention_heads_per_layer.is_some()
15556 );
15557 }
15558 return None;
15559 }
15560 let (m, _, _, _) =
15561 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15562 model = Some(m.clone());
15563 crate::gpu::GraphAttn::Full {
15564 wq: gw(wq)?,
15565 wk: gw(wk)?,
15566 wv: gw(wv)?,
15567 wo: gw(wo)?,
15568 q_norm: q_norm.as_deref(),
15569 k_norm: k_norm.as_deref(),
15570 late_qk_norm: self.qk_norm_after_rope,
15571 bias: bias
15572 .as_ref()
15573 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15574 output_gate: *output_gate,
15575 cpu_k: self.kv_cache.layers[li].k_heads(),
15576 cpu_v: self.kv_cache.layers[li].v_heads(),
15577 cpu_base: self.kv_cache.layers[li].base(),
15578 geom: self.graph_attn_geom(li),
15579 head_gate: None,
15582 }
15583 }
15584 AttnKind::LinearGdn(w) => {
15585 let Some(cfg) = self.gdn_cfg else {
15586 if batch_debug {
15587 eprintln!("batch graph: no GDN config at layer {li}");
15588 }
15589 return None;
15590 };
15591 let (m, _, _, _) = w
15592 .in_proj_qkv
15593 .graph_weight()
15594 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15595 model = Some(m.clone());
15596 crate::gpu::GraphAttn::Gdn {
15597 qkv: gw(&w.in_proj_qkv)?,
15598 z: gw(&w.in_proj_z)?,
15599 a: gw(&w.in_proj_a)?,
15600 b: gw(&w.in_proj_b)?,
15601 out: gw(&w.out_proj)?,
15602 conv1d: &w.conv1d,
15603 a_log: &w.a_log,
15604 dt_bias: &w.dt_bias,
15605 norm: &w.norm,
15606 nv: cfg.num_v_heads,
15607 nk: cfg.num_k_heads,
15608 dk: cfg.key_head_dim,
15609 dv: cfg.value_head_dim,
15610 kk: cfg.conv_kernel,
15611 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15612 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15613 }
15614 }
15615 _ => return None,
15616 };
15617 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15620 layers.push(crate::gpu::GraphLayer {
15621 input_norm: &lw.input_norm,
15622 attn,
15623 post_norm: &lw.post_norm,
15624 attn_out_norm: if g4 {
15625 lw.attn_out_norm.as_deref()
15626 } else {
15627 None
15628 },
15629 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15630 layer_scale: if g4 { lw.layer_scale } else { None },
15631 ffn: gffn,
15632 });
15633 }
15634 Some((layers, model?))
15635 })();
15636 let Some((layers, model)) = built else {
15637 {
15638 use std::sync::atomic::{AtomicBool, Ordering};
15639 static SAID: AtomicBool = AtomicBool::new(false);
15640 if !SAID.swap(true, Ordering::Relaxed) {
15641 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15642 }
15643 }
15644 return crate::gpu::BatchGraphOutcome::Declined;
15645 };
15646 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15647 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15648 }
15649 crate::gpu::forward_batch_graph(
15650 &model,
15651 self.graph_kv_id,
15652 &layers,
15653 &self.inv_freq,
15654 hiddens,
15655 nh,
15656 nkv,
15657 hd,
15658 rd,
15659 self.hidden_size,
15660 self.intermediate_size,
15661 positions,
15662 self.kv_cache.max_seq_len,
15663 gemma,
15664 self.rms_eps as f32,
15665 self.attn_scale,
15666 k,
15667 &(0..graph_end)
15668 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15669 .collect::<Vec<_>>(),
15670 self.o1_epoch,
15671 spec,
15672 layers_run,
15673 )
15674 }
15675
15676 fn draft_probe() -> bool {
15680 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15681 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15682 }
15683
15684 #[cfg(feature = "gpu")]
15696 fn dsv4_spec_on() -> bool {
15697 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15698 *ON.get_or_init(|| {
15699 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15703 return v != "0";
15704 }
15705 std::env::var("CMF_DSV4_SPEC")
15712 .map(|v| v != "0")
15713 .unwrap_or_else(|_| {
15714 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15715 })
15716 })
15717 }
15718
15719 #[cfg(feature = "gpu")]
15726 fn dsv4_spec_step(
15727 &mut self,
15728 tip_token: u32,
15729 t_next: u32,
15730 next_pos: usize,
15731 max_extra: usize,
15732 drafted: &mut usize,
15733 accepted_ctr: &mut usize,
15734 ) -> Option<(Vec<u32>, usize)> {
15735 let t_all = std::time::Instant::now();
15736 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15737 thread_local! {
15738 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15739 const { std::cell::Cell::new(None) };
15740 }
15741 LAST.with(|l| {
15742 if let Some(prev) = l.get() {
15743 eprintln!(
15744 "between rounds {:.1} ms",
15745 prev.elapsed().as_secs_f64() * 1e3
15746 );
15747 }
15748 l.set(Some(std::time::Instant::now()));
15749 });
15750 }
15751 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15752 eprintln!("spec_step: enter pos={next_pos}");
15753 }
15754 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15755 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15756 if self.dspark.is_none() {
15758 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15759 if t.is_empty() {
15760 return None;
15761 }
15762 crate::dsv4::dspark_arm(&t, cfg.dim);
15763 self.dspark = Some(crate::dsv4::DsparkState::new(
15764 self.dsv4_mtp.len(),
15765 &cfg,
15766 t.len(),
15767 ));
15768 }
15769 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15770 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15771 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15772 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15773 }
15774 let pack = pack?;
15775 let block = crate::dsv4::dspark_block();
15776 let b_box = self.dsv4.as_mut()?;
15777 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15778 let ds = self.dspark.as_mut()?;
15779 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15782 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15783 if dbg {
15784 eprintln!("spec_step: no capture");
15785 }
15786 return None;
15787 }
15788 ds.have_hidden = true;
15789 let tip_pos = next_pos.checked_sub(1)?;
15790 let draft_started = std::time::Instant::now();
15791 let mut conf = Vec::new();
15792 let props = crate::dsv4::dspark_draft_gpu(
15793 g,
15794 &self.dsv4_mtp,
15795 &cfg,
15796 ds,
15797 pack,
15798 st.kv_id,
15799 tip_token,
15800 tip_pos,
15801 self.pool.as_deref(),
15802 &mut conf,
15803 );
15804 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15805 *drafted += block;
15806 if props.is_empty() || props[0] != t_next {
15807 if dbg {
15808 eprintln!(
15809 "spec_step: draft {} (props0={:?} t_next={t_next})",
15810 if props.is_empty() {
15811 "empty"
15812 } else {
15813 "miss"
15814 },
15815 props.first()
15816 );
15817 }
15818 return None;
15819 }
15820 let mut k_verify = crate::dsv4::dspark_verify_k()
15827 .min(props.len())
15828 .min(max_extra.saturating_add(1));
15829 let conf_min = {
15835 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15836 *M.get_or_init(|| {
15837 std::env::var("CMF_DSPARK_CONF_MIN")
15838 .ok()
15839 .and_then(|v| v.parse().ok())
15840 .unwrap_or(0.0)
15841 })
15842 };
15843 if conf_min > 0.0 && conf.len() >= props.len() {
15844 let mut keep = 1usize;
15845 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15846 keep += 1;
15847 }
15848 k_verify = k_verify.min(keep.max(2));
15849 }
15850 if k_verify < 2 {
15851 return None;
15852 }
15853 let mut fed = Vec::with_capacity(k_verify);
15854 fed.push(t_next);
15855 fed.extend_from_slice(&props[1..k_verify]);
15856 let mut argmax = Vec::new();
15857 let mut logits_all = Vec::new();
15858 let mut walked = Vec::new();
15859 let txn = crate::dsv4::dsv4_verify_chunk(
15860 g,
15861 layers,
15862 &cfg,
15863 st,
15864 &fed,
15865 next_pos,
15866 &self.inv_freq,
15867 self.pool.as_deref(),
15868 &targets,
15869 &mut argmax,
15870 &mut logits_all,
15871 &mut walked,
15872 );
15873 if txn.is_none() && dbg {
15874 eprintln!("spec_step: verify declined");
15875 }
15876 let txn = txn?;
15877 let spec_gpu_end = txn.gpu_end;
15878 let b = fed.len();
15879 let mut accepted = 1usize;
15880 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15881 accepted += 1;
15882 }
15883 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15888 accepted = 1;
15889 }
15890 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15891 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15892 }
15893 let t_fin = std::time::Instant::now();
15894 if !crate::dsv4::dsv4_spec_finish(
15895 g,
15896 layers,
15897 &cfg,
15898 st,
15899 txn,
15900 accepted,
15901 &fed,
15902 &self.inv_freq,
15903 self.pool.as_deref(),
15904 ) {
15905 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15906 return None;
15907 }
15908 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15909 eprintln!(
15910 "finish(k={accepted}): {:.1} ms",
15911 t_fin.elapsed().as_secs_f64() * 1e3
15912 );
15913 }
15914 *accepted_ctr += accepted - 1;
15915 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15920 let dev_caps: Vec<usize> = targets
15925 .iter()
15926 .copied()
15927 .filter(|&t| t < spec_gpu_end)
15928 .collect();
15929 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15930 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15931 return None;
15932 }
15933 for t in 0..accepted {
15934 let tip = t + 1 == accepted;
15935 for (slot, &tl) in targets.iter().enumerate() {
15936 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15937 let lo = (di * b + t) * hc * dim;
15938 crate::dsv4::dspark_capture(
15939 &caps_all[lo..lo + hc * dim],
15940 &cfg,
15941 slot,
15942 &mut ds.main_hidden,
15943 );
15944 } else if tip
15945 && crate::dsv4::dspark_peek_slot(slot, dim, {
15946 let lo = slot * dim;
15947 &mut ds.main_hidden[lo..lo + dim]
15948 })
15949 {
15950 } else {
15955 crate::dsv4::dspark_capture(
15959 &walked[t * hc * dim..(t + 1) * hc * dim],
15960 &cfg,
15961 slot,
15962 &mut ds.main_hidden,
15963 );
15964 }
15965 }
15966 crate::dsv4::dspark_ring_append(
15967 g,
15968 &self.dsv4_mtp,
15969 &cfg,
15970 ds,
15971 next_pos + t,
15972 self.pool.as_deref(),
15973 );
15974 }
15975 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15976 self.graph_logits = Some(row);
15977 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15982 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15983 crate::dsv4::pick_tally_arm();
15984 }
15985 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15986 eprintln!(
15987 "spec_step total {:.1} ms (k={accepted})",
15988 t_all.elapsed().as_secs_f64() * 1e3
15989 );
15990 }
15991 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15992 }
15993
15994 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15995 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15996 return;
15997 }
15998 let trunk_now = crate::dsv4::pick_tally_take();
16000 crate::dsv4::trunk_freq_note(&trunk_now);
16001 if !trunk_now.is_empty() {
16002 self.dspark_trunk_picks.push(trunk_now);
16003 let keep = crate::dsv4::dspark_block();
16004 if self.dspark_trunk_picks.len() > keep {
16005 self.dspark_trunk_picks.remove(0);
16006 }
16007 }
16008 for p in std::mem::take(&mut self.dspark_pending) {
16011 let Some(i) = position.checked_sub(p.0 + 1) else {
16012 continue;
16013 };
16014 let mut p = p;
16015 if i < p.1.len() {
16016 if p.2 && p.1[i] == token_id {
16017 p.3 = i + 1;
16018 } else {
16019 p.2 = false;
16020 }
16021 if i + 1 < p.1.len() {
16022 self.dspark_pending.push(p);
16023 continue;
16024 }
16025 }
16026 self.dspark_hist.push(p.3);
16027 self.dspark_real.push(token_id);
16028 }
16029 let Some(b) = &mut self.dsv4 else { return };
16030 let (g, layers, cfg) = (&b.0, &b.1, b.2);
16031 let n_layers = layers.len();
16032 if self.dspark.is_none() {
16033 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16034 if t.is_empty() {
16035 return;
16036 }
16037 eprintln!(
16038 "DSpark: capture from layers {t:?}, block {}",
16039 crate::dsv4::dspark_block()
16040 );
16041 crate::dsv4::dspark_arm(&t, cfg.dim);
16042 self.dspark = Some(crate::dsv4::DsparkState::new(
16043 self.dsv4_mtp.len(),
16044 &cfg,
16045 t.len(),
16046 ));
16047 }
16048 let ds = self.dspark.as_mut().unwrap();
16049 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
16050 return; }
16052 let mut conf = Vec::new();
16053 crate::dsv4::pick_tally_arm();
16054 let draft_started = std::time::Instant::now();
16059 #[cfg(feature = "gpu")]
16060 let gpu_draft = crate::dsv4::dspark_gpu_on();
16061 #[cfg(not(feature = "gpu"))]
16062 let gpu_draft = false;
16063 let props = if gpu_draft {
16064 #[cfg(feature = "gpu")]
16065 {
16066 let kv_id = b.3.kv_id;
16067 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
16068 Some(pk) => crate::dsv4::dspark_draft_gpu(
16069 g,
16070 &self.dsv4_mtp,
16071 &cfg,
16072 ds,
16073 pk,
16074 kv_id,
16075 token_id,
16076 position,
16077 self.pool.as_deref(),
16078 &mut conf,
16079 ),
16080 None => Vec::new(),
16081 }
16082 }
16083 #[cfg(not(feature = "gpu"))]
16084 Vec::new()
16085 } else {
16086 crate::gpu::cpu_scope(|| {
16087 crate::dsv4::dspark_draft(
16088 g,
16089 &self.dsv4_mtp,
16090 &cfg,
16091 ds,
16092 token_id,
16093 position,
16094 self.pool.as_deref(),
16095 &mut conf,
16096 )
16097 })
16098 };
16099 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16100 let draft_picks = crate::dsv4::pick_tally_take();
16101 crate::dsv4::dspark_freq_note(&draft_picks);
16102 crate::dsv4::pick_tally_arm();
16105 if !props.is_empty() {
16106 let (tu, tt) = {
16110 let flat: Vec<(usize, Vec<usize>)> = self
16111 .dspark_trunk_picks
16112 .iter()
16113 .flat_map(|v| v.iter().cloned())
16114 .collect();
16115 let mut per: std::collections::HashMap<usize, Vec<usize>> =
16117 std::collections::HashMap::new();
16118 for (li, picks) in flat {
16119 per.entry(li).or_default().extend(picks);
16120 }
16121 let n = per.len().max(1);
16122 let mut u = 0usize;
16123 let mut t = 0usize;
16124 for (_, v) in per {
16125 t += v.len();
16126 u += v.iter().collect::<std::collections::HashSet<_>>().len();
16127 }
16128 (u / n, t / n)
16129 };
16130 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
16131 self.dspark_exp.push((tu, tt, du, dt));
16132 self.dspark_pending.push((position, props, true, 0));
16133 }
16134 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
16135 let n = self.dspark_hist.len() as f32;
16136 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
16137 let block = crate::dsv4::dspark_block();
16138 let mut at = vec![0usize; block + 1];
16139 for &k in &self.dspark_hist {
16140 at[k] += 1;
16141 }
16142 let mut surv = Vec::with_capacity(block);
16144 for i in 1..=block {
16145 let k = at[i..].iter().sum::<usize>() as f32 / n;
16146 surv.push(format!("{k:.2}"));
16147 }
16148 let distinct = self
16149 .dspark_real
16150 .iter()
16151 .collect::<std::collections::HashSet<_>>()
16152 .len();
16153 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16154 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16155 });
16156 let m = self.dspark_exp.len().max(1);
16157 eprintln!(
16158 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16159 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16160 self.dspark_hist.len(),
16161 mean + 1.0,
16162 surv.join(" ")
16163 );
16164 eprintln!(
16165 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16166 trunk experts {}/{} per layer over {block} tokens, \
16167 draft {}/{} per block, draft {:.2} ms/block",
16168 self.dspark_real.len(),
16169 tu / m,
16170 tt / m,
16171 du / m,
16172 dt / m,
16173 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16174 );
16175 }
16176 }
16177
16178 fn forward_layers_upto(
16179 &mut self,
16180 hidden: &[f32],
16181 position: usize,
16182 task_mask: Option<&TaskMask>,
16183 upto: Option<usize>,
16184 ) -> Vec<f32> {
16185 if let Some(plan) = self.gpu_plan.clone() {
16191 if upto.is_none() && plan.len() > 1 {
16192 let mut h = hidden.to_vec();
16193 for &(dev, from, upto_incl) in plan.iter() {
16194 h = crate::gpu::with_device(dev, || {
16195 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
16196 });
16197 }
16198 return h;
16199 }
16200 }
16201 self.forward_layers_span(hidden, position, task_mask, 0, upto)
16202 }
16203
16204 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
16209 self.set_gpu_plan_at(devices, None)
16210 }
16211
16212 pub fn set_gpu_plan_at(
16216 &mut self,
16217 devices: Option<&[usize]>,
16218 at: Option<usize>,
16219 ) -> Result<(), String> {
16220 let Some(devs) = devices.filter(|d| d.len() > 1) else {
16221 self.gpu_plan = None;
16222 return Ok(());
16223 };
16224 self.split_supported()?;
16225 let n = self.num_layers;
16226 if devs.len() > n {
16227 return Err(format!("{} devices for {n} layers", devs.len()));
16228 }
16229 if let Some(k) = at {
16230 if k == 0 || k >= n {
16231 return Err(format!("split at {k}: the model has {n} layers"));
16232 }
16233 if devs.len() == 2 {
16234 self.gpu_plan = Some(std::sync::Arc::new(vec![
16235 (devs[0], 0, k - 1),
16236 (devs[1], k, n - 1),
16237 ]));
16238 return Ok(());
16239 }
16240 return Err(format!(
16241 "an explicit split point takes exactly 2 devices, got {}",
16242 devs.len()
16243 ));
16244 }
16245 let per = n.div_ceil(devs.len());
16246 let mut plan = Vec::with_capacity(devs.len());
16247 let mut from = 0usize;
16248 for &d in devs {
16249 if from >= n {
16250 break;
16251 }
16252 let upto = (from + per - 1).min(n - 1);
16253 plan.push((d, from, upto));
16254 from = upto + 1;
16255 }
16256 self.gpu_plan = Some(std::sync::Arc::new(plan));
16257 Ok(())
16258 }
16259
16260 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16262 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16263 }
16264
16265 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16271 if let Some(x) = t.as_f32() {
16272 return x.to_vec();
16273 }
16274 let mut out = vec![0.0; t.rows() * t.cols()];
16275 for r in 0..t.rows() {
16276 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16277 }
16278 out
16279 }
16280
16281 fn embryo_resident_eligible(&self) -> bool {
16282 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16285 || self.num_layers != self.physical_layers
16286 || self.loop_final_norm
16287 || self.weights.layers.len() != self.num_layers
16288 || self.head_clusters.is_none()
16289 || self.final_softcap.is_some()
16290 || self.logit_multiplier.is_some()
16291 || self.attn_softcap != 0.0
16292 || self.mtp.is_some()
16293 || self.per_token_stack()
16294 || self.dsv4.is_some()
16295 || self.dsv41.is_some()
16296 || self.qwen4_exp.is_some()
16297 || self.dyn_router.is_some()
16304 || self.dyn_phi_layer.is_some()
16305 || self.dyn_blend_loaded
16306 || self.o1_cfg.is_some()
16307 || self.swa.is_some()
16308 || self.sliding_layers.is_some()
16309 || self.global_attn.is_some()
16310 || self.attention_heads_per_layer.is_some()
16311 || self.attn_v_norm
16312 || self
16313 .kv_cache
16314 .layers
16315 .iter()
16316 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16317 || self.rope_scale != 1.0
16318 || self.rope_scale_local != 1.0
16319 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16320 || self.hidden_size == 0
16321 || self.hidden_size > 1024
16322 || self.intermediate_size > 1024
16323 || self.num_heads == 0
16324 || self.num_kv_heads == 0
16325 || self.num_heads % self.num_kv_heads != 0
16326 || self.num_heads.saturating_mul(self.head_dim) > 1024
16327 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16328 || self.vocab_size == 0
16329 || self.kv_cache.max_seq_len == 0
16330 || self.rotary_dim == 0
16331 || self.rotary_dim > self.head_dim
16332 || self.rotary_dim % 2 != 0
16333 || self.inv_freq.len() < self.rotary_dim / 2
16334 {
16335 return false;
16336 }
16337 if self.weights.lm_head.as_f32().is_none()
16348 || self.weights.embed_tokens.as_f32().is_none()
16349 || self.weights.lm_head.rows() < self.vocab_size
16350 || self.weights.lm_head.cols() != self.hidden_size
16351 || self.weights.embed_tokens.rows() < self.vocab_size
16352 || self.weights.embed_tokens.cols() != self.hidden_size
16353 || self.weights.final_norm.len() != self.hidden_size
16354 {
16355 return false;
16356 }
16357 if let Some(cfg) = self.vmf_cfg {
16358 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16359 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16360 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16361 || cfg.state_len() == 0
16362 {
16363 return false;
16364 }
16365 }
16366 if let Some(g) = self.gdn_cfg {
16367 if g.num_v_heads == 0
16372 || g.num_k_heads == 0
16373 || g.num_v_heads % g.num_k_heads != 0
16374 || g.key_head_dim == 0
16375 || g.key_head_dim > 128
16376 || g.value_head_dim == 0
16377 || g.value_head_dim > 256
16378 || g.value_head_dim % 4 != 0
16379 || g.conv_kernel == 0
16380 || g.num_v_heads > 512
16381 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16382 || g.conv_dim() > 2048
16383 || g.conv_dim() % 4 != 0
16384 || g.hidden_size != self.hidden_size
16385 || g.output_gate_sigmoid
16386 || g.rms_eps != self.rms_eps
16387 || g.state_len() == 0
16388 {
16389 return false;
16390 }
16391 }
16392 let mut full_seen = false;
16393 for lw in &self.weights.layers {
16394 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16395 return false;
16396 }
16397 match &lw.attn {
16398 AttnKind::LinearGdn(w) => {
16399 let Some(g) = self.gdn_cfg else {
16400 return false;
16401 };
16402 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16403 if w.in_proj_qkv.rows() != g.conv_dim()
16404 || w.in_proj_qkv.cols() != self.hidden_size
16405 || w.in_proj_qkv.as_f32().is_none()
16406 || w.in_proj_z.rows() != nv * dv
16407 || w.in_proj_z.cols() != self.hidden_size
16408 || w.in_proj_z.as_f32().is_none()
16409 || w.in_proj_a.rows() != nv
16410 || w.in_proj_a.cols() != self.hidden_size
16411 || w.in_proj_a.as_f32().is_none()
16412 || w.in_proj_b.rows() != nv
16413 || w.in_proj_b.cols() != self.hidden_size
16414 || w.in_proj_b.as_f32().is_none()
16415 || w.conv1d.len() != g.conv_dim() * kk
16416 || w.a_log.len() != nv
16417 || w.dt_bias.len() != nv
16418 || w.norm.len() != dv
16419 || w.out_proj.rows() != self.hidden_size
16420 || w.out_proj.cols() != nv * dv
16421 || w.out_proj.as_f32().is_none()
16422 {
16423 return false;
16424 }
16425 }
16426 AttnKind::Linear(w) => {
16427 let Some(cfg) = self.vmf_cfg else {
16428 return false;
16429 };
16430 if w.thq.rows() != cfg.num_heads * cfg.nphase
16431 || w.thq.cols() != self.hidden_size
16432 || w.thq.as_f32().is_none()
16433 || w.thk.rows() != cfg.num_heads * cfg.nphase
16434 || w.thk.cols() != self.hidden_size
16435 || w.thk.as_f32().is_none()
16436 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16437 || w.v_proj.cols() != self.hidden_size
16438 || w.v_proj.as_f32().is_none()
16439 || w.out_proj.rows() != self.hidden_size
16440 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16441 || w.out_proj.as_f32().is_none()
16442 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16443 {
16444 return false;
16445 }
16446 if let Some((kg, kb)) = &w.k_gate {
16447 if kg.rows() != cfg.num_heads
16448 || kg.cols() != self.hidden_size
16449 || kg.as_f32().is_none()
16450 || kb.len() != cfg.num_heads
16451 {
16452 return false;
16453 }
16454 }
16455 if let Some(conv) = &w.conv {
16456 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16457 return false;
16458 }
16459 }
16460 }
16461 AttnKind::Full {
16462 wq,
16463 wk,
16464 wv,
16465 wo,
16466 q_norm,
16467 k_norm,
16468 output_gate,
16469 softplus_gate,
16470 bias,
16471 } => {
16472 if full_seen
16473 || q_norm.is_some()
16474 || k_norm.is_some()
16475 || *output_gate
16476 || softplus_gate.is_some()
16477 || bias.is_some()
16478 || wq.as_f32().is_none()
16479 || wk.as_f32().is_none()
16480 || wv.as_f32().is_none()
16481 || wo.as_f32().is_none()
16482 || wq.rows() != self.num_heads * self.head_dim
16483 || wk.rows() != self.num_kv_heads * self.head_dim
16484 || wv.rows() != self.num_kv_heads * self.head_dim
16485 || wq.cols() != self.hidden_size
16486 || wk.cols() != self.hidden_size
16487 || wv.cols() != self.hidden_size
16488 || wo.rows() != self.hidden_size
16489 || wo.cols() != self.num_heads * self.head_dim
16490 {
16491 return false;
16492 }
16493 full_seen = true;
16494 }
16495 AttnKind::Bounded(w) => {
16496 let Some(ac) = self.anchor_core.as_ref() else {
16499 return false;
16500 };
16501 if self.bounded_rope.is_none()
16502 || w.window != ac.window
16503 || w.sink != ac.sink
16504 || w.window == 0
16505 || w.window + w.sink > 256
16506 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16507 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16508 || w.wq.as_f32().is_none()
16509 || w.wk.as_f32().is_none()
16510 || w.wv.as_f32().is_none()
16511 || w.wo.as_f32().is_none()
16512 || w.wq.rows() != self.num_heads * self.head_dim
16513 || w.wk.rows() != self.num_kv_heads * self.head_dim
16514 || w.wv.rows() != self.num_kv_heads * self.head_dim
16515 || w.wq.cols() != self.hidden_size
16516 || w.wk.cols() != self.hidden_size
16517 || w.wv.cols() != self.hidden_size
16518 || w.wo.rows() != self.hidden_size
16519 || w.wo.cols() != self.num_heads * self.head_dim
16520 {
16521 return false;
16522 }
16523 }
16524 _ => return false,
16525 }
16526 match &lw.ffn {
16527 FfnKind::Dense(d) => {
16528 if d.act != Act::Silu
16529 || !d.segs.is_empty()
16530 || d.gate_proj.as_f32().is_none()
16531 || d.up_proj.as_f32().is_none()
16532 || d.down_proj.as_f32().is_none()
16533 || d.gate_proj.rows() != self.intermediate_size
16534 || d.gate_proj.cols() != self.hidden_size
16535 || d.up_proj.rows() != self.intermediate_size
16536 || d.up_proj.cols() != self.hidden_size
16537 || d.down_proj.rows() != self.hidden_size
16538 || d.down_proj.cols() != self.intermediate_size
16539 {
16540 return false;
16541 }
16542 }
16543 FfnKind::Moe(m) => {
16544 if m.resonance.is_none()
16545 || m.top_k != 1
16546 || m.router_sigmoid
16547 || !m.norm_topk_prob
16548 || m.expert_bias.is_some()
16549 || m.routed_scaling != 1.0
16550 || m.route_tau.is_some()
16551 || m.shared.is_none()
16552 || m.mask.is_some()
16553 || m.per_expert_scale.is_some()
16554 || m.router_input_norm
16555 || m.experts.is_empty()
16556 || m.experts.len() > 8
16557 {
16558 return false;
16559 }
16560 let r = m.resonance.as_ref().unwrap();
16561 if r.mu.len() != m.experts.len() * self.hidden_size
16562 || r.bias.len() != m.experts.len()
16563 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16564 || r.k > 128
16565 {
16566 return false;
16567 }
16568 let Some((shared, gate)) = &m.shared else {
16569 return false;
16570 };
16571 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16572 return false;
16573 }
16574 if shared.gate_proj.as_f32().is_none()
16575 || shared.up_proj.as_f32().is_none()
16576 || shared.down_proj.as_f32().is_none()
16577 || shared.gate_proj.rows() != self.intermediate_size
16578 || shared.gate_proj.cols() != self.hidden_size
16579 || shared.up_proj.rows() != self.intermediate_size
16580 || shared.up_proj.cols() != self.hidden_size
16581 || shared.down_proj.rows() != self.hidden_size
16582 || shared.down_proj.cols() != self.intermediate_size
16583 {
16584 return false;
16585 }
16586 for e in &m.experts {
16587 if e.act != Act::Silu
16588 || !e.segs.is_empty()
16589 || e.gate_proj.as_f32().is_none()
16590 || e.up_proj.as_f32().is_none()
16591 || e.down_proj.as_f32().is_none()
16592 || e.gate_proj.rows() != self.intermediate_size
16593 || e.gate_proj.cols() != self.hidden_size
16594 || e.up_proj.rows() != self.intermediate_size
16595 || e.up_proj.cols() != self.hidden_size
16596 || e.down_proj.rows() != self.hidden_size
16597 || e.down_proj.cols() != self.intermediate_size
16598 {
16599 return false;
16600 }
16601 }
16602 }
16603 FfnKind::DenseMoe(_) => return false,
16604 }
16605 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16606 return false;
16607 }
16608 }
16609 if full_seen && self.anchor_core.is_some() {
16610 return false;
16611 }
16612 full_seen || self.num_layers > 0
16613 }
16614
16615 fn embryo_resident_wanted(&self) -> bool {
16620 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16621 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16622 && matches!(
16623 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16624 Ok("1") | Ok("parallel")
16625 )
16626 && crate::gpu::enabled_here()
16627 && !self.graph_refused()
16628 && self.embryo_resident_eligible()
16629 }
16630
16631 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16639 if ids.len() < 2
16640 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16641 || !self.embryo_resident_wanted()
16642 {
16643 return None;
16644 }
16645 let model = self.ensure_embryo_graph()?;
16646 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16647 .ok()
16648 .and_then(|v| v.parse::<usize>().ok())
16649 .filter(|&v| v >= 1)
16650 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16651 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16652 let hs = self.hidden_size;
16653 let n = ids.len();
16654 let mut pos = start;
16655 let mut last = None;
16656 let mut rows = Vec::with_capacity(cmax * hs);
16657 while pos < n {
16658 let end = (pos + cmax).min(n);
16659 rows.clear();
16660 for &id in &ids[pos..end] {
16661 rows.extend_from_slice(&self.embed_single(id));
16662 }
16663 let mut lg = Vec::new();
16664 if !crate::gpu::forward_embryo_graph_chunk(
16665 &model,
16666 self.graph_kv_id,
16667 &rows,
16668 pos,
16669 end - pos,
16670 &mut lg,
16671 ) {
16672 if pos == start {
16673 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16674 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16675 }
16676 return None;
16677 }
16678 self.kv_cache.clear();
16682 self.clear_history();
16683 crate::gpu::graph_kv_reset(self.graph_kv_id);
16684 panic!(
16685 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16686 );
16687 }
16688 last = Some(lg);
16689 pos = end;
16690 }
16691 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16692 eprintln!(
16693 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16694 n - start,
16695 (n - start).div_ceil(cmax)
16696 );
16697 }
16698 last
16699 }
16700
16701 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16702 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16703 const UMAX: u32 = u32::MAX;
16704 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16705 const REC: usize = 64;
16706 struct Pack {
16707 data: Vec<f32>,
16708 }
16709 impl Pack {
16710 fn put(&mut self, x: &[f32]) -> u32 {
16711 if x.is_empty() {
16712 return u32::MAX;
16713 }
16714 let off = self.data.len();
16715 self.data.extend_from_slice(x);
16716 off as u32
16717 }
16718 }
16719 let vmf = self.vmf_cfg;
16723 let gdn = self.gdn_cfg;
16724 let mut pack = Pack { data: Vec::new() };
16725 let mut meta = vec![0u32; HEADER];
16726 meta[0] = self.hidden_size as u32;
16727 meta[1] = self.intermediate_size as u32;
16728 meta[2] = self.vocab_size as u32;
16729 meta[3] = self.num_layers as u32;
16730 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16731 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16732 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16733 if let Some(g) = gdn {
16734 meta[24] = g.num_v_heads as u32;
16735 meta[25] = g.num_k_heads as u32;
16736 meta[26] = g.key_head_dim as u32;
16737 meta[27] = g.value_head_dim as u32;
16738 meta[28] = g.conv_kernel as u32;
16739 meta[29] = g.conv_dim() as u32;
16740 }
16741 meta[7] = self.num_heads as u32;
16742 meta[8] = self.num_kv_heads as u32;
16743 meta[9] = self.head_dim as u32;
16744 meta[10] = self.kv_cache.max_seq_len as u32;
16745 let clusters = self.head_clusters.as_ref().unwrap();
16746 let cluster_count = clusters.len() / self.hidden_size;
16747 if clusters.len() % self.hidden_size != 0
16748 || cluster_count == 0
16749 || cluster_count > 1024
16750 || self.vocab_size % cluster_count != 0
16751 || self.weights.lm_head.rows() < self.vocab_size
16752 || self.weights.final_norm.len() != self.hidden_size
16753 {
16754 return None;
16755 }
16756 meta[11] = cluster_count as u32;
16757 meta[12] = (self.vocab_size / cluster_count) as u32;
16758 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16759 meta[16] = self.rotary_dim as u32;
16760 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16761 meta[19] = (self.rms_eps as f32).to_bits();
16762 let max_conv = self
16763 .weights
16764 .layers
16765 .iter()
16766 .filter_map(|lw| match &lw.attn {
16767 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16768 _ => None,
16769 })
16770 .max()
16771 .unwrap_or(1);
16772 let phase_stride = vmf
16778 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16779 .unwrap_or(0);
16780 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16781 let state_stride = phase_stride.max(gdn_stride);
16782 let bounded = self.anchor_core.clone();
16786 let (anchor_window, anchor_sink) = bounded
16787 .as_ref()
16788 .map(|ac| (ac.window, ac.sink))
16789 .unwrap_or((0, 0));
16790 let kv_stride = if bounded.is_some() {
16791 2usize
16792 .saturating_mul(self.num_kv_heads)
16793 .saturating_mul(anchor_window)
16794 .saturating_mul(self.head_dim)
16795 } else {
16796 2usize
16797 .saturating_mul(self.num_kv_heads)
16798 .saturating_mul(self.kv_cache.max_seq_len)
16799 .saturating_mul(self.head_dim)
16800 };
16801 meta[14] = state_stride as u32;
16802 meta[15] = kv_stride as u32;
16803 meta[18] = anchor_window as u32;
16804 meta[20] = anchor_sink as u32;
16805 meta[21] = match &self.bounded_rope {
16806 Some(rope) => {
16807 let off = pack.put(&rope.cos);
16809 let _ = pack.put(&rope.sin);
16810 off
16811 }
16812 None => UMAX,
16813 };
16814 let mut full_seen = false;
16815 let mut bounded_seen = 0usize;
16816 let mut phase_seen = 0usize;
16820 let mut gdn_seen = 0usize;
16821 for (li, lw) in self.weights.layers.iter().enumerate() {
16822 let base = meta.len();
16823 meta.resize(base + REC, UMAX);
16824 meta[base] = match &lw.attn {
16825 AttnKind::Linear(w) if w.phase_delta => 1,
16826 AttnKind::Linear(_) => 0,
16827 AttnKind::Full { .. } => 2,
16828 AttnKind::Bounded(_) => 3,
16829 AttnKind::LinearGdn(_) => 4,
16830 _ => UMAX,
16831 };
16832 meta[base + 1] = pack.put(&lw.input_norm);
16833 meta[base + 2] = pack.put(&lw.post_norm);
16834 meta[base + 25] = match &lw.attn {
16835 AttnKind::Linear(_) => {
16836 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16837 phase_seen += 1;
16838 off
16839 }
16840 AttnKind::LinearGdn(_) => {
16841 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16842 gdn_seen += 1;
16843 off
16844 }
16845 _ => UMAX,
16846 };
16847 match &lw.attn {
16848 AttnKind::LinearGdn(w) => {
16849 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16852 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16853 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16854 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16855 meta[base + 60] = pack.put(&w.conv1d);
16856 meta[base + 61] = pack.put(&w.a_log);
16857 meta[base + 62] = pack.put(&w.dt_bias);
16858 meta[base + 63] = pack.put(&w.norm);
16859 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16860 meta[base + 24] = 0;
16861 }
16862 AttnKind::Linear(w) => {
16863 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16864 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16865 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16866 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16867 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16868 meta[base + 7] = pack.put(&decay);
16869 if let Some((kg, kb)) = &w.k_gate {
16870 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16871 meta[base + 9] = pack.put(kb);
16872 }
16873 if let Some(conv) = &w.conv {
16874 meta[base + 10] = pack.put(conv);
16875 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16876 } else {
16877 meta[base + 24] = 0;
16878 }
16879 }
16880 AttnKind::Full { wq, wk, wv, wo, .. } => {
16881 full_seen = true;
16882 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16883 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16884 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16885 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16886 meta[base + 26] = (li * kv_stride) as u32;
16887 }
16888 AttnKind::Bounded(w) => {
16889 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16890 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16891 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16892 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16893 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16895 meta[base + 27] = pack.put(&w.sink_k);
16896 meta[base + 28] = pack.put(&w.sink_v);
16897 bounded_seen += 1;
16898 }
16899 _ => return None,
16900 }
16901 match &lw.ffn {
16902 FfnKind::Dense(d) => {
16903 meta[base + 15] = 0;
16904 meta[base + 16] = 0;
16905 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16906 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16907 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16908 }
16909 FfnKind::Moe(m) => {
16910 let r = m.resonance.as_ref().unwrap();
16911 let (shared, _) = m.shared.as_ref().unwrap();
16912 meta[base + 15] = 1;
16913 meta[base + 16] = m.experts.len() as u32;
16914 meta[base + 17] = pack.put(&r.mu);
16915 meta[base + 18] = pack.put(&r.u);
16916 meta[base + 19] = pack.put(&r.bias);
16917 meta[base + 20] = r.k as u32;
16918 let mut shell = r.effective_shell(m.experts.len());
16927 shell.push(f32::NEG_INFINITY);
16928 meta[base + 30] = pack.put(&shell);
16929 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16930 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16931 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16932 for (e, ex) in m.experts.iter().enumerate() {
16933 meta[base + 32 + e * 3] =
16934 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16935 meta[base + 33 + e * 3] =
16936 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16937 meta[base + 34 + e * 3] =
16938 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16939 }
16940 }
16941 FfnKind::DenseMoe(_) => return None,
16942 }
16943 }
16944 if !full_seen && self.num_layers == 0 {
16945 return None;
16946 }
16947 let id = {
16948 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16949 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16950 };
16951 let model = crate::gpu::EmbryoGraphModel {
16952 id,
16953 hidden: self.hidden_size,
16954 intermediate: self.intermediate_size,
16955 vocab: self.vocab_size,
16956 layers: self.num_layers,
16957 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16958 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16959 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16960 anchor_q_heads: self.num_heads,
16961 anchor_kv_heads: self.num_kv_heads,
16962 anchor_head_dim: self.head_dim,
16963 rotary_dim: self.rotary_dim,
16964 max_seq: self.kv_cache.max_seq_len,
16965 cluster_count,
16966 cluster_size: self.vocab_size / cluster_count,
16967 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16968 state_stride,
16969 kv_stride,
16970 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16971 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16972 weights: pack.data,
16973 meta,
16974 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16975 clusters: clusters.as_ref().clone(),
16976 final_norm: self.weights.final_norm.clone(),
16977 inv_freq: self.inv_freq.as_ref().clone(),
16978 bounded: bounded.is_some(),
16979 kv_layers: if bounded.is_some() {
16980 bounded_seen
16981 } else {
16982 self.num_layers
16983 },
16984 state_layers: phase_seen + gdn_seen,
16985 anchor_window,
16986 anchor_sink,
16987 phase_layers: phase_seen,
16988 gdn_layers: gdn_seen,
16989 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16990 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16991 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16992 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16993 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16994 };
16995 self.embryo_graph = Some(std::sync::Arc::new(model));
16996 }
16997 self.embryo_graph.clone()
16998 }
16999
17000 fn forward_layers_span(
17001 &mut self,
17002 hidden: &[f32],
17003 position: usize,
17004 task_mask: Option<&TaskMask>,
17005 from: usize,
17006 upto: Option<usize>,
17007 ) -> Vec<f32> {
17008 #[cfg(target_os = "macos")]
17009 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
17010 position,
17011 from,
17012 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
17013 );
17014 debug_assert!(
17015 from == 0
17016 || (self.dsv4.is_none()
17017 && self.dsv41.is_none()
17018 && self.qwen4_exp.is_none()
17019 && !self.per_token_stack())
17020 );
17021 #[cfg(target_os = "macos")]
17027 if !crate::gpu_metal::wait_replay() {
17028 self.fail_metal_graph("the pending async replay failed before a plain forward");
17029 return vec![0.0; self.hidden_size];
17030 }
17031 if let Some(b) = &mut self.qwen4_exp {
17032 let _ = (task_mask, upto);
17033 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17034 let mut logits = Vec::new();
17035 crate::qwen4_exp::forward_token(
17036 &b.0,
17037 &b.1,
17038 &b.2,
17039 &mut b.3,
17040 token_id,
17041 position,
17042 &self.inv_freq,
17043 self.pool.as_deref(),
17044 &mut logits,
17045 true,
17046 );
17047 self.graph_logits = Some(logits);
17048 return vec![0.0; self.hidden_size];
17049 }
17050 if let Some(b) = &mut self.dsv4 {
17056 let _ = (task_mask, upto);
17057 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17058 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
17059 st.pos = position;
17060 let mut logits = Vec::new();
17061 crate::dsv4::forward_token(
17062 g,
17063 layers,
17064 &cfg,
17065 st,
17066 token_id,
17067 &self.inv_freq,
17068 self.pool.as_deref(),
17069 &mut logits,
17070 );
17071 self.graph_logits = Some(logits);
17072 self.dspark_probe(position, token_id);
17073 return vec![0.0; self.hidden_size];
17076 }
17077 if let Some(b) = &mut self.dsv41 {
17079 let _ = (task_mask, upto);
17080 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17081 let mut logits = Vec::new();
17082 crate::dsv41::forward_token(
17083 &b.0,
17084 &b.1,
17085 &b.2,
17086 &mut b.3,
17087 token_id,
17088 position,
17089 self.pool.as_deref(),
17090 &mut logits,
17091 );
17092 self.graph_logits = Some(logits);
17093 return vec![0.0; self.hidden_size];
17094 }
17095 if let Some(b) = &self.g4e {
17101 let _ = (task_mask, upto);
17102 return crate::g4e::g4e_forward(
17103 &b.0,
17104 &b.1,
17105 hidden,
17106 position,
17107 &mut self.kv_cache.layers,
17108 self.num_heads,
17109 self.num_kv_heads,
17110 self.pool.as_deref(),
17111 );
17112 }
17113 if let Some(b) = &self.g3n {
17114 let _ = (task_mask, upto);
17115 return crate::g3n::g3n_forward(
17116 &b.0,
17117 &b.1,
17118 hidden,
17119 position,
17120 &mut self.kv_cache.layers,
17121 self.num_heads,
17122 self.num_kv_heads,
17123 self.head_dim,
17124 self.pool.as_deref(),
17125 );
17126 }
17127 if from == 0
17134 && upto.is_none()
17135 && task_mask.is_none()
17136 && self.anchor_core.is_some()
17137 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
17138 {
17139 static ONCE: std::sync::Once = std::sync::Once::new();
17140 ONCE.call_once(|| {
17141 eprintln!(
17142 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
17143 unsupported={} eligible={}",
17144 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17145 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17146 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17147 crate::gpu::enabled_here(),
17148 self.graph_refused(),
17149 self.embryo_resident_eligible(),
17150 );
17151 });
17152 }
17153 if from == 0
17154 && upto.is_none()
17155 && task_mask.is_none()
17156 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17160 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17161 && matches!(
17165 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17166 Ok("1") | Ok("parallel")
17167 )
17168 && crate::gpu::enabled_here()
17169 && !self.graph_refused()
17170 && (position == 0 || self.device_sequence_position().is_some())
17176 && self.embryo_resident_eligible()
17177 && let Some(model) = self.ensure_embryo_graph()
17178 {
17179 let mut lg = Vec::new();
17180 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17181 {
17182 self.graph_logits = Some(lg);
17183 return vec![0.0; self.hidden_size];
17184 }
17185 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17186 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17187 }
17188 self.mark_graph_refused();
17194 if position != 0 {
17195 self.kv_cache.clear();
17199 self.clear_history();
17200 crate::gpu::graph_kv_reset(self.graph_kv_id);
17201 panic!(
17202 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
17203 );
17204 }
17205 }
17206 let mut h = hidden.to_vec();
17207 self.mimo_moe_prepare();
17210 let _mimo_q8 = self
17211 .mimo_moe
17212 .is_on()
17213 .then(crate::qtensor::enter_full_gpu_q8_scope);
17214 let (nh, _nkv, _hd, hs, _rd, eps) = (
17217 self.num_heads,
17218 self.num_kv_heads,
17219 self.head_dim,
17220 self.hidden_size,
17221 self.rotary_dim,
17222 self.rms_eps,
17223 );
17224 let pool = self.pool.clone();
17225 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
17237 let graph_on = match graph_env.as_deref() {
17238 Some("0") => false,
17239 Some("prefill") => false, Some(_) => true,
17241 None => crate::gpu::wgpu_graph_default(),
17247 };
17248 let graph_trusted =
17249 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17250 let race_eligible =
17251 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17252 let mut tail_start = 0usize;
17253 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17254 let t_graph = std::time::Instant::now();
17255 let mut lg = Vec::new();
17256 let mut gl = 0usize;
17257 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17258 let declined = built.is_none();
17259 let built = match built {
17260 Some(Ok(hh)) => Some(hh),
17261 Some(Err(())) => {
17262 self.clear_sequence_state();
17266 self.graph_failed
17267 .store(true, std::sync::atomic::Ordering::Relaxed);
17268 self.cancel
17269 .store(true, std::sync::atomic::Ordering::Relaxed);
17270 tracing::error!("token graph failed after admission; sequence state cleared");
17271 return vec![0.0; self.hidden_size];
17272 }
17273 None => None,
17274 };
17275 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17280 self.mark_graph_refused();
17281 }
17282 graph_note(built.is_some(), gl, self.num_layers);
17283 if let Some(hh) = built {
17284 let dur = t_graph.elapsed();
17285 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17286 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17287 }
17288 if gl > 0 && gl < self.num_layers {
17289 h = hh;
17295 tail_start = gl;
17296 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17297 if !graph_trusted {
17298 crate::gpu::graph_race_record(true, dur);
17299 }
17300 if !lg.is_empty() {
17301 lg.resize(self.vocab_size, 0.0);
17304 if let Some(c) = self.final_softcap {
17305 softcap_in_place(&mut lg, c, self.pool.as_deref());
17306 }
17307 self.graph_logits = Some(lg);
17308 }
17309 return hh;
17310 }
17311 }
17317 }
17318 let span = from > 0 || upto.is_some();
17342 if span && graph_on && task_mask.is_none() && graph_trusted {
17343 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17344 let mut lg = Vec::new();
17345 let mut gl = 0usize;
17346 let span_res =
17347 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17348 let span_res = match span_res {
17349 Some(Ok(hh)) => Some(hh),
17350 Some(Err(())) => {
17351 self.clear_sequence_state();
17352 self.graph_failed
17353 .store(true, std::sync::atomic::Ordering::Relaxed);
17354 self.cancel
17355 .store(true, std::sync::atomic::Ordering::Relaxed);
17356 tracing::error!(
17357 "span token graph failed after admission; sequence state cleared"
17358 );
17359 return vec![0.0; self.hidden_size];
17360 }
17361 None => None,
17362 };
17363 graph_note(span_res.is_some(), gl, upto_excl - from);
17364 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17365 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17369 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17370 eprintln!(
17371 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17372 upto_excl - from,
17373 span_res.is_some()
17374 );
17375 }
17376 }
17377 if let Some(hh) = span_res {
17378 if gl == upto_excl - from {
17379 if !lg.is_empty() {
17380 lg.resize(self.vocab_size, 0.0);
17381 if let Some(c) = self.final_softcap {
17382 softcap_in_place(&mut lg, c, self.pool.as_deref());
17383 }
17384 self.graph_logits = Some(lg);
17385 }
17386 crate::gpu::set_layer(-1);
17387 return hh;
17388 }
17389 h = hh;
17391 tail_start = from + gl;
17392 }
17393 }
17394 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17399
17400 let host_tail = tail_start > from;
17410 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17411 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17412
17413 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17414 #[cfg(target_os = "macos")]
17415 let mut gpu_skip_until = 0usize;
17416 for li in tail_start.max(from)..self.num_layers {
17417 let _capacity_tail = automatic_gpu_prefix
17418 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17419 .map(|_| crate::gpu::enter_cpu_scope());
17420 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17422 if li > u {
17423 break;
17424 }
17425 }
17426 if let Some(mask) = task_mask {
17427 if !mask.layer_alive(li) {
17428 continue; }
17430 }
17431 if host_tail && task_mask.is_none() {
17436 match self.m3_pooled_layer(li, position, &mut h) {
17437 Ok(true) => {
17438 crate::gpu::set_layer(-1);
17439 continue;
17440 }
17441 Ok(false) => {}
17442 Err(()) => {
17443 self.clear_sequence_state();
17444 self.graph_failed
17445 .store(true, std::sync::atomic::Ordering::Relaxed);
17446 self.cancel
17447 .store(true, std::sync::atomic::Ordering::Relaxed);
17448 tracing::error!(
17449 "pooled tail token graph failed after admission; sequence state cleared"
17450 );
17451 return vec![0.0; self.hidden_size];
17452 }
17453 }
17454 }
17455 #[cfg(target_os = "macos")]
17459 {
17460 if li < gpu_skip_until {
17461 continue;
17462 }
17463 if task_mask.is_none() {
17464 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17465 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17466 return vec![0.0; self.hidden_size];
17470 }
17471 if end > li {
17472 gpu_skip_until = end;
17473 if self.is_loop_end(end - 1) && end < self.num_layers {
17476 h = inference::rms_norm(
17477 &h,
17478 &self.weights.final_norm,
17479 self.rms_eps,
17480 self.norm_style,
17481 );
17482 }
17483 continue;
17484 }
17485 }
17486 }
17487
17488 if task_mask.is_none() {
17489 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17490 crate::gpu::BatchGraphOutcome::Completed => continue,
17491 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17492 crate::gpu::BatchGraphOutcome::Declined => {}
17493 }
17494 }
17495 #[cfg(feature = "gpu")]
17496 self.pull_lagging_host_kv(li, li + 1, position);
17497 let lw = &self.weights.layers[self.phys_layer(li)];
17498 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17499 if tp.parse::<usize>().ok() == Some(position) {
17500 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17501 eprintln!(
17502 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17503 h[0], h[1]
17504 );
17505 }
17506 }
17507 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17510 inference::rms_norm_into(
17511 &h,
17512 &lw.input_norm,
17513 self.rms_eps,
17514 self.norm_style,
17515 &mut self.ws.n1,
17516 );
17517 drop(prof);
17518
17519 let attn_out = match &lw.attn {
17520 AttnKind::Mla(w) => {
17521 let inv_freq_l = self.layer_inv_freq(li);
17522 let rs = self.layer_rope_scale(li);
17523 let eps = self.rms_eps;
17524 let pool = self.pool.clone();
17525 mla_attention(
17526 w,
17527 &self.ws.n1,
17528 &mut self.kv_cache.layers[li],
17529 position,
17530 &inv_freq_l,
17531 rs,
17532 eps,
17533 pool.as_deref(),
17534 )
17535 }
17536 AttnKind::Linear(w) => {
17537 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17538 vmf_phase_forward(
17539 &self.ws.n1,
17540 w,
17541 &cfg,
17542 &mut self.kv_cache.layers[li].linear_state,
17543 self.pool.as_deref(),
17544 )
17545 }
17546 AttnKind::Kda(w) => {
17547 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17548 crate::linear_core::kda_forward(
17549 &self.ws.n1,
17550 w,
17551 &cfg,
17552 &mut self.kv_cache.layers[li].linear_state,
17553 self.pool.as_deref(),
17554 )
17555 }
17556 AttnKind::LinearGdn(w) => {
17557 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17558 gdn_forward(
17559 &self.ws.n1,
17560 w,
17561 &cfg,
17562 &mut self.kv_cache.layers[li].linear_state,
17563 self.pool.as_deref(),
17564 )
17565 }
17566 AttnKind::ShortConv(w) => {
17567 let cfg = self
17568 .short_conv_cfg
17569 .expect("short-conv layer without short_conv_cfg");
17570 short_conv_forward(
17571 &self.ws.n1,
17572 w,
17573 &cfg,
17574 &mut self.kv_cache.layers[li].linear_state,
17575 self.pool.as_deref(),
17576 )
17577 }
17578 AttnKind::Bounded(w) => {
17579 let rope = self
17582 .bounded_rope
17583 .clone()
17584 .expect("bounded layer without an installed rotation table");
17585 let cfg = crate::bounded::BoundedAttnCfg {
17586 num_heads: self.num_heads,
17587 num_kv_heads: self.num_kv_heads,
17588 head_dim: self.head_dim,
17589 hidden_size: hs,
17590 scale: self.attn_scale,
17591 rope: &rope,
17592 pool: pool.as_deref(),
17593 };
17594 crate::bounded::bounded_attention(
17595 &self.ws.n1,
17596 w,
17597 &mut self.kv_cache.layers[li],
17598 &cfg,
17599 )
17600 }
17601 AttnKind::Full {
17602 wq,
17603 wk,
17604 wv,
17605 wo,
17606 q_norm,
17607 k_norm,
17608 output_gate,
17609 softplus_gate,
17610 bias,
17611 } if self.kv_cache.layers[li].o1_sealed() => {
17612 let inv_freq_l = self.layer_inv_freq(li);
17615 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17616 let cfg = QwenAttnCfg {
17617 num_heads: self.layer_num_heads(li),
17618 num_kv_heads: nkv_l,
17619 head_dim: hd_l,
17620 hidden_size: hs,
17621 position,
17622 inv_freq: &inv_freq_l,
17623 rotary_dim: rd_l,
17624 scale: self.attn_scale,
17625 softcap: self.attn_softcap,
17626 window: None,
17627 v_norm: self.attn_v_norm,
17628 qk_norm_after_rope: self.qk_norm_after_rope,
17629 gate_sigmoid: self.proj_gate_sigmoid,
17630 q_norm: q_norm.as_deref(),
17631 k_norm: k_norm.as_deref(),
17632 output_gate: *output_gate,
17633 softplus_gate: softplus_gate
17634 .as_ref()
17635 .map(|(gate, per_head)| (gate, *per_head)),
17636 rope_scale: self.layer_rope_scale(li),
17637 bias: bias
17638 .as_ref()
17639 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17640 rms_eps: eps,
17641 norm_style: self.norm_style,
17642 pool: pool.as_deref(),
17643 v_head_dim: self.layer_v_dim(li),
17644 };
17645 attention::qwen_attention_nystrom(
17646 &self.ws.n1,
17647 wq,
17648 wk,
17649 wv,
17650 wo,
17651 &mut self.kv_cache.layers[li],
17652 &cfg,
17653 )
17654 }
17655 AttnKind::Full {
17656 wq,
17657 wk,
17658 wv,
17659 wo,
17660 q_norm,
17661 k_norm,
17662 output_gate,
17663 softplus_gate,
17664 bias,
17665 } => 'attn: {
17666 let dropin_reason =
17671 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17672 if let Some(reason) = dropin_reason {
17673 self.note_graph_decline("wgpu attn dropin", reason);
17674 }
17675 if graph_on
17676 && dropin_reason.is_none()
17677 && !*output_gate
17678 && softplus_gate.is_none()
17679 && self.attention_heads_per_layer.is_none()
17680 && bias.is_none()
17681 && task_mask.is_none()
17682 {
17683 let inv_freq_l = self.layer_inv_freq(li);
17684 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17685 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17686 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17687 wq.mapped_q1(),
17688 wk.mapped_q1(),
17689 wv.mapped_q1(),
17690 wo.mapped_q1(),
17691 ) {
17692 let gm = gm.clone();
17693 let mut out = vec![0f32; hs];
17694 let cache = &self.kv_cache.layers[li];
17695 if crate::gpu::attn_dropin(
17696 &gm,
17697 self.graph_kv_id,
17698 li,
17699 &self.ws.n1,
17700 qi,
17701 ki,
17702 vi,
17703 oi,
17704 q_norm.as_deref(),
17705 k_norm.as_deref(),
17706 self.qk_norm_after_rope,
17707 &inv_freq_l,
17708 nh,
17709 nkv_l,
17710 hd_l,
17711 rd_l,
17712 hs,
17713 position,
17714 self.kv_cache.max_seq_len,
17715 gemma,
17716 eps as f32,
17717 cache.k_heads(),
17718 cache.v_heads(),
17719 &mut out,
17720 ) {
17721 break 'attn out;
17722 }
17723 }
17724 }
17725 let masked = task_mask
17726 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17727 .unwrap_or(false);
17728 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17729 let plain = self.layer_attn_plain(li);
17732 match (masked, f32_view) {
17733 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17736 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17737 attention::multi_head_attention(
17738 &self.ws.n1,
17739 q,
17740 k,
17741 v,
17742 o,
17743 &mut self.kv_cache.layers[li],
17744 self.num_heads,
17745 self.num_kv_heads,
17746 self.head_dim,
17747 self.hidden_size,
17748 position,
17749 &active_heads,
17750 &self.inv_freq,
17751 )
17752 }
17753 (masked, _) => {
17754 if masked {
17755 tracing::warn!(
17756 "layer {li}: head mask on quantized weights or on a \
17757 window/sink/per-layer-geometry layer not supported \
17758 yet — executing dense"
17759 );
17760 }
17761 let inv_freq_l = self.layer_inv_freq(li);
17762 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17763 let cfg = QwenAttnCfg {
17764 num_heads: self.layer_num_heads(li),
17765 num_kv_heads: nkv_l,
17766 head_dim: hd_l,
17767 hidden_size: hs,
17768 position,
17769 inv_freq: &inv_freq_l,
17770 rotary_dim: rd_l,
17771 scale: self.attn_scale,
17772 softcap: self.attn_softcap,
17773 window: self.layer_window(li),
17774 v_norm: self.attn_v_norm,
17775 qk_norm_after_rope: self.qk_norm_after_rope,
17776 gate_sigmoid: self.proj_gate_sigmoid,
17777 q_norm: q_norm.as_deref(),
17778 k_norm: k_norm.as_deref(),
17779 output_gate: *output_gate,
17780 softplus_gate: softplus_gate
17781 .as_ref()
17782 .map(|(gate, per_head)| (gate, *per_head)),
17783 rope_scale: self.layer_rope_scale(li),
17784 bias: bias
17785 .as_ref()
17786 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17787 rms_eps: eps,
17788 norm_style: self.norm_style,
17789 pool: pool.as_deref(),
17790 v_head_dim: self.layer_v_dim(li),
17791 };
17792 attention::qwen_attention(
17793 &self.ws.n1,
17794 wq,
17795 wk,
17796 wv,
17797 wo,
17798 &mut self.kv_cache.layers[li],
17799 &cfg,
17800 )
17801 }
17802 }
17803 }
17804 };
17805 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17808 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17809 None => attn_out,
17810 };
17811 let lw = &self.weights.layers[self.phys_layer(li)];
17812 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17813 inference::add_rmsnorm_fused_into(
17814 &mut h,
17815 &attn_out,
17816 &lw.post_norm,
17817 self.rms_eps,
17818 self.norm_style,
17819 &mut self.ws.p1,
17820 );
17821 drop(prof);
17822 let mut attn_out = attn_out;
17823 attention::recycle_buf(&mut attn_out);
17824 let post_normed = &self.ws.p1;
17825
17826 let ffn_masked = task_mask
17827 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17828 .unwrap_or(false);
17829 let ffn_out = match (ffn_masked, &lw.ffn) {
17841 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17845 let row = task_mask
17846 .and_then(|tm| tm.ffn_masks.get(li))
17847 .map(|v| v.as_slice());
17848 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17849 }
17850 (true, FfnKind::Dense(d)) => {
17851 let tm = task_mask.unwrap();
17852 let alive = tm.ffn_active_count(li);
17853 let deep = alive * 2 <= self.intermediate_size;
17854 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17855 let active = tm.ffn_active_indices(li);
17856 sparse_ffn_quant(
17857 d,
17858 post_normed,
17859 &active,
17860 self.hidden_size,
17861 self.pool.as_deref(),
17862 )
17863 } else if deep
17864 && let (Some(g), Some(u), Some(dn)) = (
17865 d.gate_proj.as_f32(),
17866 d.up_proj.as_f32(),
17867 d.down_proj.as_f32(),
17868 )
17869 {
17870 let active = tm.ffn_active_indices(li);
17871 inference::sparse_ffn_forward(
17872 post_normed,
17873 g,
17874 u,
17875 dn,
17876 self.hidden_size,
17877 self.intermediate_size,
17878 &active,
17879 self.pool.as_deref(),
17880 )
17881 } else {
17882 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17883 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17884 }
17885 }
17886 (true, FfnKind::Moe(m)) => {
17887 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17891 ffn_forward(
17892 &lw.ffn,
17893 post_normed,
17894 self.pool.as_deref(),
17895 allowed.as_deref(),
17896 )
17897 }
17898 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17899 dm,
17900 post_normed,
17901 &h,
17902 self.rms_eps,
17903 self.norm_style,
17904 self.pool.as_deref(),
17905 ),
17906 (false, _) => match &lw.ffn {
17907 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17908 dm,
17909 post_normed,
17910 &h,
17911 self.rms_eps,
17912 self.norm_style,
17913 self.pool.as_deref(),
17914 ),
17915 FfnKind::Moe(m)
17916 if task_mask.is_none()
17917 && host_tail
17918 && li >= tail_start
17919 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17920 {
17921 moe_ffn_minicpm_tail(
17922 &mut self.minicpm_tail,
17923 li,
17924 self.num_layers,
17925 m,
17926 post_normed,
17927 self.pool.as_deref(),
17928 )
17929 }
17930 FfnKind::Moe(m)
17931 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17932 {
17933 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17934 }
17935 _ => {
17936 let allowed = match (&lw.ffn, task_mask) {
17937 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17938 _ => None,
17939 };
17940 ffn_forward(
17941 &lw.ffn,
17942 post_normed,
17943 self.pool.as_deref(),
17944 allowed.as_deref(),
17945 )
17946 }
17947 },
17948 };
17949 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17950 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17951 None => ffn_out,
17952 };
17953 for (i, &f) in ffn_out.iter().enumerate() {
17954 h[i] += f;
17955 }
17956 let mut ffn_out = ffn_out;
17957 attention::recycle_buf(&mut ffn_out);
17958
17959 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17961 for v in h.iter_mut() {
17962 *v *= sc;
17963 }
17964 }
17965 if self.layer_dump.is_some() {
17967 self.dump_layer_row(position, li, &h);
17968 }
17969
17970 if self.is_loop_end(li) && li + 1 < self.num_layers {
17973 h = inference::rms_norm(
17974 &h,
17975 &self.weights.final_norm,
17976 self.rms_eps,
17977 self.norm_style,
17978 );
17979 }
17980
17981 if self.dyn_phi_layer == Some(li) {
17985 self.update_dyn_phi(&h);
17986 }
17987 }
17988 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17990 crate::gpu::graph_race_record(false, t.elapsed());
17991 }
17992
17993 h
17994 }
17995
17996 fn update_dyn_phi(&mut self, h: &[f32]) {
17999 const A: f32 = 0.2;
18000 if self.dyn_phi_ema.len() != h.len() {
18001 self.dyn_phi_ema = vec![0.0; h.len()];
18002 self.dyn_phi_seen = 0;
18003 }
18004 if self.dyn_phi_seen == 0 {
18005 self.dyn_phi_ema.copy_from_slice(h);
18006 } else {
18007 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
18008 *e = (1.0 - A) * *e + A * v;
18009 }
18010 }
18011 self.dyn_phi_seen += 1;
18012 }
18013
18014 pub fn dyn_phi(&self) -> &[f32] {
18016 &self.dyn_phi_ema
18017 }
18018
18019 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
18021 self.dyn_phi_layer = layer;
18022 self.dyn_phi_ema.clear();
18023 self.dyn_phi_seen = 0;
18024 }
18025
18026 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
18028 let Some(model) = &self.model else {
18029 return Vec::new();
18030 };
18031 model
18032 .header
18033 .skills
18034 .iter()
18035 .enumerate()
18036 .filter_map(|(i, sk)| {
18037 if sk.is_v2() {
18041 return None;
18042 }
18043 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
18044 let sel = sk.selection.as_ref()?;
18045 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
18046 })
18047 .collect()
18048 }
18049
18050 pub fn active_skill(&self) -> Option<usize> {
18052 self.dyn_active
18053 }
18054
18055 pub fn enable_dynamic_routing(&mut self) -> usize {
18060 use crate::swarm::{DynRouter, RoutableSkill};
18061 let Some(model) = self.model.clone() else {
18062 return 0;
18063 };
18064 if let Some(r) = &model.header.router {
18071 tracing::warn!(
18072 "dynamic routing disabled: this file declares router policy '{}' with \
18073 granularity \"{}\" — the request-level decision applies instead",
18074 r.policy,
18075 r.granularity
18076 );
18077 return 0;
18078 }
18079 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
18085 || model.header.skills.iter().any(|s| s.is_v2())
18086 {
18087 tracing::warn!(
18088 "dynamic routing disabled: this file carries format-v2 skill records \
18089 (SKILLS_V2) — they route per request through a router policy only"
18090 );
18091 return 0;
18092 }
18093 if self.dyn_blend_loaded {
18096 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
18097 return 0;
18098 }
18099 if let Some(a) = self.dyn_active {
18103 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
18104 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
18105 return 0;
18106 }
18107 }
18108 let hidden = self.hidden_size;
18109 let mut skills = Vec::new();
18110 for (idx, id, _phi) in self.dynamic_skills() {
18111 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
18112 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
18113 skills.push(rs);
18114 }
18115 }
18116 }
18117 if skills.is_empty() {
18118 return 0;
18119 }
18120 let phi = skills[0].phi_layer;
18122 if skills.iter().any(|s| s.phi_layer != phi) {
18123 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
18124 }
18125 let n = skills.len();
18126 self.set_dyn_phi_layer(Some(phi));
18127 self.dyn_router = Some(DynRouter::new(skills));
18128 n
18129 }
18130
18131 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
18133 self.dyn_router
18134 .as_ref()
18135 .map(|r| r.switches.clone())
18136 .unwrap_or_default()
18137 }
18138
18139 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
18142 let _mimo_q8 = self
18143 .mimo_moe
18144 .is_on()
18145 .then(crate::qtensor::enter_full_gpu_q8_scope);
18146 let rows = self.weights.lm_head.rows();
18147 let mut logits = attention::take_buf(rows.min(self.vocab_size));
18148 let served = self.mimo_moe.is_on()
18152 && crate::gpu::mimo_q8_short_enabled()
18153 && rows == self.vocab_size
18154 && !self.weights.lm_head.has_prism_contract()
18155 && self
18156 .weights
18157 .lm_head
18158 .graph_weight()
18159 .is_some_and(|(model, idx, kind, _)| {
18160 kind == 7
18161 && crate::gpu::q82_short_rows(
18162 model,
18163 idx,
18164 hidden,
18165 1,
18166 rows,
18167 self.hidden_size,
18168 &mut logits,
18169 )
18170 });
18171 if !served {
18172 self.weights
18173 .lm_head
18174 .matvec(hidden, &mut logits, self.pool.as_deref());
18175 }
18176 logits.resize(self.vocab_size, 0.0);
18177 if let Some(m) = self.logit_multiplier {
18178 for l in logits.iter_mut() {
18179 *l *= m;
18180 }
18181 }
18182 if let Some(c) = self.final_softcap {
18183 softcap_in_place(&mut logits, c, self.pool.as_deref());
18184 }
18185 if let Some(cm) = self.head_clusters.as_ref() {
18186 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18187 }
18188 logits
18189 }
18190
18191 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18194 let h = hidden.len();
18195 let ncl = cm.len() / h.max(1);
18196 if ncl == 0 || logits.len() % ncl != 0 {
18197 return;
18198 }
18199 let cs = logits.len() / ncl;
18200 let mut lc = vec![0.0f32; ncl];
18202 for c in 0..ncl {
18203 let row = &cm[c * h..(c + 1) * h];
18204 let mut s = 0.0f32;
18205 for j in 0..h {
18206 s += row[j] * hidden[j];
18207 }
18208 lc[c] = s;
18209 }
18210 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18211 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
18212 for c in 0..ncl {
18213 let blk = &mut logits[c * cs..(c + 1) * cs];
18214 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18215 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
18216 let add = lc[c] - lse - bl;
18217 for v in blk.iter_mut() {
18218 *v += add;
18219 }
18220 }
18221 }
18222
18223 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
18228 #[cfg(target_os = "macos")]
18229 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
18230 self.clear_sequence_state();
18231 crate::gpu::graph_race_begin_generation();
18235 if task_mask.is_none() {
18236 self.o1_begin();
18237 }
18238 let mut hidden = vec![0.0f32; self.hidden_size];
18239 for (pos, &id) in ids.iter().enumerate() {
18240 let emb = self.embed_single(id);
18241 hidden = self.forward_layers(&emb, pos, task_mask);
18242 }
18243 if let Err(err) = self.o1_seal_checked() {
18244 self.o1_fail(err);
18245 }
18246 if let Some(logits) = self.graph_logits.take() {
18249 return logits;
18250 }
18251 inference::rms_norm_into(
18252 &hidden,
18253 &self.weights.final_norm,
18254 self.rms_eps,
18255 self.norm_style,
18256 &mut self.ws.n1,
18257 );
18258 self.lm_head_forward(&self.ws.n1)
18259 }
18260}
18261
18262pub fn create_test_pipeline(
18264 hidden_size: usize,
18265 intermediate_size: usize,
18266 num_heads: usize,
18267 num_kv_heads: usize,
18268 head_dim: usize,
18269 num_layers: usize,
18270 vocab_size: usize,
18271) -> Pipeline {
18272 let synth = |n: usize, salt: usize| -> Vec<f32> {
18275 (0..n)
18276 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18277 .collect()
18278 };
18279 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18280 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18281 };
18282 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18283 .map(|li| LayerWeights {
18284 input_norm: vec![1.0; hidden_size],
18285 post_norm: vec![1.0; hidden_size],
18286 attn_out_norm: None,
18287 ffn_out_norm: None,
18288 layer_scale: None,
18289 ffn: FfnKind::Dense(DenseFfn {
18290 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18291 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18292 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18293 act: Act::Silu,
18294 down_t: None,
18295 segs: Vec::new(),
18296 }),
18297 attn: AttnKind::Full {
18298 bias: None,
18299 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18300 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18301 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18302 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18303 q_norm: None,
18304 k_norm: None,
18305 output_gate: false,
18306 softplus_gate: None,
18307 },
18308 })
18309 .collect();
18310
18311 Pipeline::new(
18312 Tokenizer::byte_level(),
18313 PipelineWeights {
18314 embed_tokens: qt(vocab_size, hidden_size, 100),
18315 layers: layer_weights,
18316 lm_head: qt(vocab_size, hidden_size, 200),
18317 final_norm: vec![1.0; hidden_size],
18318 },
18319 hidden_size,
18320 intermediate_size,
18321 num_heads,
18322 num_kv_heads,
18323 head_dim,
18324 num_layers,
18325 num_layers, false, vocab_size,
18328 1e-6,
18329 10_000.0,
18330 NormStyle::Qwen,
18331 4096,
18332 SamplerConfig {
18333 seed: Some(42),
18334 ..Default::default()
18335 },
18336 )
18337}
18338
18339#[inline]
18344fn mask_bit(row: &[u8], j: usize) -> bool {
18345 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18346}
18347
18348fn mask_gain() -> f32 {
18359 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18360 *G.get_or_init(|| {
18361 std::env::var("CMF_FFN_MASK_GAIN")
18362 .ok()
18363 .and_then(|v| v.parse().ok())
18364 .unwrap_or(1.0)
18365 })
18366}
18367
18368fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18369 let fill = meanfill().and_then(|(i, v)| {
18372 let li = crate::gpu::cur_layer();
18373 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18374 });
18375 for r in 0..rows {
18376 let base = r * inter;
18377 for (bi, &byte) in row.iter().enumerate() {
18378 if byte == 0xFF {
18379 continue;
18380 }
18381 let j0 = bi * 8;
18382 for bit in 0..8 {
18383 let j = j0 + bit;
18384 if j < inter && byte & (1 << bit) == 0 {
18385 g[base + j] = fill.map_or(0.0, |f| f[j]);
18386 }
18387 }
18388 }
18389 }
18390 let gain = mask_gain();
18391 if gain != 1.0 {
18392 for v in g[..rows * inter].iter_mut() {
18393 *v *= gain;
18394 }
18395 }
18396}
18397
18398#[inline]
18400fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18401 row.is_none_or(|r| mask_bit(r, i))
18402}
18403
18404fn all_bits_on(row: &[u8], n: usize) -> bool {
18407 (0..n).all(|i| mask_bit(row, i))
18408}
18409
18410fn tube_topk() -> usize {
18418 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18419 *K.get_or_init(|| {
18420 std::env::var("CMF_TUBE_TOPK")
18421 .ok()
18422 .and_then(|v| v.parse().ok())
18423 .unwrap_or(0)
18424 })
18425}
18426
18427fn tube_score_oracle() -> bool {
18428 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18429 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18430}
18431
18432fn tube_ffn_routed(
18439 d: &DenseFfn,
18440 xs: &[f32],
18441 b: usize,
18442 pool: Option<&Pool>,
18443 mask_row: Option<&[u8]>,
18444 k: usize,
18445) -> Vec<f32> {
18446 let hidden = d.down_proj.rows();
18447 let core = d.gate_proj.rows();
18448 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18449 let mut out = match (b, core_full, mask_row) {
18450 (1, true, _) => dense_ffn(d, xs, pool),
18451 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18452 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18453 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18454 };
18455 let cand: Vec<usize> = (0..d.segs.len())
18456 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18457 .collect();
18458 if cand.is_empty() {
18459 return out;
18460 }
18461 let oracle = tube_score_oracle();
18465 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18466 let mut scores = vec![0f32; b * cand.len()];
18467 for (ci, &i) in cand.iter().enumerate() {
18468 let seg = &d.segs[i];
18469 let w = seg.width;
18470 let mut g = vec![0.0f32; b * w];
18471 if b == 1 {
18472 seg.gate.matvec(xs, &mut g, pool);
18473 } else {
18474 seg.gate.matmat(xs, b, &mut g, pool);
18475 }
18476 for v in g.iter_mut() {
18477 *v = Act::Silu.combine(*v, 1.0);
18478 }
18479 if !oracle {
18480 for t in 0..b {
18481 scores[t * cand.len() + ci] =
18482 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18483 }
18484 }
18485 if oracle || b > 1 {
18486 let mut u = vec![0.0f32; b * w];
18487 if b == 1 {
18488 seg.up.matvec(xs, &mut u, pool);
18489 } else {
18490 seg.up.matmat(xs, b, &mut u, pool);
18491 }
18492 for (a, &v) in g.iter_mut().zip(u.iter()) {
18493 *a *= v;
18494 }
18495 if oracle {
18496 for t in 0..b {
18497 scores[t * cand.len() + ci] =
18498 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18499 }
18500 }
18501 }
18502 acts.push(g);
18503 }
18504 let keep = k.min(cand.len());
18506 let mut scratch: Vec<f32> = Vec::new();
18507 for t in 0..b {
18508 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18509 .map(|ci| (scores[t * cand.len() + ci], ci))
18510 .collect();
18511 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18512 let mut alive = vec![false; cand.len()];
18513 for &(_, ci) in sc.iter().take(keep) {
18514 alive[ci] = true;
18515 }
18516 if b > 1 {
18517 for (ci, a) in acts.iter_mut().enumerate() {
18518 if !alive[ci] {
18519 let w = d.segs[cand[ci]].width;
18520 a[t * w..(t + 1) * w].fill(0.0);
18521 }
18522 }
18523 } else {
18524 for (ci, &i) in cand.iter().enumerate() {
18528 if !alive[ci] {
18529 continue;
18530 }
18531 let seg = &d.segs[i];
18532 let w = seg.width;
18533 let g = &mut acts[ci];
18534 if !tube_score_oracle() {
18535 scratch.clear();
18536 scratch.resize(w, 0.0);
18537 seg.up.matvec(xs, &mut scratch, pool);
18538 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18539 *a *= v;
18540 }
18541 }
18542 let mut acc = vec![0.0f32; hidden];
18543 seg.down.matvec(g, &mut acc, pool);
18544 for (o, a) in out.iter_mut().zip(&acc) {
18545 *o += *a;
18546 }
18547 }
18548 }
18549 }
18550 if b > 1 {
18551 for (ci, &i) in cand.iter().enumerate() {
18552 let seg = &d.segs[i];
18553 let mut acc = vec![0.0f32; b * hidden];
18554 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18555 for (o, a) in out.iter_mut().zip(&acc) {
18556 *o += *a;
18557 }
18558 }
18559 }
18560 out
18561}
18562
18563fn tube_ffn(
18569 d: &DenseFfn,
18570 xs: &[f32],
18571 b: usize,
18572 pool: Option<&Pool>,
18573 mask_row: Option<&[u8]>,
18574) -> Vec<f32> {
18575 if tube_topk() > 0 {
18576 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18577 }
18578 let hidden = d.down_proj.rows();
18579 let core = d.gate_proj.rows();
18580 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18581 let mut out = match (b, core_full, mask_row) {
18582 (1, true, _) => dense_ffn(d, xs, pool),
18583 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18584 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18585 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18586 };
18587 TUBE_SCRATCH.with(|sc| {
18588 let mut sc = sc.borrow_mut();
18589 let [g, u, acc] = &mut *sc;
18590 for seg in &d.segs {
18591 if !tube_bit(mask_row, seg.start) {
18592 continue;
18593 }
18594 let w = seg.width;
18595 g.resize(b * w, 0.0);
18596 if b == 1
18597 && d.act == Act::Silu
18598 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18599 {
18600 } else {
18602 u.resize(b * w, 0.0);
18603 if b == 1 {
18604 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18605 } else {
18606 seg.gate.matmat(xs, b, g, pool);
18607 seg.up.matmat(xs, b, u, pool);
18608 }
18609 for i in 0..b * w {
18610 g[i] = d.act.combine(g[i], u[i]);
18611 }
18612 }
18613 acc.resize(b * hidden, 0.0);
18614 acc.fill(0.0);
18615 if b == 1 {
18616 seg.down.matvec(g, acc, pool);
18617 } else {
18618 seg.down.matmat(g, b, acc, pool);
18619 }
18620 for (o, a) in out.iter_mut().zip(acc.iter()) {
18621 *o += *a;
18622 }
18623 }
18624 out
18625 })
18626}
18627
18628thread_local! {
18629 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18633 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18634}
18635
18636static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18639 std::sync::atomic::AtomicU64::new(0),
18640 std::sync::atomic::AtomicU64::new(0),
18641];
18642
18643fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18647 #[cfg(feature = "gpu")]
18648 {
18649 use std::sync::atomic::Ordering::Relaxed;
18650 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18651 (
18652 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18653 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18654 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18655 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18656 )
18657 }
18658 #[cfg(not(feature = "gpu"))]
18659 (0.0, 0.0, 0.0, 0)
18660}
18661
18662fn wgpu_mirror_counters() -> [u64; 6] {
18665 #[cfg(feature = "gpu")]
18666 {
18667 crate::gpu_wgpu::MIRROR_EVENTS
18668 .each_ref()
18669 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18670 }
18671 #[cfg(not(feature = "gpu"))]
18672 [0; 6]
18673}
18674
18675fn prefill_prof_on() -> bool {
18676 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18677 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18678}
18679
18680pub(crate) fn accumulate_dense_ffn_activation_mass(
18689 d: &DenseFfn,
18690 xs: &[f32],
18691 b: usize,
18692 pool: Option<&Pool>,
18693 mass: &mut [f64],
18694 squared_mass: &mut [f64],
18695) -> Result<(), String> {
18696 let inter = d.gate_proj.rows();
18697 let hidden = d.gate_proj.cols();
18698 if mass.len() != inter || squared_mass.len() != inter {
18699 return Err(format!(
18700 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18701 mass.len(),
18702 squared_mass.len()
18703 ));
18704 }
18705 if d.up_proj.rows() != inter
18706 || d.up_proj.cols() != hidden
18707 || d.down_proj.cols() != inter
18708 || d.down_proj.rows() != hidden
18709 {
18710 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18711 }
18712 if !d.segs.is_empty() {
18713 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18714 }
18715 if xs.len() != b.saturating_mul(hidden) {
18716 return Err(format!(
18717 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18718 xs.len()
18719 ));
18720 }
18721 if b == 0 {
18722 return Ok(());
18723 }
18724 let mut gate = vec![0.0f32; b * inter];
18725 let mut up = vec![0.0f32; b * inter];
18726 d.gate_proj.matmat(xs, b, &mut gate, pool);
18727 d.up_proj.matmat(xs, b, &mut up, pool);
18728 for row in 0..b {
18729 for i in 0..inter {
18730 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18731 let a = a as f64;
18732 mass[i] += a.abs();
18733 squared_mass[i] += a * a;
18734 }
18735 }
18736 Ok(())
18737}
18738
18739fn dense_ffn_batch(
18740 d: &DenseFfn,
18741 xs: &[f32],
18742 b: usize,
18743 pool: Option<&Pool>,
18744 mask_row: Option<&[u8]>,
18745) -> Vec<f32> {
18746 let inter = d.gate_proj.rows();
18747 let hidden = d.down_proj.rows();
18748 let fused_act = d.act.wgpu_graph_act();
18760 if mask_row.is_none()
18761 && fused_act.is_some()
18762 && b >= 32
18763 && crate::gpu::enabled_here()
18764 && !crate::gpu::mm_killed()
18765 && refit_dir().is_none()
18770 && !ffn_probe_active()
18775 && !crate::gptq_capture::capturing()
18779 {
18780 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18781 d.gate_proj.mapped_q4t(),
18782 d.up_proj.mapped_q4t(),
18783 d.down_proj.mapped_q4t(),
18784 ) {
18785 let mut out = vec![0.0f32; b * hidden];
18786 let act = fused_act.expect("checked above");
18787 if crate::gpu::q4_ffn_act(
18788 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18789 ) {
18790 return out;
18791 }
18792 }
18793 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18798 d.gate_proj.mapped_q4tp(),
18799 d.up_proj.mapped_q4tp(),
18800 d.down_proj.mapped_q4tp(),
18801 ) {
18802 let mut out = vec![0.0f32; b * hidden];
18803 let act = fused_act.expect("checked above");
18804 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18805 {
18806 return out;
18807 }
18808 }
18809 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18815 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18816 d.gate_proj.mapped_device_gemm(),
18817 d.up_proj.mapped_device_gemm(),
18818 d.down_proj.mapped_device_gemm(),
18819 ) {
18820 let mut out = vec![0.0f32; b * hidden];
18821 let act = fused_act.expect("checked above");
18822 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18823 return out;
18824 }
18825 }
18826 }
18827 let mut g = vec![0.0f32; b * inter];
18828 d.gate_proj.matmat(xs, b, &mut g, pool);
18829 let mut u = vec![0.0f32; b * inter];
18830 d.up_proj.matmat(xs, b, &mut u, pool);
18831 if gate_topk() > 0 && d.act == Act::Silu {
18832 for t in 0..b {
18833 let row = &mut g[t * inter..(t + 1) * inter];
18834 for v in row.iter_mut() {
18835 *v = Act::Silu.combine(*v, 1.0);
18836 }
18837 keep_top_k(row, gate_topk());
18838 }
18839 for i in 0..b * inter {
18840 g[i] *= u[i];
18841 }
18842 } else {
18843 for i in 0..b * inter {
18844 g[i] = d.act.combine(g[i], u[i]);
18845 }
18846 }
18847 if let Some(row) = mask_row {
18848 zero_masked_cols(&mut g, b, inter, row);
18849 }
18850 if oracle_topk() > 0 {
18851 for t in 0..b {
18852 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18853 }
18854 }
18855 let mut out = vec![0.0f32; b * hidden];
18856 d.down_proj.matmat(&g, b, &mut out, pool);
18857 if refit_dir().is_some() {
18858 let li = crate::gpu::cur_layer();
18859 if li >= 0 {
18860 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18861 }
18862 }
18863 FFN_PROBE.with(|pr| {
18867 if let Some(acc) = pr.borrow_mut().as_mut() {
18868 let li = crate::gpu::cur_layer();
18869 if li < 0 {
18870 return;
18871 }
18872 let Some(row) = acc.get_mut(li as usize) else {
18873 return;
18874 };
18875 let sq = probe_sq();
18876 for t in 0..b {
18877 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18878 *a += if sq {
18879 (v as f64) * (v as f64)
18880 } else {
18881 (v as f64).abs()
18882 };
18883 }
18884 }
18885 }
18886 });
18887 out
18888}
18889
18890fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18895 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18896 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18897 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18898 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18899 if (!on && !dump) || b == 0 {
18900 return;
18901 }
18902 let hidden = xs.len() / b;
18903 if on {
18904 let mut acc = m.act_sq.borrow_mut();
18905 if acc.len() < hidden {
18906 acc.resize(hidden, 0.0);
18907 }
18908 for t in 0..b {
18909 let row = &xs[t * hidden..(t + 1) * hidden];
18910 for (a, &v) in acc.iter_mut().zip(row) {
18911 *a += (v as f64) * (v as f64);
18912 }
18913 }
18914 }
18915 if dump {
18916 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18919 .ok()
18920 .and_then(|v| v.parse().ok())
18921 .unwrap_or(4096);
18922 let mut rows = m.act_rows.borrow_mut();
18923 if rows.len() < cap * hidden {
18924 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18925 rows.extend_from_slice(&xs[..take * hidden]);
18926 }
18927 }
18928}
18929
18930#[derive(Clone, Copy)]
18933struct SendVecs(*mut Vec<f32>);
18934unsafe impl Send for SendVecs {}
18935unsafe impl Sync for SendVecs {}
18936impl SendVecs {
18937 #[inline]
18938 fn at(self, i: usize) -> *mut Vec<f32> {
18939 unsafe { self.0.add(i) }
18940 }
18941}
18942
18943fn moe_ffn_batch(
18944 m: &MoeFfn,
18945 xs: &[f32],
18946 b: usize,
18947 hidden: usize,
18948 pool: Option<&Pool>,
18949 allowed: Option<&[bool]>,
18950) -> Vec<f32> {
18951 accumulate_act(m, xs, b);
18952 let ne = m.experts.len();
18953 let mut logits = vec![0.0f32; b * ne];
18954 match &m.resonance {
18955 Some(r) => {
18956 let hdim = xs.len() / b.max(1);
18957 for bi in 0..b {
18958 r.scores(
18959 &xs[bi * hdim..(bi + 1) * hdim],
18960 &mut logits[bi * ne..(bi + 1) * ne],
18961 );
18962 }
18963 }
18964 None => m.router.matmat(xs, b, &mut logits, pool),
18965 }
18966
18967 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18970 {
18971 let mut st = m.stats.borrow_mut();
18972 if st.len() < ne {
18973 st.resize(ne, 0);
18974 }
18975 for bi in 0..b {
18976 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18977 for &e in &idx {
18978 st[e] += 1;
18979 assign[e].push((bi, p[e] / wsum));
18980 }
18981 }
18982 }
18983
18984 let mut out = vec![0.0f32; b * hidden];
18985 let cols = m.experts[0].gate_proj.cols();
18986 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18987 let sb = list.len();
18988 let mut sub = vec![0.0f32; sb * cols];
18989 for (k, &(bi, _)) in list.iter().enumerate() {
18990 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18991 }
18992 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18993 for (k, &(bi, w)) in list.iter().enumerate() {
18994 for i in 0..hidden {
18995 out[bi * hidden + i] += w * eo[k * hidden + i];
18996 }
18997 }
18998 };
18999 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
19005 if pool.is_some() && active.len() >= 8 {
19006 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
19007 {
19008 let panel_ptr = SendVecs(panels.as_mut_ptr());
19009 let experts = &m.experts;
19012 let (active_r, assign_r) = (&active, &assign);
19013 let inherit_cpu = crate::gpu::inherit_cpu_scope();
19014 let run = |start: usize, end: usize| {
19015 let _cpu_scope = inherit_cpu();
19016 #[cfg(target_os = "macos")]
19024 let _metal_cpu = crate::gpu::enter_cpu_scope();
19025 for ai in start..end {
19026 let e = active_r[ai];
19027 let list = &assign_r[e];
19028 let sb = list.len();
19029 let mut sub = vec![0.0f32; sb * cols];
19030 for (k, &(bi, _)) in list.iter().enumerate() {
19031 sub[k * cols..(k + 1) * cols]
19032 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19033 }
19034 unsafe {
19036 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
19037 }
19038 }
19039 };
19040 match pool {
19041 Some(p) => p.run_rows(active.len(), &run),
19042 None => run(0, active.len()),
19043 }
19044 }
19045 for (ai, &e) in active.iter().enumerate() {
19046 for (k, &(bi, w)) in assign[e].iter().enumerate() {
19047 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
19048 for i in 0..hidden {
19049 out[bi * hidden + i] += w * eo[i];
19050 }
19051 }
19052 }
19053 } else {
19054 for &e in &active {
19055 run_expert(&m.experts[e], &assign[e], &mut out);
19056 }
19057 }
19058 if let Some((se, gate)) = &m.shared {
19059 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
19060 let mut gl = vec![0.0f32; b];
19061 gate.matmat(xs, b, &mut gl, pool);
19062 (0..b)
19063 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
19064 .collect()
19065 } else {
19066 (0..b).map(|bi| (bi, 1.0)).collect()
19067 };
19068 run_expert(se, &all, &mut out);
19069 }
19070 out
19071}
19072
19073fn moe_ffn_rows_exact(
19085 m: &MoeFfn,
19086 xs: &[f32],
19087 b: usize,
19088 hidden: usize,
19089 pool: Option<&Pool>,
19090) -> Vec<f32> {
19091 let mut out = vec![0.0f32; b * hidden];
19092 let per_row = |out: &mut [f32]| {
19093 for r in 0..b {
19094 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
19095 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
19096 }
19097 };
19098 let covered = !crate::gpu::enabled_here()
19099 && moe_batch_enabled()
19100 && m.shared.is_none()
19101 && m.resonance.is_none()
19102 && FFN_PROBE.with(|pr| pr.borrow().is_none())
19103 && m.experts.iter().all(|d| d.act == Act::Silu);
19104 if !covered {
19105 per_row(&mut out);
19106 return out;
19107 }
19108 let ne = m.experts.len();
19109 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
19111 for r in 0..b {
19112 let x = &xs[r * hidden..(r + 1) * hidden];
19113 accumulate_act(m, x, 1);
19114 let mut logits = vec![0.0f32; ne];
19115 m.router.matvec(x, &mut logits, pool);
19116 let (idx, p, wsum) = moe_route(&logits, m, None);
19117 {
19118 let mut st = m.stats.borrow_mut();
19119 if st.len() < ne {
19120 st.resize(ne, 0);
19121 }
19122 for &e in &idx {
19123 st[e] += 1;
19124 }
19125 }
19126 let w: Vec<f32> = idx
19127 .iter()
19128 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
19129 .collect();
19130 routes.push((idx, w));
19131 }
19132 if routes.iter().any(|(idx, _)| idx.is_empty()) {
19133 per_row(&mut out);
19134 return out;
19135 }
19136 let mut experts: Vec<usize> = Vec::new();
19138 let mut groups: Vec<Vec<usize>> = Vec::new();
19139 for (r, (idx, _)) in routes.iter().enumerate() {
19140 for &e in idx {
19141 match experts.iter().position(|&x| x == e) {
19142 Some(g) => groups[g].push(r),
19143 None => {
19144 experts.push(e);
19145 groups.push(vec![r]);
19146 }
19147 }
19148 }
19149 }
19150 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19151 let inter = m.experts[experts[0]].gate_proj.rows();
19152 let pairs: Vec<(&QTensor, &QTensor)> = experts
19153 .iter()
19154 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19155 .collect();
19156 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19157 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19158 per_row(&mut out);
19159 return out;
19160 }
19161 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19162 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19163 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19164 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19165 per_row(&mut out);
19166 return out;
19167 }
19168 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19170 let mut p = 0usize;
19171 for (g, &e) in experts.iter().enumerate() {
19172 for &r in &groups[g] {
19173 slot.insert((r, e), p);
19174 p += 1;
19175 }
19176 }
19177 for (r, (idx, w)) in routes.iter().enumerate() {
19178 let terms: Vec<(&[f32], f32)> = idx
19179 .iter()
19180 .zip(w)
19181 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19182 .collect();
19183 let row = &mut out[r * hidden..(r + 1) * hidden];
19184 for (i, dst) in row.iter_mut().enumerate() {
19185 let mut acc = 0f32;
19187 for (d, we) in &terms {
19188 acc += we * d[i];
19189 }
19190 *dst = acc;
19191 }
19192 }
19193 out
19194}
19195
19196pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
19200 match pool {
19201 Some(p) if v.len() >= 1 << 15 => {
19202 let base = v.as_mut_ptr() as usize;
19203 p.run_rows(v.len(), &|s, e| {
19204 let sl =
19207 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
19208 for x in sl {
19209 *x = c * (*x / c).tanh();
19210 }
19211 });
19212 }
19213 _ => {
19214 for x in v.iter_mut() {
19215 *x = c * (*x / c).tanh();
19216 }
19217 }
19218 }
19219}
19220
19221thread_local! {
19222 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
19226 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
19227}
19228
19229fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19231 if gate_topk() > 0
19234 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
19235 {
19236 return out;
19237 }
19238 let prism_body = d.gate_proj.has_prism_contract()
19254 || d.up_proj.has_prism_contract()
19255 || d.down_proj.has_prism_contract();
19256 if !prism_body
19257 && crate::gpu::enabled_here()
19258 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19259 {
19260 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19261 crate::gpu::ProbeArm::Gpu
19262 } else {
19263 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19264 };
19265 match arm {
19266 crate::gpu::ProbeArm::Gpu => {
19267 let t0 = std::time::Instant::now();
19268 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19269 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19270 return out;
19271 }
19272 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19276 }
19277 crate::gpu::ProbeArm::CpuTimed => {
19278 let t0 = std::time::Instant::now();
19279 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19280 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19281 return out;
19282 }
19283 crate::gpu::ProbeArm::Cpu => {
19284 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19285 }
19286 }
19287 }
19288 dense_ffn_cpu(d, x, pool)
19289}
19290
19291fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19293 let inter = d.gate_proj.rows();
19294 FFN_SCRATCH.with(|s| {
19295 let mut s = s.borrow_mut();
19296 let [g, u, ..] = &mut *s;
19297 g.resize(inter, 0.0);
19298 if gate_topk() > 0 {
19301 u.resize(inter, 0.0);
19305 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19306 for i in 0..inter {
19307 g[i] = Act::Silu.combine(g[i], 1.0);
19308 }
19309 keep_top_k(g, gate_topk());
19310 for i in 0..inter {
19311 g[i] *= u[i];
19312 }
19313 } else if d.act == Act::Silu && {
19314 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19315 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19316 } {
19317 } else {
19319 u.resize(inter, 0.0);
19320 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19322 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19323 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19324 for i in 0..inter {
19325 g[i] = d.act.combine(g[i], u[i]);
19326 }
19327 }
19328 FFN_PROBE.with(|pr| {
19336 if let Some(acc) = pr.borrow_mut().as_mut() {
19337 let li = crate::gpu::cur_layer();
19338 if li >= 0 {
19339 if let Some(row) = acc.get_mut(li as usize) {
19340 match probe_topk() {
19341 0 if probe_sq() => {
19342 for (a, &v) in row.iter_mut().zip(g.iter()) {
19343 *a += (v as f64) * (v as f64);
19344 }
19345 }
19346 0 if probe_signed() => {
19347 for (a, &v) in row.iter_mut().zip(g.iter()) {
19348 *a += v as f64;
19349 }
19350 }
19351 0 => {
19352 for (a, &v) in row.iter_mut().zip(g.iter()) {
19353 *a += (v as f64).abs();
19354 }
19355 }
19356 k => {
19357 let n = g.len();
19358 let k = k.min(n);
19359 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19360 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19361 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19362 });
19363 let thr = *kth;
19364 for (a, &v) in row.iter_mut().zip(g.iter()) {
19365 if v.abs() >= thr {
19366 *a += 1.0;
19367 }
19368 }
19369 }
19370 }
19371 }
19372 }
19373 }
19374 });
19375 if oracle_topk() > 0 {
19376 keep_top_k(g, oracle_topk());
19377 }
19378 {
19379 let li = crate::gpu::cur_layer();
19380 if li >= 0 {
19381 adump_row(li as usize, g);
19382 }
19383 }
19384 let mut out = attention::take_buf(d.down_proj.rows());
19385 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19386 d.down_proj.matvec(g, &mut out, pool);
19387 out
19388 })
19389}
19390
19391pub struct RefitAcc {
19404 pub support: Vec<u32>,
19405 pub gss: Vec<f32>,
19406 pub ya: Vec<f32>,
19407 pub hidden: usize,
19408 pub tokens: u64,
19409 pub buf_g: Vec<f32>,
19415 pub buf_o: Vec<f32>,
19416 pub buf_t: usize,
19417}
19418
19419type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19423
19424static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19425 std::sync::OnceLock::new();
19426
19427fn ffn_probe_active() -> bool {
19430 FFN_PROBE.with(|p| p.borrow().is_some())
19431}
19432
19433fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19434 REFIT
19435 .get_or_init(|| {
19436 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19437 (
19438 d,
19439 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19440 )
19441 })
19442 })
19443 .as_ref()
19444}
19445
19446fn refit_accumulate(
19448 li: usize,
19449 g: &[f32],
19450 b: usize,
19451 inter: usize,
19452 out: &[f32],
19453 hidden: usize,
19454 pool: Option<&Pool>,
19455) {
19456 let Some((dir, map)) = refit_dir() else {
19457 return;
19458 };
19459 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19460 let (from, to) = *SPAN.get_or_init(|| {
19461 let g = |k: &str, d: usize| {
19462 std::env::var(k)
19463 .ok()
19464 .and_then(|v| v.parse().ok())
19465 .unwrap_or(d)
19466 };
19467 (
19468 g("CMF_FFN_REFIT_FROM", 0),
19469 g("CMF_FFN_REFIT_TO", usize::MAX),
19470 )
19471 });
19472 if li < from || li > to {
19473 return;
19474 }
19475 let mut guard = map.lock().unwrap();
19476 let (map, shared) = &mut *guard;
19477 let acc = match map.entry(li) {
19478 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19479 std::collections::hash_map::Entry::Vacant(e) => {
19480 let path = format!("{dir}/support.{li}.u32");
19481 let Ok(bytes) = std::fs::read(&path) else {
19482 eprintln!("refit: no {path} — layer {li} skipped");
19483 return;
19484 };
19485 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19486 let support: Vec<u32> = bytes[4..4 + n * 4]
19487 .chunks_exact(4)
19488 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19489 .collect();
19490 eprintln!(
19491 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19492 (n * n + hidden * n) as f64 * 4.0 / 1e6
19493 );
19494 e.insert(RefitAcc {
19495 gss: vec![0.0; n * n],
19496 ya: vec![0.0; hidden * n],
19497 buf_g: Vec::new(),
19498 buf_o: Vec::new(),
19499 buf_t: 0,
19500 support,
19501 hidden,
19502 tokens: 0,
19503 })
19504 }
19505 };
19506 let ns = acc.support.len();
19507 let cap = refit_batch();
19509 if acc.buf_g.is_empty() {
19510 acc.buf_g = vec![0.0; ns * cap];
19511 acc.buf_o = vec![0.0; hidden * cap];
19512 }
19513 let take = b.min(cap - acc.buf_t);
19514 for t in 0..take {
19515 let col = acc.buf_t + t;
19516 for (j, &n) in acc.support.iter().enumerate() {
19517 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19518 }
19519 for h in 0..hidden {
19520 acc.buf_o[h * cap + col] = out[t * hidden + h];
19521 }
19522 }
19523 acc.buf_t += take;
19524 acc.tokens += take as u64;
19525 if acc.buf_t < cap {
19526 return;
19527 }
19528 let bt = acc.buf_t;
19529 acc.buf_t = 0;
19530 let RefitAcc {
19540 gss,
19541 ya,
19542 buf_g,
19543 buf_o,
19544 ..
19545 } = acc;
19546 let need = (ns * ns).max(hidden * ns);
19547 if shared.len() < need {
19548 shared.resize(need, 0.0);
19549 }
19550 let scratch = &mut shared[..];
19551 let _ = bt;
19552 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19553 add_into(gss, &scratch[..ns * ns], pool);
19554 if crate::gpu::gemm_nt_f32_transient(
19555 buf_o,
19556 buf_g,
19557 &mut scratch[..hidden * ns],
19558 hidden,
19559 cap,
19560 ns,
19561 ) {
19562 add_into(ya, &scratch[..hidden * ns], pool);
19563 } else {
19564 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19565 }
19566 } else {
19567 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19568 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19569 }
19570 }
19574
19575fn refit_batch() -> usize {
19577 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19578 *B.get_or_init(|| {
19579 std::env::var("CMF_FFN_REFIT_BATCH")
19580 .ok()
19581 .and_then(|v| v.parse().ok())
19582 .unwrap_or(4096)
19583 })
19584}
19585
19586fn accum_outer_t(
19589 c: &mut [f32],
19590 m: usize,
19591 n: usize,
19592 b: usize,
19593 left: &[f32],
19594 right: &[f32],
19595 pool: Option<&Pool>,
19596) {
19597 let ptr = SendMut(c.as_mut_ptr());
19598 let body = |i: usize| {
19599 let ptr = &ptr;
19600 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19601 for t in 0..b {
19602 let a = left[i * b + t];
19603 if a == 0.0 {
19604 continue;
19605 }
19606 for (j, o) in row.iter_mut().enumerate() {
19607 *o += a * right[j * b + t];
19608 }
19609 }
19610 };
19611 match pool {
19612 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19613 for i in s..e {
19614 body(i);
19615 }
19616 }),
19617 _ => {
19618 for i in 0..m {
19619 body(i);
19620 }
19621 }
19622 }
19623}
19624
19625fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19628 let n = dst.len().min(src.len());
19629 match pool {
19630 Some(p) if n >= 1 << 16 => {
19631 let ptr = SendMut(dst.as_mut_ptr());
19632 let f = |s: usize, e: usize| {
19633 let ptr = &ptr;
19634 for blk in s..e {
19635 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19636 for i in a..b {
19637 unsafe { *ptr.0.add(i) += src[i] };
19638 }
19639 }
19640 };
19641 p.run_rows(n.div_ceil(4096), &f);
19642 }
19643 _ => {
19644 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19645 *d += *v;
19646 }
19647 }
19648 }
19649}
19650
19651fn accum_outer(
19656 c: &mut [f32],
19657 m: usize,
19658 n: usize,
19659 b: usize,
19660 left: &[f32],
19661 right: &[f32],
19662 pool: Option<&Pool>,
19663) {
19664 const TILE: usize = 32;
19665 let tiles = m.div_ceil(TILE);
19666 let cp = SendMut(c.as_mut_ptr());
19667 let body = |ti: usize| {
19668 let cp = &cp;
19669 let i0 = ti * TILE;
19670 let i1 = (i0 + TILE).min(m);
19671 for t in 0..b {
19672 let r = &right[t * n..t * n + n];
19673 for i in i0..i1 {
19674 let a = left[i * b + t];
19675 if a == 0.0 {
19676 continue;
19677 }
19678 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19680 for (o, v) in row.iter_mut().zip(r) {
19681 *o += a * *v;
19682 }
19683 }
19684 }
19685 };
19686 match pool {
19687 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19688 for ti in s..e {
19689 body(ti);
19690 }
19691 }),
19692 _ => {
19693 for ti in 0..tiles {
19694 body(ti);
19695 }
19696 }
19697 }
19698}
19699
19700pub fn refit_flush() -> usize {
19702 let Some((dir, map)) = refit_dir() else {
19703 return 0;
19704 };
19705 let guard = map.lock().unwrap();
19706 let mut n = 0;
19707 for (li, acc) in guard.0.iter() {
19708 let w = |name: &str, v: &[f32]| {
19711 let path = format!("{dir}/{name}.{li}.f32");
19712 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19713 match std::fs::write(&path, &bytes) {
19714 Ok(()) => {}
19715 Err(e) => eprintln!(
19716 "refit: FAILED to write {path} ({} MB): {e}",
19717 bytes.len() / 1_000_000
19718 ),
19719 }
19720 };
19721 w("gss", &acc.gss);
19722 w("ya", &acc.ya);
19723 println!(
19724 "refit L{li}: {} support, {} tokens, hidden {}",
19725 acc.support.len(),
19726 acc.tokens,
19727 acc.hidden
19728 );
19729 n += 1;
19730 }
19731 n
19732}
19733
19734fn adump_row(li: usize, g: &[f32]) {
19739 use std::io::Write as _;
19740 static FILES: std::sync::OnceLock<
19741 Option<(
19742 String,
19743 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19744 )>,
19745 > = std::sync::OnceLock::new();
19746 let Some((prefix, map)) = FILES
19747 .get_or_init(|| {
19748 std::env::var("CMF_FFN_ADUMP")
19749 .ok()
19750 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19751 })
19752 .as_ref()
19753 else {
19754 return;
19755 };
19756 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19759 let (from, to) = *SPAN.get_or_init(|| {
19760 let g = |k: &str, d: usize| {
19761 std::env::var(k)
19762 .ok()
19763 .and_then(|v| v.parse().ok())
19764 .unwrap_or(d)
19765 };
19766 (
19767 g("CMF_FFN_ADUMP_FROM", 0),
19768 g("CMF_FFN_ADUMP_TO", usize::MAX),
19769 )
19770 });
19771 if li < from || li > to {
19772 return;
19773 }
19774 let mut map = map.lock().unwrap();
19775 let f = map.entry(li).or_insert_with(|| {
19776 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19777 });
19778 let mut bytes = Vec::with_capacity(g.len() * 2);
19779 for v in g {
19780 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19781 }
19782 let _ = f.write_all(&bytes);
19783}
19784
19785fn oracle_topk() -> usize {
19791 if exact_mtp_calibration_active() {
19792 return 0;
19793 }
19794 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19795 *K.get_or_init(|| {
19796 std::env::var("CMF_FFN_ORACLE_TOPK")
19797 .ok()
19798 .and_then(|v| v.parse().ok())
19799 .unwrap_or(0)
19800 })
19801}
19802
19803fn gate_topk() -> usize {
19809 if exact_mtp_calibration_active() {
19810 return 0;
19811 }
19812 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19813 *K.get_or_init(|| {
19814 std::env::var("CMF_FFN_GATE_TOPK")
19815 .ok()
19816 .and_then(|v| v.parse().ok())
19817 .unwrap_or(0)
19818 })
19819}
19820
19821fn gate_block() -> usize {
19828 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19829 *B.get_or_init(|| {
19830 std::env::var("CMF_FFN_GATE_BLOCK")
19831 .ok()
19832 .and_then(|v| v.parse().ok())
19833 .unwrap_or(1)
19834 })
19835}
19836
19837fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19839 let n = g.len();
19840 let nb = n.div_ceil(block);
19841 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19842 if kb >= nb {
19843 return;
19844 }
19845 let mut score: Vec<f32> = (0..nb)
19846 .map(|b| {
19847 g[b * block..((b + 1) * block).min(n)]
19848 .iter()
19849 .map(|v| v * v)
19850 .sum::<f32>()
19851 })
19852 .collect();
19853 let mut ord = score.clone();
19854 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19855 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19856 });
19857 let thr = *kth;
19858 for b in 0..nb {
19859 if score[b] < thr {
19860 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19861 }
19862 }
19863 score.clear();
19864}
19865
19866fn keep_top_k(g: &mut [f32], k: usize) {
19868 if gate_block() > 1 {
19869 return keep_top_blocks(g, k, gate_block());
19870 }
19871 let n = g.len();
19872 if k == 0 || k >= n {
19873 return;
19874 }
19875 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19876 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19877 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19878 });
19879 let thr = *kth;
19880 for v in g.iter_mut() {
19881 if v.abs() < thr {
19882 *v = 0.0;
19883 }
19884 }
19885}
19886
19887fn probe_sq() -> bool {
19891 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19892 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19893}
19894
19895fn probe_signed() -> bool {
19899 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19900 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19901}
19902
19903fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19911 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19912 M.get_or_init(|| {
19913 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19914 let b = std::fs::read(&p).ok()?;
19915 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19916 let vals: Vec<f32> = b[8..]
19917 .chunks_exact(4)
19918 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19919 .collect();
19920 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19921 Some((inter, vals))
19922 })
19923 .as_ref()
19924}
19925
19926fn probe_topk() -> usize {
19929 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19930 *K.get_or_init(|| {
19931 std::env::var("CMF_FFN_PROBE_TOPK")
19932 .ok()
19933 .and_then(|v| v.parse().ok())
19934 .unwrap_or(0)
19935 })
19936}
19937
19938thread_local! {
19939 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19942 const { std::cell::RefCell::new(None) };
19943}
19944
19945fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19958 if d.gate_proj.has_prism_contract()
19963 || d.up_proj.has_prism_contract()
19964 || d.down_proj.has_prism_contract()
19965 {
19966 return None;
19967 }
19968 let dt = d.down_t.as_ref()?;
19969 let inter = d.gate_proj.rows();
19970 let hidden = dt.cols();
19971 if k == 0 || k >= inter || d.act != Act::Silu {
19972 return None;
19973 }
19974 DYN_SCRATCH.with(|sc| {
19975 let mut sc = sc.borrow_mut();
19976 let DynScratch {
19977 g,
19978 mag,
19979 live,
19980 parts,
19981 } = &mut *sc;
19982 g.resize(inter, 0.0);
19983 d.gate_proj.matvec(x, g, pool);
19984 for v in g.iter_mut() {
19985 *v = inference::silu(*v);
19986 }
19987 mag.clear();
19990 mag.extend(g.iter().map(|v| v.abs()));
19991 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19992 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19993 });
19994 let thr = *kth;
19995 live.clear();
19996 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19997 let mut out = vec![0.0f32; hidden];
19998 match pool {
19999 Some(p) if live.len() >= 64 => {
20000 let nw = p.n_workers() + 1;
20001 parts.clear();
20002 parts.resize(nw * hidden, 0.0);
20003 let ptr = SendMut(parts.as_mut_ptr());
20004 let n = live.len();
20005 let live_ref: &[u32] = live;
20006 let g_ref: &[f32] = g;
20007 p.run(&|w, workers| {
20008 let chunk = n.div_ceil(workers);
20009 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
20010 if s >= e {
20011 return;
20012 }
20013 WORKER_SCRATCH.with(|ws| {
20014 let mut ws = ws.borrow_mut();
20015 let [scratch, acc] = &mut *ws;
20016 scratch.resize(hidden.max(x.len()), 0.0);
20017 acc.clear();
20018 acc.resize(hidden, 0.0);
20019 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
20020 if let Some(&nx) = live_ref[s..e].get(o + 1) {
20023 d.up_proj.prefetch_row(nx as usize);
20024 dt.prefetch_row(nx as usize);
20025 }
20026 let idx = nrm as usize;
20027 let up = d.up_proj.row_dot(idx, x, scratch);
20028 let a = g_ref[idx] * up;
20029 if a != 0.0 {
20030 dt.add_row_scaled(idx, a, acc, scratch);
20031 }
20032 }
20033 for (j, v) in acc.iter().enumerate() {
20034 unsafe { *ptr.at(w * hidden + j) = *v };
20035 }
20036 });
20037 });
20038 for w in 0..nw {
20039 for (j, o) in out.iter_mut().enumerate() {
20040 *o += parts[w * hidden + j];
20041 }
20042 }
20043 }
20044 _ => {
20045 WORKER_SCRATCH.with(|ws| {
20046 let mut ws = ws.borrow_mut();
20047 let [scratch, _acc] = &mut *ws;
20048 scratch.resize(hidden.max(x.len()), 0.0);
20049 for &nrm in live.iter() {
20050 let idx = nrm as usize;
20051 let up = d.up_proj.row_dot(idx, x, scratch);
20052 let a = g[idx] * up;
20053 if a != 0.0 {
20054 dt.add_row_scaled(idx, a, &mut out, scratch);
20055 }
20056 }
20057 });
20058 }
20059 }
20060 Some(out)
20061 })
20062}
20063
20064struct DynScratch {
20067 g: Vec<f32>,
20068 mag: Vec<f32>,
20069 live: Vec<u32>,
20070 parts: Vec<f32>,
20071}
20072
20073thread_local! {
20074 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
20075 std::cell::RefCell::new(DynScratch {
20076 g: Vec::new(),
20077 mag: Vec::new(),
20078 live: Vec::new(),
20079 parts: Vec::new(),
20080 })
20081 };
20082 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
20084 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
20085}
20086
20087fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
20092 let inter = d.gate_proj.rows();
20093 FFN_SCRATCH.with(|s| {
20094 let mut s = s.borrow_mut();
20095 let [g, u, ..] = &mut *s;
20096 g.resize(inter, 0.0);
20097 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
20098 } else {
20100 u.resize(inter, 0.0);
20101 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20102 for i in 0..inter {
20103 g[i] = d.act.combine(g[i], u[i]);
20104 }
20105 }
20106 zero_masked_cols(g, 1, inter, mask_row);
20107 let mut out = attention::take_buf(d.down_proj.rows());
20108 d.down_proj.matvec(g, &mut out, pool);
20109 out
20110 })
20111}
20112
20113fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
20119 if d.gate_proj.has_prism_contract()
20120 || d.up_proj.has_prism_contract()
20121 || d.down_proj.has_prism_contract()
20122 {
20123 return None;
20124 }
20125 d.act.moe_job_graph_contract()?;
20128 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
20131 return None;
20132 }
20133 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
20134 let mut model_ref = None;
20135 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
20136 let model = model_ref?;
20137 let hidden = jobs[0].down.1;
20138 let mut out = attention::take_buf(hidden);
20139 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20140 Some(out)
20141 } else {
20142 let mut out = out;
20143 attention::recycle_buf(&mut out);
20144 None
20145 }
20146}
20147
20148#[allow(clippy::type_complexity)]
20153#[allow(clippy::type_complexity)]
20154pub(crate) fn moe_parts(
20155 t: &QTensor,
20156) -> Option<(
20157 &std::sync::Arc<cortiq_core::CmfModel>,
20158 usize,
20159 usize,
20160 usize,
20161 &[f32],
20162 &[f32],
20163 bool,
20164 bool,
20165 bool,
20166)> {
20167 match t {
20168 QTensor::Mapped {
20169 model,
20170 idx,
20171 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20172 rows,
20173 cols,
20174 row_scale,
20175 col_field,
20176 ..
20177 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20178 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20179 )),
20180 QTensor::Mapped {
20182 model,
20183 idx,
20184 dtype: cortiq_core::TensorDtype::Q1,
20185 rows,
20186 cols,
20187 ..
20188 } => Some((
20189 model,
20190 *idx,
20191 *rows,
20192 *cols,
20193 &[][..],
20194 &[][..],
20195 true,
20196 false,
20197 false,
20198 )),
20199 QTensor::Mapped {
20201 model,
20202 idx,
20203 dtype: cortiq_core::TensorDtype::Q4Tiled,
20204 rows,
20205 cols,
20206 ..
20207 } => Some((
20208 model,
20209 *idx,
20210 *rows,
20211 *cols,
20212 &[][..],
20213 &[][..],
20214 false,
20215 true,
20216 false,
20217 )),
20218 QTensor::Mapped {
20220 model,
20221 idx,
20222 dtype: cortiq_core::TensorDtype::Q4TiledP,
20223 rows,
20224 cols,
20225 ..
20226 } => Some((
20227 model,
20228 *idx,
20229 *rows,
20230 *cols,
20231 &[][..],
20232 &[][..],
20233 false,
20234 true,
20235 false,
20236 )),
20237 QTensor::Mapped {
20241 model,
20242 idx,
20243 dtype: cortiq_core::TensorDtype::Q2TiledP,
20244 rows,
20245 cols,
20246 ..
20247 } => Some((
20248 model,
20249 *idx,
20250 *rows,
20251 *cols,
20252 &[][..],
20253 &[][..],
20254 false,
20255 true,
20256 true,
20257 )),
20258 _ => None,
20259 }
20260}
20261
20262#[cfg(target_os = "macos")]
20269#[allow(clippy::type_complexity)]
20270fn chunk_moe_parts(
20271 m: &MoeFfn,
20272 hidden: usize,
20273) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20274 if m.shared.is_some()
20275 || m.per_expert_scale.is_some()
20276 || m.resonance.is_some()
20277 || m.route_tau.is_some()
20278 || m.router_input_norm
20279 || m.experts.is_empty()
20280 || m.top_k == 0
20281 || std::env::var("CMF_RMS_TRACE").is_ok()
20282 || std::env::var("CMF_ACT_DUMP").is_ok()
20283 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20284 {
20285 return None;
20286 }
20287 let (rf, rr, rc) = m.router.f32_parts()?;
20288 if rr != m.experts.len() || rc != hidden {
20289 return None;
20290 }
20291 let inter = m.experts[0].gate_proj.rows();
20292 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20295 let experts = m
20296 .experts
20297 .iter()
20298 .map(|e| {
20299 if e.act != Act::Silu
20300 || !e.segs.is_empty()
20301 || e.gate_proj.rows() != inter
20302 || e.up_proj.rows() != inter
20303 || e.down_proj.cols() != inter
20304 || e.gate_proj.cols() != hidden
20305 || e.down_proj.rows() != hidden
20306 {
20307 return None;
20308 }
20309 if q8 {
20310 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20311 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20312 }
20313 Some((
20314 e.gate_proj.mapped_q4tp()?.1,
20315 e.up_proj.mapped_q4tp()?.1,
20316 e.down_proj.mapped_q4tp()?.1,
20317 ))
20318 })
20319 .collect::<Option<Vec<_>>>()?;
20320 Some((rf, experts, q8))
20321}
20322
20323#[cfg(target_os = "macos")]
20336fn metal_flint_moe_graph_parts(
20337 m: &MoeFfn,
20338 hidden: usize,
20339 scratch_key: usize,
20340) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20341 use std::sync::Arc;
20342
20343 const HIDDEN: usize = 2_048;
20344 const ROUTED_INTER: usize = 512;
20345 const SHARED_INTER: usize = 1_024;
20346 const EXPERTS: usize = 256;
20347 const TOP_K: usize = 6;
20348
20349 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20350 return None;
20351 }
20352 let routed0 = m.experts.first()?;
20353 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20354 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20355 if router_rows != EXPERTS || router_cols != HIDDEN {
20356 return None;
20357 }
20358 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20359 return None;
20360 }
20361 let same_q2 = |t: &QTensor| -> Option<usize> {
20362 let (owner, idx) = t.mapped_q2tp()?;
20363 Arc::ptr_eq(&owner, &model).then_some(idx)
20364 };
20365 let same_q4 = |t: &QTensor| -> Option<usize> {
20366 let (owner, idx) = t.mapped_q4tp()?;
20367 Arc::ptr_eq(&owner, &model).then_some(idx)
20368 };
20369 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20370 if e.act != Act::Silu
20371 || !e.segs.is_empty()
20372 || e.gate_proj.rows() != ROUTED_INTER
20373 || e.gate_proj.cols() != HIDDEN
20374 || e.up_proj.rows() != ROUTED_INTER
20375 || e.up_proj.cols() != HIDDEN
20376 || e.down_proj.rows() != HIDDEN
20377 || e.down_proj.cols() != ROUTED_INTER
20378 {
20379 return None;
20380 }
20381 Some((
20382 same_q2(&e.gate_proj)?,
20383 same_q2(&e.up_proj)?,
20384 same_q4(&e.down_proj)?,
20385 ))
20386 };
20387 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20388 if e.act != Act::Silu
20389 || !e.segs.is_empty()
20390 || e.gate_proj.rows() != SHARED_INTER
20391 || e.gate_proj.cols() != HIDDEN
20392 || e.up_proj.rows() != SHARED_INTER
20393 || e.up_proj.cols() != HIDDEN
20394 || e.down_proj.rows() != HIDDEN
20395 || e.down_proj.cols() != SHARED_INTER
20396 {
20397 return None;
20398 }
20399 Some((
20400 same_q4(&e.gate_proj)?,
20401 same_q4(&e.up_proj)?,
20402 same_q4(&e.down_proj)?,
20403 ))
20404 };
20405 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20406 if experts.len() != EXPERTS {
20407 return None;
20408 }
20409 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20410 if shared_gate.is_some() {
20411 return None;
20412 }
20413 Some(crate::gpu::GpuFlintMoe {
20414 router,
20415 bias: m.expert_bias.as_deref(),
20416 experts,
20417 scratch_key,
20418 shared: shared(shared_ffn)?,
20419 top_k: m.top_k,
20420 norm_topk: m.norm_topk_prob,
20421 route_scale: m.routed_scaling,
20422 sigmoid: m.router_sigmoid,
20423 sigmoid_denom_eps: m.sigmoid_denom_eps,
20424 })
20425}
20426
20427#[cfg(target_os = "macos")]
20428fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20429 if m.router_input_norm
20430 || m.route_tau.is_some()
20431 || m.mask.is_some()
20432 || m.per_expert_scale.is_some()
20433 || m.experts.is_empty()
20434 || m.top_k == 0
20435 || m.resonance.is_some()
20436 {
20437 return None;
20438 }
20439 let (sh, sg) = match &m.shared {
20444 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20445 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20446 None => return None,
20447 };
20448 let (rf, rr, rc) = m.router.f32_parts()?;
20449 if rr != m.experts.len() || rc != hidden {
20450 return None;
20451 }
20452 let shared_gated = sg.is_some();
20453 let sf = match sg {
20454 Some(sg) => {
20455 let (sf, sr, sc) = sg.f32_parts()?;
20456 if sr * sc != hidden {
20457 return None;
20458 }
20459 sf
20460 }
20461 None => &rf[..hidden],
20464 };
20465 if let Some(b) = &m.expert_bias {
20466 if b.len() != m.experts.len() {
20467 return None;
20468 }
20469 }
20470 let inter = m.experts[0].gate_proj.rows();
20471 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20474 let q8 = crate::gpu_metal::moe_q8::enabled()
20478 && m.shared.is_none()
20479 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20480 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20481 if e.act != Act::Silu
20482 || e.gate_proj.rows() != inter
20483 || e.gate_proj.cols() != hidden
20484 || e.up_proj.rows() != inter
20485 || e.up_proj.cols() != hidden
20486 || e.down_proj.rows() != hidden
20487 || e.down_proj.cols() != inter
20488 {
20489 return None;
20490 }
20491 if q8 {
20492 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20493 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20494 }
20495 let pick = |t: &QTensor| -> Option<usize> {
20496 if gu_q2 {
20497 t.mapped_q2tp().map(|(_, i)| i)
20498 } else {
20499 t.mapped_q4tp().map(|(_, i)| i)
20500 }
20501 };
20502 Some((
20503 pick(&e.gate_proj)?,
20504 pick(&e.up_proj)?,
20505 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20506 ))
20507 };
20508 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20509 let shared = match sh {
20510 Some(sh) => Some(trio(sh)?),
20511 None => None,
20512 };
20513 Some(crate::gpu::GpuMoe {
20514 router: rf,
20515 sgate: sf,
20516 experts,
20517 shared,
20518 n_exp: m.experts.len(),
20519 top_k: m.top_k,
20520 inter,
20521 norm_topk: m.norm_topk_prob,
20522 route_scale: m.routed_scaling,
20523 gu_q2,
20524 q8,
20525 sigmoid: m.router_sigmoid,
20526 sigmoid_denom_eps: m.sigmoid_denom_eps,
20527 bias: m.expert_bias.as_deref(),
20528 shared_gated,
20529 })
20530}
20531
20532fn forced_ids_at(step: usize) -> Option<u32> {
20538 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20539 IDS.get_or_init(|| {
20540 std::env::var("CMF_FORCE_IDS")
20541 .ok()
20542 .and_then(|p| std::fs::read_to_string(p).ok())
20543 .map(|s| {
20544 s.split_whitespace()
20545 .filter_map(|t| t.parse().ok())
20546 .collect()
20547 })
20548 .unwrap_or_default()
20549 })
20550 .get(step)
20551 .copied()
20552}
20553
20554pub(crate) fn moe_push_job_parts<'a>(
20558 gate: &'a QTensor,
20559 up: &'a QTensor,
20560 down: &'a QTensor,
20561 x: &[f32],
20562 w: f32,
20563 swiglu_limit: f32,
20564 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20565 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20566) -> Option<()> {
20567 use crate::qtensor::prescale;
20568 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20569 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20570 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20571 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20572 return None; }
20574 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20577 return None;
20578 }
20579 if !gq2 && dq2 {
20580 return None;
20581 }
20582 model_ref.get_or_insert_with(|| gm.clone());
20583 let dt = |cf: &[f32]| {
20584 if cf.is_empty() {
20585 cortiq_core::TensorDtype::Q8Row
20586 } else {
20587 cortiq_core::TensorDtype::Q8_2f
20588 }
20589 };
20590 jobs.push(crate::gpu::MoeJob {
20591 gate: (gi, gr, gc, grs),
20592 up: (ui, ur, uc, urs),
20593 down: (di, dr, dc, drs),
20594 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20595 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20596 down_col: dcf,
20597 w,
20598 q1: gq1,
20599 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20600 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20601 gu_q2: gq2,
20602 swiglu_limit,
20603 act: 0,
20604 });
20605 Some(())
20606}
20607
20608fn moe_push_job<'a>(
20610 d: &'a DenseFfn,
20611 x: &[f32],
20612 w: f32,
20613 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20614 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20615) -> Option<()> {
20616 use crate::qtensor::prescale;
20617 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20622 let act = graph_act.code();
20623 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20624 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20625 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20626 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20627 return None; }
20629 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20630 return None;
20631 }
20632 if !gq2 && dq2 {
20633 return None;
20634 }
20635 model_ref.get_or_insert_with(|| gm.clone());
20636 let gdt = if gcf.is_empty() {
20637 cortiq_core::TensorDtype::Q8Row
20638 } else {
20639 cortiq_core::TensorDtype::Q8_2f
20640 };
20641 let udt = if ucf.is_empty() {
20642 cortiq_core::TensorDtype::Q8Row
20643 } else {
20644 cortiq_core::TensorDtype::Q8_2f
20645 };
20646 jobs.push(crate::gpu::MoeJob {
20647 gate: (gi, gr, gc, grs),
20648 up: (ui, ur, uc, urs),
20649 down: (di, dr, dc, drs),
20650 xs_gate: prescale(x, gcf, gdt).into_owned(),
20651 xs_up: prescale(x, ucf, udt).into_owned(),
20652 down_col: dcf,
20653 w,
20654 q1: gq1,
20655 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20656 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20657 gu_q2: gq2,
20658 swiglu_limit,
20659 act,
20660 });
20661 Some(())
20662}
20663
20664fn sparse_ffn_quant(
20671 d: &DenseFfn,
20672 x: &[f32],
20673 active: &[u16],
20674 hidden: usize,
20675 pool: Option<&Pool>,
20676) -> Vec<f32> {
20677 let n = active.len();
20678 let inter = d.gate_proj.rows();
20679 let mut act = vec![0.0f32; n];
20680 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20683 let compute = |ai: usize| -> f32 {
20684 let idx = active[ai] as usize;
20685 if idx >= inter {
20686 return 0.0; }
20688 let mut s = if need_scratch {
20689 vec![0.0f32; hidden]
20690 } else {
20691 Vec::new()
20692 };
20693 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20694 let up = d.up_proj.row_dot(idx, x, &mut s);
20695 d.act.combine(gate, up)
20696 };
20697 match pool {
20698 Some(p) if n >= 256 => {
20699 let ptr = SendMut(act.as_mut_ptr());
20700 p.run(&|widx, nw| {
20701 let chunk = n.div_ceil(nw);
20702 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20703 for ai in s..e {
20704 unsafe { *ptr.at(ai) = compute(ai) };
20705 }
20706 });
20707 }
20708 _ => {
20709 for (ai, a) in act.iter_mut().enumerate() {
20710 *a = compute(ai);
20711 }
20712 }
20713 }
20714 let mut out = vec![0.0f32; hidden];
20716 for (ai, &idx) in active.iter().enumerate() {
20717 let w = act[ai];
20718 if w.abs() >= 1e-12 && (idx as usize) < inter {
20719 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20720 }
20721 }
20722 out
20723}
20724
20725#[doc(hidden)]
20727pub fn sparse_ffn_quant_for_test(
20728 d: &DenseFfn,
20729 x: &[f32],
20730 active: &[u16],
20731 hidden: usize,
20732) -> Vec<f32> {
20733 sparse_ffn_quant(d, x, active, hidden, None)
20734}
20735
20736fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20740 let deq = |t: &QTensor| -> Vec<f32> {
20741 let (rows, cols) = (t.rows(), t.cols());
20742 let mut out = vec![0.0f32; rows * cols];
20743 for r in 0..rows {
20744 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20745 }
20746 out
20747 };
20748 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20749}
20750
20751struct SendMut(*mut f32);
20753unsafe impl Send for SendMut {}
20754unsafe impl Sync for SendMut {}
20755impl SendMut {
20756 #[inline]
20757 #[allow(clippy::mut_from_ref)]
20760 unsafe fn at(&self, i: usize) -> &mut f32 {
20761 unsafe { &mut *self.0.add(i) }
20762 }
20763}
20764
20765pub(crate) fn moe_route(
20777 logits: &[f32],
20778 m: &MoeFfn,
20779 allowed: Option<&[bool]>,
20780) -> (Vec<usize>, Vec<f32>, f32) {
20781 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20782}
20783
20784pub(crate) fn moe_route_with_eps(
20794 logits: &[f32],
20795 m: &MoeFfn,
20796 allowed: Option<&[bool]>,
20797 sigmoid_denom_eps: f32,
20798) -> (Vec<usize>, Vec<f32>, f32) {
20799 let ne = logits.len();
20800 let admit = |e: usize| {
20806 m.mask.as_ref().is_none_or(|mk| mk[e])
20807 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20808 };
20809 if m.resonance.is_some() && m.top_k == 1 {
20822 let mut best: Option<usize> = None;
20823 for e in (0..ne).filter(|&e| admit(e)) {
20824 let l = logits[e];
20825 if l == f32::NEG_INFINITY || l.is_nan() {
20826 continue;
20827 }
20828 if best.is_none_or(|b| l > logits[b]) {
20829 best = Some(e);
20830 }
20831 }
20832 if let Some(b) = best {
20833 let mut p = vec![0.0f32; ne];
20834 p[b] = 1.0;
20835 return (vec![b], p, 1.0 / m.routed_scaling);
20836 }
20837 }
20838 let p: Vec<f32> = if m.router_sigmoid {
20844 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20845 } else {
20846 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20847 if mx == f32::NEG_INFINITY {
20848 vec![1.0 / ne.max(1) as f32; ne]
20849 } else {
20850 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20851 let s: f32 = e.iter().sum();
20852 for v in &mut e {
20853 *v /= s;
20854 }
20855 e
20856 }
20857 };
20858 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20859 match &m.expert_bias {
20861 Some(b) => idx.sort_unstable_by(|&x, &y| {
20862 (p[y] + b[y])
20863 .partial_cmp(&(p[x] + b[x]))
20864 .unwrap()
20865 .then(x.cmp(&y))
20866 }),
20867 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20868 }
20869 idx.truncate(m.top_k);
20870 if let Some(tau) = m.route_tau {
20874 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20875 if total > 0.0 {
20876 let mut acc = 0.0f32;
20877 let mut keep = idx.len();
20878 for (i, &e) in idx.iter().enumerate() {
20879 acc += p[e];
20880 if acc >= tau * total {
20881 keep = i + 1;
20882 break;
20883 }
20884 }
20885 idx.truncate(keep);
20886 }
20887 }
20888 let wsum: f32 = if m.norm_topk_prob {
20889 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20890 (if m.router_sigmoid {
20894 s + sigmoid_denom_eps
20895 } else {
20896 s
20897 }) / m.routed_scaling
20898 } else {
20899 1.0 / m.routed_scaling
20900 };
20901 (idx, p, wsum)
20902}
20903
20904fn moe_trace(idx: &[usize]) {
20906 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20907}
20908
20909pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20912 use std::io::Write;
20913 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20914 std::sync::OnceLock::new();
20915 let Some(f) = F.get_or_init(|| {
20916 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20917 Some(std::sync::Mutex::new(
20918 std::fs::OpenOptions::new()
20919 .create(true)
20920 .append(true)
20921 .open(p)
20922 .ok()?,
20923 ))
20924 }) else {
20925 return;
20926 };
20927 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20928 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20929}
20930
20931pub(crate) fn moe_ffn(
20934 m: &MoeFfn,
20935 x: &[f32],
20936 pool: Option<&Pool>,
20937 allowed: Option<&[bool]>,
20938) -> Vec<f32> {
20939 let r = moe_ffn_route(m, x, pool, allowed);
20940 moe_ffn_experts(m, x, &r, pool)
20941}
20942
20943pub(crate) struct MoeRoute {
20947 pub idx: Vec<usize>,
20948 pub p: Vec<f32>,
20949 pub wsum: f32,
20950 pub logits: Vec<f32>,
20951}
20952
20953pub(crate) fn moe_ffn_route(
20959 m: &MoeFfn,
20960 x: &[f32],
20961 pool: Option<&Pool>,
20962 allowed: Option<&[bool]>,
20963) -> MoeRoute {
20964 accumulate_act(m, x, 1);
20965 let ne = m.experts.len();
20966 let mut logits = vec![0.0f32; ne];
20967 match &m.resonance {
20968 Some(r) => r.scores(x, &mut logits),
20969 None => m.router.matvec(x, &mut logits, pool),
20970 }
20971 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20972 {
20973 let mut st = m.stats.borrow_mut();
20974 if st.len() < ne {
20975 st.resize(ne, 0);
20976 }
20977 for &e in &idx {
20978 st[e] += 1;
20979 }
20980 }
20981 moe_trace(&idx);
20987 MoeRoute {
20988 idx,
20989 p,
20990 wsum,
20991 logits,
20992 }
20993}
20994
20995#[cfg(target_os = "macos")]
21004fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
21005 const HIDDEN: usize = 2_048;
21006 const ROUTED_INTER: usize = 512;
21007 const SHARED_INTER: usize = 1_024;
21008 const EXPERTS: usize = 256;
21009 const TOP_K: usize = 6;
21010
21011 let Some(routed) = m.experts.first() else {
21012 return false;
21013 };
21014 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
21015 return false;
21016 };
21017 let Some((shared, shared_gate)) = &m.shared else {
21018 return false;
21019 };
21020 if shared_gate.is_some()
21021 || model.header.arch.arch_name != "flint"
21022 || model.header.arch.prism_hadamard.is_some()
21023 || model.header.arch.hidden_size != HIDDEN
21024 || m.experts.len() != EXPERTS
21025 || m.top_k != TOP_K
21026 || !m.router_sigmoid
21027 || !m.norm_topk_prob
21028 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
21029 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
21030 || m.route_tau.is_some()
21031 || m.mask.is_some()
21032 || m.per_expert_scale.is_some()
21033 || m.router_input_norm
21034 || m.resonance.is_some()
21035 || !m.grown.is_empty()
21036 {
21037 return false;
21038 }
21039
21040 let routed_q2 = routed.act == Act::Silu
21041 && routed.segs.is_empty()
21042 && routed.gate_proj.rows() == ROUTED_INTER
21043 && routed.gate_proj.cols() == HIDDEN
21044 && routed.up_proj.rows() == ROUTED_INTER
21045 && routed.up_proj.cols() == HIDDEN
21046 && routed.down_proj.rows() == HIDDEN
21047 && routed.down_proj.cols() == ROUTED_INTER
21048 && routed.up_proj.mapped_q2tp().is_some()
21049 && routed.down_proj.mapped_q4tp().is_some();
21050 let shared_q4 = shared.act == Act::Silu
21051 && shared.segs.is_empty()
21052 && shared.gate_proj.rows() == SHARED_INTER
21053 && shared.gate_proj.cols() == HIDDEN
21054 && shared.up_proj.rows() == SHARED_INTER
21055 && shared.up_proj.cols() == HIDDEN
21056 && shared.down_proj.rows() == HIDDEN
21057 && shared.down_proj.cols() == SHARED_INTER
21058 && shared.gate_proj.mapped_q4tp().is_some()
21059 && shared.up_proj.mapped_q4tp().is_some()
21060 && shared.down_proj.mapped_q4tp().is_some();
21061 routed_q2 && shared_q4
21062}
21063
21064pub(crate) fn moe_ffn_experts(
21067 m: &MoeFfn,
21068 x: &[f32],
21069 r: &MoeRoute,
21070 pool: Option<&Pool>,
21071) -> Vec<f32> {
21072 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
21073 if crate::gpu::enabled_here() {
21078 #[cfg(target_os = "macos")]
21079 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
21080 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
21086 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
21087 });
21088 }
21089 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
21090 crate::gpu::ProbeArm::Gpu => {
21091 let t0 = std::time::Instant::now();
21092 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
21093 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
21094 return out;
21095 }
21096 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
21097 }
21098 crate::gpu::ProbeArm::CpuTimed => {
21099 let t0 = std::time::Instant::now();
21100 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21101 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
21102 return out;
21103 }
21104 crate::gpu::ProbeArm::Cpu => {
21105 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21106 }
21107 }
21108 }
21109 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
21110}
21111
21112fn moe_ffn_minicpm_tail(
21116 slot: &mut crate::minicpm_tail::MiniCpmTail,
21117 li: usize,
21118 total_layers: usize,
21119 m: &MoeFfn,
21120 x: &[f32],
21121 pool: Option<&Pool>,
21122) -> Vec<f32> {
21123 let route = moe_ffn_route(m, x, pool, None);
21124 match slot.forward(li, m, x, &route, pool, total_layers) {
21125 Some(out) => out,
21126 None => moe_ffn_experts(m, x, &route, pool),
21131 }
21132}
21133
21134fn moe_ffn_banked(
21138 slot: &mut crate::mimo_moe::Slot,
21139 li: usize,
21140 m: &MoeFfn,
21141 x: &[f32],
21142 pool: Option<&Pool>,
21143) -> Vec<f32> {
21144 let t0 = std::time::Instant::now();
21145 let r = moe_ffn_route(m, x, pool, None);
21146 slot.note_route(t0.elapsed().as_nanos() as u64);
21147 match slot.forward(li, m, x, &r, pool) {
21148 Some(out) => out,
21149 None => crate::qtensor::float_activations_scope(|| {
21150 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21151 }),
21152 }
21153}
21154
21155fn moe_ffn_banked_rows(
21157 slot: &mut crate::mimo_moe::Slot,
21158 li: usize,
21159 m: &MoeFfn,
21160 xs: &[f32],
21161 b: usize,
21162 hidden: usize,
21163 pool: Option<&Pool>,
21164) -> Vec<f32> {
21165 let t0 = std::time::Instant::now();
21166 let routes: Vec<_> = xs
21167 .chunks_exact(hidden)
21168 .map(|x| moe_ffn_route(m, x, pool, None))
21169 .collect();
21170 slot.note_route(t0.elapsed().as_nanos() as u64);
21171 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21172 return out;
21173 }
21174 let mut out = Vec::with_capacity(b * hidden);
21175 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21176 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21177 crate::qtensor::float_activations_scope(|| {
21179 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21180 })
21181 });
21182 out.extend(row);
21183 }
21184 out
21185}
21186
21187fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21192 use std::sync::atomic::{AtomicBool, Ordering};
21193 if built {
21194 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
21195 if total_layers > 0 && layers_run < total_layers {
21196 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
21197 } else {
21198 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
21199 }
21200 } else {
21201 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
21202 }
21203 static SAID: AtomicBool = AtomicBool::new(false);
21204 if !SAID.swap(true, Ordering::Relaxed) {
21205 if built {
21206 tracing::info!("wgpu whole-token graph: ACTIVE");
21207 } else {
21208 tracing::warn!("wgpu whole-token graph refused — per-op path");
21209 }
21210 }
21211}
21212
21213pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21217pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21218pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21222pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21224
21225pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21229pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21230pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
21231 std::sync::atomic::AtomicU64::new(0);
21232pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21233pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21234pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
21238 std::sync::atomic::AtomicU64::new(0);
21239pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21240pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21241 std::sync::atomic::AtomicU64::new(0);
21242pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21243 std::sync::atomic::AtomicU64::new(0);
21244
21245fn moe_batch_enabled() -> bool {
21248 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21249 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21250}
21251
21252fn moe_ffn_cpu_batched(
21258 m: &MoeFfn,
21259 x: &[f32],
21260 idx: &[usize],
21261 p: &[f32],
21262 wsum: f32,
21263 pool: Option<&Pool>,
21264) -> Option<Vec<f32>> {
21265 if idx.is_empty() || !moe_batch_enabled() {
21266 return None;
21267 }
21268 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21272 return None;
21273 }
21274 let n = idx.len() + usize::from(m.shared.is_some());
21275 let mut pairs = Vec::with_capacity(n);
21276 let mut downs = Vec::with_capacity(n);
21277 let mut ws = Vec::with_capacity(n);
21278 for &e in idx {
21279 let d = &m.experts[e];
21280 if d.act != Act::Silu {
21281 return None;
21282 }
21283 pairs.push((&d.gate_proj, &d.up_proj));
21284 downs.push(&d.down_proj);
21285 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21286 }
21287 if let Some((se, gate)) = &m.shared {
21290 if se.act != Act::Silu {
21291 return None;
21292 }
21293 let g = gate.as_ref().map_or(1.0, |gate| {
21294 let mut gl = [0.0f32; 1];
21295 gate.matvec(x, &mut gl, pool);
21296 1.0 / (1.0 + (-gl[0]).exp())
21297 });
21298 pairs.push((&se.gate_proj, &se.up_proj));
21299 downs.push(&se.down_proj);
21300 ws.push(g);
21301 }
21302 let inter = pairs[0].0.rows();
21303 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21304 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21305 return None;
21306 }
21307 let mut out = attention::take_buf(x.len());
21308 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21309 attention::recycle_buf(&mut out);
21310 return None;
21311 }
21312 Some(out)
21313}
21314
21315pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21323pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21324pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21325pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21326pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21327
21328pub(crate) fn moe_cold_experts_cpu(
21329 experts: &[(&DenseFfn, f32)],
21330 x: &[f32],
21331 pool: Option<&Pool>,
21332) -> Vec<f32> {
21333 let mut out = attention::take_buf(x.len());
21334 if experts.is_empty() {
21335 return out;
21336 }
21337 let pairs: Vec<_> = experts
21338 .iter()
21339 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21340 .collect();
21341 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21342 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21343 let inter = experts[0].0.gate_proj.rows();
21344 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21345 let silu_only = experts
21353 .iter()
21354 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21355 if silu_only
21356 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21357 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21358 {
21359 return out;
21360 }
21361 out.fill(0.0);
21362 for &(expert, weight) in experts {
21363 let mut one = dense_ffn(expert, x, pool);
21364 for (o, v) in out.iter_mut().zip(&one) {
21365 *o += weight * v;
21366 }
21367 attention::recycle_buf(&mut one);
21368 }
21369 out
21370}
21371
21372pub(crate) fn moe_cold_experts_rows_cpu(
21376 jobs: &[Vec<(&DenseFfn, f32)>],
21377 xs: &[f32],
21378 hidden: usize,
21379 pool: Option<&Pool>,
21380) -> Vec<f32> {
21381 let mut out = vec![0.0; xs.len()];
21382 let mut experts: Vec<&DenseFfn> = Vec::new();
21383 let mut groups: Vec<Vec<usize>> = Vec::new();
21384 let mut terms = vec![Vec::new(); jobs.len()];
21385 for (r, row) in jobs.iter().enumerate() {
21386 for &(e, w) in row {
21387 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21388 Some(g) => g,
21389 None => {
21390 experts.push(e);
21391 groups.push(Vec::new());
21392 groups.len() - 1
21393 }
21394 };
21395 terms[r].push((g, groups[g].len(), w));
21396 groups[g].push(r);
21397 }
21398 }
21399 if experts.is_empty() {
21400 return out;
21401 }
21402 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21403 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21404 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21405 let count: usize = lens.iter().sum();
21406 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21407 let mut ds = vec![vec![0.0; hidden]; count];
21408 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21409 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21410 {
21411 let mut offset = 0;
21412 let offsets: Vec<_> = lens
21413 .iter()
21414 .map(|&n| {
21415 let start = offset;
21416 offset += n;
21417 start
21418 })
21419 .collect();
21420 for (r, terms) in terms.iter().enumerate() {
21421 for &(g, slot, w) in terms {
21422 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21423 .iter_mut()
21424 .zip(&ds[offsets[g] + slot])
21425 {
21426 *o += w * v;
21427 }
21428 }
21429 }
21430 } else {
21431 for (r, jobs) in jobs.iter().enumerate() {
21432 if !jobs.is_empty() {
21433 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21434 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21435 attention::recycle_buf(&mut row);
21436 }
21437 }
21438 }
21439 out
21440}
21441
21442fn moe_ffn_cpu(
21444 m: &MoeFfn,
21445 x: &[f32],
21446 idx: &[usize],
21447 p: &[f32],
21448 wsum: f32,
21449 pool: Option<&Pool>,
21450) -> Vec<f32> {
21451 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21452 return out;
21453 }
21454 let mut out = attention::take_buf(x.len());
21455 for &e in idx {
21456 let mut eo = dense_ffn(&m.experts[e], x, pool);
21457 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21458 for i in 0..out.len() {
21459 out[i] += w * eo[i];
21460 }
21461 attention::recycle_buf(&mut eo);
21462 }
21463 if let Some((se, gate)) = &m.shared {
21464 let mut so = dense_ffn(se, x, pool);
21465 let g = gate.as_ref().map_or(1.0, |gate| {
21466 let mut gl = [0.0f32; 1];
21467 gate.matvec(x, &mut gl, pool);
21468 1.0 / (1.0 + (-gl[0]).exp())
21469 });
21470 for i in 0..out.len() {
21471 out[i] += g * so[i];
21472 }
21473 attention::recycle_buf(&mut so);
21474 }
21475 out
21476}
21477
21478#[allow(clippy::too_many_arguments)]
21486pub(crate) fn mla_attention(
21487 w: &MlaWeights,
21488 normed: &[f32],
21489 cache: &mut crate::kv_cache::LayerKvCache,
21490 position: usize,
21491 inv_freq: &[f32],
21492 rope_scale: f32,
21493 eps: f64,
21494 pool: Option<&Pool>,
21495) -> Vec<f32> {
21496 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21497 let hd = dr + dn;
21498 let mut q = vec![0.0f32; nh * hd];
21499 match (&w.q_a, &w.q_a_norm) {
21500 (Some(qa), Some(qn)) => {
21501 let mut t = vec![0.0f32; qa.rows()];
21502 qa.matvec(normed, &mut t, pool);
21503 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21504 w.q_proj.matvec(&tn, &mut q, pool);
21505 }
21506 _ => w.q_proj.matvec(normed, &mut q, pool),
21507 }
21508 let mut ca = vec![0.0f32; lora + dr];
21509 w.kv_a.matvec(normed, &mut ca, pool);
21510 let (c_lat, k_rope) = ca.split_at_mut(lora);
21511 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21512 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21513 w.kv_b.matvec(&latn, &mut kvb, pool);
21514 if !w.nope {
21515 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21516 }
21517 for h in 0..nh {
21518 if !w.nope {
21519 attention::rope_rotate_scaled(
21520 &mut q[h * hd..h * hd + dr],
21521 position,
21522 inv_freq,
21523 rope_scale,
21524 );
21525 }
21526 }
21527 let mut k = vec![0.0f32; nh * hd];
21528 let mut v = vec![0.0f32; nh * hd];
21529 for h in 0..nh {
21530 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21531 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21532 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21533 }
21534 cache.append(&k, &v, &vec![true; nh]);
21535 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21536 attention::recycle_buf(&mut imp);
21537 let mut ov = vec![0.0f32; nh * dv];
21538 for h in 0..nh {
21539 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21540 }
21541 let mut out = vec![0.0f32; w.o_proj.rows()];
21542 w.o_proj.matvec(&ov, &mut out, pool);
21543 out
21544}
21545
21546fn dense_moe_ffn(
21553 dm: &DenseMoeFfn,
21554 x_normed: &[f32],
21555 h_raw: &[f32],
21556 eps: f64,
21557 norm_style: NormStyle,
21558 pool: Option<&Pool>,
21559) -> Vec<f32> {
21560 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21561 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21562 let m = &dm.moe;
21563 let ne = m.experts.len();
21564 let mut logits = vec![0.0f32; ne];
21565 if m.router_input_norm {
21566 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21567 let inv = 1.0 / (ss + eps as f32).sqrt();
21568 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21569 m.router.matvec(&xr, &mut logits, pool);
21570 } else {
21571 m.router.matvec(h_raw, &mut logits, pool);
21572 }
21573 let (idx, p, wsum) = moe_route(&logits, m, None);
21574 {
21575 let mut st = m.stats.borrow_mut();
21576 if st.len() < ne {
21577 st.resize(ne, 0);
21578 }
21579 for &e in &idx {
21580 st[e] += 1;
21581 }
21582 }
21583 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21584 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21585 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21586 for (di, mi) in d.iter_mut().zip(&mo) {
21587 *di += mi;
21588 }
21589 d
21590}
21591
21592fn moe_gpu_refused(why: &'static str) {
21599 use std::sync::atomic::{AtomicBool, Ordering};
21600 static SAID: AtomicBool = AtomicBool::new(false);
21601 if !SAID.swap(true, Ordering::Relaxed) {
21602 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21603 }
21604}
21605
21606fn moe_ffn_gpu(
21607 m: &MoeFfn,
21608 x: &[f32],
21609 idx: &[usize],
21610 p: &[f32],
21611 wsum: f32,
21612 pool: Option<&Pool>,
21613) -> Option<Vec<f32>> {
21614 use crate::gpu::MoeJob;
21615
21616 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21617 let mut model_ref = None;
21618 for &e in idx {
21619 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21620 moe_gpu_refused("push_job(expert)");
21621 return None;
21622 }
21623 }
21624 if let Some((se, gate)) = &m.shared {
21625 let g = gate.as_ref().map_or(1.0, |gate| {
21626 let mut gl = [0.0f32; 1];
21627 gate.matvec(x, &mut gl, pool);
21628 1.0 / (1.0 + (-gl[0]).exp())
21629 });
21630 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21631 moe_gpu_refused("push_job(shared)");
21632 return None;
21633 }
21634 }
21635 let Some(model) = model_ref else {
21636 moe_gpu_refused("no model_ref");
21637 return None;
21638 };
21639 let hidden = jobs[0].down.1;
21640 let mut out = vec![0.0f32; hidden];
21641 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21642 Some(out)
21643 } else {
21644 moe_gpu_refused("gpu::moe_block");
21645 None
21646 }
21647}
21648
21649fn ffn_forward(
21651 ffn: &FfnKind,
21652 x: &[f32],
21653 pool: Option<&Pool>,
21654 experts_allowed: Option<&[bool]>,
21655) -> Vec<f32> {
21656 match ffn {
21657 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21658 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21659 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21660 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21664 }
21665}
21666
21667fn ffn_forward_pair(
21671 ffn: &FfnKind,
21672 x1: &[f32],
21673 x2: &[f32],
21674 pool: Option<&Pool>,
21675 experts_allowed: Option<&[bool]>,
21676) -> (Vec<f32>, Vec<f32>) {
21677 let d = match ffn {
21678 FfnKind::Dense(d) if !d.segs.is_empty() => {
21681 return (
21682 tube_ffn(d, x1, 1, pool, None),
21683 tube_ffn(d, x2, 1, pool, None),
21684 );
21685 }
21686 FfnKind::Dense(d) => d,
21687 FfnKind::Moe(m) => {
21688 return (
21689 moe_ffn(m, x1, pool, experts_allowed),
21690 moe_ffn(m, x2, pool, experts_allowed),
21691 );
21692 }
21693 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21694 };
21695 let inter = d.gate_proj.rows();
21696 FFN_SCRATCH.with(|s| {
21697 let mut s = s.borrow_mut();
21698 let [g1, g2, u1, u2] = &mut *s;
21699 g1.resize(inter, 0.0);
21700 g2.resize(inter, 0.0);
21701 u1.resize(inter, 0.0);
21702 u2.resize(inter, 0.0);
21703 QTensor::matvec2_many(
21706 [&d.gate_proj, &d.up_proj],
21707 x1,
21708 x2,
21709 [g1.as_mut_slice(), u1.as_mut_slice()],
21710 [g2.as_mut_slice(), u2.as_mut_slice()],
21711 pool,
21712 );
21713 for i in 0..inter {
21714 g1[i] = d.act.combine(g1[i], u1[i]);
21715 g2[i] = d.act.combine(g2[i], u2[i]);
21716 }
21717 let mut o1 = attention::take_buf(d.down_proj.rows());
21718 let mut o2 = attention::take_buf(d.down_proj.rows());
21719 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21720 (o1, o2)
21721 })
21722}
21723
21724#[cfg(test)]
21725mod tests {
21726 use std::ffi::OsString;
21727 use std::sync::Mutex;
21728
21729 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21732
21733 struct ScopedTestEnv {
21734 previous: Vec<(&'static str, Option<OsString>)>,
21735 }
21736
21737 impl ScopedTestEnv {
21738 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21739 let previous = vars
21740 .iter()
21741 .map(|(name, _)| (*name, std::env::var_os(name)))
21742 .collect();
21743 for &(name, value) in vars {
21744 unsafe {
21745 match value {
21746 Some(value) => std::env::set_var(name, value),
21747 None => std::env::remove_var(name),
21748 }
21749 }
21750 }
21751 Self { previous }
21752 }
21753 }
21754
21755 impl Drop for ScopedTestEnv {
21756 fn drop(&mut self) {
21757 for (name, value) in self.previous.drain(..).rev() {
21758 unsafe {
21759 match value {
21760 Some(value) => std::env::set_var(name, value),
21761 None => std::env::remove_var(name),
21762 }
21763 }
21764 }
21765 }
21766 }
21767
21768 #[test]
21769 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21770 use super::pad_mtp_draft_logits;
21771
21772 let mut logits = vec![-3.0, -1.0, -2.0];
21773 pad_mtp_draft_logits(&mut logits, 6);
21774 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21775 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21776 assert_eq!(
21777 logits
21778 .iter()
21779 .enumerate()
21780 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21781 .map(|(i, _)| i),
21782 Some(1),
21783 "an unevaluated row cannot win an all-negative shortlist"
21784 );
21785 }
21786
21787 #[test]
21788 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21789 use super::{Pipeline, SpecMon, SpecTrial};
21790
21791 let mut mon = SpecMon::default();
21792 let start = std::time::Instant::now();
21793 mon.round(1.0, 1);
21794 let first = Pipeline::spec_trial_round(
21795 SpecTrial::Spec {
21796 t0: start,
21797 gen0: 0,
21798 rounds: 0,
21799 },
21800 &mut mon,
21801 1,
21802 );
21803 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21804
21805 mon.round(1.0, 1);
21806 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21807 assert!(
21808 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21809 "two verifier-tip-only rounds must stop the losing MTP trial"
21810 );
21811
21812 let mut viable = SpecMon::default();
21815 viable.round(1.0, 1);
21816 viable.round(1.0, 2);
21817 let continued = Pipeline::spec_trial_round(
21818 SpecTrial::Spec {
21819 t0: start,
21820 gen0: 0,
21821 rounds: 1,
21822 },
21823 &mut viable,
21824 3,
21825 );
21826 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21827 }
21828
21829 #[test]
21830 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21831 let mut p = spark_test_pipeline(None);
21832 let head_rows = 248_070;
21833 p.draft_full_streak = 7;
21834 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21835
21836 p.reset_fresh_mtp_draft_state();
21837 assert_eq!(
21838 p.draft_head_rows(head_rows),
21839 super::Pipeline::draft_vocab_rows(head_rows),
21840 "a fresh request returns to its configured shortlist"
21841 );
21842
21843 p.note_draft_id(u32::MAX);
21846 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21847 }
21848
21849 #[test]
21850 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21851 use super::graph_spec_wanted_by_policy;
21852
21853 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21857 assert!(!graph_spec_wanted_by_policy(
21858 Some("anything-but-1"),
21859 true,
21860 false,
21861 false,
21862 true,
21863 ));
21864 assert!(graph_spec_wanted_by_policy(
21865 Some("1"),
21866 false,
21867 true,
21868 false,
21869 true,
21870 ));
21871 assert!(!graph_spec_wanted_by_policy(
21872 Some("0"),
21873 true,
21874 false,
21875 false,
21876 true,
21877 ));
21878
21879 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21882 assert!(graph_spec_wanted_by_policy(
21883 Some("legacy-force"),
21884 false,
21885 true,
21886 true,
21887 false,
21888 ));
21889 }
21890
21891 #[test]
21892 fn encoder_hidden_states_are_normalized_and_reusable() {
21893 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21894 let ids = [1, 7, 11, 19];
21895
21896 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21897 let second = p
21898 .encode_hidden_states(&ids)
21899 .expect("reusable encoder forward");
21900 assert_eq!(first.len(), ids.len() * 8);
21901 assert!(first.iter().all(|v| v.is_finite()));
21902 assert_eq!(
21903 first, second,
21904 "encoder forward must start from a fresh state"
21905 );
21906
21907 assert!(p.forward_ids(&ids, None).is_ok());
21910 }
21911
21912 #[test]
21917 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21918 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21919 let dense_card = ChunkStackFacts {
21920 plain_dense: true,
21921 discrete: true,
21922 gpu_on: true,
21923 ..Default::default()
21924 };
21925 assert!(dense_card.dense_on_discrete());
21926 assert_eq!(
21928 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21929 DISCRETE_DENSE_PREFILL_CHUNK
21930 );
21931 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21932 for (label, facts) in [
21933 (
21934 "GDN hybrid / MoE / DeepSeek stack",
21935 ChunkStackFacts {
21936 plain_dense: false,
21937 ..dense_card
21938 },
21939 ),
21940 (
21941 "integrated GPU",
21942 ChunkStackFacts {
21943 discrete: false,
21944 ..dense_card
21945 },
21946 ),
21947 (
21948 "CPU only",
21949 ChunkStackFacts {
21950 gpu_on: false,
21951 discrete: false,
21952 ..dense_card
21953 },
21954 ),
21955 (
21956 "capacity split",
21957 ChunkStackFacts {
21958 capacity_split: true,
21959 ..dense_card
21960 },
21961 ),
21962 (
21963 "multi-GPU plan",
21964 ChunkStackFacts {
21965 multi_gpu: true,
21966 ..dense_card
21967 },
21968 ),
21969 (
21970 "O(1) layers",
21971 ChunkStackFacts {
21972 o1: true,
21973 ..dense_card
21974 },
21975 ),
21976 ] {
21977 assert!(!facts.dense_on_discrete(), "{label}");
21978 assert_eq!(
21979 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21980 48,
21981 "{label} keeps the historical x86 chunk"
21982 );
21983 }
21984 for dense in [false, true] {
21986 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21987 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21988 }
21989 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21991 for dense in [false, true] {
21992 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21993 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21994 }
21995 }
21996 }
21997
21998 #[test]
21999 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
22000 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
22001 let full = |host_rows, device_rows| ReuseLayer {
22002 full: true,
22003 host_rows,
22004 device_rows,
22005 device_state: false,
22006 };
22007 assert_eq!(
22010 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
22011 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
22012 );
22013 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
22015 assert_eq!(
22017 kv_reuse_plan(339, &[full(339, Some(345))]),
22018 ReusePlan::Ready
22019 );
22020 assert_eq!(
22022 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
22023 ReusePlan::Pull(vec![(0, 300, 339)])
22024 );
22025 assert_eq!(
22027 kv_reuse_plan(339, &[full(300, Some(320))]),
22028 ReusePlan::Fresh
22029 );
22030 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
22031 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
22032 let conv = |device_state| ReuseLayer {
22035 full: false,
22036 host_rows: 0,
22037 device_rows: None,
22038 device_state,
22039 };
22040 assert_eq!(
22041 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
22042 ReusePlan::Fresh
22043 );
22044 assert_eq!(
22045 kv_reuse_plan(339, &[conv(false), full(339, None)]),
22046 ReusePlan::Ready
22047 );
22048 }
22049
22050 #[test]
22051 fn nll_graph_policy_scopes_only_the_fused_head() {
22052 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
22053 ("vulkan graph", true, true, false, true, false),
22055 ("native Metal graph", true, true, true, true, true),
22057 ("masked", false, true, false, false, false),
22059 ("graph disabled", true, false, true, false, false),
22060 ] {
22061 let (graph_quality, graph_head_required) =
22062 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
22063 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
22064 assert_eq!(graph_head_required, want_head, "{label}: fused head");
22065 }
22066 }
22067
22068 #[test]
22069 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
22070 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
22071 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
22072 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
22073 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
22074 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
22075 }
22076
22077 #[test]
22078 fn cancel_flag_stops_generation() {
22079 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
22080 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22083 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
22084 assert_eq!(r.finish_reason, "cancelled");
22085 assert!(
22086 r.token_ids.is_empty(),
22087 "no tokens after cancel: {:?}",
22088 r.token_ids
22089 );
22090 assert_eq!(p.kv_cache.seq_len(), 0);
22091 assert!(p.kv_history.is_empty());
22092 assert!(!p.graph_want_logits);
22093 assert!(p.graph_logits.is_none());
22094 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
22096 assert_ne!(r2.finish_reason, "cancelled");
22097 }
22098 use super::*;
22099
22100 #[test]
22101 fn minimax_m3_arch_selects_checked_oai_activation_only() {
22102 let arch = |arch_name: &str, hidden_act: &str| {
22106 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
22107 "arch_name": arch_name,
22108 "hidden_size": 8,
22109 "intermediate_size": 16,
22110 "num_layers": 1,
22111 "num_attention_heads": 1,
22112 "num_kv_heads": 1,
22113 "head_dim": 8,
22114 "vocab_size": 16,
22115 "layer_types": ["FullAttention"],
22116 "rms_norm_eps": 1e-6,
22117 "max_position_embeddings": 16,
22118 "hidden_act": hidden_act
22119 }))
22120 .expect("minimal architecture")
22121 };
22122 let minimax = arch("minimax_m3_vl", "swigluoai");
22123 assert_eq!(
22124 Act::from_arch_full(&minimax),
22125 Act::SwiGluOai {
22126 alpha: 1.702,
22127 limit: 7.0,
22128 }
22129 );
22130
22131 let unrelated = arch("qwen2", "swigluoai");
22134 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
22135
22136 let non_oai = arch("minimax_m3_vl", "gelu");
22140 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
22141 }
22142
22143 #[test]
22144 fn minimax_swiglu_oai_act_matches_scalar_contract() {
22145 let act = Act::swiglu_oai(1.702, 7.0);
22146 let got = act.combine(10.0, -10.0);
22147 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22148 assert!((got - expected).abs() < 1e-6);
22149 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22150 assert_eq!(
22151 act.wgpu_graph_act(),
22152 Some(crate::gpu::GraphAct::SwiGluOaiM3),
22153 "the WGPU-only arm accepts only the frozen M3 constants"
22154 );
22155 assert_eq!(
22156 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22157 None,
22158 "nearby OAI constants must not borrow MiniMax's kernel"
22159 );
22160 assert_eq!(
22161 act.moe_job_graph_contract(),
22162 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22163 "the direct MoE path carries the frozen clamp with the marker"
22164 );
22165 assert_eq!(
22166 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22167 None,
22168 "a nearby clamp must not borrow MiniMax's device contract"
22169 );
22170
22171 let gate_only = act.apply(-10.0);
22174 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22175 assert!((gate_only - expected_gate).abs() < 1e-10);
22176 }
22177
22178 #[test]
22179 fn explicit_sliding_schedule_selects_local_rope_profile() {
22180 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22185 p.swa = Some((1024, usize::MAX));
22186 p.sliding_layers = Some(vec![true, true, true, false]);
22187 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22188 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22189 p.rope_scale = 1.277_258_9;
22190 p.rope_scale_local = 1.0;
22191
22192 for li in 0..3 {
22193 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22194 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
22195 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
22196 }
22197 assert_eq!(p.layer_window(3), None);
22198 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
22199 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
22200 }
22201
22202 #[test]
22210 fn dynamic_ffn_equals_the_zeroing_arm() {
22211 let (hidden, inter) = (8usize, 32usize);
22212 let synth = |n: usize, salt: usize| -> Vec<f32> {
22213 (0..n)
22214 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
22215 .collect()
22216 };
22217 let down = synth(hidden * inter, 3);
22218 let mut down_t = vec![0.0f32; inter * hidden];
22219 for r in 0..hidden {
22220 for c in 0..inter {
22221 down_t[c * hidden + r] = down[r * inter + c];
22222 }
22223 }
22224 let d = DenseFfn {
22225 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22226 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22227 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
22228 act: Act::Silu,
22229 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
22230 segs: Vec::new(),
22231 };
22232 let x = synth(hidden, 11);
22233 let k = 12usize;
22234 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
22235 let mut g = vec![0.0f32; inter];
22237 d.gate_proj.matvec(&x, &mut g, None);
22238 let mut u = vec![0.0f32; inter];
22239 d.up_proj.matvec(&x, &mut u, None);
22240 for v in g.iter_mut() {
22241 *v = inference::silu(*v);
22242 }
22243 keep_top_k(&mut g, k);
22244 for i in 0..inter {
22245 g[i] *= u[i];
22246 }
22247 let mut want = vec![0.0f32; hidden];
22248 d.down_proj.matvec(&g, &mut want, None);
22249 for (a, b) in want.iter().zip(&got) {
22250 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22251 }
22252 }
22253
22254 #[test]
22260 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22261 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22262 let inter = core + tube;
22263 let synth = |n: usize, salt: usize| -> Vec<f32> {
22264 (0..n)
22265 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22266 .collect()
22267 };
22268 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22269 let d_all = synth(hidden * inter, 3);
22270 let dense = DenseFfn {
22272 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22273 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22274 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22275 act: Act::Silu,
22276 down_t: None,
22277 segs: Vec::new(),
22278 };
22279 let rows =
22280 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22281 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22282 let mut o = Vec::with_capacity(hidden * (b - a));
22283 for r in 0..hidden {
22284 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22285 }
22286 o
22287 };
22288 let tubed = DenseFfn {
22289 down_t: None,
22290 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22291 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22292 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22293 act: Act::Silu,
22294 segs: vec![FfnSeg {
22295 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22296 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22297 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22298 start: core,
22299 width: tube,
22300 }],
22301 };
22302 let x = synth(hidden, 7);
22303 let want = dense_ffn(&dense, &x, None);
22304 let got = tube_ffn(&tubed, &x, 1, None, None);
22305 for (a, b) in want.iter().zip(&got) {
22306 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22307 }
22308 let mut bits = vec![0u8; inter.div_ceil(8)];
22310 for n in 0..core {
22311 bits[n / 8] |= 1 << (n % 8);
22312 }
22313 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22314 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22315 for (a, b) in masked.iter().zip(&closed) {
22316 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22317 }
22318 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22320 for (a, b) in closed.iter().zip(&batch) {
22321 assert_eq!(a, b, "batch arm disagrees with decode arm");
22322 }
22323 }
22324
22325 #[test]
22327 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22328 let (hidden, inter) = (16usize, 40usize);
22329 let synth = |n: usize, salt: usize| -> Vec<f32> {
22330 (0..n)
22331 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22332 .collect()
22333 };
22334 let d = DenseFfn {
22335 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22336 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22337 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22338 act: Act::Silu,
22339 down_t: None,
22340 segs: Vec::new(),
22341 };
22342 let x = synth(hidden, 9);
22343 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22345
22346 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22347
22348 let mut g = vec![0.0f32; inter];
22350 d.gate_proj.matvec(&x, &mut g, None);
22351 let mut u = vec![0.0f32; inter];
22352 d.up_proj.matvec(&x, &mut u, None);
22353 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22354 for i in 0..inter {
22355 g[i] = if act_set.contains(&(i as u16)) {
22356 inference::silu(g[i]) * u[i]
22357 } else {
22358 0.0
22359 };
22360 }
22361 let mut reference = vec![0.0f32; hidden];
22362 d.down_proj.matvec(&g, &mut reference, None);
22363
22364 let max_d = sparse
22365 .iter()
22366 .zip(&reference)
22367 .map(|(a, b)| (a - b).abs())
22368 .fold(0.0f32, f32::max);
22369 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22370 }
22371
22372 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22375 let (h, inter, heads, kv, hd) = (
22376 p.hidden_size,
22377 inter,
22378 p.num_heads,
22379 p.num_kv_heads,
22380 p.head_dim,
22381 );
22382 let synth = |n: usize, salt: usize| -> Vec<f32> {
22383 (0..n)
22384 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22385 .collect()
22386 };
22387 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22388 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22389 };
22390 p.mtp = Some(MtpModule {
22391 enorm: vec![1.0; h],
22392 hnorm: vec![1.0; h],
22393 eh_proj: qt(h, 2 * h, 301),
22394 layer: LayerWeights {
22395 input_norm: vec![1.0; h],
22396 post_norm: vec![1.0; h],
22397 attn_out_norm: None,
22398 ffn_out_norm: None,
22399 layer_scale: None,
22400 ffn: FfnKind::Dense(DenseFfn {
22401 gate_proj: qt(inter, h, 315),
22402 up_proj: qt(inter, h, 316),
22403 down_proj: qt(h, inter, 317),
22404 act: Act::Silu,
22405 down_t: None,
22406 segs: Vec::new(),
22407 }),
22408 attn: AttnKind::Full {
22409 bias: None,
22410 wq: qt(heads * hd, h, 311),
22411 wk: qt(kv * hd, h, 312),
22412 wv: qt(kv * hd, h, 313),
22413 wo: qt(h, heads * hd, 314),
22414 q_norm: None,
22415 k_norm: None,
22416 output_gate: false,
22417 softplus_gate: None,
22418 },
22419 },
22420 final_norm: vec![1.0; h],
22421 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22422 });
22423 }
22424
22425 fn attach_test_mtp(p: &mut Pipeline) {
22427 attach_test_mtp_width(p, p.intermediate_size);
22428 }
22429
22430 #[test]
22431 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22432 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22433 attach_test_mtp_width(&mut p, 11);
22436 let ids = [3, 9, 4, 12, 7, 2];
22437 let trunk = p.probe_ffn_mass(&ids);
22438 assert_eq!(trunk[0].len(), p.intermediate_size);
22439
22440 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22441 assert_eq!(got.len(), 1);
22442 assert_eq!(got[0].mass.len(), 11);
22443 assert_eq!(got[0].squared_mass.len(), 11);
22444 assert_eq!(got[0].observed_positions, ids.len() - 1);
22445 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22446 assert!(got[0]
22447 .mass
22448 .iter()
22449 .zip(&got[0].squared_mass)
22450 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22451 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22452 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22453
22454 let h = p.hidden_size;
22458 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22459 panic!("synthetic MTP must be dense")
22460 };
22461 dense.segs.push(FfnSeg {
22462 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22463 up: QTensor::from_f32(vec![0.0; h], 1, h),
22464 down: QTensor::from_f32(vec![0.0; h], h, 1),
22465 start: dense.gate_proj.rows(),
22466 width: 1,
22467 });
22468 let err = p.probe_mtp_ffn(&ids);
22469 assert!(err
22470 .unwrap_err()
22471 .contains("tube-segmented draft FFNs are not supported"));
22472 assert_eq!(p.kv_cache.seq_len(), 0);
22473 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22474 }
22475
22476 #[test]
22477 fn speculative_equals_vanilla_greedy() {
22478 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22479 let _env = ScopedTestEnv::set(&[
22482 ("CMF_GPU", Some("0")),
22483 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22484 ("CMF_MTP_PAIR", Some("1")),
22485 ]);
22486 let run = |spec: bool| {
22487 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22488 p.sampler_config.temperature = 0.0;
22489 attach_test_mtp(&mut p);
22490 p.speculative = spec;
22491 let r = p.generate("abcdef", 12, None, None).unwrap();
22492 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22493 };
22494 let (vanilla, d0, _) = run(false);
22495 let (spec, d1, a1) = run(true);
22496 assert_eq!(d0, 0, "vanilla path must not draft");
22497 assert!(d1 > 0, "speculative path must draft");
22498 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22499 assert_eq!(
22500 vanilla, spec,
22501 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22502 );
22503 }
22504
22505 #[test]
22506 fn speculative_accepts_constant_oracle() {
22507 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22508 let _env = ScopedTestEnv::set(&[
22509 ("CMF_GPU", Some("0")),
22510 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22511 ("CMF_MTP_PAIR", Some("1")),
22512 ]);
22513 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22514 p.sampler_config.temperature = 0.0;
22515 p.sampler_config.repetition_penalty = 1.0;
22516 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22519 attach_test_mtp(&mut p);
22520 p.speculative = true;
22521 let r = p.generate("abcd", 10, None, None).unwrap();
22522 assert!(r.mtp_drafted > 0);
22523 assert_eq!(
22524 r.mtp_accepted, r.mtp_drafted,
22525 "constant logits → every draft accepted"
22526 );
22527 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22530 }
22531
22532 #[test]
22533 fn host_fused_mtp_pair_is_opt_in() {
22534 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22535 let _env = ScopedTestEnv::set(&[
22536 ("CMF_GPU", Some("0")),
22537 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22538 ("CMF_MTP_PAIR", None),
22539 ]);
22540 let run = |spec: bool| {
22541 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22542 p.sampler_config.temperature = 0.0;
22543 attach_test_mtp(&mut p);
22544 p.speculative = spec;
22545 let r = p.generate("abcdef", 12, None, None).unwrap();
22546 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22547 };
22548 let (plain, d0, _) = run(false);
22549 let (guarded, d1, a1) = run(true);
22550 assert_eq!(d0, 0);
22551 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22552 assert_eq!(a1, 0);
22553 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22554 }
22555
22556 #[test]
22557 fn empty_prompt_is_an_error_not_a_panic() {
22558 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22559 let r = p.generate("", 4, None, None);
22560 assert!(r.is_err(), "empty prompt must be a clean error");
22561 }
22562
22563 #[test]
22564 fn every_token_enters_kv_exactly_once() {
22565 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22566 p.sampler_config.temperature = 0.0;
22568 let r = p.generate("abc", 2, None, None).unwrap();
22569 assert_eq!(r.prompt_tokens, 3);
22570 assert_eq!(
22574 p.kv_cache.seq_len(),
22575 3 + r.tokens_generated - 1,
22576 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22577 );
22578 }
22579
22580 #[test]
22581 fn generation_is_reproducible_with_seed() {
22582 let run = || {
22583 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22584 p.generate("hello", 8, None, None).unwrap().token_ids
22585 };
22586 assert_eq!(run(), run());
22587 }
22588
22589 #[test]
22590 fn resetting_sampler_restarts_the_seeded_stream() {
22591 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22592 let config = SamplerConfig {
22593 seed: Some(1234),
22594 ..SamplerConfig::default()
22595 };
22596 p.set_sampler_config(config.clone());
22597 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22598 p.set_sampler_config(config);
22599 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22600 assert_eq!(first, second);
22601 }
22602
22603 #[test]
22604 fn eviction_bounds_the_cache() {
22605 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22606 p.kv_cache.max_seq_len = 6;
22607 p.sampler_config.temperature = 0.0;
22608 let _ = p.generate("abcd", 12, None, None).unwrap();
22609 assert!(
22610 p.kv_cache.seq_len() <= 6 + 1,
22611 "cache must stay bounded by max_seq_len (got {})",
22612 p.kv_cache.seq_len()
22613 );
22614 }
22615
22616 #[test]
22617 fn confidence_matches_tokens_and_is_a_probability() {
22618 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22619 p.sampler_config.temperature = 0.0;
22620 p.sampler_config.repetition_penalty = 1.0;
22621 let r = p.generate("abcd", 10, None, None).unwrap();
22622 assert_eq!(
22623 r.token_confidence.len(),
22624 r.token_ids.len(),
22625 "one confidence per emitted token"
22626 );
22627 for &c in &r.token_confidence {
22628 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22629 }
22630 let logits = [1.0f32, 3.0, 0.5, 3.0];
22632 let p0 = top1_prob_t(&logits, 1, 1.0);
22633 let p1 = top1_prob_t(&logits, 3, 1.0);
22634 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22635 assert!(p0 > 0.0 && p0 < 1.0);
22636 let sharp = top1_prob_t(&logits, 1, 1.0);
22638 let soft = top1_prob_t(&logits, 1, 2.0);
22639 assert!(soft < sharp, "higher temperature lowers peak confidence");
22640 }
22641
22642 #[test]
22643 fn trace_is_opt_in_and_parallels_the_output() {
22644 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22646 p.sampler_config.temperature = 0.0;
22647 p.sampler_config.repetition_penalty = 1.0;
22648 let r = p.generate("abcd", 10, None, None).unwrap();
22649 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22650
22651 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22653 p.sampler_config.temperature = 0.0;
22654 p.sampler_config.repetition_penalty = 1.0;
22655 p.set_trace(true);
22656 let r = p.generate("abcd", 10, None, None).unwrap();
22657 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22658 for (i, tr) in r.traces.iter().enumerate() {
22659 assert_eq!(tr.t, i, "trace index is sequential");
22660 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22661 assert_eq!(
22662 tr.confidence, r.token_confidence[i],
22663 "trace confidence matches the confidence channel"
22664 );
22665 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22667 }
22668 }
22669
22670 #[test]
22671 fn explain_prefill_logits_match_greedy_first_token() {
22672 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22676 p.sampler_config.temperature = 0.0;
22677 p.sampler_config.repetition_penalty = 1.0;
22678 let ids = p.tokenizer.encode("abcd");
22679 let logits = p.prefill_next_logits(&ids, None);
22680 let argmax = logits
22681 .iter()
22682 .enumerate()
22683 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22684 .unwrap()
22685 .0 as u32;
22686 let r = p.generate("abcd", 1, None, None).unwrap();
22687 assert_eq!(
22688 argmax, r.token_ids[0],
22689 "explain preview must match greedy emit"
22690 );
22691 }
22692
22693 #[test]
22694 fn laguna_shared_expert_is_unconditionally_added() {
22695 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22696 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22697 let zero_dense = || DenseFfn {
22698 gate_proj: matrix(vec![0.0; 4]),
22699 up_proj: matrix(vec![0.0; 4]),
22700 down_proj: matrix(vec![0.0; 4]),
22701 act: Act::Silu,
22702 down_t: None,
22703 segs: Vec::new(),
22704 };
22705 let shared = DenseFfn {
22706 gate_proj: identity(),
22707 up_proj: identity(),
22708 down_proj: identity(),
22709 act: Act::Silu,
22710 down_t: None,
22711 segs: Vec::new(),
22712 };
22713 let x = [1.0, 2.0];
22714 let expected = dense_ffn(&shared, &x, None);
22715 let moe = MoeFfn {
22716 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22717 experts: vec![zero_dense()],
22718 top_k: 1,
22719 norm_topk_prob: true,
22720 router_sigmoid: true,
22721 sigmoid_denom_eps: 1e-6,
22722 expert_bias: None,
22723 routed_scaling: 1.0,
22724 route_tau: None,
22725 shared: Some((shared, None)),
22726 stats: std::cell::RefCell::new(Vec::new()),
22727 act_sq: std::cell::RefCell::new(Vec::new()),
22728 act_rows: std::cell::RefCell::new(Vec::new()),
22729 mask: None,
22730 per_expert_scale: None,
22731 router_input_norm: false,
22732 resonance: None,
22733 grown: Vec::new(),
22734 };
22735 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22736 for (actual, expected) in actual.iter().zip(expected) {
22737 assert!((actual - expected).abs() < 1e-6);
22738 }
22739 }
22740
22741 fn mimo_test_pipeline() -> Pipeline {
22750 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22751 let kvh = [1usize, 2, 2, 1];
22752 let synth = |n: usize, salt: usize| -> Vec<f32> {
22753 (0..n)
22754 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22755 .collect()
22756 };
22757 let qt = |rows: usize, cols: usize, salt: usize| {
22758 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22759 };
22760 let dense = |inter: usize, salt: usize| DenseFfn {
22761 gate_proj: qt(inter, hs, salt),
22762 up_proj: qt(inter, hs, salt + 1),
22763 down_proj: qt(hs, inter, salt + 2),
22764 act: Act::Silu,
22765 down_t: None,
22766 segs: Vec::new(),
22767 };
22768 let layers: Vec<LayerWeights> = (0..4)
22769 .map(|li| LayerWeights {
22770 input_norm: vec![1.0; hs],
22771 post_norm: vec![1.0; hs],
22772 attn_out_norm: None,
22773 ffn_out_norm: None,
22774 layer_scale: None,
22775 ffn: if li == 0 {
22776 FfnKind::Dense(dense(inter, 50))
22777 } else {
22778 FfnKind::Moe(MoeFfn {
22779 router: qt(4, hs, 60 + li),
22780 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22781 top_k: 2,
22782 norm_topk_prob: true,
22783 router_sigmoid: true,
22784 sigmoid_denom_eps: 1e-6,
22785 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22786 routed_scaling: 1.0,
22787 route_tau: None,
22788 shared: None,
22789 stats: std::cell::RefCell::new(Vec::new()),
22790 act_sq: std::cell::RefCell::new(Vec::new()),
22791 act_rows: std::cell::RefCell::new(Vec::new()),
22792 mask: None,
22793 per_expert_scale: None,
22794 router_input_norm: false,
22795 resonance: None,
22796 grown: Vec::new(),
22797 })
22798 },
22799 attn: AttnKind::Full {
22800 wq: qt(nh * hd, hs, li * 10 + 1),
22801 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22802 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22803 wo: qt(hs, nh * vd, li * 10 + 4),
22804 q_norm: None,
22805 k_norm: None,
22806 output_gate: false,
22807 softplus_gate: None,
22808 bias: None,
22809 },
22810 })
22811 .collect();
22812 let mut p = Pipeline::new(
22813 Tokenizer::byte_level(),
22814 PipelineWeights {
22815 embed_tokens: qt(vocab, hs, 100),
22816 layers,
22817 lm_head: qt(vocab, hs, 200),
22818 final_norm: vec![1.0; hs],
22819 },
22820 hs,
22821 inter,
22822 nh,
22823 1, hd,
22825 4,
22826 4,
22827 false,
22828 vocab,
22829 1e-6,
22830 1e7,
22831 NormStyle::Qwen,
22832 4096,
22833 SamplerConfig {
22834 seed: Some(7),
22835 ..Default::default()
22836 },
22837 );
22838 p.layer_dump = None;
22840 p.set_rotary(4, 1e7);
22841 p.sliding_layers = Some(vec![false, true, true, false]);
22842 p.swa = Some((3, usize::MAX));
22843 p.rotary_dim_local = Some(4);
22844 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22845 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22846 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22847 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22848 p
22849 }
22850
22851 #[test]
22852 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22853 let mut p = mimo_test_pipeline();
22854 p.speculative = false;
22855 p.ignore_eos = true;
22856 p.sampler_config.temperature = 0.0;
22857 p.sampler_config.repetition_penalty = 1.0;
22858 let a = vec![3, 5, 7, 9, 11, 13];
22859 let b = vec![4, 8, 12, 16, 20, 24];
22860 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22861 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22862 let actual = p
22866 .generate_from_embeds(&a, &rows, 8, None, None)
22867 .unwrap()
22868 .token_ids;
22869 assert_eq!(actual, expected);
22870 assert!(p.kv_history.is_empty());
22871 let mut extended = a.clone();
22872 extended.push(17);
22873 let after_media = p
22874 .generate_from_ids(&extended, 8, None, None)
22875 .unwrap()
22876 .token_ids;
22877 p.reset_session();
22878 let fresh = p
22879 .generate_from_ids(&extended, 8, None, None)
22880 .unwrap()
22881 .token_ids;
22882 assert_eq!(after_media, fresh);
22883 assert!(
22884 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22885 .is_err()
22886 );
22887 p.reset_session();
22891 p.generate_from_ids(&a, 1, None, None).unwrap();
22892 let mut media_ids = p.kv_history.clone();
22893 assert!(!media_ids.is_empty());
22894 media_ids.extend_from_slice(&[19, 21, 23]);
22895 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22896 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22897 let mut oracle = mimo_test_pipeline();
22898 oracle.speculative = false;
22899 oracle.ignore_eos = true;
22900 oracle.sampler_config.temperature = 0.0;
22901 oracle.sampler_config.repetition_penalty = 1.0;
22902 let expected = oracle
22903 .generate_from_ids(&source_ids, 8, None, None)
22904 .unwrap()
22905 .token_ids;
22906 assert_eq!(
22907 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22908 .unwrap()
22909 .token_ids,
22910 expected
22911 );
22912 assert!(p.kv_history.is_empty());
22913 let mut bad = rows;
22914 bad[0] = f32::NAN;
22915 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22916 }
22917
22918 fn f32_bits(v: &[f32]) -> Vec<u32> {
22919 v.iter().map(|x| x.to_bits()).collect()
22920 }
22921
22922 #[test]
22929 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22930 let mut p = mimo_test_pipeline();
22931 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22932 assert_eq!(kv, vec![1, 2, 2, 1]);
22933 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22934 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22935 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22936 let hs = p.hidden_size;
22937 let mut decode = Vec::new();
22938 for (pos, &id) in ids.iter().enumerate() {
22939 let e = p.embed_single(id);
22940 let h = p.forward_layers(&e, pos, None);
22941 decode.push(p.logits_from_hidden(&h));
22942 }
22943 for l in &p.kv_cache.layers {
22944 assert_eq!(l.seq_len, 12);
22945 assert_eq!(l.head_values(0).len(), 12 * 8);
22947 }
22948 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22949
22950 p.clear_sequence_state();
22951 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22952 for pos in 0..ids.len() {
22953 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22954 assert_eq!(
22955 f32_bits(&decode[pos]),
22956 f32_bits(&lg),
22957 "whole prompt, pos {pos}"
22958 );
22959 }
22960
22961 p.clear_sequence_state();
22962 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22963 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22964 for pos in 0..ids.len() {
22965 let row = if pos < 5 {
22966 &a[pos * hs..(pos + 1) * hs]
22967 } else {
22968 &b[(pos - 5) * hs..(pos - 4) * hs]
22969 };
22970 let lg = p.logits_from_hidden(row);
22971 assert_eq!(
22972 f32_bits(&decode[pos]),
22973 f32_bits(&lg),
22974 "two chunks, pos {pos}"
22975 );
22976 }
22977
22978 let last = |p: &mut Pipeline| {
22981 p.clear_sequence_state();
22982 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22983 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22984 };
22985 let base = last(&mut p);
22986 let mut no_sinks = mimo_test_pipeline();
22987 for l in &mut no_sinks.kv_cache.layers {
22988 l.sinks = None;
22989 }
22990 assert_ne!(
22991 f32_bits(&last(&mut no_sinks)),
22992 f32_bits(&base),
22993 "sinks are live"
22994 );
22995 let mut wide = mimo_test_pipeline();
22996 wide.swa = Some((64, usize::MAX));
22997 assert_ne!(
22998 f32_bits(&last(&mut wide)),
22999 f32_bits(&base),
23000 "window is live"
23001 );
23002
23003 p.clear_sequence_state();
23005 p.ignore_eos = true;
23006 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
23007 assert_eq!(r.token_ids.len(), 4);
23008 }
23009
23010 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
23013 let (hs, nh) = (16usize, 4usize);
23014 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
23015 p.layer_dump = None;
23016 p.swa = Some((6, 4));
23017 p.proj_gate_sigmoid = true;
23018 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
23019 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
23020 let g: Vec<f32> = (0..nh * hs)
23021 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
23022 .collect();
23023 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
23024 }
23025 }
23026 p.swa_trim = trim;
23027 p
23028 }
23029
23030 #[test]
23036 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
23037 let mut a = spark_test_pipeline(None);
23038 let mut b = spark_test_pipeline(Some((2, 4)));
23039 assert_eq!(
23040 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
23041 vec![Some(6), Some(6), Some(6), None]
23042 );
23043 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
23044 let mut pos = 0usize;
23045 for &n in [5usize, 13, 7, 16].iter().cycle() {
23046 if pos >= ids.len() {
23047 break;
23048 }
23049 let end = (pos + n).min(ids.len());
23050 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23051 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23052 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
23053 pos = end;
23054 }
23055 for _ in 0..23 {
23056 let e = a.embed_single(((pos * 7) % 64) as u32);
23057 let ha = a.forward_layers(&e, pos, None);
23058 let hb = b.forward_layers(&e, pos, None);
23059 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
23060 for li in 0..3 {
23061 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
23062 }
23063 pos += 1;
23064 }
23065 for round in 0..9 {
23066 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
23067 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
23068 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
23069 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
23070 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
23071 if round % 2 == 1 {
23072 for p in [&mut a, &mut b] {
23074 for l in &mut p.kv_cache.layers {
23075 l.truncate_last(2);
23076 }
23077 }
23078 } else {
23079 pos += 2;
23080 }
23081 }
23082 let e = a.embed_single(9);
23083 let (ha, hb) = (
23084 a.forward_layers(&e, pos, None),
23085 b.forward_layers(&e, pos, None),
23086 );
23087 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
23088 for li in 0..3 {
23089 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
23090 assert!(lb.base() > 0, "layer {li} trimmed");
23091 assert_eq!(la.base(), 0);
23092 assert_eq!(lb.pos_len(), la.seq_len);
23093 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
23094 }
23095 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
23096 assert_eq!(
23097 (gb.base(), gb.seq_len),
23098 (0, ga.seq_len),
23099 "the global layer keeps all"
23100 );
23101 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
23102 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
23103 }
23104
23105 #[test]
23110 fn swa_trim_wire_handoff_continues_bitwise() {
23111 let mut a = spark_test_pipeline(None);
23112 let mut b = spark_test_pipeline(Some((2, 4)));
23113 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
23114 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23115 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23116 assert_eq!(f32_bits(&ha), f32_bits(&hb));
23117 let mut pos = ids.len();
23118 for _ in 0..5 {
23119 let e = a.embed_single(((pos * 5) % 64) as u32);
23120 assert_eq!(
23121 f32_bits(&a.forward_layers(&e, pos, None)),
23122 f32_bits(&b.forward_layers(&e, pos, None))
23123 );
23124 pos += 1;
23125 }
23126 let mut c = spark_test_pipeline(Some((2, 4)));
23127 for li in 0..4 {
23128 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
23129 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
23130 }
23131 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
23132 assert_eq!(c.kv_cache.seq_len(), pos);
23133 for step in 0..17 {
23134 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
23135 let ha = a.forward_layers(&e, pos, None);
23136 let hc = c.forward_layers(&e, pos, None);
23137 assert_eq!(
23138 f32_bits(&ha),
23139 f32_bits(&hc),
23140 "after the hand-off, step {step}"
23141 );
23142 pos += 1;
23143 }
23144 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23145 }
23146
23147 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23152 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23153 let synth = |len: usize, salt: usize| -> Vec<f32> {
23154 (0..len)
23155 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23156 .collect()
23157 };
23158 let qt = |rows: usize, cols: usize, salt: usize| {
23159 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23160 };
23161 let layers = widths
23162 .iter()
23163 .copied()
23164 .enumerate()
23165 .map(|(k, inter)| {
23166 let s = 500 + k * 40;
23167 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23168 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23169 MtpModule {
23170 enorm: vec![1.0; hs],
23171 hnorm: vec![1.0; hs],
23172 eh_proj: qt(hs, 2 * hs, s),
23173 layer: LayerWeights {
23174 input_norm: vec![1.0; hs],
23175 post_norm: vec![1.0; hs],
23176 attn_out_norm: None,
23177 ffn_out_norm: None,
23178 layer_scale: None,
23179 attn: AttnKind::Full {
23180 wq: qt(nh * hd, hs, s + 1),
23181 wk: qt(nkv * hd, hs, s + 2),
23182 wv: qt(nkv * vd, hs, s + 3),
23183 wo: qt(hs, nh * vd, s + 4),
23184 q_norm: None,
23185 k_norm: None,
23186 output_gate: false,
23187 softplus_gate: None,
23188 bias: None,
23189 },
23190 ffn: FfnKind::Dense(DenseFfn {
23191 gate_proj: qt(inter, hs, s + 5),
23192 up_proj: qt(inter, hs, s + 6),
23193 down_proj: qt(hs, inter, s + 7),
23194 act: Act::Silu,
23195 down_t: None,
23196 segs: Vec::new(),
23197 }),
23198 },
23199 final_norm: vec![1.0; hs],
23200 kv,
23201 }
23202 })
23203 .collect();
23204 mimo_mtp::MimoMtp::from_layers(layers)
23205 }
23206
23207 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
23208 mimo_test_mtp_widths(&vec![24; n], gain)
23209 }
23210
23211 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
23212 p.clear_sequence_state();
23213 p.speculative = spec;
23214 p.ignore_eos = true;
23215 p.sampler_config.temperature = 0.0;
23216 p.generate_from_ids(ids, n, None, None).unwrap()
23217 }
23218
23219 #[test]
23220 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
23221 let mut p = mimo_test_pipeline();
23222 let widths = [7usize, 13, 29];
23223 assert_ne!(widths[0], p.intermediate_size);
23224 assert_ne!(widths[1], p.intermediate_size);
23225 assert_ne!(widths[2], p.intermediate_size);
23226 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
23227 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
23228
23229 let trunk = p.probe_ffn_mass(&ids);
23230 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
23231 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
23232 assert_eq!(got.len(), widths.len());
23233 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
23234 assert_eq!(stat.mass.len(), width, "draft layer {k}");
23235 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
23236 assert_eq!(stat.observed_positions, ids.len() - k - 1);
23237 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
23238 assert!(stat
23239 .mass
23240 .iter()
23241 .zip(&stat.squared_mass)
23242 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23243 }
23244 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23245 let st = p.mimo_mtp.as_ref().unwrap();
23246 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23247
23248 let h = p.hidden_size;
23251 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23252 panic!("synthetic MiMo MTP must be dense")
23253 };
23254 dense.segs.push(FfnSeg {
23255 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23256 up: QTensor::from_f32(vec![0.0; h], 1, h),
23257 down: QTensor::from_f32(vec![0.0; h], h, 1),
23258 start: dense.gate_proj.rows(),
23259 width: 1,
23260 });
23261 assert!(p
23262 .probe_mtp_ffn(&ids)
23263 .unwrap_err()
23264 .contains("tube-segmented draft FFNs are not supported"));
23265 assert_eq!(p.kv_cache.seq_len(), 0);
23266 assert!(p
23267 .mimo_mtp
23268 .as_ref()
23269 .unwrap()
23270 .layers
23271 .iter()
23272 .all(|m| m.kv.seq_len == 0));
23273 }
23274
23275 #[test]
23276 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23277 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23278 p.clear_sequence_state();
23279 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23280 let _exact = ExactMtpCalibrationGuard::enter();
23281 let got = crate::gpu::cpu_scope(|| {
23282 let trunk = p.probe_trunk_rows(ids)?;
23283 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
23284 })
23285 .expect("teacher-forced MiMo probe");
23286 st.reset();
23287 p.mimo_mtp = Some(st);
23288 p.reset_mtp_probe_state();
23289 got
23290 }
23291
23292 for post_norm_hidden in [false, true] {
23293 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23294 let make = || {
23295 let mut p = mimo_test_pipeline();
23296 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23297 st.post_norm_hidden = post_norm_hidden;
23298 p.mimo_mtp = Some(st);
23299 p
23300 };
23301 let mut full = make();
23302 let expected = collect(&mut full, &ids, usize::MAX);
23303 for rows in [1usize, 7, 128] {
23304 let mut chunked = make();
23305 let got = collect(&mut chunked, &ids, rows);
23306 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23307 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23308 assert_eq!(
23309 got.observed_positions, expected.observed_positions,
23310 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23311 );
23312 assert_eq!(
23313 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23314 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23315 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23316 );
23317 assert_eq!(
23318 got.squared_mass
23319 .iter()
23320 .map(|v| v.to_bits())
23321 .collect::<Vec<_>>(),
23322 expected
23323 .squared_mass
23324 .iter()
23325 .map(|v| v.to_bits())
23326 .collect::<Vec<_>>(),
23327 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23328 );
23329 }
23330 }
23331 }
23332 }
23333
23334 #[test]
23340 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23341 for post in [false, true] {
23344 let mut p = mimo_test_pipeline();
23345 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23347 let mut st0 = mimo_test_mtp(3, 1.0);
23348 st0.post_norm_hidden = post;
23349 p.mimo_mtp = Some(st0);
23350 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23351 let hs = p.hidden_size;
23352 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23353 p.mimo_note_rows(&hb, 0);
23354 let mut st = p.mimo_mtp.take().unwrap();
23355 let k = 3;
23358 let mut inc = Vec::new();
23359 for t in 0..ids.len() - k - 1 {
23360 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23361 }
23362 let s = ids.len();
23365 let mut reference = vec![vec![0u32; k]; s - k - 1];
23366 let mut fresh = mimo_test_mtp(3, 1.0);
23367 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23368 let n = s - layer - 1;
23369 let mut cats = vec![0.0f32; n * 2 * hs];
23370 for j in 0..n {
23371 let e = p.embed_single(ids[j + layer + 1]);
23372 let raw = &hb[j * hs..(j + 1) * hs];
23373 let g = if post {
23374 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23375 } else {
23376 raw.to_vec()
23377 };
23378 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23379 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23380 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23381 }
23382 let mut x = vec![0.0f32; n * hs];
23383 m.eh_proj.matmat(&cats, n, &mut x, None);
23384 p.mimo_mtp_block(m, &mut x, n, 0);
23385 for (t, row) in reference.iter_mut().enumerate() {
23386 let y = inference::rms_norm(
23387 &x[t * hs..(t + 1) * hs],
23388 &m.final_norm,
23389 p.rms_eps,
23390 p.norm_style,
23391 );
23392 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23393 }
23394 }
23395 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23396 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23398 assert!(distinct.len() > 3, "{inc:?}");
23399 let last_t = ids.len() - k - 2;
23401 for m in &st.layers {
23402 assert_eq!(m.kv.seq_len, last_t + 1);
23403 }
23404 }
23405 }
23406
23407 #[test]
23414 fn mimo_speculative_greedy_equals_plain_greedy() {
23415 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23416 let _env = ScopedTestEnv::set(&[
23417 ("CMF_GPU", Some("0")),
23418 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23419 ]);
23420 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23421 let n = 24;
23422 let mut p = mimo_test_pipeline();
23423 let plain = mimo_greedy(&mut p, &ids, n, false);
23424 assert_eq!(plain.mtp_drafted, 0);
23425 assert_eq!(plain.token_ids.len(), n);
23426 let plain_kv = p.kv_cache.layers[0].seq_len;
23427
23428 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23430 let spec = mimo_greedy(&mut p, &ids, n, true);
23431 assert!(spec.mtp_drafted > 0, "the round must draft");
23432 assert_eq!(spec.token_ids, plain.token_ids);
23433 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23434
23435 let mut truth: Vec<u32> = ids.clone();
23438 truth.extend(&plain.token_ids);
23439 let mut noisy = truth.clone();
23440 for (i, t) in noisy.iter_mut().enumerate() {
23441 if i % 5 == 0 {
23442 *t = (*t + 1) % 64;
23443 }
23444 }
23445 let mut st = mimo_test_mtp(3, 1.0);
23446 st.draft_override = Some(noisy);
23447 p.mimo_mtp = Some(st);
23448 let spec = mimo_greedy(&mut p, &ids, n, true);
23449 assert_eq!(spec.token_ids, plain.token_ids);
23450 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23451 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23452 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23453 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23454 assert!(
23455 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23456 "{:?}",
23457 stats.accept_hist
23458 );
23459 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23460
23461 let mut st = mimo_test_mtp(3, 1.0);
23464 st.draft_override = Some(truth);
23465 p.mimo_mtp = Some(st);
23466 let spec = mimo_greedy(&mut p, &ids, n, true);
23467 assert_eq!(spec.token_ids, plain.token_ids);
23468 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23469 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23470
23471 let off = mimo_greedy(&mut p, &ids, n, false);
23473 assert_eq!(off.token_ids, plain.token_ids);
23474 assert_eq!(off.mtp_drafted, 0);
23475 }
23476
23477 #[test]
23486 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23487 let p = mimo_test_pipeline();
23488 assert_eq!(
23489 p.graph_attn_decline_reason(),
23490 Some("per-layer KV head counts")
23491 );
23492 assert_eq!(p.wgpu_graph_attn_decline(), None);
23493 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23494 assert_eq!(
23495 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23496 (1, 4, 4, None, false)
23497 );
23498 assert_eq!(g0.invf, p.inv_freq.as_slice());
23499 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23500 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23501 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23502 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23503 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23504 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23505 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23506
23507 let emb = p.embed_single(3);
23510 let mut lg = Vec::new();
23511 assert!(
23512 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23513 .is_none()
23514 );
23515 let mut hid = emb.clone();
23516 assert_eq!(
23517 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23518 crate::gpu::BatchGraphOutcome::Declined
23519 );
23520 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23521 assert!(p.try_multi_burst(3, 0, 4).is_none());
23522 assert!(
23523 p.graph_declines().is_empty(),
23524 "no attention decline logged: {:?}",
23525 p.graph_declines()
23526 );
23527 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23532 assert_eq!(plain().graph_attn_decline_reason(), None);
23533 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23534 assert!(
23535 plain().graph_attn_geom(0).is_none(),
23536 "uniform models keep the historical arms"
23537 );
23538 let mut q = plain();
23539 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23540 assert_eq!(
23541 q.graph_attn_decline_reason(),
23542 Some("learned attention sinks")
23543 );
23544 assert_eq!(
23545 q.graph_attn_geom(1).unwrap().sink,
23546 Some(&[0.25f32, -0.25][..])
23547 );
23548 let mut q = plain();
23549 q.set_attn_geometry(None, Some(2)).unwrap();
23550 assert_eq!(
23551 q.graph_attn_decline_reason(),
23552 Some("V heads narrower than Q/K heads")
23553 );
23554 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23555 let mut q = plain();
23556 q.sliding_layers = Some(vec![true, false]);
23557 q.swa = Some((4, usize::MAX));
23558 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23559 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23560 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23561
23562 let mut q = mimo_test_pipeline();
23566 q.rope_scale = 2.0;
23567 q.rope_scale_local = 1.25;
23568 assert_eq!(q.wgpu_graph_attn_decline(), None);
23569 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23570 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23571
23572 let mut q = plain();
23575 q.rope_scale = 2.0;
23576 assert_eq!(
23577 q.wgpu_graph_attn_decline(),
23578 Some("scaled RoPE without per-layer geometry")
23579 );
23580 }
23581
23582 #[cfg(target_os = "macos")]
23588 #[test]
23589 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23590 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23594 p.swa = Some((16, usize::MAX));
23595 p.sliding_layers = Some(vec![true, true]);
23596 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23597 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23598
23599 assert_eq!(
23600 p.graph_attn_decline_reason(),
23601 Some("learned attention sinks")
23602 );
23603 let swa_graph = p.metal_graph_swa();
23604 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23605 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23606 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23607 assert!(
23608 native_sink_attend,
23609 "the q1 sink admission must follow the SWA door"
23610 );
23611 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23614 }
23615
23616 #[test]
23617 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23618 let mut p = mimo_test_pipeline();
23619 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23620 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23621 for _ in 0..if li == 0 { 2 } else { 12 } {
23622 layer.append(&row, &row, &[]);
23623 }
23624 }
23625 p.mimo_verify_rewind(9).unwrap();
23626 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23627 for layer in &p.kv_cache.layers[1..] {
23628 assert_eq!(layer.seq_len, 9);
23629 }
23630 }
23631
23632 #[test]
23636 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23637 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23638 let _ = std::fs::remove_dir_all(&dir);
23639 let mut p = mimo_test_pipeline();
23640 let hs = p.hidden_size;
23641 let ids = [5u32, 9, 11, 2, 40];
23642 p.layer_dump = Some(dir.join("decode"));
23643 for (pos, &id) in ids.iter().enumerate() {
23644 let e = p.embed_single(id);
23645 let _ = p.forward_layers(&e, pos, None);
23646 }
23647 p.clear_sequence_state();
23648 p.layer_dump = Some(dir.join("prefill"));
23649 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23650 for pos in 0..ids.len() {
23651 for li in 0..p.num_layers {
23652 let name = format!("p{pos:06}_l{li:02}.f32");
23653 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23654 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23655 assert_eq!(a.len(), hs * 4, "{name}");
23656 assert_eq!(a, b, "{name}");
23657 }
23658 }
23659 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23660 let vals: Vec<f32> = last
23661 .chunks(4)
23662 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23663 .collect();
23664 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23665 let _ = std::fs::remove_dir_all(&dir);
23666 }
23667
23668 #[test]
23669 fn attn_geometry_and_sinks_are_validated() {
23670 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23671 assert!(
23672 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23673 "one entry per layer"
23674 );
23675 assert!(
23676 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23677 "3 does not divide 4"
23678 );
23679 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23680 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23681 assert!(
23682 p.set_attn_geometry(None, Some(5)).is_err(),
23683 "V wider than the head"
23684 );
23685 p.set_attn_geometry(None, Some(4)).unwrap();
23686 assert_eq!(
23687 p.v_head_dim, None,
23688 "v_head_dim == head_dim is the uniform case"
23689 );
23690 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23691 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23692 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23693 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23694 assert!(
23695 p.kv_cache.layers[1].sinks.is_some(),
23696 "a reshape keeps the layer's sinks"
23697 );
23698 assert_eq!(p.layer_geom(1).0, 4);
23699 assert!(
23700 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23701 "one sink per Q head"
23702 );
23703 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23704 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23705 }
23706
23707 #[test]
23710 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23711 let cfg = || {
23712 Some(crate::nystrom::O1Cfg {
23713 layers: crate::nystrom::O1Layers::All,
23714 m: 4,
23715 w: 8,
23716 sink: 2,
23717 rect: crate::nystrom::O1Rect::Aggregate,
23718 })
23719 };
23720 let mut p = mimo_test_pipeline();
23721 p.set_o1(cfg());
23722 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23723 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23724 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23725 q.sliding_layers = Some(vec![false, false, true]);
23726 q.swa = Some((4, usize::MAX));
23727 q.set_o1(cfg());
23728 assert_eq!(q.o1_flags, vec![true, false, false]);
23729 }
23730
23731 #[test]
23732 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23733 const B: usize = 19;
23734 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23735 p.set_o1(Some(crate::nystrom::O1Cfg {
23736 layers: crate::nystrom::O1Layers::All,
23737 m: 4,
23738 w: 8,
23739 sink: 2,
23740 rect: crate::nystrom::O1Rect::Aggregate,
23741 }));
23742 p.o1_begin_with_prefix(Some(B));
23743 let ids: Vec<u32> = (0..B as u32).collect();
23744 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23745
23746 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23747 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23748 let next = p.embed_single(B as u32);
23749 let _ = p.forward_layers(&next, B, None);
23750 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23751 }
23752
23753 #[test]
23754 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23755 const B: usize = 19;
23756 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23757 let gdn_cfg = crate::linear_core::GdnCfg {
23761 num_v_heads: 2,
23762 num_k_heads: 1,
23763 key_head_dim: 2,
23764 value_head_dim: 4,
23765 conv_kernel: 3,
23766 hidden_size: 8,
23767 rms_eps: 1e-6,
23768 output_gate_sigmoid: false,
23769 };
23770 let synth = |n: usize, salt: usize| -> Vec<f32> {
23771 (0..n)
23772 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23773 .collect()
23774 };
23775 let qt = |rows: usize, cols: usize, salt: usize| {
23776 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23777 };
23778 let c_dim = gdn_cfg.conv_dim();
23779 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23780 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23781 in_proj_qkv: qt(c_dim, 8, 1),
23782 in_proj_z: qt(vd, 8, 2),
23783 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23784 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23785 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23786 a_log: vec![0.2, 0.5],
23787 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23788 norm: vec![1.0; gdn_cfg.value_head_dim],
23789 out_proj: qt(8, vd, 7),
23790 });
23791 p.gdn_cfg = Some(gdn_cfg);
23792 p.set_o1(Some(crate::nystrom::O1Cfg {
23793 layers: crate::nystrom::O1Layers::All,
23794 m: 4,
23795 w: 8,
23796 sink: 2,
23797 rect: crate::nystrom::O1Rect::Aggregate,
23798 }));
23799 p.o1_begin_with_prefix(Some(B));
23800 for pos in 0..B - 2 {
23801 let emb = p.embed_single(pos as u32);
23802 let _ = p.forward_layers(&emb, pos, None);
23803 }
23804 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23805
23806 let e1 = p.embed_single((B - 2) as u32);
23807 let e2 = p.embed_single((B - 1) as u32);
23808 let _ = p.forward_pair(&e1, &e2, B - 2);
23809
23810 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23811 assert!(
23812 p.kv_cache
23813 .layers
23814 .iter()
23815 .enumerate()
23816 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23817 );
23818 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23819 assert_ne!(
23820 p.kv_cache.layers[0].linear_state, lane1_state,
23821 "real pair must commit GDN lane 2 before returning"
23822 );
23823 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23824 let next = p.embed_single(B as u32);
23825 let _ = p.forward_layers(&next, B, None);
23826 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23827 }
23828
23829 #[test]
23830 fn o1_error_observation_stays_terminal_until_reset() {
23831 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23832 p.set_o1(Some(crate::nystrom::O1Cfg {
23833 layers: crate::nystrom::O1Layers::All,
23834 m: 4,
23835 w: 8,
23836 sink: 2,
23837 rect: crate::nystrom::O1Rect::Aggregate,
23838 }));
23839 p.o1_begin();
23840 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23841
23842 assert!(p.o1_seal_checked().is_err());
23843 assert!(
23844 p.o1_seal_checked().is_err(),
23845 "retry must see the sticky error"
23846 );
23847 let k = vec![0.2f32; 4];
23848 let v = vec![0.3f32; 4];
23849 p.kv_cache.layers[0].append(&k, &v, &[]);
23850 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23851
23852 p.reset_session();
23853 p.o1_begin();
23854 p.kv_cache.layers[0].append(&k, &v, &[]);
23855 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23856 }
23857
23858 #[test]
23859 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23860 let ids = vec![1u32, 2, 3, 4, 5, 6];
23861 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23862 p.graph_logits = Some(vec![123.0]);
23863 p.graph_want_logits = true;
23864 p.graph_failed
23865 .store(true, std::sync::atomic::Ordering::Relaxed);
23866 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23867 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23868 assert!(err.contains("before NLL"));
23869 assert!(p.graph_logits.is_none());
23870 assert!(!p.graph_want_logits);
23871 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23872 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23873
23874 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23875 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23876 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23877 assert_eq!(actual.1, expected.1);
23878 assert!((actual.0 - expected.0).abs() < 1e-9);
23879 }
23880
23881 #[test]
23882 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23883 let ids = vec![1u32, 2, 3, 4, 5, 6];
23884 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23885 p.nll_test_fail_at = Some(1);
23886 let err = p
23887 .nll_ids_from(&ids, 0)
23888 .expect_err("one-shot forward failure");
23889 assert!(err.contains("forward") || err.contains("score row"));
23890 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23891 assert!(!p.graph_want_logits);
23892 assert!(p.graph_logits.is_none());
23893 assert!(p.kv_history.is_empty());
23894
23895 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23896 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23897 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23898 assert_eq!(actual.1, expected.1);
23899 assert!((actual.0 - expected.0).abs() < 1e-9);
23900 }
23901
23902 #[test]
23903 fn nll_serial_failure_before_first_row_is_reported() {
23904 let ids = vec![1u32, 2, 3, 4];
23905 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23906 p.nll_test_force_serial = true;
23907 p.nll_test_fail_at = Some(0);
23908 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23909 assert!(err.contains("serial forward"));
23910 assert!(p.kv_history.is_empty());
23911 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23912 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23913 }
23914
23915 #[test]
23916 fn ffn_probe_failure_discards_recorder_and_state() {
23917 let ids = vec![1u32, 2, 3, 4];
23918 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23919 p.nll_test_fail_at = Some(0);
23920 let err = p
23921 .probe_ffn_mass_batch(&ids)
23922 .expect_err("probe forward failure");
23923 assert!(err.contains("NLL"));
23924 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23925 assert!(p.kv_history.is_empty());
23926 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23927 }
23928
23929 #[test]
23930 fn nll_test_controls_are_pipeline_scoped() {
23931 let ids = vec![1u32, 2, 3, 4];
23932 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23933 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23934 failing.nll_test_force_serial = true;
23935 failing.nll_test_fail_at = Some(0);
23936
23937 assert!(!failing.can_prefill_batched());
23938 assert!(unaffected.can_prefill_batched());
23939 let expected = unaffected
23940 .nll_ids_from(&ids, 0)
23941 .expect("unaffected pipeline remains usable");
23942 let err = failing
23943 .nll_ids_from(&ids, 0)
23944 .expect_err("failure injection belongs to failing pipeline");
23945 assert!(err.contains("serial forward"));
23946 assert!(failing.nll_test_fail_at.is_none());
23947 assert!(unaffected.can_prefill_batched());
23948 let actual = unaffected
23949 .nll_ids_from(&ids, 0)
23950 .expect("unaffected pipeline remains reusable");
23951 assert_eq!(actual.1, expected.1);
23952 assert!((actual.0 - expected.0).abs() < 1e-9);
23953 }
23954
23955 #[test]
23956 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23957 let ids = vec![1u32, 2, 3, 4, 5, 6];
23958 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23959 p.graph_logits = Some(vec![123.0]);
23960 p.graph_want_logits = true;
23961 p.graph_failed
23962 .store(true, std::sync::atomic::Ordering::Relaxed);
23963 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23964
23965 let err = p
23966 .forward_ids(&ids, None)
23967 .expect_err("a failed forward must not become a valid head result");
23968 assert!(err.contains("forward_ids setup"));
23969 assert!(p.graph_logits.is_none());
23970 assert!(!p.graph_want_logits);
23971 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23972 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23973 assert_eq!(p.kv_cache.seq_len(), 0);
23974
23975 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23976 .forward_ids(&ids, None)
23977 .expect("fresh forward_ids");
23978 let actual = p
23979 .forward_ids(&ids, None)
23980 .expect("pipeline remains reusable after a failed forward");
23981 assert_eq!(actual.len(), expected.len());
23982 assert!(
23983 actual
23984 .iter()
23985 .zip(expected)
23986 .all(|(a, b)| (a - b).abs() < 1e-9)
23987 );
23988 assert_eq!(p.kv_cache.seq_len(), ids.len());
23989 }
23990
23991 #[test]
23992 fn sigmoid_router_floor_is_explicit_per_architecture() {
23993 let zero = || DenseFfn {
23999 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24000 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24001 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24002 act: Act::Silu,
24003 down_t: None,
24004 segs: Vec::new(),
24005 };
24006 let m = MoeFfn {
24007 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24008 experts: vec![zero(), zero()],
24009 top_k: 1,
24010 norm_topk_prob: true,
24011 router_sigmoid: true,
24012 sigmoid_denom_eps: 1e-20,
24013 expert_bias: None,
24014 routed_scaling: 2.5,
24015 route_tau: None,
24016 shared: None,
24017 stats: std::cell::RefCell::new(Vec::new()),
24018 act_sq: std::cell::RefCell::new(Vec::new()),
24019 act_rows: std::cell::RefCell::new(Vec::new()),
24020 mask: None,
24021 per_expert_scale: None,
24022 router_input_norm: false,
24023 resonance: None,
24024 grown: Vec::new(),
24025 };
24026 let logits = [-20.0f32, -20.0];
24027 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
24028 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24029 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
24030 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
24031 assert!((flint_wsum - reference_expected).abs() < 1e-15);
24032 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
24033 assert!(legacy_wsum > minimax_wsum * 100.0);
24034 }
24035
24036 #[test]
24037 fn resonance_scores_match_formula_and_stable_tie() {
24038 let r = Resonance {
24039 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
24041 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
24042 k: 1,
24043 bias: vec![1.5, 0.5, 0.0],
24044 shell: Vec::new(),
24045 };
24046 let x = [1.0f32, 1.0];
24047 let mut got = vec![0.0; 3];
24048 r.scores(&x, &mut got);
24049 assert!((got[0] - 0.5).abs() < 1e-6);
24053 assert!((got[1] - 0.5).abs() < 1e-6);
24054 assert!(got[2].abs() < 1e-6);
24055 let best = got
24056 .iter()
24057 .enumerate()
24058 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
24059 .map(|(i, _)| i);
24060 assert_eq!(best, Some(0));
24061 assert!(got.iter().all(|v| v.is_finite()));
24062 }
24063
24064 #[test]
24069 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
24070 let plain = Resonance {
24076 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
24077 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
24078 k: 1,
24079 bias: vec![1.5, 0.5, 0.0, 0.0],
24080 shell: Vec::new(),
24081 };
24082 let shelled = Resonance {
24083 mu: plain.mu.clone(),
24084 u: plain.u.clone(),
24085 k: 1,
24086 bias: plain.bias.clone(),
24087 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
24088 };
24089 assert!(!plain.has_shell());
24090 assert!(shelled.has_shell());
24091 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
24092 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
24093 set_growth_shell(Some(true));
24094 assert!(growth_shell_enabled());
24095 let x = [1.0f32, 1.0];
24098 plain.scores(&x, &mut a);
24099 shelled.scores(&x, &mut b);
24100 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
24101 assert!(a[2] == 0.0 && a[3] == 0.0);
24102 let xo = [3.0f32, 0.0];
24105 plain.scores(&xo, &mut a);
24106 shelled.scores(&xo, &mut b);
24107 assert_eq!(a[2], -6.0);
24108 assert_eq!(a[3], -6.0);
24109 assert_eq!(
24110 bits(&a[..3]),
24111 bits(&b[..3]),
24112 "trunk rows + the boundary row"
24113 );
24114 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
24115 assert_eq!(shelled.effective_shell(4), shelled.shell);
24116 set_growth_shell(Some(false));
24119 assert!(!growth_shell_enabled());
24120 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
24121 shelled.scores(&xo, &mut b);
24122 assert_eq!(bits(&a), bits(&b));
24123 set_growth_shell(None);
24124 let short = Resonance {
24127 shell: vec![f32::INFINITY, f32::INFINITY],
24128 ..shelled
24129 };
24130 set_growth_shell(Some(true));
24131 short.scores(&xo, &mut b);
24132 assert_eq!(bits(&a), bits(&b));
24133 set_growth_shell(None);
24134 }
24135
24136 #[test]
24140 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
24141 let zero = || DenseFfn {
24142 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24143 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24144 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24145 act: Act::Silu,
24146 down_t: None,
24147 segs: Vec::new(),
24148 };
24149 let moe = |sigmoid: bool| MoeFfn {
24150 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24151 experts: vec![zero(), zero(), zero(), zero()],
24152 top_k: 1,
24153 norm_topk_prob: true,
24154 router_sigmoid: sigmoid,
24155 sigmoid_denom_eps: 1e-6,
24156 expert_bias: None,
24157 routed_scaling: 1.0,
24158 route_tau: None,
24159 shared: None,
24160 stats: std::cell::RefCell::new(Vec::new()),
24161 act_sq: std::cell::RefCell::new(Vec::new()),
24162 act_rows: std::cell::RefCell::new(Vec::new()),
24163 mask: None,
24164 per_expert_scale: None,
24165 router_input_norm: false,
24166 resonance: None,
24167 grown: Vec::new(),
24168 };
24169 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24170 for sigmoid in [false, true] {
24171 let m = moe(sigmoid);
24172 let (idx, p, wsum) = moe_route(&logits, &m, None);
24173 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24174 assert_eq!(p[1], 0.0);
24175 assert_eq!(p[3], 0.0);
24176 assert!(p[2] > p[0] && p[0] > 0.0);
24177 assert!(p.iter().all(|v| v.is_finite()));
24178 let w = p[2] / wsum;
24179 if sigmoid {
24180 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24182 } else {
24183 assert_eq!(
24184 w, 1.0,
24185 "softmax: the renormalized top-1 weight is exactly 1"
24186 );
24187 }
24188 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24191 assert_eq!(idx, vec![2]);
24192 }
24193 let m = moe(false);
24195 let (idx, _, _) = moe_route(
24196 &[
24197 f32::NEG_INFINITY,
24198 f32::NEG_INFINITY,
24199 f32::NEG_INFINITY,
24200 -9.0,
24201 ],
24202 &m,
24203 None,
24204 );
24205 assert_eq!(idx, vec![3]);
24206 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
24209 assert_eq!(idx, vec![0]);
24210 assert!(p.iter().all(|&v| v == 0.25));
24211 assert!(wsum.is_finite() && wsum > 0.0);
24212 }
24213
24214 #[cfg(feature = "gpu")]
24220 #[test]
24221 fn minimax_device_route_matches_host_sigmoid_contract() {
24222 match crate::gpu_wgpu::selected_and_up() {
24223 None => {
24224 crate::gpu_wgpu::skip_or_fail(module_path!());
24225 return;
24226 }
24227 Some(false) => {
24235 eprintln!(
24236 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24237 module_path!()
24238 );
24239 return;
24240 }
24241 Some(true) => {}
24242 }
24243 let n = 128usize;
24244 let top_k = 4usize;
24245 let logits: Vec<f32> = (0..n)
24246 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24247 .collect();
24248 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24249 let m = MoeFfn {
24250 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24251 experts: Vec::new(),
24252 top_k,
24253 norm_topk_prob: true,
24254 router_sigmoid: true,
24255 sigmoid_denom_eps: 1e-20,
24256 expert_bias: Some(bias.clone()),
24257 routed_scaling: 2.0,
24258 route_tau: None,
24259 shared: None,
24260 stats: std::cell::RefCell::new(Vec::new()),
24261 act_sq: std::cell::RefCell::new(Vec::new()),
24262 act_rows: std::cell::RefCell::new(Vec::new()),
24263 mask: None,
24264 per_expert_scale: None,
24265 router_input_norm: false,
24266 resonance: None,
24267 grown: Vec::new(),
24268 };
24269 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24270 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24271
24272 for shared in [Some(n as u32), None] {
24273 let mut gi = Vec::new();
24274 let mut gw = Vec::new();
24275 assert!(
24276 crate::gpu_wgpu::minimax_moe_route_for_test(
24277 &logits,
24278 Some(&bias),
24279 top_k,
24280 2.0,
24281 shared,
24282 &mut gi,
24283 &mut gw,
24284 ),
24285 "the device did not come up — the test checked nothing"
24286 );
24287 let routed = if shared.is_some() { top_k } else { gi.len() };
24288 assert_eq!(
24289 &gi[..routed],
24290 &ci[..],
24291 "different experts or order (shared={shared:?})"
24292 );
24293 let d = cw
24294 .iter()
24295 .zip(&gw[..routed])
24296 .map(|(a, b)| (a - b).abs())
24297 .fold(0.0f32, f32::max);
24298 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24299 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24300 if let Some(s) = shared {
24301 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24302 assert_eq!(gw[top_k], 1.0, "shared weight");
24303 }
24304 }
24305 }
24306
24307 #[cfg(feature = "gpu")]
24312 #[test]
24313 fn minimax_device_cold_weights_match_host_contract() {
24314 match crate::gpu_wgpu::selected_and_up() {
24315 None => {
24316 crate::gpu_wgpu::skip_or_fail(module_path!());
24317 return;
24318 }
24319 Some(false) => {
24327 eprintln!(
24328 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24329 module_path!()
24330 );
24331 return;
24332 }
24333 Some(true) => {}
24334 }
24335 let n = 128usize;
24336 let top_k = 4usize;
24337 let logits: Vec<f32> = (0..n)
24338 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24339 .collect();
24340 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24341 let m = MoeFfn {
24342 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24343 experts: Vec::new(),
24344 top_k,
24345 norm_topk_prob: true,
24346 router_sigmoid: true,
24347 sigmoid_denom_eps: 1e-20,
24348 expert_bias: Some(bias.clone()),
24349 routed_scaling: 2.0,
24350 route_tau: None,
24351 shared: None,
24352 stats: std::cell::RefCell::new(Vec::new()),
24353 act_sq: std::cell::RefCell::new(Vec::new()),
24354 act_rows: std::cell::RefCell::new(Vec::new()),
24355 mask: None,
24356 per_expert_scale: None,
24357 router_input_norm: false,
24358 resonance: None,
24359 grown: Vec::new(),
24360 };
24361 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24362 assert_eq!(ci.len(), top_k);
24363
24364 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24366 let mut cold = Vec::new();
24367 let mut idx = Vec::new();
24368 let mut dw = Vec::new();
24369 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24370 &logits,
24371 Some(&bias),
24372 top_k,
24373 2.0,
24374 &resident,
24375 n as u32,
24376 &mut cold,
24377 &mut idx,
24378 &mut dw,
24379 ));
24380 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24381 for (r, &e) in ci.iter().enumerate() {
24386 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24387 let host = p[e] / wsum;
24388 let d = (dw[r] - host).abs();
24389 assert!(
24390 d <= 1e-6 * host.abs().max(1.0),
24391 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24392 dw[r]
24393 );
24394 }
24395 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24396 assert_eq!(dw[top_k], 1.0, "shared weight");
24397
24398 let mut remap = resident.clone();
24400 remap[ci[0]] = u32::MAX;
24401 remap[ci[2]] = u32::MAX;
24402 let mut cold = Vec::new();
24403 let mut idx2 = Vec::new();
24404 let mut dw2 = Vec::new();
24405 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24406 &logits,
24407 Some(&bias),
24408 top_k,
24409 2.0,
24410 &remap,
24411 n as u32,
24412 &mut cold,
24413 &mut idx2,
24414 &mut dw2,
24415 ));
24416 let mut resident_weight_checked = 0usize;
24424 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24425 if dw2[r] == 0.0 {
24426 continue; }
24428 let e = slot as usize;
24429 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24430 let host = p[e] / wsum;
24431 let d = (dw2[r] - host).abs();
24432 assert!(
24433 d <= 1e-6 * host.abs().max(1.0),
24434 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24435 );
24436 resident_weight_checked += 1;
24437 }
24438 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24439 println!("partial bank: checked {resident_weight_checked} resident weights");
24440 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24441 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24442 ids.sort_unstable();
24443 let mut want = vec![ci[0], ci[2]];
24444 want.sort_unstable();
24445 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24446 for &(e, w) in &cold {
24447 let host = p[e] / wsum;
24448 let d = (w - host).abs();
24449 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24450 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24451 }
24452 }
24453
24454 #[test]
24459 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24460 let zero = || DenseFfn {
24461 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24462 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24463 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24464 act: Act::Silu,
24465 down_t: None,
24466 segs: Vec::new(),
24467 };
24468 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24469 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24470 experts: vec![zero(), zero(), zero()],
24471 top_k: 1,
24472 norm_topk_prob: norm_topk,
24473 router_sigmoid: false,
24474 sigmoid_denom_eps: 1e-6,
24475 expert_bias: None,
24476 routed_scaling: 1.0,
24477 route_tau: None,
24478 shared: None,
24479 stats: std::cell::RefCell::new(Vec::new()),
24480 act_sq: std::cell::RefCell::new(Vec::new()),
24481 act_rows: std::cell::RefCell::new(Vec::new()),
24482 mask: None,
24483 per_expert_scale: None,
24484 router_input_norm: false,
24485 resonance: resonant.then(|| Resonance {
24486 mu: vec![0.0; 6],
24487 u: Vec::new(),
24488 k: 0,
24489 bias: vec![0.0; 3],
24490 shell: Vec::new(),
24491 }),
24492 grown: Vec::new(),
24493 };
24494 let lo = -0.1f32;
24497 let hi = f32::from_bits(lo.to_bits() - 1);
24498 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24499 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24500 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24502 assert_eq!(
24503 idx,
24504 vec![0],
24505 "softmax tie → lower index (the gated contract)"
24506 );
24507 for norm in [true, false] {
24510 let m = moe(true, norm);
24511 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24512 assert_eq!(idx, vec![1], "norm_topk {norm}");
24513 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24514 assert_eq!(p[1] / wsum, 1.0);
24515 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24518 assert_eq!(idx, vec![0]);
24519 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24521 assert_eq!(idx, vec![2]);
24522 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24523 assert_eq!(idx, vec![0]);
24524 assert_eq!(p[0] / wsum, 1.0);
24525 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24528 assert_eq!(idx, vec![0]);
24529 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24530 }
24531 }
24532}