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;
18#[path = "pipeline_masked.rs"]
20pub mod masked;
21#[path = "pipeline_speech.rs"]
22pub mod speech;
23use crate::kv_cache::KvCache;
24use crate::linear_core::{
25 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
26 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
27 vmf_phase_pair,
28};
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
32use crate::tokenizer::Tokenizer;
33use cortiq_core::mask::TaskMask;
34use cortiq_core::types::NormStyle;
35
36pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
37 std::sync::atomic::AtomicBool::new(false);
38
39#[derive(Clone)]
46struct GraphMoeBank {
47 experts: Vec<(usize, usize, usize)>,
48 q4tp: Option<bool>,
49 gu_q2: Option<bool>,
50 q82: Option<bool>,
51 act: Option<crate::gpu::GraphAct>,
52 model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
53}
54
55type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
61
62thread_local! {
67 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
68}
69
70struct ExactMtpCalibrationGuard;
71
72impl ExactMtpCalibrationGuard {
73 fn enter() -> Self {
74 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
75 Self
76 }
77}
78
79impl Drop for ExactMtpCalibrationGuard {
80 fn drop(&mut self) {
81 EXACT_MTP_CALIBRATION.with(|depth| {
82 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
83 depth.set(depth.get().saturating_sub(1));
84 });
85 }
86}
87
88fn exact_mtp_calibration_active() -> bool {
89 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
90}
91
92fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
96 logits.resize(vocab_size, f32::NEG_INFINITY);
97}
98
99struct ForwardScratch {
103 n1: Vec<f32>,
104 n2: Vec<f32>,
105 p1: Vec<f32>,
106 p2: Vec<f32>,
107}
108
109impl ForwardScratch {
110 fn new(hidden: usize) -> Self {
111 Self {
112 n1: vec![0.0; hidden],
113 n2: vec![0.0; hidden],
114 p1: vec![0.0; hidden],
115 p2: vec![0.0; hidden],
116 }
117 }
118}
119
120pub struct Pipeline {
122 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
127 pub tokenizer: std::sync::Arc<Tokenizer>,
130 pub kv_cache: KvCache,
131 pub sampler_config: SamplerConfig,
132 pub weights: PipelineWeights,
133 pub hidden_size: usize,
134 pub intermediate_size: usize,
135 pub num_heads: usize,
136 pub num_kv_heads: usize,
137 pub head_dim: usize,
138 pub num_layers: usize,
140 pub physical_layers: usize,
142 pub loop_final_norm: bool,
144 pub layer_map: Option<Vec<usize>>,
147 pub mhc: Option<Box<crate::sensenova::Mhc>>,
149 mhc_r: Vec<f32>,
151 pub mrope_sections: Option<[usize; 3]>,
155 pub prompt_mrope: Option<Vec<[i64; 3]>>,
156 pub vocab_size: usize,
157 pub rms_eps: f64,
158 pub rope_base: f32,
159 pub norm_style: NormStyle,
160 pub rotary_dim: usize,
162 pub attention_heads_per_layer: Option<Vec<usize>>,
164 pub kv_heads_per_layer: Option<Vec<usize>>,
169 pub v_head_dim: Option<usize>,
174 pub layer_dump: Option<std::path::PathBuf>,
187 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
190 pub(crate) mimo_moe: crate::mimo_moe::Slot,
193 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
198 pub vmf_cfg: Option<VmfPhaseCfg>,
200 pub gdn_cfg: Option<GdnCfg>,
202 pub logit_multiplier: Option<f32>,
204 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
209 graph_failed: std::sync::atomic::AtomicBool,
214 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
217 pub kv_history: Vec<u32>,
222 pub kv_history_device: bool,
225 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
227 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
230 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
233 pub dsv4: Option<
237 Box<(
238 crate::dsv4::Dsv4Globals,
239 Vec<crate::dsv4::Dsv4Layer>,
240 crate::dsv4::Dsv4Cfg,
241 crate::dsv4::Dsv4State,
242 )>,
243 >,
244 pub dsv41: Option<
248 Box<(
249 crate::dsv41::Dsv41Globals,
250 Vec<crate::dsv41::Dsv41Layer>,
251 crate::dsv41::Dsv41Cfg,
252 crate::dsv41::Dsv41State,
253 )>,
254 >,
255 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
257 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
259 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
264 clover_stats: clover::CloverStats,
265 pub qwen4_exp: Option<
266 Box<(
267 crate::qwen4_exp::Globals,
268 Vec<crate::qwen4_exp::Layer>,
269 crate::qwen4_exp::Cfg,
270 crate::qwen4_exp::State,
271 )>,
272 >,
273 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
277 pub dspark: Option<crate::dsv4::DsparkState>,
279 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
282 pub dspark_hist: Vec<usize>,
284 pub dspark_real: Vec<u32>,
288 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
292 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
294 pub dspark_draft_ns: u128,
298 m3_winners: Vec<Vec<u32>>,
300 m3_counts: Vec<u32>,
302 m3_n_exp: usize,
303 m3_tokens: u64,
304 m3_prewarmed: bool,
305 pub short_conv_cfg: Option<ShortConvCfg>,
308 pub mtp: Option<MtpModule>,
310 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
314 verify_exact_moe: bool,
317 pub speculative: bool,
319 pub ignore_eos: bool,
325 pub draft_full_streak: u32,
331 pub spec_k_adapt: Option<usize>,
339 pub spec_acc_ewma: f32,
341 rng: SplitMix64,
342 sampler_scratch: SamplerScratch,
343 spec_forced: Option<u32>,
349 spec_q: Vec<Vec<f32>>,
350 spec_p: Vec<f32>,
351 spec_res: Vec<f32>,
352 spec_qs: Vec<sampler::Sparse>,
354 spec_ps: sampler::Sparse,
355 spec_ress: sampler::Sparse,
356 mtp_graph_mode: Option<bool>,
363 #[cfg(target_os = "macos")]
366 metal_verify: Option<MetalVerifyPending>,
367 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
371 ws: ForwardScratch,
375 pool: Option<std::sync::Arc<Pool>>,
377 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
381 pub(crate) dyn_force_f32: bool,
383 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
388 pub(crate) dyn_active: Option<usize>,
394 pub(crate) dyn_blend_loaded: bool,
398 pub(crate) dyn_phi_layer: Option<usize>,
401 dyn_phi_ema: Vec<f32>,
403 dyn_phi_seen: usize,
404 pub dyn_router: Option<crate::swarm::DynRouter>,
407 o1_cfg: Option<crate::nystrom::O1Cfg>,
410 o1_epoch: u64,
413 o1_flags: Vec<bool>,
415 trace: bool,
418 calib_temp: f32,
421 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
423 graph_kv_id: u64,
424 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
427 graph_want_logits: bool,
428 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
432 graph_head_required: bool,
433 graph_logits: Option<Vec<f32>>,
436 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
439 graph_refused: std::sync::atomic::AtomicBool,
445 pub embed_multiplier: f32,
447 pub attn_scale: f32,
450 pub swa: Option<(usize, usize)>,
453 pub swa_trim: Option<(usize, usize)>,
460 swa_trim_anchor: usize,
464 pub chunk_min_rows: usize,
468 pub sliding_layers: Option<Vec<bool>>,
471 pub no_rope_layers: Option<Vec<bool>>,
476 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
482 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
485 pub kv_prefix: KvPrefix,
489 pub last_prefill_tokens: usize,
492 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
495 pub rotary_dim_local: Option<usize>,
496 pub rope_scale: f32,
497 pub rope_scale_local: f32,
498 pub global_attn: Option<(usize, usize)>,
501 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
504 pub attn_v_norm: bool,
506 pub qk_norm_after_rope: bool,
508 pub proj_gate_sigmoid: bool,
511 pub final_softcap: Option<f32>,
513 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
517 pub attn_softcap: f32,
519 confidence_on: bool,
523 #[cfg(test)]
526 nll_test_fail_at: Option<usize>,
527 #[cfg(test)]
530 nll_test_force_serial: bool,
531}
532
533#[cfg(target_os = "macos")]
534impl Drop for Pipeline {
535 fn drop(&mut self) {
536 let _ = crate::gpu_metal::wait_replay();
538 crate::gpu::kv_mirror_drop(self.graph_kv_id);
539 }
540}
541
542#[cfg(not(target_os = "macos"))]
543impl Drop for Pipeline {
544 fn drop(&mut self) {
545 crate::gpu::graph_kv_reset(self.graph_kv_id);
549 }
550}
551
552pub struct PipelineWeights {
557 pub embed_tokens: QTensor,
559 pub layers: Vec<LayerWeights>,
561 pub lm_head: QTensor,
563 pub final_norm: Vec<f32>,
565}
566
567pub struct LayerWeights {
569 pub input_norm: Vec<f32>,
570 pub post_norm: Vec<f32>,
573 pub attn_out_norm: Option<Vec<f32>>,
576 pub layer_scale: Option<f32>,
578 pub ffn_out_norm: Option<Vec<f32>>,
581 pub ffn: FfnKind,
582 pub attn: AttnKind,
583}
584
585#[derive(Clone, Copy, PartialEq, Debug, Default)]
589pub enum Act {
590 #[default]
591 Silu,
592 GeluTanh,
593 Gelu,
595 Situ {
598 beta: f32,
599 linear_beta: f32,
600 },
601 SwiGluOai {
606 alpha: f32,
607 limit: f32,
608 },
609}
610
611impl Act {
612 pub fn from_arch(name: &str) -> Self {
613 if name == "gelu_tanh" {
614 Self::GeluTanh
615 } else if name == "gelu" {
616 Self::Gelu
617 } else {
618 Self::Silu
619 }
620 }
621
622 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
624 match arch.hidden_act.as_str() {
625 "situ" => Self::Situ {
626 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
627 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
628 },
629 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
634 alpha: 1.702,
635 limit: 7.0,
636 },
637 other => Self::from_arch(other),
638 }
639 }
640
641 #[inline]
646 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
647 Self::SwiGluOai { alpha, limit }
648 }
649
650 #[inline]
651 pub fn apply(self, x: f32) -> f32 {
652 match self {
653 Self::Silu => inference::silu(x),
654 Self::GeluTanh => inference::gelu_tanh(x),
655 Self::Gelu => inference::gelu_erf(x),
656 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
657 Self::SwiGluOai { alpha, limit } => {
658 let gate = x.min(limit);
659 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
660 }
661 }
662 }
663
664 #[inline]
667 pub fn combine(self, g: f32, u: f32) -> f32 {
668 match self {
669 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
670 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
671 }
672 Self::SwiGluOai { alpha, limit } => {
673 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
674 }
675 _ => self.apply(g) * u,
676 }
677 }
678
679 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
683 match self {
684 Self::Silu => Some(crate::gpu::GraphAct::Silu),
685 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
686 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
687 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
688 }
689 }
690
691 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
697 self.moe_job_graph_contract().map(|(act, _)| act)
698 }
699
700 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
706 match self {
707 Self::SwiGluOai { alpha, limit }
708 if alpha.to_bits() == 1.702f32.to_bits()
709 && limit.to_bits() == 7.0f32.to_bits() =>
710 {
711 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
712 }
713 _ => self.graph_act().map(|act| (act, 0.0)),
714 }
715 }
716}
717
718pub struct DenseFfn {
720 pub gate_proj: QTensor,
721 pub up_proj: QTensor,
722 pub down_proj: QTensor,
723 pub act: Act,
725 pub down_t: Option<QTensor>,
731 pub segs: Vec<FfnSeg>,
738}
739
740pub struct FfnSeg {
745 pub gate: QTensor,
746 pub up: QTensor,
747 pub down: QTensor,
748 pub start: usize,
749 pub width: usize,
750}
751
752pub enum FfnKind {
755 Dense(DenseFfn),
756 Moe(MoeFfn),
760 DenseMoe(Box<DenseMoeFfn>),
767}
768
769pub struct DenseMoeFfn {
771 pub dense: DenseFfn,
772 pub moe: MoeFfn,
773 pub post_norm_1: Vec<f32>,
775 pub pre_norm_2: Vec<f32>,
778 pub post_norm_2: Vec<f32>,
780}
781
782pub struct MoeFfn {
783 pub router: QTensor,
785 pub experts: Vec<DenseFfn>,
786 pub top_k: usize,
787 pub norm_topk_prob: bool,
788 pub router_sigmoid: bool,
791 pub sigmoid_denom_eps: f32,
797 pub expert_bias: Option<Vec<f32>>,
801 pub routed_scaling: f32,
804 pub route_tau: Option<f32>,
810 pub shared: Option<(DenseFfn, Option<QTensor>)>,
813 pub stats: std::cell::RefCell<Vec<u64>>,
817 pub act_sq: std::cell::RefCell<Vec<f64>>,
824 pub act_rows: std::cell::RefCell<Vec<f32>>,
830 pub mask: Option<Vec<bool>>,
835 pub per_expert_scale: Option<Vec<f32>>,
838 pub router_input_norm: bool,
842 pub resonance: Option<Resonance>,
846 pub grown: Vec<GrownExpert>,
852}
853
854#[derive(Debug, Clone, PartialEq, Eq)]
856pub struct GrownExpert {
857 pub record: String,
859 pub record_index: usize,
861 pub layer: usize,
862 pub expert: usize,
866}
867
868pub struct Resonance {
870 pub mu: Vec<f32>,
872 pub u: Vec<f32>,
874 pub k: usize,
875 pub bias: Vec<f32>,
877 pub shell: Vec<f32>,
883}
884
885static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
888
889pub fn growth_shell_enabled() -> bool {
894 use std::sync::atomic::Ordering;
895 match GROWTH_SHELL.load(Ordering::Relaxed) {
896 1 => true,
897 2 => false,
898 _ => {
899 let off = std::env::var("CMF_GROWTH_SHELL")
900 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
901 .unwrap_or(false);
902 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
903 !off
904 }
905 }
906}
907
908pub fn set_growth_shell(on: Option<bool>) {
913 GROWTH_SHELL.store(
914 match on {
915 Some(true) => 1,
916 Some(false) => 2,
917 None => 0,
918 },
919 std::sync::atomic::Ordering::Relaxed,
920 );
921}
922
923impl Resonance {
924 pub fn has_shell(&self) -> bool {
926 self.shell.iter().any(|s| s.is_finite())
927 }
928
929 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
932 let mut out = vec![f32::INFINITY; ne];
933 if growth_shell_enabled() {
934 for (o, s) in out.iter_mut().zip(&self.shell) {
935 *o = *s;
936 }
937 }
938 out
939 }
940
941 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
946 let h = x.len();
947 let ne = out.len();
948 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
949 for e in 0..ne {
950 let mu = &self.mu[e * h..(e + 1) * h];
951 let mut d2 = 0.0f32;
952 for j in 0..h {
953 let d = x[j] - mu[j];
954 d2 += d * d;
955 }
956 let mut proj = 0.0f32;
957 for i in 0..self.k {
958 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
959 let mut p = 0.0f32;
960 for j in 0..h {
961 p += (x[j] - mu[j]) * u[j];
962 }
963 proj += p * p;
964 }
965 let err = d2 - proj;
966 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
967 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
968 out[e] = f32::NEG_INFINITY;
969 }
970 }
971 }
972}
973
974pub enum AttnKind {
977 Full {
979 wq: QTensor,
980 wk: QTensor,
981 wv: QTensor,
982 wo: QTensor,
983 q_norm: Option<Vec<f32>>,
984 k_norm: Option<Vec<f32>>,
985 output_gate: bool,
986 softplus_gate: Option<(QTensor, bool)>,
990 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
992 },
993 Linear(VmfPhaseWeights),
995 LinearGdn(GdnWeights),
997 ShortConv(ShortConvWeights),
1000 Mla(Box<MlaWeights>),
1008 Kda(Box<crate::linear_core::KdaWeights>),
1012 Bounded(Box<crate::bounded::BoundedWeights>),
1017}
1018
1019pub struct MlaWeights {
1021 pub q_proj: QTensor,
1025 pub q_a: Option<QTensor>,
1028 pub q_a_norm: Option<Vec<f32>>,
1029 pub kv_a: QTensor,
1031 pub kv_a_norm: Vec<f32>,
1033 pub kv_b: QTensor,
1035 pub o_proj: QTensor,
1037 pub nh: usize,
1038 pub qk_rope: usize,
1039 pub qk_nope: usize,
1040 pub v_dim: usize,
1041 pub lora: usize,
1042 pub scale: f32,
1044 pub nope: bool,
1046}
1047
1048pub struct MtpModule {
1053 pub enorm: Vec<f32>,
1054 pub hnorm: Vec<f32>,
1055 pub eh_proj: QTensor,
1057 pub layer: LayerWeights,
1058 pub final_norm: Vec<f32>,
1059 pub kv: crate::kv_cache::LayerKvCache,
1060}
1061
1062const MTP_PROBE_FFN_ROWS: usize = 128;
1071
1072#[derive(Clone, Debug, PartialEq)]
1077pub struct MtpFfnProbe {
1078 pub mass: Vec<f64>,
1079 pub squared_mass: Vec<f64>,
1080 pub observed_positions: usize,
1081}
1082
1083#[cfg(target_os = "macos")]
1090enum MetalRowsItem<'a> {
1091 Gdn {
1092 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1093 first: usize,
1094 },
1095 Attn {
1096 l: crate::gpu_metal::AttnGpuLayer<'a>,
1097 li: usize,
1098 q_norm: Option<&'a [f32]>,
1099 k_norm: Option<&'a [f32]>,
1100 output_gate: bool,
1101 },
1102}
1103
1104#[cfg(target_os = "macos")]
1105struct MetalVerifyPending {
1106 graph: crate::gpu_metal::VerifyGraph,
1107 gdn_layers: Vec<usize>,
1108 attn_layers: Vec<(usize, usize)>,
1109}
1110
1111#[cfg(target_os = "macos")]
1115struct MetalWarmPending {
1116 graph: crate::gpu_metal::VerifyGraph,
1117 cpu_stored: usize,
1118 b: usize,
1119}
1120
1121#[cfg(target_os = "macos")]
1122enum MetalRowsRun {
1123 Declined,
1125 Failed,
1128 Completed(MetalVerifyPending),
1129}
1130
1131#[cfg(target_os = "macos")]
1132enum MetalPrefillOutcome {
1133 Declined,
1134 Failed,
1135 Completed(Vec<f32>),
1136}
1137
1138#[cfg(target_os = "macos")]
1139enum MetalBatchNllOutcome {
1140 Declined,
1141 Failed(String),
1142 Completed(f64, usize),
1143}
1144
1145#[derive(Clone, Copy)]
1151enum SpecTrial {
1152 Spec {
1153 t0: std::time::Instant,
1154 gen0: usize,
1155 rounds: usize,
1156 },
1157 Plain {
1158 t0: std::time::Instant,
1159 gen0: usize,
1160 },
1161 Decided {
1162 spec: bool,
1163 recheck_at: usize,
1164 },
1165}
1166
1167pub(crate) fn spec_time_level() -> u8 {
1171 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1172 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1173 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1174 Err(_) => 0,
1175 })
1176}
1177
1178fn graph_moe_bank_on() -> bool {
1181 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1182 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1183}
1184
1185fn graph_spec_wanted_by_policy(
1193 spec_env: Option<&str>,
1194 spec_default_ok: bool,
1195 penalized: bool,
1196 metal_wgpu: bool,
1197 native_metal: bool,
1198) -> bool {
1199 match spec_env {
1200 Some("0") => false,
1201 Some("1") => true,
1206 Some(_) => !native_metal,
1207 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1210 }
1211}
1212
1213struct SpecStampLog {
1219 t_last: std::time::Instant,
1220 items: Vec<(&'static str, f32)>,
1221}
1222
1223static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1224
1225pub(crate) fn spec_stamp(name: &'static str) {
1226 if spec_time_level() == 0 {
1227 return;
1228 }
1229 if let Ok(mut g) = SPEC_STAMPS.lock() {
1230 if let Some(log) = g.as_mut() {
1231 let now = std::time::Instant::now();
1232 log.items
1233 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1234 log.t_last = now;
1235 }
1236 }
1237}
1238
1239fn spec_stamps_begin() {
1240 if spec_time_level() == 0 {
1241 return;
1242 }
1243 if let Ok(mut g) = SPEC_STAMPS.lock() {
1244 *g = Some(SpecStampLog {
1245 t_last: std::time::Instant::now(),
1246 items: Vec::with_capacity(64),
1247 });
1248 }
1249}
1250
1251fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1252 SPEC_STAMPS
1253 .lock()
1254 .ok()
1255 .and_then(|mut g| g.take())
1256 .map(|l| l.items)
1257 .unwrap_or_default()
1258}
1259
1260fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1263 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1264 for &(n, ms) in items {
1265 match agg.iter_mut().find(|e| e.0 == n) {
1266 Some(e) => {
1267 e.1 += ms;
1268 e.2 += 1;
1269 }
1270 None => agg.push((n, ms, 1)),
1271 }
1272 }
1273 let mut s = String::with_capacity(agg.len() * 16);
1274 for (n, ms, k) in agg {
1275 if k > 1 {
1276 s.push_str(&format!("{n} {ms:.1}/{k} "));
1277 } else {
1278 s.push_str(&format!("{n} {ms:.1} "));
1279 }
1280 }
1281 s
1282}
1283
1284#[derive(Default, Clone, Copy)]
1306struct SpecMon {
1307 round_ms: f64,
1308 tokens: f64,
1309 plain_ms: f64,
1310 n: u32,
1311 fails: u32,
1312 zero_accept_rounds: u8,
1317 metal: bool,
1318}
1319
1320const SPEC_PROXY_TOKENS: f64 = 3.5;
1323const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1326
1327impl SpecMon {
1328 fn round(&mut self, dt_ms: f64, produced: usize) {
1329 if produced <= 1 {
1330 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1331 } else {
1332 self.zero_accept_rounds = 0;
1333 }
1334 self.n += 1;
1335 if self.n == 1 {
1336 return; }
1338 let a = if self.n == 2 { 1.0 } else { 0.3 };
1339 self.round_ms += a * (dt_ms - self.round_ms);
1340 self.tokens += a * (produced as f64 - self.tokens);
1341 }
1342 fn pays(&self) -> bool {
1343 if self.plain_ms > 0.0 {
1344 self.tokens * self.plain_ms > self.round_ms * 1.03
1345 } else {
1346 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1347 }
1348 }
1349 fn repeated_zero_acceptance(&self) -> bool {
1350 self.zero_accept_rounds >= 2
1351 }
1352 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1354 let n = generated.saturating_sub(gen0);
1355 if n >= 8 {
1356 return true;
1357 }
1358 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1359 }
1360}
1361
1362pub const KV_PREFIX_TAIL: usize = 128;
1365
1366#[derive(Debug, Clone, Default)]
1372pub struct KvPrefix {
1373 len: usize,
1374 hash: u64,
1375 tail: Vec<u32>,
1376 device: bool,
1381}
1382
1383impl KvPrefix {
1384 #[inline]
1385 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1386 for &id in ids {
1387 h ^= id as u64;
1388 h = h.wrapping_mul(0x100000001b3);
1389 h ^= h >> 29;
1390 }
1391 h
1392 }
1393
1394 pub fn clear(&mut self) {
1395 self.len = 0;
1396 self.hash = 0xcbf29ce484222325;
1397 self.tail.clear();
1398 self.device = false;
1399 }
1400
1401 pub fn on_device(&self) -> bool {
1403 self.device
1404 }
1405
1406 pub fn set_on_device(&mut self, device: bool) {
1408 self.device = device;
1409 }
1410
1411 pub fn len(&self) -> usize {
1413 self.len
1414 }
1415
1416 pub fn is_empty(&self) -> bool {
1417 self.len == 0
1418 }
1419
1420 pub fn tail_len(&self) -> usize {
1422 self.tail.len()
1423 }
1424
1425 pub fn set(&mut self, ids: &[u32]) {
1427 self.clear();
1428 self.extend(ids);
1429 }
1430
1431 pub fn extend(&mut self, more: &[u32]) {
1433 if self.len == 0 && self.hash == 0 {
1434 self.hash = 0xcbf29ce484222325;
1435 }
1436 self.hash = Self::fold(self.hash, more);
1437 self.len += more.len();
1438 if more.len() >= KV_PREFIX_TAIL {
1439 self.tail.clear();
1440 self.tail
1441 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1442 } else {
1443 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1444 self.tail.drain(..drop);
1445 self.tail.extend_from_slice(more);
1446 }
1447 }
1448
1449 pub fn extension(&self, ids: &[u32]) -> usize {
1453 if self.len == 0 || ids.len() <= self.len {
1454 return 0;
1455 }
1456 let t = self.tail.len();
1457 if ids[self.len - t..self.len] != self.tail[..] {
1458 return 0;
1459 }
1460 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1461 return 0;
1462 }
1463 self.len
1464 }
1465}
1466
1467pub struct GenerateResult {
1469 pub text: String,
1470 pub token_ids: Vec<u32>,
1471 pub prompt_tokens: usize,
1472 pub tokens_generated: usize,
1473 pub finish_reason: String,
1474 pub mtp_drafted: usize,
1476 pub mtp_accepted: usize,
1477 pub token_confidence: Vec<f32>,
1482 pub traces: Vec<TokenTrace>,
1485}
1486
1487#[derive(Clone, Debug)]
1492pub struct TokenTrace {
1493 pub t: usize,
1495 pub token_id: u32,
1497 pub confidence: f32,
1499 pub active_skill: Option<String>,
1501 pub recon: Option<f32>,
1505 pub switched: bool,
1508}
1509
1510#[cfg_attr(not(test), allow(dead_code))]
1515fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1516 let t = if temp > 1e-3 { temp } else { 1.0 };
1517 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1518 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1519 if sum > 0.0 {
1520 (((logits[id as usize] - max) / t).exp()) / sum
1521 } else {
1522 0.0
1523 }
1524}
1525
1526fn prefill_batched() -> bool {
1529 std::env::var("CMF_PREFILL")
1530 .map(|v| v != "seq")
1531 .unwrap_or(true)
1532}
1533
1534#[inline]
1538fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
1540 if a.len() != b.len() {
1541 return f64::INFINITY;
1542 }
1543 let (mut d, mut r) = (0f64, 0f64);
1544 for (&x, &y) in a.iter().zip(b) {
1545 let e = (x - y) as f64;
1546 d += e * e;
1547 r += y as f64 * y as f64;
1548 }
1549 (d / r.max(1e-30)).sqrt()
1550}
1551
1552fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1553 let graph_quality = unmasked && prefer_graph;
1554 let fused_head_quality = graph_quality && native_metal;
1555 (graph_quality, fused_head_quality)
1556}
1557
1558#[derive(Clone, Copy)]
1562enum PrefillIn<'a> {
1563 Ids(&'a [u32]),
1564 Hidden(&'a [f32]),
1565}
1566
1567impl Pipeline {
1574 pub(crate) fn can_prefill_batched(&self) -> bool {
1575 #[cfg(test)]
1576 let force_serial = self.nll_test_force_serial;
1577 #[cfg(not(test))]
1578 let force_serial = false;
1579 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1580 }
1581
1582 fn automatic_gpu_prefix(&self) -> Option<usize> {
1585 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1586 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1587 }
1588
1589 pub fn prefill_chunk(&self) -> usize {
1593 let env = env_prefill_chunk();
1594 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1595 return prefill_chunk_rule(env, ChunkHost::here(), false);
1596 }
1597 prefill_chunk_rule(
1598 None,
1599 ChunkHost::Other,
1600 self.chunk_stack_facts().dense_on_discrete(),
1601 )
1602 }
1603
1604 fn per_token_stack(&self) -> bool {
1608 self.g3n.is_some() || self.g4e.is_some()
1609 }
1610
1611 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1612 let plain_dense = !self.weights.layers.is_empty()
1613 && !self.per_token_stack()
1614 && self.dsv4.is_none()
1615 && self.dsv41.is_none()
1616 && self.qwen4_exp.is_none()
1617 && self.weights.layers.iter().all(|lw| {
1618 matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
1623 && matches!(lw.ffn, FfnKind::Dense(_))
1624 });
1625 let gpu_on = crate::gpu::enabled();
1626 ChunkStackFacts {
1627 plain_dense,
1628 discrete: gpu_on && crate::gpu::discrete(),
1629 gpu_on,
1630 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1633 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1634 multi_gpu: self.gpu_plan.is_some(),
1635 o1: self.o1_active(),
1636 }
1637 }
1638}
1639
1640pub fn prefill_chunk() -> usize {
1649 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1650}
1651
1652fn env_prefill_chunk() -> Option<usize> {
1653 std::env::var("CMF_PREFILL_CHUNK")
1654 .ok()
1655 .and_then(|v| v.parse::<usize>().ok())
1656}
1657
1658#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1660enum ChunkHost {
1661 Macos,
1662 Aarch64,
1664 Other,
1666}
1667
1668impl ChunkHost {
1669 fn here() -> Self {
1670 if cfg!(target_os = "macos") {
1671 ChunkHost::Macos
1672 } else if cfg!(target_arch = "aarch64") {
1673 ChunkHost::Aarch64
1674 } else {
1675 ChunkHost::Other
1676 }
1677 }
1678}
1679
1680const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1687
1688fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1694 if let Some(n) = env {
1695 return n.max(1);
1696 }
1697 match host {
1698 ChunkHost::Macos => 512,
1699 ChunkHost::Aarch64 => 256,
1702 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1703 ChunkHost::Other => 48,
1704 }
1705}
1706
1707#[derive(Clone, Copy, Debug, Default)]
1709struct ChunkStackFacts {
1710 plain_dense: bool,
1714 discrete: bool,
1716 gpu_on: bool,
1718 capacity_split: bool,
1720 multi_gpu: bool,
1722 o1: bool,
1724}
1725
1726impl ChunkStackFacts {
1727 fn dense_on_discrete(self) -> bool {
1728 self.plain_dense
1729 && self.discrete
1730 && self.gpu_on
1731 && !self.capacity_split
1732 && !self.multi_gpu
1733 && !self.o1
1734 }
1735}
1736
1737#[inline]
1743fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1744 if end <= start || start >= input_len {
1745 return 0;
1746 }
1747 let rows = (end.min(input_len) - start).min(input_len - start);
1748 if end < input_len {
1749 rows
1750 } else {
1751 rows.saturating_sub(1)
1752 }
1753}
1754
1755pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1757
1758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1760pub(crate) struct ReuseLayer {
1761 pub full: bool,
1764 pub host_rows: usize,
1767 pub device_rows: Option<usize>,
1769 pub device_state: bool,
1771}
1772
1773#[derive(Debug, Clone, PartialEq, Eq)]
1775pub(crate) enum ReusePlan {
1776 Ready,
1778 Pull(Vec<(usize, usize, usize)>),
1781 Fresh,
1783}
1784
1785pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1795 let mut pulls = Vec::new();
1796 for (li, l) in layers.iter().enumerate() {
1797 if !l.full {
1798 if l.device_state {
1799 return ReusePlan::Fresh;
1800 }
1801 continue;
1802 }
1803 if l.host_rows == reuse_from {
1804 continue;
1805 }
1806 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1807 pulls.push((li, l.host_rows, reuse_from));
1808 continue;
1809 }
1810 return ReusePlan::Fresh;
1811 }
1812 if pulls.is_empty() {
1813 ReusePlan::Ready
1814 } else {
1815 ReusePlan::Pull(pulls)
1816 }
1817}
1818
1819impl Pipeline {
1820 fn reset_fresh_mtp_draft_state(&mut self) {
1823 self.draft_full_streak = 0;
1824 }
1825
1826 fn clear_sequence_state(&mut self) {
1834 #[cfg(target_os = "macos")]
1837 let _ = crate::gpu_metal::wait_replay();
1838 self.kv_cache.clear();
1839 self.clear_history();
1842 self.graph_logits = None;
1843 if let Some(b) = &mut self.dsv41 {
1844 b.3.clear();
1845 }
1846 crate::gpu::graph_kv_reset(self.graph_kv_id);
1847 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1852 }
1853
1854 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1861 if self.graph_prefill_preferred() {
1862 return true;
1863 }
1864 let kv_id = self.graph_kv_id;
1865 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1866 .map(|li| {
1867 let full = matches!(
1868 self.weights.layers[self.phys_layer(li)].attn,
1869 AttnKind::Full { .. }
1870 );
1871 ReuseLayer {
1872 full,
1873 host_rows: self.kv_cache.layers[li].pos_len(),
1876 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1877 device_state: crate::gpu::graph_state_resident(kv_id, li),
1878 }
1879 })
1880 .collect();
1881 if layers
1885 .iter()
1886 .all(|l| l.device_rows.is_none() && !l.device_state)
1887 {
1888 return true;
1889 }
1890 let plan = kv_reuse_plan(reuse_from, &layers);
1891 let (what, rows, n) = match &plan {
1892 ReusePlan::Ready => ("host ready", 0, 0),
1893 ReusePlan::Fresh => ("fresh", 0, 0),
1894 ReusePlan::Pull(p) => (
1895 "pull",
1896 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1897 p.len(),
1898 ),
1899 };
1900 let t0 = std::time::Instant::now();
1901 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1902 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1903 eprintln!(
1904 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1905 if ok { "" } else { " (failed → fresh)" },
1906 t0.elapsed().as_secs_f64() * 1e3
1907 );
1908 }
1909 ok
1910 }
1911
1912 fn apply_kv_reuse_plan(
1913 &mut self,
1914 reuse_from: usize,
1915 plan: ReusePlan,
1916 layers: &[ReuseLayer],
1917 ) -> bool {
1918 let kv_id = self.graph_kv_id;
1919 match plan {
1920 ReusePlan::Fresh => return false,
1921 ReusePlan::Ready => {}
1922 ReusePlan::Pull(pulls) => {
1923 let (nkv, hd) = {
1928 let c = &self.kv_cache.layers[pulls[0].0];
1929 (c.num_kv_heads, c.head_dim)
1930 };
1931 let uniform = pulls.iter().all(|&(li, _, _)| {
1932 let c = &self.kv_cache.layers[li];
1933 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1934 });
1935 let batched = if uniform {
1936 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1937 } else {
1938 None
1939 };
1940 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1941 Some(rows) => rows,
1942 None => {
1943 let mut rows = Vec::with_capacity(pulls.len());
1944 for &(li, from, to) in &pulls {
1945 let (lnkv, lhd) = {
1946 let c = &self.kv_cache.layers[li];
1947 (c.num_kv_heads, c.head_dim)
1948 };
1949 let Some((k, v, first_valid)) =
1950 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1951 else {
1952 return false;
1953 };
1954 let need_from = match self.layer_window(li) {
1958 Some(w) => from.max((to + 1).saturating_sub(w)),
1959 None => from,
1960 };
1961 if first_valid > need_from {
1962 return false;
1963 }
1964 rows.push((k, v));
1965 }
1966 rows
1967 }
1968 };
1969 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1970 let cache = &mut self.kv_cache.layers[li];
1971 let row = cache.num_kv_heads * cache.head_dim;
1972 for p in 0..to - from {
1973 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1974 }
1975 if cache.pos_len() != to {
1976 return false;
1977 }
1978 }
1979 }
1980 }
1981 for (li, l) in layers.iter().enumerate() {
1985 if l.full
1986 && l.device_rows.is_some_and(|d| d > reuse_from)
1987 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1988 {
1989 return false;
1990 }
1991 }
1992 true
1993 }
1994
1995 fn finish_generation(
2001 &mut self,
2002 mtp: &mut Option<MtpModule>,
2003 router: &mut Option<crate::swarm::DynRouter>,
2004 clear_sequence: bool,
2005 ) {
2006 if router.is_some() {
2010 let _ = self.set_active_skill(None);
2011 }
2012 #[cfg(target_os = "macos")]
2019 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2020 if clear_sequence {
2021 self.clear_sequence_state();
2022 if let Some(m) = mtp.as_mut() {
2023 m.kv.clear();
2029 }
2030 if let Some(m) = self.mtp.as_mut() {
2031 m.kv.clear();
2035 }
2036 }
2037 self.graph_want_logits = false;
2038 self.graph_head_required = false;
2039 self.graph_logits = None;
2040 self.graph_failed
2041 .store(false, std::sync::atomic::Ordering::Relaxed);
2042 self.cancel
2043 .store(false, std::sync::atomic::Ordering::Relaxed);
2044 self.dyn_router = router.take().or(self.dyn_router.take());
2045 self.mtp = mtp.take().or(self.mtp.take());
2046 self.mtp_graph_mode = None;
2047 self.spec_forced = None;
2048 }
2049
2050 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2054 if self
2055 .graph_failed
2056 .swap(false, std::sync::atomic::Ordering::Relaxed)
2057 {
2058 self.cancel
2059 .store(false, std::sync::atomic::Ordering::Relaxed);
2060 self.clear_sequence_state();
2061 self.graph_logits = None;
2062 self.graph_want_logits = false;
2063 self.graph_head_required = false;
2064 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2065 }
2066 Ok(())
2067 }
2068
2069 #[cfg(target_os = "macos")]
2070 fn fail_metal_graph(&mut self, reason: &str) {
2071 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2072 self.clear_sequence_state();
2073 self.graph_logits = None;
2074 self.graph_failed
2075 .store(true, std::sync::atomic::Ordering::Relaxed);
2076 self.cancel
2077 .store(true, std::sync::atomic::Ordering::Relaxed);
2078 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2079 }
2080
2081 fn nll_begin(&mut self) -> Result<(), String> {
2086 if self
2087 .graph_failed
2088 .swap(false, std::sync::atomic::Ordering::Relaxed)
2089 {
2090 self.cancel
2091 .store(false, std::sync::atomic::Ordering::Relaxed);
2092 self.clear_sequence_state();
2093 self.graph_logits = None;
2094 self.graph_want_logits = false;
2095 self.graph_head_required = false;
2096 return Err("GPU graph failed before NLL scoring".to_string());
2097 }
2098 self.clear_sequence_state();
2099 self.graph_logits = None;
2100 self.graph_want_logits = false;
2101 self.graph_head_required = false;
2102 Ok(())
2103 }
2104
2105 fn nll_end(&mut self) {
2109 self.clear_sequence_state();
2110 self.graph_logits = None;
2111 self.graph_want_logits = false;
2112 self.graph_head_required = false;
2113 self.graph_failed
2114 .store(false, std::sync::atomic::Ordering::Relaxed);
2115 }
2116
2117 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2120 #[cfg(test)]
2121 if self.nll_test_fail_at == Some(pos) {
2122 self.nll_test_fail_at = None;
2123 self.graph_failed
2124 .store(true, std::sync::atomic::Ordering::Relaxed);
2125 self.cancel
2126 .store(true, std::sync::atomic::Ordering::Relaxed);
2127 }
2128 if self
2129 .graph_failed
2130 .swap(false, std::sync::atomic::Ordering::Relaxed)
2131 {
2132 self.cancel
2133 .store(false, std::sync::atomic::Ordering::Relaxed);
2134 self.clear_sequence_state();
2135 self.graph_logits = None;
2136 self.graph_want_logits = false;
2137 return Err(format!(
2138 "GPU graph failed during NLL {phase} at position {pos}"
2139 ));
2140 }
2141 Ok(())
2142 }
2143
2144 #[inline]
2148 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2149 match &self.layer_map {
2150 Some(m) => m[virtual_idx],
2151 None => virtual_idx % self.physical_layers,
2152 }
2153 }
2154
2155 fn onevision2_prefill(&self) -> bool {
2158 self.model.as_ref().is_some_and(|m| m.header.arch.arch_name == "llava_onevision2")
2159 && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2160 }
2161
2162 #[inline]
2165 pub fn mhc_layer(&self, li: usize) -> bool {
2166 self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2167 }
2168
2169 fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2172 let p = self.phys_layer(li);
2173 let m = self.mhc.as_deref().expect("mhc layer");
2174 let n = m.s.streams * m.dim;
2175 if self.mhc_r.len() != n {
2176 self.mhc_r.resize(n, 0.0);
2177 }
2178 if !ffn && m.enters(p) {
2179 m.enter(&mut self.mhc_r, h);
2180 }
2181 m.route(p, ffn, &self.mhc_r, h)
2182 }
2183
2184 fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2187 let p = self.phys_layer(li);
2188 let m = self.mhc.as_deref().expect("mhc layer");
2189 m.merge(&mut self.mhc_r, y, c);
2190 if ffn && m.leaves(p) {
2191 m.leave(&self.mhc_r, h);
2192 }
2193 }
2194
2195 #[inline]
2198 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2199 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2200 }
2201
2202 #[allow(clippy::too_many_arguments)]
2204
2205 #[cfg(target_os = "macos")]
2224 fn graph_prefill_preferred(&self) -> bool {
2225 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2226 if !crate::gpu::enabled_here()
2227 || !graph_force
2228 || std::env::var("CMF_GPU_BLOCK")
2229 .map(|v| v == "0")
2230 .unwrap_or(false)
2231 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2234 {
2235 return false;
2236 }
2237 self.weights
2238 .layers
2239 .iter()
2240 .any(|lw| {
2241 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2242 })
2243 }
2244
2245 #[cfg(not(target_os = "macos"))]
2252 fn batch_prefix_prefill(&self) -> bool {
2253 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2254 Ok("0") => return false,
2255 Ok("1") => true,
2256 _ => false,
2257 };
2258 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2259 && crate::gpu::enabled_here()
2260 && !self.graph_refused()
2261 && (forced || self.graph_attn_decline_reason().is_some())
2262 && self.wgpu_graph_attn_decline().is_none()
2263 && self.attn_softcap == 0.0
2264 && self
2265 .weights
2266 .layers
2267 .iter()
2268 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2269 && self.automatic_gpu_prefix().is_some()
2270 }
2271
2272 fn g4_batch_prefill(&self) -> bool {
2278 #[cfg(target_os = "macos")]
2279 {
2280 false
2281 }
2282 #[cfg(not(target_os = "macos"))]
2283 {
2284 !self.weights.layers.is_empty()
2288 && !self.per_token_stack()
2289 && (self.global_attn.is_some() || self.attn_v_norm)
2290 && crate::gpu::g4_batch_on()
2291 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2292 && crate::gpu::enabled_here()
2293 && crate::gpu::discrete()
2294 && !self.graph_refused()
2295 && !self.o1_active()
2296 && self.attn_softcap == 0.0
2297 && self.wgpu_graph_attn_decline().is_none()
2298 && self.weights.layers.iter().all(|lw| {
2299 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2300 })
2301 && self.automatic_gpu_prefix().is_none()
2302 }
2303 }
2304
2305 #[cfg(not(target_os = "macos"))]
2312 fn g4_batch_ingest(
2313 &mut self,
2314 ids: &[u32],
2315 pos0: usize,
2316 keep_all: bool,
2317 ) -> Result<(usize, Vec<f32>), String> {
2318 let hs = self.hidden_size;
2319 let chunk = self.generation_batch_k().max(1);
2320 let end_all = pos0 + ids.len();
2321 let mut pos = pos0;
2322 let mut out = Vec::new();
2323 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2324 let end = (pos + chunk).min(end_all);
2325 let bk = end - pos;
2326 let mut hiddens = vec![0f32; bk * hs];
2327 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2328 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2329 }
2330 let positions: Vec<usize> = (pos..end).collect();
2331 let t_chunk = std::time::Instant::now();
2332 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2333 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2334 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2335 eprintln!(
2336 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2337 bk as f64 / (ms / 1000.0)
2338 );
2339 }
2340 match outcome {
2341 crate::gpu::BatchGraphOutcome::Completed => {
2342 if keep_all {
2343 out.extend_from_slice(&hiddens);
2344 } else {
2345 out = hiddens[(bk - 1) * hs..].to_vec();
2346 }
2347 pos = end;
2348 }
2349 crate::gpu::BatchGraphOutcome::Failed => {
2350 return Err("Gemma-4 batched prefill failed after admission".into());
2351 }
2352 crate::gpu::BatchGraphOutcome::Declined => {
2353 #[cfg(feature = "gpu")]
2354 if pos > pos0 {
2355 self.pull_lagging_host_kv(0, self.num_layers, pos);
2356 }
2357 break;
2358 }
2359 }
2360 }
2361 Ok((pos - pos0, out))
2362 }
2363
2364 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2369 #[cfg(not(target_os = "macos"))]
2370 {
2371 if ids.is_empty() || !self.g4_batch_prefill() {
2372 return None;
2373 }
2374 self.clear_sequence_state();
2375 match self.g4_batch_ingest(ids, 0, true) {
2376 Ok((n, rows)) if n == ids.len() => Some(rows),
2377 _ => {
2378 self.clear_sequence_state();
2379 None
2380 }
2381 }
2382 }
2383 #[cfg(target_os = "macos")]
2384 {
2385 let _ = ids;
2386 None
2387 }
2388 }
2389
2390 #[cfg(not(target_os = "macos"))]
2391 fn graph_prefill_preferred(&self) -> bool {
2392 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2400 if !graph_on || !crate::gpu::enabled_here() {
2401 return false;
2402 }
2403 if self.embryo_resident_eligible() {
2407 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2412 }
2413 if self.o1_active() {
2427 return false;
2428 }
2429 if self.wgpu_graph_attn_decline().is_some() {
2433 return false;
2434 }
2435 if self
2436 .weights
2437 .layers
2438 .iter()
2439 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2440 || self.onevision2_prefill()
2444 {
2445 return true;
2446 }
2447 self.weights
2458 .layers
2459 .iter()
2460 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2461 && self.automatic_gpu_prefix().is_none()
2462 }
2463
2464 #[cfg(target_os = "macos")]
2465 fn q1_graph_gpu(
2466 &mut self,
2467 start: usize,
2468 upto: Option<usize>,
2469 position: usize,
2470 h: &mut [f32],
2471 ) -> usize {
2472 let _mt0 = std::time::Instant::now(); use crate::gpu::{
2474 AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
2475 };
2476 const MOE_WORKING_SET_REASON: &str =
2477 "model weights exceed Metal working set for native MoE graph";
2478 if self
2482 .graph_declines
2483 .borrow()
2484 .iter()
2485 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2486 {
2487 return start;
2488 }
2489 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2490 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2492 || !crate::gpu::enabled_here()
2493 || !graph_force
2494 || std::env::var("CMF_GPU_BLOCK")
2495 .map(|v| v == "0")
2496 .unwrap_or(false)
2497 {
2498 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2499 eprintln!(
2500 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2501 self.attn_softcap > 0.0,
2502 crate::gpu::enabled_here(),
2503 graph_force,
2504 );
2505 }
2506 if self.graph_head_required {
2507 self.fail_metal_graph("native graph front gate refused");
2508 }
2509 return start;
2510 }
2511 let gemma4 = self.metal_gemma4();
2521 let swa_graph = self.metal_graph_swa() || gemma4;
2522 let graph_attn_reason = self.graph_attn_decline_reason();
2529 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2530 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2531 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
2535 if (self.swa.is_some() && !swa_graph)
2536 || (self.global_attn.is_some() && !gemma4)
2537 || self.attention_heads_per_layer.is_some()
2538 || (self.attn_v_norm && !gemma4)
2539 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2542 || self.weights.layers.iter().any(|lw| {
2543 ((lw.layer_scale.is_some()
2544 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
2545 && !gemma4)
2546 || matches!(&lw.ffn, FfnKind::Dense(d)
2547 if !(matches!(d.act, Act::Silu | Act::Gelu)
2548 || (gemma4 && d.act == Act::GeluTanh)))
2549 })
2550 {
2551 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2554 self.note_graph_decline("metal block graph", reason);
2555 }
2556 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2557 eprintln!(
2558 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2559 self.swa.is_some(),
2560 self.global_attn.is_some(),
2561 self.attention_heads_per_layer.is_some(),
2562 self.attn_v_norm,
2563 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2564 );
2565 }
2566 if self.graph_head_required {
2567 self.fail_metal_graph("native graph architecture gate refused");
2568 }
2569 return start;
2570 }
2571 let limit = upto
2574 .map(|u| u + 1)
2575 .unwrap_or(self.num_layers)
2576 .min(self.num_layers);
2577
2578 enum Item<'a> {
2579 Gdn {
2580 run: Vec<GdnGpuLayer<'a>>,
2581 first: usize,
2582 },
2583 Sconv {
2585 run: Vec<SconvGpuLayer<'a>>,
2586 first: usize,
2587 },
2588 Attn {
2589 l: AttnGpuLayer<'a>,
2590 li: usize,
2591 q_norm: Option<&'a [f32]>,
2592 k_norm: Option<&'a [f32]>,
2593 output_gate: bool,
2594 proj_gate: Option<(&'a QTensor, bool)>,
2598 head_gate_w: Option<&'a [f32]>,
2601 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2602 full_gpu: bool,
2605 },
2606 }
2607
2608 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2615 let attend_contract = attend_mode != "0"
2616 && attend_mode != "off"
2617 && self.head_dim % 4 == 0
2618 && self.head_dim <= 256
2619 && (self.rotary_dim == 0
2623 || (self.rotary_dim >= 2
2624 && self.rotary_dim <= self.head_dim
2625 && (self.rotary_dim / 2) % 32 == 0))
2626 && self.num_kv_heads > 0
2627 && self.num_heads % self.num_kv_heads == 0;
2628
2629 let mut plan: Vec<Item> = Vec::new();
2630 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2631 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2633 let mut scan = start;
2634 while scan < limit {
2635 if self.prompt_mrope.is_some()
2639 || (self.mhc_layer(scan)
2640 && (!crate::gpu_metal::mhc::available()
2641 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
2642 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
2643 || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
2644 {
2645 break;
2646 }
2647 let lw = &self.weights.layers[self.phys_layer(scan)];
2648 let ffn = match &lw.ffn {
2649 FfnKind::Dense(d) if d.segs.is_empty() => {
2650 let (Some(g), Some(u), Some(dn)) = (
2651 d.gate_proj.metal_graph_parts(),
2652 d.up_proj.metal_graph_parts(),
2653 d.down_proj.metal_graph_parts(),
2654 ) else {
2655 if block_diag {
2656 eprintln!(
2657 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2658 );
2659 }
2660 break;
2661 };
2662 MetalFfn::Dense {
2663 gate: g,
2664 up: u,
2665 down: dn,
2666 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2669 }
2670 }
2671 FfnKind::Moe(m) => {
2672 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2678 {
2679 MetalFfn::FlintMoe(flint)
2680 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2681 MetalFfn::Moe(moe)
2682 } else {
2683 if block_diag {
2684 eprintln!(
2685 "block-graph: L{scan} MoE outside the graph contract — run ends"
2686 );
2687 }
2688 break;
2689 };
2690 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2691 model_ref.get_or_insert_with(|| model.clone());
2692 }
2693 ffn
2694 }
2695 _ => {
2696 if block_diag {
2697 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2698 }
2699 break;
2700 }
2701 };
2702 match &lw.attn {
2703 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2704 let parts = (
2705 w.in_proj_qkv.metal_graph_parts(),
2706 w.in_proj_z.metal_graph_parts(),
2707 w.in_proj_a.f32_parts(),
2708 w.in_proj_b.f32_parts(),
2709 w.out_proj.metal_graph_parts(),
2710 );
2711 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2712 if block_diag {
2713 eprintln!(
2714 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2715 w.in_proj_qkv.metal_graph_parts().is_some(),
2716 w.in_proj_z.metal_graph_parts().is_some(),
2717 w.in_proj_a.f32_parts().is_some(),
2718 w.in_proj_b.f32_parts().is_some(),
2719 w.out_proj.metal_graph_parts().is_some(),
2720 );
2721 }
2722 break;
2723 };
2724 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2725 model_ref.get_or_insert_with(|| model.clone());
2726 }
2727 let gl = GdnGpuLayer {
2728 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2729 attn_norm: &lw.input_norm,
2730 post_norm: &lw.post_norm,
2731 qkv,
2732 z,
2733 a,
2734 b,
2735 out,
2736 ffn,
2737 conv1d: &w.conv1d,
2738 a_log: &w.a_log,
2739 dt_bias: &w.dt_bias,
2740 gnorm: &w.norm,
2741 };
2742 match plan.last_mut() {
2743 Some(Item::Gdn { run, .. }) => run.push(gl),
2744 _ => plan.push(Item::Gdn {
2745 run: vec![gl],
2746 first: scan,
2747 }),
2748 }
2749 }
2750 AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
2751 let (Some(inp), Some(out)) =
2752 (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
2753 else {
2754 if block_diag {
2755 eprintln!("block-graph: L{scan} short-conv parts refused");
2756 }
2757 break;
2758 };
2759 if let QTensor::Mapped { model, .. } = &w.in_proj {
2760 model_ref.get_or_insert_with(|| model.clone());
2761 }
2762 let sl = SconvGpuLayer {
2763 attn_norm: &lw.input_norm,
2764 post_norm: &lw.post_norm,
2765 in_proj: inp,
2766 taps: &w.conv,
2767 out,
2768 attn_out_norm: lw.attn_out_norm.as_deref(),
2769 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2770 ffn,
2771 };
2772 match plan.last_mut() {
2773 Some(Item::Sconv { run, .. }) => run.push(sl),
2774 _ => plan.push(Item::Sconv {
2775 run: vec![sl],
2776 first: scan,
2777 }),
2778 }
2779 }
2780 AttnKind::Full {
2781 wq,
2782 wk,
2783 wv,
2784 wo,
2785 q_norm,
2786 k_norm,
2787 output_gate,
2788 softplus_gate,
2789 bias,
2790 } if (!self.kv_cache.layers[scan].o1_sealed()
2791 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2796 && softplus_gate
2800 .as_ref()
2801 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2802 {
2803 let parts = (
2804 wq.metal_graph_parts(),
2805 wk.metal_graph_parts(),
2806 wv.metal_graph_parts(),
2807 wo.metal_graph_parts(),
2808 );
2809 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2810 break;
2811 };
2812 if let QTensor::Mapped { model, .. } = wq {
2813 model_ref.get_or_insert_with(|| model.clone());
2814 }
2815 let cache = &self.kv_cache.layers[scan];
2816 let o1_metal = cache.o1.is_some()
2820 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2821 && cache.o1_views().is_some();
2822 let head_gate_w = softplus_gate
2826 .as_ref()
2827 .and_then(|(g, _)| g.f32_parts())
2828 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2829 .map(|(d, _, _)| d);
2830 let (nkv_g, hd_g) = if gemma4 {
2833 let (a, b, _) = self.layer_geom(scan);
2834 (a, b)
2835 } else {
2836 (self.num_kv_heads, self.head_dim)
2837 };
2838 let full_gpu = attend_contract
2839 && softplus_gate.is_none() == head_gate_w.is_none()
2840 && cache.mode == crate::kv_cache::KvMode::F32
2841 && (cache.o1.is_none() || o1_metal)
2842 && bias.is_none()
2843 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2844 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2845 && pk.1 == nkv_g * hd_g
2846 && pv.1 == nkv_g * hd_g
2847 && po.2 == self.num_heads * hd_g;
2848 if !full_gpu && self.mhc_layer(scan) {
2851 break;
2852 }
2853 plan.push(Item::Attn {
2854 l: AttnGpuLayer {
2855 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
2856 attn_norm: &lw.input_norm,
2857 post_norm: &lw.post_norm,
2858 wq: pq,
2859 wk: pk,
2860 wv: pv,
2861 wo: po,
2862 ffn,
2863 attn_out_norm: lw.attn_out_norm.as_deref(),
2864 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2865 layer_scale: lw.layer_scale,
2866 },
2867 li: scan,
2868 q_norm: q_norm.as_deref(),
2869 k_norm: k_norm.as_deref(),
2870 output_gate: *output_gate,
2871 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2872 head_gate_w,
2873 bias: bias
2874 .as_ref()
2875 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2876 full_gpu,
2877 });
2878 }
2879 _ => break,
2880 }
2881 scan += 1;
2882 }
2883 if let Some(m) = self.mhc.as_deref() {
2887 let inside = |li: usize| m.on(self.phys_layer(li));
2888 let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
2889 let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
2890 if starts_mid || stops_mid {
2891 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2892 eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
2893 }
2894 return start;
2895 }
2896 }
2897 let Some(model) = model_ref else {
2898 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2899 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2900 }
2901 if self.graph_head_required {
2902 self.fail_metal_graph("native graph has no mapped model reference");
2903 }
2904 return start;
2905 };
2906 if plan.is_empty() {
2907 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2908 eprintln!("q1-graph: empty plan at layer {start}");
2909 }
2910 if self.graph_head_required {
2911 self.fail_metal_graph("native graph plan is empty");
2912 }
2913 return start;
2914 }
2915 let has_moe = plan.iter().any(|it| match it {
2916 Item::Gdn { run, .. } => run
2917 .iter()
2918 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2919 Item::Sconv { run, .. } => run
2920 .iter()
2921 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2922 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2923 });
2924 let has_flint_moe = plan.iter().any(|it| match it {
2925 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2926 Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2927 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2928 });
2929 let metal_working_set = crate::gpu_metal::working_set_bytes();
2930 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2931 let model_bytes = model.primary_bytes().len() as u64;
2932 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2938 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2939 let pager_cap = if !has_moe {
2948 None
2949 } else if has_flint_moe {
2950 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2954 } else {
2955 crate::gpu_metal::moe_pager_cap_bytes()
2956 };
2957 let pager_eligible =
2958 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2959 let mut pager_span: Option<(usize, usize)> = None;
2960 if pager_eligible {
2961 let cap = pager_cap.expect("checked above");
2962 let tail_span = (self.graph_want_logits
2966 && upto.is_none()
2967 && scan == self.num_layers
2968 && std::env::var("CMF_GPU_LMHEAD")
2969 .map(|v| v != "0")
2970 .unwrap_or(true))
2971 .then(|| self.weights.lm_head.metal_graph_parts())
2972 .flatten()
2973 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2974 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2975 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2976 Some(match span {
2977 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2978 None => (lo, hi),
2979 })
2980 };
2981 let page_span = |span: (usize, usize), include_tail: bool| {
2982 let (mut lo, mut hi) = span;
2983 if include_tail {
2984 if let Some((tail_lo, tail_hi)) = tail_span {
2985 lo = lo.min(tail_lo);
2986 hi = hi.max(tail_hi);
2987 }
2988 }
2989 crate::gpu_metal::page_round_span(lo, hi)
2990 };
2991 let mut selected = 0usize;
2992 let mut selected_span: Option<(usize, usize)> = None;
2993 let mut gdn_prefix: Option<(usize, usize)> = None;
3001 'items: for (i, item) in plan.iter().enumerate() {
3002 match item {
3003 Item::Sconv { .. } => break,
3006 Item::Attn { li, .. } => {
3007 let Some(span) = add_layer_span(selected_span, *li) else {
3008 break;
3009 };
3010 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3011 break;
3012 };
3013 if page_hi - page_lo > cap {
3014 break;
3015 }
3016 selected = i + 1;
3017 selected_span = Some((page_lo, page_hi));
3018 }
3019 Item::Gdn { first, run } => {
3020 let mut whole = None;
3027 for take in 1..=run.len() {
3028 whole = add_layer_span(whole, *first + take - 1);
3029 if whole.is_none() {
3030 break;
3031 }
3032 }
3033 let whole_fits_empty_page = whole
3034 .and_then(|span| page_span(span, false))
3035 .is_some_and(|(lo, hi)| hi - lo <= cap);
3036 if whole_fits_empty_page {
3037 let mut span = selected_span;
3038 for take in 1..=run.len() {
3039 let Some(next) = add_layer_span(span, *first + take - 1) else {
3040 break 'items;
3041 };
3042 span = Some(next);
3043 }
3044 let Some(span) = span else {
3045 break;
3046 };
3047 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3048 else {
3049 break;
3050 };
3051 if page_hi - page_lo > cap {
3052 break;
3053 }
3054 selected = i + 1;
3055 selected_span = Some((page_lo, page_hi));
3056 continue;
3057 }
3058 if selected != 0 {
3059 break;
3060 }
3061 let mut span = None;
3062 let mut best: Option<(usize, (usize, usize))> = None;
3063 for take in 1..=run.len() {
3064 let Some(next) = add_layer_span(span, *first + take - 1) else {
3065 break;
3066 };
3067 let Some((page_lo, page_hi)) = page_span(next, false) else {
3070 break;
3071 };
3072 if page_hi - page_lo > cap {
3073 break;
3074 }
3075 span = Some((page_lo, page_hi));
3076 best = Some((take, (page_lo, page_hi)));
3077 }
3078 let Some((take, page)) = best else {
3079 break;
3080 };
3081 selected = i + 1;
3082 selected_span = Some(page);
3083 if take != run.len() {
3084 gdn_prefix = Some((i, take));
3085 break 'items;
3086 }
3087 }
3088 }
3089 }
3090 if selected != 0 {
3091 if let Some((item, take)) = gdn_prefix {
3092 match &mut plan[item] {
3093 Item::Gdn { run, .. } => run.truncate(take),
3094 Item::Sconv { .. } | Item::Attn { .. } => {
3095 unreachable!("only a GDN run is splittable")
3096 }
3097 }
3098 }
3099 let (page_lo, page_hi) = selected_span.expect("selected page span");
3100 tracing::info!(
3101 page_start = page_lo,
3102 page_end = page_hi,
3103 page_mb = (page_hi - page_lo) / (1024 * 1024),
3104 cap_mb = cap / (1024 * 1024),
3105 graph_items = selected,
3106 "native Metal MoE graph uses transient bounded weight page"
3107 );
3108 plan.truncate(selected);
3109 pager_span = Some((page_lo, page_hi));
3110 } else {
3111 tracing::warn!(
3112 cap_mb = cap / (1024 * 1024),
3113 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3114 );
3115 }
3116 }
3117 if has_moe && needs_bounded_pager && pager_span.is_none() {
3126 tracing::info!(
3127 model_bytes,
3128 metal_working_set,
3129 "native Metal MoE graph declined before allocation"
3130 );
3131 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3132 if self.graph_head_required {
3133 self.fail_metal_graph(MOE_WORKING_SET_REASON);
3134 }
3135 return start;
3136 }
3137 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3138 let dev_attend = attend_contract
3139 && (self.head_dim <= 128
3140 || has_moe
3141 || (self.head_dim <= 256 && has_gdn)
3147 || (self.head_dim <= 256 && swa_graph)
3151 || attend_mode == "force"
3152 || attend_mode == "256");
3153 if !dev_attend {
3154 for it in &mut plan {
3155 if let Item::Attn { li, full_gpu, .. } = it {
3156 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3159 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3160 if !keep_o1 {
3161 *full_gpu = false;
3162 }
3163 }
3164 }
3165 }
3166 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3167 use std::sync::atomic::{AtomicBool, Ordering};
3168 static SAID: AtomicBool = AtomicBool::new(false);
3169 if !SAID.swap(true, Ordering::Relaxed) {
3170 let fg = plan
3171 .iter()
3172 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3173 .count();
3174 let att = plan
3175 .iter()
3176 .filter(|it| matches!(it, Item::Attn { .. }))
3177 .count();
3178 eprintln!(
3179 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3180 plan.len(),
3181 self.head_dim,
3182 self.rotary_dim,
3183 self.num_kv_heads,
3184 self.num_heads,
3185 );
3186 }
3187 }
3188 let dims = GraphDims {
3189 hidden: self.hidden_size,
3190 eps: self.rms_eps as f32,
3191 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3192 };
3193 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3197 crate::gpu_metal::MoePagerPageScope::new(
3198 _mt0,
3199 position,
3200 start,
3201 page_lo,
3202 page_hi,
3203 pager_cap.unwrap_or(0),
3207 )
3208 });
3209 let graph_new = match pager_span {
3210 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3211 None => TokenGraph::new(&model, dims, h),
3212 };
3213 let Some(mut graph) = graph_new else {
3214 if self.graph_head_required {
3215 self.fail_metal_graph("native TokenGraph allocation refused");
3216 }
3217 return start;
3218 };
3219 if swa_graph && self.head_dim > 128 {
3220 graph.set_attend_blk(1, 32);
3230 }
3231 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3232 nv: cfg.num_v_heads,
3233 nk: cfg.num_k_heads,
3234 dk: cfg.key_head_dim,
3235 dv: cfg.value_head_dim,
3236 kk: cfg.conv_kernel,
3237 hidden: self.hidden_size,
3238 inter: self.intermediate_size,
3239 c_dim: cfg.conv_dim(),
3240 eps: cfg.rms_eps as f32,
3241 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3242 });
3243 let mut valid = 0usize;
3247 let mut end = start;
3248 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3250 static ONCE: std::sync::Once = std::sync::Once::new();
3251 ONCE.call_once(|| {
3252 for it in &plan {
3253 match it {
3254 Item::Gdn { first, run } => {
3255 eprintln!("plan: Gdn first={first} len={}", run.len())
3256 }
3257 Item::Sconv { first, run } => {
3258 eprintln!("plan: Sconv first={first} len={}", run.len())
3259 }
3260 Item::Attn { li, full_gpu, .. } => {
3261 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3262 }
3263 }
3264 }
3265 });
3266 }
3267 for item in &plan {
3268 let ok = match item {
3269 Item::Gdn { run, .. } => gcfg
3270 .as_ref()
3271 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3272 .unwrap_or(false),
3273 Item::Sconv { run, .. } => self
3274 .short_conv_cfg
3275 .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
3276 Item::Attn { l, .. } => graph.attn_ok(l),
3277 };
3278 if !ok {
3279 if block_diag {
3280 eprintln!(
3281 "block-graph: plan item {} ({}) failed graph preflight",
3282 valid,
3283 match item {
3284 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3285 Item::Sconv { run, first } => {
3286 format!("short-conv run L{first}+{}", run.len())
3287 }
3288 Item::Attn { li, .. } => format!("Attn L{li}"),
3289 }
3290 );
3291 }
3292 break;
3293 }
3294 valid += 1;
3295 end += match item {
3296 Item::Gdn { run, .. } => run.len(),
3297 Item::Sconv { run, .. } => run.len(),
3298 Item::Attn { .. } => 1,
3299 };
3300 }
3301 plan.truncate(valid);
3302 if plan.is_empty() {
3303 if self.graph_head_required {
3304 self.fail_metal_graph("native graph preflight produced no valid items");
3305 }
3306 return start;
3307 }
3308
3309 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3310 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3311 return start;
3312 }
3313
3314 let one_pass = |t: (usize, usize, usize)| {
3323 use cortiq_core::TensorDtype as D;
3324 matches!(
3325 model.tensors[t.0].dtype,
3326 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3327 )
3328 };
3329 let dense_fast = plan.iter().all(|it| match it {
3330 Item::Attn {
3331 l, li, full_gpu, ..
3332 } => {
3333 *full_gpu
3334 && self.kv_cache.layers[*li].o1.is_none()
3335 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3336 && match l.ffn {
3337 MetalFfn::Dense { gate, up, down, .. } => {
3338 one_pass(gate) && one_pass(up) && one_pass(down)
3339 }
3340 _ => false,
3341 }
3342 }
3343 Item::Sconv { run, .. } => run.iter().all(|l| {
3346 one_pass(l.in_proj)
3347 && one_pass(l.out)
3348 && match l.ffn {
3349 MetalFfn::Dense { gate, up, down, .. } => {
3350 one_pass(gate) && one_pass(up) && one_pass(down)
3351 }
3352 _ => false,
3353 }
3354 }),
3355 Item::Gdn { .. } => false,
3356 });
3357 let moe_plan = !dense_fast
3361 && plan.iter().all(|it| match it {
3362 Item::Attn {
3363 l, li, full_gpu, ..
3364 } => {
3365 *full_gpu
3366 && self.kv_cache.layers[*li].o1.is_none()
3367 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3368 }
3369 Item::Gdn { .. } | Item::Sconv { .. } => false,
3370 });
3371 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3372 if block_diag {
3373 use std::sync::atomic::{AtomicBool, Ordering};
3374 static SAID_AB: AtomicBool = AtomicBool::new(false);
3375 if !SAID_AB.swap(true, Ordering::Relaxed) {
3376 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3377 }
3378 }
3379 let _mv_fast = match ab {
3380 Some((bits, _)) => {
3381 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3382 crate::gpu_metal::MvFastGuard::set_raw(bits)
3383 }
3384 None if moe_plan => {
3385 graph.set_dense_concurrent(
3386 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3387 );
3388 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3389 }
3390 None => {
3391 graph.set_dense_concurrent(dense_fast);
3392 let q8r4 = if swa_graph {
3395 crate::gpu_metal::DENSE_Q8R4
3396 } else {
3397 0
3398 };
3399 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3400 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3401 } else {
3402 0
3403 })
3404 }
3405 };
3406
3407 let pool = self.pool.clone();
3410 let (nh, nkv, hd, hs, eps) = (
3411 self.num_heads,
3412 self.num_kv_heads,
3413 self.head_dim,
3414 self.hidden_size,
3415 self.rms_eps,
3416 );
3417 let norm_style = self.norm_style;
3418 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3419 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3420 let kv_id = self.graph_kv_id;
3421 let mut pending: Vec<(usize, usize)> = Vec::new();
3424 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3427 for item in &plan {
3428 let _xt0 = std::time::Instant::now();
3429 let _xkind: u32 = match item {
3430 Item::Gdn { .. } | Item::Sconv { .. } => 2,
3431 Item::Attn { .. } => 3,
3432 };
3433 if self.loop_final_norm {
3435 let item_start = match item {
3436 Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
3437 Item::Attn { li, .. } => *li,
3438 };
3439 if item_start > start && self.is_loop_end(item_start - 1) {
3440 graph.encode_loop_norm(&self.weights.final_norm);
3441 }
3442 }
3443 match item {
3444 Item::Gdn { run, first } => {
3445 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3446 if l.linear_state.len() != want {
3447 l.linear_state = vec![0f32; want];
3448 }
3449 }
3450 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3451 .iter()
3452 .map(|l| l.linear_state.as_slice())
3453 .collect();
3454 let _ig = std::time::Instant::now();
3455 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3456 tracing::error!("q1 graph: GDN run refused after validation");
3458 return start;
3459 }
3460 graph.commit_kind = 2;
3463 graph.commit();
3464 crate::gpu::stageprof(0, _ig.elapsed());
3465 pending.push((*first, run.len()));
3466 }
3467 Item::Sconv { run, first } => {
3468 let sc = self.short_conv_cfg.expect("validated short-conv run");
3469 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3470 if l.linear_state.len() != sc.state_len() {
3471 l.linear_state = vec![0f32; sc.state_len()];
3472 }
3473 }
3474 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3475 .iter()
3476 .map(|l| l.linear_state.as_slice())
3477 .collect();
3478 if !graph.encode_sconv_run(run, &ro, sc.kernel) {
3479 tracing::error!("q1 graph: short-conv run refused after validation");
3480 return start;
3481 }
3482 graph.commit_kind = 2;
3483 graph.commit();
3484 pending.push((*first, run.len()));
3485 }
3486 Item::Attn {
3487 l,
3488 li,
3489 q_norm,
3490 k_norm,
3491 output_gate,
3492 proj_gate,
3493 head_gate_w,
3494 bias,
3495 full_gpu,
3496 } => {
3497 let _ia = std::time::Instant::now();
3498 let inv_freq_l = self.layer_inv_freq(*li);
3504 let rd_l = self.layer_geom(*li).2;
3505 let window_l = self.layer_window(*li);
3506 let head_gate_w = *head_gate_w;
3507 let (nkv, hd) = if gemma4 {
3510 let (a, b, _) = self.layer_geom(*li);
3511 (a, b)
3512 } else {
3513 (nkv, hd)
3514 };
3515 if *full_gpu {
3517 let cache = &self.kv_cache.layers[*li];
3518 let o1p = if cache.o1.is_some() {
3519 match cache.o1_views() {
3520 Some(views) => Some(crate::gpu::O1AttnParams {
3521 views,
3522 epoch: self.o1_epoch,
3523 }),
3524 None => None,
3526 }
3527 } else {
3528 None
3529 };
3530 let o1_layer = cache.o1.is_some();
3531 if o1_layer && o1p.is_none() {
3532 }
3534 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3535 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3536 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3537 debug_assert!(
3540 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3541 );
3542 let p = crate::gpu::AttnDeviceParams {
3543 kv_id,
3544 layer: *li,
3545 nh,
3546 nkv,
3547 hd,
3548 rd: rd_l,
3549 position,
3550 scale: self.attn_scale,
3551 eps: eps as f32,
3552 gemma,
3553 late_qk_norm: self.qk_norm_after_rope,
3554 output_gate: *output_gate,
3555 q_norm: *q_norm,
3556 k_norm: *k_norm,
3557 inv_freq: &inv_freq_l,
3558 rope_scale: self.layer_rope_scale(*li),
3559 cpu_k,
3560 cpu_v,
3561 cpu_stored,
3562 cpu_gen: cache.generation(),
3563 o1: o1p,
3564 window: window_l,
3565 softmax_sink: cache.sinks.as_deref(),
3566 head_gate: head_gate_w,
3567 v_norm: self.attn_v_norm,
3568 };
3569 let o1_bad = o1_layer && p.o1.is_none();
3570 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3571 {
3572 if p.o1.is_none() {
3574 dev_attn.push((*li, nkv, hd));
3575 }
3576 graph.commit_kind = 3;
3577 graph.commit();
3578 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3582 continue;
3583 }
3584 }
3586 graph.encode_attn_prefix(l);
3587 if let Err(err) = graph.sync_checked() {
3588 self.fail_metal_graph(&err);
3589 return start;
3590 }
3591 if !pending.is_empty() {
3592 let idxs: Vec<usize> =
3593 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3594 let mut outs: Vec<&mut [f32]> = self
3595 .kv_cache
3596 .layers
3597 .iter_mut()
3598 .enumerate()
3599 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3600 .map(|(_, s)| s.linear_state.as_mut_slice())
3601 .collect();
3602 graph.read_states(&mut outs);
3603 }
3604 let mut q_raw = attention::take_buf(l.wq.1);
3605 let mut k = attention::take_buf(l.wk.1);
3606 let mut v = attention::take_buf(l.wv.1);
3607 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3608 let mut gate_raw = proj_gate.map(|(gp, _)| {
3611 let mut normed = attention::take_buf(hs);
3612 graph.read_normed(&mut normed);
3613 let mut raw = attention::take_buf(gp.rows());
3614 gp.matvec(&normed, &mut raw, pool.as_deref());
3615 attention::recycle_buf(&mut normed);
3616 raw
3617 });
3618 let cfg = QwenAttnCfg {
3619 mrope: None,
3620 num_heads: nh,
3621 num_kv_heads: nkv,
3622 head_dim: hd,
3623 hidden_size: hs,
3624 position,
3625 inv_freq: &inv_freq_l,
3626 rotary_dim: rd_l,
3627 scale: self.attn_scale,
3628 softcap: self.attn_softcap,
3629 window: window_l,
3630 v_norm: self.attn_v_norm,
3633 qk_norm_after_rope: self.qk_norm_after_rope,
3634 gate_sigmoid: self.proj_gate_sigmoid,
3635 q_norm: *q_norm,
3636 k_norm: *k_norm,
3637 output_gate: *output_gate,
3638 softplus_gate: None,
3639 rope_scale: self.layer_rope_scale(*li),
3640 bias: *bias,
3641 rms_eps: eps,
3642 norm_style,
3643 pool: pool.as_deref(),
3644 v_head_dim: hd,
3645 };
3646 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3649 || std::env::var("CMF_ATTN_DUMP").is_ok();
3650 let _ = full_gpu;
3651 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3652 let mut ao = attention::qwen_attention_core(
3653 q_raw,
3654 k,
3655 v,
3656 &mut self.kv_cache.layers[*li],
3657 &cfg,
3658 );
3659 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3663 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3664 let (cq, _cg, _ck, _cv) =
3665 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3666 let cache = &self.kv_cache.layers[*li];
3667 let n = cache.head_keys(0).len() / hd;
3668 let mut bytes: Vec<u8> = Vec::new();
3669 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3670 bytes.extend_from_slice(&v.to_le_bytes());
3671 }
3672 for v in &cq {
3673 bytes.extend_from_slice(&v.to_le_bytes());
3674 }
3675 for g in 0..nkv {
3676 for v in cache.head_keys(g) {
3677 bytes.extend_from_slice(&v.to_le_bytes());
3678 }
3679 }
3680 for g in 0..nkv {
3681 for v in cache.head_values(g) {
3682 bytes.extend_from_slice(&v.to_le_bytes());
3683 }
3684 }
3685 let _ =
3686 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3687 }
3688 }
3689 if let Some((qr0, k0, v0)) =
3690 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3691 {
3692 let (cq, _cg, ck, cv) =
3693 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3694 let mut h_now = vec![0f32; hs];
3695 graph.read_h(&mut h_now);
3696 let cache = &self.kv_cache.layers[*li];
3697 let n_after = cache.head_keys(0).len() / hd;
3698 let stored = n_after.saturating_sub(1);
3702 let cpu_k: Vec<&[f32]> = (0..nkv)
3703 .map(|g| &cache.head_keys(g)[..stored * hd])
3704 .collect();
3705 let cpu_v: Vec<&[f32]> = (0..nkv)
3706 .map(|g| &cache.head_values(g)[..stored * hd])
3707 .collect();
3708 let p = crate::gpu::AttnDeviceParams {
3709 kv_id,
3710 layer: *li,
3711 nh,
3712 nkv,
3713 hd,
3714 rd: rd_l,
3718 position,
3719 scale: self.attn_scale,
3720 eps: eps as f32,
3721 gemma,
3722 late_qk_norm: self.qk_norm_after_rope,
3723 output_gate: *output_gate,
3724 q_norm: *q_norm,
3725 k_norm: *k_norm,
3726 inv_freq: &inv_freq_l,
3727 rope_scale: self.layer_rope_scale(*li),
3728 cpu_k,
3729 cpu_v,
3730 cpu_stored: stored,
3731 cpu_gen: cache.generation(),
3732 o1: None,
3733 window: window_l,
3734 softmax_sink: cache.sinks.as_deref(),
3735 head_gate: None,
3736 v_norm: self.attn_v_norm,
3737 };
3738 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3739 let md = |a: &[f32], b: &[f32]| {
3740 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3741 };
3742 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3743 eprintln!(
3744 "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}",
3745 nn(&cq),
3746 md(&cq, &dq),
3747 nn(&ck),
3748 md(&ck, &dk),
3749 nn(&cv),
3750 md(&cv, &dv),
3751 nn(&ao),
3752 md(&ao, &dao)
3753 );
3754 } else {
3755 eprintln!("attn-oracle L{li}: device probe declined");
3756 }
3757 }
3758 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3759 attention::apply_projected_gate(
3762 &mut ao,
3763 raw,
3764 per_head,
3765 hd,
3766 self.proj_gate_sigmoid,
3767 );
3768 }
3769 if let Some(mut raw) = gate_raw.take() {
3770 attention::recycle_buf(&mut raw);
3771 }
3772 graph.encode_attn_suffix(l, &ao);
3773 graph.commit();
3776 attention::recycle_buf(&mut ao);
3777 }
3778 }
3779
3780 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3781 }
3782 let mut lm_rows = None;
3787 if self.graph_want_logits
3788 && upto.is_none()
3789 && end == self.num_layers
3790 && std::env::var("CMF_GPU_LMHEAD")
3791 .map(|v| v != "0")
3792 .unwrap_or(true)
3793 {
3794 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3795 if graph.lm_head_ok(lm) {
3796 graph.encode_lm_head(&self.weights.final_norm, lm);
3797 lm_rows = Some(lm.1);
3798 }
3799 }
3800 }
3801 if self.graph_head_required && lm_rows.is_none() {
3802 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3803 self.fail_metal_graph("fused graph head was requested but not encodable");
3804 return start;
3805 }
3806 let _sy0 = std::time::Instant::now();
3807 if let Err(err) = graph.sync_checked() {
3808 if let Some(profile) = pager_profile_page.as_mut() {
3809 profile.finish(start, graph.pager_page_stats(), "sync-error");
3810 }
3811 self.fail_metal_graph(&err);
3812 return start;
3813 }
3814 let _rs0 = std::time::Instant::now();
3815 if !pending.is_empty() {
3816 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3817 let mut outs: Vec<&mut [f32]> = self
3818 .kv_cache
3819 .layers
3820 .iter_mut()
3821 .enumerate()
3822 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3823 .map(|(_, s)| s.linear_state.as_mut_slice())
3824 .collect();
3825 graph.read_states(&mut outs);
3826 }
3827 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3828 use std::sync::atomic::{AtomicU64, Ordering};
3829 static SY: AtomicU64 = AtomicU64::new(0);
3830 static RS: AtomicU64 = AtomicU64::new(0);
3831 static N: AtomicU64 = AtomicU64::new(0);
3832 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3833 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3834 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3835 if n % 100 == 0 {
3836 eprintln!(
3837 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3838 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3839 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3840 );
3841 }
3842 }
3843 if let Some(rows) = lm_rows {
3844 crate::gpu::hostprof_encode_done(_mt0);
3845 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3846 graph.read_logits(&mut lg);
3847 crate::gpu::hostprof_total(_mt0);
3848 lg.resize(self.vocab_size, 0.0);
3849 if let Some(c) = self.final_softcap {
3850 softcap_in_place(&mut lg, c, self.pool.as_deref());
3851 }
3852 self.graph_logits = Some(lg);
3853 }
3854 graph.read_h(h);
3855 if self.graph_head_required && self.graph_logits.is_none() {
3856 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3857 self.fail_metal_graph("fused graph head completed without logits readback");
3858 return start;
3859 }
3860 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3861 METAL_GRAPH_LAYERS.fetch_add(
3862 end.saturating_sub(start) as u64,
3863 std::sync::atomic::Ordering::Relaxed,
3864 );
3865 if self.graph_head_required {
3866 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3867 }
3868 for (li, nkv, hd) in dev_attn {
3872 let mut krow = attention::take_buf(nkv * hd);
3873 let mut vrow = attention::take_buf(nkv * hd);
3874 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3875 let cache = &mut self.kv_cache.layers[li];
3876 cache.append(&krow, &vrow, &[]);
3877 let n = cache.seq_len;
3878 let mut imp = attention::take_buf(n);
3879 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3880 cache.accumulate_imp(&imp);
3881 attention::recycle_buf(&mut imp);
3882 }
3883 attention::recycle_buf(&mut krow);
3884 attention::recycle_buf(&mut vrow);
3885 }
3886 if let Some(profile) = pager_profile_page.as_mut() {
3887 profile.finish(end, graph.pager_page_stats(), "page-complete");
3888 }
3889 if let Some((_, arm)) = ab {
3890 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3891 }
3892 end
3893 }
3894
3895 pub fn new(
3896 tokenizer: Tokenizer,
3897 weights: PipelineWeights,
3898 hidden_size: usize,
3899 intermediate_size: usize,
3900 num_heads: usize,
3901 num_kv_heads: usize,
3902 head_dim: usize,
3903 num_layers: usize,
3904 physical_layers: usize,
3905 loop_final_norm: bool,
3906 vocab_size: usize,
3907 rms_eps: f64,
3908 rope_base: f32,
3909 norm_style: NormStyle,
3910 max_seq_len: usize,
3911 sampler_config: SamplerConfig,
3912 ) -> Self {
3913 let rng = match sampler_config.seed {
3914 Some(s) => SplitMix64::new(s),
3915 None => SplitMix64::from_entropy(),
3916 };
3917 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3918 let pool = Pool::from_env();
3919 if let Some(p) = &pool {
3920 tracing::info!("worker pool: {} threads", p.n_workers());
3921 if let Some(model) = weights
3923 .lm_head
3924 .model_arc()
3925 .or_else(|| weights.embed_tokens.model_arc())
3926 {
3927 let regions: Vec<&[u8]> =
3928 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3929 p.bind_numa(®ions);
3930 }
3931 }
3932 Self {
3933 gpu_plan: None,
3934 tokenizer: std::sync::Arc::new(tokenizer),
3935 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3936 sampler_config,
3937 weights,
3938 hidden_size,
3939 intermediate_size,
3940 num_heads,
3941 num_kv_heads,
3942 head_dim,
3943 num_layers,
3944 physical_layers,
3945 loop_final_norm,
3946 layer_map: None,
3947 mhc: None,
3948 mhc_r: Vec::new(),
3949 mrope_sections: None,
3950 prompt_mrope: None,
3951 vocab_size,
3952 rms_eps,
3953 rope_base,
3954 norm_style,
3955 rotary_dim: head_dim,
3956 attention_heads_per_layer: None,
3957 kv_heads_per_layer: None,
3958 v_head_dim: None,
3959 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3960 .filter(|v| !v.is_empty())
3961 .map(std::path::PathBuf::from),
3962 graph_declines: std::cell::RefCell::new(Vec::new()),
3963 mimo_moe: Default::default(),
3964 minicpm_tail: Default::default(),
3965 vmf_cfg: None,
3966 gdn_cfg: None,
3967 kda_cfg: None,
3968 g3n: None,
3969 g4e: None,
3970 dsv4: None,
3971 dsv41: None,
3972 dsv41_vision: None,
3973 dsv41_prefill: None,
3974 qwen4_exp: None,
3975 clover: None,
3976 clover_stats: clover::CloverStats::default(),
3977 dsv4_mtp: Vec::new(),
3978 dspark: None,
3979 dspark_pending: Vec::new(),
3980 dspark_hist: Vec::new(),
3981 dspark_real: Vec::new(),
3982 dspark_trunk_picks: Vec::new(),
3983 dspark_exp: Vec::new(),
3984 dspark_draft_ns: 0,
3985 m3_winners: Vec::new(),
3986 m3_counts: Vec::new(),
3987 m3_n_exp: 0,
3988 m3_tokens: 0,
3989 m3_prewarmed: false,
3990 logit_multiplier: None,
3991 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3992 graph_failed: std::sync::atomic::AtomicBool::new(false),
3993 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3994 kv_history: Vec::new(),
3995 kv_history_device: false,
3996 short_conv_cfg: None,
3997 mtp: None,
3998 mimo_mtp: None,
3999 verify_exact_moe: false,
4000 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4001 ignore_eos: false,
4002 draft_full_streak: 0,
4003 spec_k_adapt: None,
4004 spec_acc_ewma: 0.7,
4005 rng,
4006 sampler_scratch: SamplerScratch::default(),
4007 spec_forced: None,
4008 spec_q: Vec::new(),
4009 spec_p: Vec::new(),
4010 spec_res: Vec::new(),
4011 spec_qs: Vec::new(),
4012 spec_ps: Vec::new(),
4013 spec_ress: Vec::new(),
4014 mtp_graph_mode: None,
4015 #[cfg(target_os = "macos")]
4016 metal_verify: None,
4017 inv_freq,
4018 ws: ForwardScratch::new(hidden_size),
4019 pool,
4020 model: None,
4021 dyn_force_f32: false,
4022 dyn_skill_layers: Vec::new(),
4023 dyn_active: None,
4024 dyn_blend_loaded: false,
4025 dyn_phi_layer: None,
4026 dyn_phi_ema: Vec::new(),
4027 dyn_phi_seen: 0,
4028 dyn_router: None,
4029 o1_cfg: None,
4030 o1_epoch: 0,
4031 o1_flags: Vec::new(),
4032 trace: false,
4033 calib_temp: 1.0,
4034 confidence_on: true,
4035 embed_multiplier: 1.0,
4036 attn_scale: 1.0 / (head_dim as f32).sqrt(),
4037 swa: None,
4038 swa_trim: None,
4039 swa_trim_anchor: usize::MAX,
4040 chunk_min_rows: 32,
4041 sliding_layers: None,
4042 no_rope_layers: None,
4043 anchor_core: None,
4044 bounded_rope: None,
4045 kv_prefix: KvPrefix::default(),
4046 last_prefill_tokens: 0,
4047 inv_freq_local: None,
4048 rotary_dim_local: None,
4049 rope_scale: 1.0,
4050 rope_scale_local: 1.0,
4051 global_attn: None,
4052 inv_freq_global: None,
4053 attn_v_norm: false,
4054 qk_norm_after_rope: false,
4055 proj_gate_sigmoid: false,
4056 final_softcap: None,
4057 head_clusters: None,
4058 attn_softcap: 0.0,
4059 graph_want_logits: false,
4060 graph_head_required: false,
4061 graph_logits: None,
4062 embryo_graph: None,
4063 graph_refused: std::sync::atomic::AtomicBool::new(false),
4064 graph_kv_id: {
4065 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4066 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4067 },
4068 #[cfg(test)]
4069 nll_test_fail_at: None,
4070 #[cfg(test)]
4071 nll_test_force_serial: false,
4072 }
4073 }
4074
4075 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4083 if let Err(e) = self.try_set_o1(cfg) {
4084 tracing::error!("{e}");
4085 }
4086 }
4087
4088 pub fn bounded_native(&self) -> bool {
4092 self.anchor_core.is_some()
4093 }
4094
4095 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
4098 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
4099 }
4100
4101 pub fn o1_refusal(&self) -> Option<String> {
4103 self.anchor_core.as_ref().map(|ac| {
4104 format!(
4105 "--o1 / CMF_O1 refused: the anchor is native bounded \
4106 (anchor_core kind={} window={} sink={}); the file's operator \
4107 is executed as-is and no post-hoc Nyström overlay applies",
4108 ac.kind, ac.window, ac.sink
4109 )
4110 })
4111 }
4112
4113 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
4115 if let Some(c) = &cfg {
4116 if let Some(why) = self.o1_refusal() {
4117 self.o1_flags = Vec::new();
4118 self.o1_cfg = None;
4119 return Err(why);
4120 }
4121 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
4122 self.o1_flags.clear();
4123 self.o1_cfg = None;
4124 return Err(format!(
4125 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
4126 c.w, c.sink
4127 ));
4128 }
4129 }
4130 self.o1_flags = match &cfg {
4131 Some(c) => {
4132 let mut flags = c.layer_flags(self.num_layers);
4133 for (li, f) in flags.iter_mut().enumerate() {
4134 if *f
4140 && (!matches!(
4141 self.weights.layers[self.phys_layer(li)].attn,
4142 AttnKind::Full { .. }
4143 ) || self.layer_window(li).is_some()
4144 || self.kv_cache.layers[li].sinks.is_some()
4145 || self.layer_v_dim(li) != self.layer_geom(li).1)
4146 {
4147 *f = false;
4148 }
4149 }
4150 flags
4151 }
4152 None => Vec::new(),
4153 };
4154 if let Some(c) = &cfg {
4155 let n = self.o1_flags.iter().filter(|&&f| f).count();
4156 tracing::info!(
4157 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
4158 self.num_layers,
4159 c.m,
4160 c.w,
4161 c.sink,
4162 c.rect
4163 );
4164 }
4165 self.o1_cfg = cfg;
4166 Ok(())
4167 }
4168
4169 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
4175 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
4176 return Err(format!(
4177 "anchor_core kind '{}' is not executable by this runtime",
4178 cfg.kind
4179 ));
4180 }
4181 if cfg.window == 0 {
4182 return Err("anchor_core.window must be >= 1".into());
4183 }
4184 let mut n = 0usize;
4185 for li in 0..self.num_layers {
4186 let pl = self.phys_layer(li);
4187 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
4188 if w.window != cfg.window
4189 || w.sink != cfg.sink
4190 || w.group_windows != cfg.group_windows
4191 {
4192 return Err(format!(
4193 "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
4194 with anchor_core (window {} sink {} groups {:?})",
4195 w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
4196 ));
4197 }
4198 self.kv_cache.layers[li].install_bounded(cfg.window);
4199 n += 1;
4200 }
4201 }
4202 if n == 0 {
4203 return Err("anchor_core is present but no layer executes it".into());
4204 }
4205 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
4206 self.bounded_rope = Some(std::sync::Arc::new(rope));
4207 self.anchor_core = Some(cfg.clone());
4208 self.embryo_graph = None;
4209 tracing::info!(
4210 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
4211 cfg.kind,
4212 cfg.window,
4213 cfg.sink,
4214 self.kv_cache
4215 .layers
4216 .iter()
4217 .map(|l| l.bounded_state_bytes())
4218 .max()
4219 .unwrap_or(0)
4220 );
4221 Ok(())
4222 }
4223
4224 pub fn install_wire_identity(&mut self, identity: u64) {
4228 for li in 0..self.kv_cache.layers.len() {
4229 let pl = self.phys_layer(li);
4230 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4231 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4232 Some(AttnKind::Linear(_))
4233 | Some(AttnKind::LinearGdn(_))
4234 | Some(AttnKind::ShortConv(_))
4235 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4236 _ => crate::kv_cache::WireKind::Full,
4237 };
4238 let l = &mut self.kv_cache.layers[li];
4239 l.wire_kind = kind;
4240 l.wire_identity = identity;
4241 l.wire_layer = li as u32;
4244 }
4245 }
4246
4247 pub fn clear_history(&mut self) {
4249 self.kv_history.clear();
4250 self.kv_history_device = false;
4251 self.kv_prefix.clear();
4252 }
4253
4254 pub fn graph_refused(&self) -> bool {
4258 self.graph_refused
4259 .load(std::sync::atomic::Ordering::Relaxed)
4260 }
4261
4262 pub fn mark_graph_refused(&self) {
4264 if !self
4265 .graph_refused
4266 .swap(true, std::sync::atomic::Ordering::Relaxed)
4267 {
4268 tracing::info!(
4269 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4270 self.graph_kv_id
4271 );
4272 }
4273 }
4274
4275 pub fn device_sequence_position(&self) -> Option<usize> {
4279 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4280 }
4281
4282 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4287 let dev = self.device_sequence_position();
4288 if recorded_on_device {
4289 dev == Some(n) && self.embryo_resident_wanted()
4290 } else {
4291 dev.is_none()
4292 }
4293 }
4294
4295 pub(crate) fn invalidate_for_weight_change(&mut self) {
4303 self.clear_sequence_state();
4304 self.embryo_graph = None;
4305 }
4306
4307 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4313 let (n, on_device) = if self.bounded_native() {
4314 (
4315 self.kv_prefix.extension(input_ids),
4316 self.kv_prefix.on_device(),
4317 )
4318 } else {
4319 let h = &self.kv_history;
4320 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4321 (h.len(), self.kv_history_device)
4322 } else if !h.is_empty() && !self.kv_history_device {
4323 (self.rollback_prefix_len(input_ids), false)
4324 } else {
4325 (0, false)
4326 }
4327 };
4328 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4331 tracing::warn!(
4332 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4333 the device now holds {:?} — re-prefilling from zero",
4334 if on_device { "resident device" } else { "host" },
4335 self.device_sequence_position()
4336 );
4337 return 0;
4338 }
4339 n
4340 }
4341
4342 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4353 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4354 || self.device_sequence_position().is_some()
4355 {
4356 return 0;
4357 }
4358 let h = &self.kv_history;
4359 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4360 let n = lcp.min(input_ids.len().saturating_sub(1));
4361 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4362 return 0;
4363 }
4364 let kv_id = self.graph_kv_id;
4365 for li in 0..self.num_layers {
4366 let l = &self.kv_cache.layers[li];
4367 let tail_ok = l.base() == 0
4368 || self
4369 .layer_window(li)
4370 .is_some_and(|w| l.base() + w <= n + 1);
4371 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4372 || l.o1.is_some()
4373 || l.bounded.is_some()
4374 || !l.linear_state.is_empty()
4375 || l.mode != crate::kv_cache::KvMode::F32
4376 || l.pos_len() != h.len()
4377 || !tail_ok
4378 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4379 || crate::gpu::graph_state_resident(kv_id, li)
4380 {
4381 return 0;
4382 }
4383 }
4384 n
4385 }
4386
4387 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4390 self.cached_prefix_len(input_ids)
4391 }
4392
4393 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4399 let on_device = self.device_sequence_position().is_some();
4400 if self.bounded_native() {
4401 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4402 let prev_device = self.kv_prefix.on_device();
4403 self.kv_history.clear();
4404 self.kv_history_device = false;
4405 if keep && prev_device == on_device {
4406 self.kv_prefix.extend(&consumed[reused..]);
4407 } else {
4408 self.kv_prefix.set(consumed);
4409 }
4410 self.kv_prefix.set_on_device(on_device);
4411 } else {
4412 self.kv_history = consumed.to_vec();
4413 self.kv_history_device = on_device;
4414 }
4415 }
4416
4417 pub fn o1_active(&self) -> bool {
4419 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4420 }
4421
4422 pub fn generation_batch_k(&self) -> usize {
4440 if let Some(k) = std::env::var("CMF_BATCH_K")
4441 .ok()
4442 .and_then(|v| v.parse::<usize>().ok())
4443 {
4444 return k;
4445 }
4446 if self.g4_batch_prefill() {
4449 return 128;
4450 }
4451 #[cfg(not(target_os = "macos"))]
4456 if self.o1_active()
4457 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4458 && crate::gpu::enabled_here()
4459 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
4460 {
4461 return 128;
4462 }
4463 #[cfg(not(target_os = "macos"))]
4464 if self.graph_prefill_preferred() && !self.o1_active() {
4465 let moe = self
4469 .weights
4470 .layers
4471 .iter()
4472 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4473 let q82 = crate::gpu::moe_q82_graph_on()
4479 && self.weights.layers.iter().any(|lw| {
4480 matches!(&lw.ffn, FfnKind::Moe(m)
4481 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4482 });
4483 let q4tp_moe = self.weights.layers.iter().any(|lw| {
4490 matches!(&lw.ffn, FfnKind::Moe(m)
4491 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4492 });
4493 return if q82 {
4494 128
4495 } else if q4tp_moe || self.onevision2_prefill() {
4496 256
4497 } else if moe {
4498 32
4499 } else {
4500 128
4501 };
4502 }
4503 0
4504 }
4505
4506 pub fn generation_graph_prefill(&self) -> bool {
4507 let graph = self.graph_prefill_preferred();
4508 #[cfg(not(target_os = "macos"))]
4519 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4520 return false;
4521 }
4522 graph
4523 }
4524
4525 pub fn o1_device_stats(&self) -> (usize, u64) {
4530 crate::gpu::o1_device_stats(self.graph_kv_id)
4531 }
4532
4533 pub fn o1_begin(&mut self) {
4538 self.o1_begin_with_prefix(None);
4539 }
4540
4541 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4545 if let Some(c) = &self.o1_cfg {
4546 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4547 let boundary = requested_prefix.map(|p| {
4548 p.max(
4549 crate::nystrom::o1_deferred_boundary(w, sink)
4550 .expect("o1 config boundary validated in set_o1"),
4551 )
4552 });
4553 for (li, &f) in self.o1_flags.iter().enumerate() {
4554 if f {
4555 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4556 }
4557 }
4558 }
4559 }
4560
4561 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4563 self.o1_cfg.as_ref().and_then(|c| {
4564 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4565 .map(|floor| requested_prefix.max(floor))
4566 })
4567 }
4568
4569 fn o1_note_transition(&mut self) {
4570 let mut transitioned = false;
4574 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4575 if flagged {
4576 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4577 }
4578 }
4579 if transitioned {
4580 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4581 }
4582 }
4583
4584 fn o1_pending(&self) -> bool {
4585 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4586 f && self.kv_cache.layers[li].seq_len > 0
4587 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4588 })
4589 }
4590
4591 fn o1_fail(&mut self, err: String) {
4592 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4593 self.clear_sequence_state();
4594 self.graph_failed
4595 .store(true, std::sync::atomic::Ordering::Relaxed);
4596 self.cancel
4597 .store(true, std::sync::atomic::Ordering::Relaxed);
4598 }
4599
4600 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4605 if self.o1_cfg.is_none() {
4606 return Ok(false);
4607 }
4608 let mut participating = false;
4609 for li in 0..self.num_layers {
4610 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4611 continue;
4612 }
4613 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4614 return Err(err);
4615 }
4616 if self.kv_cache.layers[li].seq_len == 0 {
4617 continue;
4618 }
4619 participating = true;
4620 let num_heads = self.layer_num_heads(li);
4621 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4622 }
4623 self.o1_note_transition();
4624 for li in 0..self.num_layers {
4625 if self.o1_flags.get(li).copied().unwrap_or(false) {
4626 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4627 return Err(err);
4628 }
4629 }
4630 }
4631 Ok(participating
4632 && (0..self.num_layers).all(|li| {
4633 !self.o1_flags.get(li).copied().unwrap_or(false)
4634 || self.kv_cache.layers[li].seq_len == 0
4635 || self.kv_cache.layers[li].o1_sealed()
4636 }))
4637 }
4638
4639 fn o1_progress(&mut self) {
4642 if !self.o1_active() {
4643 return;
4644 }
4645 for li in 0..self.num_layers {
4646 if self.o1_flags.get(li).copied().unwrap_or(false) {
4647 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4648 self.o1_fail(err);
4649 return;
4650 }
4651 }
4652 }
4653 self.o1_note_transition();
4657 if !self.o1_pending() {
4658 return;
4659 }
4660 if let Err(err) = self.o1_seal_checked() {
4661 self.o1_fail(err);
4662 }
4663 }
4664
4665 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4670 if self
4671 .graph_failed
4672 .swap(false, std::sync::atomic::Ordering::Relaxed)
4673 {
4674 self.cancel
4675 .store(false, std::sync::atomic::Ordering::Relaxed);
4676 self.clear_sequence_state();
4677 return Err(format!("{phase}: deferred O(1) transition failed"));
4678 }
4679 Ok(())
4680 }
4681
4682 pub fn o1_seal(&mut self) {
4686 if let Err(err) = self.o1_seal_checked() {
4687 self.o1_fail(err);
4688 }
4689 }
4690
4691 fn o1_prefix_on_graph(
4700 &mut self,
4701 ids: &[u32],
4702 chunk: usize,
4703 ) -> Result<Option<Vec<f32>>, String> {
4704 let o1_layers: Vec<usize> = (0..self.num_layers)
4705 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
4706 .collect();
4707 let Some(&first) = o1_layers.first() else {
4708 return Ok(None);
4709 };
4710 let (nkv, hd) = {
4711 let c = &self.kv_cache.layers[first];
4712 (c.num_kv_heads, c.head_dim)
4713 };
4714 let nh = self.num_heads;
4715 if chunk == 0
4716 || o1_layers.iter().any(|&li| {
4717 let c = &self.kv_cache.layers[li];
4718 self.layer_num_heads(li) != nh
4719 || (c.num_kv_heads, c.head_dim) != (nkv, hd)
4720 || c.seq_len != 0
4721 || c.o1_pending_boundary().is_none()
4722 })
4723 || !crate::gpu::o1_qcap_begin(&o1_layers)
4724 {
4725 return Ok(None);
4726 }
4727 let hs = self.hidden_size;
4728 let n = ids.len();
4729 let mut last = vec![0f32; hs];
4730 let mut pos = 0;
4731 while pos < n {
4732 let end = (pos + chunk).min(n);
4733 let bk = end - pos;
4734 let mut hiddens = vec![0f32; bk * hs];
4735 for (j, &id) in ids[pos..end].iter().enumerate() {
4736 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4737 }
4738 let positions: Vec<usize> = (pos..end).collect();
4739 match self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None) {
4740 crate::gpu::BatchGraphOutcome::Completed => {
4741 last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4742 pos = end;
4743 }
4744 crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
4745 let _ = crate::gpu::o1_qcap_take();
4746 return Ok(None);
4747 }
4748 _ => {
4749 let _ = crate::gpu::o1_qcap_take();
4750 return Err("O(1) prefix batch graph stopped after admission".into());
4751 }
4752 }
4753 }
4754 let q = crate::gpu::o1_qcap_take().ok_or("O(1) prefix: query readback failed")?;
4755 let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
4756 let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
4757 .ok_or("O(1) prefix: K/V readback failed")?;
4758 let (row, qrow) = (nkv * hd, nh * hd);
4759 for ((li, qs), (k, v)) in q.into_iter().zip(kv) {
4760 if k.len() != n * row || v.len() != n * row || qs.len() != n * qrow {
4761 return Err(format!(
4762 "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
4763 k.len(),
4764 v.len(),
4765 qs.len(),
4766 n * row,
4767 n * row,
4768 n * qrow
4769 ));
4770 }
4771 let cache = &mut self.kv_cache.layers[li];
4772 for p in 0..n {
4773 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
4774 cache.o1_push_q(&qs[p * qrow..(p + 1) * qrow]);
4775 }
4776 }
4777 Ok(Some(last))
4778 }
4779
4780 pub fn set_trace(&mut self, on: bool) {
4782 self.trace = on;
4783 }
4784
4785 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4788 self.rng = match config.seed {
4789 Some(seed) => SplitMix64::new(seed),
4790 None => SplitMix64::from_entropy(),
4791 };
4792 self.sampler_config = config;
4793 }
4794
4795 pub fn set_confidence(&mut self, on: bool) {
4800 self.confidence_on = on;
4801 }
4802
4803 pub fn set_calib_temp(&mut self, t: f32) {
4806 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4807 }
4808
4809 pub fn calib_temp(&self) -> f32 {
4811 self.calib_temp
4812 }
4813
4814 pub fn rope_inv_freq(&self) -> &[f32] {
4818 self.inv_freq.as_slice()
4819 }
4820
4821 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4824 self.rotary_dim = rotary_dim.min(self.head_dim);
4825 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4826 self.embryo_graph = None;
4830 }
4831
4832 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4833 QwenAttnCfg {
4834 mrope: None,
4835 num_heads: self.num_heads,
4836 num_kv_heads: self.num_kv_heads,
4837 head_dim: self.head_dim,
4838 hidden_size: self.hidden_size,
4839 position,
4840 inv_freq: &self.inv_freq,
4841 rotary_dim: self.rotary_dim,
4842 scale: self.attn_scale,
4843 softcap: self.attn_softcap,
4844 window: None,
4845 v_norm: false,
4846 qk_norm_after_rope: self.qk_norm_after_rope,
4847 gate_sigmoid: self.proj_gate_sigmoid,
4848 q_norm: None,
4849 k_norm: None,
4850 output_gate: false,
4851 softplus_gate: None,
4852 rope_scale: self.rope_scale,
4853 bias: None,
4854 rms_eps: self.rms_eps,
4855 norm_style: self.norm_style,
4856 pool: self.pool.as_deref(),
4857 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4858 }
4859 }
4860
4861 pub fn generate(
4863 &mut self,
4864 prompt: &str,
4865 max_tokens: usize,
4866 task_mask: Option<&TaskMask>,
4867 on_token: Option<TokenCallback>,
4868 ) -> Result<GenerateResult, String> {
4869 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4870 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4871 }
4872
4873 pub fn generate_from_vl(
4876 &mut self,
4877 input: &crate::dsv41_vision::PreparedVlInputs,
4878 max_tokens: usize,
4879 task_mask: Option<&TaskMask>,
4880 on_token: Option<TokenCallback>,
4881 ) -> Result<GenerateResult, String> {
4882 let Some(dsv41) = &self.dsv41 else {
4883 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4884 };
4885 if input.token_ids.is_empty() {
4886 return Err("empty V4.1 multimodal prompt".into());
4887 }
4888 if input.token_types.len() != input.token_ids.len() {
4889 return Err(format!(
4890 "V4.1 token type count {} != token count {}",
4891 input.token_types.len(),
4892 input.token_ids.len()
4893 ));
4894 }
4895 let dim = dsv41.2.dim;
4896 let mut embeddings = vec![None; input.token_ids.len()];
4897 let mut participates = vec![true; input.token_ids.len()];
4898 if !input.images.is_empty() {
4899 let vision = self
4900 .dsv41_vision
4901 .as_ref()
4902 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4903 for image in &input.images {
4904 let end = image.start.saturating_add(image.types.len());
4905 if end > input.token_ids.len() {
4906 return Err(format!(
4907 "V4.1 image span {}..{} exceeds prompt length {}",
4908 image.start,
4909 end,
4910 input.token_ids.len()
4911 ));
4912 }
4913 let mut span = vec![0.0f32; image.types.len() * dim];
4914 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4915 for (offset, &kind) in image.types.iter().enumerate() {
4916 let pos = image.start + offset;
4917 if input.token_types[pos] != kind {
4918 return Err(format!(
4919 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4920 input.token_types[pos]
4921 ));
4922 }
4923 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4924 participates[pos] = false;
4925 }
4926 }
4927 }
4928 for (pos, &kind) in input.token_types.iter().enumerate() {
4929 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4930 return Err(format!("V4.1 text position {pos} has an image embedding"));
4931 }
4932 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4933 return Err(format!("V4.1 image position {pos} has no image embedding"));
4934 }
4935 }
4936 self.dsv41_prefill = Some((embeddings, participates));
4937 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4938 self.dsv41_prefill = None;
4939 result
4940 }
4941
4942 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4944 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4945 }
4946
4947 pub fn generate_from_ids(
4955 &mut self,
4956 input_ids: &[u32],
4957 max_tokens: usize,
4958 task_mask: Option<&TaskMask>,
4959 on_token: Option<TokenCallback>,
4960 ) -> Result<GenerateResult, String> {
4961 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4962 }
4963
4964 pub fn generate_from_embeds(
4970 &mut self,
4971 input_ids: &[u32],
4972 prompt_rows: &[f32],
4973 max_tokens: usize,
4974 task_mask: Option<&TaskMask>,
4975 on_token: Option<TokenCallback>,
4976 ) -> Result<GenerateResult, String> {
4977 if input_ids.is_empty()
4978 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4979 {
4980 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4981 }
4982 if prompt_rows.iter().any(|x| !x.is_finite()) {
4983 return Err("embedded prompt contains non-finite values".into());
4984 }
4985 if !self.can_prefill_batched()
4986 || self.dyn_router.is_some()
4987 || self.o1_active()
4988 || self.mtp.is_some()
4989 || self.gpu_plan.is_some()
4990 {
4991 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4992 }
4993 self.generate_with_prompt_rows(
4994 input_ids,
4995 Some(prompt_rows),
4996 max_tokens,
4997 task_mask,
4998 on_token,
4999 )
5000 }
5001
5002 fn generate_with_prompt_rows(
5003 &mut self,
5004 input_ids: &[u32],
5005 prompt_rows: Option<&[f32]>,
5006 max_tokens: usize,
5007 task_mask: Option<&TaskMask>,
5008 mut on_token: Option<TokenCallback>,
5009 ) -> Result<GenerateResult, String> {
5010 #[cfg(target_os = "macos")]
5011 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
5012 if std::env::var("CMF_TRACE_H").is_ok() {
5013 eprintln!("input_ids: {input_ids:?}");
5014 }
5015 if input_ids.is_empty() {
5016 return Err("empty prompt: nothing to generate from".to_string());
5017 }
5018 self.graph_failed
5022 .store(false, std::sync::atomic::Ordering::Relaxed);
5023 let task_mask = self.drop_open_mask(task_mask);
5028 if self.clover.is_some() {
5031 if prompt_rows.is_some() || task_mask.is_some() {
5032 return Err("block-diffusion generation takes token prompts without task masks".into());
5033 }
5034 return self.clover_generate(input_ids, max_tokens, on_token);
5035 }
5036
5037 let mut reuse_from = {
5045 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
5046 if on
5047 && prompt_rows.is_none()
5048 && task_mask.is_none()
5049 && self.mtp.is_none()
5050 && !(self.mimo_mtp.is_some() && self.speculative)
5051 && self.o1_cfg.is_none()
5052 && self.dsv41.is_none()
5053 {
5054 self.cached_prefix_len(input_ids)
5055 } else {
5056 0
5057 }
5058 };
5059 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
5062 reuse_from = 0;
5063 }
5064 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
5067 for l in &mut self.kv_cache.layers {
5068 let d = l.pos_len() - reuse_from;
5069 l.truncate_last(d);
5070 }
5071 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5072 eprintln!(
5073 "kv-reuse: rolled back {} answer position(s) to the common prefix",
5074 self.kv_history.len() - reuse_from
5075 );
5076 }
5077 self.kv_history.truncate(reuse_from);
5078 self.graph_logits = None;
5079 }
5080 self.swa_trim_anchor = input_ids.len();
5081 self.last_prefill_tokens = input_ids.len() - reuse_from;
5082 let bounded_native = self.bounded_native();
5083 if reuse_from == 0 {
5084 self.reset_fresh_mtp_draft_state();
5089 self.clear_sequence_state();
5090 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
5091 eprintln!(
5092 "kv-reuse: {} of {} prompt positions already cached",
5093 reuse_from,
5094 input_ids.len()
5095 );
5096 }
5097 crate::gpu::graph_race_begin_generation();
5098 let o1_prefill = if self.o1_active() && task_mask.is_none() {
5102 std::env::var("CMF_O1_PREFILL")
5103 .ok()
5104 .and_then(|v| v.parse::<usize>().ok())
5105 .filter(|&p| p > 0)
5106 } else {
5107 None
5108 };
5109 if task_mask.is_none() {
5110 self.o1_begin_with_prefix(o1_prefill);
5111 }
5112
5113 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
5119 #[cfg(target_os = "macos")]
5157 let metal_graph = crate::gpu::q1_force()
5158 && crate::gpu::enabled_here()
5159 && std::env::var("CMF_GPU_BLOCK")
5160 .map(|v| v != "0")
5161 .unwrap_or(true);
5162 #[cfg(target_os = "macos")]
5168 let native_metal_backend = crate::gpu::q1_force();
5169 #[cfg(not(target_os = "macos"))]
5170 let metal_graph = false;
5171 #[cfg(not(target_os = "macos"))]
5172 let native_metal_backend = false;
5173 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
5174 let spec_cheap_round =
5178 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
5179 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
5180 || match spec_sample_env.as_deref() {
5181 Some("1") => true,
5182 Some(_) => false,
5183 None => metal_graph && spec_cheap_round,
5184 };
5185 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
5201 for lw in &self.weights.layers {
5202 if let FfnKind::Dense(d) = &lw.ffn {
5203 dense_n += 1;
5204 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
5205 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
5206 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
5207 {
5208 dense_q4tp += 1;
5209 }
5210 }
5211 }
5212 #[cfg(feature = "gpu")]
5221 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
5222 #[cfg(not(feature = "gpu"))]
5223 let mtp_on_graph = true;
5224 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
5225 let penalized = !metal_graph
5237 && (self.sampler_config.repetition_penalty != 1.0
5238 || self.sampler_config.presence_penalty != 0.0
5239 || !self.sampler_config.suppress_tokens.is_empty());
5240 #[cfg(feature = "gpu")]
5245 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
5246 #[cfg(not(feature = "gpu"))]
5247 let metal_wgpu = false;
5248 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
5249 let spec_wanted = graph_spec_wanted_by_policy(
5250 spec_env.as_deref(),
5251 spec_default_ok,
5252 penalized,
5253 metal_wgpu,
5254 native_metal_backend,
5255 );
5256 if spec_wanted && metal_wgpu {
5257 tracing::warn!(
5258 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
5259 verified on this backend (garbage measured on Qwen3.5-0.8B)"
5260 );
5261 }
5262 let graph_spec = self.speculative
5266 && (graph_on || metal_graph)
5267 && self.mtp.is_some()
5268 && task_mask.is_none()
5269 && !self.o1_active()
5270 && spec_sampling_ok
5271 && spec_wanted;
5272 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
5285 let pair_pays = host_pair_opt_in
5286 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
5287 #[cfg(target_os = "macos")]
5291 if metal_graph {
5292 static SAID: std::sync::Once = std::sync::Once::new();
5293 SAID.call_once(|| {
5294 let spec = if graph_spec {
5295 let k = std::env::var("CMF_GRAPH_SPEC_K")
5296 .ok()
5297 .and_then(|v| v.parse::<usize>().ok())
5298 .filter(|&v| (1..=8).contains(&v))
5299 .unwrap_or(7);
5300 let arm = if self.sampler_config.temperature < 1e-6 {
5301 "greedy"
5302 } else {
5303 "sampling"
5304 };
5305 format!(
5306 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
5307 Self::draft_vocab_rows(usize::MAX)
5308 )
5309 } else if !self.speculative {
5310 "spec off (CMF_MTP=0)".to_string()
5311 } else if self.mtp.is_none() {
5312 "spec off (no MTP head)".to_string()
5313 } else if !spec_sampling_ok {
5314 if spec_cheap_round {
5315 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
5316 } else {
5317 "spec off (sampling without a top-k: the dense chain \
5318 costs more than it saves)"
5319 .to_string()
5320 }
5321 } else if !spec_wanted {
5322 if spec_env.is_none() {
5323 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5324 } else {
5325 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5326 }
5327 } else if task_mask.is_some() {
5328 "spec off (task mask)".to_string()
5329 } else if !graph_spec && !pair_pays {
5330 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5331 .to_string()
5332 } else {
5333 "spec off (O(1) attention)".to_string()
5334 };
5335 let on = |var: &str| {
5336 if std::env::var(var).as_deref() == Ok("0") {
5337 "off"
5338 } else {
5339 "on"
5340 }
5341 };
5342 tracing::info!(
5343 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5344 MTP graph {}, attend {}, probe {}",
5345 if crate::gpu_metal::state4_on() {
5346 "on"
5347 } else {
5348 "off"
5349 },
5350 if crate::gpu_metal::async_replay_on() {
5351 "on"
5352 } else {
5353 "off"
5354 },
5355 on("CMF_METAL_PREFILL"),
5356 on("CMF_MTP_GRAPH"),
5357 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5358 if crate::gpu::probe_enabled() {
5359 "bypassed (q1 force)"
5360 } else {
5361 "off"
5362 },
5363 );
5364 });
5365 }
5366 let spec_active = self.speculative
5367 && self.mtp.is_some()
5368 && task_mask.is_none()
5369 && !self.o1_active()
5370 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5371 let mut mtp = if spec_active { self.mtp.take() } else { None };
5374 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5375 eprintln!(
5376 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5377 mtp.is_some(),
5378 self.speculative,
5379 self.sampler_config.temperature < 1e-6,
5380 );
5381 }
5382 if let Some(m) = &mut mtp {
5383 m.kv.clear();
5384 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5386 self.mtp_graph_mode = None;
5387 }
5388 let mimo_spec = self.speculative
5392 && self.mimo_mtp.is_some()
5393 && task_mask.is_none()
5394 && !self.o1_active()
5395 && self.dyn_router.is_none()
5396 && self.sampler_config.temperature < 1e-6
5397 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5398 if let Some(st) = self.mimo_mtp.as_mut() {
5399 st.reset();
5400 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5401 Self::mimo_mtp_hist_cap(st, input_ids.len());
5402 }
5403 }
5404 let mut router = if mtp.is_none() {
5408 self.dyn_router.take()
5409 } else {
5410 None
5411 };
5412 let mut reuse_from = reuse_from;
5413 if let Some(r) = &mut router {
5414 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5417 let _ = self.set_active_skill(None);
5420 reuse_from = 0;
5421 self.last_prefill_tokens = input_ids.len();
5422 }
5423 }
5424
5425 let mut all_ids = input_ids.to_vec();
5426 let mut generated = 0usize;
5427 let mut finish_reason = "max_tokens".to_string();
5428 let mut drafted = 0usize;
5429 let mut accepted = 0usize;
5430 let mut dsv4_spec_bad = 0usize;
5437 let mut dsv4_spec_retry_at = 0usize;
5438 let mut confidence: Vec<f32> = Vec::new();
5439 let trace_on = self.trace;
5440 let calib_temp = self.calib_temp;
5441 let mut traces: Vec<TokenTrace> = Vec::new();
5442
5443 let mut hidden = vec![0.0f32; self.hidden_size];
5449 let mut pos = reuse_from;
5450 let fuse_lm = mtp.is_none()
5459 && router.is_none()
5460 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5461 self.graph_logits = None;
5462 self.graph_want_logits = false;
5463 let _tpf = std::time::Instant::now();
5464 let batch_k = self.generation_batch_k();
5465 if let Some(rows) = prompt_rows {
5466 let hs = self.hidden_size;
5467 let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
5469 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5470 let end = (pos + chunk).min(input_ids.len());
5471 let hb = match self.prefill_input_rows(
5472 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5473 pos,
5474 task_mask,
5475 ) {
5476 Ok(hb) => hb,
5477 Err(err) => {
5478 self.prompt_mrope = None;
5479 self.finish_generation(&mut mtp, &mut router, true);
5480 return Err(err);
5481 }
5482 };
5483 if mimo_spec {
5484 self.mimo_note_rows(&hb, pos);
5485 }
5486 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5487 pos = end;
5488 }
5489 }
5490 self.prompt_mrope = None;
5493 while self.qwen4_exp.is_some()
5504 && mtp.is_none()
5505 && pos < input_ids.len()
5506 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5507 {
5508 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5511 let want_logits = end == input_ids.len();
5512 let mut lg = Vec::new();
5513 if let Some(b) = &mut self.qwen4_exp {
5514 crate::qwen4_exp::forward_tokens(
5515 &b.0,
5516 &b.1,
5517 &b.2,
5518 &mut b.3,
5519 &input_ids[pos..end],
5520 pos,
5521 &self.inv_freq,
5522 self.pool.as_deref(),
5523 &mut lg,
5524 want_logits,
5525 );
5526 }
5527 if want_logits {
5528 self.graph_logits = Some(lg);
5529 }
5530 pos = end;
5531 hidden.fill(0.0);
5532 }
5533 while self.dsv4.is_some()
5534 && mtp.is_none()
5535 && pos < input_ids.len()
5536 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5537 {
5538 let end = (pos + prefill_chunk()).min(input_ids.len());
5539 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5540 let mut lg = Vec::new();
5541 if let Some(b) = &mut self.dsv4 {
5542 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5543 crate::dsv4::forward_chunk(
5544 g,
5545 layers,
5546 &cfg,
5547 st,
5548 &ids,
5549 pos,
5550 &self.inv_freq,
5551 self.pool.as_deref(),
5552 &mut lg,
5553 end == input_ids.len(),
5554 );
5555 }
5556 if end == input_ids.len() {
5557 self.graph_logits = Some(lg);
5558 }
5559 pos = end;
5560 hidden = vec![0.0; self.hidden_size];
5561 }
5562 let dsv41_prefill = self.dsv41_prefill.take();
5563 while self.dsv41.is_some()
5564 && mtp.is_none()
5565 && pos < input_ids.len()
5566 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5567 {
5568 let end = (pos + prefill_chunk()).min(input_ids.len());
5569 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5570 let mut lg = Vec::new();
5571 if let Some(b) = &mut self.dsv41 {
5572 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5573 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5574 crate::dsv41::forward_chunk_masked_with_embeddings(
5575 g,
5576 layers,
5577 cfg,
5578 st,
5579 &ids,
5580 pos,
5581 &embeddings[pos..end],
5582 &participates[pos..end],
5583 self.pool.as_deref(),
5584 &mut lg,
5585 );
5586 } else {
5587 crate::dsv41::forward_chunk(
5588 g,
5589 layers,
5590 cfg,
5591 st,
5592 &ids,
5593 pos,
5594 self.pool.as_deref(),
5595 &mut lg,
5596 );
5597 }
5598 }
5599 if end == input_ids.len() {
5600 self.graph_logits = Some(lg);
5601 }
5602 pos = end;
5603 hidden = vec![0.0; self.hidden_size];
5604 }
5605 let dyn_prefill = router.is_some();
5610 let o1_prefill_limit = o1_prefill
5618 .and_then(|requested| self.o1_effective_boundary(requested))
5619 .map(|boundary| boundary.min(input_ids.len()));
5620 let mut o1_sealed = false;
5621 if let Some(limit) = o1_prefill_limit {
5622 if pos == 0
5629 && limit < input_ids.len()
5630 && batch_k > 0
5631 && task_mask.is_none()
5632 && mtp.is_none()
5633 && !dyn_prefill
5634 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5635 && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
5636 && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
5637 {
5638 let t_px = std::time::Instant::now();
5639 match self.o1_prefix_on_graph(&input_ids[..limit], batch_k) {
5640 Ok(Some(last)) => {
5641 hidden.copy_from_slice(&last);
5642 pos = limit;
5643 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5644 eprintln!(
5645 "o1 prefix on the batch graph: {limit} rows in {:.1} ms",
5646 t_px.elapsed().as_secs_f64() * 1e3
5647 );
5648 }
5649 }
5650 Ok(None) => {}
5651 Err(err) => {
5652 self.finish_generation(&mut mtp, &mut router, true);
5653 return Err(err);
5654 }
5655 }
5656 }
5657 if pos < limit && self.can_prefill_batched() && limit > 2 {
5660 let chunk = self.prefill_chunk();
5661 let hs = self.hidden_size;
5662 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5663 let end = (pos + chunk).min(limit);
5664 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5665 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5666 pos = end;
5667 }
5668 } else {
5669 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5670 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5671 pos += 1;
5672 }
5673 }
5674 if pos >= limit {
5675 o1_sealed = match self.o1_seal_checked() {
5676 Ok(sealed) => sealed,
5677 Err(err) => {
5678 self.finish_generation(&mut mtp, &mut router, true);
5679 return Err(err);
5680 }
5681 };
5682 tracing::info!(
5683 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5684 o1_prefill.unwrap_or(0),
5685 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5686 .unwrap_or(limit),
5687 limit,
5688 input_ids.len()
5689 );
5690 }
5691 }
5692 let graph_prefill = self.graph_prefill_preferred();
5698 #[cfg(target_os = "macos")]
5706 if task_mask.is_none()
5707 && !dyn_prefill
5708 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5709 && crate::gpu::enabled_here()
5710 && self.gdn_cfg.is_some()
5711 && !self.per_token_stack()
5712 && input_ids.len() > 8
5713 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5714 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5715 {
5716 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5717 .ok()
5718 .and_then(|v| v.parse().ok())
5719 .filter(|&v| (16..=512).contains(&v))
5720 .unwrap_or(256);
5721 let hs = self.hidden_size;
5722 let _tp = std::time::Instant::now();
5723 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5724 let end = (pos + chunk).min(input_ids.len());
5725 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5726 MetalPrefillOutcome::Completed(hb) => hb,
5727 MetalPrefillOutcome::Declined => break,
5728 MetalPrefillOutcome::Failed => {
5729 self.finish_generation(&mut mtp, &mut router, true);
5730 return Err("ordinary Metal prefill failed after admission".into());
5731 }
5732 };
5733 if let Some(m) = &mut mtp {
5734 let n_pairs = if end < input_ids.len() {
5735 end - pos
5736 } else {
5737 end - pos - 1
5738 };
5739 if n_pairs > 0 {
5740 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5741 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5742 .collect();
5743 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5744 for (j, (h, t)) in pairs.iter().enumerate() {
5745 let h = h.to_vec();
5746 let _ = self.mtp_step(m, &h, *t, pos + j);
5747 }
5748 }
5749 }
5750 }
5751 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5752 pos = end;
5753 }
5754 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5755 eprintln!(
5756 "metal-prefill: {} of {} tokens in {:.1} ms",
5757 pos,
5758 input_ids.len(),
5759 _tp.elapsed().as_secs_f64() * 1e3
5760 );
5761 }
5762 }
5763 #[cfg(not(target_os = "macos"))]
5768 if task_mask.is_none()
5769 && !dyn_prefill
5770 && mtp.is_none()
5771 && o1_prefill.is_none()
5772 && batch_k > 0
5773 && pos + 1 < input_ids.len()
5774 && self.g4_batch_prefill()
5775 {
5776 let t_g4 = std::time::Instant::now();
5777 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5778 Ok((n, last)) => {
5779 if n > 0 {
5780 hidden.copy_from_slice(&last);
5781 }
5782 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5783 eprintln!(
5784 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5785 input_ids.len() - pos,
5786 t_g4.elapsed().as_secs_f64() * 1e3
5787 );
5788 }
5789 pos += n;
5790 }
5791 Err(e) => {
5792 self.finish_generation(&mut mtp, &mut router, true);
5793 return Err(e);
5794 }
5795 }
5796 }
5797 self.mimo_moe_prepare();
5798 #[cfg(not(target_os = "macos"))]
5804 if task_mask.is_none()
5805 && !dyn_prefill
5806 && !graph_prefill
5807 && mtp.is_none()
5808 && o1_prefill.is_none()
5809 && !self.o1_active()
5810 && input_ids.len() > 2
5811 && self.batch_prefix_prefill()
5812 {
5813 let chunk = self.prefill_chunk().max(1);
5814 let hs = self.hidden_size;
5815 let t_bp = std::time::Instant::now();
5816 let pos0 = pos;
5817 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5818 let end = (pos + chunk).min(input_ids.len());
5819 let bk = end - pos;
5820 let mut hiddens = vec![0f32; bk * hs];
5821 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5822 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5823 }
5824 let positions: Vec<usize> = (pos..end).collect();
5825 let mut run = 0usize;
5826 let outcome = self.try_batch_graph_wgpu_prefix(
5827 &mut hiddens,
5828 &positions,
5829 bk,
5830 None,
5831 Some(&mut run),
5832 );
5833 match outcome {
5834 crate::gpu::BatchGraphOutcome::Completed => {
5835 let hb = if run < self.num_layers {
5836 self.prefill_batch_span(
5837 PrefillIn::Hidden(&hiddens),
5838 pos,
5839 None,
5840 run,
5841 self.num_layers,
5842 )
5843 } else {
5844 hiddens
5845 };
5846 if mimo_spec {
5847 self.mimo_note_rows(&hb, pos);
5848 }
5849 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5850 pos = end;
5851 }
5852 crate::gpu::BatchGraphOutcome::Failed => {
5853 self.finish_generation(&mut mtp, &mut router, true);
5854 return Err("batched prefix prefill failed after admission".into());
5855 }
5856 crate::gpu::BatchGraphOutcome::Declined => {
5857 #[cfg(feature = "gpu")]
5860 if pos > pos0 {
5861 self.pull_lagging_host_kv(0, self.num_layers, pos);
5862 }
5863 break;
5864 }
5865 }
5866 }
5867 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5868 eprintln!(
5869 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5870 pos - pos0,
5871 input_ids.len(),
5872 t_bp.elapsed().as_secs_f64() * 1e3
5873 );
5874 }
5875 }
5876 if task_mask.is_none()
5877 && !dyn_prefill
5878 && !graph_prefill
5879 && self.can_prefill_batched()
5880 && !self.per_token_stack()
5881 && o1_prefill.is_none()
5882 && input_ids.len() > 2
5883 {
5884 let chunk = self.prefill_chunk();
5890 let hs = self.hidden_size;
5891 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5892 let end = (pos + chunk).min(input_ids.len());
5893 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5894 if mimo_spec {
5895 self.mimo_note_rows(&hb, pos);
5896 }
5897 if let Some(m) = &mut mtp {
5898 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5899 .ok()
5900 .and_then(|v| v.parse().ok())
5901 .unwrap_or(0);
5902 for p in pos..end {
5903 if p + 1 < input_ids.len() {
5904 if probe >= 1 && p + 2 < input_ids.len() {
5905 let (d1, mut hx) = self.mtp_step_h(
5909 m,
5910 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5911 input_ids[p + 1],
5912 p,
5913 );
5914 let mut ok = d1 == input_ids[p + 2];
5915 Self::chain_probe_note(0, ok);
5916 let mut d_prev = d1;
5917 let mut extra = 0usize;
5918 for j in 1..probe {
5919 if p + 2 + j >= input_ids.len() {
5920 break;
5921 }
5922 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5923 extra += 1;
5924 ok = ok && dj == input_ids[p + 2 + j];
5925 Self::chain_probe_note(j, ok);
5926 d_prev = dj;
5927 hx = hj;
5928 }
5929 m.kv.truncate_last(extra);
5930 } else {
5931 let _ = self.mtp_step(
5932 m,
5933 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5934 input_ids[p + 1],
5935 p,
5936 );
5937 }
5938 }
5939 }
5940 }
5941 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5942 pos = end;
5943 }
5944 }
5945 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5946 if task_mask.is_none()
5947 && !dyn_prefill
5948 && !graph_prefill
5949 && !pair_off
5950 && self.pair_supported()
5951 && o1_prefill.is_none()
5952 {
5953 while pos + 1 < input_ids.len()
5954 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5955 {
5956 let e1 = self.embed_single(input_ids[pos]);
5957 let e2 = self.embed_single(input_ids[pos + 1]);
5958 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5959 if mimo_spec {
5960 self.mimo_note_rows(&h1, pos);
5961 self.mimo_note_rows(&h2, pos + 1);
5962 }
5963 self.commit_linear_scratch();
5965 if let Some(m) = &mut mtp {
5966 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5967 if pos + 2 < input_ids.len() {
5968 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5969 .ok()
5970 .and_then(|v| v.parse().ok())
5971 .unwrap_or(0);
5972 if probe >= 1 && pos + 3 < input_ids.len() {
5973 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5977 let mut ok = d1 == input_ids[pos + 3];
5978 Self::chain_probe_note(0, ok);
5979 let mut d_prev = d1;
5980 let mut extra = 0usize;
5981 for j in 1..probe {
5982 if pos + 3 + j >= input_ids.len() {
5983 break;
5984 }
5985 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5986 extra += 1;
5987 ok = ok && dj == input_ids[pos + 3 + j];
5988 Self::chain_probe_note(j, ok);
5989 d_prev = dj;
5990 hx = hj;
5991 }
5992 m.kv.truncate_last(extra);
5993 } else {
5994 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5995 }
5996 }
5997 }
5998 hidden = h2;
5999 pos += 2;
6000 }
6001 }
6002 let o1_batch_ready = o1_sealed
6015 && o1_prefill.is_some()
6016 && mtp.is_none()
6017 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6018 && (0..self.num_layers).all(|li| {
6019 let cache = &self.kv_cache.layers[self.phys_layer(li)];
6020 cache.o1.is_none() || cache.o1_views().is_some()
6021 });
6022 let mtp_batch_prefill = mtp.is_some()
6027 && graph_prefill
6028 && task_mask.is_none()
6029 && !dyn_prefill
6030 && !self.o1_active()
6031 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
6032 if batch_k > 0
6033 && (graph_prefill || o1_batch_ready)
6034 && task_mask.is_none()
6035 && (!self.o1_active() || o1_batch_ready)
6036 && (mtp.is_none() || mtp_batch_prefill)
6037 && !dyn_prefill
6038 && pos + 1 < input_ids.len()
6039 {
6040 let hs = self.hidden_size;
6041 let chunk = batch_k;
6042 while pos < input_ids.len() {
6043 let end = (pos + chunk).min(input_ids.len());
6044 let bk = end - pos;
6045 let mut hiddens = vec![0f32; bk * hs];
6046 for (j, &id) in input_ids[pos..end].iter().enumerate() {
6047 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6048 }
6049 let positions: Vec<usize> = (pos..end).collect();
6050 let t_chunk = std::time::Instant::now();
6051 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6052 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
6053 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6054 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
6055 eprintln!(
6056 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
6057 if o1_batch_ready {
6058 "o1"
6059 } else if mtp_batch_prefill {
6060 "ordinary_mtp"
6061 } else {
6062 "ordinary"
6063 },
6064 bk as f64 / (ms / 1000.0)
6065 );
6066 }
6067 {
6068 use std::sync::atomic::{AtomicBool, Ordering};
6069 static SAID: AtomicBool = AtomicBool::new(false);
6070 if !SAID.swap(true, Ordering::Relaxed) {
6071 if ok_b {
6072 tracing::info!(
6073 "batched prefill: ACTIVE mode={} (k={bk})",
6074 if o1_batch_ready {
6075 "o1"
6076 } else if mtp_batch_prefill {
6077 "ordinary_mtp"
6078 } else {
6079 "ordinary"
6080 }
6081 );
6082 } else {
6083 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
6084 }
6085 }
6086 }
6087 if ok_b {
6088 if mimo_spec {
6089 self.mimo_note_rows(&hiddens, pos);
6090 }
6091 if mtp_batch_prefill {
6092 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
6093 if n_pairs > 0 {
6094 let rows: Vec<Vec<f32>> = (0..n_pairs)
6100 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
6101 .collect();
6102 let pairs: Vec<(&[f32], u32)> = rows
6103 .iter()
6104 .enumerate()
6105 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
6106 .collect();
6107 if std::env::var("CMF_GRAPH_PROF").is_ok() {
6108 eprintln!(
6109 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
6110 pos,
6111 n_pairs,
6112 pos + n_pairs - 1,
6113 );
6114 }
6115 let warm_error = if let Some(m) = mtp.as_mut() {
6116 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
6117 } else {
6118 None
6119 };
6120 if let Some(err) = warm_error {
6121 self.finish_generation(&mut mtp, &mut router, true);
6126 return Err(err.to_string());
6127 }
6128 }
6129 }
6130 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6131 pos = end;
6132 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
6133 self.finish_generation(&mut mtp, &mut router, true);
6138 return Err(if o1_batch_ready {
6139 "sealed O(1) batch graph failed after admission".to_string()
6140 } else {
6141 "ordinary recurrent batch graph failed after admission".to_string()
6142 });
6143 } else {
6144 break; }
6146 }
6147 }
6148 if graph_prefill
6152 && task_mask.is_none()
6153 && mtp.is_none()
6154 && !dyn_prefill
6155 && pos == 0
6156 && input_ids.len() > 1
6157 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
6158 {
6159 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
6160 self.graph_logits = Some(lg);
6161 hidden = vec![0.0; self.hidden_size];
6162 pos = input_ids.len();
6163 }
6164 }
6165 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
6166 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
6167 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
6168 if mimo_spec {
6169 self.mimo_note_rows(&hidden, pos);
6170 }
6171 if let Some(m) = &mut mtp {
6172 if pos + 1 < input_ids.len() {
6173 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
6179 .ok()
6180 .and_then(|v| v.parse().ok())
6181 .unwrap_or(0);
6182 if probe >= 1 && pos + 2 < input_ids.len() {
6183 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
6184 let mut ok = d1 == input_ids[pos + 2];
6185 Self::chain_probe_note(0, ok);
6186 let mut d_prev = d1;
6187 let mut extra = 0usize;
6188 for j in 1..probe {
6189 if pos + 2 + j >= input_ids.len() {
6190 break;
6191 }
6192 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
6193 extra += 1;
6194 ok = ok && dj == input_ids[pos + 2 + j];
6195 Self::chain_probe_note(j, ok);
6196 d_prev = dj;
6197 hx = hj;
6198 }
6199 m.kv.truncate_last(extra);
6202 } else {
6203 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
6204 }
6205 }
6206 }
6207 pos += 1;
6208 }
6209 if std::env::var("CMF_PREFILL_PROF").is_ok() {
6210 eprintln!(
6211 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
6212 input_ids.len(),
6213 _tpf.elapsed().as_secs_f64() * 1000.0
6214 );
6215 }
6216 if self
6217 .graph_failed
6218 .swap(false, std::sync::atomic::Ordering::Relaxed)
6219 {
6220 self.finish_generation(&mut mtp, &mut router, true);
6225 return Err("GPU token graph failed during prefill".to_string());
6226 }
6227 if self
6230 .cancel
6231 .swap(false, std::sync::atomic::Ordering::Relaxed)
6232 {
6233 self.finish_generation(&mut mtp, &mut router, true);
6237 return Ok(GenerateResult {
6238 text: String::new(),
6239 token_ids: Vec::new(),
6240 prompt_tokens: input_ids.len(),
6241 tokens_generated: 0,
6242 finish_reason: "cancelled".to_string(),
6243 mtp_drafted: 0,
6244 mtp_accepted: 0,
6245 token_confidence: Vec::new(),
6246 traces: Vec::new(),
6247 });
6248 }
6249
6250 if !o1_sealed {
6253 match self.o1_seal_checked() {
6254 Ok(_) => {}
6255 Err(err) => {
6256 self.finish_generation(&mut mtp, &mut router, true);
6257 return Err(err);
6258 }
6259 }
6260 }
6261
6262 macro_rules! commit {
6264 ($id:expr) => {{
6265 all_ids.push($id);
6266 generated += 1;
6267 self.note_draft_id($id);
6268 if self.tokenizer.is_eos($id) && !self.ignore_eos {
6269 finish_reason = "stop".to_string();
6270 false
6271 } else {
6272 let token_text = self.tokenizer.decode_token($id);
6273 let mut go = true;
6274 if let Some(ref mut cb) = on_token {
6275 if !cb(&token_text) {
6276 finish_reason = "cancelled".to_string();
6277 go = false;
6278 }
6279 }
6280 go
6281 }
6282 }};
6283 }
6284
6285 let mut spec_trial = SpecTrial::Spec {
6296 t0: std::time::Instant::now(),
6297 gen0: generated,
6298 rounds: 0,
6299 };
6300 let mut spec_mon = SpecMon {
6306 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
6307 ..SpecMon::default()
6308 };
6309 let mut spec_watchdog_off = false;
6310 let mut spec_walls: Vec<f32> = Vec::new();
6313 let mut spec_round_end: Option<std::time::Instant> = None;
6316 if mimo_spec {
6317 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
6318 if let Some(mut st) = self.mimo_mtp.take() {
6319 self.mimo_mtp_probe(&mut st, input_ids, &path);
6320 self.mimo_mtp = Some(st);
6321 }
6322 }
6323 }
6324 let mut next_pos = input_ids.len();
6326 'decode: while generated < max_tokens {
6327 if self
6328 .graph_failed
6329 .swap(false, std::sync::atomic::Ordering::Relaxed)
6330 {
6331 self.finish_generation(&mut mtp, &mut router, true);
6336 return Err("GPU token graph failed during decode".to_string());
6337 }
6338 if self
6339 .cancel
6340 .swap(false, std::sync::atomic::Ordering::Relaxed)
6341 {
6342 finish_reason = "cancelled".to_string();
6343 break 'decode;
6344 }
6345 if mimo_spec && next_pos > 0 {
6350 self.mimo_note_rows(&hidden, next_pos - 1);
6353 }
6354 let forced = self.spec_forced.take();
6355 let mut logits = match (forced, self.graph_logits.take()) {
6356 (Some(_), _) => Vec::new(),
6357 (None, Some(lg)) => lg,
6358 (None, None) => {
6359 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6360 #[cfg(feature = "gpu")]
6365 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6366 lg
6367 } else {
6368 inference::rms_norm_into(
6369 &hidden,
6370 &self.weights.final_norm,
6371 self.rms_eps,
6372 self.norm_style,
6373 &mut self.ws.n1,
6374 );
6375 self.lm_head_forward(&self.ws.n1)
6376 };
6377 #[cfg(not(feature = "gpu"))]
6378 let logits_out = {
6379 inference::rms_norm_into(
6380 &hidden,
6381 &self.weights.final_norm,
6382 self.rms_eps,
6383 self.norm_style,
6384 &mut self.ws.n1,
6385 );
6386 self.lm_head_forward(&self.ws.n1)
6387 };
6388 logits_out
6389 }
6390 };
6391 if generated
6394 == std::env::var("CMF_LOGIT_DUMP_STEP")
6395 .ok()
6396 .and_then(|v| v.parse().ok())
6397 .unwrap_or(0)
6398 {
6399 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6400 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6401 for v in hidden.iter().chain(logits.iter()) {
6402 bytes.extend_from_slice(&v.to_le_bytes());
6403 }
6404 if let Err(e) = std::fs::write(&path, &bytes) {
6405 eprintln!("logit dump: failed to write {path}: {e}");
6406 self.finish_generation(&mut mtp, &mut router, true);
6407 return Err(format!("logit dump write failed: {e}"));
6408 }
6409 }
6410 }
6411 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6415 if !logits.is_empty() {
6416 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6417 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6418 if let Err(e) =
6419 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6420 {
6421 eprintln!("logit dump: failed to write {}: {e}", path.display());
6422 }
6423 }
6424 }
6425 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6426 Some(c) => c,
6427 None => {
6428 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6429 sampler::sample_with_scratch_pool(
6430 &logits,
6431 &self.sampler_config,
6432 self.sampler_config.penalty_past(&all_ids, bounded_native),
6433 &mut self.rng,
6434 &mut self.sampler_scratch,
6435 self.pool.as_deref(),
6436 )
6437 }
6438 };
6439 if self.confidence_on {
6440 confidence.push(if logits.is_empty() {
6441 0.0
6442 } else {
6443 sampler::top1_prob_pool(
6444 self.pool.as_deref(),
6445 &mut self.sampler_scratch,
6446 &logits,
6447 t_next,
6448 calib_temp,
6449 )
6450 });
6451 }
6452 if !logits.is_empty() {
6453 attention::recycle_buf(&mut logits);
6454 }
6455 if trace_on {
6456 let skill = router.as_ref().and_then(|r| r.active_id());
6460 traces.push(TokenTrace {
6461 t: generated,
6462 token_id: t_next,
6463 confidence: confidence.last().copied().unwrap_or(0.0),
6464 active_skill: skill,
6465 recon: None,
6466 switched: false,
6467 });
6468 }
6469 if !commit!(t_next) {
6470 break 'decode;
6471 }
6472 if generated >= max_tokens {
6473 break 'decode;
6474 }
6475
6476 self.swa_trim_tails();
6479 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6480 static SAID: std::sync::Once = std::sync::Once::new();
6486 SAID.call_once(|| {
6487 tracing::warn!(
6488 "KV cache full at {} positions — evicting half; quality \
6489 will degrade. Raise CMF_MAX_SEQ.",
6490 self.kv_cache.max_seq_len,
6491 );
6492 });
6493 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6494 self.kv_cache.evict(keep);
6495 }
6496
6497 if graph_spec {
6500 match spec_trial {
6501 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6502 spec_mon.plain_ms =
6503 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6504 let keep = spec_mon.pays();
6505 tracing::info!(
6506 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6507 spec_mon.tokens,
6508 spec_mon.round_ms,
6509 spec_mon.plain_ms,
6510 if keep { "speculating" } else { "plain" }
6511 );
6512 spec_mon.fails = 0;
6513 spec_trial = SpecTrial::Decided {
6514 spec: keep,
6515 recheck_at: if keep { usize::MAX } else { generated + 128 },
6516 };
6517 }
6518 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6519 spec_mon.n = 0;
6520 spec_mon.zero_accept_rounds = 0;
6521 spec_trial = SpecTrial::Spec {
6522 t0: std::time::Instant::now(),
6523 gen0: generated,
6524 rounds: 0,
6525 };
6526 }
6527 _ => {}
6528 }
6529 spec_watchdog_off = matches!(
6530 spec_trial,
6531 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6532 );
6533 }
6534 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6536 let budget = max_tokens - generated - 1;
6537 if let Some(mut st) = self.mimo_mtp.take() {
6538 let k = st.depth.min(budget);
6539 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6540 self.mimo_mtp = Some(st);
6541 let r = match r {
6542 Ok(r) => r,
6543 Err(err) => {
6544 self.finish_generation(&mut mtp, &mut router, true);
6545 return Err(err);
6546 }
6547 };
6548 if let Some(r) = r {
6549 drafted += r.drafted;
6550 accepted += r.accepted.len();
6551 let mut stopped = false;
6552 for &id in &r.accepted {
6553 if self.confidence_on {
6554 confidence.push(0.0);
6555 }
6556 if !commit!(id) {
6557 stopped = true;
6558 break;
6559 }
6560 }
6561 if stopped {
6562 break 'decode;
6563 }
6564 next_pos += r.accepted.len() + 1;
6565 hidden = r.hidden;
6566 self.graph_logits = Some(r.logits);
6569 continue 'decode;
6570 }
6571 }
6572 }
6573 #[cfg(feature = "gpu")]
6576 if self.speculative
6577 && self.qwen4_exp.is_some()
6578 && task_mask.is_none()
6579 && self.sampler_config.temperature < 1e-6
6580 && generated + 1 < max_tokens
6581 && next_pos > 0
6582 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6583 {
6584 let r = match &mut self.qwen4_exp {
6585 Some(b) => crate::qwen4_exp::spec_round(
6586 &b.0,
6587 &b.1,
6588 &b.2,
6589 &mut b.3,
6590 next_pos,
6591 &all_ids,
6592 &self.inv_freq,
6593 self.pool.as_deref(),
6594 ),
6595 None => None,
6596 };
6597 if let Some(r) = r {
6598 drafted += r.drafted;
6599 accepted += r.accepted.len();
6600 let mut stopped = false;
6601 for &id in &r.accepted {
6602 if self.confidence_on {
6603 confidence.push(0.0);
6604 }
6605 if !commit!(id) {
6606 stopped = true;
6607 break;
6608 }
6609 }
6610 if stopped {
6611 break 'decode;
6612 }
6613 next_pos += r.accepted.len() + 1;
6614 hidden.fill(0.0);
6615 self.graph_logits = Some(r.logits);
6616 continue 'decode;
6617 }
6618 }
6619 match &mut mtp {
6620 #[cfg(feature = "gpu")]
6622 Some(m)
6623 if graph_spec
6624 && !spec_watchdog_off
6625 && generated + 1 < max_tokens
6626 && next_pos > 0 =>
6627 {
6628 let t_round = std::time::Instant::now();
6629 if spec_time_level() >= 2 {
6630 if let Some(t) = spec_round_end.take() {
6631 eprintln!(
6632 "spec-gap {:.2} ms (host between rounds)",
6633 t.elapsed().as_secs_f64() * 1e3
6634 );
6635 }
6636 }
6637 spec_stamps_begin();
6638 #[cfg(target_os = "macos")]
6643 let allocs0 =
6644 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6645 #[cfg(not(target_os = "macos"))]
6646 let allocs0 = 0u64;
6647 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6648 m,
6649 &hidden,
6650 t_next,
6651 next_pos,
6652 &mut drafted,
6653 &mut accepted,
6654 &mut all_ids,
6655 max_tokens - generated,
6656 ) {
6657 next_pos = n_pos;
6658 hidden = new_h;
6659 let level = spec_time_level();
6660 if level > 0 {
6661 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6662 let stamps = spec_stamps_take();
6663 let median = if spec_walls.len() >= 3 {
6666 let mut s = spec_walls.clone();
6667 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6668 Some(s[s.len() / 2])
6669 } else {
6670 None
6671 };
6672 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6673 #[cfg(target_os = "macos")]
6674 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6675 .load(std::sync::atomic::Ordering::Relaxed)
6676 - allocs0;
6677 #[cfg(not(target_os = "macos"))]
6678 let allocs = allocs0;
6679 eprintln!(
6680 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6681 extra.len() + 1,
6682 if allocs > 0 {
6683 format!(" [{allocs} new device buffers]")
6684 } else {
6685 String::new()
6686 },
6687 match (outlier, median) {
6688 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6689 _ => String::new(),
6690 }
6691 );
6692 if level >= 2 || outlier {
6693 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6694 eprintln!(
6695 "spec-stamps: {}| untracked {:.1}",
6696 spec_stamps_format(&stamps),
6697 wall - sum
6698 );
6699 }
6700 if spec_mon.n >= 1 {
6701 spec_walls.push(wall);
6702 }
6703 }
6704 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6708 spec_trial = Self::spec_trial_round(
6711 spec_trial,
6712 &mut spec_mon,
6713 generated + extra.len() + 1,
6714 );
6715 let mut stopped = false;
6716 for &id in &extra {
6717 if self.confidence_on {
6718 confidence.push(0.0);
6719 }
6720 if trace_on {
6733 traces.push(TokenTrace {
6734 t: generated,
6735 token_id: id,
6736 confidence: confidence.last().copied().unwrap_or(0.0),
6737 active_skill: None,
6738 recon: None,
6739 switched: false,
6740 });
6741 }
6742 if !commit!(id) {
6743 stopped = true;
6744 break;
6745 }
6746 }
6747 if stopped {
6748 break 'decode;
6749 }
6750 if spec_time_level() >= 2 {
6751 spec_round_end = Some(std::time::Instant::now());
6752 }
6753 continue 'decode;
6754 }
6755 if self
6756 .graph_failed
6757 .swap(false, std::sync::atomic::Ordering::Relaxed)
6758 {
6759 self.finish_generation(&mut mtp, &mut router, true);
6765 return Err("GPU MTP graph failed during speculative decode".to_string());
6766 }
6767 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6778 spec_mon.tokens = 0.0;
6779 spec_mon.fails = 3;
6780 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6781 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6782 next_pos += 1;
6783 continue 'decode;
6784 }
6785 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6787 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6788 drafted += 1;
6789 let emb1 = self.embed_single(t_next);
6790 let emb2 = self.embed_single(draft);
6791 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6792
6793 inference::rms_norm_into(
6794 &h1,
6795 &self.weights.final_norm,
6796 self.rms_eps,
6797 self.norm_style,
6798 &mut self.ws.n1,
6799 );
6800 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6801 let t_after = sampler::sample_with_scratch_pool(
6802 &logits1,
6803 &self.sampler_config,
6804 self.sampler_config.penalty_past(&all_ids, bounded_native),
6805 &mut self.rng,
6806 &mut self.sampler_scratch,
6807 self.pool.as_deref(),
6808 );
6809 if self.confidence_on {
6810 confidence.push(sampler::top1_prob_pool(
6811 self.pool.as_deref(),
6812 &mut self.sampler_scratch,
6813 &logits1,
6814 t_after,
6815 calib_temp,
6816 ));
6817 }
6818 attention::recycle_buf(&mut logits1);
6819 if trace_on {
6820 traces.push(TokenTrace {
6823 t: generated,
6824 token_id: t_after,
6825 confidence: confidence.last().copied().unwrap_or(0.0),
6826 active_skill: None,
6827 recon: None,
6828 switched: false,
6829 });
6830 }
6831 let stop = !commit!(t_after);
6832
6833 if t_after == draft {
6834 accepted += 1;
6835 self.commit_linear_scratch();
6836 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6837 hidden = h2;
6838 next_pos += 2;
6839 } else {
6840 for layer in &mut self.kv_cache.layers {
6842 layer.truncate_last(1);
6843 }
6844 if !stop {
6845 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6846 hidden = self.forward_layers(
6847 &self.embed_single(t_after),
6848 next_pos + 1,
6849 None,
6850 );
6851 }
6852 next_pos += 2;
6853 }
6854 if stop {
6855 break 'decode;
6856 }
6857 }
6858 _ => {
6860 #[cfg(feature = "gpu")]
6865 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6866 static SAID: std::sync::Once = std::sync::Once::new();
6867 SAID.call_once(|| {
6868 eprintln!(
6869 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6870 !self.dsv4_mtp.is_empty(),
6871 task_mask.is_none(),
6872 router.is_none(),
6873 !trace_on,
6874 self.sampler_config.temperature < 1e-6,
6875 self.sampler_config.repetition_penalty == 1.0,
6876 );
6877 });
6878 }
6879 #[cfg(feature = "gpu")]
6880 if Self::dsv4_spec_on()
6881 && self.dsv4.is_some()
6882 && !self.dsv4_mtp.is_empty()
6883 && task_mask.is_none()
6884 && router.is_none()
6885 && !trace_on
6886 && self.sampler_config.temperature < 1e-6
6887 && self.sampler_config.repetition_penalty == 1.0
6888 && generated + 1 < max_tokens
6889 && all_ids.len() >= 2
6890 && generated >= dsv4_spec_retry_at
6891 {
6892 let tip_token = all_ids[all_ids.len() - 2];
6893 let drafted0 = drafted;
6894 let round = self.dsv4_spec_step(
6895 tip_token,
6896 t_next,
6897 next_pos,
6898 max_tokens.saturating_sub(generated),
6899 &mut drafted,
6900 &mut accepted,
6901 );
6902 if drafted > drafted0 {
6903 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6904 if useful {
6905 dsv4_spec_bad = 0;
6906 } else {
6907 dsv4_spec_bad += 1;
6908 if dsv4_spec_bad >= 2 {
6909 dsv4_spec_bad = 0;
6910 dsv4_spec_retry_at = generated.saturating_add(32);
6911 tracing::info!(
6912 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6913 );
6914 }
6915 }
6916 }
6917 if let Some((extra, n_pos)) = round {
6918 next_pos = n_pos;
6919 let mut stopped = false;
6920 for &id in &extra {
6921 if self.confidence_on {
6922 confidence.push(0.0);
6923 }
6924 if !commit!(id) {
6925 stopped = true;
6926 break;
6927 }
6928 }
6929 if stopped {
6930 break 'decode;
6931 }
6932 continue 'decode;
6933 }
6934 }
6935 self.graph_want_logits = fuse_lm;
6936 let mut t_fwd = t_next;
6942 let pure_greedy = self.sampler_config.temperature < 1e-6
6943 && self.sampler_config.repetition_penalty == 1.0
6944 && self.sampler_config.presence_penalty == 0.0
6945 && self.sampler_config.suppress_tokens.is_empty();
6946 let burst_k = std::env::var("CMF_MULTISTEP")
6951 .ok()
6952 .and_then(|v| v.parse::<usize>().ok())
6953 .unwrap_or(0);
6954 if pure_greedy
6955 && burst_k >= 1
6956 && fuse_lm
6957 && task_mask.is_none()
6958 && router.is_none()
6959 && !trace_on
6960 && !self.confidence_on
6961 && std::env::var_os("CMF_FORCE_IDS").is_none()
6966 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6967 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6968 {
6969 let mut stopped = false;
6970 loop {
6971 if self
6976 .cancel
6977 .swap(false, std::sync::atomic::Ordering::Relaxed)
6978 {
6979 finish_reason = "cancelled".to_string();
6980 stopped = true;
6981 break;
6982 }
6983 let room = max_tokens.saturating_sub(generated);
6984 if room <= 2 {
6985 break;
6986 }
6987 let k = burst_k.min(room - 1);
6988 if k < 1 {
6989 break;
6990 }
6991 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6992 if self
6993 .graph_failed
6994 .swap(false, std::sync::atomic::Ordering::Relaxed)
6995 {
6996 self.finish_generation(&mut mtp, &mut router, true);
6997 return Err(
6998 "GPU token graph failed during greedy burst".to_string()
6999 );
7000 }
7001 break;
7002 };
7003 next_pos += k;
7004 for &id in &ids {
7005 if !commit!(id) {
7006 stopped = true;
7007 break;
7008 }
7009 }
7010 if stopped {
7011 break;
7012 }
7013 t_fwd = *ids.last().unwrap();
7014 }
7015 if stopped {
7016 break 'decode;
7017 }
7018 }
7019 #[cfg(target_os = "macos")]
7029 if graph_spec
7030 && spec_watchdog_off
7031 && next_pos > 0
7032 && self.mtp_graph_mode == Some(true)
7033 && crate::gpu::q1_force()
7034 {
7035 if let Some(m) = mtp.as_mut() {
7036 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
7037 }
7038 }
7039 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
7040 next_pos += 1;
7041 if let Some(r) = &mut router {
7044 let phi = self.dyn_phi_ema.clone();
7045 let decision = r.step(&phi, generated);
7046 if let Some(new_active) = decision {
7047 let _ = self.set_active_skill(new_active);
7048 }
7049 if trace_on {
7052 if let Some(last) = traces.last_mut() {
7053 let e = r.last_best_e();
7054 last.recon = e.is_finite().then_some(e);
7055 last.switched = decision.is_some();
7056 }
7057 }
7058 }
7059 }
7060 }
7061 }
7062
7063 let cancelled = finish_reason == "cancelled";
7064 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
7068 if mimo_spec {
7069 if let Some(st) = self.mimo_mtp.as_ref() {
7070 let line = st.stats.line();
7071 tracing::info!("{line}");
7072 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
7073 eprintln!("{line}");
7074 }
7075 }
7076 }
7077 self.finish_generation(&mut mtp, &mut router, cancelled);
7078
7079 let output_ids = &all_ids[input_ids.len()..];
7080 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
7084 let consumed = std::mem::take(&mut all_ids);
7089 if dyn_switched {
7090 self.clear_sequence_state();
7091 } else if cancelled || mimo_spec || prompt_rows.is_some() {
7092 self.clear_history();
7093 } else {
7094 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
7095 }
7096 all_ids = consumed;
7097 let output_ids = &all_ids[input_ids.len()..];
7098 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
7100 Ok(GenerateResult {
7101 text: self.tokenizer.decode(output_ids),
7102 token_ids: output_ids.to_vec(),
7103 prompt_tokens: input_ids.len(),
7104 tokens_generated: generated,
7105 finish_reason,
7106 mtp_drafted: drafted,
7107 mtp_accepted: accepted,
7108 token_confidence: confidence,
7109 traces,
7110 })
7111 }
7112
7113 fn mtp_step(
7117 &mut self,
7118 m: &mut MtpModule,
7119 hidden: &[f32],
7120 next_token: u32,
7121 position: usize,
7122 ) -> u32 {
7123 self.mtp_step_h(m, hidden, next_token, position).0
7124 }
7125
7126 fn chain_probe_note(depth: usize, prefix_ok: bool) {
7130 use std::sync::Mutex;
7131 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
7132 let mut t = T.lock().unwrap();
7133 if t.len() <= depth {
7134 t.resize(depth + 1, (0, 0));
7135 }
7136 t[depth].0 += 1;
7137 t[depth].1 += prefix_ok as u64;
7138 if depth == 0 && t[0].0 % 128 == 0 {
7139 let line: Vec<String> = t
7140 .iter()
7141 .enumerate()
7142 .map(|(d, (n, k))| {
7143 format!(
7144 "d{}={:.0}%({n})",
7145 d + 1,
7146 100.0 * *k as f64 / (*n).max(1) as f64
7147 )
7148 })
7149 .collect();
7150 eprintln!("mtp-chain: {}", line.join(" "));
7151 }
7152 }
7153
7154 fn mtp_step_hl(
7162 &mut self,
7163 m: &mut MtpModule,
7164 hidden: &[f32],
7165 next_token: u32,
7166 position: usize,
7167 ) -> (Vec<f32>, Vec<f32>) {
7168 #[cfg(target_os = "macos")]
7173 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
7174 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
7175 self.mtp_graph_mode = Some(true);
7176 return r;
7177 }
7178 if self.mtp_graph_mode == Some(true) {
7179 tracing::error!("mtp Metal graph failed after admission");
7180 self.clear_sequence_state();
7181 self.graph_failed
7182 .store(true, std::sync::atomic::Ordering::Relaxed);
7183 self.cancel
7184 .store(true, std::sync::atomic::Ordering::Relaxed);
7185 return (Vec::new(), Vec::new());
7186 }
7187 self.mtp_graph_mode = Some(false);
7188 }
7189 #[cfg(feature = "gpu")]
7190 if self.mtp_graph_mode != Some(false) {
7191 if !self.mtp_graph_ok(m) {
7192 if self.mtp_graph_mode == Some(true) {
7193 tracing::error!("mtp graph became unavailable after admission");
7198 self.clear_sequence_state();
7199 self.graph_failed
7200 .store(true, std::sync::atomic::Ordering::Relaxed);
7201 self.cancel
7202 .store(true, std::sync::atomic::Ordering::Relaxed);
7203 return (Vec::new(), Vec::new());
7204 }
7205 self.mtp_graph_mode = Some(false);
7206 } else {
7207 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
7208 self.mtp_graph_mode = Some(true);
7209 return r;
7210 }
7211 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7212 return (Vec::new(), Vec::new());
7219 }
7220 tracing::error!("mtp graph failed or declined after admission");
7224 self.clear_sequence_state();
7225 self.graph_failed
7226 .store(true, std::sync::atomic::Ordering::Relaxed);
7227 self.cancel
7228 .store(true, std::sync::atomic::Ordering::Relaxed);
7229 return (Vec::new(), Vec::new());
7230 }
7231 }
7232 let e = self.embed_single(next_token);
7236 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7237 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7238 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7239 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7240 let mut x = vec![0.0f32; self.hidden_size];
7241 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7242
7243 let lw = &m.layer;
7245 inference::rms_norm_into(
7246 &x,
7247 &lw.input_norm,
7248 self.rms_eps,
7249 self.norm_style,
7250 &mut self.ws.n1,
7251 );
7252 let attn = match &lw.attn {
7253 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7255 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7256 AttnKind::Full {
7257 wq,
7258 wk,
7259 wv,
7260 wo,
7261 q_norm,
7262 k_norm,
7263 output_gate,
7264 softplus_gate,
7265 bias,
7266 } => {
7267 let mut cfg = self.attn_cfg(position);
7268 cfg.q_norm = q_norm.as_deref();
7269 cfg.k_norm = k_norm.as_deref();
7270 cfg.output_gate = *output_gate;
7271 cfg.softplus_gate = softplus_gate
7272 .as_ref()
7273 .map(|(gate, per_head)| (gate, *per_head));
7274 cfg.bias = bias
7275 .as_ref()
7276 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7277 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7278 }
7279 AttnKind::Linear(_)
7280 | AttnKind::LinearGdn(_)
7281 | AttnKind::ShortConv(_)
7282 | AttnKind::Bounded(_) => {
7283 unreachable!("MTP block is full attention")
7284 }
7285 };
7286 for (i, &a) in attn.iter().enumerate() {
7287 x[i] += a;
7288 }
7289 inference::rms_norm_into(
7290 &x,
7291 &lw.post_norm,
7292 self.rms_eps,
7293 self.norm_style,
7294 &mut self.ws.p1,
7295 );
7296 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
7297 for (i, &f) in ffn.iter().enumerate() {
7298 x[i] += f;
7299 }
7300
7301 inference::rms_norm_into(
7302 &x,
7303 &m.final_norm,
7304 self.rms_eps,
7305 self.norm_style,
7306 &mut self.ws.n1,
7307 );
7308 let lg = self.lm_head_forward(&self.ws.n1);
7309 (lg, x)
7310 }
7311
7312 fn mtp_step_h(
7314 &mut self,
7315 m: &mut MtpModule,
7316 hidden: &[f32],
7317 next_token: u32,
7318 position: usize,
7319 ) -> (u32, Vec<f32>) {
7320 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
7321 let draft = sampler::argmax(&lg);
7322 attention::recycle_buf(&mut lg);
7323 (draft, x)
7324 }
7325
7326 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
7332 match trial {
7333 SpecTrial::Spec { t0, gen0, rounds } => {
7334 let rounds = rounds + 1;
7335 if mon.repeated_zero_acceptance() {
7343 tracing::info!(
7344 "speculation trial: no accepted drafts in two rounds — timing plain"
7345 );
7346 return SpecTrial::Plain {
7347 t0: std::time::Instant::now(),
7348 gen0: generated,
7349 };
7350 }
7351 if rounds >= 5 {
7352 if mon.plain_ms > 0.0 {
7353 let keep = mon.pays();
7354 mon.fails = 0;
7355 tracing::info!(
7356 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
7357 mon.tokens,
7358 mon.round_ms,
7359 mon.plain_ms,
7360 if keep { "speculating" } else { "plain" }
7361 );
7362 SpecTrial::Decided {
7363 spec: keep,
7364 recheck_at: if keep { usize::MAX } else { generated + 128 },
7365 }
7366 } else if mon.pays() {
7367 mon.fails = 0;
7372 tracing::info!(
7373 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7374 mon.tokens,
7375 mon.round_ms,
7376 );
7377 SpecTrial::Decided {
7378 spec: true,
7379 recheck_at: usize::MAX,
7380 }
7381 } else {
7382 SpecTrial::Plain {
7383 t0: std::time::Instant::now(),
7384 gen0: generated,
7385 }
7386 }
7387 } else {
7388 SpecTrial::Spec { t0, gen0, rounds }
7389 }
7390 }
7391 SpecTrial::Decided { spec: true, .. } => {
7392 if mon.pays() {
7393 mon.fails = 0;
7394 trial
7395 } else {
7396 mon.fails += 1;
7397 if mon.fails >= 4 {
7398 if mon.plain_ms <= 0.0 {
7399 tracing::info!(
7403 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7404 mon.tokens,
7405 mon.round_ms,
7406 );
7407 return SpecTrial::Plain {
7408 t0: std::time::Instant::now(),
7409 gen0: generated,
7410 };
7411 }
7412 tracing::info!(
7413 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7414 mon.tokens,
7415 mon.round_ms,
7416 mon.plain_ms
7417 );
7418 SpecTrial::Decided {
7419 spec: false,
7420 recheck_at: generated + 128,
7421 }
7422 } else {
7423 trial
7424 }
7425 }
7426 }
7427 other => other,
7428 }
7429 }
7430
7431 fn mtp_kv_id(&self) -> u64 {
7434 self.graph_kv_id | (1u64 << 40)
7435 }
7436
7437 const MTP_LAYER_BASE: usize = 0;
7442
7443 #[cfg(feature = "gpu")]
7450 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7451 self.mtp_graph_mode != Some(true)
7452 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7453 }
7454
7455 #[cfg(feature = "gpu")]
7461 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7462 let mut ok = true;
7463 let mut expected = false;
7464 for li in 0..self.num_layers {
7465 if matches!(
7466 self.weights.layers[self.phys_layer(li)].attn,
7467 AttnKind::Full { .. }
7468 ) {
7469 expected = true;
7470 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7471 }
7472 }
7473 !expected || ok
7474 }
7475
7476 fn graph_gdn_layer_count(&self) -> usize {
7480 (0..self.num_layers)
7481 .filter(|&li| {
7482 matches!(
7483 &self.weights.layers[self.phys_layer(li)].attn,
7484 AttnKind::LinearGdn(_)
7485 )
7486 })
7487 .count()
7488 }
7489
7490 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7493 let e = self.embed_single(next_token);
7494 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7495 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7496 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7497 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7498 let mut x = vec![0.0f32; self.hidden_size];
7499 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7500 x
7501 }
7502
7503 #[cfg(feature = "gpu")]
7506 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7507 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7508 return false;
7509 }
7510 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7511 || !crate::gpu::enabled_here()
7512 || self.attn_softcap > 0.0
7513 || self.attention_heads_per_layer.is_some()
7514 || self.v_head_dim.is_some()
7517 {
7518 return false;
7519 }
7520 matches!(
7521 &m.layer.attn,
7522 AttnKind::Full {
7523 softplus_gate: None,
7524 ..
7525 }
7526 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7527 }
7528
7529 #[cfg(feature = "gpu")]
7533 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7534 if !self.mtp_block_graph_ok(m) {
7535 return false;
7536 }
7537 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7538 return false;
7539 };
7540 let FfnKind::Dense(d) = &m.layer.ffn else {
7541 return false;
7542 };
7543 d.segs.is_empty()
7544 && wq.graph_weight().is_some()
7545 && wk.graph_weight().is_some()
7546 && wv.graph_weight().is_some()
7547 && wo.graph_weight().is_some()
7548 && d.gate_proj.graph_weight().is_some()
7549 && d.up_proj.graph_weight().is_some()
7550 && d.down_proj.graph_weight().is_some()
7551 && self.weights.lm_head.graph_weight().is_some()
7552 }
7553
7554 #[cfg(feature = "gpu")]
7560 fn mtp_step_graph(
7561 &mut self,
7562 m: &mut MtpModule,
7563 hidden: &[f32],
7564 next_token: u32,
7565 position: usize,
7566 ) -> Option<(Vec<f32>, Vec<f32>)> {
7567 if !self.mtp_graph_ok(m) {
7568 return None;
7569 }
7570 let lw = &m.layer;
7571 let AttnKind::Full {
7572 wq,
7573 wk,
7574 wv,
7575 wo,
7576 q_norm,
7577 k_norm,
7578 output_gate,
7579 softplus_gate,
7580 bias,
7581 } = &lw.attn
7582 else {
7583 return None;
7584 };
7585 if softplus_gate.is_some() {
7586 return None;
7587 }
7588 let FfnKind::Dense(d) = &lw.ffn else {
7589 return None;
7590 };
7591 if !d.segs.is_empty() {
7592 return None; }
7594 let mut x = self.mtp_block_input(m, hidden, next_token);
7597 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7598 let (_, i, kind, rs) = t.graph_weight()?;
7599 Some(crate::gpu::GraphW {
7600 idx: i,
7601 kind,
7602 row_scale: rs,
7603 data: &[],
7604 prism: crate::gpu::GraphPrismOp::None,
7605 affine: false,
7606 })
7607 }
7608 let (model, _, _, _) = wq.graph_weight()?;
7609 let model = model.clone();
7610 let (lm_gw, lm_rows) = {
7611 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7612 let rows = if kind == 6 {
7616 self.draft_head_rows(self.weights.lm_head.rows())
7617 } else {
7618 self.weights.lm_head.rows()
7619 };
7620 (
7621 crate::gpu::GraphW {
7622 idx: i,
7623 kind,
7624 row_scale: rs,
7625 data: &[],
7626 prism: crate::gpu::GraphPrismOp::None,
7627 affine: false,
7628 },
7629 rows,
7630 )
7631 };
7632 let layer = crate::gpu::GraphLayer {
7633 mhc: None,
7634 input_norm: &lw.input_norm,
7635 attn: crate::gpu::GraphAttn::Full {
7636 wq: gw(wq)?,
7637 wk: gw(wk)?,
7638 wv: gw(wv)?,
7639 wo: gw(wo)?,
7640 q_norm: q_norm.as_deref(),
7641 k_norm: k_norm.as_deref(),
7642 late_qk_norm: self.qk_norm_after_rope,
7643 bias: bias
7644 .as_ref()
7645 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7646 output_gate: *output_gate,
7647 cpu_k: m.kv.k_heads(),
7648 cpu_v: m.kv.v_heads(),
7649 cpu_base: m.kv.base(),
7650 geom: None,
7651 head_gate: None,
7652 },
7653 post_norm: &lw.post_norm,
7654 attn_out_norm: None,
7655 ffn_out_norm: None,
7656 layer_scale: None,
7657 ffn: crate::gpu::GraphFfn::Dense {
7658 gate: gw(&d.gate_proj)?,
7659 up: gw(&d.up_proj)?,
7660 down: gw(&d.down_proj)?,
7661 act: d.act.graph_act()?,
7662 },
7663 };
7664 let nh = self.num_heads;
7665 let (nkv, hd, rd) = self.layer_geom(0);
7666 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7667 let mut logits = Vec::new();
7668 let ok = crate::gpu::forward_token_graph(
7669 &model,
7670 self.mtp_kv_id(),
7671 std::slice::from_ref(&layer),
7672 &[None],
7673 self.o1_epoch,
7674 &self.inv_freq,
7675 &mut x,
7676 nh,
7677 nkv,
7678 hd,
7679 self.attn_scale,
7680 rd,
7681 self.hidden_size,
7682 d.gate_proj.rows(),
7686 position,
7687 self.kv_cache.max_seq_len,
7688 gemma,
7689 self.rms_eps as f32,
7690 Some((&lm_gw, lm_rows)),
7691 &m.final_norm,
7692 &mut logits,
7693 &[],
7694 1,
7695 None,
7696 None,
7697 None,
7698 Self::MTP_LAYER_BASE,
7699 true,
7700 );
7701 match ok {
7702 crate::gpu::TokenGraphOutcome::Completed => {}
7703 crate::gpu::TokenGraphOutcome::Declined => return None,
7704 crate::gpu::TokenGraphOutcome::Failed => {
7705 self.clear_sequence_state();
7709 self.graph_failed
7710 .store(true, std::sync::atomic::Ordering::Relaxed);
7711 self.cancel
7712 .store(true, std::sync::atomic::Ordering::Relaxed);
7713 return None;
7714 }
7715 }
7716 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7720 Some((logits, x))
7721 }
7722
7723 #[cfg(feature = "gpu")]
7731 fn mtp_warm_graph(
7732 &mut self,
7733 m: &mut MtpModule,
7734 pairs: &[(&[f32], u32)],
7735 first_pos: usize,
7736 ) -> crate::gpu::BatchGraphOutcome {
7737 if pairs.is_empty() {
7738 return crate::gpu::BatchGraphOutcome::Completed;
7739 }
7740 if !self.mtp_block_graph_ok(m) {
7741 return crate::gpu::BatchGraphOutcome::Declined;
7742 }
7743 let hs = self.hidden_size;
7744 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7747 for (h, t) in pairs {
7748 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7749 }
7750 let lw = &m.layer;
7751 let AttnKind::Full {
7752 wq,
7753 wk,
7754 wv,
7755 wo,
7756 q_norm,
7757 k_norm,
7758 output_gate,
7759 bias,
7760 ..
7761 } = &lw.attn
7762 else {
7763 return crate::gpu::BatchGraphOutcome::Declined;
7764 };
7765 let FfnKind::Dense(d) = &lw.ffn else {
7766 return crate::gpu::BatchGraphOutcome::Declined;
7767 };
7768 if !d.segs.is_empty() {
7769 return crate::gpu::BatchGraphOutcome::Declined; }
7771 let (
7774 AttnKind::Full {
7775 softplus_gate: None,
7776 ..
7777 },
7778 Some(gact),
7779 ) = (&lw.attn, d.act.graph_act())
7780 else {
7781 return crate::gpu::BatchGraphOutcome::Declined;
7782 };
7783 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7784 let (_, i, kind, rs) = t.graph_weight()?;
7785 Some(crate::gpu::GraphW {
7786 idx: i,
7787 kind,
7788 row_scale: rs,
7789 data: &[],
7790 prism: crate::gpu::GraphPrismOp::None,
7791 affine: false,
7792 })
7793 }
7794 let Some((model, _, _, _)) = wq.graph_weight() else {
7795 return crate::gpu::BatchGraphOutcome::Declined;
7796 };
7797 let model = model.clone();
7798 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7799 gw(wq),
7800 gw(wk),
7801 gw(wv),
7802 gw(wo),
7803 gw(&d.gate_proj),
7804 gw(&d.up_proj),
7805 gw(&d.down_proj),
7806 ) else {
7807 return crate::gpu::BatchGraphOutcome::Declined;
7808 };
7809 let layer = crate::gpu::GraphLayer {
7810 mhc: None,
7811 input_norm: &lw.input_norm,
7812 attn: crate::gpu::GraphAttn::Full {
7813 wq: gwq,
7814 wk: gwk,
7815 wv: gwv,
7816 wo: gwo,
7817 q_norm: q_norm.as_deref(),
7818 k_norm: k_norm.as_deref(),
7819 late_qk_norm: self.qk_norm_after_rope,
7820 bias: bias
7821 .as_ref()
7822 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7823 output_gate: *output_gate,
7824 cpu_k: m.kv.k_heads(),
7825 cpu_v: m.kv.v_heads(),
7826 cpu_base: m.kv.base(),
7827 geom: None,
7828 head_gate: None,
7829 },
7830 post_norm: &lw.post_norm,
7831 attn_out_norm: None,
7832 ffn_out_norm: None,
7833 layer_scale: None,
7834 ffn: crate::gpu::GraphFfn::Dense {
7835 gate: gg,
7836 up: gu,
7837 down: gd,
7838 act: gact,
7839 },
7840 };
7841 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7842 let nh = self.num_heads;
7843 let (nkv, hd, rd) = self.layer_geom(0);
7844 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7845 crate::gpu::forward_batch_graph(
7846 &model,
7847 self.mtp_kv_id(),
7848 std::slice::from_ref(&layer),
7849 &self.inv_freq,
7850 &mut hiddens,
7851 nh,
7852 nkv,
7853 hd,
7854 rd,
7855 hs,
7856 d.gate_proj.rows(),
7859 &positions,
7860 self.kv_cache.max_seq_len,
7861 gemma,
7862 self.rms_eps as f32,
7863 self.attn_scale,
7864 pairs.len(),
7865 &[],
7866 0,
7867 None,
7868 None,
7869 )
7870 }
7871
7872 #[cfg(feature = "gpu")]
7879 fn mtp_warm_graph_fallback(
7880 &mut self,
7881 m: &mut MtpModule,
7882 pairs: &[(&[f32], u32)],
7883 first_pos: usize,
7884 ) -> bool {
7885 if pairs.is_empty() {
7886 return true;
7887 }
7888 let graphable = self.mtp_block_graph_ok(m);
7889 if !graphable {
7890 if self.mtp_graph_mode == Some(true) {
7894 return false;
7895 }
7896 self.mtp_graph_mode = Some(false);
7897 for (j, (h, t)) in pairs.iter().enumerate() {
7898 self.mtp_warm(m, h, *t, first_pos + j);
7899 }
7900 return true;
7901 }
7902
7903 for (j, (h, t)) in pairs.iter().enumerate() {
7908 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7909 return false;
7910 }
7911 }
7912 self.mtp_graph_mode = Some(true);
7913 true
7914 }
7915
7916 #[cfg(feature = "gpu")]
7921 fn mtp_warm_prefill_pairs(
7922 &mut self,
7923 m: &mut MtpModule,
7924 pairs: &[(&[f32], u32)],
7925 first_pos: usize,
7926 ) -> Result<(), &'static str> {
7927 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7932 if self.mtp_graph_mode == Some(true) {
7933 return Err("MTP token graph became unavailable after admission");
7934 }
7935 self.mtp_graph_mode = Some(false);
7936 for (j, (h, t)) in pairs.iter().enumerate() {
7937 self.mtp_warm(m, h, *t, first_pos + j);
7938 }
7939 return Ok(());
7940 }
7941 match self.mtp_warm_graph(m, pairs, first_pos) {
7942 crate::gpu::BatchGraphOutcome::Completed => {
7943 if !pairs.is_empty() {
7944 self.mtp_graph_mode = Some(true);
7945 }
7946 Ok(())
7947 }
7948 crate::gpu::BatchGraphOutcome::Declined => {
7949 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7950 Ok(())
7951 } else {
7952 Err("MTP warm-up fallback failed after device admission")
7953 }
7954 }
7955 crate::gpu::BatchGraphOutcome::Failed => {
7956 Err("MTP warm batch graph failed after admission")
7957 }
7958 }
7959 }
7960
7961 #[cfg(not(feature = "gpu"))]
7962 fn mtp_warm_prefill_pairs(
7963 &mut self,
7964 m: &mut MtpModule,
7965 pairs: &[(&[f32], u32)],
7966 first_pos: usize,
7967 ) -> Result<(), &'static str> {
7968 for (j, (h, t)) in pairs.iter().enumerate() {
7969 self.mtp_warm(m, h, *t, first_pos + j);
7970 }
7971 Ok(())
7972 }
7973
7974 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7978 let e = self.embed_single(next_token);
7979 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7980 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7981 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7982 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7983 let mut x = vec![0.0f32; self.hidden_size];
7984 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7985 inference::rms_norm_into(
7986 &x,
7987 &m.layer.input_norm,
7988 self.rms_eps,
7989 self.norm_style,
7990 &mut self.ws.n1,
7991 );
7992 let attn = match &m.layer.attn {
7993 AttnKind::Full {
7994 wq,
7995 wk,
7996 wv,
7997 wo,
7998 q_norm,
7999 k_norm,
8000 output_gate,
8001 softplus_gate,
8002 bias,
8003 } => {
8004 let mut cfg = self.attn_cfg(position);
8005 cfg.q_norm = q_norm.as_deref();
8006 cfg.k_norm = k_norm.as_deref();
8007 cfg.output_gate = *output_gate;
8008 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
8009 cfg.bias = bias
8010 .as_ref()
8011 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
8012 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
8013 }
8014 _ => return,
8015 };
8016 let _ = attn;
8017 }
8018
8019 #[cfg(feature = "gpu")]
8026 #[allow(clippy::too_many_arguments)]
8027 fn graph_spec_step(
8028 &mut self,
8029 m: &mut MtpModule,
8030 hidden: &[f32],
8031 t_next: u32,
8032 next_pos: usize,
8033 drafted: &mut usize,
8034 accepted: &mut usize,
8035 all_ids: &mut Vec<u32>,
8039 room: usize,
8044 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
8045 #[cfg(target_os = "macos")]
8056 let metal_native = crate::gpu::q1_force();
8057 #[cfg(not(target_os = "macos"))]
8058 let metal_native = false;
8059 #[cfg(feature = "gpu")]
8060 let k_default = if metal_native {
8061 7
8064 } else if crate::gpu_wgpu::verify_i8_on() {
8065 5
8066 } else {
8067 4
8068 };
8069 #[cfg(not(feature = "gpu"))]
8070 let k_default = 4;
8071 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
8072 .ok()
8073 .and_then(|v| v.parse().ok())
8074 .filter(|&v| (1..=8).contains(&v));
8075 let (k_start, k_max) = if metal_native {
8080 (7, 7)
8081 } else {
8082 (3, k_default.max(5))
8083 };
8084 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
8085 let k_spec = k_full.min(room).max(1);
8086 let k_capped = k_spec < k_full;
8089 if next_pos == 0 {
8090 return None;
8091 }
8092 let t_round = std::time::Instant::now();
8093 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
8109 let sub0 = subs();
8110 let cfg = self.sampler_config.clone();
8115 let penalized = !(cfg.repetition_penalty == 1.0
8116 && cfg.presence_penalty == 0.0
8117 && cfg.suppress_tokens.is_empty());
8118 let greedy_pen = cfg.temperature < 1e-6 && penalized;
8123 let sampling = cfg.temperature >= 1e-6;
8124 let sparse = sampling && sampler::sparse_ok(&cfg);
8130 let base_len = all_ids.len();
8131 if sampling && !sparse && self.spec_q.len() < k_spec {
8132 self.spec_q.resize_with(k_spec, Vec::new);
8133 }
8134 if sparse && self.spec_qs.len() < k_spec {
8135 self.spec_qs.resize_with(k_spec, Vec::new);
8136 }
8137 let mut drafts = Vec::with_capacity(k_spec);
8142 let mut hx = hidden.to_vec();
8143 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
8146 spec_stamp("pro");
8147 #[cfg(target_os = "macos")]
8153 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
8154 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
8155 Ok(ids) => {
8156 self.mtp_graph_mode = Some(true);
8157 drafts = ids;
8158 }
8159 Err(true) => {
8160 tracing::error!("mtp Metal draft chain failed after commit");
8161 self.clear_sequence_state();
8162 self.graph_failed
8163 .store(true, std::sync::atomic::Ordering::Relaxed);
8164 self.cancel
8165 .store(true, std::sync::atomic::Ordering::Relaxed);
8166 return None;
8167 }
8168 Err(false) => {}
8169 }
8170 }
8171 for j in drafts.len()..k_spec {
8172 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
8173 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
8174 if spec_dbg {
8175 let saved = self.mtp_graph_mode;
8176 self.mtp_graph_mode = Some(false);
8177 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8178 self.mtp_graph_mode = saved;
8179 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8180 return None;
8181 }
8182 m.kv.truncate_last(1);
8183 dbg_ref = Some(r);
8184 }
8185 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
8186 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
8187 return None;
8188 }
8189 if let Some((lg_cpu, h_cpu)) = dbg_ref {
8190 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
8191 let dl = lg
8192 .iter()
8193 .zip(&lg_cpu)
8194 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8195 let dh = hj
8196 .iter()
8197 .zip(&h_cpu)
8198 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
8199 eprintln!(
8200 "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 {}",
8201 next_pos - 1 + j,
8202 sampler::argmax(&lg_cpu),
8203 sampler::argmax(&lg),
8204 n(&h_cpu),
8205 n(&hj),
8206 m.kv.seq_len
8207 );
8208 }
8209 let dj = if sparse {
8210 let mut q = std::mem::take(&mut self.spec_qs[j]);
8211 let ok = sampler::sparse_distribution_into(
8212 &lg,
8213 &cfg,
8214 all_ids,
8215 &mut self.sampler_scratch,
8216 self.pool.as_deref(),
8217 &mut q,
8218 );
8219 let d = if ok {
8220 sampler::draw_sparse(&q, &mut self.rng)
8221 } else {
8222 let t = sampler::argmax(&lg);
8224 q.clear();
8225 q.push((t, 1.0));
8226 t
8227 };
8228 self.spec_qs[j] = q;
8229 all_ids.push(d);
8230 d
8231 } else if sampling {
8232 let mut q = std::mem::take(&mut self.spec_q[j]);
8233 sampler::distribution_into(
8234 &lg,
8235 &cfg,
8236 all_ids,
8237 &mut self.sampler_scratch,
8238 self.pool.as_deref(),
8239 &mut q,
8240 );
8241 let d = sampler::draw(&q, &mut self.rng);
8242 self.spec_q[j] = q;
8243 all_ids.push(d); d
8245 } else if greedy_pen {
8246 let d = sampler::argmax_penalized(
8247 &lg,
8248 &cfg,
8249 all_ids,
8250 &mut self.sampler_scratch,
8251 self.pool.as_deref(),
8252 );
8253 all_ids.push(d);
8254 d
8255 } else {
8256 sampler::argmax(&lg)
8257 };
8258 attention::recycle_buf(&mut lg);
8259 drafts.push(dj);
8260 hx = hj;
8261 spec_stamp("d.pick");
8262 }
8263 all_ids.truncate(base_len);
8264 *drafted += k_spec;
8265 let t_draft = t_round.elapsed();
8266 let sub_draft = subs();
8267 let b = k_spec + 1;
8270 let mut hiddens = vec![0.0f32; b * self.hidden_size];
8271 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
8272 let e = self.embed_single(t);
8273 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
8274 }
8275 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
8276 spec_stamp("v.emb");
8277 let (lm_gw, lm_rows) = {
8278 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
8279 (
8280 crate::gpu::GraphW {
8281 idx: i,
8282 kind,
8283 row_scale: rs,
8284 data: &[],
8285 prism: crate::gpu::GraphPrismOp::None,
8286 affine: false,
8287 },
8288 self.weights.lm_head.rows(),
8289 )
8290 };
8291 let mut logits = Vec::new();
8292 let final_norm = self.weights.final_norm.clone();
8293 #[cfg(target_os = "macos")]
8302 let greedy_dev = metal_native
8303 && !sampling
8304 && !greedy_pen
8305 && !self.confidence_on
8306 && self.final_softcap.is_none()
8307 && self.vocab_size == lm_rows
8313 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
8314 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
8315 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
8316 #[cfg(not(target_os = "macos"))]
8317 let greedy_dev = false;
8318 let mut dev_ids: Vec<u32> = Vec::new();
8319 #[cfg(target_os = "macos")]
8320 let verify_outcome = if metal_native {
8321 let lm = self.weights.lm_head.q1_parts()?;
8322 let n_score = self.vocab_size.min(lm_rows);
8323 self.try_batch_graph_metal(
8324 &mut hiddens,
8325 &positions,
8326 b,
8327 Some((lm, &final_norm, &mut logits)),
8328 if greedy_dev {
8329 Some((n_score, &mut dev_ids))
8330 } else {
8331 None
8332 },
8333 )
8334 } else {
8335 self.try_batch_graph_wgpu(
8336 &mut hiddens,
8337 &positions,
8338 b,
8339 Some(crate::gpu::SpecTail {
8340 lm: lm_gw,
8341 lm_rows,
8342 final_norm: &final_norm,
8343 logits_out: &mut logits,
8344 pick: None,
8345 }),
8346 )
8347 };
8348 #[cfg(not(target_os = "macos"))]
8349 let verify_outcome = self.try_batch_graph_wgpu(
8350 &mut hiddens,
8351 &positions,
8352 b,
8353 Some(crate::gpu::SpecTail {
8354 lm: lm_gw,
8355 lm_rows,
8356 final_norm: &final_norm,
8357 logits_out: &mut logits,
8358 pick: None,
8359 }),
8360 );
8361 match verify_outcome {
8362 crate::gpu::BatchGraphOutcome::Completed => {}
8363 crate::gpu::BatchGraphOutcome::Declined => {
8364 m.kv.truncate_last(k_spec);
8368 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8369 self.clear_sequence_state();
8370 self.graph_failed
8371 .store(true, std::sync::atomic::Ordering::Relaxed);
8372 self.cancel
8373 .store(true, std::sync::atomic::Ordering::Relaxed);
8374 tracing::error!("MTP graph mirror rewind failed after verify decline");
8375 }
8376 return None;
8377 }
8378 crate::gpu::BatchGraphOutcome::Failed => {
8379 self.clear_sequence_state();
8383 self.graph_failed
8384 .store(true, std::sync::atomic::Ordering::Relaxed);
8385 self.cancel
8386 .store(true, std::sync::atomic::Ordering::Relaxed);
8387 tracing::error!("MTP verify batch graph failed after admission");
8388 return None;
8389 }
8390 }
8391 #[cfg(target_os = "macos")]
8397 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8398 let snap: Vec<Vec<f32>> = self
8399 .kv_cache
8400 .layers
8401 .iter()
8402 .map(|l| l.linear_state.clone())
8403 .collect();
8404 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8405 let toks: Vec<u32> = std::iter::once(t_next)
8406 .chain(drafts.iter().copied())
8407 .collect();
8408 let want_save = self.graph_want_logits;
8409 self.graph_want_logits = false;
8410 for (i, &t) in toks.iter().enumerate() {
8411 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8412 let _ = self.graph_logits.take();
8413 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8417 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8418 }
8419 let ref_lg = self.logits_from_hidden(&hi);
8420 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8421 let ra = sampler::argmax(&ref_lg);
8422 let va = sampler::argmax(row);
8423 let mut md = 0f32;
8424 let mut rms = 0f64;
8425 for j in 0..lm_rows.min(ref_lg.len()) {
8426 let d = (ref_lg[j] - row[j]).abs();
8427 md = md.max(d);
8428 rms += (d as f64) * (d as f64);
8429 }
8430 let mut hd = 0f32;
8431 for j in 0..self.hidden_size {
8432 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8433 }
8434 eprintln!(
8435 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8436 next_pos + i,
8437 if ra == va { "OK" } else { "MISMATCH" },
8438 (rms / lm_rows as f64).sqrt()
8439 );
8440 }
8441 self.graph_want_logits = want_save;
8442 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8445 if l.linear_state.len() == st.len() {
8446 l.linear_state.copy_from_slice(&st);
8447 } else {
8448 l.linear_state = st;
8449 }
8450 }
8451 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8452 let extra = l.seq_len.saturating_sub(n0);
8453 if extra > 0 {
8454 l.truncate_last(extra);
8455 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8456 }
8457 }
8458 }
8459 let t_verify = t_round.elapsed();
8460 let sub_verify = subs();
8461 let mut a = 0usize;
8466 let mut forced: Option<u32> = None;
8467 let ids: Vec<u32> = if sparse {
8468 let mut p = std::mem::take(&mut self.spec_ps);
8469 let mut res = std::mem::take(&mut self.spec_ress);
8470 while a < k_spec {
8471 let ok = sampler::sparse_distribution_into(
8472 &logits[a * lm_rows..(a + 1) * lm_rows],
8473 &cfg,
8474 all_ids,
8475 &mut self.sampler_scratch,
8476 self.pool.as_deref(),
8477 &mut p,
8478 );
8479 if !ok {
8480 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8481 p.clear();
8482 p.push((t, 1.0));
8483 }
8484 match sampler::spec_accept_or_correct_sparse(
8485 &p,
8486 &self.spec_qs[a],
8487 drafts[a],
8488 &mut self.rng,
8489 &mut res,
8490 ) {
8491 None => {
8492 all_ids.push(drafts[a]);
8493 a += 1;
8494 }
8495 Some(c) => {
8496 forced = Some(c);
8497 break;
8498 }
8499 }
8500 }
8501 all_ids.truncate(base_len);
8502 self.spec_ps = p;
8503 self.spec_ress = res;
8504 drafts.clone()
8505 } else if sampling {
8506 let mut p = std::mem::take(&mut self.spec_p);
8507 let mut res = std::mem::take(&mut self.spec_res);
8508 while a < k_spec {
8509 sampler::distribution_into(
8510 &logits[a * lm_rows..(a + 1) * lm_rows],
8511 &cfg,
8512 all_ids,
8513 &mut self.sampler_scratch,
8514 self.pool.as_deref(),
8515 &mut p,
8516 );
8517 match sampler::spec_accept_or_correct(
8518 &p,
8519 &self.spec_q[a],
8520 drafts[a],
8521 &mut self.rng,
8522 &mut res,
8523 self.pool.as_deref(),
8524 ) {
8525 None => {
8526 all_ids.push(drafts[a]);
8527 a += 1;
8528 }
8529 Some(c) => {
8530 forced = Some(c);
8531 break;
8532 }
8533 }
8534 }
8535 all_ids.truncate(base_len);
8536 self.spec_p = p;
8537 self.spec_res = res;
8538 drafts.clone()
8540 } else if greedy_pen {
8541 let mut ids: Vec<u32> = Vec::with_capacity(b);
8545 for i in 0..b {
8546 let t = sampler::argmax_penalized(
8547 &logits[i * lm_rows..(i + 1) * lm_rows],
8548 &cfg,
8549 all_ids,
8550 &mut self.sampler_scratch,
8551 self.pool.as_deref(),
8552 );
8553 ids.push(t);
8554 if i < k_spec && t == drafts[i] {
8555 all_ids.push(t);
8556 } else {
8557 break;
8558 }
8559 }
8560 all_ids.truncate(base_len);
8561 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8562 a += 1;
8563 }
8564 ids
8567 } else if greedy_dev && dev_ids.len() == b {
8568 let ids = std::mem::take(&mut dev_ids);
8569 while a < k_spec && ids[a] == drafts[a] {
8570 a += 1;
8571 }
8572 ids
8573 } else {
8574 if logits.len() < b * lm_rows {
8575 self.clear_sequence_state();
8578 self.graph_failed
8579 .store(true, std::sync::atomic::Ordering::Relaxed);
8580 self.cancel
8581 .store(true, std::sync::atomic::Ordering::Relaxed);
8582 tracing::error!("Metal verify returned neither logits nor argmax ids");
8583 return None;
8584 }
8585 let ids: Vec<u32> = (0..b)
8586 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8587 .collect();
8588 while a < k_spec && ids[a] == drafts[a] {
8589 a += 1;
8590 }
8591 ids
8592 };
8593 spec_stamp("acc");
8594 if spec_dbg {
8595 eprintln!(
8596 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8597 drafts, ids
8598 );
8599 }
8600 #[cfg(target_os = "macos")]
8604 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8605 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8606 {
8607 let snap: Vec<Vec<f32>> = self
8608 .kv_cache
8609 .layers
8610 .iter()
8611 .map(|l| l.linear_state.clone())
8612 .collect();
8613 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8614 let toks: Vec<u32> = std::iter::once(t_next)
8615 .chain(drafts.iter().copied())
8616 .collect();
8617 let want_save = self.graph_want_logits;
8618 self.graph_want_logits = false;
8619 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8620 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8621 let _ = self.graph_logits.take();
8622 }
8623 self.graph_want_logits = want_save;
8624 let plain_states: Vec<Vec<f32>> = self
8625 .kv_cache
8626 .layers
8627 .iter()
8628 .map(|l| l.linear_state.clone())
8629 .collect();
8630 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8631 let mut rows = Vec::new();
8632 for (li, (l, n0)) in self
8633 .kv_cache
8634 .layers
8635 .iter_mut()
8636 .zip(attn_lens.iter())
8637 .enumerate()
8638 {
8639 let extra = l.seq_len.saturating_sub(*n0);
8640 if extra > 0 {
8641 let mut kk = Vec::new();
8642 let mut vv = Vec::new();
8643 for g in 0..nkv {
8644 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8645 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8646 }
8647 rows.push((li, kk, vv));
8648 l.truncate_last(extra);
8649 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8650 }
8651 }
8652 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8653 if l.linear_state.len() == st.len() {
8654 l.linear_state.copy_from_slice(&st);
8655 } else {
8656 l.linear_state = st;
8657 }
8658 }
8659 Some((plain_states, rows))
8660 } else {
8661 None
8662 };
8663 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8664 #[cfg(target_os = "macos")]
8673 let mut warm_pending: Option<MetalWarmPending> = None;
8674 #[cfg(target_os = "macos")]
8675 if metal_native {
8676 m.kv.truncate_last(k_spec.saturating_sub(1));
8677 if self.mtp_graph_mode == Some(true) {
8678 crate::gpu_metal::kv_mirror_set_stored(
8681 self.mtp_kv_id(),
8682 Self::MTP_LAYER_BASE,
8683 m.kv.seq_len,
8684 );
8685 if !warm_off && a > 0 {
8686 let pairs: Vec<(&[f32], u32)> = (0..a)
8687 .map(|j| {
8688 (
8689 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8690 ids[j],
8691 )
8692 })
8693 .collect();
8694 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8695 }
8696 }
8697 spec_stamp("c.wsub");
8698 }
8699 #[cfg(target_os = "macos")]
8701 if metal_native {
8702 if !self.metal_verify_commit(a) {
8705 self.clear_sequence_state();
8706 self.graph_failed
8707 .store(true, std::sync::atomic::Ordering::Relaxed);
8708 self.cancel
8709 .store(true, std::sync::atomic::Ordering::Relaxed);
8710 tracing::error!("Metal verify state/KV handoff failed after admission");
8711 return None;
8712 }
8713 if let Some((plain_states, rows)) = commit_ref {
8714 crate::gpu_metal::queue_fence();
8715 let _ = crate::gpu_metal::wait_replay();
8718 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8719 let mut worst_s = 0f32;
8720 let mut worst_li = 0usize;
8721 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8722 if l.linear_state.len() != ps.len() || ps.is_empty() {
8723 continue;
8724 }
8725 let d = l
8726 .linear_state
8727 .iter()
8728 .zip(ps)
8729 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8730 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8731 let rel = d / n.max(1e-6);
8732 if rel > worst_s {
8733 worst_s = rel;
8734 worst_li = li;
8735 }
8736 }
8737 let mut worst_k = 0f32;
8738 for (li, kk, vv) in &rows {
8739 let l = &self.kv_cache.layers[*li];
8740 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8741 let mut ck = Vec::new();
8742 let mut cv = Vec::new();
8743 for g in 0..nkv {
8744 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8745 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8746 }
8747 if ck.len() == kk.len() {
8748 let dk = ck
8749 .iter()
8750 .zip(kk)
8751 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8752 let dv = cv
8753 .iter()
8754 .zip(vv)
8755 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8756 worst_k = worst_k.max(dk).max(dv);
8757 } else {
8758 eprintln!(
8759 "commit-check L{li}: kv row count mismatch {} vs {}",
8760 ck.len(),
8761 kk.len()
8762 );
8763 }
8764 }
8765 eprintln!(
8766 "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}"
8767 );
8768 }
8769 }
8770 if !metal_native && a + 1 < b {
8771 let expected_gdn_layers = self.graph_gdn_layer_count();
8772 if expected_gdn_layers > 0
8773 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8774 {
8775 self.clear_sequence_state();
8776 self.graph_failed
8777 .store(true, std::sync::atomic::Ordering::Relaxed);
8778 self.cancel
8779 .store(true, std::sync::atomic::Ordering::Relaxed);
8780 tracing::error!("GDN speculative restore failed after verify");
8781 return None;
8782 }
8783 }
8784 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8785 self.clear_sequence_state();
8790 self.graph_failed
8791 .store(true, std::sync::atomic::Ordering::Relaxed);
8792 self.cancel
8793 .store(true, std::sync::atomic::Ordering::Relaxed);
8794 tracing::error!("trunk graph KV rewind failed after speculative verify");
8795 return None;
8796 }
8797 *accepted += a;
8798 if !metal_native {
8809 m.kv.truncate_last(k_spec.saturating_sub(1));
8811 }
8812 spec_stamp("c.trunc");
8813 if !metal_native
8814 && self.mtp_graph_mode == Some(true)
8815 && !self.rewind_mtp_graph_mirror(next_pos)
8816 {
8817 self.clear_sequence_state();
8821 self.graph_failed
8822 .store(true, std::sync::atomic::Ordering::Relaxed);
8823 self.cancel
8824 .store(true, std::sync::atomic::Ordering::Relaxed);
8825 tracing::error!("MTP graph mirror rewind failed after verify commit");
8826 return None;
8827 }
8828 if !warm_off && a > 0 {
8829 let mut warmed = false;
8832 #[cfg(target_os = "macos")]
8833 if metal_native && self.mtp_graph_mode == Some(true) {
8834 warmed = match warm_pending.take() {
8838 Some(p) => self.mtp_warm_batch_finish(m, p),
8839 None => false,
8840 };
8841 if !warmed {
8842 warmed = true;
8843 for j in 0..a {
8844 let row =
8845 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8846 if self
8847 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8848 .is_none()
8849 {
8850 warmed = false;
8851 break;
8852 }
8853 }
8854 }
8855 }
8856 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8857 let rows: Vec<Vec<f32>> = (0..a)
8858 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8859 .collect();
8860 let pairs: Vec<(&[f32], u32)> = rows
8861 .iter()
8862 .zip(ids.iter())
8863 .map(|(r, &t)| (r.as_slice(), t))
8864 .collect();
8865 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8866 Ok(()) => warmed = true,
8867 Err(err) => {
8868 tracing::error!("{err}");
8874 self.clear_sequence_state();
8875 self.graph_failed
8876 .store(true, std::sync::atomic::Ordering::Relaxed);
8877 self.cancel
8878 .store(true, std::sync::atomic::Ordering::Relaxed);
8879 return None;
8880 }
8881 }
8882 }
8883 if !warmed {
8884 for j in 0..a {
8885 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8886 let row = row.to_vec();
8887 self.mtp_warm(m, &row, ids[j], next_pos + j);
8888 }
8889 }
8890 }
8891 spec_stamp("c.warm");
8895 if let Some(c) = forced {
8896 self.spec_forced = Some(c);
8897 self.graph_logits = None;
8898 } else if greedy_dev && logits.is_empty() {
8899 self.spec_forced = Some(ids[a]);
8902 self.graph_logits = None;
8903 } else {
8904 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8905 row.resize(self.vocab_size, 0.0);
8906 if let Some(c) = self.final_softcap {
8907 for l in row.iter_mut() {
8908 *l = c * (*l / c).tanh();
8909 }
8910 }
8911 self.graph_logits = Some(row);
8912 }
8913 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8914 spec_stamp("c.row");
8915 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8921 let end = subs();
8922 eprintln!(
8923 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8924 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8925 t_draft.as_secs_f64() * 1e3,
8926 sub_draft - sub0,
8927 (t_verify - t_draft).as_secs_f64() * 1e3,
8928 sub_verify - sub_draft,
8929 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8930 end - sub_verify,
8931 self.draft_full_streak,
8932 );
8933 }
8934 if k_env.is_none() && !metal_native && !k_capped {
8939 let f = a as f32 / k_spec.max(1) as f32;
8943 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8944 let mut k_next = k_spec;
8945 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8946 k_next = k_spec + 1;
8947 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8948 k_next = k_spec - 1;
8949 }
8950 if k_next != k_spec {
8951 self.spec_acc_ewma = 0.6;
8952 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8953 eprintln!("spec-k: {k_spec} → {k_next}");
8954 }
8955 }
8956 self.spec_k_adapt = Some(k_next);
8957 }
8958 spec_stamp("end");
8959 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8960 }
8961
8962 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8971 if !self.pair_supported() {
8972 return (0.0, 0.0);
8973 }
8974 if self.gdn_cfg.is_some()
8982 && self.graph_prefill_preferred()
8983 && self.automatic_gpu_prefix().is_some()
8984 {
8985 tracing::info!(
8986 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8987 );
8988 return (0.0, 0.0);
8989 }
8990 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8997 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8998 let emb1 = self.embed_single(1);
8999 let emb2 = self.embed_single(2);
9000 let pos = self.kv_cache.seq_len();
9001
9002 let t0 = std::time::Instant::now();
9003 for _ in 0..iters {
9004 let _ = self.forward_layers(&emb1, pos, None);
9005 let _ = self.forward_layers(&emb2, pos + 1, None);
9006 for l in &mut self.kv_cache.layers {
9007 l.truncate_last(2);
9008 }
9009 }
9010 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9011
9012 let t1 = std::time::Instant::now();
9013 for _ in 0..iters {
9014 let _ = self.forward_pair(&emb1, &emb2, pos);
9015 for l in &mut self.kv_cache.layers {
9016 l.truncate_last(2);
9017 }
9018 }
9019 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
9020 match graph_env {
9021 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
9022 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
9023 }
9024 (singles_ms, pair_ms)
9025 }
9026
9027 fn pair_supported(&self) -> bool {
9035 !self.weights.layers.is_empty()
9042 && !self.per_token_stack()
9043 && !self
9044 .weights
9045 .layers
9046 .iter()
9047 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
9048 }
9049
9050 fn forward_pair(
9051 &mut self,
9052 emb1: &[f32],
9053 emb2: &[f32],
9054 position: usize,
9055 ) -> (Vec<f32>, Vec<f32>) {
9056 self.mimo_moe_prepare();
9059 let mut h1 = emb1.to_vec();
9060 let mut h2 = emb2.to_vec();
9061 let (_nkv, _hd, hs, _rd, eps) = (
9062 self.num_kv_heads,
9063 self.head_dim,
9064 self.hidden_size,
9065 self.rotary_dim,
9066 self.rms_eps,
9067 );
9068 let pool = self.pool.clone();
9069 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9076
9077 for li in 0..self.num_layers {
9078 let _capacity_tail = automatic_gpu_prefix
9079 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
9080 .map(|_| crate::gpu::enter_cpu_scope());
9081 crate::gpu::set_layer(li as i64);
9084 let lw = &self.weights.layers[self.phys_layer(li)];
9085 inference::rms_norm_into(
9088 &h1,
9089 &lw.input_norm,
9090 self.rms_eps,
9091 self.norm_style,
9092 &mut self.ws.n1,
9093 );
9094 inference::rms_norm_into(
9095 &h2,
9096 &lw.input_norm,
9097 self.rms_eps,
9098 self.norm_style,
9099 &mut self.ws.n2,
9100 );
9101
9102 let (a1, a2) = match &lw.attn {
9103 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9104 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9105 AttnKind::Bounded(w) => {
9106 let rope = self
9109 .bounded_rope
9110 .clone()
9111 .expect("bounded layer without an installed rotation table");
9112 let cfg = crate::bounded::BoundedAttnCfg {
9113 num_heads: self.num_heads,
9114 num_kv_heads: self.num_kv_heads,
9115 head_dim: self.head_dim,
9116 hidden_size: hs,
9117 scale: self.attn_scale,
9118 rope: &rope,
9119 pool: pool.as_deref(),
9120 };
9121 let a1 = crate::bounded::bounded_attention(
9122 &self.ws.n1,
9123 w,
9124 &mut self.kv_cache.layers[li],
9125 &cfg,
9126 );
9127 let a2 = crate::bounded::bounded_attention(
9128 &self.ws.n2,
9129 w,
9130 &mut self.kv_cache.layers[li],
9131 &cfg,
9132 );
9133 (a1, a2)
9134 }
9135 AttnKind::Linear(w) => {
9136 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
9137 let layer = &mut self.kv_cache.layers[li];
9138 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9139 vmf_phase_pair(
9140 &self.ws.n1,
9141 &self.ws.n2,
9142 w,
9143 &cfg,
9144 state,
9145 scratch,
9146 self.pool.as_deref(),
9147 )
9148 }
9149 AttnKind::LinearGdn(w) => {
9150 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9151 let layer = &mut self.kv_cache.layers[li];
9152 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9153 gdn_pair(
9154 &self.ws.n1,
9155 &self.ws.n2,
9156 w,
9157 &cfg,
9158 state,
9159 scratch,
9160 self.pool.as_deref(),
9161 )
9162 }
9163 AttnKind::ShortConv(w) => {
9164 let cfg = self
9165 .short_conv_cfg
9166 .expect("short-conv layer without short_conv_cfg");
9167 let layer = &mut self.kv_cache.layers[li];
9168 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
9169 short_conv_pair(
9170 &self.ws.n1,
9171 &self.ws.n2,
9172 w,
9173 &cfg,
9174 state,
9175 scratch,
9176 self.pool.as_deref(),
9177 )
9178 }
9179 AttnKind::Full {
9180 wq,
9181 wk,
9182 wv,
9183 wo,
9184 q_norm,
9185 k_norm,
9186 output_gate,
9187 softplus_gate,
9188 bias,
9189 } => {
9190 let inv_freq_l = self.layer_inv_freq(li);
9191 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9192 let cfg = QwenAttnCfg {
9193 mrope: None,
9194 num_heads: self.layer_num_heads(li),
9195 num_kv_heads: nkv_l,
9196 head_dim: hd_l,
9197 hidden_size: hs,
9198 position,
9199 inv_freq: &inv_freq_l,
9200 rotary_dim: rd_l,
9201 scale: self.attn_scale,
9202 softcap: self.attn_softcap,
9203 window: self.layer_window(li),
9204 v_norm: self.attn_v_norm,
9205 qk_norm_after_rope: self.qk_norm_after_rope,
9206 gate_sigmoid: self.proj_gate_sigmoid,
9207 q_norm: q_norm.as_deref(),
9208 k_norm: k_norm.as_deref(),
9209 output_gate: *output_gate,
9210 softplus_gate: softplus_gate
9211 .as_ref()
9212 .map(|(gate, per_head)| (gate, *per_head)),
9213 rope_scale: self.layer_rope_scale(li),
9214 bias: bias
9215 .as_ref()
9216 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9217 rms_eps: eps,
9218 norm_style: self.norm_style,
9219 pool: pool.as_deref(),
9220 v_head_dim: self.layer_v_dim(li),
9221 };
9222 attention::qwen_attention_pair(
9223 &self.ws.n1,
9224 &self.ws.n2,
9225 wq,
9226 wk,
9227 wv,
9228 wo,
9229 &mut self.kv_cache.layers[li],
9230 &cfg,
9231 )
9232 }
9233 };
9234 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
9235 Some(w) => (
9236 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
9237 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
9238 ),
9239 None => (a1, a2),
9240 };
9241 for i in 0..self.hidden_size {
9242 h1[i] += a1[i];
9243 h2[i] += a2[i];
9244 }
9245 let (mut a1, mut a2) = (a1, a2);
9246 attention::recycle_buf(&mut a1);
9247 attention::recycle_buf(&mut a2);
9248
9249 let lw = &self.weights.layers[self.phys_layer(li)];
9250 inference::rms_norm_into(
9251 &h1,
9252 &lw.post_norm,
9253 self.rms_eps,
9254 self.norm_style,
9255 &mut self.ws.p1,
9256 );
9257 inference::rms_norm_into(
9258 &h2,
9259 &lw.post_norm,
9260 self.rms_eps,
9261 self.norm_style,
9262 &mut self.ws.p2,
9263 );
9264 let (f1, f2) = match &lw.ffn {
9265 FfnKind::DenseMoe(dm) => (
9268 dense_moe_ffn(
9269 dm,
9270 &self.ws.p1,
9271 &h1,
9272 self.rms_eps,
9273 self.norm_style,
9274 self.pool.as_deref(),
9275 ),
9276 dense_moe_ffn(
9277 dm,
9278 &self.ws.p2,
9279 &h2,
9280 self.rms_eps,
9281 self.norm_style,
9282 self.pool.as_deref(),
9283 ),
9284 ),
9285 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
9286 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
9287 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
9288 ),
9289 _ => ffn_forward_pair(
9290 &lw.ffn,
9291 &self.ws.p1,
9292 &self.ws.p2,
9293 self.pool.as_deref(),
9294 None,
9295 ),
9296 };
9297 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
9298 Some(w) => (
9299 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
9300 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
9301 ),
9302 None => (f1, f2),
9303 };
9304 for i in 0..self.hidden_size {
9305 h1[i] += f1[i];
9306 h2[i] += f2[i];
9307 }
9308 let (mut f1, mut f2) = (f1, f2);
9309 attention::recycle_buf(&mut f1);
9310 attention::recycle_buf(&mut f2);
9311 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
9312 for i in 0..self.hidden_size {
9313 h1[i] *= sc;
9314 h2[i] *= sc;
9315 }
9316 }
9317 if self.is_loop_end(li) && li + 1 < self.num_layers {
9319 h1 = inference::rms_norm(
9320 &h1,
9321 &self.weights.final_norm,
9322 self.rms_eps,
9323 self.norm_style,
9324 );
9325 h2 = inference::rms_norm(
9326 &h2,
9327 &self.weights.final_norm,
9328 self.rms_eps,
9329 self.norm_style,
9330 );
9331 }
9332 }
9333 crate::gpu::set_layer(-1); if self.o1_active() {
9340 self.commit_linear_scratch();
9341 }
9342 self.o1_progress();
9343 self.swa_trim_tails();
9344 (h1, h2)
9345 }
9346
9347 fn commit_linear_scratch(&mut self) {
9349 for layer in &mut self.kv_cache.layers {
9350 if !layer.linear_scratch.is_empty() {
9351 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
9352 layer.linear_scratch.clear();
9353 }
9354 }
9355 }
9356
9357 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9365 #[cfg(target_os = "macos")]
9366 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9367 if ids.is_empty() {
9368 return Err("empty id sequence".to_string());
9369 }
9370
9371 self.nll_begin()?;
9372 let result = (|| {
9373 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9374 self.nll_check_graph("encoder prefill", 0)?;
9375 let expected = ids.len() * self.hidden_size;
9376 if hidden.len() != expected {
9377 return Err(format!(
9378 "encoder prefill returned {} values for {} rows of hidden size {}",
9379 hidden.len(),
9380 ids.len(),
9381 self.hidden_size
9382 ));
9383 }
9384
9385 let mut out = vec![0.0f32; expected];
9386 for (src, dst) in hidden
9387 .chunks_exact(self.hidden_size)
9388 .zip(out.chunks_exact_mut(self.hidden_size))
9389 {
9390 inference::rms_norm_into(
9391 src,
9392 &self.weights.final_norm,
9393 self.rms_eps,
9394 self.norm_style,
9395 dst,
9396 );
9397 }
9398 Ok(out)
9399 })();
9400 self.nll_end();
9401 result
9402 }
9403
9404 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9409 #[cfg(target_os = "macos")]
9410 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9411 if ids.is_empty() {
9412 return Err("empty id sequence".to_string());
9413 }
9414 self.nll_begin()?;
9415 let result = (|| {
9416 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9417 self.nll_check_graph("hidden prefill", 0)?;
9418 if hidden.len() != ids.len() * self.hidden_size {
9419 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9420 }
9421 Ok(hidden)
9422 })();
9423 self.nll_end();
9424 result
9425 }
9426
9427 pub fn forward_ids(
9430 &mut self,
9431 ids: &[u32],
9432 task_mask: Option<&TaskMask>,
9433 ) -> Result<Vec<f32>, String> {
9434 #[cfg(target_os = "macos")]
9435 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9436 if ids.is_empty() {
9437 return Err("empty id sequence".to_string());
9438 }
9439 self.clear_sequence_state();
9440 self.check_forward_graph("forward_ids setup", 0)?;
9441 if task_mask.is_none() {
9442 self.o1_begin();
9443 }
9444 let mut hidden = vec![0.0f32; self.hidden_size];
9445 let mut pos = 0usize;
9446 if let Some(b) = &mut self.dsv41 {
9447 let pool = self.pool.clone();
9448 let mut logits = Vec::new();
9449 crate::dsv41::forward_chunk(
9450 &b.0,
9451 &b.1,
9452 &b.2,
9453 &mut b.3,
9454 ids,
9455 0,
9456 pool.as_deref(),
9457 &mut logits,
9458 );
9459 if let Err(err) = self.o1_seal_checked() {
9460 self.clear_sequence_state();
9461 return Err(err);
9462 }
9463 return Ok(logits);
9464 }
9465 #[cfg(not(target_os = "macos"))]
9476 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9477 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9478 Ok(r) => r,
9479 Err(e) => {
9480 self.clear_sequence_state();
9481 return Err(e);
9482 }
9483 };
9484 if n > 0 {
9485 hidden.copy_from_slice(&last);
9486 pos = n;
9487 }
9488 }
9489 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9490 let chunk = self.prefill_chunk();
9494 let hs = self.hidden_size;
9495 while pos < ids.len() {
9496 let end = (pos + chunk).min(ids.len());
9497 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9498 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9499 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9500 pos = end;
9501 }
9502 }
9503 if task_mask.is_none()
9512 && !self.graph_prefill_preferred()
9513 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9514 && self.pair_supported()
9515 {
9516 while pos + 1 < ids.len() {
9517 let e1 = self.embed_single(ids[pos]);
9518 let e2 = self.embed_single(ids[pos + 1]);
9519 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9520 self.check_forward_graph("forward_ids pair", pos + 1)?;
9521 self.commit_linear_scratch();
9522 hidden = h2;
9523 pos += 2;
9524 }
9525 }
9526 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9529 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9530 self.graph_logits = Some(lg);
9531 hidden = vec![0.0; self.hidden_size];
9532 pos = ids.len();
9533 }
9534 }
9535 while pos < ids.len() {
9536 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9537 self.check_forward_graph("forward_ids", pos)?;
9538 pos += 1;
9539 }
9540 if let Some(logits) = self.graph_logits.take() {
9541 if let Err(err) = self.o1_seal_checked() {
9545 self.clear_sequence_state();
9546 return Err(err);
9547 }
9548 return Ok(logits);
9549 }
9550 if let Err(err) = self.o1_seal_checked() {
9554 self.clear_sequence_state();
9555 return Err(err);
9556 }
9557 let normed = inference::rms_norm(
9558 &hidden,
9559 &self.weights.final_norm,
9560 self.rms_eps,
9561 self.norm_style,
9562 );
9563 Ok(self.lm_head_forward(&normed))
9564 }
9565
9566 #[doc(hidden)]
9570 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9571 #[cfg(target_os = "macos")]
9572 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9573 if ids.is_empty() {
9574 return Err("empty id sequence".to_string());
9575 }
9576 self.clear_sequence_state();
9577 self.dsv41
9578 .as_ref()
9579 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9580 self.o1_begin();
9581 let rows = {
9582 let pool = self.pool.clone();
9583 let b = self
9584 .dsv41
9585 .as_mut()
9586 .expect("dsv41 checked above; state cannot change during forward");
9587 let mut rows = Vec::with_capacity(ids.len());
9588 for (position, &id) in ids.iter().enumerate() {
9589 let mut logits = Vec::new();
9590 crate::dsv41::forward_token(
9591 &b.0,
9592 &b.1,
9593 &b.2,
9594 &mut b.3,
9595 id,
9596 position,
9597 pool.as_deref(),
9598 &mut logits,
9599 );
9600 rows.push(logits);
9601 }
9602 rows
9603 };
9604 self.o1_seal();
9605 Ok(rows)
9606 }
9607
9608 pub fn dsv41_moe_capture(
9612 &mut self,
9613 ids: &[u32],
9614 lo: usize,
9615 hi: usize,
9616 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9617 #[cfg(target_os = "macos")]
9618 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9619 if ids.is_empty() {
9620 return Err("empty id sequence".to_string());
9621 }
9622 self.dsv41
9623 .as_ref()
9624 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9625 self.clear_sequence_state();
9626 let res = {
9627 let pool = self.pool.clone();
9628 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9629 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9630 };
9631 self.clear_sequence_state();
9632 res
9633 }
9634
9635 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9642 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9643 Ok((nll / cnt.max(1) as f64).exp())
9644 }
9645
9646 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9653 if !self.can_prefill_batched() {
9654 return self.nll_ids_from(ids, 0).map(|_| ());
9655 }
9656 self.nll_begin()?;
9657 let result: Result<(), String> = (|| {
9658 let chunk = chunk.max(1);
9659 let mut pos = 0usize;
9660 while pos < ids.len() {
9661 let end = (pos + chunk).min(ids.len());
9662 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9663 self.nll_check_graph("calibration prefill", pos)?;
9664 pos = end;
9665 }
9666 Ok(())
9667 })();
9668 self.nll_end();
9669 result
9670 }
9671
9672 pub fn calib_window_span(
9681 &mut self,
9682 ids: &[u32],
9683 input: Option<&[f32]>,
9684 from: usize,
9685 mid: usize,
9686 upto: usize,
9687 ) -> Result<Vec<f32>, String> {
9688 if !self.can_prefill_batched() {
9689 return Err("calib_window_span: this architecture has no batched prefill".into());
9690 }
9691 self.nll_begin()?;
9692 let result: Result<Vec<f32>, String> = (|| {
9693 let h = if mid > from {
9694 let inp = match input {
9695 Some(h) => PrefillIn::Hidden(h),
9696 None => PrefillIn::Ids(ids),
9697 };
9698 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9699 self.nll_check_graph("calibration span", 0)?;
9700 h
9701 } else {
9702 input.map(<[f32]>::to_vec).unwrap_or_default()
9703 };
9704 if upto > mid {
9705 let inp = if mid > from || input.is_some() {
9706 PrefillIn::Hidden(&h)
9707 } else {
9708 PrefillIn::Ids(ids)
9709 };
9710 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9711 self.nll_check_graph("calibration span", 0)?;
9712 }
9713 Ok(h)
9714 })();
9715 self.nll_end();
9716 result
9717 }
9718
9719 #[cfg(feature = "gpu")]
9723 pub fn qwen4_capture(
9724 &mut self,
9725 ids: &[u32],
9726 lo: usize,
9727 hi: usize,
9728 ) -> Result<Vec<Vec<f32>>, String> {
9729 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9730 crate::qwen4_exp::capture_window(
9731 &b.0,
9732 &b.1,
9733 &b.2,
9734 &mut b.3,
9735 ids,
9736 lo,
9737 hi,
9738 &self.inv_freq,
9739 self.pool.as_deref(),
9740 )
9741 }
9742
9743 #[cfg(feature = "gpu")]
9749 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9750 use crate::gpu_wgpu::clover as cap;
9751 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9752 let ne = (lo..hi)
9753 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9754 FfnKind::Moe(m) => Some(m.experts.len()),
9755 _ => None,
9756 })
9757 .ok_or("no MoE layer in the capture range")?;
9758 let chunk = self.prefill_chunk().max(1);
9759 self.clear_sequence_state();
9760 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9761 return Err("capture unavailable (no wgpu device)".into());
9762 }
9763 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9764 let res = (|| {
9765 for (ci, part) in ids.chunks(chunk).enumerate() {
9766 let mut hiddens = Vec::with_capacity(part.len() * hs);
9767 for &id in part {
9768 hiddens.extend_from_slice(&self.embed_single(id));
9769 }
9770 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9771 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9772 crate::gpu::BatchGraphOutcome::Completed => {}
9773 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9774 }
9775 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9776 if written < hi - lo {
9777 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9778 }
9779 for (o, r) in out.iter_mut().zip(rows) {
9780 o.extend_from_slice(&r);
9781 }
9782 }
9783 Ok(())
9784 })();
9785 cap::moe_capture_end();
9786 self.clear_sequence_state();
9787 res.map(|_| out)
9788 }
9789
9790 pub fn moe_expert_dirs(
9793 &self,
9794 li: usize,
9795 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9796 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9797 return None;
9798 };
9799 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9800 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9801 let shared = match &m.shared {
9802 Some((se, _)) => Some(dirs(se)?),
9803 None => None,
9804 };
9805 Some((routed, shared))
9806 }
9807
9808 pub fn qwen4_expert_dirs(
9810 &self,
9811 li: usize,
9812 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9813 let b = self.qwen4_exp.as_ref()?;
9814 crate::qwen4_exp::expert_dirs(&b.1, li)
9815 }
9816
9817 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9832 self.reset_mtp_probe_state();
9833 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9839 let result = if ids.len() < 2 {
9840 Err("MTP FFN probe needs at least two token ids".into())
9841 } else if ids
9842 .iter()
9843 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9844 {
9845 Err("MTP FFN probe received a token id outside the embedding table".into())
9846 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9847 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9850 } else if self.mimo_mtp.is_some() {
9851 let mut st = self
9852 .mimo_mtp
9853 .take()
9854 .expect("checked MiMo MTP before detaching it");
9855 let result = match self.probe_trunk_rows(ids) {
9856 Ok(trunk) => crate::gpu::cpu_scope(|| {
9857 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9858 }),
9859 Err(e) => Err(e),
9860 };
9861 st.reset();
9864 self.mimo_mtp = Some(st);
9865 result
9866 } else if self.mtp.is_some() {
9867 let mut mtp = self
9868 .mtp
9869 .take()
9870 .expect("checked canonical MTP before detaching it");
9871 let result = match self.probe_trunk_rows(ids) {
9872 Ok(trunk) => crate::gpu::cpu_scope(|| {
9873 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9874 }),
9875 Err(e) => Err(e),
9876 };
9877 mtp.kv.clear();
9878 self.mtp = Some(mtp);
9879 result
9880 } else {
9881 Err("MTP FFN probe: this pipeline has no draft stack".into())
9882 };
9883 self.reset_mtp_probe_state();
9886 result
9887 }
9888
9889 fn reset_mtp_probe_state(&mut self) {
9891 self.clear_sequence_state();
9892 if let Some(mtp) = self.mtp.as_mut() {
9893 mtp.kv.clear();
9894 }
9895 if let Some(mimo) = self.mimo_mtp.as_mut() {
9896 mimo.reset();
9897 }
9898 self.mtp_graph_mode = None;
9899 }
9900
9901 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9907 if ids.len() < 2 {
9908 return Err("MTP FFN probe needs at least two token ids".into());
9909 }
9910 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9911 self.clear_sequence_state();
9912 Ok(rows)
9913 }
9914
9915 fn mtp_ffn_probe_teacher_forced(
9921 &mut self,
9922 mtp: &mut MtpModule,
9923 ids: &[u32],
9924 trunk: &[f32],
9925 ) -> Result<Vec<MtpFfnProbe>, String> {
9926 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9927 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9928 };
9929 let inter = dense.gate_proj.rows();
9930 let (hs, n) = (self.hidden_size, ids.len() - 1);
9931 if trunk.len() != n * hs {
9932 return Err(format!(
9933 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9934 trunk.len()
9935 ));
9936 }
9937 let mut mass = vec![0.0f64; inter];
9938 let mut squared_mass = vec![0.0f64; inter];
9939 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9940 for position in 0..n {
9941 self.mtp_probe_ffn_input(
9942 mtp,
9943 &trunk[position * hs..(position + 1) * hs],
9944 ids[position + 1],
9945 position,
9946 )?;
9947 inputs.extend_from_slice(&self.ws.p1[..hs]);
9948 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9949 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9950 unreachable!("checked dense above");
9951 };
9952 accumulate_dense_ffn_activation_mass(
9953 dense,
9954 &inputs,
9955 inputs.len() / hs,
9956 self.pool.as_deref(),
9957 &mut mass,
9958 &mut squared_mass,
9959 )?;
9960 inputs.clear();
9961 }
9962 }
9963 Ok(vec![MtpFfnProbe {
9964 mass,
9965 squared_mass,
9966 observed_positions: n,
9967 }])
9968 }
9969
9970 fn mtp_probe_ffn_input(
9976 &mut self,
9977 mtp: &mut MtpModule,
9978 hidden: &[f32],
9979 next_token: u32,
9980 position: usize,
9981 ) -> Result<(), String> {
9982 let e = self.embed_single(next_token);
9983 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9984 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9985 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9986 inference::rms_norm_into(
9987 hidden,
9988 &mtp.hnorm,
9989 self.rms_eps,
9990 self.norm_style,
9991 cat_h,
9992 );
9993 let mut x = vec![0.0f32; self.hidden_size];
9994 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9995
9996 let lw = &mtp.layer;
9997 inference::rms_norm_into(
9998 &x,
9999 &lw.input_norm,
10000 self.rms_eps,
10001 self.norm_style,
10002 &mut self.ws.n1,
10003 );
10004 let attn = match &lw.attn {
10005 AttnKind::Full {
10006 wq,
10007 wk,
10008 wv,
10009 wo,
10010 q_norm,
10011 k_norm,
10012 output_gate,
10013 softplus_gate,
10014 bias,
10015 } => {
10016 let mut cfg = self.attn_cfg(position);
10017 cfg.q_norm = q_norm.as_deref();
10018 cfg.k_norm = k_norm.as_deref();
10019 cfg.output_gate = *output_gate;
10020 cfg.softplus_gate = softplus_gate
10021 .as_ref()
10022 .map(|(gate, per_head)| (gate, *per_head));
10023 cfg.bias = bias
10024 .as_ref()
10025 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10026 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
10027 }
10028 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
10029 };
10030 for (dst, src) in x.iter_mut().zip(attn) {
10031 *dst += src;
10032 }
10033 inference::rms_norm_into(
10034 &x,
10035 &lw.post_norm,
10036 self.rms_eps,
10037 self.norm_style,
10038 &mut self.ws.p1,
10039 );
10040 Ok(())
10041 }
10042
10043 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
10048 self.clear_sequence_state();
10049 FFN_PROBE.with(|p| {
10050 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10051 });
10052 crate::gpu::cpu_scope(|| {
10053 for (pos, &id) in ids.iter().enumerate() {
10054 let emb = self.embed_single(id);
10055 let _ = self.forward_layers(&emb, pos, None);
10056 }
10057 });
10058 self.clear_sequence_state();
10059 FFN_PROBE
10060 .with(|p| p.borrow_mut().take())
10061 .unwrap_or_default()
10062 }
10063
10064 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
10068 if let Err(err) = self.nll_begin() {
10069 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
10073 self.nll_end();
10074 return Err(err);
10075 }
10076 FFN_PROBE.with(|p| {
10077 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
10078 });
10079 let result: Result<(), String> = (|| {
10080 for chunk in ids.chunks(256) {
10081 if chunk.len() < 2 {
10082 continue;
10083 }
10084 self.nll_ids_masked(chunk, 0, None)?;
10085 }
10086 Ok(())
10087 })();
10088 self.nll_end();
10089 let probe = FFN_PROBE
10090 .with(|p| p.borrow_mut().take())
10091 .unwrap_or_default();
10092 match result {
10093 Ok(()) => Ok(probe),
10094 Err(err) => {
10095 drop(probe);
10096 Err(err)
10097 }
10098 }
10099 }
10100
10101 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
10105 self.nll_begin()?;
10106 let result: Result<f64, String> = (|| {
10107 let mut nll = 0f64;
10108 let mut cnt = 0usize;
10109 let mut hidden = vec![0f32; self.hidden_size];
10110 for (pos, &id) in ids.iter().enumerate() {
10111 if pos > 0 {
10112 inference::rms_norm_into(
10113 &hidden,
10114 &self.weights.final_norm,
10115 self.rms_eps,
10116 self.norm_style,
10117 &mut self.ws.n1,
10118 );
10119 let mut logits = self.lm_head_forward(&self.ws.n1);
10120 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
10121 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
10122 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
10123 nll -= p.max(1e-300).ln();
10124 cnt += 1;
10125 attention::recycle_buf(&mut logits);
10126 }
10127 let emb = self.embed_single(id);
10128 hidden = self.forward_layers(&emb, pos, Some(mask));
10129 self.nll_check_graph("masked serial forward", pos)?;
10130 let _ = self.graph_logits.take();
10134 }
10135 Ok((nll / cnt.max(1) as f64).exp())
10136 })();
10137 self.nll_end();
10138 result
10139 }
10140
10141 pub fn nll_ids_masked(
10160 &mut self,
10161 ids: &[u32],
10162 start: usize,
10163 task_mask: Option<&TaskMask>,
10164 ) -> Result<(f64, usize), String> {
10165 let task_mask = self.drop_open_mask(task_mask);
10166 self.nll_ids_inner(ids, start, task_mask)
10167 }
10168
10169 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
10170 self.nll_ids_inner(ids, start, None)
10171 }
10172
10173 pub fn prefill_parity_probe(
10182 &mut self,
10183 ids: &[u32],
10184 split: usize,
10185 bk: usize,
10186 ) -> Result<String, String> {
10187 use std::fmt::Write as _;
10188 let (n, hs) = (ids.len(), self.hidden_size);
10189 if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
10190 return Err(format!(
10191 "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
10192 ));
10193 }
10194 let kinds: Vec<u8> = (0..self.num_layers)
10196 .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
10197 AttnKind::LinearGdn(_) => 1,
10198 AttnKind::Full { .. } => 2,
10199 _ => 0,
10200 })
10201 .collect();
10202 let kv_id = self.graph_kv_id;
10203 let host_chunk = self.prefill_chunk().max(1);
10204 let host_states = |p: &Self| -> Vec<Vec<f32>> {
10205 (0..p.num_layers)
10206 .map(|li| {
10207 if kinds[li] == 1 {
10208 p.kv_cache.layers[li].linear_state.clone()
10209 } else {
10210 Vec::new()
10211 }
10212 })
10213 .collect()
10214 };
10215 let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
10216 (0..kinds.len())
10217 .map(|li| {
10218 if kinds[li] == 1 {
10219 crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
10220 } else {
10221 Vec::new()
10222 }
10223 })
10224 .collect()
10225 };
10226 let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10228 (0..p.num_layers)
10229 .map(|li| {
10230 let c = &p.kv_cache.layers[li];
10231 if kinds[li] != 2
10232 || c.mode != crate::kv_cache::KvMode::F32
10233 || c.base() != 0
10234 || c.seq_len != n
10235 {
10236 return (Vec::new(), Vec::new());
10237 }
10238 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10239 let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
10240 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10241 for pos in 0..n {
10242 let d = (pos * nkv + h) * hd;
10243 k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
10244 v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
10245 }
10246 }
10247 (k, v)
10248 })
10249 .collect()
10250 };
10251 let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
10252 (0..p.num_layers)
10253 .map(|li| {
10254 let c = &p.kv_cache.layers[li];
10255 if kinds[li] != 2 {
10256 return (Vec::new(), Vec::new());
10257 }
10258 crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
10259 .and_then(|mut v| v.pop())
10260 .unwrap_or_default()
10261 })
10262 .collect()
10263 };
10264 let host_walk = |p: &mut Self,
10265 from: usize,
10266 to: usize,
10267 rows: &mut [f32],
10268 at_split: &mut Option<Vec<Vec<f32>>>| {
10269 let mut pos = from;
10270 while pos < to {
10271 let mut end = (pos + host_chunk).min(to);
10272 if pos < split && end > split {
10273 end = split;
10274 }
10275 let hb = p.prefill_batch(&ids[pos..end], pos);
10276 rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10277 pos = end;
10278 if pos == split {
10279 *at_split = Some(host_states(p));
10280 }
10281 }
10282 };
10283 let graph_walk = |p: &mut Self,
10284 from: usize,
10285 to: usize,
10286 rows: &mut [f32],
10287 at_split: &mut Option<Vec<Vec<f32>>>|
10288 -> Result<(), String> {
10289 let mut pos = from;
10290 while pos < to {
10291 let mut end = (pos + bk).min(to);
10292 if pos < split && end > split {
10293 end = split;
10294 }
10295 let k = end - pos;
10296 let mut hid = vec![0f32; k * hs];
10297 for (j, &id) in ids[pos..end].iter().enumerate() {
10298 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10299 }
10300 let positions: Vec<usize> = (pos..end).collect();
10301 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10302 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10303 return Err(format!(
10304 "prefill parity: batch graph {outcome:?} at position {pos}"
10305 ));
10306 }
10307 rows[pos * hs..end * hs].copy_from_slice(&hid);
10308 pos = end;
10309 if pos == split {
10310 *at_split = Some(graph_states(p));
10311 }
10312 }
10313 Ok(())
10314 };
10315 let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
10316
10317 let t = std::time::Instant::now();
10318 self.clear_sequence_state();
10319 let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
10320 host_walk(self, 0, n, &mut hh, &mut h_split);
10321 let (h_end, h_kv) = (host_states(self), host_kv(self));
10322 let t_h = t.elapsed().as_secs_f64();
10323
10324 let t = std::time::Instant::now();
10325 self.clear_sequence_state();
10326 let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
10327 let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
10328 let (g_end, g_kv) = (graph_states(self), graph_kv(self));
10329 let t_g = t.elapsed().as_secs_f64();
10330
10331 let t = std::time::Instant::now();
10332 self.clear_sequence_state();
10333 let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
10334 host_walk(self, 0, split, &mut hm, &mut m_split);
10335 let m_run = graph_walk(self, split, n, &mut hm, &mut None);
10336 let (m_end, m_kv) = (graph_states(self), graph_kv(self));
10337 let t_m = t.elapsed().as_secs_f64();
10338 self.clear_sequence_state();
10339 g_run?;
10340 m_run?;
10341
10342 let (sh, sg, sm) = (
10343 score(self, &hh, 0),
10344 score(self, &hg, 0),
10345 score(self, &hm, split),
10346 );
10347 let mut r = String::new();
10348 let _ = writeln!(
10349 r,
10350 "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
10351 (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
10352 );
10353 let _ = writeln!(
10354 r,
10355 " bin | NLL host graph h+g | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
10356 );
10357 let bin = 256usize;
10358 let mut b0 = 0;
10359 while b0 + 1 < n {
10360 let b1 = (b0 + bin).min(n - 1);
10361 let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
10362 let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
10363 (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
10364 };
10365 let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
10366 let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
10367 if ps.is_empty() {
10368 return None;
10369 }
10370 let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
10371 Some(100.0 * same as f64 / ps.len() as f64)
10372 };
10373 let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
10374 (b0.max(from)..b1)
10375 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
10376 .reduce(f64::max)
10377 };
10378 let f =
10379 |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
10380 let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
10381 let _ = writeln!(
10382 r,
10383 " {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
10384 f(mean(&sh, 0), 3),
10385 f(mean(&sg, 0), 3),
10386 f(mean(&sm, split), 3),
10387 f(agree(&sh, 0, &sg, 0), 1),
10388 f(agree(&sm, split, &sg, 0), 1),
10389 e(hid_rel(&hh, 0)),
10390 e(hid_rel(&hm, split)),
10391 );
10392 b0 = b1;
10393 }
10394 let ring_len = self
10396 .gdn_cfg
10397 .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
10398 .unwrap_or(0);
10399 let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
10400 let _ = write!(r, " GDN {label} (ring/S rel):");
10401 let mut first = None;
10402 for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
10403 let (x, y) = (&a[li], &b[li]);
10404 if x.len() != y.len() || x.len() <= ring_len {
10405 let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
10406 continue;
10407 }
10408 let (rr, rs) = (
10409 rel_l2(&x[..ring_len], &y[..ring_len]),
10410 rel_l2(&x[ring_len..], &y[ring_len..]),
10411 );
10412 if first.is_none() && rr.max(rs) > 1e-2 {
10413 first = Some(li);
10414 }
10415 let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
10416 }
10417 let _ = writeln!(
10418 r,
10419 "\n first GDN layer past 1e-2: {}",
10420 first.map_or("none".to_string(), |l| l.to_string())
10421 );
10422 };
10423 match (&h_split, &g_split) {
10424 (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
10425 _ => {
10426 let _ = writeln!(r, " GDN at split: not captured");
10427 }
10428 }
10429 if let (Some(a), Some(b)) = (&m_split, &h_split) {
10430 state_line(&mut r, "h+g host part vs host at split", a, b);
10431 }
10432 state_line(&mut r, "host vs graph at end", &h_end, &g_end);
10433 state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
10434 let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
10437 let _ = writeln!(r, " K/V {label} (worst bin K/V rel, first bin past 1e-2):");
10438 for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
10439 let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
10440 if ak.len() != gk.len() || ak.is_empty() {
10441 let _ = writeln!(r, " L{li}: rows {}/{}", ak.len(), gk.len());
10442 continue;
10443 }
10444 let row = ak.len() / n;
10445 let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
10446 let mut b0 = from;
10447 while b0 < n {
10448 let b1 = (b0 + bin).min(n);
10449 let (s0, s1) = (b0 * row, b1 * row);
10450 let (rk, rv) = (
10451 rel_l2(&ak[s0..s1], &gk[s0..s1]),
10452 rel_l2(&av[s0..s1], &gv[s0..s1]),
10453 );
10454 (wk, wv) = (wk.max(rk), wv.max(rv));
10455 if first.is_none() && rk.max(rv) > 1e-2 {
10456 first = Some(b0);
10457 }
10458 b0 = b1;
10459 }
10460 let _ = writeln!(
10461 r,
10462 " L{li}: {wk:.1e}/{wv:.1e} first {}",
10463 first.map_or("none".to_string(), |p| p.to_string())
10464 );
10465 }
10466 };
10467 kv_line(&mut r, "host vs graph", &h_kv, 0);
10468 kv_line(&mut r, "h+g vs graph", &m_kv, 0);
10469 Ok(r)
10470 }
10471
10472 fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
10476 let (n, hs) = (ids.len(), self.hidden_size);
10477 let vrows = self.weights.lm_head.rows();
10478 let vs = self.vocab_size.min(vrows);
10479 let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
10480 let mut pos = from;
10481 while pos + 1 < n {
10482 let end = (pos + 32).min(n - 1);
10483 let sb = end - pos;
10484 let mut normed = vec![0f32; sb * hs];
10485 for j in 0..sb {
10486 let r = inference::rms_norm(
10487 &rows[(pos + j) * hs..(pos + j + 1) * hs],
10488 &self.weights.final_norm,
10489 self.rms_eps,
10490 self.norm_style,
10491 );
10492 normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
10493 }
10494 let mut logits = vec![0f32; sb * vrows];
10495 self.weights
10496 .lm_head
10497 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10498 for j in 0..sb {
10499 let lg = &mut logits[j * vrows..j * vrows + vs];
10500 if let Some(mu) = self.logit_multiplier {
10501 lg.iter_mut().for_each(|v| *v *= mu);
10502 }
10503 if let Some(c) = self.final_softcap {
10504 softcap_in_place(lg, c, self.pool.as_deref());
10505 }
10506 let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
10507 for (i, &v) in lg.iter().enumerate() {
10508 if v > max {
10509 (max, top) = (v, i);
10510 }
10511 }
10512 let lse = lg
10513 .iter()
10514 .map(|&v| ((v - max) as f64).exp())
10515 .sum::<f64>()
10516 .ln()
10517 + max as f64;
10518 out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
10519 }
10520 pos = end;
10521 }
10522 out
10523 }
10524
10525 pub fn o1_seal_parity_probe(
10539 &mut self,
10540 ids: &[u32],
10541 cfg: crate::nystrom::O1Cfg,
10542 requested: usize,
10543 bk: usize,
10544 ) -> Result<String, String> {
10545 use std::fmt::Write as _;
10546 let (n, hs) = (ids.len(), self.hidden_size);
10547 self.set_o1(Some(cfg));
10548 let split = self
10549 .o1_effective_boundary(requested)
10550 .ok_or("o1 parity: no O(1) configuration")?;
10551 if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
10552 return Err(format!(
10553 "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
10554 ));
10555 }
10556 let o1_layers: Vec<usize> = (0..self.num_layers)
10557 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
10558 .collect();
10559 let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
10560 let mut pos = from;
10561 while pos < to {
10562 let end = (pos + bk).min(to);
10563 let k = end - pos;
10564 let mut hid = vec![0f32; k * hs];
10565 for (j, &id) in ids[pos..end].iter().enumerate() {
10566 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
10567 }
10568 let positions: Vec<usize> = (pos..end).collect();
10569 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
10570 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10571 return Err(format!(
10572 "o1 parity: batch graph {outcome:?} at position {pos}"
10573 ));
10574 }
10575 rows[pos * hs..end * hs].copy_from_slice(&hid);
10576 pos = end;
10577 }
10578 Ok(())
10579 };
10580 let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
10583 for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
10584 let c = &mut p.kv_cache.layers[li];
10585 let (row, qrow) = (k.len() / split, q.len() / split);
10586 for pos in 0..split {
10587 c.append(
10588 &k[pos * row..(pos + 1) * row],
10589 &v[pos * row..(pos + 1) * row],
10590 &[],
10591 );
10592 c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
10593 }
10594 }
10595 p.o1_seal_checked()
10596 };
10597 let steps: usize = std::env::var("CMF_PARITY_GREEDY")
10600 .ok()
10601 .and_then(|v| v.parse().ok())
10602 .unwrap_or(40);
10603 let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
10606 let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
10607 let argmax = |lg: &[f32], vs: usize| {
10608 (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
10609 };
10610 let normed = inference::rms_norm(
10611 &rows[(n - 1) * hs..n * hs],
10612 &p.weights.final_norm,
10613 p.rms_eps,
10614 p.norm_style,
10615 );
10616 let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
10617 for s in 1..steps {
10618 let (tok, at) = (out[s - 1], n + s - 1);
10619 let lg = if token_decode {
10620 p.decode_step_logits(tok, at)
10621 } else {
10622 let mut hid = p.embed_single(tok);
10623 let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
10624 if outcome != crate::gpu::BatchGraphOutcome::Completed {
10625 return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
10626 }
10627 let normed =
10628 inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
10629 p.lm_head_forward(&normed)
10630 };
10631 out.push(argmax(&lg, p.vocab_size));
10632 }
10633 Ok(out)
10634 };
10635
10636 self.clear_sequence_state();
10642 self.o1_begin_with_prefix(Some(requested));
10643 let walk_chunk = self.prefill_chunk().max(1);
10644 let walk_from = (split - 1) / walk_chunk * walk_chunk;
10645 let mut ow = vec![0f32; n * hs];
10646 let mut pos = 0;
10647 while pos < split {
10648 let end = (pos + walk_chunk).min(split);
10649 let hb = self.prefill_batch(&ids[pos..end], pos);
10650 ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10651 pos = end;
10652 }
10653 self.o1_seal_checked()?;
10654 let ow_run = graph_rows(self, split, n, &mut ow);
10655 let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
10656
10657 let t = std::time::Instant::now();
10661 self.clear_sequence_state();
10662 self.o1_begin_with_prefix(Some(split + 1));
10663 let mut oh = vec![0f32; n * hs];
10664 let mut pos = 0;
10665 while pos < split {
10666 let end = (pos + 128).min(split);
10667 let hb = self.prefill_batch(&ids[pos..end], pos);
10668 oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
10669 pos = end;
10670 }
10671 let mut host = Vec::with_capacity(o1_layers.len());
10672 for &li in &o1_layers {
10673 let c = &self.kv_cache.layers[li];
10674 let q = c
10675 .o1_q_trace()
10676 .ok_or("o1 parity: host walk sealed early")?
10677 .to_vec();
10678 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
10679 let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
10680 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
10681 for p in 0..split {
10682 let d = (p * nkv + h) * hd;
10683 k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
10684 v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
10685 }
10686 }
10687 host.push((q, k, v));
10688 }
10689 for &li in &o1_layers {
10690 self.kv_cache.layers[li].o1_set_boundary(split);
10691 }
10692 self.o1_seal_checked()?;
10693 let oh_run = graph_rows(self, split, n, &mut oh);
10694 let t_oh = t.elapsed().as_secs_f64();
10695 let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
10696
10697 self.clear_sequence_state();
10701 self.o1_begin_with_prefix(Some(requested));
10702 let mut og = vec![0f32; n * hs];
10703 graph_rows(self, 0, split, &mut og)?;
10704 let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
10705 .ok()
10706 .and_then(|v| v.parse().ok())
10707 .unwrap_or(0.0);
10708 if perturb > 0.0 {
10709 let mut seed = 0x9e37_79b9_7f4a_7c15u64;
10710 let noisy: Vec<_> = host
10711 .iter()
10712 .map(|(q, k, v)| {
10713 let mut jitter = |x: &f32| {
10714 seed ^= seed << 13;
10715 seed ^= seed >> 7;
10716 seed ^= seed << 17;
10717 x * if seed & 1 == 0 {
10718 1.0 + perturb
10719 } else {
10720 1.0 - perturb
10721 }
10722 };
10723 let k: Vec<f32> = k.iter().map(&mut jitter).collect();
10724 let v: Vec<f32> = v.iter().map(&mut jitter).collect();
10725 (q.clone(), k, v)
10726 })
10727 .collect();
10728 seal_from(self, &noisy)?;
10729 } else {
10730 seal_from(self, &host)?;
10731 }
10732 let og_run = graph_rows(self, split, n, &mut og);
10733 let og_text = og_run.clone().and_then(|_| greedy(self, &og));
10734
10735 self.clear_sequence_state();
10737 self.o1_begin_with_prefix(Some(requested));
10738 let mut od = vec![0f32; n * hs];
10739 graph_rows(self, 0, split, &mut od)?;
10740 let mut dev = Vec::with_capacity(o1_layers.len());
10741 let mut kv_rel = Vec::with_capacity(o1_layers.len());
10742 for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
10743 let c = &self.kv_cache.layers[li];
10744 let (k, v) = crate::gpu::graph_kv_read_rows(
10745 self.graph_kv_id,
10746 &[(li, 0, split)],
10747 c.num_kv_heads,
10748 c.head_dim,
10749 )
10750 .and_then(|mut rows| rows.pop())
10751 .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
10752 kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
10753 dev.push((q.clone(), k, v));
10754 }
10755 seal_from(self, &dev)?;
10756 let od_run = graph_rows(self, split, n, &mut od);
10757 let od_text = od_run.clone().and_then(|_| greedy(self, &od));
10758
10759 self.clear_sequence_state();
10761 self.set_o1(None);
10762 let mut xx = vec![0f32; n * hs];
10763 let x_run = graph_rows(self, 0, n, &mut xx);
10764 let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
10765 self.clear_sequence_state();
10766 oh_run?;
10767 ow_run?;
10768 og_run?;
10769 od_run?;
10770 x_run?;
10771
10772 let sx = self.parity_score(ids, &xx, split);
10773 let arms = [
10774 ("OH", self.parity_score(ids, &oh, split)),
10775 ("OG", self.parity_score(ids, &og, split)),
10776 ("OD", self.parity_score(ids, &od, split)),
10777 ("OW", self.parity_score(ids, &ow, split)),
10778 ];
10779 let mut r = String::new();
10780 let _ = writeln!(
10781 r,
10782 "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
10783 OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
10784 );
10785 let _ = write!(r, " seal K/V rel host vs graph:");
10786 for (li, rk, rv) in &kv_rel {
10787 let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
10788 }
10789 let worst = |a: &[f32], p0: usize, p1: usize| {
10792 (p0..p1)
10793 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
10794 .fold(0f64, f64::max)
10795 };
10796 let _ = writeln!(
10797 r,
10798 "\n prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
10799 OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
10800 worst(&oh, 0, walk_from),
10801 worst(&oh, walk_from, split),
10802 worst(&ow, 0, walk_from),
10803 worst(&ow, walk_from, split),
10804 );
10805 let _ = writeln!(
10806 r,
10807 " bin | NLL exact OH OG OD OW | top1=exact OH OG OD OW"
10808 );
10809 let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
10810 (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
10811 };
10812 let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
10813 let same = (b0..b1)
10814 .filter(|&p| s[p - split].1 == sx[p - split].1)
10815 .count();
10816 100.0 * same as f64 / (b1 - b0).max(1) as f64
10817 };
10818 let mut b0 = split;
10819 let mut total = [0f64; 5];
10820 while b0 + 1 < n {
10821 let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
10822 let _ = writeln!(
10823 r,
10824 " {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
10825 mean(&sx, b0, b1),
10826 mean(&arms[0].1, b0, b1),
10827 mean(&arms[1].1, b0, b1),
10828 mean(&arms[2].1, b0, b1),
10829 mean(&arms[3].1, b0, b1),
10830 agree(&arms[0].1, b0, b1),
10831 agree(&arms[1].1, b0, b1),
10832 agree(&arms[2].1, b0, b1),
10833 agree(&arms[3].1, b0, b1),
10834 );
10835 b0 = b1;
10836 }
10837 for (i, s) in std::iter::once(&sx)
10838 .chain(arms.iter().map(|a| &a.1))
10839 .enumerate()
10840 {
10841 total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
10842 }
10843 let _ = writeln!(
10844 r,
10845 " all | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
10846 total[0],
10847 total[1],
10848 total[2],
10849 total[3],
10850 total[4],
10851 (total[1] - total[0]).exp(),
10852 (total[2] - total[0]).exp(),
10853 (total[3] - total[0]).exp(),
10854 (total[4] - total[0]).exp(),
10855 );
10856 for (name, text) in [
10857 ("X ", x_text),
10858 ("OH", oh_text),
10859 ("OG", og_text),
10860 ("OD", od_text),
10861 ("OW", ow_text),
10862 ] {
10863 let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
10864 let _ = writeln!(r, " greedy {name}: {:?}", s);
10865 }
10866 Ok(r)
10867 }
10868
10869 fn nll_ids_inner(
10870 &mut self,
10871 ids: &[u32],
10872 start: usize,
10873 task_mask: Option<&TaskMask>,
10874 ) -> Result<(f64, usize), String> {
10875 self.nll_begin()?;
10876 #[cfg(target_os = "macos")]
10878 if task_mask.is_none() && self.is_block_diffusion() {
10879 if let Some(r) = self.clover_metal_nll(ids, start) {
10880 self.nll_end();
10881 return r;
10882 }
10883 }
10884 let result: Result<(f64, usize), String> = (|| {
10885 let mut nll = 0f64;
10886 let mut cnt = 0usize;
10887 let (graph_quality, fused_head_quality) = nll_graph_policy(
10900 task_mask.is_none(),
10901 self.graph_prefill_preferred(),
10902 crate::gpu::q1_force(),
10903 );
10904 self.graph_head_required = fused_head_quality;
10905 self.graph_want_logits = fused_head_quality;
10906 #[cfg(target_os = "macos")]
10907 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
10908 match self.nll_batch_metal(ids, start) {
10909 MetalBatchNllOutcome::Completed(nll, count) => {
10910 return Ok((nll, count));
10911 }
10912 MetalBatchNllOutcome::Declined => {}
10913 MetalBatchNllOutcome::Failed(err) => return Err(err),
10914 }
10915 }
10916 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
10921 if self.can_prefill_batched() && !graph_quality && !force_serial {
10922 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
10931 .ok()
10932 .and_then(|v| v.parse().ok())
10933 .filter(|&c: &usize| c > 0)
10934 .unwrap_or(128);
10935 const LM_SUB: usize = 32;
10936 let n = ids.len().saturating_sub(1);
10937 let hs = self.hidden_size;
10938 let rows = self.weights.lm_head.rows();
10939 let mut pos = 0usize;
10940 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
10941 while pos < n {
10942 let end = (pos + chunk_len).min(n);
10943 let bsz = end - pos;
10944 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
10945 self.nll_check_graph("batched prefill", pos)?;
10946 if state_trace && end % 256 == 0 {
10947 self.trace_recurrent_state(end);
10948 }
10949 let mut k0 = 0usize;
10950 while k0 < bsz {
10951 let k1 = (k0 + LM_SUB).min(bsz);
10952 let sb = k1 - k0;
10953 if pos + k1 <= start {
10956 k0 = k1;
10957 continue;
10958 }
10959 let mut normed = vec![0.0f32; sb * hs];
10960 for k in 0..sb {
10961 let r = inference::rms_norm(
10962 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
10963 &self.weights.final_norm,
10964 self.rms_eps,
10965 self.norm_style,
10966 );
10967 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
10968 }
10969 let mut logits = vec![0.0f32; sb * rows];
10970 self.weights
10971 .lm_head
10972 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
10973 for k in 0..sb {
10974 if pos + k0 + k < start {
10975 continue;
10976 }
10977 self.nll_check_graph("batched score row", pos + k0 + k)?;
10978 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
10979 if let Some(mu) = self.logit_multiplier {
10980 for v in lg.iter_mut() {
10981 *v *= mu;
10982 }
10983 }
10984 if let Some(c) = self.final_softcap {
10988 softcap_in_place(lg, c, self.pool.as_deref());
10989 }
10990 if let Some(cm) = self.head_clusters.clone() {
10993 self.hierarchical_head_logprobs(
10994 &normed[k * hs..(k + 1) * hs],
10995 &cm,
10996 lg,
10997 );
10998 }
10999 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
11000 let target = ids[pos + k0 + k + 1] as usize;
11001 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11002 let lse: f64 = lg
11003 .iter()
11004 .map(|&v| ((v - max) as f64).exp())
11005 .sum::<f64>()
11006 .ln()
11007 + max as f64;
11008 nll += lse - lg[target] as f64;
11009 cnt += 1;
11010 if std::env::var("CMF_PPL_TRACE").is_ok() {
11011 let top = lg
11012 .iter()
11013 .enumerate()
11014 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11015 .map(|(i, _)| i)
11016 .unwrap_or(0);
11017 eprintln!(
11018 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
11019 pos + k0 + k,
11020 target,
11021 lse - lg[target] as f64,
11022 top,
11023 lg[target],
11024 lg[top]
11025 );
11026 }
11027 }
11028 k0 = k1;
11029 }
11030 pos = end;
11031 }
11032 return Ok((nll, cnt));
11033 }
11034 for pos in 0..ids.len().saturating_sub(1) {
11035 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
11036 self.nll_check_graph("serial forward", pos)?;
11037 let out_of_band = self.graph_logits.take();
11045 if self.graph_head_required && out_of_band.is_none() {
11046 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
11047 return Err(format!(
11048 "fused Metal graph head did not complete at NLL position {pos}"
11049 ));
11050 }
11051 if pos < start {
11052 continue;
11053 }
11054 let logits = match out_of_band {
11055 Some(lg) => lg,
11056 None => {
11057 let normed = inference::rms_norm(
11058 &hidden,
11059 &self.weights.final_norm,
11060 self.rms_eps,
11061 self.norm_style,
11062 );
11063 self.lm_head_forward(&normed)
11067 }
11068 };
11069 let target = ids[pos + 1] as usize;
11070 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11071 let lse: f64 = logits
11072 .iter()
11073 .map(|&v| ((v - max) as f64).exp())
11074 .sum::<f64>()
11075 .ln()
11076 + max as f64;
11077 let tok_nll = lse - logits[target] as f64;
11078 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11079 let top = logits
11080 .iter()
11081 .enumerate()
11082 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11083 .map(|(i, _)| i)
11084 .unwrap_or(0);
11085 eprintln!(
11086 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11087 logits[target], logits[top]
11088 );
11089 }
11090 nll += tok_nll;
11091 cnt += 1;
11092 }
11093 Ok((nll, cnt))
11094 })();
11095 self.nll_end();
11096 result
11097 }
11098
11099 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
11104 let normed = inference::rms_norm(
11105 hidden,
11106 &self.weights.final_norm,
11107 self.rms_eps,
11108 self.norm_style,
11109 );
11110 let mut logits = self.lm_head_forward(&normed);
11113 let target = target as usize;
11114 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11115 let lse: f64 = logits
11116 .iter()
11117 .map(|&v| ((v - max) as f64).exp())
11118 .sum::<f64>()
11119 .ln()
11120 + max as f64;
11121 let tok_nll = lse - logits[target] as f64;
11122 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11123 let top = logits
11124 .iter()
11125 .enumerate()
11126 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11127 .map(|(i, _)| i)
11128 .unwrap_or(0);
11129 eprintln!(
11130 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11131 logits[target], logits[top]
11132 );
11133 }
11134 attention::recycle_buf(&mut logits);
11135 tok_nll
11136 }
11137
11138 fn trace_recurrent_state(&self, pos: usize) {
11146 let stats = |v: &[f32]| -> (f64, f64) {
11147 if v.is_empty() {
11148 return (0.0, 0.0);
11149 }
11150 let (mut ss, mut mx) = (0f64, 0f64);
11151 for &x in v {
11152 ss += (x as f64) * (x as f64);
11153 mx = mx.max((x as f64).abs());
11154 }
11155 ((ss / v.len() as f64).sqrt(), mx)
11156 };
11157 for (li, l) in self.kv_cache.layers.iter().enumerate() {
11158 let lw = &self.weights.layers[self.phys_layer(li)];
11159 let (kind, s_len) = match &lw.attn {
11160 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
11161 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
11162 AttnKind::Bounded(_) => ("bounded", 0),
11163 AttnKind::Full { .. } => ("full", 0),
11164 _ => ("other", 0),
11165 };
11166 let (rms, max) = stats(&l.linear_state);
11167 let s_part = if kind == "vmf" {
11168 &l.linear_state[..s_len.min(l.linear_state.len())]
11169 } else if kind == "gdn" {
11170 let ring = l
11171 .linear_state
11172 .len()
11173 .saturating_sub(s_len)
11174 .min(l.linear_state.len());
11175 &l.linear_state[ring..]
11176 } else {
11177 &l.linear_state[..0]
11178 };
11179 let (s_rms, s_max) = stats(s_part);
11180 let (ring_rms, ring_len) = match &l.bounded {
11181 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
11182 None => (0.0, 0),
11183 };
11184 eprintln!(
11185 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
11186 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
11187 l.linear_state.len(),
11188 l.seq_len
11189 );
11190 }
11191 }
11192
11193 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
11211 self.nll_begin()?;
11216 let requested_prefix = (prefill > 0).then_some(prefill);
11217 self.o1_begin_with_prefix(requested_prefix);
11218 let n = ids.len().saturating_sub(1);
11219 let requested_start = prefill.min(n);
11220 let exact_end = if self.o1_active() {
11225 match requested_prefix {
11226 Some(requested) => self.o1_effective_boundary(requested),
11227 None => self
11228 .o1_cfg
11229 .as_ref()
11230 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
11231 }
11232 .unwrap_or(requested_start)
11233 .min(n)
11234 } else {
11235 requested_start
11236 };
11237 let mut nll = 0f64;
11238 let mut cnt = 0usize;
11239
11240 let mut pos = 0usize;
11244 if self.can_prefill_batched() {
11245 const CHUNK: usize = 128;
11246 while pos < exact_end {
11247 let end = (pos + CHUNK).min(exact_end);
11248 let hiddens = self.prefill_batch(&ids[pos..end], pos);
11249 if self
11250 .graph_failed
11251 .swap(false, std::sync::atomic::Ordering::Relaxed)
11252 {
11253 self.cancel
11254 .store(false, std::sync::atomic::Ordering::Relaxed);
11255 self.nll_end();
11256 return Err("GPU graph failed during O(1) NLL prefix".into());
11257 }
11258 for row in 0..end - pos {
11259 let score_pos = pos + row;
11260 if score_pos >= requested_start && score_pos < n {
11261 nll += self.nll_from_hidden(
11262 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
11263 ids[score_pos + 1],
11264 score_pos,
11265 );
11266 cnt += 1;
11267 }
11268 }
11269 pos = end;
11270 }
11271 } else {
11272 while pos < exact_end {
11273 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11274 if self
11275 .graph_failed
11276 .swap(false, std::sync::atomic::Ordering::Relaxed)
11277 {
11278 self.cancel
11279 .store(false, std::sync::atomic::Ordering::Relaxed);
11280 self.nll_end();
11281 return Err("GPU graph failed during O(1) NLL prefix".into());
11282 }
11283 if pos >= requested_start && pos < n {
11284 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11285 cnt += 1;
11286 }
11287 pos += 1;
11288 }
11289 }
11290 self.o1_seal_checked().map_err(|err| {
11291 self.nll_end();
11292 err
11293 })?;
11294
11295 let batch_k = std::env::var("CMF_BATCH_K")
11304 .ok()
11305 .and_then(|v| v.parse::<usize>().ok())
11306 .unwrap_or(0);
11307 let batch_admitted = batch_k > 0
11308 && self.can_prefill_batched()
11309 && self.o1_active()
11310 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
11311 && (0..self.num_layers).all(|li| {
11312 let cache = &self.kv_cache.layers[self.phys_layer(li)];
11313 cache.o1.is_none() || cache.o1_views().is_some()
11314 });
11315 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11316 eprintln!(
11317 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
11318 batch_admitted,
11319 batch_k,
11320 n.saturating_sub(exact_end),
11321 );
11322 }
11323 let mut batch_completed = false;
11324 if batch_admitted && exact_end < n {
11325 let hs = self.hidden_size;
11326 let mut batch_pos = exact_end;
11327 while batch_pos < n {
11328 let end = (batch_pos + batch_k).min(n);
11329 let bk = end - batch_pos;
11330 let mut hiddens = vec![0.0f32; bk * hs];
11331 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
11332 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
11333 }
11334 let positions: Vec<usize> = (batch_pos..end).collect();
11335 let t_batch = std::time::Instant::now();
11336 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
11337 if std::env::var("CMF_GRAPH_PROF").is_ok() {
11338 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
11339 eprintln!(
11340 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
11341 batch_pos,
11342 end.saturating_sub(1),
11343 bk as f64 / (ms / 1000.0),
11344 );
11345 }
11346 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
11347 self.nll_end();
11348 return Err(err);
11349 }
11350 match outcome {
11351 crate::gpu::BatchGraphOutcome::Completed => {
11352 batch_completed = true;
11353 for row in 0..bk {
11354 nll += self.nll_from_hidden(
11355 &hiddens[row * hs..(row + 1) * hs],
11356 ids[batch_pos + row + 1],
11357 batch_pos + row,
11358 );
11359 cnt += 1;
11360 }
11361 batch_pos = end;
11362 }
11363 crate::gpu::BatchGraphOutcome::Declined => {
11364 if batch_completed {
11365 self.nll_end();
11366 return Err(format!(
11367 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
11368 ));
11369 }
11370 break;
11371 }
11372 crate::gpu::BatchGraphOutcome::Failed => {
11373 self.nll_end();
11374 return Err(format!(
11375 "O(1) NLL batch graph failed after admission at position {batch_pos}"
11376 ));
11377 }
11378 }
11379 }
11380 if batch_completed && cnt == n.saturating_sub(requested_start) {
11381 self.nll_end();
11382 return Ok((nll, cnt));
11383 }
11384 }
11385
11386 for pos in exact_end..n {
11391 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11392 if self
11393 .graph_failed
11394 .swap(false, std::sync::atomic::Ordering::Relaxed)
11395 {
11396 self.cancel
11397 .store(false, std::sync::atomic::Ordering::Relaxed);
11398 self.nll_end();
11399 return Err(format!(
11400 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
11401 ));
11402 }
11403 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
11404 cnt += 1;
11405 }
11406 self.nll_end();
11407 Ok((nll, cnt))
11408 }
11409
11410 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
11418 self.clear_sequence_state();
11419 let n = ids.len().saturating_sub(1);
11420 let mut correct = Vec::with_capacity(n);
11421 let mut pmax = Vec::with_capacity(n);
11422 for pos in 0..n {
11423 let emb = self.embed_single(ids[pos]);
11424 let hidden = self.forward_layers(&emb, pos, None);
11425 let logits = if let Some(logits) = self.graph_logits.take() {
11426 logits
11427 } else {
11428 let normed = inference::rms_norm(
11429 &hidden,
11430 &self.weights.final_norm,
11431 self.rms_eps,
11432 self.norm_style,
11433 );
11434 self.lm_head_forward(&normed)
11438 };
11439 let target = ids[pos + 1] as usize;
11440 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
11441 for (i, &v) in logits.iter().enumerate() {
11442 if v > mval {
11443 mval = v;
11444 amax = i;
11445 }
11446 }
11447 correct.push(amax == target);
11448 let row: Vec<f32> = temps
11449 .iter()
11450 .map(|&t| {
11451 let tt = t.max(1e-3);
11452 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
11453 1.0 / s.max(1e-12) })
11455 .collect();
11456 pmax.push(row);
11457 }
11458 self.clear_sequence_state();
11459 (correct, pmax)
11460 }
11461
11462 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
11469 if self.dyn_router.is_none() {
11470 return Ok((self.ppl_ids(ids)?, 0));
11471 }
11472 self.nll_begin()?;
11473 let saved_active = self.dyn_active;
11474 let mut router = self
11475 .dyn_router
11476 .take()
11477 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
11478 router.reset();
11479 self.dyn_phi_seen = 0;
11480 let _ = self.set_active_skill(None);
11481
11482 let result: Result<(f64, usize), String> = (|| {
11483 let mut nll = 0f64;
11484 let mut cnt = 0usize;
11485 for pos in 0..ids.len().saturating_sub(1) {
11486 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
11487 self.nll_check_graph("dynamic serial forward", pos)?;
11488 let out_of_band = self.graph_logits.take();
11489 let mut logits = match out_of_band {
11490 Some(lg) => lg,
11491 None => {
11492 let normed = inference::rms_norm(
11493 &hidden,
11494 &self.weights.final_norm,
11495 self.rms_eps,
11496 self.norm_style,
11497 );
11498 self.lm_head_forward(&normed)
11502 }
11503 };
11504 let target = ids[pos + 1] as usize;
11505 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
11506 let lse: f64 = logits
11507 .iter()
11508 .map(|&v| ((v - max) as f64).exp())
11509 .sum::<f64>()
11510 .ln()
11511 + max as f64;
11512 let tok_nll = lse - logits[target] as f64;
11513 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
11514 let top = logits
11515 .iter()
11516 .enumerate()
11517 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
11518 .map(|(i, _)| i)
11519 .unwrap_or(0);
11520 eprintln!(
11521 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
11522 logits[target], logits[top]
11523 );
11524 }
11525 nll += tok_nll;
11526 cnt += 1;
11527 attention::recycle_buf(&mut logits);
11528 let phi = self.dyn_phi_ema.clone();
11530 if let Some(new_active) = router.step(&phi, pos) {
11531 let _ = self.set_active_skill(new_active);
11532 }
11533 }
11534 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
11535 })();
11536
11537 let _ = self.set_active_skill(saved_active);
11540 self.dyn_router = Some(router);
11541 self.nll_end();
11542 result
11543 }
11544
11545 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
11547 self.clear_sequence_state();
11548 let mut acc = vec![0f32; self.hidden_size];
11549 for (pos, &id) in ids.iter().enumerate() {
11550 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11551 for (a, v) in acc.iter_mut().zip(&h) {
11552 *a += v;
11553 }
11554 }
11555 let n = ids.len().max(1) as f32;
11556 for a in acc.iter_mut() {
11557 *a /= n;
11558 }
11559 self.clear_sequence_state();
11560 acc
11561 }
11562
11563 pub fn probe_phi_span(
11577 &mut self,
11578 ids: &[u32],
11579 layer: usize,
11580 span: std::ops::Range<usize>,
11581 ) -> Vec<f32> {
11582 #[cfg(target_os = "macos")]
11583 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11584 let end = span.end.min(ids.len());
11585 let start = span.start.min(end);
11586 let reset = |p: &mut Self| p.clear_sequence_state();
11587 reset(self);
11588 let mut acc = vec![0f32; self.hidden_size];
11589 for (pos, &id) in ids[..end].iter().enumerate() {
11590 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
11591 if pos >= start {
11592 for (a, v) in acc.iter_mut().zip(&h) {
11593 *a += v;
11594 }
11595 }
11596 }
11597 let n = end - start;
11598 if n > 0 {
11599 let n = n as f32;
11600 for a in acc.iter_mut() {
11601 *a /= n;
11602 }
11603 }
11604 reset(self);
11605 acc
11606 }
11607
11608 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
11615 #[cfg(target_os = "macos")]
11616 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
11617 self.graph_logits = None;
11618 let hidden = self.forward_layers(&self.embed_single(token), position, None);
11619 if let Some(logits) = self.graph_logits.take() {
11620 return logits;
11621 }
11622 inference::rms_norm_into(
11623 &hidden,
11624 &self.weights.final_norm,
11625 self.rms_eps,
11626 self.norm_style,
11627 &mut self.ws.n1,
11628 );
11629 self.lm_head_forward(&self.ws.n1)
11630 }
11631
11632 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
11638 self.prefill_batch_masked(ids, start_pos, None)
11639 }
11640
11641 fn prefill_batch_masked(
11647 &mut self,
11648 ids: &[u32],
11649 start_pos: usize,
11650 task_mask: Option<&TaskMask>,
11651 ) -> Vec<f32> {
11652 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
11653 }
11654
11655 fn prefill_rows(
11663 &mut self,
11664 ids: &[u32],
11665 pos: usize,
11666 task_mask: Option<&TaskMask>,
11667 ) -> Result<Vec<f32>, String> {
11668 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
11669 }
11670
11671 fn prefill_input_rows(
11672 &mut self,
11673 input: PrefillIn<'_>,
11674 pos: usize,
11675 task_mask: Option<&TaskMask>,
11676 ) -> Result<Vec<f32>, String> {
11677 self.mimo_moe_prepare();
11678 let hs = self.hidden_size;
11679 let bk = match input {
11680 PrefillIn::Ids(ids) => ids.len(),
11681 PrefillIn::Hidden(rows) => rows.len() / hs,
11682 };
11683 #[cfg(not(target_os = "macos"))]
11684 if task_mask.is_none()
11685 && !self.o1_active()
11686 && bk > 1
11687 && (self.batch_prefix_prefill()
11688 || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
11691 || (self.verify_exact_moe
11692 && crate::gpu::enabled_here()
11693 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
11694 {
11695 let mut hiddens = match input {
11696 PrefillIn::Hidden(rows) => rows.to_vec(),
11697 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
11698 };
11699 let positions: Vec<usize> = (pos..pos + bk).collect();
11700 let mut run = 0usize;
11701 match self.try_batch_graph_wgpu_prefix(
11702 &mut hiddens,
11703 &positions,
11704 bk,
11705 None,
11706 Some(&mut run),
11707 ) {
11708 crate::gpu::BatchGraphOutcome::Completed => {
11709 let out = if run < self.num_layers {
11710 self.prefill_batch_span(
11711 PrefillIn::Hidden(&hiddens),
11712 pos,
11713 None,
11714 run,
11715 self.num_layers,
11716 )
11717 } else {
11718 hiddens
11719 };
11720 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11721 Err("MiMo attention graph failed after admission".into())
11722 } else {
11723 Ok(out)
11724 };
11725 }
11726 crate::gpu::BatchGraphOutcome::Failed => {
11727 return Err("batched prefix prefill failed after admission".into());
11728 }
11729 crate::gpu::BatchGraphOutcome::Declined => {
11730 #[cfg(feature = "gpu")]
11732 self.pull_lagging_host_kv(0, self.num_layers, pos);
11733 }
11734 }
11735 }
11736 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
11737 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
11738 Err("batch tail graph failed after admission".into())
11739 } else {
11740 Ok(out)
11741 }
11742 }
11743
11744 fn prefill_batch_span(
11750 &mut self,
11751 input: PrefillIn<'_>,
11752 start_pos: usize,
11753 task_mask: Option<&TaskMask>,
11754 from: usize,
11755 upto_excl: usize,
11756 ) -> Vec<f32> {
11757 let hs = self.hidden_size;
11758 let b = match input {
11759 PrefillIn::Ids(ids) => ids.len(),
11760 PrefillIn::Hidden(hb) => hb.len() / hs,
11761 };
11762 let upto_excl = upto_excl.min(self.num_layers);
11763 let mut h: Vec<f32>;
11767 let mut h_ready;
11768 match input {
11769 PrefillIn::Ids(_) => {
11770 h = vec![0.0; b * hs];
11771 h_ready = false;
11772 }
11773 PrefillIn::Hidden(hb) => {
11774 h = hb.to_vec();
11775 h_ready = true;
11776 }
11777 }
11778 let fill_h = |h: &mut Vec<f32>, me: &Self| {
11779 if let PrefillIn::Ids(ids) = input {
11780 for (bi, &id) in ids.iter().enumerate() {
11781 let e = me.embed_single(id);
11782 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
11783 }
11784 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11785 if let Ok(t) = tp.parse::<usize>() {
11786 if t >= start_pos && t < start_pos + ids.len() {
11787 let bi = t - start_pos;
11788 let row = &h[bi * hs..(bi + 1) * hs];
11789 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11790 eprintln!(
11791 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
11792 ids[bi],
11793 row[0],
11794 row[1],
11795 ids.len(),
11796 &ids[..ids.len().min(8)]
11797 );
11798 }
11799 }
11800 }
11801 }
11802 };
11803 let (_nkv, _hd, _rd, eps) = (
11804 self.num_kv_heads,
11805 self.head_dim,
11806 self.rotary_dim,
11807 self.rms_eps,
11808 );
11809 let pool = self.pool.clone();
11810 let norm_style = self.norm_style;
11811 self.mimo_moe_prepare();
11812 let automatic_gpu_prefix = self.automatic_gpu_prefix();
11813
11814 #[cfg(target_os = "macos")]
11815 let mut chunk_skip_until = 0usize;
11816 let mut mhc_rs: Vec<f32> = Vec::new();
11818 for li in from..upto_excl {
11819 let _capacity_tail = automatic_gpu_prefix
11820 .filter(|&prefix| {
11821 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
11822 })
11823 .map(|_| crate::gpu::enter_cpu_scope());
11824 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
11829 let _fast_gemm = self
11830 .proj_gate_sigmoid
11831 .then(crate::gpu::enter_prefill_fast_gemm);
11832 #[cfg(target_os = "macos")]
11838 if task_mask.is_none() {
11839 if li < chunk_skip_until {
11840 continue;
11841 }
11842 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
11848 fill_h(&mut h, self);
11849 h_ready = true;
11850 }
11851 let ids_for_embed = match input {
11852 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
11853 PrefillIn::Hidden(_) => None,
11854 };
11855 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
11856 if end > li {
11857 h_ready = true;
11858 chunk_skip_until = end;
11859 if self.is_loop_end(end - 1) && end < self.num_layers {
11862 for bi in 0..b {
11863 let normed = inference::rms_norm(
11864 &h[bi * hs..(bi + 1) * hs],
11865 &self.weights.final_norm,
11866 eps,
11867 norm_style,
11868 );
11869 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11870 }
11871 }
11872 continue;
11873 }
11874 }
11875 if !h_ready {
11876 fill_h(&mut h, self);
11877 h_ready = true;
11878 }
11879 if task_mask.is_none() && self.verify_exact_moe {
11880 let positions: Vec<_> = (start_pos..start_pos + b).collect();
11881 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
11882 crate::gpu::BatchGraphOutcome::Completed => continue,
11883 crate::gpu::BatchGraphOutcome::Failed => return h,
11884 crate::gpu::BatchGraphOutcome::Declined => {}
11885 }
11886 }
11887 #[cfg(feature = "gpu")]
11888 self.pull_lagging_host_kv(li, li + 1, start_pos);
11889 let mhc_attn = self.mhc_layer(li).then(|| {
11893 let m = self.mhc.as_deref().expect("mhc layer");
11894 let p = self.phys_layer(li);
11895 let n = m.s.streams * hs;
11896 mhc_rs.resize(b * n, 0.0);
11897 let coefs: Vec<_> = (0..b)
11898 .map(|bi| {
11899 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
11900 if m.enters(p) {
11901 m.enter(r, &h[bi * hs..(bi + 1) * hs]);
11902 }
11903 m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
11904 })
11905 .collect();
11906 (h.clone(), coefs)
11907 });
11908 let lw = &self.weights.layers[self.phys_layer(li)];
11909 let t_attn = std::time::Instant::now();
11910 match &lw.attn {
11912 AttnKind::Kda(w) => {
11913 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
11915 let mut normed = vec![0.0f32; b * hs];
11916 for bi in 0..b {
11917 inference::rms_norm_into(
11918 &h[bi * hs..(bi + 1) * hs],
11919 &lw.input_norm,
11920 eps,
11921 norm_style,
11922 &mut normed[bi * hs..(bi + 1) * hs],
11923 );
11924 }
11925 let attn = crate::linear_core::kda_forward_batch(
11926 &normed,
11927 b,
11928 w,
11929 &cfg,
11930 &mut self.kv_cache.layers[li].linear_state,
11931 pool.as_deref(),
11932 );
11933 for (dst, &a) in h.iter_mut().zip(&attn) {
11934 *dst += a;
11935 }
11936 }
11937 AttnKind::LinearGdn(w) => {
11938 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
11940 let mut normed = vec![0.0f32; b * hs];
11941 for bi in 0..b {
11942 let r = inference::rms_norm(
11943 &h[bi * hs..(bi + 1) * hs],
11944 &lw.input_norm,
11945 eps,
11946 norm_style,
11947 );
11948 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11949 }
11950 let attn = crate::linear_core::gdn_forward_batch(
11951 &normed,
11952 b,
11953 w,
11954 &cfg,
11955 &mut self.kv_cache.layers[li].linear_state,
11956 pool.as_deref(),
11957 );
11958 for (dst, &a) in h.iter_mut().zip(&attn) {
11959 *dst += a;
11960 }
11961 }
11962 AttnKind::ShortConv(w) => {
11963 let cfg = self
11966 .short_conv_cfg
11967 .expect("short-conv layer without short_conv_cfg");
11968 let mut normed = vec![0.0f32; b * hs];
11969 for bi in 0..b {
11970 inference::rms_norm_into(
11971 &h[bi * hs..(bi + 1) * hs],
11972 &lw.input_norm,
11973 eps,
11974 norm_style,
11975 &mut normed[bi * hs..(bi + 1) * hs],
11976 );
11977 }
11978 let mut attn = short_conv_forward_batch(
11979 &normed,
11980 b,
11981 w,
11982 &cfg,
11983 &mut self.kv_cache.layers[li].linear_state,
11984 pool.as_deref(),
11985 );
11986 if let Some(wn) = &lw.attn_out_norm {
11989 for bi in 0..b {
11990 inference::rms_norm_into(
11991 &attn[bi * hs..(bi + 1) * hs],
11992 wn,
11993 eps,
11994 norm_style,
11995 &mut normed[bi * hs..(bi + 1) * hs],
11996 );
11997 }
11998 attn.copy_from_slice(&normed);
11999 }
12000 for (dst, &a) in h.iter_mut().zip(&attn) {
12001 *dst += a;
12002 }
12003 }
12004 AttnKind::Mla(w) => {
12005 let inv_freq_l = self.layer_inv_freq(li);
12008 let rs = self.layer_rope_scale(li);
12009 let mut normed = vec![0.0f32; hs];
12010 for bi in 0..b {
12011 inference::rms_norm_into(
12012 &h[bi * hs..(bi + 1) * hs],
12013 &lw.input_norm,
12014 eps,
12015 norm_style,
12016 &mut normed,
12017 );
12018 let ao = mla_attention(
12019 w,
12020 &normed,
12021 &mut self.kv_cache.layers[li],
12022 start_pos + bi,
12023 &inv_freq_l,
12024 rs,
12025 eps,
12026 pool.as_deref(),
12027 );
12028 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
12029 *dst += a;
12030 }
12031 }
12032 }
12033 AttnKind::Full {
12034 wq,
12035 wk,
12036 wv,
12037 wo,
12038 q_norm,
12039 k_norm,
12040 output_gate,
12041 softplus_gate,
12042 bias,
12043 } => {
12044 let mut normed = vec![0.0f32; b * hs];
12048 for bi in 0..b {
12049 inference::rms_norm_into(
12050 &h[bi * hs..(bi + 1) * hs],
12051 &lw.input_norm,
12052 eps,
12053 norm_style,
12054 &mut normed[bi * hs..(bi + 1) * hs],
12055 );
12056 }
12057 let inv_freq_l = self.layer_inv_freq(li);
12058 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12059 let cfg = QwenAttnCfg {
12060 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
12061 num_heads: self.layer_num_heads(li),
12062 num_kv_heads: nkv_l,
12063 head_dim: hd_l,
12064 hidden_size: hs,
12065 position: start_pos,
12066 inv_freq: &inv_freq_l,
12067 rotary_dim: rd_l,
12068 scale: self.attn_scale,
12069 softcap: self.attn_softcap,
12070 window: self.layer_window(li),
12071 v_norm: self.attn_v_norm,
12072 qk_norm_after_rope: self.qk_norm_after_rope,
12073 gate_sigmoid: self.proj_gate_sigmoid,
12074 q_norm: q_norm.as_deref(),
12075 k_norm: k_norm.as_deref(),
12076 output_gate: *output_gate,
12077 softplus_gate: softplus_gate
12078 .as_ref()
12079 .map(|(gate, per_head)| (gate, *per_head)),
12080 rope_scale: self.layer_rope_scale(li),
12081 bias: bias
12082 .as_ref()
12083 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12084 rms_eps: eps,
12085 norm_style,
12086 pool: pool.as_deref(),
12087 v_head_dim: self.layer_v_dim(li),
12088 };
12089 #[cfg(feature = "gpu")]
12090 let _mirror = self
12091 .prefill_mirror_target(li)
12092 .map(crate::gpu::enter_prefill_mirror);
12093 let mut attn = attention::qwen_attention_batch(
12094 &normed,
12095 b,
12096 wq,
12097 wk,
12098 wv,
12099 wo,
12100 &mut self.kv_cache.layers[li],
12101 &cfg,
12102 );
12103 if let Some(w) = &lw.attn_out_norm {
12104 for bi in 0..b {
12105 inference::rms_norm_into(
12106 &attn[bi * hs..(bi + 1) * hs],
12107 w,
12108 eps,
12109 norm_style,
12110 &mut normed[bi * hs..(bi + 1) * hs],
12111 );
12112 }
12113 attn.copy_from_slice(&normed);
12114 }
12115 for (dst, &a) in h.iter_mut().zip(&attn) {
12116 *dst += a;
12117 }
12118 }
12119 AttnKind::Bounded(w) => {
12120 let mut normed = vec![0.0f32; b * hs];
12123 for bi in 0..b {
12124 inference::rms_norm_into(
12125 &h[bi * hs..(bi + 1) * hs],
12126 &lw.input_norm,
12127 eps,
12128 norm_style,
12129 &mut normed[bi * hs..(bi + 1) * hs],
12130 );
12131 }
12132 let rope = self
12133 .bounded_rope
12134 .clone()
12135 .expect("bounded layer without an installed rotation table");
12136 let cfg = crate::bounded::BoundedAttnCfg {
12137 num_heads: self.num_heads,
12138 num_kv_heads: self.num_kv_heads,
12139 head_dim: self.head_dim,
12140 hidden_size: hs,
12141 scale: self.attn_scale,
12142 rope: &rope,
12143 pool: pool.as_deref(),
12144 };
12145 let mut attn = crate::bounded::bounded_attention_batch(
12146 &normed,
12147 b,
12148 w,
12149 &mut self.kv_cache.layers[li],
12150 &cfg,
12151 );
12152 if let Some(wn) = &lw.attn_out_norm {
12153 for bi in 0..b {
12154 inference::rms_norm_into(
12155 &attn[bi * hs..(bi + 1) * hs],
12156 wn,
12157 eps,
12158 norm_style,
12159 &mut normed[bi * hs..(bi + 1) * hs],
12160 );
12161 }
12162 attn.copy_from_slice(&normed);
12163 }
12164 for (dst, &a) in h.iter_mut().zip(&attn) {
12165 *dst += a;
12166 }
12167 attention::recycle_buf(&mut attn);
12168 }
12169 AttnKind::Linear(w) => {
12170 for bi in 0..b {
12171 let normed = inference::rms_norm(
12172 &h[bi * hs..(bi + 1) * hs],
12173 &lw.input_norm,
12174 eps,
12175 norm_style,
12176 );
12177 vmf_phase_forward(
12178 &normed,
12179 w,
12180 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
12181 &mut self.kv_cache.layers[li].linear_state,
12182 pool.as_deref(),
12183 )
12184 .iter()
12185 .enumerate()
12186 .for_each(|(i, &a)| h[bi * hs + i] += a);
12187 }
12188 }
12189 }
12190
12191 let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
12194 let m = self.mhc.as_deref().expect("mhc layer");
12195 let p = self.phys_layer(li);
12196 let n = m.s.streams * hs;
12197 let mut y = vec![0f32; hs];
12198 (0..b)
12199 .map(|bi| {
12200 for (e, v) in y.iter_mut().enumerate() {
12201 *v = h[bi * hs + e] - branch[bi * hs + e];
12202 }
12203 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12204 m.merge(r, &y, &coefs[bi]);
12205 m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
12206 })
12207 .collect::<Vec<_>>()
12208 });
12209 let lw = &self.weights.layers[self.phys_layer(li)];
12211 let mut post = vec![0.0f32; b * hs];
12212 for bi in 0..b {
12213 let r =
12214 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
12215 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12216 }
12217 let mask_row = task_mask
12220 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
12221 .and_then(|m| m.ffn_masks.get(li))
12222 .map(|v| v.as_slice());
12223 let attn_ns = t_attn.elapsed().as_nanos() as u64;
12224 let t_ffn = std::time::Instant::now();
12225 let mut ffn = match &lw.ffn {
12226 FfnKind::Dense(d) if !d.segs.is_empty() => {
12227 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
12228 }
12229 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
12230 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
12231 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
12232 }
12233 FfnKind::Moe(m) if self.verify_exact_moe => {
12234 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
12235 }
12236 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
12239 let before = m.stats.borrow().clone();
12240 let out = crate::gpu::cpu_scope(|| {
12241 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
12242 });
12243 self.mimo_moe.prime(li, m, &before);
12244 out
12245 }
12246 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
12247 FfnKind::DenseMoe(dm) => {
12250 let mut out = vec![0.0f32; b * hs];
12251 for bi in 0..b {
12252 let r = dense_moe_ffn(
12253 dm,
12254 &post[bi * hs..(bi + 1) * hs],
12255 &h[bi * hs..(bi + 1) * hs],
12256 eps,
12257 norm_style,
12258 pool.as_deref(),
12259 );
12260 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
12261 }
12262 out
12263 }
12264 };
12265 if prefill_prof_on() {
12266 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
12267 PREFILL_SPLIT[1].fetch_add(
12268 t_ffn.elapsed().as_nanos() as u64,
12269 std::sync::atomic::Ordering::Relaxed,
12270 );
12271 }
12272 if let Some(w) = &lw.ffn_out_norm {
12273 for bi in 0..b {
12274 inference::rms_norm_into(
12275 &ffn[bi * hs..(bi + 1) * hs],
12276 w,
12277 eps,
12278 norm_style,
12279 &mut post[bi * hs..(bi + 1) * hs],
12280 );
12281 }
12282 ffn.copy_from_slice(&post);
12283 }
12284 if let Some(coefs) = mhc_ffn {
12285 let m = self.mhc.as_deref().expect("mhc layer");
12288 let p = self.phys_layer(li);
12289 let n = m.s.streams * hs;
12290 for bi in 0..b {
12291 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
12292 m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
12293 if m.leaves(p) {
12294 m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
12295 }
12296 }
12297 } else {
12298 for (dst, &f) in h.iter_mut().zip(&ffn) {
12299 *dst += f;
12300 }
12301 }
12302 if let Some(sc) = lw.layer_scale {
12303 for v in h.iter_mut() {
12304 *v *= sc;
12305 }
12306 }
12307 if self.layer_dump.is_some() {
12309 for bi in 0..b {
12310 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
12311 }
12312 }
12313 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
12314 if let Ok(t) = tp.parse::<usize>() {
12315 if t >= start_pos && t < start_pos + b {
12316 let bi = t - start_pos;
12317 let row = &h[bi * hs..(bi + 1) * hs];
12318 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
12319 eprintln!(
12320 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
12321 row[0], row[1]
12322 );
12323 }
12324 }
12325 }
12326 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
12330 let row = &h[(b - 1) * hs..b * hs];
12331 let rms =
12332 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
12333 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
12334 eprintln!(
12335 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
12336 match &self.weights.layers[self.phys_layer(li)].attn {
12337 AttnKind::LinearGdn(_) => "gdn",
12338 AttnKind::Linear(_) => "vmf",
12339 AttnKind::ShortConv(_) => "conv",
12340 _ => "attn",
12341 },
12342 match &lw.ffn {
12343 FfnKind::Moe(_) => "moe",
12344 FfnKind::Dense(_) => "dense",
12345 FfnKind::DenseMoe(_) => "dense+moe",
12346 },
12347 );
12348 }
12349 if self.is_loop_end(li) && li + 1 < self.num_layers {
12351 for bi in 0..b {
12352 let normed = inference::rms_norm(
12353 &h[bi * hs..(bi + 1) * hs],
12354 &self.weights.final_norm,
12355 eps,
12356 norm_style,
12357 );
12358 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
12359 }
12360 }
12361 if std::env::var("CMF_TRACE_H").is_ok() {
12362 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
12363 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
12364 eprintln!(
12365 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
12366 lw.layer_scale
12367 );
12368 }
12369 }
12370 crate::gpu::set_layer(-1); if prefill_prof_on() {
12372 let ms = |a: &std::sync::atomic::AtomicU64| {
12373 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
12374 };
12375 let sp = &attention::ATTN_SPLIT;
12376 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
12377 eprintln!(
12378 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
12379 host gate {:.1}], host loop {:.1}, attend {:.1} \
12380 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
12381 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
12382 ms(&PREFILL_SPLIT[0]),
12383 ms(&sp[0]),
12384 qkv_enq,
12385 qkv_wait,
12386 ms(&sp[6]),
12387 ms(&sp[1]),
12388 ms(&sp[2]),
12389 ms(&sp[4]),
12390 ms(&sp[5]),
12391 ca_wait,
12392 ms(&sp[3]),
12393 ms(&PREFILL_SPLIT[1]),
12394 );
12395 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
12396 eprintln!(
12397 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
12398 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
12399 fallbacks (cumulative)",
12400 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
12401 );
12402 }
12403 self.o1_progress();
12408 self.swa_trim_tails();
12410 h
12411 }
12412
12413 fn embed_single(&self, id: u32) -> Vec<f32> {
12415 let mut out = vec![0.0f32; self.hidden_size];
12416 if (id as usize) < self.weights.embed_tokens.rows() {
12417 self.weights.embed_tokens.row_f32(id as usize, &mut out);
12418 }
12419 if self.embed_multiplier != 1.0 {
12420 for v in out.iter_mut() {
12421 *v *= self.embed_multiplier;
12422 }
12423 }
12424 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
12428 let mut v = vec![0.0f32; self.hidden_size.max(1)];
12429 v[0] = id as f32;
12430 return v;
12431 }
12432 if let Some(b) = &self.g4e {
12436 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12437 }
12438 if let Some(b) = &self.g3n {
12439 return b.0.extend_embedding(id, &out, self.pool.as_deref());
12440 }
12441 out
12442 }
12443
12444 #[cfg(target_os = "macos")]
12450 fn chunk_run_gpu(
12451 &mut self,
12452 li0: usize,
12453 h: &mut [f32],
12454 b: usize,
12455 pos0: usize,
12456 embed_ids: Option<&[u32]>,
12457 cap: usize,
12458 ) -> usize {
12459 let gemma4 =
12466 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
12467 if !crate::gpu::enabled_here()
12468 || std::env::var("CMF_GPU_CHUNK")
12469 .map(|v| v == "0")
12470 .unwrap_or(false)
12471 || b < self.chunk_min_rows
12472 || self.prompt_mrope.is_some()
12474 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
12477 || (self.global_attn.is_some() && !gemma4)
12478 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
12482 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
12485 || self.o1_active()
12488 || (self.attn_v_norm && !gemma4)
12489 || !self.metal_rope_scale_ok()
12490 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
12491 {
12492 return li0;
12493 }
12494 let Some(model) = self.model.clone() else {
12495 return li0;
12496 };
12497 let (nh, nkv, hd, hs) = (
12498 self.num_heads,
12499 self.num_kv_heads,
12500 self.head_dim,
12501 self.hidden_size,
12502 );
12503 let loop_end = if self.loop_final_norm {
12507 ((li0 / self.physical_layers) + 1) * self.physical_layers
12508 } else {
12509 self.num_layers
12510 };
12511 let run_end = self.num_layers.min(loop_end).min(cap);
12512 let tables: Vec<std::sync::Arc<Vec<f32>>> =
12517 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
12518 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
12519 let mut stored_at: Vec<usize> = Vec::new();
12520 for li in li0..run_end {
12521 if self.mhc_layer(li) {
12522 break;
12523 }
12524 let lw = &self.weights.layers[self.phys_layer(li)];
12525 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
12526 && !gemma4
12527 {
12528 break;
12529 }
12530 let AttnKind::Full {
12531 wq,
12532 wk,
12533 wv,
12534 wo,
12535 q_norm,
12536 k_norm,
12537 output_gate: false,
12538 softplus_gate,
12539 bias,
12540 } = &lw.attn
12541 else {
12542 break;
12543 };
12544 let head_gate = match softplus_gate {
12546 None => None,
12547 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
12548 Some((d, r, c)) if r == nh && c == hs => Some(d),
12549 _ => break,
12550 },
12551 Some(_) => break,
12552 };
12553 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
12557 FfnKind::Dense(d)
12558 if (matches!(d.act, Act::Silu | Act::Gelu)
12559 || (gemma4 && d.act == Act::GeluTanh))
12560 && d.segs.is_empty() =>
12561 {
12562 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
12563 }
12564 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
12565 Some((router, experts, q8)) => {
12566 let e0 = &m.experts[0];
12567 let route = Box::new(move |lg: &[f32]| {
12568 let (idx, p, wsum) = moe_route(lg, m, None);
12570 let mut st = m.stats.borrow_mut();
12571 if st.len() < m.experts.len() {
12572 st.resize(m.experts.len(), 0);
12573 }
12574 for &e in &idx {
12575 st[e] += 1;
12576 }
12577 let w = idx.iter().map(|&e| p[e] / wsum).collect();
12578 (idx, w)
12579 });
12580 (
12581 &e0.gate_proj,
12582 &e0.up_proj,
12583 &e0.down_proj,
12584 Act::Silu,
12585 Some(crate::gpu_metal::ChunkMoe {
12586 router,
12587 experts,
12588 q8,
12589 route,
12590 }),
12591 )
12592 }
12593 None => break,
12594 },
12595 _ => break,
12596 };
12597 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
12603 t.q8_row_parts()
12604 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
12605 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12606 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
12607 }
12608 fn cf(t: &QTensor) -> &[f32] {
12609 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
12610 }
12611 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
12612 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
12613 if moe.is_none()
12623 && !gemma4
12624 && ffn_act != Act::Gelu
12625 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
12626 {
12627 break;
12628 }
12629 let parts = (
12630 cw(wq),
12631 cw(wk),
12632 cw(wv),
12633 cw(wo),
12634 cw(gate_t),
12635 cw(up_t),
12636 cw(down_t),
12637 );
12638 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
12639 else {
12640 break;
12641 };
12642 let layer = &self.kv_cache.layers[li];
12643 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
12644 break;
12645 }
12646 debug_assert!(
12648 layer.base() == 0
12649 || self
12650 .layer_window(li)
12651 .is_some_and(|w| layer.head_len(0) + 1 >= w)
12652 );
12653 stored_at.push(layer.head_len(0));
12654 let (nkv, hd) = if gemma4 {
12657 let (a, bb, _) = self.layer_geom(li);
12658 (a, bb)
12659 } else {
12660 (nkv, hd)
12661 };
12662 layers.push(crate::gpu_metal::ChunkLayer {
12663 model: &model,
12664 kv_id: self.graph_kv_id,
12665 layer: li,
12666 wq: pq,
12667 wk: pk,
12668 wv: pv,
12669 wo: po,
12670 gate: pg,
12671 up: pu,
12672 down: pd,
12673 input_norm: &lw.input_norm,
12674 post_norm: &lw.post_norm,
12675 bias: bias
12676 .as_ref()
12677 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
12678 q_norm: q_norm.as_deref(),
12679 k_norm: k_norm.as_deref(),
12680 inv_freq: &tables[li - li0],
12681 rope_scale: self.layer_rope_scale(li),
12682 rd: self.layer_geom(li).2,
12683 nh,
12684 nkv,
12685 hd,
12686 hs,
12687 inter: gate_t.rows(),
12688 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
12689 late_qk_norm: self.qk_norm_after_rope,
12690 eps: self.rms_eps as f32,
12691 window: self.layer_window(li),
12692 head_gate,
12693 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
12694 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
12695 v_norm: self.attn_v_norm,
12696 scale: self.attn_scale,
12697 attn_out_norm: lw.attn_out_norm.as_deref(),
12698 ffn_out_norm: lw.ffn_out_norm.as_deref(),
12699 layer_scale: lw.layer_scale,
12700 }),
12701 col,
12702 ffn_col: if moe.is_none() && !gemma4 {
12705 ffn_col
12706 } else {
12707 [&[][..]; 3]
12708 },
12709 moe,
12710 });
12711 }
12712 if layers.is_empty() {
12713 return li0;
12714 }
12715 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
12717 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
12718 .iter()
12719 .zip(&rows_kv)
12720 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
12721 .collect();
12722 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
12723 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
12724 let li = layers[i].layer;
12725 let layer = &self.kv_cache.layers[li];
12726 let nkv = rows_kv[i].0;
12727 io.push(crate::gpu_metal::ChunkIo {
12728 cpu_stored: stored_at[i],
12729 cpu_gen: layer.generation(),
12730 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
12731 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
12732 out_k: ok,
12733 out_v: ov,
12734 imp: oi,
12735 });
12736 }
12737 let n_run = layers.len();
12738 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
12739 let ep = embed_ids.and_then(|ids| {
12742 self.weights
12743 .embed_tokens
12744 .q8_row_parts()
12745 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
12746 idx,
12747 rows,
12748 row_scale: rs,
12749 ids,
12750 mult: self.embed_multiplier,
12751 })
12752 });
12753 if embed_ids.is_some() && ep.is_none() {
12754 return li0;
12755 }
12756 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
12757 return li0;
12758 }
12759 drop(io);
12760 drop(layers);
12761 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
12764 let li = li0 + i;
12765 let row = rows_kv[i].1;
12766 let layer = &mut self.kv_cache.layers[li];
12767 for bi in 0..b {
12768 layer.append(
12769 &ok[bi * row..(bi + 1) * row],
12770 &ov[bi * row..(bi + 1) * row],
12771 &[],
12772 );
12773 }
12774 layer.accumulate_imp(oi);
12775 }
12776 last
12777 }
12778
12779 fn layer_is_local(&self, li: usize) -> bool {
12782 if let Some(layers) = &self.sliding_layers {
12783 return layers.get(li).copied().unwrap_or(false);
12784 }
12785 match self.swa {
12786 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
12787 None => false,
12788 }
12789 }
12790
12791 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
12794 if self.layer_is_local(li) {
12795 if let Some(f) = &self.inv_freq_local {
12796 return f.clone();
12797 }
12798 } else if let Some(f) = &self.inv_freq_global {
12799 return f.clone();
12800 }
12801 self.inv_freq.clone()
12802 }
12803
12804 fn layer_window(&self, li: usize) -> Option<usize> {
12806 self.swa
12807 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
12808 }
12809
12810 fn swa_trim_tails(&mut self) {
12819 let Some((slack, align)) = self.swa_trim else {
12820 return;
12821 };
12822 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
12823 return;
12824 }
12825 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
12826 if let Some(w) = self.layer_window(li) {
12827 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
12828 }
12829 }
12830 }
12831
12832 fn layer_num_heads(&self, li: usize) -> usize {
12833 self.attention_heads_per_layer
12834 .as_ref()
12835 .and_then(|v| v.get(li).copied())
12836 .unwrap_or(self.num_heads)
12837 }
12838
12839 fn layer_rope_scale(&self, li: usize) -> f32 {
12840 if self.layer_is_local(li) {
12841 self.rope_scale_local
12842 } else {
12843 self.rope_scale
12844 }
12845 }
12846
12847 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
12850 if !self.layer_is_local(li) {
12851 if let Some((ghd, gkv)) = self.global_attn {
12852 return (gkv, ghd, ghd);
12853 }
12854 }
12855 let rd = if self.layer_is_local(li) {
12856 self.rotary_dim_local.unwrap_or(self.rotary_dim)
12857 } else {
12858 self.rotary_dim
12859 };
12860 let rd = if self
12862 .no_rope_layers
12863 .as_ref()
12864 .and_then(|v| v.get(li))
12865 .copied()
12866 == Some(true)
12867 {
12868 0
12869 } else {
12870 rd
12871 };
12872 (self.layer_num_kv_heads(li), self.head_dim, rd)
12873 }
12874
12875 fn layer_num_kv_heads(&self, li: usize) -> usize {
12878 self.kv_heads_per_layer
12879 .as_ref()
12880 .and_then(|v| v.get(self.phys_layer(li)).copied())
12881 .unwrap_or(self.num_kv_heads)
12882 }
12883
12884 fn layer_v_dim(&self, li: usize) -> usize {
12886 let (_, hd, _) = self.layer_geom(li);
12887 self.v_head_dim.unwrap_or(hd).min(hd)
12888 }
12889
12890 pub fn set_attn_geometry(
12899 &mut self,
12900 kv_heads_per_layer: Option<Vec<usize>>,
12901 v_head_dim: Option<usize>,
12902 ) -> Result<(), String> {
12903 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
12904 if self.global_attn.is_some() {
12905 return Err(
12906 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
12907 attention geometry"
12908 .into(),
12909 );
12910 }
12911 if self
12912 .weights
12913 .layers
12914 .iter()
12915 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
12916 {
12917 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
12918 }
12919 }
12920 if let Some(vd) = v_head_dim {
12921 if vd == 0 || vd > self.head_dim {
12922 return Err(format!(
12923 "v_head_dim {vd} must be in 1..={} (head_dim)",
12924 self.head_dim
12925 ));
12926 }
12927 }
12928 if let Some(v) = &kv_heads_per_layer {
12929 if v.len() != self.physical_layers {
12930 return Err(format!(
12931 "kv_heads_per_layer has {} entries, expected {} layers",
12932 v.len(),
12933 self.physical_layers
12934 ));
12935 }
12936 for (li, &nkv) in v.iter().enumerate() {
12937 let is_attn = matches!(
12938 self.weights.layers.get(li).map(|lw| &lw.attn),
12939 Some(AttnKind::Full { .. }) | None
12940 );
12941 if !is_attn {
12942 continue;
12943 }
12944 let nh = self
12945 .attention_heads_per_layer
12946 .as_ref()
12947 .and_then(|h| h.get(li).copied())
12948 .unwrap_or(self.num_heads);
12949 if nkv == 0 || nh % nkv != 0 {
12950 return Err(format!(
12951 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
12952 ));
12953 }
12954 }
12955 }
12956 self.kv_heads_per_layer = kv_heads_per_layer;
12957 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
12958 if self.kv_heads_per_layer.is_some() {
12959 for li in 0..self.kv_cache.layers.len() {
12960 let full = matches!(
12961 self.weights
12962 .layers
12963 .get(self.phys_layer(li))
12964 .map(|lw| &lw.attn),
12965 Some(AttnKind::Full { .. })
12966 );
12967 let nkv = self.layer_num_kv_heads(li);
12968 let cache = &self.kv_cache.layers[li];
12969 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
12970 let sinks = cache.sinks.clone();
12971 self.kv_cache.layers[li] =
12972 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
12973 self.kv_cache.layers[li].sinks = sinks;
12974 }
12975 }
12976 }
12977 Ok(())
12978 }
12979
12980 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
12984 let Some(lw) = self.weights.layers.get(phys) else {
12985 return Err(format!("sinks for layer {phys}: no such layer"));
12986 };
12987 if !matches!(lw.attn, AttnKind::Full { .. }) {
12988 return Err(format!(
12989 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
12990 ));
12991 }
12992 let nh = self
12993 .attention_heads_per_layer
12994 .as_ref()
12995 .and_then(|h| h.get(phys).copied())
12996 .unwrap_or(self.num_heads);
12997 if sinks.len() != nh {
12998 return Err(format!(
12999 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
13000 sinks.len()
13001 ));
13002 }
13003 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
13004 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
13005 }
13006 for li in 0..self.kv_cache.layers.len() {
13007 if self.phys_layer(li) == phys {
13008 self.kv_cache.layers[li].sinks = Some(sinks.clone());
13009 }
13010 }
13011 Ok(())
13012 }
13013
13014 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
13024 if self.kv_heads_per_layer.is_some() {
13025 return Some("per-layer KV head counts");
13026 }
13027 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
13028 return Some("V heads narrower than Q/K heads");
13029 }
13030 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
13031 return Some("learned attention sinks");
13032 }
13033 if self.swa.is_some() || self.sliding_layers.is_some() {
13034 return Some("sliding-window layers");
13035 }
13036 None
13037 }
13038
13039 #[cfg(target_os = "macos")]
13047 fn metal_rope_scale_ok(&self) -> bool {
13048 !(self.qk_norm_after_rope
13049 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
13050 }
13051
13052 #[cfg(target_os = "macos")]
13069 fn metal_graph_swa(&self) -> bool {
13070 (self.swa.is_some() || self.sliding_layers.is_some())
13071 && self.attention_heads_per_layer.is_none()
13072 && !self.attn_v_norm
13073 && self.attn_softcap == 0.0
13074 && self.kv_heads_per_layer.is_none()
13075 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
13076 && self.global_attn.is_none()
13077 && self.inv_freq_global.is_none()
13078 && self.metal_rope_scale_ok()
13079 && !(0..self.num_layers).any(|li| {
13080 self.layer_is_local(li)
13081 && self.inv_freq_local.is_none()
13082 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13083 })
13084 }
13085
13086 #[cfg(target_os = "macos")]
13094 fn metal_gemma4(&self) -> bool {
13095 crate::gpu_metal::gemma4::enabled()
13096 && (self.global_attn.is_some() || self.attn_v_norm)
13097 && self.attention_heads_per_layer.is_none()
13098 && self.kv_heads_per_layer.is_none()
13099 && self.v_head_dim.is_none()
13100 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
13101 && self.attn_softcap == 0.0
13102 && !self.qk_norm_after_rope
13103 && self.metal_rope_scale_ok()
13104 && self
13105 .weights
13106 .layers
13107 .iter()
13108 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
13109 && !(0..self.num_layers).any(|li| {
13110 self.layer_is_local(li)
13111 && self.inv_freq_local.is_none()
13112 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13113 })
13114 }
13115
13116 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
13123 let geometry_reason = self.graph_attn_decline_reason();
13127 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
13128 if has_scaled_rope && geometry_reason.is_none() {
13129 return Some("scaled RoPE without per-layer geometry");
13130 }
13131 if has_scaled_rope && self.qk_norm_after_rope {
13136 return Some("scaled RoPE with post-RoPE Q/K norm");
13137 }
13138 geometry_reason?;
13139 if self.attention_heads_per_layer.is_some() {
13145 return Some("per-layer Q head counts with per-layer geometry");
13146 }
13147 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
13152 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
13153 if !gemma4
13154 && self.weights.layers.iter().any(|lw| {
13155 lw.layer_scale.is_some()
13156 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
13157 })
13158 {
13159 return Some("sandwich norms / layer scale with per-layer geometry");
13160 }
13161 if self.weights.layers.iter().any(|lw| {
13165 matches!(
13166 &lw.attn,
13167 AttnKind::Full {
13168 output_gate: true,
13169 ..
13170 }
13171 )
13172 }) && self.v_head_dim.is_some()
13173 {
13174 return Some("gated attention with V narrower than K");
13175 }
13176 if (0..self.num_layers).any(|li| {
13177 self.layer_is_local(li)
13178 && self.inv_freq_local.is_none()
13179 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
13180 }) {
13181 return Some("local rotary width without a local RoPE table");
13182 }
13183 None
13184 }
13185
13186 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
13190 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
13193 return None;
13194 }
13195 if self.global_attn.is_some() {
13196 return Some("per-layer head width (Gemma-4 global layers)");
13197 }
13198 if self.attn_v_norm {
13199 return Some("V norm");
13200 }
13201 if self.weights.layers.iter().any(|lw| {
13202 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
13203 }) {
13204 return Some("sandwich norms / layer scale");
13205 }
13206 None
13207 }
13208
13209 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
13214 self.graph_attn_decline_reason()?;
13215 let (nkv, hd, rd) = self.layer_geom(li);
13216 let invf: &[f32] = if self.layer_is_local(li) {
13217 match &self.inv_freq_local {
13218 Some(f) => f.as_slice(),
13219 None => self.inv_freq.as_slice(),
13220 }
13221 } else {
13222 match &self.inv_freq_global {
13223 Some(f) => f.as_slice(),
13224 None => self.inv_freq.as_slice(),
13225 }
13226 };
13227 Some(crate::gpu::GraphAttnGeom {
13228 nkv,
13229 hd,
13230 dv: self.layer_v_dim(li),
13231 rd,
13232 invf,
13233 rope_scale: self.layer_rope_scale(li),
13234 window: self.layer_window(li),
13235 sink: self.kv_cache.layers[li].sinks.as_deref(),
13236 v_norm: self.attn_v_norm,
13237 })
13238 }
13239
13240 #[cfg(feature = "gpu")]
13252 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
13253 if !self.proj_gate_sigmoid
13254 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
13255 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
13256 || self.graph_refused()
13257 || self.o1_active()
13258 || self.attn_softcap > 0.0
13259 || self.wgpu_graph_attn_decline().is_some()
13260 {
13261 return None;
13262 }
13263 let g = self.graph_attn_geom(li)?;
13264 let (nkv, hd, _) = self.layer_geom(li);
13265 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
13267 return None;
13268 }
13269 Some(crate::gpu::PrefillMirror {
13270 kv_id: self.graph_kv_id,
13271 layer: li,
13272 limit: self.kv_cache.max_seq_len,
13273 })
13274 }
13275
13276 #[cfg(feature = "gpu")]
13285 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
13286 let kv_id = self.graph_kv_id;
13287 for li in from..upto.min(self.num_layers) {
13288 if !matches!(
13289 self.weights.layers[self.phys_layer(li)].attn,
13290 AttnKind::Full { .. }
13291 ) {
13292 continue;
13293 }
13294 let host = self.kv_cache.layers[li].pos_len();
13296 if host >= position {
13297 continue;
13298 }
13299 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
13300 continue;
13301 };
13302 let to = dev.min(position);
13303 if to <= host {
13304 continue;
13305 }
13306 let (nkv, hd) = {
13307 let c = &self.kv_cache.layers[li];
13308 (c.num_kv_heads, c.head_dim)
13309 };
13310 let Some((k, v, first_valid)) =
13311 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
13312 else {
13313 continue;
13314 };
13315 let need_from = match self.layer_window(li) {
13318 Some(w) => host.max((position + 1).saturating_sub(w)),
13319 None => host,
13320 };
13321 if first_valid > need_from {
13322 tracing::warn!(
13323 "layer {li}: device KV rows {host}..{to} no longer resident \
13324 (from {first_valid}); host attention will miss them"
13325 );
13326 }
13327 let row = nkv * hd;
13328 let cache = &mut self.kv_cache.layers[li];
13329 for p in 0..to - host {
13330 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
13331 }
13332 }
13333 }
13334
13335 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
13338 let mut seen = self.graph_declines.borrow_mut();
13339 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
13340 tracing::warn!("{site} declined: {reason} (CPU attention path)");
13341 seen.push((site, reason));
13342 }
13343 }
13344
13345 pub fn graph_declines(&self) -> Vec<String> {
13348 self.graph_declines
13349 .borrow()
13350 .iter()
13351 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
13352 .collect()
13353 }
13354
13355 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
13364 let Some(dir) = &self.layer_dump else {
13365 return;
13366 };
13367 let mut bytes = Vec::with_capacity(row.len() * 4);
13368 for v in row {
13369 bytes.extend_from_slice(&v.to_le_bytes());
13370 }
13371 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
13372 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
13373 use std::sync::atomic::{AtomicBool, Ordering};
13374 static SAID: AtomicBool = AtomicBool::new(false);
13375 if !SAID.swap(true, Ordering::Relaxed) {
13376 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
13377 }
13378 }
13379 }
13380
13381 fn mimo_moe_prepare(&mut self) {
13384 if !self.mimo_moe.is_undecided() {
13385 return;
13386 }
13387 let slot = {
13388 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
13389 .filter_map(
13390 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
13391 FfnKind::Moe(m) => Some((li, m)),
13392 _ => None,
13393 },
13394 )
13395 .collect();
13396 if layers.is_empty()
13399 || self.physical_layers != self.num_layers
13400 || self.gpu_plan.is_some()
13401 {
13402 crate::mimo_moe::Slot::Off
13403 } else {
13404 let graph_prefix =
13408 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
13409 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
13410 }
13411 };
13412 self.mimo_moe = slot;
13413 }
13414
13415 #[cfg(test)]
13416 pub(crate) fn test_graph_kv_id(&self) -> u64 {
13417 self.graph_kv_id
13418 }
13419
13420 pub(crate) fn mimo_graph_layer_rows(
13424 &mut self,
13425 li: usize,
13426 h: &mut [f32],
13427 positions: &[usize],
13428 ) -> crate::gpu::BatchGraphOutcome {
13429 use crate::gpu::BatchGraphOutcome as Out;
13430 let b = positions.len();
13431 if !(1..=4).contains(&b)
13432 || h.len() != b * self.hidden_size
13433 || !self.mimo_moe.is_dynamic(li, true)
13434 || !crate::gpu::enabled_here()
13435 || !crate::gpu::wgpu_active()
13436 || self.o1_active()
13437 || self.physical_layers != self.num_layers
13438 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
13442 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
13443 || self.wgpu_graph_attn_decline().is_some()
13444 {
13445 return Out::Declined;
13446 }
13447 let attn_started = std::time::Instant::now();
13448 let outcome = {
13449 let lw = &self.weights.layers[li];
13450 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13451 return Out::Declined;
13452 }
13453 let FfnKind::Moe(m) = &lw.ffn else {
13454 return Out::Declined;
13455 };
13456 let AttnKind::Full {
13457 wq,
13458 wk,
13459 wv,
13460 wo,
13461 q_norm,
13462 k_norm,
13463 output_gate,
13464 softplus_gate,
13465 bias,
13466 } = &lw.attn
13467 else {
13468 return Out::Declined;
13469 };
13470 if *output_gate || softplus_gate.is_some() {
13471 return Out::Declined;
13472 }
13473 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
13474 m.experts
13475 .first()?
13476 .gate_proj
13477 .mapped_q4tp()
13478 .map(|(m, _)| m.clone())
13479 }) else {
13480 return Out::Declined;
13481 };
13482 fn gw<'a>(
13483 t: &'a QTensor,
13484 owner: &std::sync::Arc<cortiq_core::CmfModel>,
13485 ) -> Option<crate::gpu::GraphW<'a>> {
13486 if let Some((m, idx, kind, rs)) = t.graph_weight() {
13487 if m.uid() != owner.uid() || t.has_prism_contract() {
13488 return None;
13489 }
13490 return Some(crate::gpu::GraphW {
13491 idx,
13492 kind,
13493 row_scale: rs,
13494 data: &[],
13495 prism: crate::gpu::GraphPrismOp::None,
13496 affine: false,
13497 });
13498 }
13499 t.as_f32().map(|data| crate::gpu::GraphW {
13500 idx: 0,
13501 kind: 4,
13502 row_scale: &[],
13503 data,
13504 prism: crate::gpu::GraphPrismOp::None,
13505 affine: false,
13506 })
13507 }
13508 let (Some(q), Some(k), Some(v), Some(o)) = (
13509 gw(wq, &model),
13510 gw(wk, &model),
13511 gw(wv, &model),
13512 gw(wo, &model),
13513 ) else {
13514 return Out::Declined;
13515 };
13516 let layer = crate::gpu::GraphLayer {
13517 mhc: None,
13518 input_norm: &lw.input_norm,
13519 post_norm: &lw.post_norm,
13520 attn_out_norm: None,
13521 ffn_out_norm: None,
13522 layer_scale: None,
13523 ffn: crate::gpu::GraphFfn::AttentionOnly,
13524 attn: crate::gpu::GraphAttn::Full {
13525 wq: q,
13526 wk: k,
13527 wv: v,
13528 wo: o,
13529 q_norm: q_norm.as_deref(),
13530 k_norm: k_norm.as_deref(),
13531 late_qk_norm: self.qk_norm_after_rope,
13532 bias: bias
13533 .as_ref()
13534 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
13535 output_gate: false,
13536 cpu_k: self.kv_cache.layers[li].k_heads(),
13537 cpu_v: self.kv_cache.layers[li].v_heads(),
13538 cpu_base: self.kv_cache.layers[li].base(),
13539 geom: self.graph_attn_geom(li),
13540 head_gate: None,
13541 },
13542 };
13543 let (nkv, hd, rd) = self.layer_geom(li);
13544 crate::gpu::forward_batch_graph_at(
13545 &model,
13546 self.graph_kv_id,
13547 li,
13548 &[layer],
13549 &self.inv_freq,
13550 h,
13551 self.layer_num_heads(li),
13552 nkv,
13553 hd,
13554 rd,
13555 self.hidden_size,
13556 1,
13557 positions,
13558 self.kv_cache.max_seq_len,
13559 self.norm_style == cortiq_core::NormStyle::Gemma,
13560 self.rms_eps as f32,
13561 self.attn_scale,
13562 b,
13563 &[],
13564 self.o1_epoch,
13565 None,
13566 None,
13567 )
13568 };
13569 match outcome {
13570 Out::Completed => {}
13571 Out::Declined => return Out::Declined,
13572 Out::Failed => {
13573 self.graph_failed
13574 .store(true, std::sync::atomic::Ordering::Relaxed);
13575 return Out::Failed;
13576 }
13577 }
13578 let attn_ns = attn_started.elapsed().as_nanos() as u64;
13579 let hs = self.hidden_size;
13580 let lw = &self.weights.layers[li];
13581 let FfnKind::Moe(m) = &lw.ffn else {
13582 unreachable!()
13583 };
13584 let mut post = vec![0.0; h.len()];
13585 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
13586 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
13587 }
13588 let mut ffn = if b == 1 {
13589 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
13590 } else {
13591 moe_ffn_banked_rows(
13592 &mut self.mimo_moe,
13593 li,
13594 m,
13595 &post,
13596 b,
13597 hs,
13598 self.pool.as_deref(),
13599 )
13600 };
13601 for (x, &f) in h.iter_mut().zip(&ffn) {
13602 *x += f;
13603 }
13604 attention::recycle_buf(&mut ffn);
13605 if self.layer_dump.is_some() {
13606 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
13607 self.dump_layer_row(pos, li, row);
13608 }
13609 }
13610 crate::mimo_moe::note_attention_graph(b, attn_ns);
13611 Out::Completed
13612 }
13613
13614 fn layer_attn_plain(&self, li: usize) -> bool {
13615 self.kv_heads_per_layer.is_none()
13616 && self.v_head_dim.is_none()
13617 && self.global_attn.is_none()
13618 && self.layer_window(li).is_none()
13619 && self.kv_cache.layers[li].sinks.is_none()
13620 }
13621
13622 fn forward_layers(
13624 &mut self,
13625 hidden: &[f32],
13626 position: usize,
13627 task_mask: Option<&TaskMask>,
13628 ) -> Vec<f32> {
13629 let out = self.forward_layers_upto(hidden, position, task_mask, None);
13630 self.o1_progress();
13631 self.swa_trim_tails();
13632 out
13633 }
13634
13635 pub fn embed_id(&self, id: u32) -> Vec<f32> {
13643 self.embed_single(id)
13644 }
13645
13646 pub fn split_supported(&self) -> Result<(), String> {
13650 if self.dsv4.is_some() {
13651 return Err(
13652 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
13653 );
13654 }
13655 if self.dsv41.is_some() {
13656 return Err(
13657 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
13658 .into(),
13659 );
13660 }
13661 if self.qwen4_exp.is_some() {
13662 return Err(
13663 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
13664 );
13665 }
13666 if self.per_token_stack() {
13667 return Err(
13668 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
13669 );
13670 }
13671 Ok(())
13672 }
13673
13674 pub fn forward_span(
13679 &mut self,
13680 hidden: &[f32],
13681 position: usize,
13682 from: usize,
13683 upto: usize,
13684 task_mask: Option<&TaskMask>,
13685 ) -> Result<Vec<f32>, String> {
13686 self.split_supported()?;
13687 if from > upto || upto >= self.num_layers {
13688 return Err(format!(
13689 "forward_span: layer range {from}..={upto} outside 0..{}",
13690 self.num_layers
13691 ));
13692 }
13693 if hidden.len() != self.hidden_size {
13694 return Err(format!(
13695 "forward_span: hidden len {} ≠ hidden_size {}",
13696 hidden.len(),
13697 self.hidden_size
13698 ));
13699 }
13700 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
13701 self.o1_progress();
13702 self.swa_trim_tails();
13703 if self
13704 .graph_failed
13705 .swap(false, std::sync::atomic::Ordering::Relaxed)
13706 {
13707 self.cancel
13708 .store(false, std::sync::atomic::Ordering::Relaxed);
13709 self.clear_sequence_state();
13710 return Err("forward_span: deferred O(1) transition failed".into());
13711 }
13712 Ok(out)
13713 }
13714
13715 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
13718 let normed = inference::rms_norm(
13719 hidden,
13720 &self.weights.final_norm,
13721 self.rms_eps,
13722 self.norm_style,
13723 );
13724 self.lm_head_forward(&normed)
13725 }
13726
13727 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
13729 sampler::sample_with_scratch(
13730 logits,
13731 &self.sampler_config,
13732 past_tokens,
13733 &mut self.rng,
13734 &mut self.sampler_scratch,
13735 )
13736 }
13737
13738 pub fn reset_session(&mut self) {
13740 self.clear_sequence_state();
13741 }
13742
13743 pub fn prefill_span_ids(
13749 &mut self,
13750 ids: &[u32],
13751 start_pos: usize,
13752 upto: usize,
13753 task_mask: Option<&TaskMask>,
13754 ) -> Result<Vec<f32>, String> {
13755 self.split_supported()?;
13756 if upto >= self.num_layers {
13757 return Err(format!(
13758 "prefill_span_ids: upto {upto} outside 0..{}",
13759 self.num_layers
13760 ));
13761 }
13762 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13766 let out =
13767 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
13768 self.check_o1_progress_failure("prefill_span_ids")?;
13769 Ok(out)
13770 } else {
13771 let hs = self.hidden_size;
13772 let mut out = Vec::with_capacity(ids.len() * hs);
13773 for (i, &id) in ids.iter().enumerate() {
13774 let emb = self.embed_id(id);
13775 out.extend_from_slice(&self.forward_span(
13776 &emb,
13777 start_pos + i,
13778 0,
13779 upto,
13780 task_mask,
13781 )?);
13782 }
13783 Ok(out)
13784 }
13785 }
13786
13787 pub fn prefill_span_hidden(
13790 &mut self,
13791 hidden: &[f32],
13792 start_pos: usize,
13793 from: usize,
13794 upto: usize,
13795 task_mask: Option<&TaskMask>,
13796 ) -> Result<Vec<f32>, String> {
13797 self.split_supported()?;
13798 let hs = self.hidden_size;
13799 if hidden.is_empty() || hidden.len() % hs != 0 {
13800 return Err(format!(
13801 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
13802 hidden.len()
13803 ));
13804 }
13805 if from > upto || upto >= self.num_layers {
13806 return Err(format!(
13807 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
13808 self.num_layers
13809 ));
13810 }
13811 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
13812 let out = self.prefill_batch_span(
13813 PrefillIn::Hidden(hidden),
13814 start_pos,
13815 task_mask,
13816 from,
13817 upto + 1,
13818 );
13819 self.check_o1_progress_failure("prefill_span_hidden")?;
13820 Ok(out)
13821 } else {
13822 let b = hidden.len() / hs;
13823 let mut out = Vec::with_capacity(hidden.len());
13824 for i in 0..b {
13825 let h = self.forward_span(
13826 &hidden[i * hs..(i + 1) * hs],
13827 start_pos + i,
13828 from,
13829 upto,
13830 task_mask,
13831 )?;
13832 out.extend_from_slice(&h);
13833 }
13834 Ok(out)
13835 }
13836 }
13837
13838 fn try_token_graph_wgpu(
13842 &self,
13843 hidden: &[f32],
13844 position: usize,
13845 logits_out: &mut Vec<f32>,
13846 layers_run: &mut usize,
13847 ) -> Option<Result<Vec<f32>, ()>> {
13848 self.try_token_graph_wgpu_steps(
13849 hidden,
13850 position,
13851 logits_out,
13852 1,
13853 None,
13854 Some(layers_run),
13855 0,
13856 self.num_layers,
13857 )
13858 }
13859
13860 fn try_token_graph_wgpu_span(
13864 &self,
13865 hidden: &[f32],
13866 position: usize,
13867 logits_out: &mut Vec<f32>,
13868 from: usize,
13869 upto_excl: usize,
13870 layers_run: &mut usize,
13871 ) -> Option<Result<Vec<f32>, ()>> {
13872 self.try_token_graph_wgpu_steps(
13873 hidden,
13874 position,
13875 logits_out,
13876 1,
13877 None,
13878 Some(layers_run),
13879 from,
13880 upto_excl,
13881 )
13882 }
13883
13884 #[cfg(feature = "gpu")]
13888 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
13889 if !crate::minicpm_tail::minimax_pool_on()
13890 || !crate::gpu::enabled_here()
13891 || self.head_clusters.is_some()
13892 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
13893 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
13894 {
13895 return None;
13896 }
13897 let (model, i, kind, rs) = self
13898 .weights
13899 .lm_head
13900 .graph_weight()
13901 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
13902 let name = &model.tensors[i].name;
13903 let prism = if crate::prism::is_inverse_embedding(&model, name) {
13904 crate::gpu::GraphPrismOp::InverseEmbedding
13905 } else if crate::prism::is_forward_weight(&model, name) {
13906 crate::gpu::GraphPrismOp::Forward
13907 } else {
13908 crate::gpu::GraphPrismOp::None
13909 };
13910 let gw = crate::gpu::GraphW {
13911 idx: i,
13912 kind,
13913 row_scale: rs,
13914 data: &[],
13915 prism,
13916 affine: crate::prism::is_affine_target(&model, name),
13917 };
13918 let rows = self.weights.lm_head.rows();
13919 let mut h = hidden.to_vec();
13920 let mut lg = Vec::new();
13921 let mut layers_run = 0usize;
13922 let outcome = crate::gpu::forward_token_graph(
13923 &model,
13924 self.graph_kv_id,
13925 &[],
13926 &[],
13927 self.o1_epoch,
13928 &self.inv_freq,
13929 &mut h,
13930 self.num_heads,
13931 self.num_kv_heads,
13932 self.head_dim,
13933 self.attn_scale,
13934 self.head_dim,
13935 self.hidden_size,
13936 self.intermediate_size,
13937 0,
13938 self.kv_cache.max_seq_len,
13939 self.norm_style == cortiq_core::NormStyle::Gemma,
13940 self.rms_eps as f32,
13941 Some((&gw, rows)),
13942 &self.weights.final_norm,
13943 &mut lg,
13944 &[],
13945 1,
13946 None,
13947 None,
13948 Some(&mut layers_run),
13949 0,
13950 false,
13951 );
13952 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
13953 return None;
13954 }
13955 lg.resize(self.vocab_size, 0.0);
13958 if let Some(m) = self.logit_multiplier {
13959 for l in lg.iter_mut() {
13960 *l *= m;
13961 }
13962 }
13963 if let Some(c) = self.final_softcap {
13964 softcap_in_place(&mut lg, c, self.pool.as_deref());
13965 }
13966 Some(lg)
13967 }
13968
13969 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
13972 if winners.is_empty() {
13973 return;
13974 }
13975 if self.m3_n_exp == 0 {
13976 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
13977 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
13978 }
13979 for &e in winners {
13980 let e = e as usize;
13981 if e < self.m3_n_exp && li < self.num_layers {
13982 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
13983 }
13984 }
13985 if li + 1 == self.num_layers {
13986 self.m3_tokens += 1;
13987 if self.m3_tokens % 16 == 0 {
13988 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
13989 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
13990 let _ = std::fs::write(&path, bytes);
13991 }
13992 }
13993 }
13994 }
13995
13996 fn m3_prewarm(&mut self) {
14001 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
14002 return;
14003 };
14004 let Ok(bytes) = std::fs::read(&path) else {
14005 tracing::warn!("m3 prewarm: cannot read profile {path}");
14006 return;
14007 };
14008 let counts: Vec<u32> = bytes
14009 .chunks_exact(4)
14010 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
14011 .collect();
14012 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
14013 if counts.len() != self.num_layers * n_exp {
14014 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
14015 return;
14016 }
14017 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
14018 .ok()
14019 .and_then(|v| v.parse().ok())
14020 .unwrap_or(300);
14021 let mut ranked: Vec<(u32, usize, usize)> = counts
14022 .iter()
14023 .enumerate()
14024 .filter(|(_, c)| **c > 0)
14025 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
14026 .collect();
14027 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
14028 ranked.truncate(limit);
14029 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
14030 for (_, li, e) in ranked {
14031 per_layer[li].push(e);
14032 }
14033 let mut admitted = 0usize;
14034 for (li, ids) in per_layer.iter().enumerate() {
14035 if ids.is_empty() {
14036 continue;
14037 }
14038 let lw = &self.weights.layers[self.phys_layer(li)];
14039 let FfnKind::Moe(m) = &lw.ffn else {
14040 continue;
14041 };
14042 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
14044 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
14045 admitted += ids.len();
14046 }
14047 }
14048 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
14049 }
14050
14051 #[cfg(feature = "gpu")]
14062 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
14063 if self.o1_active() || self.attn_softcap > 0.0 {
14064 return Ok(false);
14065 }
14066 if !self.m3_prewarmed {
14067 self.m3_prewarmed = true;
14068 self.m3_prewarm();
14069 }
14070 let frame = {
14071 let lw = &self.weights.layers[self.phys_layer(li)];
14072 let FfnKind::Moe(m) = &lw.ffn else {
14073 return Ok(false);
14074 };
14075 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
14076 return Ok(false);
14077 }
14078 let t_arm = std::time::Instant::now();
14079 if self.m3_winners.len() != self.num_layers {
14080 self.m3_winners = vec![Vec::new(); self.num_layers];
14081 }
14082 let pred = std::mem::take(&mut self.m3_winners[li]);
14083 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
14084 if armed.is_none() {
14085 self.m3_winners[li] = pred; }
14087 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14088 match armed {
14089 Some(frame) => frame,
14090 None => return Ok(false),
14093 }
14094 };
14095 crate::gpu_wgpu::pooled_frame_enter(frame);
14096 let want_logits = self.graph_want_logits;
14100 self.graph_want_logits = false;
14101 let mut lg = Vec::new();
14102 let mut gl = 0usize;
14103 let t_span = std::time::Instant::now();
14104 let span = crate::gpu::gpu_scope(|| {
14105 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
14106 });
14107 self.graph_want_logits = want_logits;
14108 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14109 let hh = match span {
14110 Some(Ok(hh)) => hh,
14111 Some(Err(())) => return Err(()),
14112 None => {
14113 return Ok(false);
14116 }
14117 };
14118 if gl != 1 {
14119 return if gl == 0 { Ok(false) } else { Err(()) };
14122 }
14123 *h = hh;
14124 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
14125 self.m3_note_route(li, &winners);
14126 if self.m3_winners.len() != self.num_layers {
14127 self.m3_winners = vec![Vec::new(); self.num_layers];
14128 }
14129 self.m3_winners[li] = winners;
14130 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
14131 cold.retain(|&(_, w)| {
14134 if w.is_finite() {
14135 true
14136 } else {
14137 tracing::error!("pooled tail: non-finite cold weight from expert route");
14138 false
14139 }
14140 });
14141 if cold.is_empty() {
14142 return Ok(true);
14143 }
14144 let x = crate::gpu_wgpu::pooled_frame_take_input();
14145 if x.len() != self.hidden_size {
14146 return Err(());
14147 }
14148 let lw = &self.weights.layers[self.phys_layer(li)];
14154 let FfnKind::Moe(m) = &lw.ffn else {
14155 return Err(());
14156 };
14157 let t_cold = std::time::Instant::now();
14158 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
14159 let mut out = crate::attention::take_buf(self.hidden_size);
14160 let on_device = !force_host
14161 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
14162 if !on_device {
14163 crate::attention::recycle_buf(&mut out);
14164 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
14165 .iter()
14166 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
14167 .collect::<Result<_, ()>>()?;
14168 out = crate::gpu::cpu_scope(|| {
14169 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
14170 });
14171 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
14172 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
14173 }
14174 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
14175 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14176 if out.len() != h.len() {
14177 crate::attention::recycle_buf(&mut out);
14178 return Err(());
14179 }
14180 for (dst, src) in h.iter_mut().zip(out.iter()) {
14181 *dst += src;
14182 }
14183 crate::attention::recycle_buf(&mut out);
14184 if on_device {
14185 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
14186 }
14187 Ok(true)
14188 }
14189
14190 #[cfg(not(feature = "gpu"))]
14191 fn m3_pooled_layer(
14192 &mut self,
14193 _li: usize,
14194 _position: usize,
14195 _h: &mut Vec<f32>,
14196 ) -> Result<bool, ()> {
14197 Ok(false)
14198 }
14199
14200 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
14204 if self.o1_active() || self.attn_softcap > 0.0 {
14205 return None;
14206 }
14207 if let Some(reason) = self.wgpu_graph_attn_decline() {
14210 self.note_graph_decline("wgpu multi-burst", reason);
14211 return None;
14212 }
14213 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
14214 if !graph_on || self.graph_refused() {
14215 return None;
14222 }
14223 let emb = self.embed_single(t_next);
14224 let mut lg = Vec::new();
14225 let mut ids = Vec::new();
14226 match self.try_token_graph_wgpu_steps(
14227 &emb,
14228 position,
14229 &mut lg,
14230 k,
14231 Some(&mut ids),
14232 None,
14233 0,
14234 self.num_layers,
14235 ) {
14236 Some(Ok(_)) => {}
14237 Some(Err(())) => {
14238 self.graph_failed
14243 .store(true, std::sync::atomic::Ordering::Relaxed);
14244 return None;
14245 }
14246 None => return None,
14247 }
14248 (ids.len() == k).then_some(ids)
14249 }
14250
14251 fn try_token_graph_wgpu_steps(
14255 &self,
14256 hidden: &[f32],
14257 position: usize,
14258 logits_out: &mut Vec<f32>,
14259 steps: usize,
14260 ids_out: Option<&mut Vec<u32>>,
14261 layers_run: Option<&mut usize>,
14262 from: usize,
14263 upto_excl: usize,
14264 ) -> Option<Result<Vec<f32>, ()>> {
14265 if self.prompt_mrope.is_some()
14269 || self.mhc.as_deref().is_some_and(|m| {
14270 from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
14271 })
14272 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
14273 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
14274 && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
14275 {
14276 return None;
14277 }
14278 let upto_excl = match self.mimo_moe.graph_prefix_end() {
14281 Some(end) if end < upto_excl => {
14282 if steps != 1 || layers_run.is_none() || from >= end {
14283 return None;
14284 }
14285 end
14286 }
14287 _ => upto_excl,
14288 };
14289 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
14292 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
14293 return None;
14297 }
14298 if let Some(reason) = self.wgpu_graph_attn_decline() {
14305 self.note_graph_decline("wgpu token graph", reason);
14306 return None;
14307 }
14308 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
14313 .map(|li| {
14314 if !o1_gpu {
14315 return None;
14316 }
14317 self.kv_cache.layers[self.phys_layer(li)].o1_views()
14318 })
14319 .collect();
14320 if self.o1_active() && o1_gpu {
14321 let want: usize = (from..upto_excl)
14324 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
14325 .count();
14326 let have = o1_views.iter().filter(|v| v.is_some()).count();
14327 if want == 0 || have != want {
14328 use std::sync::atomic::{AtomicUsize, Ordering};
14338 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
14339 let code = have * 1000 + want;
14340 if LAST.swap(code, Ordering::Relaxed) != code {
14341 tracing::warn!(
14342 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
14343 );
14344 }
14345 return None;
14346 }
14347 }
14348 let nh = self.num_heads;
14349 let (nkv, hd, rd) = self.layer_geom(0);
14350 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14351 let mut layers = Vec::with_capacity(upto_excl - from);
14352 let mut model = None;
14353 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
14354 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
14355 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
14357 let name = &m.tensors[i].name;
14358 let prism = if crate::prism::is_inverse_embedding(m, name) {
14359 crate::gpu::GraphPrismOp::InverseEmbedding
14360 } else if crate::prism::is_forward_weight(m, name) {
14361 crate::gpu::GraphPrismOp::Forward
14362 } else {
14363 crate::gpu::GraphPrismOp::None
14364 };
14365 return Some(crate::gpu::GraphW {
14366 idx: i,
14367 kind,
14368 row_scale: rs,
14369 data: &[],
14370 prism,
14371 affine: crate::prism::is_affine_target(m, name),
14372 });
14373 }
14374 match t.as_f32() {
14376 Some(d) => Some(crate::gpu::GraphW {
14377 idx: 0,
14378 kind: 4,
14379 row_scale: &[],
14380 data: d,
14381 prism: crate::gpu::GraphPrismOp::None,
14382 affine: false,
14383 }),
14384 None => {
14385 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
14386 eprintln!("batch graph: weight has no graph/f32 representation");
14387 }
14388 None
14389 }
14390 }
14391 }
14392 for li in from..upto_excl {
14393 let lw = &self.weights.layers[self.phys_layer(li)];
14394 if dbg {
14395 let ak = match &lw.attn {
14396 AttnKind::Mla(_) => "Mla".into(),
14397 AttnKind::Full {
14398 output_gate, bias, ..
14399 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
14400 AttnKind::LinearGdn(_) => "LinearGdn".into(),
14401 AttnKind::Kda(_) => "Kda".into(),
14402 AttnKind::Linear(_) => "Linear".into(),
14403 AttnKind::ShortConv(_) => "ShortConv".into(),
14404 AttnKind::Bounded(_) => "Bounded".into(),
14405 };
14406 let fk = match &lw.ffn {
14407 FfnKind::Dense(_) => "Dense",
14408 FfnKind::Moe(_) => "Moe",
14409 FfnKind::DenseMoe(_) => "DenseMoe",
14410 };
14411 eprintln!("graph L{li}: attn={ak} ffn={fk}");
14412 }
14413 let gffn = match &lw.ffn {
14414 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
14418 FfnKind::Dense(d) => {
14419 let Some(act) = d.act.wgpu_graph_act() else {
14423 self.note_graph_decline(
14424 "wgpu token graph",
14425 "dense FFN activation without a graph kernel",
14426 );
14427 return None;
14428 };
14429 crate::gpu::GraphFfn::Dense {
14430 gate: gw(&d.gate_proj)?,
14431 up: gw(&d.up_proj)?,
14432 down: gw(&d.down_proj)?,
14433 act,
14434 }
14435 }
14436 FfnKind::Moe(m) => {
14437 if m.route_tau.is_some() || m.mask.is_some() {
14445 return None;
14446 }
14447 let shared = m.shared.as_ref();
14448 let inter = m.experts.first()?.gate_proj.rows();
14454 let detached_shared = match shared {
14462 Some((se, None))
14463 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14464 {
14465 if !matches!(se.act, Act::Silu)
14466 || se.gate_proj.mapped_q4tp().is_none()
14467 || se.up_proj.mapped_q4tp().is_none()
14468 || se.down_proj.mapped_q4tp().is_none()
14469 {
14470 return None;
14471 }
14472 Some(crate::gpu::GraphMoeDetachedShared {
14473 gate: gw(&se.gate_proj)?,
14474 up: gw(&se.up_proj)?,
14475 down: gw(&se.down_proj)?,
14476 width: se.gate_proj.rows(),
14477 })
14478 }
14479 Some((se, Some(_)))
14483 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
14484 {
14485 return None;
14486 }
14487 _ => None,
14488 };
14489 let has_shared = shared.is_some() && detached_shared.is_none();
14490 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
14491 let sgate = match shared {
14492 Some((_, Some(sg))) if has_shared => gw(sg)?,
14493 _ => gw(&m.router)?,
14497 };
14498 let router = gw(&m.router)?;
14499 if router.prism != crate::gpu::GraphPrismOp::None
14505 || sgate.prism != crate::gpu::GraphPrismOp::None
14506 || router.affine
14507 || sgate.affine
14508 {
14509 tracing::warn!(
14510 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
14511 );
14512 return None;
14513 }
14514 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
14515 let edge = |t: &QTensor| {
14516 t.graph_weight()
14517 .or_else(|| t.graph_weight_descriptor())
14518 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
14519 };
14520 let bank_key: Option<GraphMoeKey> = match (
14521 m.experts.first().and_then(|e| edge(&e.gate_proj)),
14522 m.experts.last().and_then(|e| edge(&e.down_proj)),
14523 ) {
14524 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
14525 self.phys_layer(li),
14526 m.experts.as_ptr() as usize,
14527 m.experts.len(),
14528 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
14529 fm,
14530 fi,
14531 di,
14532 )),
14533 _ => None,
14534 };
14535 let cached =
14536 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
14537 let bank = if let Some(b) = cached {
14538 b
14539 } else {
14540 let mut experts = Vec::with_capacity(m.experts.len() + 1);
14541 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14542 let mut q4tp: Option<bool> = None;
14545 let mut gu_q2: Option<bool> = None;
14548 let mut q82: Option<bool> = None;
14550 let mut moe_act = None;
14554 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
14555 let Some(e_act) = e.act.wgpu_graph_act() else {
14556 return None;
14557 };
14558 match moe_act {
14559 Some(existing) if existing != e_act => return None,
14560 None => moe_act = Some(e_act),
14561 _ => {}
14562 }
14563 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
14564 return None;
14565 }
14566 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
14571 let Some((em, ei, _, _)) = expert_weight
14572 .graph_weight()
14573 .or_else(|| expert_weight.graph_weight_descriptor())
14574 else {
14575 return None;
14576 };
14577 let name = &em.tensors[ei].name;
14578 if crate::prism::is_forward_weight(em, name)
14579 || crate::prism::is_inverse_embedding(em, name)
14580 || crate::prism::is_affine_target(em, name)
14581 {
14582 tracing::warn!(
14583 "resident MoE declined: expert Prism/affine transform is not implemented"
14584 );
14585 return None;
14586 }
14587 }
14588 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
14589 Some((mm, gi)) => (
14590 mm,
14591 gi,
14592 e.up_proj.mapped_q4t()?.1,
14593 e.down_proj.mapped_q4t()?.1,
14594 false,
14595 false,
14596 false,
14597 ),
14598 None => match e.gate_proj.mapped_q2tp() {
14599 Some((mm, gi)) => (
14600 mm,
14601 gi,
14602 e.up_proj.mapped_q2tp()?.1,
14603 e.down_proj.mapped_q4tp()?.1,
14604 true,
14605 true,
14606 false,
14607 ),
14608 None => match e.gate_proj.mapped_q4tp() {
14609 Some((mm, gi)) => (
14610 mm,
14611 gi,
14612 e.up_proj.mapped_q4tp()?.1,
14613 e.down_proj.mapped_q4tp()?.1,
14614 true,
14615 false,
14616 false,
14617 ),
14618 None => {
14621 if !crate::gpu::moe_q82_graph_on() {
14622 return None;
14623 }
14624 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
14625 (
14626 mm,
14627 gi,
14628 e.up_proj.mapped_q8_2f()?.1,
14629 e.down_proj.mapped_q8_2f()?.1,
14630 false,
14631 false,
14632 true,
14633 )
14634 }
14635 },
14636 },
14637 };
14638 if *q4tp.get_or_insert(is_p) != is_p
14639 || *gu_q2.get_or_insert(is_q2) != is_q2
14640 || *q82.get_or_insert(is_8) != is_8
14641 {
14642 tracing::warn!(
14648 "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."
14649 );
14650 return None;
14651 }
14652 bank_model.get_or_insert_with(|| mm.clone());
14653 experts.push((gi, ui, di));
14654 }
14655 let b = GraphMoeBank {
14656 experts,
14657 q4tp,
14658 gu_q2,
14659 q82,
14660 act: moe_act,
14661 model: bank_model,
14662 };
14663 if let Some(k) = bank_key {
14664 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
14665 }
14666 b
14667 };
14668 if let Some(mm) = &bank.model {
14669 model.get_or_insert_with(|| mm.clone());
14670 }
14671 crate::gpu::GraphFfn::Moe {
14672 router,
14673 shared_gate: sgate,
14674 experts: bank.experts,
14675 n_exp: m.experts.len(),
14676 top_k: std::env::var("CMF_TOPK_PROBE")
14682 .ok()
14683 .and_then(|v| v.parse::<usize>().ok())
14684 .filter(|k| *k > 0 && *k <= m.top_k)
14685 .unwrap_or(m.top_k),
14686 inter,
14687 act: bank.act?,
14688 norm_topk: m.norm_topk_prob,
14689 q4tp: bank.q4tp?,
14690 gu_q2: bank.gu_q2.unwrap_or(false),
14691 q82: bank.q82.unwrap_or(false),
14692 sigmoid: m.router_sigmoid,
14693 sigmoid_denom_eps: m.sigmoid_denom_eps,
14694 bias: m.expert_bias.as_deref(),
14695 has_shared,
14696 detached_shared,
14697 shared_gated,
14698 route_scale: m.routed_scaling,
14699 }
14700 }
14701 };
14702 let attn = match &lw.attn {
14703 AttnKind::Full {
14704 wq,
14705 wk,
14706 wv,
14707 wo,
14708 q_norm,
14709 k_norm,
14710 output_gate,
14711 softplus_gate,
14712 bias,
14713 } => {
14714 if self.attention_heads_per_layer.is_some() {
14715 return None;
14716 }
14717 let head_gate = match softplus_gate {
14721 None => None,
14722 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
14723 Some(_) => {
14724 self.note_graph_decline(
14725 "wgpu token graph",
14726 "projected softplus / per-element output gate",
14727 );
14728 return None;
14729 }
14730 };
14731 let (m, _, _, _) = wq.graph_weight_or_blr()?;
14732 model = Some(m.clone());
14733 crate::gpu::GraphAttn::Full {
14734 wq: gw(wq)?,
14735 wk: gw(wk)?,
14736 wv: gw(wv)?,
14737 wo: gw(wo)?,
14738 q_norm: q_norm.as_deref(),
14739 k_norm: k_norm.as_deref(),
14740 late_qk_norm: self.qk_norm_after_rope,
14741 bias: bias
14742 .as_ref()
14743 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
14744 output_gate: *output_gate,
14745 cpu_k: self.kv_cache.layers[li].k_heads(),
14746 cpu_v: self.kv_cache.layers[li].v_heads(),
14747 cpu_base: self.kv_cache.layers[li].base(),
14748 geom: self.graph_attn_geom(li),
14749 head_gate,
14750 }
14751 }
14752 AttnKind::LinearGdn(w) => {
14753 let cfg = self.gdn_cfg?;
14754 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
14755 model = Some(m.clone());
14756 crate::gpu::GraphAttn::Gdn {
14757 qkv: gw(&w.in_proj_qkv)?,
14758 z: gw(&w.in_proj_z)?,
14759 a: gw(&w.in_proj_a)?,
14760 b: gw(&w.in_proj_b)?,
14761 out: gw(&w.out_proj)?,
14762 conv1d: &w.conv1d,
14763 a_log: &w.a_log,
14764 dt_bias: &w.dt_bias,
14765 norm: &w.norm,
14766 nv: cfg.num_v_heads,
14767 nk: cfg.num_k_heads,
14768 dk: cfg.key_head_dim,
14769 dv: cfg.value_head_dim,
14770 kk: cfg.conv_kernel,
14771 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14772 preview: None,
14773 }
14774 }
14775 AttnKind::ShortConv(w) => {
14776 let cfg = self.short_conv_cfg?;
14777 let (m, _, _, _) = w
14778 .in_proj
14779 .graph_weight()
14780 .or_else(|| w.in_proj.graph_weight_descriptor())?;
14781 model = Some(m.clone());
14782 crate::gpu::GraphAttn::ShortConv {
14783 inp: gw(&w.in_proj)?,
14784 out: gw(&w.out_proj)?,
14785 taps: &w.conv,
14786 kernel: cfg.kernel,
14787 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
14788 }
14789 }
14790 _ => return None,
14791 };
14792 layers.push(crate::gpu::GraphLayer {
14793 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
14794 input_norm: &lw.input_norm,
14795 attn,
14796 post_norm: &lw.post_norm,
14797 attn_out_norm: lw.attn_out_norm.as_deref(),
14798 ffn_out_norm: lw.ffn_out_norm.as_deref(),
14799 layer_scale: lw.layer_scale,
14800 ffn: gffn,
14801 });
14802 }
14803 let model = model?;
14804 let lm_gw = if upto_excl == self.num_layers
14810 && self.graph_want_logits
14811 && std::env::var("CMF_GPU_LMHEAD")
14812 .map(|v| v != "0")
14813 .unwrap_or(true)
14814 {
14815 self.weights
14816 .lm_head
14817 .graph_weight()
14818 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
14819 .map(|(m, i, kind, rs)| {
14820 let name = &m.tensors[i].name;
14821 let prism = if crate::prism::is_inverse_embedding(m, name) {
14822 crate::gpu::GraphPrismOp::InverseEmbedding
14823 } else if crate::prism::is_forward_weight(m, name) {
14824 crate::gpu::GraphPrismOp::Forward
14825 } else {
14826 crate::gpu::GraphPrismOp::None
14827 };
14828 (
14829 crate::gpu::GraphW {
14830 idx: i,
14831 kind,
14832 row_scale: rs,
14833 data: &[],
14834 prism,
14835 affine: crate::prism::is_affine_target(m, name),
14836 },
14837 self.weights.lm_head.rows(),
14838 )
14839 })
14840 } else {
14841 None
14842 };
14843 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
14844 let emb_gw = if steps > 1 {
14846 self.weights
14847 .embed_tokens
14848 .graph_weight()
14849 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
14850 .map(|(m, i, kind, rs)| {
14851 let name = &m.tensors[i].name;
14852 let prism = if crate::prism::is_inverse_embedding(m, name) {
14853 crate::gpu::GraphPrismOp::InverseEmbedding
14854 } else if crate::prism::is_forward_weight(m, name) {
14855 crate::gpu::GraphPrismOp::Forward
14856 } else {
14857 crate::gpu::GraphPrismOp::None
14858 };
14859 (
14860 crate::gpu::GraphW {
14861 idx: i,
14862 kind,
14863 row_scale: rs,
14864 data: &[],
14865 prism,
14866 affine: crate::prism::is_affine_target(m, name),
14867 },
14868 self.weights.embed_tokens.rows(),
14869 self.embed_multiplier,
14870 )
14871 })
14872 } else {
14873 None
14874 };
14875
14876 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
14882 (from..upto_excl.min(self.num_layers - 1))
14883 .filter(|&li| (li + 1) % self.physical_layers == 0)
14884 .map(|li| li - from)
14885 .collect()
14886 } else {
14887 Vec::new()
14888 };
14889 let mut h = hidden.to_vec();
14890 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
14896 let outcome = crate::gpu::forward_token_graph(
14897 &model,
14898 self.graph_kv_id,
14899 &layers,
14900 &o1_views,
14901 self.o1_epoch,
14902 &self.inv_freq,
14903 &mut h,
14904 nh,
14905 nkv,
14906 hd,
14907 self.attn_scale,
14908 rd,
14909 self.hidden_size,
14910 self.intermediate_size,
14911 position,
14912 self.kv_cache.max_seq_len,
14913 gemma,
14914 self.rms_eps as f32,
14915 lm,
14916 &self.weights.final_norm,
14917 logits_out,
14918 &loop_norm_at,
14919 steps,
14920 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
14921 ids_out,
14922 layers_run,
14923 from,
14924 dump_hidden,
14925 );
14926 match outcome {
14927 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
14928 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
14929 crate::gpu::TokenGraphOutcome::Declined => None,
14930 }
14931 }
14932
14933 #[cfg(target_os = "macos")]
14942 #[allow(clippy::type_complexity)]
14943 fn metal_rows_plan(
14944 &self,
14945 ) -> Option<(
14946 Vec<MetalRowsItem<'_>>,
14947 std::sync::Arc<cortiq_core::CmfModel>,
14948 Option<crate::gpu_metal::GdnGpuCfg>,
14949 )> {
14950 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
14951 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
14952 if !graph_force
14953 || !crate::gpu::enabled_here()
14954 || std::env::var("CMF_GPU_BLOCK")
14955 .map(|v| v == "0")
14956 .unwrap_or(false)
14957 || self.attn_softcap > 0.0
14958 || self.o1_active()
14959 || self.swa.is_some()
14960 || self.global_attn.is_some()
14961 || self.attention_heads_per_layer.is_some()
14962 || self.graph_attn_decline_reason().is_some()
14964 || self.attn_v_norm
14965 || self.loop_final_norm
14966 || !self.metal_rope_scale_ok()
14967 {
14968 return None;
14969 }
14970 let attend_contract = self.head_dim % 4 == 0
14971 && self.head_dim <= 256
14972 && self.rotary_dim >= 2
14973 && self.rotary_dim <= self.head_dim
14974 && (self.rotary_dim / 2) % 32 == 0
14975 && self.num_kv_heads > 0
14976 && self.num_heads % self.num_kv_heads == 0;
14977 if !attend_contract {
14978 return None;
14979 }
14980 let mut plan: Vec<MetalRowsItem> = Vec::new();
14981 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
14982 for li in 0..self.num_layers {
14983 let lw = &self.weights.layers[self.phys_layer(li)];
14984 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14985 return None;
14986 }
14987 let ffn = match &lw.ffn {
14988 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
14989 let (Some(g), Some(u), Some(dn)) = (
14990 d.gate_proj.metal_graph_parts(),
14991 d.up_proj.metal_graph_parts(),
14992 d.down_proj.metal_graph_parts(),
14993 ) else {
14994 return None;
14995 };
14996 MetalFfn::Dense {
14997 gate: g,
14998 up: u,
14999 down: dn,
15000 act: 0, }
15002 }
15003 _ => return None,
15004 };
15005 match &lw.attn {
15006 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
15007 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
15008 w.in_proj_qkv.metal_graph_parts(),
15009 w.in_proj_z.metal_graph_parts(),
15010 w.in_proj_a.f32_parts(),
15011 w.in_proj_b.f32_parts(),
15012 w.out_proj.metal_graph_parts(),
15013 ) else {
15014 return None;
15015 };
15016 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
15017 model_ref.get_or_insert_with(|| model.clone());
15018 }
15019 let gl = GdnGpuLayer {
15020 mhc: None,
15021 attn_norm: &lw.input_norm,
15022 post_norm: &lw.post_norm,
15023 qkv,
15024 z,
15025 a,
15026 b: bb,
15027 out,
15028 ffn,
15029 conv1d: &w.conv1d,
15030 a_log: &w.a_log,
15031 dt_bias: &w.dt_bias,
15032 gnorm: &w.norm,
15033 };
15034 match plan.last_mut() {
15035 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
15036 _ => plan.push(MetalRowsItem::Gdn {
15037 run: vec![gl],
15038 first: li,
15039 }),
15040 }
15041 }
15042 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 } => {
15053 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
15054 wq.metal_graph_parts(),
15055 wk.metal_graph_parts(),
15056 wv.metal_graph_parts(),
15057 wo.metal_graph_parts(),
15058 ) else {
15059 return None;
15060 };
15061 if let QTensor::Mapped { model, .. } = wq {
15062 model_ref.get_or_insert_with(|| model.clone());
15063 }
15064 let cache = &self.kv_cache.layers[li];
15065 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
15066 return None;
15067 }
15068 plan.push(MetalRowsItem::Attn {
15069 l: AttnGpuLayer {
15070 mhc: None,
15071 attn_norm: &lw.input_norm,
15072 post_norm: &lw.post_norm,
15073 wq: pq,
15074 wk: pk,
15075 wv: pv,
15076 wo: po,
15077 ffn,
15078 attn_out_norm: None,
15079 ffn_out_norm: None,
15080 layer_scale: None,
15081 },
15082 li,
15083 q_norm: q_norm.as_deref(),
15084 k_norm: k_norm.as_deref(),
15085 output_gate: *output_gate,
15086 });
15087 }
15088 _ => return None,
15089 }
15090 }
15091 let model = model_ref?;
15092 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
15093 nv: cfg.num_v_heads,
15094 nk: cfg.num_k_heads,
15095 dk: cfg.key_head_dim,
15096 dv: cfg.value_head_dim,
15097 kk: cfg.conv_kernel,
15098 hidden: self.hidden_size,
15099 inter: self.intermediate_size,
15100 c_dim: cfg.conv_dim(),
15101 eps: cfg.rms_eps as f32,
15102 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15103 });
15104 Some((plan, model, gcfg))
15105 }
15106
15107 #[cfg(target_os = "macos")]
15109 #[allow(clippy::too_many_arguments)]
15110 fn metal_attn_params<'a>(
15111 li: usize,
15112 cache: &'a crate::kv_cache::LayerKvCache,
15113 q_norm: Option<&'a [f32]>,
15114 k_norm: Option<&'a [f32]>,
15115 output_gate: bool,
15116 inv_freq: &'a [f32],
15117 rope_scale: f32,
15118 geom: (usize, usize, usize, usize),
15119 pos0: usize,
15120 kv_id: u64,
15121 scale: f32,
15122 eps: f32,
15123 gemma: bool,
15124 late_qk_norm: bool,
15125 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
15126 let (nh, nkv, hd, rd) = geom;
15127 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15128 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15129 let cpu_stored = cpu_k[0].len() / hd;
15130 (
15131 crate::gpu_metal::AttnDeviceParams {
15132 kv_id,
15133 layer: li,
15134 nh,
15135 nkv,
15136 hd,
15137 rd,
15138 position: pos0,
15139 scale,
15140 eps,
15141 gemma,
15142 late_qk_norm,
15143 output_gate,
15144 q_norm,
15145 k_norm,
15146 inv_freq,
15147 rope_scale,
15148 cpu_k,
15149 cpu_v,
15150 cpu_stored,
15151 cpu_gen: cache.generation(),
15152 o1: None,
15153 window: None,
15154 softmax_sink: cache.sinks.as_deref(),
15155 head_gate: None,
15156 v_norm: false,
15157 },
15158 cpu_stored,
15159 )
15160 }
15161
15162 #[cfg(target_os = "macos")]
15167 #[allow(clippy::type_complexity)]
15168 fn metal_rows_run(
15169 &mut self,
15170 hiddens: &mut [f32],
15171 pos0: usize,
15172 b: usize,
15173 prefill: bool,
15174 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15175 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
15179 ) -> MetalRowsRun {
15180 use crate::gpu_metal::{GraphDims, VerifyGraph};
15181 if !crate::gpu_metal::wait_replay() {
15187 tracing::error!("Metal rows graph: the pending async replay failed");
15188 return MetalRowsRun::Failed;
15189 }
15190 spec_stamp("v.wait");
15191 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
15197 if want > 0 {
15198 let phys = self.physical_layers.max(1);
15199 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
15200 let is_gdn = self
15201 .weights
15202 .layers
15203 .get(li % phys)
15204 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
15205 if is_gdn && l.linear_state.len() != want {
15206 l.linear_state = vec![0f32; want];
15207 }
15208 }
15209 }
15210 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
15211 return MetalRowsRun::Declined;
15212 };
15213 spec_stamp("v.plan");
15214 let dims = GraphDims {
15215 hidden: self.hidden_size,
15216 eps: self.rms_eps as f32,
15217 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15218 };
15219 let Some(mut graph) = (if prefill {
15220 VerifyGraph::new_prefill(&model, dims, hiddens, b)
15221 } else {
15222 VerifyGraph::new(&model, dims, hiddens, b)
15223 }) else {
15224 return MetalRowsRun::Declined;
15225 };
15226 let geom = (
15227 self.num_heads,
15228 self.num_kv_heads,
15229 self.head_dim,
15230 self.rotary_dim,
15231 );
15232 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15233 let eps = self.rms_eps as f32;
15234 let kv_id = self.graph_kv_id;
15235 let inv_freq = self.inv_freq.clone();
15236 for item in &plan {
15237 let ok = match item {
15238 MetalRowsItem::Gdn { run, .. } => gcfg
15239 .as_ref()
15240 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
15241 .unwrap_or(false),
15242 MetalRowsItem::Attn {
15243 l,
15244 li,
15245 q_norm,
15246 k_norm,
15247 output_gate,
15248 } => {
15249 let (p, _) = Self::metal_attn_params(
15250 *li,
15251 &self.kv_cache.layers[*li],
15252 *q_norm,
15253 *k_norm,
15254 *output_gate,
15255 &inv_freq,
15256 self.layer_rope_scale(*li),
15257 geom,
15258 pos0,
15259 kv_id,
15260 self.attn_scale,
15261 eps,
15262 gemma,
15263 self.qk_norm_after_rope,
15264 );
15265 graph.attn_ok(l, &p)
15266 }
15267 };
15268 if !ok {
15269 use std::sync::atomic::{AtomicBool, Ordering};
15270 static SAID: AtomicBool = AtomicBool::new(false);
15271 if !SAID.swap(true, Ordering::Relaxed) {
15272 tracing::warn!("metal rows graph: a layer failed preflight — declining");
15273 }
15274 return MetalRowsRun::Declined;
15275 }
15276 }
15277 let lm = match &spec {
15278 Some((lm, _, _)) => {
15279 if !graph.lm_head_ok(*lm) {
15280 return MetalRowsRun::Declined;
15281 }
15282 Some(*lm)
15283 }
15284 None => None,
15285 };
15286 let mut gdn_layers = Vec::new();
15287 let mut attn_layers = Vec::new();
15288 for item in &plan {
15289 match item {
15290 MetalRowsItem::Gdn { run, first } => {
15291 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
15292 .iter()
15293 .map(|l| l.linear_state.as_slice())
15294 .collect();
15295 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
15296 return MetalRowsRun::Declined;
15297 }
15298 gdn_layers.extend(*first..*first + run.len());
15299 }
15300 MetalRowsItem::Attn {
15301 l,
15302 li,
15303 q_norm,
15304 k_norm,
15305 output_gate,
15306 } => {
15307 let (p, cpu_stored) = Self::metal_attn_params(
15308 *li,
15309 &self.kv_cache.layers[*li],
15310 *q_norm,
15311 *k_norm,
15312 *output_gate,
15313 &inv_freq,
15314 self.layer_rope_scale(*li),
15315 geom,
15316 pos0,
15317 kv_id,
15318 self.attn_scale,
15319 eps,
15320 gemma,
15321 self.qk_norm_after_rope,
15322 );
15323 if !graph.encode_attn_b(l, &p) {
15324 return MetalRowsRun::Declined;
15325 }
15326 attn_layers.push((*li, cpu_stored));
15327 }
15328 }
15329 }
15330 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
15331 if !graph.encode_lm_head_b(final_norm, lm) {
15332 return MetalRowsRun::Declined;
15333 }
15334 if let Some((n, _)) = argmax_out.as_ref() {
15339 if !graph.encode_argmax_b(*n) {
15340 argmax_out = None;
15341 }
15342 }
15343 }
15344 spec_stamp("v.enc");
15345 if !graph.sync() {
15346 return MetalRowsRun::Failed;
15347 }
15348 spec_stamp("v.gpu");
15349 match (spec, argmax_out) {
15350 (Some(_), Some((_, ids))) => {
15351 ids.resize(b, 0);
15352 if !graph.read_argmax(ids) {
15353 return MetalRowsRun::Failed;
15354 }
15355 spec_stamp("v.am");
15356 }
15357 (Some((lm, _, logits)), None) => {
15358 logits.resize(b * lm.1, 0.0);
15359 if !graph.read_logits(logits) {
15360 return MetalRowsRun::Failed;
15361 }
15362 spec_stamp("v.lg");
15363 }
15364 (None, _) => {}
15365 }
15366 if !graph.read_hidden(hiddens) {
15367 return MetalRowsRun::Failed;
15368 }
15369 spec_stamp("v.hid");
15370 MetalRowsRun::Completed(MetalVerifyPending {
15371 graph,
15372 gdn_layers,
15373 attn_layers,
15374 })
15375 }
15376
15377 #[cfg(target_os = "macos")]
15383 fn try_batch_graph_metal(
15384 &mut self,
15385 hiddens: &mut [f32],
15386 positions: &[usize],
15387 b: usize,
15388 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15389 argmax_out: Option<(usize, &mut Vec<u32>)>,
15390 ) -> crate::gpu::BatchGraphOutcome {
15391 let _t0 = std::time::Instant::now();
15392 if positions.len() != b
15393 || self.mhc.is_some()
15394 || self.prompt_mrope.is_some()
15395 || positions.windows(2).any(|w| w[1] != w[0] + 1)
15396 || hiddens.len() != b * self.hidden_size
15397 {
15398 return crate::gpu::BatchGraphOutcome::Declined;
15399 }
15400 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
15401 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
15402 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
15403 MetalRowsRun::Completed(pending) => pending,
15404 };
15405 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15406 eprintln!(
15407 "metal-verify: {:.1} ms | b={b}",
15408 _t0.elapsed().as_secs_f64() * 1e3
15409 );
15410 }
15411 self.metal_verify = Some(pending);
15412 crate::gpu::BatchGraphOutcome::Completed
15413 }
15414
15415 #[cfg(target_os = "macos")]
15420 fn prefill_rows_metal(
15421 &mut self,
15422 ids: &[u32],
15423 start_pos: usize,
15424 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
15425 ) -> MetalPrefillOutcome {
15426 let b = ids.len();
15427 if b == 0 || b > 512 {
15428 return MetalPrefillOutcome::Declined;
15429 }
15430 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15431 let with_head = spec.is_some();
15432 let hs = self.hidden_size;
15433 let mut hiddens = vec![0f32; b * hs];
15434 for (j, &id) in ids.iter().enumerate() {
15435 let e = self.embed_single(id);
15436 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
15437 }
15438 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
15439 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
15440 MetalRowsRun::Failed => {
15441 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15442 return MetalPrefillOutcome::Failed;
15443 }
15444 MetalRowsRun::Completed(pending) => pending,
15445 };
15446 let idxs = pending.gdn_layers.clone();
15448 let mut outs: Vec<&mut [f32]> = self
15449 .kv_cache
15450 .layers
15451 .iter_mut()
15452 .enumerate()
15453 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15454 .map(|(_, l)| l.linear_state.as_mut_slice())
15455 .collect();
15456 if !pending.graph.finish_states(&mut outs) {
15457 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15458 return MetalPrefillOutcome::Failed;
15459 }
15460 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15461 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15465 for (li, cpu_stored) in &pending.attn_layers {
15466 let mut kbuf = vec![0f32; b * nkv * hd];
15467 let mut vbuf = vec![0f32; b * nkv * hd];
15468 if !crate::gpu_metal::kv_mirror_read_rows(
15469 self.graph_kv_id,
15470 *li,
15471 nkv,
15472 hd,
15473 *cpu_stored,
15474 b,
15475 &mut kbuf,
15476 &mut vbuf,
15477 ) {
15478 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
15479 return MetalPrefillOutcome::Failed;
15480 }
15481 rows.push((*li, *cpu_stored, kbuf, vbuf));
15482 }
15483 for (li, cpu_stored, kbuf, vbuf) in rows {
15484 let cache = &mut self.kv_cache.layers[li];
15485 for r in 0..b {
15486 cache.append(
15487 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15488 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15489 &[],
15490 );
15491 }
15492 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
15493 }
15494 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15495 if with_head {
15496 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
15497 }
15498 MetalPrefillOutcome::Completed(hiddens)
15499 }
15500
15501 #[cfg(target_os = "macos")]
15502 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
15503 self.prefill_rows_metal(ids, start_pos, None)
15504 }
15505
15506 #[cfg(target_os = "macos")]
15511 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
15512 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
15513 return MetalBatchNllOutcome::Declined;
15514 }
15515 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
15516 return MetalBatchNllOutcome::Declined;
15517 };
15518 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
15519 .ok()
15520 .and_then(|v| v.parse::<usize>().ok())
15521 .filter(|&v| (1..=512).contains(&v))
15522 .unwrap_or(32);
15523 let final_norm = self.weights.final_norm.clone();
15524 let mut nll = 0.0f64;
15525 let mut count = 0usize;
15526 let mut pos = 0usize;
15527 let mut completed = 0usize;
15528 while pos < ids.len() {
15529 let end = (pos + chunk).min(ids.len());
15530 let mut logits = Vec::new();
15531 let outcome =
15532 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
15533 match outcome {
15534 MetalPrefillOutcome::Declined => {
15535 return if completed == 0 {
15536 MetalBatchNllOutcome::Declined
15537 } else {
15538 MetalBatchNllOutcome::Failed(format!(
15539 "ordinary Metal NLL batch declined after {completed} chunks"
15540 ))
15541 };
15542 }
15543 MetalPrefillOutcome::Failed => {
15544 return MetalBatchNllOutcome::Failed(
15545 "ordinary Metal NLL batch failed after admission".to_string(),
15546 );
15547 }
15548 MetalPrefillOutcome::Completed(_) => {}
15549 }
15550 completed += 1;
15551 let vocab = self.vocab_size.min(lm.1);
15552 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
15553 return MetalBatchNllOutcome::Failed(
15554 "ordinary Metal NLL head returned an invalid shape".to_string(),
15555 );
15556 }
15557 for row in 0..(end - pos) {
15558 let absolute = pos + row;
15559 if absolute < start || absolute + 1 >= ids.len() {
15560 continue;
15561 }
15562 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
15563 if let Some(mu) = self.logit_multiplier {
15564 for v in lg.iter_mut() {
15565 *v *= mu;
15566 }
15567 }
15568 if let Some(c) = self.final_softcap {
15569 softcap_in_place(lg, c, self.pool.as_deref());
15570 }
15571 let target = ids[absolute + 1] as usize;
15572 if target >= vocab {
15573 return MetalBatchNllOutcome::Failed(format!(
15574 "target token {target} exceeds Metal head rows {vocab}"
15575 ));
15576 }
15577 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
15578 let lse: f64 = lg
15579 .iter()
15580 .map(|&v| ((v - max) as f64).exp())
15581 .sum::<f64>()
15582 .ln()
15583 + max as f64;
15584 nll += lse - lg[target] as f64;
15585 count += 1;
15586 }
15587 pos = end;
15588 }
15589 MetalBatchNllOutcome::Completed(nll, count)
15590 }
15591
15592 #[cfg(target_os = "macos")]
15596 fn metal_verify_commit(&mut self, a: usize) -> bool {
15597 let Some(mut pending) = self.metal_verify.take() else {
15598 return false;
15599 };
15600 let n = a + 1;
15601 let idxs = pending.gdn_layers.clone();
15603 let mut outs: Vec<&mut [f32]> = self
15604 .kv_cache
15605 .layers
15606 .iter_mut()
15607 .enumerate()
15608 .filter(|(i, _)| idxs.binary_search(i).is_ok())
15609 .map(|(_, l)| l.linear_state.as_mut_slice())
15610 .collect();
15611 if !pending.graph.commit(n, &mut outs) {
15612 return false;
15613 }
15614 spec_stamp("c.replay");
15615 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15616 let mut rows = Vec::with_capacity(pending.attn_layers.len());
15620 for (li, cpu_stored) in &pending.attn_layers {
15621 let mut kbuf = vec![0f32; n * nkv * hd];
15622 let mut vbuf = vec![0f32; n * nkv * hd];
15623 if !crate::gpu_metal::kv_mirror_read_rows(
15624 self.graph_kv_id,
15625 *li,
15626 nkv,
15627 hd,
15628 *cpu_stored,
15629 n,
15630 &mut kbuf,
15631 &mut vbuf,
15632 ) {
15633 return false;
15634 }
15635 rows.push((*li, *cpu_stored, kbuf, vbuf));
15636 }
15637 for (li, cpu_stored, kbuf, vbuf) in rows {
15638 let cache = &mut self.kv_cache.layers[li];
15639 for r in 0..n {
15640 cache.append(
15641 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15642 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15643 &[],
15644 );
15645 }
15646 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
15647 }
15648 spec_stamp("c.kv");
15649 true
15650 }
15651
15652 #[cfg(target_os = "macos")]
15659 fn mtp_warm_batch_submit(
15660 &mut self,
15661 m: &mut MtpModule,
15662 pairs: &[(&[f32], u32)],
15663 first_pos: usize,
15664 ) -> Option<MetalWarmPending> {
15665 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
15666 let b = pairs.len();
15667 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
15668 return None;
15669 }
15670 let AttnKind::Full {
15671 wq,
15672 wk,
15673 wv,
15674 wo,
15675 q_norm,
15676 k_norm,
15677 output_gate,
15678 softplus_gate: None,
15679 bias: None,
15680 } = &m.layer.attn
15681 else {
15682 return None;
15683 };
15684 let FfnKind::Dense(d) = &m.layer.ffn else {
15685 return None;
15686 };
15687 if !d.segs.is_empty() {
15688 return None;
15689 }
15690 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15691 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15692 else {
15693 return None;
15694 };
15695 let (Some(g), Some(u), Some(dn)) = (
15696 d.gate_proj.q1_parts(),
15697 d.up_proj.q1_parts(),
15698 d.down_proj.q1_parts(),
15699 ) else {
15700 return None;
15701 };
15702 let Some(eh) = m.eh_proj.q1_parts() else {
15703 return None;
15704 };
15705 let QTensor::Mapped { model, .. } = wq else {
15706 return None;
15707 };
15708 let model = model.clone();
15709 let hs = self.hidden_size;
15710 let mut cat = vec![0f32; b * 2 * hs];
15712 for (j, (h, tok)) in pairs.iter().enumerate() {
15713 let e = self.embed_single(*tok);
15714 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
15715 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
15716 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
15717 }
15718 let dims = GraphDims {
15719 hidden: hs,
15720 eps: self.rms_eps as f32,
15721 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15722 };
15723 spec_stamp("w.cat");
15724 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
15725 return None;
15726 };
15727 spec_stamp("w.new");
15728 let l = AttnGpuLayer {
15729 mhc: None,
15730 attn_norm: &m.layer.input_norm,
15731 post_norm: &m.layer.post_norm,
15732 wq: pq,
15733 wk: pk,
15734 wv: pv,
15735 wo: po,
15736 ffn: MetalFfn::Dense {
15737 gate: g,
15738 up: u,
15739 down: dn,
15740 act: (d.act == Act::Gelu) as u8,
15741 },
15742 attn_out_norm: None,
15743 ffn_out_norm: None,
15744 layer_scale: None,
15745 };
15746 let (nh, nkv, hd, rd) = (
15747 self.num_heads,
15748 self.num_kv_heads,
15749 self.head_dim,
15750 self.rotary_dim,
15751 );
15752 let inv_freq = self.inv_freq.clone();
15753 let cpu_stored;
15754 {
15755 let cache = &m.kv;
15756 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15757 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15758 cpu_stored = cpu_k[0].len() / hd;
15759 if cpu_stored > first_pos {
15764 spec_stamp("w.decl");
15765 return None;
15766 }
15767 let p = AttnDeviceParams {
15768 kv_id: self.mtp_kv_id(),
15769 layer: Self::MTP_LAYER_BASE,
15770 nh,
15771 nkv,
15772 hd,
15773 rd,
15774 position: first_pos,
15775 scale: self.attn_scale,
15776 eps: self.rms_eps as f32,
15777 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15778 late_qk_norm: self.qk_norm_after_rope,
15779 output_gate: *output_gate,
15780 q_norm: q_norm.as_deref(),
15781 k_norm: k_norm.as_deref(),
15782 inv_freq: &inv_freq,
15783 rope_scale: self.rope_scale,
15784 cpu_k,
15785 cpu_v,
15786 cpu_stored,
15787 cpu_gen: cache.generation(),
15788 o1: None,
15789 window: None,
15790 softmax_sink: None,
15791 head_gate: None,
15792 v_norm: false,
15793 };
15794 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
15795 return None;
15796 }
15797 }
15798 spec_stamp("w.enc");
15799 if !graph.submit() {
15800 return None;
15801 }
15802 spec_stamp("w.sub");
15803 Some(MetalWarmPending {
15804 graph,
15805 cpu_stored,
15806 b,
15807 })
15808 }
15809
15810 #[cfg(target_os = "macos")]
15813 fn mtp_warm_batch_metal(
15814 &mut self,
15815 m: &mut MtpModule,
15816 pairs: &[(&[f32], u32)],
15817 first_pos: usize,
15818 ) -> bool {
15819 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
15820 Some(p) => self.mtp_warm_batch_finish(m, p),
15821 None => false,
15822 }
15823 }
15824
15825 #[cfg(target_os = "macos")]
15830 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
15831 let MetalWarmPending {
15832 mut graph,
15833 cpu_stored,
15834 b,
15835 } = pending;
15836 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
15837 if !graph.sync() {
15838 return false;
15839 }
15840 spec_stamp("w.gpu");
15841 let mut kbuf = vec![0f32; b * nkv * hd];
15842 let mut vbuf = vec![0f32; b * nkv * hd];
15843 if !crate::gpu_metal::kv_mirror_read_rows(
15844 self.mtp_kv_id(),
15845 Self::MTP_LAYER_BASE,
15846 nkv,
15847 hd,
15848 cpu_stored,
15849 b,
15850 &mut kbuf,
15851 &mut vbuf,
15852 ) {
15853 return false;
15854 }
15855 for r in 0..b {
15856 m.kv.append(
15857 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15858 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15859 &[],
15860 );
15861 }
15862 crate::gpu_metal::kv_mirror_set_stored(
15863 self.mtp_kv_id(),
15864 Self::MTP_LAYER_BASE,
15865 cpu_stored + b,
15866 );
15867 spec_stamp("w.kv");
15868 true
15869 }
15870
15871 pub(crate) fn note_draft_id(&mut self, id: u32) {
15878 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
15879 if (id as usize) >= cut {
15880 self.draft_full_streak = 16;
15881 } else {
15882 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
15883 }
15884 }
15885
15886 fn draft_head_rows(&self, head_rows: usize) -> usize {
15889 if self.draft_full_streak > 0 {
15890 head_rows
15891 } else {
15892 Self::draft_vocab_rows(head_rows)
15893 }
15894 }
15895
15896 fn draft_vocab_rows(head_rows: usize) -> usize {
15899 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
15900 let n = *N.get_or_init(|| {
15901 std::env::var("CMF_DRAFT_VOCAB")
15902 .ok()
15903 .and_then(|v| v.parse().ok())
15904 .unwrap_or(65536)
15905 });
15906 if n == 0 { head_rows } else { n.min(head_rows) }
15907 }
15908
15909 #[cfg(target_os = "macos")]
15914 fn mtp_step_metal(
15915 &mut self,
15916 m: &mut MtpModule,
15917 hidden: &[f32],
15918 next_token: u32,
15919 position: usize,
15920 want_logits: bool,
15921 ) -> Option<(Vec<f32>, Vec<f32>)> {
15922 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15923 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15924 || !crate::gpu::q1_force()
15925 || !crate::gpu::enabled_here()
15926 || self.attn_softcap > 0.0
15927 || self.attention_heads_per_layer.is_some()
15928 || m.kv.mode != crate::kv_cache::KvMode::F32
15929 || m.kv.o1.is_some()
15930 {
15931 return None;
15932 }
15933 let AttnKind::Full {
15934 wq,
15935 wk,
15936 wv,
15937 wo,
15938 q_norm,
15939 k_norm,
15940 output_gate,
15941 softplus_gate: None,
15942 bias: None,
15943 } = &m.layer.attn
15944 else {
15945 return None;
15946 };
15947 let FfnKind::Dense(d) = &m.layer.ffn else {
15948 return None;
15949 };
15950 if d.act != Act::Silu || !d.segs.is_empty() {
15951 return None;
15952 }
15953 let (pq, pk, pv, po) = (
15954 wq.q1_parts()?,
15955 wk.q1_parts()?,
15956 wv.q1_parts()?,
15957 wo.q1_parts()?,
15958 );
15959 let (g, u, dn) = (
15960 d.gate_proj.q1_parts()?,
15961 d.up_proj.q1_parts()?,
15962 d.down_proj.q1_parts()?,
15963 );
15964 let QTensor::Mapped { model, .. } = wq else {
15965 return None;
15966 };
15967 let model = model.clone();
15968 let lm = if want_logits {
15969 Some(self.weights.lm_head.q1_parts()?)
15970 } else {
15971 None
15972 };
15973 let dims = GraphDims {
15974 hidden: self.hidden_size,
15975 eps: self.rms_eps as f32,
15976 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15977 };
15978 let hs = self.hidden_size;
15981 let mut x = vec![0f32; hs];
15982 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15989 crate::gpu_metal::DENSE_MV,
15990 );
15991 let mut graph = TokenGraph::new(&model, dims, &x)?;
15992 let mut folded = false;
15993 if let Some(eh) = m.eh_proj.q1_parts() {
15994 let e = self.embed_single(next_token);
15995 let mut cat = vec![0.0f32; 2 * hs];
15996 let (cat_e, cat_h) = cat.split_at_mut(hs);
15997 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
15998 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
15999 folded = graph.encode_input_proj(eh, &cat);
16000 }
16001 if !folded {
16002 x = self.mtp_block_input(m, hidden, next_token);
16003 graph = TokenGraph::new(&model, dims, &x)?;
16004 }
16005 spec_stamp("d.in");
16006 let l = AttnGpuLayer {
16007 mhc: None,
16008 attn_norm: &m.layer.input_norm,
16009 post_norm: &m.layer.post_norm,
16010 wq: pq,
16011 wk: pk,
16012 wv: pv,
16013 wo: po,
16014 ffn: MetalFfn::Dense {
16015 gate: g,
16016 up: u,
16017 down: dn,
16018 act: (d.act == Act::Gelu) as u8,
16019 },
16020 attn_out_norm: None,
16021 ffn_out_norm: None,
16022 layer_scale: None,
16023 };
16024 let (nh, nkv, hd, rd) = (
16025 self.num_heads,
16026 self.num_kv_heads,
16027 self.head_dim,
16028 self.rotary_dim,
16029 );
16030 let inv_freq = self.inv_freq.clone();
16031 {
16032 let cache = &m.kv;
16033 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16034 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16035 let cpu_stored = cpu_k[0].len() / hd;
16036 let p = AttnDeviceParams {
16037 kv_id: self.mtp_kv_id(),
16038 layer: Self::MTP_LAYER_BASE,
16039 nh,
16040 nkv,
16041 hd,
16042 rd,
16043 position,
16044 scale: self.attn_scale,
16045 eps: self.rms_eps as f32,
16046 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16047 late_qk_norm: self.qk_norm_after_rope,
16048 output_gate: *output_gate,
16049 q_norm: q_norm.as_deref(),
16050 k_norm: k_norm.as_deref(),
16051 inv_freq: &inv_freq,
16052 rope_scale: self.rope_scale,
16053 cpu_k,
16054 cpu_v,
16055 cpu_stored,
16056 cpu_gen: cache.generation(),
16057 o1: None,
16058 window: None,
16059 softmax_sink: None,
16060 head_gate: None,
16061 v_norm: false,
16062 };
16063 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16064 return None;
16065 }
16066 }
16067 let draft_rows = if let Some(lm) = lm {
16073 self.draft_head_rows(lm.1)
16074 } else {
16075 0
16076 };
16077 if let Some(lm) = lm {
16078 if !graph.lm_head_ok(lm) {
16079 return None;
16080 }
16081 if draft_rows < lm.1 {
16082 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16083 return None;
16084 }
16085 } else {
16086 graph.encode_lm_head(&m.final_norm, lm);
16087 }
16088 }
16089 spec_stamp("d.enc");
16090 if graph.sync_checked().is_err() {
16091 return None;
16092 }
16093 spec_stamp("d.gpu");
16094 let mut logits = Vec::new();
16095 if let Some(lm) = lm {
16096 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
16097 logits = attention::take_buf(n_read);
16098 graph.read_logits(&mut logits);
16099 pad_mtp_draft_logits(&mut logits, self.vocab_size);
16101 }
16102 graph.finish(&mut x);
16103 let mut krow = attention::take_buf(nkv * hd);
16104 let mut vrow = attention::take_buf(nkv * hd);
16105 if crate::gpu_metal::kv_mirror_read_last(
16106 self.mtp_kv_id(),
16107 Self::MTP_LAYER_BASE,
16108 nkv,
16109 hd,
16110 &mut krow,
16111 &mut vrow,
16112 ) {
16113 m.kv.append(&krow, &vrow, &[]);
16114 }
16115 attention::recycle_buf(&mut krow);
16116 attention::recycle_buf(&mut vrow);
16117 spec_stamp("d.rd");
16118 Some((logits, x))
16119 }
16120
16121 fn mtp_chain_on() -> bool {
16134 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16135 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
16136 }
16137
16138 #[cfg(target_os = "macos")]
16150 fn mtp_draft_chain_metal(
16151 &mut self,
16152 m: &mut MtpModule,
16153 hidden: &[f32],
16154 t_next: u32,
16155 position: usize,
16156 k: usize,
16157 ) -> Result<Vec<u32>, bool> {
16158 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
16159 if k == 0
16160 || k > 64
16161 || !Self::mtp_chain_on()
16162 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
16163 || !crate::gpu::q1_force()
16164 || !crate::gpu::enabled_here()
16165 || self.attn_softcap > 0.0
16166 || self.attention_heads_per_layer.is_some()
16167 || m.kv.mode != crate::kv_cache::KvMode::F32
16168 || m.kv.o1.is_some()
16169 || self.dsv4.is_some()
16171 || self.dsv41.is_some()
16172 || self.qwen4_exp.is_some()
16173 || self.per_token_stack()
16174 {
16175 return Err(false);
16176 }
16177 let AttnKind::Full {
16178 wq,
16179 wk,
16180 wv,
16181 wo,
16182 q_norm,
16183 k_norm,
16184 output_gate,
16185 softplus_gate: None,
16186 bias: None,
16187 } = &m.layer.attn
16188 else {
16189 return Err(false);
16190 };
16191 let FfnKind::Dense(d) = &m.layer.ffn else {
16192 return Err(false);
16193 };
16194 if d.act != Act::Silu || !d.segs.is_empty() {
16195 return Err(false);
16196 }
16197 let (Some(pq), Some(pk), Some(pv), Some(po)) =
16198 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
16199 else {
16200 return Err(false);
16201 };
16202 let (Some(g), Some(u), Some(dn)) = (
16203 d.gate_proj.q1_parts(),
16204 d.up_proj.q1_parts(),
16205 d.down_proj.q1_parts(),
16206 ) else {
16207 return Err(false);
16208 };
16209 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
16210 return Err(false);
16211 };
16212 let QTensor::Mapped { model, .. } = wq else {
16213 return Err(false);
16214 };
16215 let model = model.clone();
16216 let QTensor::Mapped {
16219 model: em,
16220 idx: eidx,
16221 dtype: cortiq_core::TensorDtype::Q4TiledP,
16222 ..
16223 } = &self.weights.embed_tokens
16224 else {
16225 return Err(false);
16226 };
16227 if !std::sync::Arc::ptr_eq(em, &model)
16228 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
16229 {
16230 return Err(false);
16231 }
16232 let embed = (
16233 *eidx,
16234 self.weights.embed_tokens.rows(),
16235 self.weights.embed_tokens.cols(),
16236 );
16237 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
16238 return Err(false);
16239 }
16240 let dims = GraphDims {
16241 hidden: self.hidden_size,
16242 eps: self.rms_eps as f32,
16243 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16244 };
16245 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
16249 crate::gpu_metal::DENSE_MV,
16250 );
16251 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
16252 return Err(false);
16253 };
16254 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
16255 return Err(false);
16256 }
16257 let l = AttnGpuLayer {
16258 mhc: None,
16259 attn_norm: &m.layer.input_norm,
16260 post_norm: &m.layer.post_norm,
16261 wq: pq,
16262 wk: pk,
16263 wv: pv,
16264 wo: po,
16265 ffn: MetalFfn::Dense {
16266 gate: g,
16267 up: u,
16268 down: dn,
16269 act: (d.act == Act::Gelu) as u8,
16270 },
16271 attn_out_norm: None,
16272 ffn_out_norm: None,
16273 layer_scale: None,
16274 };
16275 let (nh, nkv, hd, rd) = (
16276 self.num_heads,
16277 self.num_kv_heads,
16278 self.head_dim,
16279 self.rotary_dim,
16280 );
16281 let inv_freq = self.inv_freq.clone();
16282 let draft_rows = self.draft_head_rows(lm.1);
16283 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
16284 if n_arg == 0 {
16285 return Err(false);
16286 }
16287 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
16294 let t_chain = std::time::Instant::now();
16295 graph.chain_ids_init(t_next, k);
16296 let cpu_stored;
16297 {
16298 let cache = &m.kv;
16299 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
16300 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
16301 cpu_stored = cpu_k[0].len() / hd;
16302 for j in 0..k {
16303 if !graph.encode_chain_input(
16304 embed,
16305 j as u32,
16306 &m.enorm,
16307 &m.hnorm,
16308 self.embed_multiplier,
16309 eh,
16310 ) {
16311 return Err(false);
16312 }
16313 let p = AttnDeviceParams {
16317 kv_id: self.mtp_kv_id(),
16318 layer: Self::MTP_LAYER_BASE,
16319 nh,
16320 nkv,
16321 hd,
16322 rd,
16323 position: position + j,
16324 scale: self.attn_scale,
16325 eps: self.rms_eps as f32,
16326 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
16327 late_qk_norm: self.qk_norm_after_rope,
16328 output_gate: *output_gate,
16329 q_norm: q_norm.as_deref(),
16330 k_norm: k_norm.as_deref(),
16331 inv_freq: &inv_freq,
16332 rope_scale: self.rope_scale,
16333 cpu_k: cpu_k.clone(),
16334 cpu_v: cpu_v.clone(),
16335 cpu_stored: cpu_stored + j,
16336 cpu_gen: cache.generation(),
16337 o1: None,
16338 window: None,
16339 softmax_sink: None,
16340 head_gate: None,
16341 v_norm: false,
16342 };
16343 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
16344 return Err(false);
16345 }
16346 if draft_rows < lm.1 {
16347 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
16348 return Err(false);
16349 }
16350 } else {
16351 graph.encode_lm_head(&m.final_norm, lm);
16352 }
16353 if !graph.encode_argmax(n_arg, j as u32 + 1) {
16354 return Err(false);
16355 }
16356 if split {
16357 graph.commit();
16360 }
16361 }
16362 }
16363 let t_enc = t_chain.elapsed();
16364 if graph.sync_checked().is_err() {
16365 return Err(true);
16366 }
16367 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
16368 eprintln!(
16369 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
16370 t_enc.as_secs_f64() * 1e3,
16371 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
16372 if split { ", split" } else { "" }
16373 );
16374 }
16375 let mut ids = vec![0u32; k];
16376 if !graph.chain_ids_read(&mut ids) {
16377 return Err(true);
16378 }
16379 let mut kbuf = vec![0f32; k * nkv * hd];
16380 let mut vbuf = vec![0f32; k * nkv * hd];
16381 if !crate::gpu_metal::kv_mirror_read_rows(
16382 self.mtp_kv_id(),
16383 Self::MTP_LAYER_BASE,
16384 nkv,
16385 hd,
16386 cpu_stored,
16387 k,
16388 &mut kbuf,
16389 &mut vbuf,
16390 ) {
16391 return Err(true);
16392 }
16393 for r in 0..k {
16394 m.kv.append(
16395 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
16396 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
16397 &[],
16398 );
16399 }
16400 Ok(ids)
16401 }
16402
16403 fn try_batch_graph_wgpu(
16404 &self,
16405 hiddens: &mut [f32],
16406 positions: &[usize],
16407 k: usize,
16408 spec: Option<crate::gpu::SpecTail<'_>>,
16409 ) -> crate::gpu::BatchGraphOutcome {
16410 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
16411 }
16412
16413 fn try_batch_graph_wgpu_prefix(
16418 &self,
16419 hiddens: &mut [f32],
16420 positions: &[usize],
16421 k: usize,
16422 spec: Option<crate::gpu::SpecTail<'_>>,
16423 layers_run: Option<&mut usize>,
16424 ) -> crate::gpu::BatchGraphOutcome {
16425 if self.mhc.as_ref().is_some_and(|m| {
16429 m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
16430 })
16431 {
16432 return crate::gpu::BatchGraphOutcome::Declined;
16433 }
16434 let graph_end = match self.mimo_moe.graph_prefix_end() {
16435 Some(end) if end < self.num_layers => {
16436 if layers_run.is_none() || spec.is_some() || end == 0 {
16437 return crate::gpu::BatchGraphOutcome::Declined;
16438 }
16439 end
16440 }
16441 _ => self.num_layers,
16442 };
16443 let _tb = std::time::Instant::now();
16444 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
16445 if self.attn_softcap > 0.0 {
16446 return crate::gpu::BatchGraphOutcome::Declined; }
16448 if let Some(reason) = self
16451 .wgpu_graph_attn_decline()
16452 .or_else(|| self.batch_graph_gemma_decline())
16453 {
16454 self.note_graph_decline("wgpu batch graph", reason);
16455 return crate::gpu::BatchGraphOutcome::Declined;
16456 }
16457 let nh = self.num_heads;
16458 let (nkv, hd, rd) = self.layer_geom(0);
16459 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
16460 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
16461 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
16462 {
16463 let name = &m.tensors[i].name;
16464 let prism = if crate::prism::is_inverse_embedding(m, name) {
16465 crate::gpu::GraphPrismOp::InverseEmbedding
16466 } else if crate::prism::is_forward_weight(m, name) {
16467 crate::gpu::GraphPrismOp::Forward
16468 } else {
16469 crate::gpu::GraphPrismOp::None
16470 };
16471 return Some(crate::gpu::GraphW {
16472 idx: i,
16473 kind,
16474 row_scale: rs,
16475 data: &[],
16476 prism,
16477 affine: crate::prism::is_affine_target(m, name),
16478 });
16479 }
16480 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
16481 eprintln!(
16482 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
16483 t.rows(),
16484 t.cols()
16485 );
16486 }
16487 t.as_f32().map(|d| crate::gpu::GraphW {
16488 idx: 0,
16489 kind: 4,
16490 row_scale: &[],
16491 data: d,
16492 prism: crate::gpu::GraphPrismOp::None,
16493 affine: false,
16494 })
16495 }
16496 let built: Option<(
16497 Vec<crate::gpu::GraphLayer<'_>>,
16498 std::sync::Arc<cortiq_core::CmfModel>,
16499 )> = (|| {
16500 let mut layers = Vec::with_capacity(graph_end);
16501 let mut model = None;
16502 for li in 0..graph_end {
16503 let lw = &self.weights.layers[self.phys_layer(li)];
16504 let gffn = match &lw.ffn {
16511 FfnKind::Dense(d) if !d.segs.is_empty() => {
16512 if batch_debug {
16513 eprintln!("batch graph: dense segmented FFN at layer {li}");
16514 }
16515 return None;
16516 }
16517 FfnKind::Dense(d) => {
16518 let Some(act) = d.act.wgpu_graph_act() else {
16519 if batch_debug {
16520 eprintln!(
16521 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
16522 d.act
16523 );
16524 }
16525 return None;
16526 };
16527 crate::gpu::GraphFfn::Dense {
16528 gate: gw(&d.gate_proj)?,
16529 up: gw(&d.up_proj)?,
16530 down: gw(&d.down_proj)?,
16531 act,
16532 }
16533 }
16534 FfnKind::Moe(m) => {
16535 if m.route_tau.is_some() || m.mask.is_some() {
16542 return None;
16543 }
16544 let shared = m.shared.as_ref();
16548 let has_shared = shared.is_some();
16549 let shared_gated = matches!(shared, Some((_, Some(_))));
16550 let sgate = match shared {
16551 Some((_, Some(sg))) => gw(sg)?,
16552 _ => gw(&m.router)?,
16556 };
16557 let router = gw(&m.router)?;
16558 if router.prism != crate::gpu::GraphPrismOp::None
16564 || router.affine
16565 || sgate.prism != crate::gpu::GraphPrismOp::None
16566 || sgate.affine
16567 {
16568 return None;
16569 }
16570 let inter = m.experts.first()?.gate_proj.rows();
16571 let mut experts = Vec::with_capacity(m.experts.len() + 1);
16572 let mut q4tp: Option<bool> = None;
16573 let mut gu_q2: Option<bool> = None;
16574 let mut q82: Option<bool> = None;
16575 let mut moe_act = None;
16579 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
16580 let Some(e_act) = e.act.wgpu_graph_act() else {
16581 return None;
16582 };
16583 match moe_act {
16584 Some(existing) if existing != e_act => return None,
16585 None => moe_act = Some(e_act),
16586 _ => {}
16587 }
16588 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
16589 return None;
16590 }
16591 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
16595 {
16596 Some((mm, gi)) => (
16597 mm,
16598 gi,
16599 e.up_proj.mapped_q4t()?.1,
16600 e.down_proj.mapped_q4t()?.1,
16601 false,
16602 false,
16603 false,
16604 ),
16605 None => match e.gate_proj.mapped_q2tp() {
16606 Some((mm, gi)) => (
16607 mm,
16608 gi,
16609 e.up_proj.mapped_q2tp()?.1,
16610 e.down_proj.mapped_q4tp()?.1,
16611 true,
16612 true,
16613 false,
16614 ),
16615 None => match e.gate_proj.mapped_q4tp() {
16616 Some((mm, gi)) => (
16617 mm,
16618 gi,
16619 e.up_proj.mapped_q4tp()?.1,
16620 e.down_proj.mapped_q4tp()?.1,
16621 true,
16622 false,
16623 false,
16624 ),
16625 None => {
16626 if !crate::gpu::moe_q82_graph_on() {
16627 return None;
16628 }
16629 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
16630 (
16631 mm,
16632 gi,
16633 e.up_proj.mapped_q8_2f()?.1,
16634 e.down_proj.mapped_q8_2f()?.1,
16635 false,
16636 false,
16637 true,
16638 )
16639 }
16640 },
16641 },
16642 };
16643 if *q4tp.get_or_insert(is_p) != is_p
16644 || *gu_q2.get_or_insert(is_q2) != is_q2
16645 || *q82.get_or_insert(is_8) != is_8
16646 {
16647 return None;
16648 }
16649 if [gi, ui, di].into_iter().any(|idx| {
16650 mm.tensors.get(idx).is_some_and(|t| {
16651 crate::prism::is_forward_weight(mm, &t.name)
16652 || crate::prism::is_affine_target(mm, &t.name)
16653 })
16654 }) {
16655 return None;
16656 }
16657 model.get_or_insert_with(|| mm.clone());
16658 experts.push((gi, ui, di));
16659 }
16660 crate::gpu::GraphFfn::Moe {
16661 router,
16662 shared_gate: sgate,
16663 experts,
16664 n_exp: m.experts.len(),
16665 top_k: m.top_k,
16666 inter,
16667 act: moe_act?,
16668 norm_topk: m.norm_topk_prob,
16669 q4tp: q4tp?,
16670 gu_q2: gu_q2.unwrap_or(false),
16671 q82: q82.unwrap_or(false),
16672 sigmoid: m.router_sigmoid,
16673 sigmoid_denom_eps: m.sigmoid_denom_eps,
16674 bias: m.expert_bias.as_deref(),
16675 has_shared,
16676 detached_shared: None,
16677 shared_gated,
16678 route_scale: m.routed_scaling,
16679 }
16680 }
16681 _ => return None,
16682 };
16683 let attn = match &lw.attn {
16684 AttnKind::Full {
16685 wq,
16686 wk,
16687 wv,
16688 wo,
16689 q_norm,
16690 k_norm,
16691 output_gate,
16692 softplus_gate,
16693 bias,
16694 } => {
16695 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
16696 if batch_debug {
16697 eprintln!(
16698 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
16699 softplus_gate.is_some(),
16700 self.attention_heads_per_layer.is_some()
16701 );
16702 }
16703 return None;
16704 }
16705 let (m, _, _, _) =
16706 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
16707 model = Some(m.clone());
16708 crate::gpu::GraphAttn::Full {
16709 wq: gw(wq)?,
16710 wk: gw(wk)?,
16711 wv: gw(wv)?,
16712 wo: gw(wo)?,
16713 q_norm: q_norm.as_deref(),
16714 k_norm: k_norm.as_deref(),
16715 late_qk_norm: self.qk_norm_after_rope,
16716 bias: bias
16717 .as_ref()
16718 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
16719 output_gate: *output_gate,
16720 cpu_k: self.kv_cache.layers[li].k_heads(),
16721 cpu_v: self.kv_cache.layers[li].v_heads(),
16722 cpu_base: self.kv_cache.layers[li].base(),
16723 geom: self.graph_attn_geom(li),
16724 head_gate: None,
16727 }
16728 }
16729 AttnKind::LinearGdn(w) => {
16730 let Some(cfg) = self.gdn_cfg else {
16731 if batch_debug {
16732 eprintln!("batch graph: no GDN config at layer {li}");
16733 }
16734 return None;
16735 };
16736 let (m, _, _, _) = w
16737 .in_proj_qkv
16738 .graph_weight()
16739 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
16740 model = Some(m.clone());
16741 crate::gpu::GraphAttn::Gdn {
16742 qkv: gw(&w.in_proj_qkv)?,
16743 z: gw(&w.in_proj_z)?,
16744 a: gw(&w.in_proj_a)?,
16745 b: gw(&w.in_proj_b)?,
16746 out: gw(&w.out_proj)?,
16747 conv1d: &w.conv1d,
16748 a_log: &w.a_log,
16749 dt_bias: &w.dt_bias,
16750 norm: &w.norm,
16751 nv: cfg.num_v_heads,
16752 nk: cfg.num_k_heads,
16753 dk: cfg.key_head_dim,
16754 dv: cfg.value_head_dim,
16755 kk: cfg.conv_kernel,
16756 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
16757 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
16758 }
16759 }
16760 _ => return None,
16761 };
16762 let g4 = self.global_attn.is_some() || self.attn_v_norm;
16765 layers.push(crate::gpu::GraphLayer {
16766 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
16767 input_norm: &lw.input_norm,
16768 attn,
16769 post_norm: &lw.post_norm,
16770 attn_out_norm: if g4 {
16771 lw.attn_out_norm.as_deref()
16772 } else {
16773 None
16774 },
16775 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
16776 layer_scale: if g4 { lw.layer_scale } else { None },
16777 ffn: gffn,
16778 });
16779 }
16780 Some((layers, model?))
16781 })();
16782 let Some((layers, model)) = built else {
16783 {
16784 use std::sync::atomic::{AtomicBool, Ordering};
16785 static SAID: AtomicBool = AtomicBool::new(false);
16786 if !SAID.swap(true, Ordering::Relaxed) {
16787 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
16788 }
16789 }
16790 return crate::gpu::BatchGraphOutcome::Declined;
16791 };
16792 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
16793 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
16794 }
16795 #[cfg(feature = "gpu")]
16798 crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
16799 #[cfg(feature = "gpu")]
16801 let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
16802 let half = rd / 2;
16803 let mut tab = vec![0f32; k * 64];
16804 for (i, &p) in positions.iter().enumerate() {
16805 for j in 0..half.min(64) {
16806 let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
16807 tab[i * 64 + j] = a * self.inv_freq[j];
16808 }
16809 }
16810 crate::gpu_wgpu::set_batch_mrope(Some(tab));
16811 struct Clear;
16812 impl Drop for Clear {
16813 fn drop(&mut self) {
16814 crate::gpu_wgpu::set_batch_mrope(None);
16815 }
16816 }
16817 Clear
16818 });
16819 crate::gpu::forward_batch_graph(
16820 &model,
16821 self.graph_kv_id,
16822 &layers,
16823 &self.inv_freq,
16824 hiddens,
16825 nh,
16826 nkv,
16827 hd,
16828 rd,
16829 self.hidden_size,
16830 self.intermediate_size,
16831 positions,
16832 self.kv_cache.max_seq_len,
16833 gemma,
16834 self.rms_eps as f32,
16835 self.attn_scale,
16836 k,
16837 &(0..graph_end)
16838 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
16839 .collect::<Vec<_>>(),
16840 self.o1_epoch,
16841 spec,
16842 layers_run,
16843 )
16844 }
16845
16846 fn draft_probe() -> bool {
16850 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16851 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
16852 }
16853
16854 #[cfg(feature = "gpu")]
16866 fn dsv4_spec_on() -> bool {
16867 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16868 *ON.get_or_init(|| {
16869 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
16873 return v != "0";
16874 }
16875 std::env::var("CMF_DSV4_SPEC")
16882 .map(|v| v != "0")
16883 .unwrap_or_else(|_| {
16884 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
16885 })
16886 })
16887 }
16888
16889 #[cfg(feature = "gpu")]
16896 fn dsv4_spec_step(
16897 &mut self,
16898 tip_token: u32,
16899 t_next: u32,
16900 next_pos: usize,
16901 max_extra: usize,
16902 drafted: &mut usize,
16903 accepted_ctr: &mut usize,
16904 ) -> Option<(Vec<u32>, usize)> {
16905 let t_all = std::time::Instant::now();
16906 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
16907 thread_local! {
16908 static LAST: std::cell::Cell<Option<std::time::Instant>> =
16909 const { std::cell::Cell::new(None) };
16910 }
16911 LAST.with(|l| {
16912 if let Some(prev) = l.get() {
16913 eprintln!(
16914 "between rounds {:.1} ms",
16915 prev.elapsed().as_secs_f64() * 1e3
16916 );
16917 }
16918 l.set(Some(std::time::Instant::now()));
16919 });
16920 }
16921 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16922 eprintln!("spec_step: enter pos={next_pos}");
16923 }
16924 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
16925 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
16926 if self.dspark.is_none() {
16928 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16929 if t.is_empty() {
16930 return None;
16931 }
16932 crate::dsv4::dspark_arm(&t, cfg.dim);
16933 self.dspark = Some(crate::dsv4::DsparkState::new(
16934 self.dsv4_mtp.len(),
16935 &cfg,
16936 t.len(),
16937 ));
16938 }
16939 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16940 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
16941 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
16942 eprintln!("spec_step: the pack was not built (targets {targets:?})");
16943 }
16944 let pack = pack?;
16945 let block = crate::dsv4::dspark_block();
16946 let b_box = self.dsv4.as_mut()?;
16947 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
16948 let ds = self.dspark.as_mut()?;
16949 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
16952 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
16953 if dbg {
16954 eprintln!("spec_step: no capture");
16955 }
16956 return None;
16957 }
16958 ds.have_hidden = true;
16959 let tip_pos = next_pos.checked_sub(1)?;
16960 let draft_started = std::time::Instant::now();
16961 let mut conf = Vec::new();
16962 let props = crate::dsv4::dspark_draft_gpu(
16963 g,
16964 &self.dsv4_mtp,
16965 &cfg,
16966 ds,
16967 pack,
16968 st.kv_id,
16969 tip_token,
16970 tip_pos,
16971 self.pool.as_deref(),
16972 &mut conf,
16973 );
16974 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16975 *drafted += block;
16976 if props.is_empty() || props[0] != t_next {
16977 if dbg {
16978 eprintln!(
16979 "spec_step: draft {} (props0={:?} t_next={t_next})",
16980 if props.is_empty() {
16981 "empty"
16982 } else {
16983 "miss"
16984 },
16985 props.first()
16986 );
16987 }
16988 return None;
16989 }
16990 let mut k_verify = crate::dsv4::dspark_verify_k()
16997 .min(props.len())
16998 .min(max_extra.saturating_add(1));
16999 let conf_min = {
17005 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
17006 *M.get_or_init(|| {
17007 std::env::var("CMF_DSPARK_CONF_MIN")
17008 .ok()
17009 .and_then(|v| v.parse().ok())
17010 .unwrap_or(0.0)
17011 })
17012 };
17013 if conf_min > 0.0 && conf.len() >= props.len() {
17014 let mut keep = 1usize;
17015 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
17016 keep += 1;
17017 }
17018 k_verify = k_verify.min(keep.max(2));
17019 }
17020 if k_verify < 2 {
17021 return None;
17022 }
17023 let mut fed = Vec::with_capacity(k_verify);
17024 fed.push(t_next);
17025 fed.extend_from_slice(&props[1..k_verify]);
17026 let mut argmax = Vec::new();
17027 let mut logits_all = Vec::new();
17028 let mut walked = Vec::new();
17029 let txn = crate::dsv4::dsv4_verify_chunk(
17030 g,
17031 layers,
17032 &cfg,
17033 st,
17034 &fed,
17035 next_pos,
17036 &self.inv_freq,
17037 self.pool.as_deref(),
17038 &targets,
17039 &mut argmax,
17040 &mut logits_all,
17041 &mut walked,
17042 );
17043 if txn.is_none() && dbg {
17044 eprintln!("spec_step: verify declined");
17045 }
17046 let txn = txn?;
17047 let spec_gpu_end = txn.gpu_end;
17048 let b = fed.len();
17049 let mut accepted = 1usize;
17050 while accepted < b && fed[accepted] == argmax[accepted - 1] {
17051 accepted += 1;
17052 }
17053 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
17058 accepted = 1;
17059 }
17060 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
17061 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
17062 }
17063 let t_fin = std::time::Instant::now();
17064 if !crate::dsv4::dsv4_spec_finish(
17065 g,
17066 layers,
17067 &cfg,
17068 st,
17069 txn,
17070 accepted,
17071 &fed,
17072 &self.inv_freq,
17073 self.pool.as_deref(),
17074 ) {
17075 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
17076 return None;
17077 }
17078 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17079 eprintln!(
17080 "finish(k={accepted}): {:.1} ms",
17081 t_fin.elapsed().as_secs_f64() * 1e3
17082 );
17083 }
17084 *accepted_ctr += accepted - 1;
17085 let (hc, dim) = (cfg.hc_mult, cfg.dim);
17090 let dev_caps: Vec<usize> = targets
17095 .iter()
17096 .copied()
17097 .filter(|&t| t < spec_gpu_end)
17098 .collect();
17099 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
17100 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
17101 return None;
17102 }
17103 for t in 0..accepted {
17104 let tip = t + 1 == accepted;
17105 for (slot, &tl) in targets.iter().enumerate() {
17106 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
17107 let lo = (di * b + t) * hc * dim;
17108 crate::dsv4::dspark_capture(
17109 &caps_all[lo..lo + hc * dim],
17110 &cfg,
17111 slot,
17112 &mut ds.main_hidden,
17113 );
17114 } else if tip
17115 && crate::dsv4::dspark_peek_slot(slot, dim, {
17116 let lo = slot * dim;
17117 &mut ds.main_hidden[lo..lo + dim]
17118 })
17119 {
17120 } else {
17125 crate::dsv4::dspark_capture(
17129 &walked[t * hc * dim..(t + 1) * hc * dim],
17130 &cfg,
17131 slot,
17132 &mut ds.main_hidden,
17133 );
17134 }
17135 }
17136 crate::dsv4::dspark_ring_append(
17137 g,
17138 &self.dsv4_mtp,
17139 &cfg,
17140 ds,
17141 next_pos + t,
17142 self.pool.as_deref(),
17143 );
17144 }
17145 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
17146 self.graph_logits = Some(row);
17147 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
17152 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
17153 crate::dsv4::pick_tally_arm();
17154 }
17155 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
17156 eprintln!(
17157 "spec_step total {:.1} ms (k={accepted})",
17158 t_all.elapsed().as_secs_f64() * 1e3
17159 );
17160 }
17161 Some((fed[1..accepted].to_vec(), next_pos + accepted))
17162 }
17163
17164 fn dspark_probe(&mut self, position: usize, token_id: u32) {
17165 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
17166 return;
17167 }
17168 let trunk_now = crate::dsv4::pick_tally_take();
17170 crate::dsv4::trunk_freq_note(&trunk_now);
17171 if !trunk_now.is_empty() {
17172 self.dspark_trunk_picks.push(trunk_now);
17173 let keep = crate::dsv4::dspark_block();
17174 if self.dspark_trunk_picks.len() > keep {
17175 self.dspark_trunk_picks.remove(0);
17176 }
17177 }
17178 for p in std::mem::take(&mut self.dspark_pending) {
17181 let Some(i) = position.checked_sub(p.0 + 1) else {
17182 continue;
17183 };
17184 let mut p = p;
17185 if i < p.1.len() {
17186 if p.2 && p.1[i] == token_id {
17187 p.3 = i + 1;
17188 } else {
17189 p.2 = false;
17190 }
17191 if i + 1 < p.1.len() {
17192 self.dspark_pending.push(p);
17193 continue;
17194 }
17195 }
17196 self.dspark_hist.push(p.3);
17197 self.dspark_real.push(token_id);
17198 }
17199 let Some(b) = &mut self.dsv4 else { return };
17200 let (g, layers, cfg) = (&b.0, &b.1, b.2);
17201 let n_layers = layers.len();
17202 if self.dspark.is_none() {
17203 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
17204 if t.is_empty() {
17205 return;
17206 }
17207 eprintln!(
17208 "DSpark: capture from layers {t:?}, block {}",
17209 crate::dsv4::dspark_block()
17210 );
17211 crate::dsv4::dspark_arm(&t, cfg.dim);
17212 self.dspark = Some(crate::dsv4::DsparkState::new(
17213 self.dsv4_mtp.len(),
17214 &cfg,
17215 t.len(),
17216 ));
17217 }
17218 let ds = self.dspark.as_mut().unwrap();
17219 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
17220 return; }
17222 let mut conf = Vec::new();
17223 crate::dsv4::pick_tally_arm();
17224 let draft_started = std::time::Instant::now();
17229 #[cfg(feature = "gpu")]
17230 let gpu_draft = crate::dsv4::dspark_gpu_on();
17231 #[cfg(not(feature = "gpu"))]
17232 let gpu_draft = false;
17233 let props = if gpu_draft {
17234 #[cfg(feature = "gpu")]
17235 {
17236 let kv_id = b.3.kv_id;
17237 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
17238 Some(pk) => crate::dsv4::dspark_draft_gpu(
17239 g,
17240 &self.dsv4_mtp,
17241 &cfg,
17242 ds,
17243 pk,
17244 kv_id,
17245 token_id,
17246 position,
17247 self.pool.as_deref(),
17248 &mut conf,
17249 ),
17250 None => Vec::new(),
17251 }
17252 }
17253 #[cfg(not(feature = "gpu"))]
17254 Vec::new()
17255 } else {
17256 crate::gpu::cpu_scope(|| {
17257 crate::dsv4::dspark_draft(
17258 g,
17259 &self.dsv4_mtp,
17260 &cfg,
17261 ds,
17262 token_id,
17263 position,
17264 self.pool.as_deref(),
17265 &mut conf,
17266 )
17267 })
17268 };
17269 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
17270 let draft_picks = crate::dsv4::pick_tally_take();
17271 crate::dsv4::dspark_freq_note(&draft_picks);
17272 crate::dsv4::pick_tally_arm();
17275 if !props.is_empty() {
17276 let (tu, tt) = {
17280 let flat: Vec<(usize, Vec<usize>)> = self
17281 .dspark_trunk_picks
17282 .iter()
17283 .flat_map(|v| v.iter().cloned())
17284 .collect();
17285 let mut per: std::collections::HashMap<usize, Vec<usize>> =
17287 std::collections::HashMap::new();
17288 for (li, picks) in flat {
17289 per.entry(li).or_default().extend(picks);
17290 }
17291 let n = per.len().max(1);
17292 let mut u = 0usize;
17293 let mut t = 0usize;
17294 for (_, v) in per {
17295 t += v.len();
17296 u += v.iter().collect::<std::collections::HashSet<_>>().len();
17297 }
17298 (u / n, t / n)
17299 };
17300 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
17301 self.dspark_exp.push((tu, tt, du, dt));
17302 self.dspark_pending.push((position, props, true, 0));
17303 }
17304 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
17305 let n = self.dspark_hist.len() as f32;
17306 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
17307 let block = crate::dsv4::dspark_block();
17308 let mut at = vec![0usize; block + 1];
17309 for &k in &self.dspark_hist {
17310 at[k] += 1;
17311 }
17312 let mut surv = Vec::with_capacity(block);
17314 for i in 1..=block {
17315 let k = at[i..].iter().sum::<usize>() as f32 / n;
17316 surv.push(format!("{k:.2}"));
17317 }
17318 let distinct = self
17319 .dspark_real
17320 .iter()
17321 .collect::<std::collections::HashSet<_>>()
17322 .len();
17323 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
17324 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
17325 });
17326 let m = self.dspark_exp.len().max(1);
17327 eprintln!(
17328 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
17329 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
17330 self.dspark_hist.len(),
17331 mean + 1.0,
17332 surv.join(" ")
17333 );
17334 eprintln!(
17335 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
17336 trunk experts {}/{} per layer over {block} tokens, \
17337 draft {}/{} per block, draft {:.2} ms/block",
17338 self.dspark_real.len(),
17339 tu / m,
17340 tt / m,
17341 du / m,
17342 dt / m,
17343 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
17344 );
17345 }
17346 }
17347
17348 fn forward_layers_upto(
17349 &mut self,
17350 hidden: &[f32],
17351 position: usize,
17352 task_mask: Option<&TaskMask>,
17353 upto: Option<usize>,
17354 ) -> Vec<f32> {
17355 if let Some(plan) = self.gpu_plan.clone() {
17361 if upto.is_none() && plan.len() > 1 {
17362 let mut h = hidden.to_vec();
17363 for &(dev, from, upto_incl) in plan.iter() {
17364 h = crate::gpu::with_device(dev, || {
17365 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
17366 });
17367 }
17368 return h;
17369 }
17370 }
17371 self.forward_layers_span(hidden, position, task_mask, 0, upto)
17372 }
17373
17374 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
17379 self.set_gpu_plan_at(devices, None)
17380 }
17381
17382 pub fn set_gpu_plan_at(
17386 &mut self,
17387 devices: Option<&[usize]>,
17388 at: Option<usize>,
17389 ) -> Result<(), String> {
17390 let Some(devs) = devices.filter(|d| d.len() > 1) else {
17391 self.gpu_plan = None;
17392 return Ok(());
17393 };
17394 self.split_supported()?;
17395 let n = self.num_layers;
17396 if devs.len() > n {
17397 return Err(format!("{} devices for {n} layers", devs.len()));
17398 }
17399 if let Some(k) = at {
17400 if k == 0 || k >= n {
17401 return Err(format!("split at {k}: the model has {n} layers"));
17402 }
17403 if devs.len() == 2 {
17404 self.gpu_plan = Some(std::sync::Arc::new(vec![
17405 (devs[0], 0, k - 1),
17406 (devs[1], k, n - 1),
17407 ]));
17408 return Ok(());
17409 }
17410 return Err(format!(
17411 "an explicit split point takes exactly 2 devices, got {}",
17412 devs.len()
17413 ));
17414 }
17415 let per = n.div_ceil(devs.len());
17416 let mut plan = Vec::with_capacity(devs.len());
17417 let mut from = 0usize;
17418 for &d in devs {
17419 if from >= n {
17420 break;
17421 }
17422 let upto = (from + per - 1).min(n - 1);
17423 plan.push((d, from, upto));
17424 from = upto + 1;
17425 }
17426 self.gpu_plan = Some(std::sync::Arc::new(plan));
17427 Ok(())
17428 }
17429
17430 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
17432 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
17433 }
17434
17435 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
17441 if let Some(x) = t.as_f32() {
17442 return x.to_vec();
17443 }
17444 let mut out = vec![0.0; t.rows() * t.cols()];
17445 for r in 0..t.rows() {
17446 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
17447 }
17448 out
17449 }
17450
17451 fn embryo_resident_eligible(&self) -> bool {
17452 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
17455 || self.num_layers != self.physical_layers
17456 || self.loop_final_norm
17457 || self.weights.layers.len() != self.num_layers
17458 || self.head_clusters.is_none()
17459 || self.final_softcap.is_some()
17460 || self.logit_multiplier.is_some()
17461 || self.attn_softcap != 0.0
17462 || self.mtp.is_some()
17463 || self.per_token_stack()
17464 || self.dsv4.is_some()
17465 || self.dsv41.is_some()
17466 || self.qwen4_exp.is_some()
17467 || self.dyn_router.is_some()
17474 || self.dyn_phi_layer.is_some()
17475 || self.dyn_blend_loaded
17476 || self.o1_cfg.is_some()
17477 || self.swa.is_some()
17478 || self.sliding_layers.is_some()
17479 || self.global_attn.is_some()
17480 || self.attention_heads_per_layer.is_some()
17481 || self.attn_v_norm
17482 || self
17483 .kv_cache
17484 .layers
17485 .iter()
17486 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
17487 || self.rope_scale != 1.0
17488 || self.rope_scale_local != 1.0
17489 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
17490 || self.hidden_size == 0
17491 || self.hidden_size > 1024
17492 || self.intermediate_size > 1024
17493 || self.num_heads == 0
17494 || self.num_kv_heads == 0
17495 || self.num_heads % self.num_kv_heads != 0
17496 || self.num_heads.saturating_mul(self.head_dim) > 1024
17497 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
17498 || self.vocab_size == 0
17499 || self.kv_cache.max_seq_len == 0
17500 || self.rotary_dim == 0
17501 || self.rotary_dim > self.head_dim
17502 || self.rotary_dim % 2 != 0
17503 || self.inv_freq.len() < self.rotary_dim / 2
17504 {
17505 return false;
17506 }
17507 if self.weights.lm_head.as_f32().is_none()
17518 || self.weights.embed_tokens.as_f32().is_none()
17519 || self.weights.lm_head.rows() < self.vocab_size
17520 || self.weights.lm_head.cols() != self.hidden_size
17521 || self.weights.embed_tokens.rows() < self.vocab_size
17522 || self.weights.embed_tokens.cols() != self.hidden_size
17523 || self.weights.final_norm.len() != self.hidden_size
17524 {
17525 return false;
17526 }
17527 if let Some(cfg) = self.vmf_cfg {
17528 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
17529 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
17530 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
17531 || cfg.state_len() == 0
17532 {
17533 return false;
17534 }
17535 }
17536 if let Some(g) = self.gdn_cfg {
17537 if g.num_v_heads == 0
17542 || g.num_k_heads == 0
17543 || g.num_v_heads % g.num_k_heads != 0
17544 || g.key_head_dim == 0
17545 || g.key_head_dim > 128
17546 || g.value_head_dim == 0
17547 || g.value_head_dim > 256
17548 || g.value_head_dim % 4 != 0
17549 || g.conv_kernel == 0
17550 || g.num_v_heads > 512
17551 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
17552 || g.conv_dim() > 2048
17553 || g.conv_dim() % 4 != 0
17554 || g.hidden_size != self.hidden_size
17555 || g.output_gate_sigmoid
17556 || g.rms_eps != self.rms_eps
17557 || g.state_len() == 0
17558 {
17559 return false;
17560 }
17561 }
17562 let mut full_seen = false;
17563 for lw in &self.weights.layers {
17564 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
17565 return false;
17566 }
17567 match &lw.attn {
17568 AttnKind::LinearGdn(w) => {
17569 let Some(g) = self.gdn_cfg else {
17570 return false;
17571 };
17572 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
17573 if w.in_proj_qkv.rows() != g.conv_dim()
17574 || w.in_proj_qkv.cols() != self.hidden_size
17575 || w.in_proj_qkv.as_f32().is_none()
17576 || w.in_proj_z.rows() != nv * dv
17577 || w.in_proj_z.cols() != self.hidden_size
17578 || w.in_proj_z.as_f32().is_none()
17579 || w.in_proj_a.rows() != nv
17580 || w.in_proj_a.cols() != self.hidden_size
17581 || w.in_proj_a.as_f32().is_none()
17582 || w.in_proj_b.rows() != nv
17583 || w.in_proj_b.cols() != self.hidden_size
17584 || w.in_proj_b.as_f32().is_none()
17585 || w.conv1d.len() != g.conv_dim() * kk
17586 || w.a_log.len() != nv
17587 || w.dt_bias.len() != nv
17588 || w.norm.len() != dv
17589 || w.out_proj.rows() != self.hidden_size
17590 || w.out_proj.cols() != nv * dv
17591 || w.out_proj.as_f32().is_none()
17592 {
17593 return false;
17594 }
17595 }
17596 AttnKind::Linear(w) => {
17597 let Some(cfg) = self.vmf_cfg else {
17598 return false;
17599 };
17600 if w.thq.rows() != cfg.num_heads * cfg.nphase
17601 || w.thq.cols() != self.hidden_size
17602 || w.thq.as_f32().is_none()
17603 || w.thk.rows() != cfg.num_heads * cfg.nphase
17604 || w.thk.cols() != self.hidden_size
17605 || w.thk.as_f32().is_none()
17606 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
17607 || w.v_proj.cols() != self.hidden_size
17608 || w.v_proj.as_f32().is_none()
17609 || w.out_proj.rows() != self.hidden_size
17610 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
17611 || w.out_proj.as_f32().is_none()
17612 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
17613 {
17614 return false;
17615 }
17616 if let Some((kg, kb)) = &w.k_gate {
17617 if kg.rows() != cfg.num_heads
17618 || kg.cols() != self.hidden_size
17619 || kg.as_f32().is_none()
17620 || kb.len() != cfg.num_heads
17621 {
17622 return false;
17623 }
17624 }
17625 if let Some(conv) = &w.conv {
17626 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
17627 return false;
17628 }
17629 }
17630 }
17631 AttnKind::Full {
17632 wq,
17633 wk,
17634 wv,
17635 wo,
17636 q_norm,
17637 k_norm,
17638 output_gate,
17639 softplus_gate,
17640 bias,
17641 } => {
17642 if full_seen
17643 || q_norm.is_some()
17644 || k_norm.is_some()
17645 || *output_gate
17646 || softplus_gate.is_some()
17647 || bias.is_some()
17648 || wq.as_f32().is_none()
17649 || wk.as_f32().is_none()
17650 || wv.as_f32().is_none()
17651 || wo.as_f32().is_none()
17652 || wq.rows() != self.num_heads * self.head_dim
17653 || wk.rows() != self.num_kv_heads * self.head_dim
17654 || wv.rows() != self.num_kv_heads * self.head_dim
17655 || wq.cols() != self.hidden_size
17656 || wk.cols() != self.hidden_size
17657 || wv.cols() != self.hidden_size
17658 || wo.rows() != self.hidden_size
17659 || wo.cols() != self.num_heads * self.head_dim
17660 {
17661 return false;
17662 }
17663 full_seen = true;
17664 }
17665 AttnKind::Bounded(w) => {
17666 let Some(ac) = self.anchor_core.as_ref() else {
17670 return false;
17671 };
17672 if self.bounded_rope.is_none()
17673 || w.window != ac.window
17674 || w.sink != ac.sink
17675 || w.group_windows != ac.group_windows
17676 || w.window == 0
17677 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
17678 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
17679 || w.wq.as_f32().is_none()
17680 || w.wk.as_f32().is_none()
17681 || w.wv.as_f32().is_none()
17682 || w.wo.as_f32().is_none()
17683 || w.wq.rows() != self.num_heads * self.head_dim
17684 || w.wk.rows() != self.num_kv_heads * self.head_dim
17685 || w.wv.rows() != self.num_kv_heads * self.head_dim
17686 || w.wq.cols() != self.hidden_size
17687 || w.wk.cols() != self.hidden_size
17688 || w.wv.cols() != self.hidden_size
17689 || w.wo.rows() != self.hidden_size
17690 || w.wo.cols() != self.num_heads * self.head_dim
17691 {
17692 return false;
17693 }
17694 }
17695 _ => return false,
17696 }
17697 match &lw.ffn {
17698 FfnKind::Dense(d) => {
17699 if d.act != Act::Silu
17700 || !d.segs.is_empty()
17701 || d.gate_proj.as_f32().is_none()
17702 || d.up_proj.as_f32().is_none()
17703 || d.down_proj.as_f32().is_none()
17704 || d.gate_proj.rows() != self.intermediate_size
17705 || d.gate_proj.cols() != self.hidden_size
17706 || d.up_proj.rows() != self.intermediate_size
17707 || d.up_proj.cols() != self.hidden_size
17708 || d.down_proj.rows() != self.hidden_size
17709 || d.down_proj.cols() != self.intermediate_size
17710 {
17711 return false;
17712 }
17713 }
17714 FfnKind::Moe(m) => {
17715 if m.resonance.is_none()
17716 || m.top_k != 1
17717 || m.router_sigmoid
17718 || !m.norm_topk_prob
17719 || m.expert_bias.is_some()
17720 || m.routed_scaling != 1.0
17721 || m.route_tau.is_some()
17722 || m.shared.is_none()
17723 || m.mask.is_some()
17724 || m.per_expert_scale.is_some()
17725 || m.router_input_norm
17726 || m.experts.is_empty()
17727 || m.experts.len() > 8
17728 {
17729 return false;
17730 }
17731 let r = m.resonance.as_ref().unwrap();
17732 if r.mu.len() != m.experts.len() * self.hidden_size
17733 || r.bias.len() != m.experts.len()
17734 || r.u.len() != m.experts.len() * r.k * self.hidden_size
17735 || r.k > 128
17736 {
17737 return false;
17738 }
17739 let Some((shared, gate)) = &m.shared else {
17740 return false;
17741 };
17742 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
17743 return false;
17744 }
17745 if shared.gate_proj.as_f32().is_none()
17746 || shared.up_proj.as_f32().is_none()
17747 || shared.down_proj.as_f32().is_none()
17748 || shared.gate_proj.rows() != self.intermediate_size
17749 || shared.gate_proj.cols() != self.hidden_size
17750 || shared.up_proj.rows() != self.intermediate_size
17751 || shared.up_proj.cols() != self.hidden_size
17752 || shared.down_proj.rows() != self.hidden_size
17753 || shared.down_proj.cols() != self.intermediate_size
17754 {
17755 return false;
17756 }
17757 for e in &m.experts {
17758 if e.act != Act::Silu
17759 || !e.segs.is_empty()
17760 || e.gate_proj.as_f32().is_none()
17761 || e.up_proj.as_f32().is_none()
17762 || e.down_proj.as_f32().is_none()
17763 || e.gate_proj.rows() != self.intermediate_size
17764 || e.gate_proj.cols() != self.hidden_size
17765 || e.up_proj.rows() != self.intermediate_size
17766 || e.up_proj.cols() != self.hidden_size
17767 || e.down_proj.rows() != self.hidden_size
17768 || e.down_proj.cols() != self.intermediate_size
17769 {
17770 return false;
17771 }
17772 }
17773 }
17774 FfnKind::DenseMoe(_) => return false,
17775 }
17776 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
17777 return false;
17778 }
17779 }
17780 if full_seen && self.anchor_core.is_some() {
17781 return false;
17782 }
17783 full_seen || self.num_layers > 0
17784 }
17785
17786 fn embryo_resident_wanted(&self) -> bool {
17791 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17792 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17793 && matches!(
17794 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17795 Ok("1") | Ok("parallel")
17796 )
17797 && crate::gpu::enabled_here()
17798 && !self.graph_refused()
17799 && self.embryo_resident_eligible()
17800 }
17801
17802 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
17810 if ids.len() < 2
17811 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
17812 || !self.embryo_resident_wanted()
17813 {
17814 return None;
17815 }
17816 let model = self.ensure_embryo_graph()?;
17817 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
17818 .ok()
17819 .and_then(|v| v.parse::<usize>().ok())
17820 .filter(|&v| v >= 1)
17821 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
17822 .min(crate::gpu::EMBRYO_CHUNK_MAX);
17823 let hs = self.hidden_size;
17824 let n = ids.len();
17825 let mut pos = start;
17826 let mut last = None;
17827 let mut rows = Vec::with_capacity(cmax * hs);
17828 while pos < n {
17829 let end = (pos + cmax).min(n);
17830 rows.clear();
17831 for &id in &ids[pos..end] {
17832 rows.extend_from_slice(&self.embed_single(id));
17833 }
17834 let mut lg = Vec::new();
17835 if !crate::gpu::forward_embryo_graph_chunk(
17836 &model,
17837 self.graph_kv_id,
17838 &rows,
17839 pos,
17840 end - pos,
17841 &mut lg,
17842 ) {
17843 if pos == start {
17844 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17845 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
17846 }
17847 return None;
17848 }
17849 self.kv_cache.clear();
17853 self.clear_history();
17854 crate::gpu::graph_kv_reset(self.graph_kv_id);
17855 panic!(
17856 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
17857 );
17858 }
17859 last = Some(lg);
17860 pos = end;
17861 }
17862 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17863 eprintln!(
17864 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
17865 n - start,
17866 (n - start).div_ceil(cmax)
17867 );
17868 }
17869 last
17870 }
17871
17872 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
17873 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
17874 const UMAX: u32 = u32::MAX;
17875 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
17876 const REC: usize = 64;
17877 struct Pack {
17878 data: Vec<f32>,
17879 }
17880 impl Pack {
17881 fn put(&mut self, x: &[f32]) -> u32 {
17882 if x.is_empty() {
17883 return u32::MAX;
17884 }
17885 let off = self.data.len();
17886 self.data.extend_from_slice(x);
17887 off as u32
17888 }
17889 }
17890 let vmf = self.vmf_cfg;
17894 let gdn = self.gdn_cfg;
17895 let mut pack = Pack { data: Vec::new() };
17896 let mut meta = vec![0u32; HEADER];
17897 meta[0] = self.hidden_size as u32;
17898 meta[1] = self.intermediate_size as u32;
17899 meta[2] = self.vocab_size as u32;
17900 meta[3] = self.num_layers as u32;
17901 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
17902 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
17903 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
17904 if let Some(g) = gdn {
17905 meta[24] = g.num_v_heads as u32;
17906 meta[25] = g.num_k_heads as u32;
17907 meta[26] = g.key_head_dim as u32;
17908 meta[27] = g.value_head_dim as u32;
17909 meta[28] = g.conv_kernel as u32;
17910 meta[29] = g.conv_dim() as u32;
17911 }
17912 meta[7] = self.num_heads as u32;
17913 meta[8] = self.num_kv_heads as u32;
17914 meta[9] = self.head_dim as u32;
17915 meta[10] = self.kv_cache.max_seq_len as u32;
17916 let clusters = self.head_clusters.as_ref().unwrap();
17917 let cluster_count = clusters.len() / self.hidden_size;
17918 if clusters.len() % self.hidden_size != 0
17919 || cluster_count == 0
17920 || cluster_count > 1024
17921 || self.vocab_size % cluster_count != 0
17922 || self.weights.lm_head.rows() < self.vocab_size
17923 || self.weights.final_norm.len() != self.hidden_size
17924 {
17925 return None;
17926 }
17927 meta[11] = cluster_count as u32;
17928 meta[12] = (self.vocab_size / cluster_count) as u32;
17929 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
17930 meta[16] = self.rotary_dim as u32;
17931 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
17932 meta[19] = (self.rms_eps as f32).to_bits();
17933 let max_conv = self
17934 .weights
17935 .layers
17936 .iter()
17937 .filter_map(|lw| match &lw.attn {
17938 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
17939 _ => None,
17940 })
17941 .max()
17942 .unwrap_or(1);
17943 let phase_stride = vmf
17949 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
17950 .unwrap_or(0);
17951 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
17952 let state_stride = phase_stride.max(gdn_stride);
17953 let bounded = self.anchor_core.clone();
17957 let (anchor_window, anchor_sink) = bounded
17958 .as_ref()
17959 .map(|ac| (ac.window, ac.sink))
17960 .unwrap_or((0, 0));
17961 let kv_stride = if bounded.is_some() {
17962 2usize
17963 .saturating_mul(self.num_kv_heads)
17964 .saturating_mul(anchor_window)
17965 .saturating_mul(self.head_dim)
17966 } else {
17967 2usize
17968 .saturating_mul(self.num_kv_heads)
17969 .saturating_mul(self.kv_cache.max_seq_len)
17970 .saturating_mul(self.head_dim)
17971 };
17972 meta[14] = state_stride as u32;
17973 meta[15] = kv_stride as u32;
17974 meta[18] = anchor_window as u32;
17975 meta[20] = anchor_sink as u32;
17976 meta[21] = match &self.bounded_rope {
17977 Some(rope) => {
17978 let off = pack.put(&rope.cos);
17980 let _ = pack.put(&rope.sin);
17981 off
17982 }
17983 None => UMAX,
17984 };
17985 meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
17988 Some(ac) => {
17989 let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
17990 pack.put(&gw)
17991 }
17992 None => UMAX,
17993 };
17994 let mut full_seen = false;
17995 let mut bounded_seen = 0usize;
17996 let mut phase_seen = 0usize;
18000 let mut gdn_seen = 0usize;
18001 for (li, lw) in self.weights.layers.iter().enumerate() {
18002 let base = meta.len();
18003 meta.resize(base + REC, UMAX);
18004 meta[base] = match &lw.attn {
18005 AttnKind::Linear(w) if w.phase_delta => 1,
18006 AttnKind::Linear(_) => 0,
18007 AttnKind::Full { .. } => 2,
18008 AttnKind::Bounded(_) => 3,
18009 AttnKind::LinearGdn(_) => 4,
18010 _ => UMAX,
18011 };
18012 meta[base + 1] = pack.put(&lw.input_norm);
18013 meta[base + 2] = pack.put(&lw.post_norm);
18014 meta[base + 25] = match &lw.attn {
18015 AttnKind::Linear(_) => {
18016 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18017 phase_seen += 1;
18018 off
18019 }
18020 AttnKind::LinearGdn(_) => {
18021 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
18022 gdn_seen += 1;
18023 off
18024 }
18025 _ => UMAX,
18026 };
18027 match &lw.attn {
18028 AttnKind::LinearGdn(w) => {
18029 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
18032 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
18033 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
18034 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
18035 meta[base + 60] = pack.put(&w.conv1d);
18036 meta[base + 61] = pack.put(&w.a_log);
18037 meta[base + 62] = pack.put(&w.dt_bias);
18038 meta[base + 63] = pack.put(&w.norm);
18039 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18040 meta[base + 24] = 0;
18041 }
18042 AttnKind::Linear(w) => {
18043 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
18044 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
18045 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
18046 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
18047 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
18048 meta[base + 7] = pack.put(&decay);
18049 if let Some((kg, kb)) = &w.k_gate {
18050 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
18051 meta[base + 9] = pack.put(kb);
18052 }
18053 if let Some(conv) = &w.conv {
18054 meta[base + 10] = pack.put(conv);
18055 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
18056 } else {
18057 meta[base + 24] = 0;
18058 }
18059 }
18060 AttnKind::Full { wq, wk, wv, wo, .. } => {
18061 full_seen = true;
18062 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
18063 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
18064 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
18065 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
18066 meta[base + 26] = (li * kv_stride) as u32;
18067 }
18068 AttnKind::Bounded(w) => {
18069 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
18070 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
18071 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
18072 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
18073 meta[base + 26] = (bounded_seen * kv_stride) as u32;
18075 meta[base + 27] = pack.put(&w.sink_k);
18076 meta[base + 28] = pack.put(&w.sink_v);
18077 bounded_seen += 1;
18078 }
18079 _ => return None,
18080 }
18081 match &lw.ffn {
18082 FfnKind::Dense(d) => {
18083 meta[base + 15] = 0;
18084 meta[base + 16] = 0;
18085 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
18086 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
18087 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
18088 }
18089 FfnKind::Moe(m) => {
18090 let r = m.resonance.as_ref().unwrap();
18091 let (shared, _) = m.shared.as_ref().unwrap();
18092 meta[base + 15] = 1;
18093 meta[base + 16] = m.experts.len() as u32;
18094 meta[base + 17] = pack.put(&r.mu);
18095 meta[base + 18] = pack.put(&r.u);
18096 meta[base + 19] = pack.put(&r.bias);
18097 meta[base + 20] = r.k as u32;
18098 let mut shell = r.effective_shell(m.experts.len());
18107 shell.push(f32::NEG_INFINITY);
18108 meta[base + 30] = pack.put(&shell);
18109 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
18110 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
18111 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
18112 for (e, ex) in m.experts.iter().enumerate() {
18113 meta[base + 32 + e * 3] =
18114 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
18115 meta[base + 33 + e * 3] =
18116 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
18117 meta[base + 34 + e * 3] =
18118 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
18119 }
18120 }
18121 FfnKind::DenseMoe(_) => return None,
18122 }
18123 }
18124 if !full_seen && self.num_layers == 0 {
18125 return None;
18126 }
18127 let id = {
18128 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
18129 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
18130 };
18131 let model = crate::gpu::EmbryoGraphModel {
18132 id,
18133 hidden: self.hidden_size,
18134 intermediate: self.intermediate_size,
18135 vocab: self.vocab_size,
18136 layers: self.num_layers,
18137 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
18138 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
18139 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
18140 anchor_q_heads: self.num_heads,
18141 anchor_kv_heads: self.num_kv_heads,
18142 anchor_head_dim: self.head_dim,
18143 rotary_dim: self.rotary_dim,
18144 max_seq: self.kv_cache.max_seq_len,
18145 cluster_count,
18146 cluster_size: self.vocab_size / cluster_count,
18147 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
18148 state_stride,
18149 kv_stride,
18150 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
18151 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
18152 weights: pack.data,
18153 meta,
18154 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
18155 clusters: clusters.as_ref().clone(),
18156 final_norm: self.weights.final_norm.clone(),
18157 inv_freq: self.inv_freq.as_ref().clone(),
18158 bounded: bounded.is_some(),
18159 kv_layers: if bounded.is_some() {
18160 bounded_seen
18161 } else {
18162 self.num_layers
18163 },
18164 state_layers: phase_seen + gdn_seen,
18165 anchor_window,
18166 anchor_sink,
18167 phase_layers: phase_seen,
18168 gdn_layers: gdn_seen,
18169 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
18170 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
18171 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
18172 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
18173 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
18174 };
18175 self.embryo_graph = Some(std::sync::Arc::new(model));
18176 }
18177 self.embryo_graph.clone()
18178 }
18179
18180 fn forward_layers_span(
18181 &mut self,
18182 hidden: &[f32],
18183 position: usize,
18184 task_mask: Option<&TaskMask>,
18185 from: usize,
18186 upto: Option<usize>,
18187 ) -> Vec<f32> {
18188 #[cfg(target_os = "macos")]
18189 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
18190 position,
18191 from,
18192 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
18193 );
18194 debug_assert!(
18195 from == 0
18196 || (self.dsv4.is_none()
18197 && self.dsv41.is_none()
18198 && self.qwen4_exp.is_none()
18199 && !self.per_token_stack())
18200 );
18201 #[cfg(target_os = "macos")]
18207 if !crate::gpu_metal::wait_replay() {
18208 self.fail_metal_graph("the pending async replay failed before a plain forward");
18209 return vec![0.0; self.hidden_size];
18210 }
18211 if let Some(b) = &mut self.qwen4_exp {
18212 let _ = (task_mask, upto);
18213 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18214 let mut logits = Vec::new();
18215 crate::qwen4_exp::forward_token(
18216 &b.0,
18217 &b.1,
18218 &b.2,
18219 &mut b.3,
18220 token_id,
18221 position,
18222 &self.inv_freq,
18223 self.pool.as_deref(),
18224 &mut logits,
18225 true,
18226 );
18227 self.graph_logits = Some(logits);
18228 return vec![0.0; self.hidden_size];
18229 }
18230 if let Some(b) = &mut self.dsv4 {
18236 let _ = (task_mask, upto);
18237 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18238 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
18239 st.pos = position;
18240 let mut logits = Vec::new();
18241 crate::dsv4::forward_token(
18242 g,
18243 layers,
18244 &cfg,
18245 st,
18246 token_id,
18247 &self.inv_freq,
18248 self.pool.as_deref(),
18249 &mut logits,
18250 );
18251 self.graph_logits = Some(logits);
18252 self.dspark_probe(position, token_id);
18253 return vec![0.0; self.hidden_size];
18256 }
18257 if let Some(b) = &mut self.dsv41 {
18259 let _ = (task_mask, upto);
18260 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
18261 let mut logits = Vec::new();
18262 crate::dsv41::forward_token(
18263 &b.0,
18264 &b.1,
18265 &b.2,
18266 &mut b.3,
18267 token_id,
18268 position,
18269 self.pool.as_deref(),
18270 &mut logits,
18271 );
18272 self.graph_logits = Some(logits);
18273 return vec![0.0; self.hidden_size];
18274 }
18275 if let Some(b) = &self.g4e {
18281 let _ = (task_mask, upto);
18282 return crate::g4e::g4e_forward(
18283 &b.0,
18284 &b.1,
18285 hidden,
18286 position,
18287 &mut self.kv_cache.layers,
18288 self.num_heads,
18289 self.num_kv_heads,
18290 self.pool.as_deref(),
18291 );
18292 }
18293 if let Some(b) = &self.g3n {
18294 let _ = (task_mask, upto);
18295 return crate::g3n::g3n_forward(
18296 &b.0,
18297 &b.1,
18298 hidden,
18299 position,
18300 &mut self.kv_cache.layers,
18301 self.num_heads,
18302 self.num_kv_heads,
18303 self.head_dim,
18304 self.pool.as_deref(),
18305 );
18306 }
18307 if from == 0
18314 && upto.is_none()
18315 && task_mask.is_none()
18316 && self.anchor_core.is_some()
18317 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
18318 {
18319 static ONCE: std::sync::Once = std::sync::Once::new();
18320 ONCE.call_once(|| {
18321 eprintln!(
18322 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
18323 unsupported={} eligible={}",
18324 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
18325 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
18326 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
18327 crate::gpu::enabled_here(),
18328 self.graph_refused(),
18329 self.embryo_resident_eligible(),
18330 );
18331 });
18332 }
18333 if from == 0
18334 && upto.is_none()
18335 && task_mask.is_none()
18336 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
18340 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
18341 && matches!(
18345 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
18346 Ok("1") | Ok("parallel")
18347 )
18348 && crate::gpu::enabled_here()
18349 && !self.graph_refused()
18350 && (position == 0 || self.device_sequence_position().is_some())
18356 && self.embryo_resident_eligible()
18357 && let Some(model) = self.ensure_embryo_graph()
18358 {
18359 let mut lg = Vec::new();
18360 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
18361 {
18362 self.graph_logits = Some(lg);
18363 return vec![0.0; self.hidden_size];
18364 }
18365 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
18366 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
18367 }
18368 self.mark_graph_refused();
18374 if position != 0 {
18375 self.kv_cache.clear();
18379 self.clear_history();
18380 crate::gpu::graph_kv_reset(self.graph_kv_id);
18381 panic!(
18382 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
18383 );
18384 }
18385 }
18386 let mut h = hidden.to_vec();
18387 self.mimo_moe_prepare();
18390 let _mimo_q8 = self
18391 .mimo_moe
18392 .is_on()
18393 .then(crate::qtensor::enter_full_gpu_q8_scope);
18394 let (nh, _nkv, _hd, hs, _rd, eps) = (
18397 self.num_heads,
18398 self.num_kv_heads,
18399 self.head_dim,
18400 self.hidden_size,
18401 self.rotary_dim,
18402 self.rms_eps,
18403 );
18404 let pool = self.pool.clone();
18405 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
18417 let graph_on = match graph_env.as_deref() {
18418 Some("0") => false,
18419 Some("prefill") => false, Some(_) => true,
18421 None => crate::gpu::wgpu_graph_default(),
18427 };
18428 let graph_trusted =
18429 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
18430 let race_eligible =
18431 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
18432 let mut tail_start = 0usize;
18433 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
18434 let t_graph = std::time::Instant::now();
18435 let mut lg = Vec::new();
18436 let mut gl = 0usize;
18437 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
18438 let declined = built.is_none();
18439 let built = match built {
18440 Some(Ok(hh)) => Some(hh),
18441 Some(Err(())) => {
18442 self.clear_sequence_state();
18446 self.graph_failed
18447 .store(true, std::sync::atomic::Ordering::Relaxed);
18448 self.cancel
18449 .store(true, std::sync::atomic::Ordering::Relaxed);
18450 tracing::error!("token graph failed after admission; sequence state cleared");
18451 return vec![0.0; self.hidden_size];
18452 }
18453 None => None,
18454 };
18455 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
18460 self.mark_graph_refused();
18461 }
18462 graph_note(built.is_some(), gl, self.num_layers);
18463 if let Some(hh) = built {
18464 let dur = t_graph.elapsed();
18465 if std::env::var("CMF_GRAPH_PROF").is_ok() {
18466 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
18467 }
18468 if gl > 0 && gl < self.num_layers {
18469 h = hh;
18475 tail_start = gl;
18476 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
18477 if !graph_trusted {
18478 crate::gpu::graph_race_record(true, dur);
18479 }
18480 if !lg.is_empty() {
18481 lg.resize(self.vocab_size, 0.0);
18484 if let Some(c) = self.final_softcap {
18485 softcap_in_place(&mut lg, c, self.pool.as_deref());
18486 }
18487 self.graph_logits = Some(lg);
18488 }
18489 return hh;
18490 }
18491 }
18497 }
18498 let span = from > 0 || upto.is_some();
18522 if span && graph_on && task_mask.is_none() && graph_trusted {
18523 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
18524 let mut lg = Vec::new();
18525 let mut gl = 0usize;
18526 let span_res =
18527 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
18528 let span_res = match span_res {
18529 Some(Ok(hh)) => Some(hh),
18530 Some(Err(())) => {
18531 self.clear_sequence_state();
18532 self.graph_failed
18533 .store(true, std::sync::atomic::Ordering::Relaxed);
18534 self.cancel
18535 .store(true, std::sync::atomic::Ordering::Relaxed);
18536 tracing::error!(
18537 "span token graph failed after admission; sequence state cleared"
18538 );
18539 return vec![0.0; self.hidden_size];
18540 }
18541 None => None,
18542 };
18543 graph_note(span_res.is_some(), gl, upto_excl - from);
18544 if std::env::var("CMF_GPU_DEBUG").is_ok() {
18545 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
18549 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
18550 eprintln!(
18551 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
18552 upto_excl - from,
18553 span_res.is_some()
18554 );
18555 }
18556 }
18557 if let Some(hh) = span_res {
18558 if gl == upto_excl - from {
18559 if !lg.is_empty() {
18560 lg.resize(self.vocab_size, 0.0);
18561 if let Some(c) = self.final_softcap {
18562 softcap_in_place(&mut lg, c, self.pool.as_deref());
18563 }
18564 self.graph_logits = Some(lg);
18565 }
18566 crate::gpu::set_layer(-1);
18567 return hh;
18568 }
18569 h = hh;
18571 tail_start = from + gl;
18572 }
18573 }
18574 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
18579
18580 let host_tail = tail_start > from;
18590 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
18591 let automatic_gpu_prefix = self.automatic_gpu_prefix();
18592
18593 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
18594 #[cfg(target_os = "macos")]
18595 let mut gpu_skip_until = 0usize;
18596 for li in tail_start.max(from)..self.num_layers {
18597 let _capacity_tail = automatic_gpu_prefix
18598 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
18599 .map(|_| crate::gpu::enter_cpu_scope());
18600 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
18602 if li > u {
18603 break;
18604 }
18605 }
18606 if let Some(mask) = task_mask {
18607 if !mask.layer_alive(li) {
18608 continue; }
18610 }
18611 if host_tail && task_mask.is_none() {
18616 match self.m3_pooled_layer(li, position, &mut h) {
18617 Ok(true) => {
18618 crate::gpu::set_layer(-1);
18619 continue;
18620 }
18621 Ok(false) => {}
18622 Err(()) => {
18623 self.clear_sequence_state();
18624 self.graph_failed
18625 .store(true, std::sync::atomic::Ordering::Relaxed);
18626 self.cancel
18627 .store(true, std::sync::atomic::Ordering::Relaxed);
18628 tracing::error!(
18629 "pooled tail token graph failed after admission; sequence state cleared"
18630 );
18631 return vec![0.0; self.hidden_size];
18632 }
18633 }
18634 }
18635 #[cfg(target_os = "macos")]
18639 {
18640 if li < gpu_skip_until {
18641 continue;
18642 }
18643 if task_mask.is_none() {
18644 let end = self.q1_graph_gpu(li, upto, position, &mut h);
18645 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
18646 return vec![0.0; self.hidden_size];
18650 }
18651 if end > li {
18652 gpu_skip_until = end;
18653 if self.is_loop_end(end - 1) && end < self.num_layers {
18656 h = inference::rms_norm(
18657 &h,
18658 &self.weights.final_norm,
18659 self.rms_eps,
18660 self.norm_style,
18661 );
18662 }
18663 continue;
18664 }
18665 }
18666 }
18667
18668 if task_mask.is_none() {
18669 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
18670 crate::gpu::BatchGraphOutcome::Completed => continue,
18671 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
18672 crate::gpu::BatchGraphOutcome::Declined => {}
18673 }
18674 }
18675 #[cfg(feature = "gpu")]
18676 self.pull_lagging_host_kv(li, li + 1, position);
18677 let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
18680 let lw = &self.weights.layers[self.phys_layer(li)];
18681 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
18682 if tp.parse::<usize>().ok() == Some(position) {
18683 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
18684 eprintln!(
18685 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
18686 h[0], h[1]
18687 );
18688 }
18689 }
18690 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
18693 inference::rms_norm_into(
18694 &h,
18695 &lw.input_norm,
18696 self.rms_eps,
18697 self.norm_style,
18698 &mut self.ws.n1,
18699 );
18700 drop(prof);
18701
18702 let attn_out = match &lw.attn {
18703 AttnKind::Mla(w) => {
18704 let inv_freq_l = self.layer_inv_freq(li);
18705 let rs = self.layer_rope_scale(li);
18706 let eps = self.rms_eps;
18707 let pool = self.pool.clone();
18708 mla_attention(
18709 w,
18710 &self.ws.n1,
18711 &mut self.kv_cache.layers[li],
18712 position,
18713 &inv_freq_l,
18714 rs,
18715 eps,
18716 pool.as_deref(),
18717 )
18718 }
18719 AttnKind::Linear(w) => {
18720 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
18721 vmf_phase_forward(
18722 &self.ws.n1,
18723 w,
18724 &cfg,
18725 &mut self.kv_cache.layers[li].linear_state,
18726 self.pool.as_deref(),
18727 )
18728 }
18729 AttnKind::Kda(w) => {
18730 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
18731 crate::linear_core::kda_forward(
18732 &self.ws.n1,
18733 w,
18734 &cfg,
18735 &mut self.kv_cache.layers[li].linear_state,
18736 self.pool.as_deref(),
18737 )
18738 }
18739 AttnKind::LinearGdn(w) => {
18740 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
18741 gdn_forward(
18742 &self.ws.n1,
18743 w,
18744 &cfg,
18745 &mut self.kv_cache.layers[li].linear_state,
18746 self.pool.as_deref(),
18747 )
18748 }
18749 AttnKind::ShortConv(w) => {
18750 let cfg = self
18751 .short_conv_cfg
18752 .expect("short-conv layer without short_conv_cfg");
18753 short_conv_forward(
18754 &self.ws.n1,
18755 w,
18756 &cfg,
18757 &mut self.kv_cache.layers[li].linear_state,
18758 self.pool.as_deref(),
18759 )
18760 }
18761 AttnKind::Bounded(w) => {
18762 let rope = self
18765 .bounded_rope
18766 .clone()
18767 .expect("bounded layer without an installed rotation table");
18768 let cfg = crate::bounded::BoundedAttnCfg {
18769 num_heads: self.num_heads,
18770 num_kv_heads: self.num_kv_heads,
18771 head_dim: self.head_dim,
18772 hidden_size: hs,
18773 scale: self.attn_scale,
18774 rope: &rope,
18775 pool: pool.as_deref(),
18776 };
18777 crate::bounded::bounded_attention(
18778 &self.ws.n1,
18779 w,
18780 &mut self.kv_cache.layers[li],
18781 &cfg,
18782 )
18783 }
18784 AttnKind::Full {
18785 wq,
18786 wk,
18787 wv,
18788 wo,
18789 q_norm,
18790 k_norm,
18791 output_gate,
18792 softplus_gate,
18793 bias,
18794 } if self.kv_cache.layers[li].o1_sealed() => {
18795 let inv_freq_l = self.layer_inv_freq(li);
18798 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18799 let cfg = QwenAttnCfg {
18800 mrope: None,
18801 num_heads: self.layer_num_heads(li),
18802 num_kv_heads: nkv_l,
18803 head_dim: hd_l,
18804 hidden_size: hs,
18805 position,
18806 inv_freq: &inv_freq_l,
18807 rotary_dim: rd_l,
18808 scale: self.attn_scale,
18809 softcap: self.attn_softcap,
18810 window: None,
18811 v_norm: self.attn_v_norm,
18812 qk_norm_after_rope: self.qk_norm_after_rope,
18813 gate_sigmoid: self.proj_gate_sigmoid,
18814 q_norm: q_norm.as_deref(),
18815 k_norm: k_norm.as_deref(),
18816 output_gate: *output_gate,
18817 softplus_gate: softplus_gate
18818 .as_ref()
18819 .map(|(gate, per_head)| (gate, *per_head)),
18820 rope_scale: self.layer_rope_scale(li),
18821 bias: bias
18822 .as_ref()
18823 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18824 rms_eps: eps,
18825 norm_style: self.norm_style,
18826 pool: pool.as_deref(),
18827 v_head_dim: self.layer_v_dim(li),
18828 };
18829 attention::qwen_attention_nystrom(
18830 &self.ws.n1,
18831 wq,
18832 wk,
18833 wv,
18834 wo,
18835 &mut self.kv_cache.layers[li],
18836 &cfg,
18837 )
18838 }
18839 AttnKind::Full {
18840 wq,
18841 wk,
18842 wv,
18843 wo,
18844 q_norm,
18845 k_norm,
18846 output_gate,
18847 softplus_gate,
18848 bias,
18849 } => 'attn: {
18850 let dropin_reason =
18855 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
18856 if let Some(reason) = dropin_reason {
18857 self.note_graph_decline("wgpu attn dropin", reason);
18858 }
18859 if graph_on
18860 && dropin_reason.is_none()
18861 && !*output_gate
18862 && softplus_gate.is_none()
18863 && self.attention_heads_per_layer.is_none()
18864 && bias.is_none()
18865 && task_mask.is_none()
18866 {
18867 let inv_freq_l = self.layer_inv_freq(li);
18868 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18869 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18870 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
18871 wq.mapped_q1(),
18872 wk.mapped_q1(),
18873 wv.mapped_q1(),
18874 wo.mapped_q1(),
18875 ) {
18876 let gm = gm.clone();
18877 let mut out = vec![0f32; hs];
18878 let cache = &self.kv_cache.layers[li];
18879 if crate::gpu::attn_dropin(
18880 &gm,
18881 self.graph_kv_id,
18882 li,
18883 &self.ws.n1,
18884 qi,
18885 ki,
18886 vi,
18887 oi,
18888 q_norm.as_deref(),
18889 k_norm.as_deref(),
18890 self.qk_norm_after_rope,
18891 &inv_freq_l,
18892 nh,
18893 nkv_l,
18894 hd_l,
18895 rd_l,
18896 hs,
18897 position,
18898 self.kv_cache.max_seq_len,
18899 gemma,
18900 eps as f32,
18901 cache.k_heads(),
18902 cache.v_heads(),
18903 &mut out,
18904 ) {
18905 break 'attn out;
18906 }
18907 }
18908 }
18909 let masked = task_mask
18910 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
18911 .unwrap_or(false);
18912 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
18913 let plain = self.layer_attn_plain(li);
18916 match (masked, f32_view) {
18917 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
18920 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
18921 attention::multi_head_attention(
18922 &self.ws.n1,
18923 q,
18924 k,
18925 v,
18926 o,
18927 &mut self.kv_cache.layers[li],
18928 self.num_heads,
18929 self.num_kv_heads,
18930 self.head_dim,
18931 self.hidden_size,
18932 position,
18933 &active_heads,
18934 &self.inv_freq,
18935 )
18936 }
18937 (masked, _) => {
18938 if masked {
18939 tracing::warn!(
18940 "layer {li}: head mask on quantized weights or on a \
18941 window/sink/per-layer-geometry layer not supported \
18942 yet — executing dense"
18943 );
18944 }
18945 let inv_freq_l = self.layer_inv_freq(li);
18946 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
18947 let cfg = QwenAttnCfg {
18948 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
18949 num_heads: self.layer_num_heads(li),
18950 num_kv_heads: nkv_l,
18951 head_dim: hd_l,
18952 hidden_size: hs,
18953 position,
18954 inv_freq: &inv_freq_l,
18955 rotary_dim: rd_l,
18956 scale: self.attn_scale,
18957 softcap: self.attn_softcap,
18958 window: self.layer_window(li),
18959 v_norm: self.attn_v_norm,
18960 qk_norm_after_rope: self.qk_norm_after_rope,
18961 gate_sigmoid: self.proj_gate_sigmoid,
18962 q_norm: q_norm.as_deref(),
18963 k_norm: k_norm.as_deref(),
18964 output_gate: *output_gate,
18965 softplus_gate: softplus_gate
18966 .as_ref()
18967 .map(|(gate, per_head)| (gate, *per_head)),
18968 rope_scale: self.layer_rope_scale(li),
18969 bias: bias
18970 .as_ref()
18971 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18972 rms_eps: eps,
18973 norm_style: self.norm_style,
18974 pool: pool.as_deref(),
18975 v_head_dim: self.layer_v_dim(li),
18976 };
18977 attention::qwen_attention(
18978 &self.ws.n1,
18979 wq,
18980 wk,
18981 wv,
18982 wo,
18983 &mut self.kv_cache.layers[li],
18984 &cfg,
18985 )
18986 }
18987 }
18988 }
18989 };
18990 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
18993 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
18994 None => attn_out,
18995 };
18996 let mhc_ffn = match mhc_attn {
18997 Some(c) => {
18998 self.mhc_post(li, false, &attn_out, &c, &mut h);
19001 let cf = self.mhc_pre(li, true, &mut h);
19002 let lw = &self.weights.layers[self.phys_layer(li)];
19003 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
19004 Some(cf)
19005 }
19006 None => {
19007 let lw = &self.weights.layers[self.phys_layer(li)];
19008 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
19009 inference::add_rmsnorm_fused_into(
19010 &mut h,
19011 &attn_out,
19012 &lw.post_norm,
19013 self.rms_eps,
19014 self.norm_style,
19015 &mut self.ws.p1,
19016 );
19017 drop(prof);
19018 None
19019 }
19020 };
19021 let lw = &self.weights.layers[self.phys_layer(li)];
19022 let mut attn_out = attn_out;
19023 attention::recycle_buf(&mut attn_out);
19024 let post_normed = &self.ws.p1;
19025
19026 let ffn_masked = task_mask
19027 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
19028 .unwrap_or(false);
19029 let ffn_out = match (ffn_masked, &lw.ffn) {
19041 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
19045 let row = task_mask
19046 .and_then(|tm| tm.ffn_masks.get(li))
19047 .map(|v| v.as_slice());
19048 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
19049 }
19050 (true, FfnKind::Dense(d)) => {
19051 let tm = task_mask.unwrap();
19052 let alive = tm.ffn_active_count(li);
19053 let deep = alive * 2 <= self.intermediate_size;
19054 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
19055 let active = tm.ffn_active_indices(li);
19056 sparse_ffn_quant(
19057 d,
19058 post_normed,
19059 &active,
19060 self.hidden_size,
19061 self.pool.as_deref(),
19062 )
19063 } else if deep
19064 && let (Some(g), Some(u), Some(dn)) = (
19065 d.gate_proj.as_f32(),
19066 d.up_proj.as_f32(),
19067 d.down_proj.as_f32(),
19068 )
19069 {
19070 let active = tm.ffn_active_indices(li);
19071 inference::sparse_ffn_forward(
19072 post_normed,
19073 g,
19074 u,
19075 dn,
19076 self.hidden_size,
19077 self.intermediate_size,
19078 &active,
19079 self.pool.as_deref(),
19080 )
19081 } else {
19082 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
19083 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
19084 }
19085 }
19086 (true, FfnKind::Moe(m)) => {
19087 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
19091 ffn_forward(
19092 &lw.ffn,
19093 post_normed,
19094 self.pool.as_deref(),
19095 allowed.as_deref(),
19096 )
19097 }
19098 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
19099 dm,
19100 post_normed,
19101 &h,
19102 self.rms_eps,
19103 self.norm_style,
19104 self.pool.as_deref(),
19105 ),
19106 (false, _) => match &lw.ffn {
19107 FfnKind::DenseMoe(dm) => dense_moe_ffn(
19108 dm,
19109 post_normed,
19110 &h,
19111 self.rms_eps,
19112 self.norm_style,
19113 self.pool.as_deref(),
19114 ),
19115 FfnKind::Moe(m)
19116 if task_mask.is_none()
19117 && host_tail
19118 && li >= tail_start
19119 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
19120 {
19121 moe_ffn_minicpm_tail(
19122 &mut self.minicpm_tail,
19123 li,
19124 self.num_layers,
19125 m,
19126 post_normed,
19127 self.pool.as_deref(),
19128 )
19129 }
19130 FfnKind::Moe(m)
19131 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
19132 {
19133 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
19134 }
19135 _ => {
19136 let allowed = match (&lw.ffn, task_mask) {
19137 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
19138 _ => None,
19139 };
19140 ffn_forward(
19141 &lw.ffn,
19142 post_normed,
19143 self.pool.as_deref(),
19144 allowed.as_deref(),
19145 )
19146 }
19147 },
19148 };
19149 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
19150 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
19151 None => ffn_out,
19152 };
19153 match mhc_ffn {
19154 Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
19155 None => {
19156 for (i, &f) in ffn_out.iter().enumerate() {
19157 h[i] += f;
19158 }
19159 }
19160 }
19161 let mut ffn_out = ffn_out;
19162 attention::recycle_buf(&mut ffn_out);
19163
19164 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
19166 for v in h.iter_mut() {
19167 *v *= sc;
19168 }
19169 }
19170 if self.layer_dump.is_some() {
19172 self.dump_layer_row(position, li, &h);
19173 }
19174
19175 if self.is_loop_end(li) && li + 1 < self.num_layers {
19178 h = inference::rms_norm(
19179 &h,
19180 &self.weights.final_norm,
19181 self.rms_eps,
19182 self.norm_style,
19183 );
19184 }
19185
19186 if self.dyn_phi_layer == Some(li) {
19190 self.update_dyn_phi(&h);
19191 }
19192 }
19193 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
19195 crate::gpu::graph_race_record(false, t.elapsed());
19196 }
19197
19198 h
19199 }
19200
19201 fn update_dyn_phi(&mut self, h: &[f32]) {
19204 const A: f32 = 0.2;
19205 if self.dyn_phi_ema.len() != h.len() {
19206 self.dyn_phi_ema = vec![0.0; h.len()];
19207 self.dyn_phi_seen = 0;
19208 }
19209 if self.dyn_phi_seen == 0 {
19210 self.dyn_phi_ema.copy_from_slice(h);
19211 } else {
19212 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
19213 *e = (1.0 - A) * *e + A * v;
19214 }
19215 }
19216 self.dyn_phi_seen += 1;
19217 }
19218
19219 pub fn dyn_phi(&self) -> &[f32] {
19221 &self.dyn_phi_ema
19222 }
19223
19224 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
19226 self.dyn_phi_layer = layer;
19227 self.dyn_phi_ema.clear();
19228 self.dyn_phi_seen = 0;
19229 }
19230
19231 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
19233 let Some(model) = &self.model else {
19234 return Vec::new();
19235 };
19236 model
19237 .header
19238 .skills
19239 .iter()
19240 .enumerate()
19241 .filter_map(|(i, sk)| {
19242 if sk.is_v2() {
19246 return None;
19247 }
19248 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
19249 let sel = sk.selection.as_ref()?;
19250 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
19251 })
19252 .collect()
19253 }
19254
19255 pub fn active_skill(&self) -> Option<usize> {
19257 self.dyn_active
19258 }
19259
19260 pub fn enable_dynamic_routing(&mut self) -> usize {
19265 use crate::swarm::{DynRouter, RoutableSkill};
19266 let Some(model) = self.model.clone() else {
19267 return 0;
19268 };
19269 if let Some(r) = &model.header.router {
19276 tracing::warn!(
19277 "dynamic routing disabled: this file declares router policy '{}' with \
19278 granularity \"{}\" — the request-level decision applies instead",
19279 r.policy,
19280 r.granularity
19281 );
19282 return 0;
19283 }
19284 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
19290 || model.header.skills.iter().any(|s| s.is_v2())
19291 {
19292 tracing::warn!(
19293 "dynamic routing disabled: this file carries format-v2 skill records \
19294 (SKILLS_V2) — they route per request through a router policy only"
19295 );
19296 return 0;
19297 }
19298 if self.dyn_blend_loaded {
19301 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
19302 return 0;
19303 }
19304 if let Some(a) = self.dyn_active {
19308 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
19309 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
19310 return 0;
19311 }
19312 }
19313 let hidden = self.hidden_size;
19314 let mut skills = Vec::new();
19315 for (idx, id, _phi) in self.dynamic_skills() {
19316 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
19317 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
19318 skills.push(rs);
19319 }
19320 }
19321 }
19322 if skills.is_empty() {
19323 return 0;
19324 }
19325 let phi = skills[0].phi_layer;
19327 if skills.iter().any(|s| s.phi_layer != phi) {
19328 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
19329 }
19330 let n = skills.len();
19331 self.set_dyn_phi_layer(Some(phi));
19332 self.dyn_router = Some(DynRouter::new(skills));
19333 n
19334 }
19335
19336 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
19338 self.dyn_router
19339 .as_ref()
19340 .map(|r| r.switches.clone())
19341 .unwrap_or_default()
19342 }
19343
19344 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
19347 let _mimo_q8 = self
19348 .mimo_moe
19349 .is_on()
19350 .then(crate::qtensor::enter_full_gpu_q8_scope);
19351 let rows = self.weights.lm_head.rows();
19352 let mut logits = attention::take_buf(rows.min(self.vocab_size));
19353 let served = self.mimo_moe.is_on()
19357 && crate::gpu::mimo_q8_short_enabled()
19358 && rows == self.vocab_size
19359 && !self.weights.lm_head.has_prism_contract()
19360 && self
19361 .weights
19362 .lm_head
19363 .graph_weight()
19364 .is_some_and(|(model, idx, kind, _)| {
19365 kind == 7
19366 && crate::gpu::q82_short_rows(
19367 model,
19368 idx,
19369 hidden,
19370 1,
19371 rows,
19372 self.hidden_size,
19373 &mut logits,
19374 )
19375 });
19376 if !served {
19377 self.weights
19378 .lm_head
19379 .matvec(hidden, &mut logits, self.pool.as_deref());
19380 }
19381 logits.resize(self.vocab_size, 0.0);
19382 if let Some(m) = self.logit_multiplier {
19383 for l in logits.iter_mut() {
19384 *l *= m;
19385 }
19386 }
19387 if let Some(c) = self.final_softcap {
19388 softcap_in_place(&mut logits, c, self.pool.as_deref());
19389 }
19390 if let Some(cm) = self.head_clusters.as_ref() {
19391 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
19392 }
19393 logits
19394 }
19395
19396 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
19399 let h = hidden.len();
19400 let ncl = cm.len() / h.max(1);
19401 if ncl == 0 || logits.len() % ncl != 0 {
19402 return;
19403 }
19404 let cs = logits.len() / ncl;
19405 let mut lc = vec![0.0f32; ncl];
19407 for c in 0..ncl {
19408 let row = &cm[c * h..(c + 1) * h];
19409 let mut s = 0.0f32;
19410 for j in 0..h {
19411 s += row[j] * hidden[j];
19412 }
19413 lc[c] = s;
19414 }
19415 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19416 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
19417 for c in 0..ncl {
19418 let blk = &mut logits[c * cs..(c + 1) * cs];
19419 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
19420 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
19421 let add = lc[c] - lse - bl;
19422 for v in blk.iter_mut() {
19423 *v += add;
19424 }
19425 }
19426 }
19427
19428 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
19433 #[cfg(target_os = "macos")]
19434 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
19435 self.clear_sequence_state();
19436 crate::gpu::graph_race_begin_generation();
19440 if task_mask.is_none() {
19441 self.o1_begin();
19442 }
19443 let mut hidden = vec![0.0f32; self.hidden_size];
19444 for (pos, &id) in ids.iter().enumerate() {
19445 let emb = self.embed_single(id);
19446 hidden = self.forward_layers(&emb, pos, task_mask);
19447 }
19448 if let Err(err) = self.o1_seal_checked() {
19449 self.o1_fail(err);
19450 }
19451 if let Some(logits) = self.graph_logits.take() {
19454 return logits;
19455 }
19456 inference::rms_norm_into(
19457 &hidden,
19458 &self.weights.final_norm,
19459 self.rms_eps,
19460 self.norm_style,
19461 &mut self.ws.n1,
19462 );
19463 self.lm_head_forward(&self.ws.n1)
19464 }
19465}
19466
19467pub fn create_test_pipeline(
19469 hidden_size: usize,
19470 intermediate_size: usize,
19471 num_heads: usize,
19472 num_kv_heads: usize,
19473 head_dim: usize,
19474 num_layers: usize,
19475 vocab_size: usize,
19476) -> Pipeline {
19477 let synth = |n: usize, salt: usize| -> Vec<f32> {
19480 (0..n)
19481 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
19482 .collect()
19483 };
19484 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19485 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19486 };
19487 let layer_weights: Vec<LayerWeights> = (0..num_layers)
19488 .map(|li| LayerWeights {
19489 input_norm: vec![1.0; hidden_size],
19490 post_norm: vec![1.0; hidden_size],
19491 attn_out_norm: None,
19492 ffn_out_norm: None,
19493 layer_scale: None,
19494 ffn: FfnKind::Dense(DenseFfn {
19495 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
19496 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
19497 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
19498 act: Act::Silu,
19499 down_t: None,
19500 segs: Vec::new(),
19501 }),
19502 attn: AttnKind::Full {
19503 bias: None,
19504 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
19505 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
19506 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
19507 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
19508 q_norm: None,
19509 k_norm: None,
19510 output_gate: false,
19511 softplus_gate: None,
19512 },
19513 })
19514 .collect();
19515
19516 Pipeline::new(
19517 Tokenizer::byte_level(),
19518 PipelineWeights {
19519 embed_tokens: qt(vocab_size, hidden_size, 100),
19520 layers: layer_weights,
19521 lm_head: qt(vocab_size, hidden_size, 200),
19522 final_norm: vec![1.0; hidden_size],
19523 },
19524 hidden_size,
19525 intermediate_size,
19526 num_heads,
19527 num_kv_heads,
19528 head_dim,
19529 num_layers,
19530 num_layers, false, vocab_size,
19533 1e-6,
19534 10_000.0,
19535 NormStyle::Qwen,
19536 4096,
19537 SamplerConfig {
19538 seed: Some(42),
19539 ..Default::default()
19540 },
19541 )
19542}
19543
19544#[inline]
19549fn mask_bit(row: &[u8], j: usize) -> bool {
19550 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
19551}
19552
19553fn mask_gain() -> f32 {
19564 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
19565 *G.get_or_init(|| {
19566 std::env::var("CMF_FFN_MASK_GAIN")
19567 .ok()
19568 .and_then(|v| v.parse().ok())
19569 .unwrap_or(1.0)
19570 })
19571}
19572
19573fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
19574 let fill = meanfill().and_then(|(i, v)| {
19577 let li = crate::gpu::cur_layer();
19578 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
19579 });
19580 for r in 0..rows {
19581 let base = r * inter;
19582 for (bi, &byte) in row.iter().enumerate() {
19583 if byte == 0xFF {
19584 continue;
19585 }
19586 let j0 = bi * 8;
19587 for bit in 0..8 {
19588 let j = j0 + bit;
19589 if j < inter && byte & (1 << bit) == 0 {
19590 g[base + j] = fill.map_or(0.0, |f| f[j]);
19591 }
19592 }
19593 }
19594 }
19595 let gain = mask_gain();
19596 if gain != 1.0 {
19597 for v in g[..rows * inter].iter_mut() {
19598 *v *= gain;
19599 }
19600 }
19601}
19602
19603#[inline]
19605fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
19606 row.is_none_or(|r| mask_bit(r, i))
19607}
19608
19609fn all_bits_on(row: &[u8], n: usize) -> bool {
19612 (0..n).all(|i| mask_bit(row, i))
19613}
19614
19615fn tube_topk() -> usize {
19623 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19624 *K.get_or_init(|| {
19625 std::env::var("CMF_TUBE_TOPK")
19626 .ok()
19627 .and_then(|v| v.parse().ok())
19628 .unwrap_or(0)
19629 })
19630}
19631
19632fn tube_score_oracle() -> bool {
19633 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19634 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
19635}
19636
19637fn tube_ffn_routed(
19644 d: &DenseFfn,
19645 xs: &[f32],
19646 b: usize,
19647 pool: Option<&Pool>,
19648 mask_row: Option<&[u8]>,
19649 k: usize,
19650) -> Vec<f32> {
19651 let hidden = d.down_proj.rows();
19652 let core = d.gate_proj.rows();
19653 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19654 let mut out = match (b, core_full, mask_row) {
19655 (1, true, _) => dense_ffn(d, xs, pool),
19656 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19657 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19658 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19659 };
19660 let cand: Vec<usize> = (0..d.segs.len())
19661 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
19662 .collect();
19663 if cand.is_empty() {
19664 return out;
19665 }
19666 let oracle = tube_score_oracle();
19670 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
19671 let mut scores = vec![0f32; b * cand.len()];
19672 for (ci, &i) in cand.iter().enumerate() {
19673 let seg = &d.segs[i];
19674 let w = seg.width;
19675 let mut g = vec![0.0f32; b * w];
19676 if b == 1 {
19677 seg.gate.matvec(xs, &mut g, pool);
19678 } else {
19679 seg.gate.matmat(xs, b, &mut g, pool);
19680 }
19681 for v in g.iter_mut() {
19682 *v = Act::Silu.combine(*v, 1.0);
19683 }
19684 if !oracle {
19685 for t in 0..b {
19686 scores[t * cand.len() + ci] =
19687 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19688 }
19689 }
19690 if oracle || b > 1 {
19691 let mut u = vec![0.0f32; b * w];
19692 if b == 1 {
19693 seg.up.matvec(xs, &mut u, pool);
19694 } else {
19695 seg.up.matmat(xs, b, &mut u, pool);
19696 }
19697 for (a, &v) in g.iter_mut().zip(u.iter()) {
19698 *a *= v;
19699 }
19700 if oracle {
19701 for t in 0..b {
19702 scores[t * cand.len() + ci] =
19703 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
19704 }
19705 }
19706 }
19707 acts.push(g);
19708 }
19709 let keep = k.min(cand.len());
19711 let mut scratch: Vec<f32> = Vec::new();
19712 for t in 0..b {
19713 let mut sc: Vec<(f32, usize)> = (0..cand.len())
19714 .map(|ci| (scores[t * cand.len() + ci], ci))
19715 .collect();
19716 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
19717 let mut alive = vec![false; cand.len()];
19718 for &(_, ci) in sc.iter().take(keep) {
19719 alive[ci] = true;
19720 }
19721 if b > 1 {
19722 for (ci, a) in acts.iter_mut().enumerate() {
19723 if !alive[ci] {
19724 let w = d.segs[cand[ci]].width;
19725 a[t * w..(t + 1) * w].fill(0.0);
19726 }
19727 }
19728 } else {
19729 for (ci, &i) in cand.iter().enumerate() {
19733 if !alive[ci] {
19734 continue;
19735 }
19736 let seg = &d.segs[i];
19737 let w = seg.width;
19738 let g = &mut acts[ci];
19739 if !tube_score_oracle() {
19740 scratch.clear();
19741 scratch.resize(w, 0.0);
19742 seg.up.matvec(xs, &mut scratch, pool);
19743 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
19744 *a *= v;
19745 }
19746 }
19747 let mut acc = vec![0.0f32; hidden];
19748 seg.down.matvec(g, &mut acc, pool);
19749 for (o, a) in out.iter_mut().zip(&acc) {
19750 *o += *a;
19751 }
19752 }
19753 }
19754 }
19755 if b > 1 {
19756 for (ci, &i) in cand.iter().enumerate() {
19757 let seg = &d.segs[i];
19758 let mut acc = vec![0.0f32; b * hidden];
19759 seg.down.matmat(&acts[ci], b, &mut acc, pool);
19760 for (o, a) in out.iter_mut().zip(&acc) {
19761 *o += *a;
19762 }
19763 }
19764 }
19765 out
19766}
19767
19768fn tube_ffn(
19774 d: &DenseFfn,
19775 xs: &[f32],
19776 b: usize,
19777 pool: Option<&Pool>,
19778 mask_row: Option<&[u8]>,
19779) -> Vec<f32> {
19780 if tube_topk() > 0 {
19781 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
19782 }
19783 let hidden = d.down_proj.rows();
19784 let core = d.gate_proj.rows();
19785 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
19786 let mut out = match (b, core_full, mask_row) {
19787 (1, true, _) => dense_ffn(d, xs, pool),
19788 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
19789 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
19790 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
19791 };
19792 TUBE_SCRATCH.with(|sc| {
19793 let mut sc = sc.borrow_mut();
19794 let [g, u, acc] = &mut *sc;
19795 for seg in &d.segs {
19796 if !tube_bit(mask_row, seg.start) {
19797 continue;
19798 }
19799 let w = seg.width;
19800 g.resize(b * w, 0.0);
19801 if b == 1
19802 && d.act == Act::Silu
19803 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
19804 {
19805 } else {
19807 u.resize(b * w, 0.0);
19808 if b == 1 {
19809 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
19810 } else {
19811 seg.gate.matmat(xs, b, g, pool);
19812 seg.up.matmat(xs, b, u, pool);
19813 }
19814 for i in 0..b * w {
19815 g[i] = d.act.combine(g[i], u[i]);
19816 }
19817 }
19818 acc.resize(b * hidden, 0.0);
19819 acc.fill(0.0);
19820 if b == 1 {
19821 seg.down.matvec(g, acc, pool);
19822 } else {
19823 seg.down.matmat(g, b, acc, pool);
19824 }
19825 for (o, a) in out.iter_mut().zip(acc.iter()) {
19826 *o += *a;
19827 }
19828 }
19829 out
19830 })
19831}
19832
19833thread_local! {
19834 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
19838 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
19839}
19840
19841static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
19844 std::sync::atomic::AtomicU64::new(0),
19845 std::sync::atomic::AtomicU64::new(0),
19846];
19847
19848fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
19852 #[cfg(feature = "gpu")]
19853 {
19854 use std::sync::atomic::Ordering::Relaxed;
19855 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
19856 (
19857 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
19858 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
19859 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
19860 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
19861 )
19862 }
19863 #[cfg(not(feature = "gpu"))]
19864 (0.0, 0.0, 0.0, 0)
19865}
19866
19867fn wgpu_mirror_counters() -> [u64; 6] {
19870 #[cfg(feature = "gpu")]
19871 {
19872 crate::gpu_wgpu::MIRROR_EVENTS
19873 .each_ref()
19874 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
19875 }
19876 #[cfg(not(feature = "gpu"))]
19877 [0; 6]
19878}
19879
19880fn prefill_prof_on() -> bool {
19881 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19882 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
19883}
19884
19885pub(crate) fn accumulate_dense_ffn_activation_mass(
19894 d: &DenseFfn,
19895 xs: &[f32],
19896 b: usize,
19897 pool: Option<&Pool>,
19898 mass: &mut [f64],
19899 squared_mass: &mut [f64],
19900) -> Result<(), String> {
19901 let inter = d.gate_proj.rows();
19902 let hidden = d.gate_proj.cols();
19903 if mass.len() != inter || squared_mass.len() != inter {
19904 return Err(format!(
19905 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
19906 mass.len(),
19907 squared_mass.len()
19908 ));
19909 }
19910 if d.up_proj.rows() != inter
19911 || d.up_proj.cols() != hidden
19912 || d.down_proj.cols() != inter
19913 || d.down_proj.rows() != hidden
19914 {
19915 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
19916 }
19917 if !d.segs.is_empty() {
19918 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
19919 }
19920 if xs.len() != b.saturating_mul(hidden) {
19921 return Err(format!(
19922 "MTP FFN probe: {} input values for {b} × {hidden} rows",
19923 xs.len()
19924 ));
19925 }
19926 if b == 0 {
19927 return Ok(());
19928 }
19929 let mut gate = vec![0.0f32; b * inter];
19930 let mut up = vec![0.0f32; b * inter];
19931 d.gate_proj.matmat(xs, b, &mut gate, pool);
19932 d.up_proj.matmat(xs, b, &mut up, pool);
19933 for row in 0..b {
19934 for i in 0..inter {
19935 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
19936 let a = a as f64;
19937 mass[i] += a.abs();
19938 squared_mass[i] += a * a;
19939 }
19940 }
19941 Ok(())
19942}
19943
19944fn dense_ffn_batch(
19945 d: &DenseFfn,
19946 xs: &[f32],
19947 b: usize,
19948 pool: Option<&Pool>,
19949 mask_row: Option<&[u8]>,
19950) -> Vec<f32> {
19951 let inter = d.gate_proj.rows();
19952 let hidden = d.down_proj.rows();
19953 let fused_act = d.act.wgpu_graph_act();
19965 if mask_row.is_none()
19966 && fused_act.is_some()
19967 && b >= 32
19968 && crate::gpu::enabled_here()
19969 && !crate::gpu::mm_killed()
19970 && refit_dir().is_none()
19975 && !ffn_probe_active()
19980 && !crate::gptq_capture::capturing()
19984 {
19985 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
19986 d.gate_proj.mapped_q4t(),
19987 d.up_proj.mapped_q4t(),
19988 d.down_proj.mapped_q4t(),
19989 ) {
19990 let mut out = vec![0.0f32; b * hidden];
19991 let act = fused_act.expect("checked above");
19992 if crate::gpu::q4_ffn_act(
19993 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
19994 ) {
19995 return out;
19996 }
19997 }
19998 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20003 d.gate_proj.mapped_q4tp(),
20004 d.up_proj.mapped_q4tp(),
20005 d.down_proj.mapped_q4tp(),
20006 ) {
20007 let mut out = vec![0.0f32; b * hidden];
20008 let act = fused_act.expect("checked above");
20009 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
20010 {
20011 return out;
20012 }
20013 }
20014 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
20020 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
20021 d.gate_proj.mapped_device_gemm(),
20022 d.up_proj.mapped_device_gemm(),
20023 d.down_proj.mapped_device_gemm(),
20024 ) {
20025 let mut out = vec![0.0f32; b * hidden];
20026 let act = fused_act.expect("checked above");
20027 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
20028 return out;
20029 }
20030 }
20031 }
20032 let mut g = vec![0.0f32; b * inter];
20033 d.gate_proj.matmat(xs, b, &mut g, pool);
20034 let mut u = vec![0.0f32; b * inter];
20035 d.up_proj.matmat(xs, b, &mut u, pool);
20036 if gate_topk() > 0 && d.act == Act::Silu {
20037 for t in 0..b {
20038 let row = &mut g[t * inter..(t + 1) * inter];
20039 for v in row.iter_mut() {
20040 *v = Act::Silu.combine(*v, 1.0);
20041 }
20042 keep_top_k(row, gate_topk());
20043 }
20044 for i in 0..b * inter {
20045 g[i] *= u[i];
20046 }
20047 } else {
20048 for i in 0..b * inter {
20049 g[i] = d.act.combine(g[i], u[i]);
20050 }
20051 }
20052 if let Some(row) = mask_row {
20053 zero_masked_cols(&mut g, b, inter, row);
20054 }
20055 if oracle_topk() > 0 {
20056 for t in 0..b {
20057 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
20058 }
20059 }
20060 let mut out = vec![0.0f32; b * hidden];
20061 d.down_proj.matmat(&g, b, &mut out, pool);
20062 if refit_dir().is_some() {
20063 let li = crate::gpu::cur_layer();
20064 if li >= 0 {
20065 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
20066 }
20067 }
20068 FFN_PROBE.with(|pr| {
20072 if let Some(acc) = pr.borrow_mut().as_mut() {
20073 let li = crate::gpu::cur_layer();
20074 if li < 0 {
20075 return;
20076 }
20077 let Some(row) = acc.get_mut(li as usize) else {
20078 return;
20079 };
20080 let sq = probe_sq();
20081 for t in 0..b {
20082 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
20083 *a += if sq {
20084 (v as f64) * (v as f64)
20085 } else {
20086 (v as f64).abs()
20087 };
20088 }
20089 }
20090 }
20091 });
20092 out
20093}
20094
20095fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
20100 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20101 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20102 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
20103 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
20104 if (!on && !dump) || b == 0 {
20105 return;
20106 }
20107 let hidden = xs.len() / b;
20108 if on {
20109 let mut acc = m.act_sq.borrow_mut();
20110 if acc.len() < hidden {
20111 acc.resize(hidden, 0.0);
20112 }
20113 for t in 0..b {
20114 let row = &xs[t * hidden..(t + 1) * hidden];
20115 for (a, &v) in acc.iter_mut().zip(row) {
20116 *a += (v as f64) * (v as f64);
20117 }
20118 }
20119 }
20120 if dump {
20121 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
20124 .ok()
20125 .and_then(|v| v.parse().ok())
20126 .unwrap_or(4096);
20127 let mut rows = m.act_rows.borrow_mut();
20128 if rows.len() < cap * hidden {
20129 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
20130 rows.extend_from_slice(&xs[..take * hidden]);
20131 }
20132 }
20133}
20134
20135#[derive(Clone, Copy)]
20138struct SendVecs(*mut Vec<f32>);
20139unsafe impl Send for SendVecs {}
20140unsafe impl Sync for SendVecs {}
20141impl SendVecs {
20142 #[inline]
20143 fn at(self, i: usize) -> *mut Vec<f32> {
20144 unsafe { self.0.add(i) }
20145 }
20146}
20147
20148fn moe_ffn_batch(
20149 m: &MoeFfn,
20150 xs: &[f32],
20151 b: usize,
20152 hidden: usize,
20153 pool: Option<&Pool>,
20154 allowed: Option<&[bool]>,
20155) -> Vec<f32> {
20156 accumulate_act(m, xs, b);
20157 let ne = m.experts.len();
20158 let mut logits = vec![0.0f32; b * ne];
20159 match &m.resonance {
20160 Some(r) => {
20161 let hdim = xs.len() / b.max(1);
20162 for bi in 0..b {
20163 r.scores(
20164 &xs[bi * hdim..(bi + 1) * hdim],
20165 &mut logits[bi * ne..(bi + 1) * ne],
20166 );
20167 }
20168 }
20169 None => m.router.matmat(xs, b, &mut logits, pool),
20170 }
20171
20172 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
20175 {
20176 let mut st = m.stats.borrow_mut();
20177 if st.len() < ne {
20178 st.resize(ne, 0);
20179 }
20180 for bi in 0..b {
20181 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
20182 for &e in &idx {
20183 st[e] += 1;
20184 assign[e].push((bi, p[e] / wsum));
20185 }
20186 }
20187 }
20188
20189 let mut out = vec![0.0f32; b * hidden];
20190 let cols = m.experts[0].gate_proj.cols();
20191 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
20192 let sb = list.len();
20193 let mut sub = vec![0.0f32; sb * cols];
20194 for (k, &(bi, _)) in list.iter().enumerate() {
20195 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20196 }
20197 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
20198 for (k, &(bi, w)) in list.iter().enumerate() {
20199 for i in 0..hidden {
20200 out[bi * hidden + i] += w * eo[k * hidden + i];
20201 }
20202 }
20203 };
20204 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
20210 if pool.is_some() && active.len() >= 8 {
20211 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
20212 {
20213 let panel_ptr = SendVecs(panels.as_mut_ptr());
20214 let experts = &m.experts;
20217 let (active_r, assign_r) = (&active, &assign);
20218 let inherit_cpu = crate::gpu::inherit_cpu_scope();
20219 let run = |start: usize, end: usize| {
20220 let _cpu_scope = inherit_cpu();
20221 #[cfg(target_os = "macos")]
20229 let _metal_cpu = crate::gpu::enter_cpu_scope();
20230 for ai in start..end {
20231 let e = active_r[ai];
20232 let list = &assign_r[e];
20233 let sb = list.len();
20234 let mut sub = vec![0.0f32; sb * cols];
20235 for (k, &(bi, _)) in list.iter().enumerate() {
20236 sub[k * cols..(k + 1) * cols]
20237 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
20238 }
20239 unsafe {
20241 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
20242 }
20243 }
20244 };
20245 match pool {
20246 Some(p) => p.run_rows(active.len(), &run),
20247 None => run(0, active.len()),
20248 }
20249 }
20250 for (ai, &e) in active.iter().enumerate() {
20251 for (k, &(bi, w)) in assign[e].iter().enumerate() {
20252 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
20253 for i in 0..hidden {
20254 out[bi * hidden + i] += w * eo[i];
20255 }
20256 }
20257 }
20258 } else {
20259 for &e in &active {
20260 run_expert(&m.experts[e], &assign[e], &mut out);
20261 }
20262 }
20263 if let Some((se, gate)) = &m.shared {
20264 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
20265 let mut gl = vec![0.0f32; b];
20266 gate.matmat(xs, b, &mut gl, pool);
20267 (0..b)
20268 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
20269 .collect()
20270 } else {
20271 (0..b).map(|bi| (bi, 1.0)).collect()
20272 };
20273 run_expert(se, &all, &mut out);
20274 }
20275 out
20276}
20277
20278fn moe_ffn_rows_exact(
20290 m: &MoeFfn,
20291 xs: &[f32],
20292 b: usize,
20293 hidden: usize,
20294 pool: Option<&Pool>,
20295) -> Vec<f32> {
20296 let mut out = vec![0.0f32; b * hidden];
20297 let per_row = |out: &mut [f32]| {
20298 for r in 0..b {
20299 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
20300 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
20301 }
20302 };
20303 let covered = !crate::gpu::enabled_here()
20304 && moe_batch_enabled()
20305 && m.shared.is_none()
20306 && m.resonance.is_none()
20307 && FFN_PROBE.with(|pr| pr.borrow().is_none())
20308 && m.experts.iter().all(|d| d.act == Act::Silu);
20309 if !covered {
20310 per_row(&mut out);
20311 return out;
20312 }
20313 let ne = m.experts.len();
20314 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
20316 for r in 0..b {
20317 let x = &xs[r * hidden..(r + 1) * hidden];
20318 accumulate_act(m, x, 1);
20319 let mut logits = vec![0.0f32; ne];
20320 m.router.matvec(x, &mut logits, pool);
20321 let (idx, p, wsum) = moe_route(&logits, m, None);
20322 {
20323 let mut st = m.stats.borrow_mut();
20324 if st.len() < ne {
20325 st.resize(ne, 0);
20326 }
20327 for &e in &idx {
20328 st[e] += 1;
20329 }
20330 }
20331 let w: Vec<f32> = idx
20332 .iter()
20333 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
20334 .collect();
20335 routes.push((idx, w));
20336 }
20337 if routes.iter().any(|(idx, _)| idx.is_empty()) {
20338 per_row(&mut out);
20339 return out;
20340 }
20341 let mut experts: Vec<usize> = Vec::new();
20343 let mut groups: Vec<Vec<usize>> = Vec::new();
20344 for (r, (idx, _)) in routes.iter().enumerate() {
20345 for &e in idx {
20346 match experts.iter().position(|&x| x == e) {
20347 Some(g) => groups[g].push(r),
20348 None => {
20349 experts.push(e);
20350 groups.push(vec![r]);
20351 }
20352 }
20353 }
20354 }
20355 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
20356 let inter = m.experts[experts[0]].gate_proj.rows();
20357 let pairs: Vec<(&QTensor, &QTensor)> = experts
20358 .iter()
20359 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
20360 .collect();
20361 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
20362 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
20363 per_row(&mut out);
20364 return out;
20365 }
20366 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
20367 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
20368 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
20369 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
20370 per_row(&mut out);
20371 return out;
20372 }
20373 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
20375 let mut p = 0usize;
20376 for (g, &e) in experts.iter().enumerate() {
20377 for &r in &groups[g] {
20378 slot.insert((r, e), p);
20379 p += 1;
20380 }
20381 }
20382 for (r, (idx, w)) in routes.iter().enumerate() {
20383 let terms: Vec<(&[f32], f32)> = idx
20384 .iter()
20385 .zip(w)
20386 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
20387 .collect();
20388 let row = &mut out[r * hidden..(r + 1) * hidden];
20389 for (i, dst) in row.iter_mut().enumerate() {
20390 let mut acc = 0f32;
20392 for (d, we) in &terms {
20393 acc += we * d[i];
20394 }
20395 *dst = acc;
20396 }
20397 }
20398 out
20399}
20400
20401pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
20405 match pool {
20406 Some(p) if v.len() >= 1 << 15 => {
20407 let base = v.as_mut_ptr() as usize;
20408 p.run_rows(v.len(), &|s, e| {
20409 let sl =
20412 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
20413 for x in sl {
20414 *x = c * (*x / c).tanh();
20415 }
20416 });
20417 }
20418 _ => {
20419 for x in v.iter_mut() {
20420 *x = c * (*x / c).tanh();
20421 }
20422 }
20423 }
20424}
20425
20426thread_local! {
20427 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
20431 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
20432}
20433
20434fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20436 if gate_topk() > 0
20439 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
20440 {
20441 return out;
20442 }
20443 let prism_body = d.gate_proj.has_prism_contract()
20459 || d.up_proj.has_prism_contract()
20460 || d.down_proj.has_prism_contract();
20461 if !prism_body
20462 && crate::gpu::enabled_here()
20463 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
20464 {
20465 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
20466 crate::gpu::ProbeArm::Gpu
20467 } else {
20468 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
20469 };
20470 match arm {
20471 crate::gpu::ProbeArm::Gpu => {
20472 let t0 = std::time::Instant::now();
20473 if let Some(out) = dense_ffn_gpu(d, x, pool) {
20474 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
20475 return out;
20476 }
20477 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
20481 }
20482 crate::gpu::ProbeArm::CpuTimed => {
20483 let t0 = std::time::Instant::now();
20484 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20485 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
20486 return out;
20487 }
20488 crate::gpu::ProbeArm::Cpu => {
20489 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
20490 }
20491 }
20492 }
20493 dense_ffn_cpu(d, x, pool)
20494}
20495
20496fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
20498 let inter = d.gate_proj.rows();
20499 FFN_SCRATCH.with(|s| {
20500 let mut s = s.borrow_mut();
20501 let [g, u, ..] = &mut *s;
20502 g.resize(inter, 0.0);
20503 if gate_topk() > 0 {
20506 u.resize(inter, 0.0);
20510 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20511 for i in 0..inter {
20512 g[i] = Act::Silu.combine(g[i], 1.0);
20513 }
20514 keep_top_k(g, gate_topk());
20515 for i in 0..inter {
20516 g[i] *= u[i];
20517 }
20518 } else if d.act == Act::Silu && {
20519 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20520 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
20521 } {
20522 } else {
20524 u.resize(inter, 0.0);
20525 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
20527 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20528 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
20529 for i in 0..inter {
20530 g[i] = d.act.combine(g[i], u[i]);
20531 }
20532 }
20533 FFN_PROBE.with(|pr| {
20541 if let Some(acc) = pr.borrow_mut().as_mut() {
20542 let li = crate::gpu::cur_layer();
20543 if li >= 0 {
20544 if let Some(row) = acc.get_mut(li as usize) {
20545 match probe_topk() {
20546 0 if probe_sq() => {
20547 for (a, &v) in row.iter_mut().zip(g.iter()) {
20548 *a += (v as f64) * (v as f64);
20549 }
20550 }
20551 0 if probe_signed() => {
20552 for (a, &v) in row.iter_mut().zip(g.iter()) {
20553 *a += v as f64;
20554 }
20555 }
20556 0 => {
20557 for (a, &v) in row.iter_mut().zip(g.iter()) {
20558 *a += (v as f64).abs();
20559 }
20560 }
20561 k => {
20562 let n = g.len();
20563 let k = k.min(n);
20564 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
20565 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
20566 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
20567 });
20568 let thr = *kth;
20569 for (a, &v) in row.iter_mut().zip(g.iter()) {
20570 if v.abs() >= thr {
20571 *a += 1.0;
20572 }
20573 }
20574 }
20575 }
20576 }
20577 }
20578 }
20579 });
20580 if oracle_topk() > 0 {
20581 keep_top_k(g, oracle_topk());
20582 }
20583 {
20584 let li = crate::gpu::cur_layer();
20585 if li >= 0 {
20586 adump_row(li as usize, g);
20587 }
20588 }
20589 let mut out = attention::take_buf(d.down_proj.rows());
20590 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
20591 d.down_proj.matvec(g, &mut out, pool);
20592 out
20593 })
20594}
20595
20596pub struct RefitAcc {
20609 pub support: Vec<u32>,
20610 pub gss: Vec<f32>,
20611 pub ya: Vec<f32>,
20612 pub hidden: usize,
20613 pub tokens: u64,
20614 pub buf_g: Vec<f32>,
20620 pub buf_o: Vec<f32>,
20621 pub buf_t: usize,
20622}
20623
20624type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
20628
20629static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
20630 std::sync::OnceLock::new();
20631
20632fn ffn_probe_active() -> bool {
20635 FFN_PROBE.with(|p| p.borrow().is_some())
20636}
20637
20638fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
20639 REFIT
20640 .get_or_init(|| {
20641 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
20642 (
20643 d,
20644 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
20645 )
20646 })
20647 })
20648 .as_ref()
20649}
20650
20651fn refit_accumulate(
20653 li: usize,
20654 g: &[f32],
20655 b: usize,
20656 inter: usize,
20657 out: &[f32],
20658 hidden: usize,
20659 pool: Option<&Pool>,
20660) {
20661 let Some((dir, map)) = refit_dir() else {
20662 return;
20663 };
20664 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20665 let (from, to) = *SPAN.get_or_init(|| {
20666 let g = |k: &str, d: usize| {
20667 std::env::var(k)
20668 .ok()
20669 .and_then(|v| v.parse().ok())
20670 .unwrap_or(d)
20671 };
20672 (
20673 g("CMF_FFN_REFIT_FROM", 0),
20674 g("CMF_FFN_REFIT_TO", usize::MAX),
20675 )
20676 });
20677 if li < from || li > to {
20678 return;
20679 }
20680 let mut guard = map.lock().unwrap();
20681 let (map, shared) = &mut *guard;
20682 let acc = match map.entry(li) {
20683 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
20684 std::collections::hash_map::Entry::Vacant(e) => {
20685 let path = format!("{dir}/support.{li}.u32");
20686 let Ok(bytes) = std::fs::read(&path) else {
20687 eprintln!("refit: no {path} — layer {li} skipped");
20688 return;
20689 };
20690 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
20691 let support: Vec<u32> = bytes[4..4 + n * 4]
20692 .chunks_exact(4)
20693 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
20694 .collect();
20695 eprintln!(
20696 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
20697 (n * n + hidden * n) as f64 * 4.0 / 1e6
20698 );
20699 e.insert(RefitAcc {
20700 gss: vec![0.0; n * n],
20701 ya: vec![0.0; hidden * n],
20702 buf_g: Vec::new(),
20703 buf_o: Vec::new(),
20704 buf_t: 0,
20705 support,
20706 hidden,
20707 tokens: 0,
20708 })
20709 }
20710 };
20711 let ns = acc.support.len();
20712 let cap = refit_batch();
20714 if acc.buf_g.is_empty() {
20715 acc.buf_g = vec![0.0; ns * cap];
20716 acc.buf_o = vec![0.0; hidden * cap];
20717 }
20718 let take = b.min(cap - acc.buf_t);
20719 for t in 0..take {
20720 let col = acc.buf_t + t;
20721 for (j, &n) in acc.support.iter().enumerate() {
20722 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
20723 }
20724 for h in 0..hidden {
20725 acc.buf_o[h * cap + col] = out[t * hidden + h];
20726 }
20727 }
20728 acc.buf_t += take;
20729 acc.tokens += take as u64;
20730 if acc.buf_t < cap {
20731 return;
20732 }
20733 let bt = acc.buf_t;
20734 acc.buf_t = 0;
20735 let RefitAcc {
20745 gss,
20746 ya,
20747 buf_g,
20748 buf_o,
20749 ..
20750 } = acc;
20751 let need = (ns * ns).max(hidden * ns);
20752 if shared.len() < need {
20753 shared.resize(need, 0.0);
20754 }
20755 let scratch = &mut shared[..];
20756 let _ = bt;
20757 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
20758 add_into(gss, &scratch[..ns * ns], pool);
20759 if crate::gpu::gemm_nt_f32_transient(
20760 buf_o,
20761 buf_g,
20762 &mut scratch[..hidden * ns],
20763 hidden,
20764 cap,
20765 ns,
20766 ) {
20767 add_into(ya, &scratch[..hidden * ns], pool);
20768 } else {
20769 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20770 }
20771 } else {
20772 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
20773 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
20774 }
20775 }
20779
20780fn refit_batch() -> usize {
20782 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
20783 *B.get_or_init(|| {
20784 std::env::var("CMF_FFN_REFIT_BATCH")
20785 .ok()
20786 .and_then(|v| v.parse().ok())
20787 .unwrap_or(4096)
20788 })
20789}
20790
20791fn accum_outer_t(
20794 c: &mut [f32],
20795 m: usize,
20796 n: usize,
20797 b: usize,
20798 left: &[f32],
20799 right: &[f32],
20800 pool: Option<&Pool>,
20801) {
20802 let ptr = SendMut(c.as_mut_ptr());
20803 let body = |i: usize| {
20804 let ptr = &ptr;
20805 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
20806 for t in 0..b {
20807 let a = left[i * b + t];
20808 if a == 0.0 {
20809 continue;
20810 }
20811 for (j, o) in row.iter_mut().enumerate() {
20812 *o += a * right[j * b + t];
20813 }
20814 }
20815 };
20816 match pool {
20817 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
20818 for i in s..e {
20819 body(i);
20820 }
20821 }),
20822 _ => {
20823 for i in 0..m {
20824 body(i);
20825 }
20826 }
20827 }
20828}
20829
20830fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
20833 let n = dst.len().min(src.len());
20834 match pool {
20835 Some(p) if n >= 1 << 16 => {
20836 let ptr = SendMut(dst.as_mut_ptr());
20837 let f = |s: usize, e: usize| {
20838 let ptr = &ptr;
20839 for blk in s..e {
20840 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
20841 for i in a..b {
20842 unsafe { *ptr.0.add(i) += src[i] };
20843 }
20844 }
20845 };
20846 p.run_rows(n.div_ceil(4096), &f);
20847 }
20848 _ => {
20849 for (d, v) in dst.iter_mut().zip(&src[..n]) {
20850 *d += *v;
20851 }
20852 }
20853 }
20854}
20855
20856fn accum_outer(
20861 c: &mut [f32],
20862 m: usize,
20863 n: usize,
20864 b: usize,
20865 left: &[f32],
20866 right: &[f32],
20867 pool: Option<&Pool>,
20868) {
20869 const TILE: usize = 32;
20870 let tiles = m.div_ceil(TILE);
20871 let cp = SendMut(c.as_mut_ptr());
20872 let body = |ti: usize| {
20873 let cp = &cp;
20874 let i0 = ti * TILE;
20875 let i1 = (i0 + TILE).min(m);
20876 for t in 0..b {
20877 let r = &right[t * n..t * n + n];
20878 for i in i0..i1 {
20879 let a = left[i * b + t];
20880 if a == 0.0 {
20881 continue;
20882 }
20883 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
20885 for (o, v) in row.iter_mut().zip(r) {
20886 *o += a * *v;
20887 }
20888 }
20889 }
20890 };
20891 match pool {
20892 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
20893 for ti in s..e {
20894 body(ti);
20895 }
20896 }),
20897 _ => {
20898 for ti in 0..tiles {
20899 body(ti);
20900 }
20901 }
20902 }
20903}
20904
20905pub fn refit_flush() -> usize {
20907 let Some((dir, map)) = refit_dir() else {
20908 return 0;
20909 };
20910 let guard = map.lock().unwrap();
20911 let mut n = 0;
20912 for (li, acc) in guard.0.iter() {
20913 let w = |name: &str, v: &[f32]| {
20916 let path = format!("{dir}/{name}.{li}.f32");
20917 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
20918 match std::fs::write(&path, &bytes) {
20919 Ok(()) => {}
20920 Err(e) => eprintln!(
20921 "refit: FAILED to write {path} ({} MB): {e}",
20922 bytes.len() / 1_000_000
20923 ),
20924 }
20925 };
20926 w("gss", &acc.gss);
20927 w("ya", &acc.ya);
20928 println!(
20929 "refit L{li}: {} support, {} tokens, hidden {}",
20930 acc.support.len(),
20931 acc.tokens,
20932 acc.hidden
20933 );
20934 n += 1;
20935 }
20936 n
20937}
20938
20939fn adump_row(li: usize, g: &[f32]) {
20944 use std::io::Write as _;
20945 static FILES: std::sync::OnceLock<
20946 Option<(
20947 String,
20948 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
20949 )>,
20950 > = std::sync::OnceLock::new();
20951 let Some((prefix, map)) = FILES
20952 .get_or_init(|| {
20953 std::env::var("CMF_FFN_ADUMP")
20954 .ok()
20955 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
20956 })
20957 .as_ref()
20958 else {
20959 return;
20960 };
20961 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
20964 let (from, to) = *SPAN.get_or_init(|| {
20965 let g = |k: &str, d: usize| {
20966 std::env::var(k)
20967 .ok()
20968 .and_then(|v| v.parse().ok())
20969 .unwrap_or(d)
20970 };
20971 (
20972 g("CMF_FFN_ADUMP_FROM", 0),
20973 g("CMF_FFN_ADUMP_TO", usize::MAX),
20974 )
20975 });
20976 if li < from || li > to {
20977 return;
20978 }
20979 let mut map = map.lock().unwrap();
20980 let f = map.entry(li).or_insert_with(|| {
20981 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
20982 });
20983 let mut bytes = Vec::with_capacity(g.len() * 2);
20984 for v in g {
20985 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
20986 }
20987 let _ = f.write_all(&bytes);
20988}
20989
20990fn oracle_topk() -> usize {
20996 if exact_mtp_calibration_active() {
20997 return 0;
20998 }
20999 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21000 *K.get_or_init(|| {
21001 std::env::var("CMF_FFN_ORACLE_TOPK")
21002 .ok()
21003 .and_then(|v| v.parse().ok())
21004 .unwrap_or(0)
21005 })
21006}
21007
21008fn gate_topk() -> usize {
21014 if exact_mtp_calibration_active() {
21015 return 0;
21016 }
21017 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21018 *K.get_or_init(|| {
21019 std::env::var("CMF_FFN_GATE_TOPK")
21020 .ok()
21021 .and_then(|v| v.parse().ok())
21022 .unwrap_or(0)
21023 })
21024}
21025
21026fn gate_block() -> usize {
21033 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21034 *B.get_or_init(|| {
21035 std::env::var("CMF_FFN_GATE_BLOCK")
21036 .ok()
21037 .and_then(|v| v.parse().ok())
21038 .unwrap_or(1)
21039 })
21040}
21041
21042fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
21044 let n = g.len();
21045 let nb = n.div_ceil(block);
21046 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
21047 if kb >= nb {
21048 return;
21049 }
21050 let mut score: Vec<f32> = (0..nb)
21051 .map(|b| {
21052 g[b * block..((b + 1) * block).min(n)]
21053 .iter()
21054 .map(|v| v * v)
21055 .sum::<f32>()
21056 })
21057 .collect();
21058 let mut ord = score.clone();
21059 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
21060 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21061 });
21062 let thr = *kth;
21063 for b in 0..nb {
21064 if score[b] < thr {
21065 g[b * block..((b + 1) * block).min(n)].fill(0.0);
21066 }
21067 }
21068 score.clear();
21069}
21070
21071fn keep_top_k(g: &mut [f32], k: usize) {
21073 if gate_block() > 1 {
21074 return keep_top_blocks(g, k, gate_block());
21075 }
21076 let n = g.len();
21077 if k == 0 || k >= n {
21078 return;
21079 }
21080 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
21081 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21082 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21083 });
21084 let thr = *kth;
21085 for v in g.iter_mut() {
21086 if v.abs() < thr {
21087 *v = 0.0;
21088 }
21089 }
21090}
21091
21092fn probe_sq() -> bool {
21096 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21097 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
21098}
21099
21100fn probe_signed() -> bool {
21104 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21105 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
21106}
21107
21108fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
21116 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
21117 M.get_or_init(|| {
21118 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
21119 let b = std::fs::read(&p).ok()?;
21120 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
21121 let vals: Vec<f32> = b[8..]
21122 .chunks_exact(4)
21123 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
21124 .collect();
21125 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
21126 Some((inter, vals))
21127 })
21128 .as_ref()
21129}
21130
21131fn probe_topk() -> usize {
21134 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21135 *K.get_or_init(|| {
21136 std::env::var("CMF_FFN_PROBE_TOPK")
21137 .ok()
21138 .and_then(|v| v.parse().ok())
21139 .unwrap_or(0)
21140 })
21141}
21142
21143thread_local! {
21144 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
21147 const { std::cell::RefCell::new(None) };
21148}
21149
21150fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
21163 if d.gate_proj.has_prism_contract()
21168 || d.up_proj.has_prism_contract()
21169 || d.down_proj.has_prism_contract()
21170 {
21171 return None;
21172 }
21173 let dt = d.down_t.as_ref()?;
21174 let inter = d.gate_proj.rows();
21175 let hidden = dt.cols();
21176 if k == 0 || k >= inter || d.act != Act::Silu {
21177 return None;
21178 }
21179 DYN_SCRATCH.with(|sc| {
21180 let mut sc = sc.borrow_mut();
21181 let DynScratch {
21182 g,
21183 mag,
21184 live,
21185 parts,
21186 } = &mut *sc;
21187 g.resize(inter, 0.0);
21188 d.gate_proj.matvec(x, g, pool);
21189 for v in g.iter_mut() {
21190 *v = inference::silu(*v);
21191 }
21192 mag.clear();
21195 mag.extend(g.iter().map(|v| v.abs()));
21196 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
21197 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
21198 });
21199 let thr = *kth;
21200 live.clear();
21201 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
21202 let mut out = vec![0.0f32; hidden];
21203 match pool {
21204 Some(p) if live.len() >= 64 => {
21205 let nw = p.n_workers() + 1;
21206 parts.clear();
21207 parts.resize(nw * hidden, 0.0);
21208 let ptr = SendMut(parts.as_mut_ptr());
21209 let n = live.len();
21210 let live_ref: &[u32] = live;
21211 let g_ref: &[f32] = g;
21212 p.run(&|w, workers| {
21213 let chunk = n.div_ceil(workers);
21214 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
21215 if s >= e {
21216 return;
21217 }
21218 WORKER_SCRATCH.with(|ws| {
21219 let mut ws = ws.borrow_mut();
21220 let [scratch, acc] = &mut *ws;
21221 scratch.resize(hidden.max(x.len()), 0.0);
21222 acc.clear();
21223 acc.resize(hidden, 0.0);
21224 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
21225 if let Some(&nx) = live_ref[s..e].get(o + 1) {
21228 d.up_proj.prefetch_row(nx as usize);
21229 dt.prefetch_row(nx as usize);
21230 }
21231 let idx = nrm as usize;
21232 let up = d.up_proj.row_dot(idx, x, scratch);
21233 let a = g_ref[idx] * up;
21234 if a != 0.0 {
21235 dt.add_row_scaled(idx, a, acc, scratch);
21236 }
21237 }
21238 for (j, v) in acc.iter().enumerate() {
21239 unsafe { *ptr.at(w * hidden + j) = *v };
21240 }
21241 });
21242 });
21243 for w in 0..nw {
21244 for (j, o) in out.iter_mut().enumerate() {
21245 *o += parts[w * hidden + j];
21246 }
21247 }
21248 }
21249 _ => {
21250 WORKER_SCRATCH.with(|ws| {
21251 let mut ws = ws.borrow_mut();
21252 let [scratch, _acc] = &mut *ws;
21253 scratch.resize(hidden.max(x.len()), 0.0);
21254 for &nrm in live.iter() {
21255 let idx = nrm as usize;
21256 let up = d.up_proj.row_dot(idx, x, scratch);
21257 let a = g[idx] * up;
21258 if a != 0.0 {
21259 dt.add_row_scaled(idx, a, &mut out, scratch);
21260 }
21261 }
21262 });
21263 }
21264 }
21265 Some(out)
21266 })
21267}
21268
21269struct DynScratch {
21272 g: Vec<f32>,
21273 mag: Vec<f32>,
21274 live: Vec<u32>,
21275 parts: Vec<f32>,
21276}
21277
21278thread_local! {
21279 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
21280 std::cell::RefCell::new(DynScratch {
21281 g: Vec::new(),
21282 mag: Vec::new(),
21283 live: Vec::new(),
21284 parts: Vec::new(),
21285 })
21286 };
21287 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
21289 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
21290}
21291
21292fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
21297 let inter = d.gate_proj.rows();
21298 FFN_SCRATCH.with(|s| {
21299 let mut s = s.borrow_mut();
21300 let [g, u, ..] = &mut *s;
21301 g.resize(inter, 0.0);
21302 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
21303 } else {
21305 u.resize(inter, 0.0);
21306 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
21307 for i in 0..inter {
21308 g[i] = d.act.combine(g[i], u[i]);
21309 }
21310 }
21311 zero_masked_cols(g, 1, inter, mask_row);
21312 let mut out = attention::take_buf(d.down_proj.rows());
21313 d.down_proj.matvec(g, &mut out, pool);
21314 out
21315 })
21316}
21317
21318fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
21324 if d.gate_proj.has_prism_contract()
21325 || d.up_proj.has_prism_contract()
21326 || d.down_proj.has_prism_contract()
21327 {
21328 return None;
21329 }
21330 d.act.moe_job_graph_contract()?;
21333 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
21336 return None;
21337 }
21338 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
21339 let mut model_ref = None;
21340 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
21341 let model = model_ref?;
21342 let hidden = jobs[0].down.1;
21343 let mut out = attention::take_buf(hidden);
21344 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21345 Some(out)
21346 } else {
21347 let mut out = out;
21348 attention::recycle_buf(&mut out);
21349 None
21350 }
21351}
21352
21353#[allow(clippy::type_complexity)]
21358#[allow(clippy::type_complexity)]
21359pub(crate) fn moe_parts(
21360 t: &QTensor,
21361) -> Option<(
21362 &std::sync::Arc<cortiq_core::CmfModel>,
21363 usize,
21364 usize,
21365 usize,
21366 &[f32],
21367 &[f32],
21368 bool,
21369 bool,
21370 bool,
21371)> {
21372 match t {
21373 QTensor::Mapped {
21374 model,
21375 idx,
21376 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
21377 rows,
21378 cols,
21379 row_scale,
21380 col_field,
21381 ..
21382 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
21383 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
21384 )),
21385 QTensor::Mapped {
21387 model,
21388 idx,
21389 dtype: cortiq_core::TensorDtype::Q1,
21390 rows,
21391 cols,
21392 ..
21393 } => Some((
21394 model,
21395 *idx,
21396 *rows,
21397 *cols,
21398 &[][..],
21399 &[][..],
21400 true,
21401 false,
21402 false,
21403 )),
21404 QTensor::Mapped {
21406 model,
21407 idx,
21408 dtype: cortiq_core::TensorDtype::Q4Tiled,
21409 rows,
21410 cols,
21411 ..
21412 } => Some((
21413 model,
21414 *idx,
21415 *rows,
21416 *cols,
21417 &[][..],
21418 &[][..],
21419 false,
21420 true,
21421 false,
21422 )),
21423 QTensor::Mapped {
21425 model,
21426 idx,
21427 dtype: cortiq_core::TensorDtype::Q4TiledP,
21428 rows,
21429 cols,
21430 ..
21431 } => Some((
21432 model,
21433 *idx,
21434 *rows,
21435 *cols,
21436 &[][..],
21437 &[][..],
21438 false,
21439 true,
21440 false,
21441 )),
21442 QTensor::Mapped {
21446 model,
21447 idx,
21448 dtype: cortiq_core::TensorDtype::Q2TiledP,
21449 rows,
21450 cols,
21451 ..
21452 } => Some((
21453 model,
21454 *idx,
21455 *rows,
21456 *cols,
21457 &[][..],
21458 &[][..],
21459 false,
21460 true,
21461 true,
21462 )),
21463 _ => None,
21464 }
21465}
21466
21467#[cfg(target_os = "macos")]
21474#[allow(clippy::type_complexity)]
21475fn chunk_moe_parts(
21476 m: &MoeFfn,
21477 hidden: usize,
21478) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
21479 if m.shared.is_some()
21480 || m.per_expert_scale.is_some()
21481 || m.resonance.is_some()
21482 || m.route_tau.is_some()
21483 || m.router_input_norm
21484 || m.experts.is_empty()
21485 || m.top_k == 0
21486 || std::env::var("CMF_RMS_TRACE").is_ok()
21487 || std::env::var("CMF_ACT_DUMP").is_ok()
21488 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
21489 {
21490 return None;
21491 }
21492 let (rf, rr, rc) = m.router.f32_parts()?;
21493 if rr != m.experts.len() || rc != hidden {
21494 return None;
21495 }
21496 let inter = m.experts[0].gate_proj.rows();
21497 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
21500 let experts = m
21501 .experts
21502 .iter()
21503 .map(|e| {
21504 if e.act != Act::Silu
21505 || !e.segs.is_empty()
21506 || e.gate_proj.rows() != inter
21507 || e.up_proj.rows() != inter
21508 || e.down_proj.cols() != inter
21509 || e.gate_proj.cols() != hidden
21510 || e.down_proj.rows() != hidden
21511 {
21512 return None;
21513 }
21514 if q8 {
21515 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21516 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21517 }
21518 Some((
21519 e.gate_proj.mapped_q4tp()?.1,
21520 e.up_proj.mapped_q4tp()?.1,
21521 e.down_proj.mapped_q4tp()?.1,
21522 ))
21523 })
21524 .collect::<Option<Vec<_>>>()?;
21525 Some((rf, experts, q8))
21526}
21527
21528#[cfg(target_os = "macos")]
21541fn metal_flint_moe_graph_parts(
21542 m: &MoeFfn,
21543 hidden: usize,
21544 scratch_key: usize,
21545) -> Option<crate::gpu::GpuFlintMoe<'_>> {
21546 use std::sync::Arc;
21547
21548 const HIDDEN: usize = 2_048;
21549 const ROUTED_INTER: usize = 512;
21550 const SHARED_INTER: usize = 1_024;
21551 const EXPERTS: usize = 256;
21552 const TOP_K: usize = 6;
21553
21554 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
21555 return None;
21556 }
21557 let routed0 = m.experts.first()?;
21558 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
21559 let (router, router_rows, router_cols) = m.router.f32_parts()?;
21560 if router_rows != EXPERTS || router_cols != HIDDEN {
21561 return None;
21562 }
21563 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
21564 return None;
21565 }
21566 let same_q2 = |t: &QTensor| -> Option<usize> {
21567 let (owner, idx) = t.mapped_q2tp()?;
21568 Arc::ptr_eq(&owner, &model).then_some(idx)
21569 };
21570 let same_q4 = |t: &QTensor| -> Option<usize> {
21571 let (owner, idx) = t.mapped_q4tp()?;
21572 Arc::ptr_eq(&owner, &model).then_some(idx)
21573 };
21574 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21575 if e.act != Act::Silu
21576 || !e.segs.is_empty()
21577 || e.gate_proj.rows() != ROUTED_INTER
21578 || e.gate_proj.cols() != HIDDEN
21579 || e.up_proj.rows() != ROUTED_INTER
21580 || e.up_proj.cols() != HIDDEN
21581 || e.down_proj.rows() != HIDDEN
21582 || e.down_proj.cols() != ROUTED_INTER
21583 {
21584 return None;
21585 }
21586 Some((
21587 same_q2(&e.gate_proj)?,
21588 same_q2(&e.up_proj)?,
21589 same_q4(&e.down_proj)?,
21590 ))
21591 };
21592 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21593 if e.act != Act::Silu
21594 || !e.segs.is_empty()
21595 || e.gate_proj.rows() != SHARED_INTER
21596 || e.gate_proj.cols() != HIDDEN
21597 || e.up_proj.rows() != SHARED_INTER
21598 || e.up_proj.cols() != HIDDEN
21599 || e.down_proj.rows() != HIDDEN
21600 || e.down_proj.cols() != SHARED_INTER
21601 {
21602 return None;
21603 }
21604 Some((
21605 same_q4(&e.gate_proj)?,
21606 same_q4(&e.up_proj)?,
21607 same_q4(&e.down_proj)?,
21608 ))
21609 };
21610 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
21611 if experts.len() != EXPERTS {
21612 return None;
21613 }
21614 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
21615 if shared_gate.is_some() {
21616 return None;
21617 }
21618 Some(crate::gpu::GpuFlintMoe {
21619 router,
21620 bias: m.expert_bias.as_deref(),
21621 experts,
21622 scratch_key,
21623 shared: shared(shared_ffn)?,
21624 top_k: m.top_k,
21625 norm_topk: m.norm_topk_prob,
21626 route_scale: m.routed_scaling,
21627 sigmoid: m.router_sigmoid,
21628 sigmoid_denom_eps: m.sigmoid_denom_eps,
21629 })
21630}
21631
21632#[cfg(target_os = "macos")]
21633fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
21634 if m.router_input_norm
21635 || m.route_tau.is_some()
21636 || m.mask.is_some()
21637 || m.per_expert_scale.is_some()
21638 || m.experts.is_empty()
21639 || m.top_k == 0
21640 || m.resonance.is_some()
21641 {
21642 return None;
21643 }
21644 let (sh, sg) = match &m.shared {
21649 Some((sh, sg)) => (Some(sh), sg.as_ref()),
21650 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
21651 None => return None,
21652 };
21653 let (rf, rr, rc) = m.router.f32_parts()?;
21654 if rr != m.experts.len() || rc != hidden {
21655 return None;
21656 }
21657 let shared_gated = sg.is_some();
21658 let sf = match sg {
21659 Some(sg) => {
21660 let (sf, sr, sc) = sg.f32_parts()?;
21661 if sr * sc != hidden {
21662 return None;
21663 }
21664 sf
21665 }
21666 None => &rf[..hidden],
21669 };
21670 if let Some(b) = &m.expert_bias {
21671 if b.len() != m.experts.len() {
21672 return None;
21673 }
21674 }
21675 let inter = m.experts[0].gate_proj.rows();
21676 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
21679 let q8 = crate::gpu_metal::moe_q8::enabled()
21683 && m.shared.is_none()
21684 && m.experts[0].gate_proj.q8_2f_parts().is_some();
21685 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
21686 if e.act != Act::Silu
21687 || e.gate_proj.rows() != inter
21688 || e.gate_proj.cols() != hidden
21689 || e.up_proj.rows() != inter
21690 || e.up_proj.cols() != hidden
21691 || e.down_proj.rows() != hidden
21692 || e.down_proj.cols() != inter
21693 {
21694 return None;
21695 }
21696 if q8 {
21697 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
21698 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
21699 }
21700 let pick = |t: &QTensor| -> Option<usize> {
21701 if gu_q2 {
21702 t.mapped_q2tp().map(|(_, i)| i)
21703 } else {
21704 t.mapped_q4tp().map(|(_, i)| i)
21705 }
21706 };
21707 Some((
21708 pick(&e.gate_proj)?,
21709 pick(&e.up_proj)?,
21710 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
21711 ))
21712 };
21713 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
21714 let shared = match sh {
21715 Some(sh) => Some(trio(sh)?),
21716 None => None,
21717 };
21718 Some(crate::gpu::GpuMoe {
21719 router: rf,
21720 sgate: sf,
21721 experts,
21722 shared,
21723 n_exp: m.experts.len(),
21724 top_k: m.top_k,
21725 inter,
21726 norm_topk: m.norm_topk_prob,
21727 route_scale: m.routed_scaling,
21728 gu_q2,
21729 q8,
21730 sigmoid: m.router_sigmoid,
21731 sigmoid_denom_eps: m.sigmoid_denom_eps,
21732 bias: m.expert_bias.as_deref(),
21733 shared_gated,
21734 })
21735}
21736
21737fn forced_ids_at(step: usize) -> Option<u32> {
21743 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
21744 IDS.get_or_init(|| {
21745 std::env::var("CMF_FORCE_IDS")
21746 .ok()
21747 .and_then(|p| std::fs::read_to_string(p).ok())
21748 .map(|s| {
21749 s.split_whitespace()
21750 .filter_map(|t| t.parse().ok())
21751 .collect()
21752 })
21753 .unwrap_or_default()
21754 })
21755 .get(step)
21756 .copied()
21757}
21758
21759pub(crate) fn moe_push_job_parts<'a>(
21763 gate: &'a QTensor,
21764 up: &'a QTensor,
21765 down: &'a QTensor,
21766 x: &[f32],
21767 w: f32,
21768 swiglu_limit: f32,
21769 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21770 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21771) -> Option<()> {
21772 use crate::qtensor::prescale;
21773 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
21774 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
21775 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
21776 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21777 return None; }
21779 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
21782 return None;
21783 }
21784 if !gq2 && dq2 {
21785 return None;
21786 }
21787 model_ref.get_or_insert_with(|| gm.clone());
21788 let dt = |cf: &[f32]| {
21789 if cf.is_empty() {
21790 cortiq_core::TensorDtype::Q8Row
21791 } else {
21792 cortiq_core::TensorDtype::Q8_2f
21793 }
21794 };
21795 jobs.push(crate::gpu::MoeJob {
21796 gate: (gi, gr, gc, grs),
21797 up: (ui, ur, uc, urs),
21798 down: (di, dr, dc, drs),
21799 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
21800 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
21801 down_col: dcf,
21802 w,
21803 q1: gq1,
21804 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
21805 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
21806 gu_q2: gq2,
21807 swiglu_limit,
21808 act: 0,
21809 });
21810 Some(())
21811}
21812
21813fn moe_push_job<'a>(
21815 d: &'a DenseFfn,
21816 x: &[f32],
21817 w: f32,
21818 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
21819 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
21820) -> Option<()> {
21821 use crate::qtensor::prescale;
21822 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
21827 let act = graph_act.code();
21828 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
21829 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
21830 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
21831 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
21832 return None; }
21834 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
21835 return None;
21836 }
21837 if !gq2 && dq2 {
21838 return None;
21839 }
21840 model_ref.get_or_insert_with(|| gm.clone());
21841 let gdt = if gcf.is_empty() {
21842 cortiq_core::TensorDtype::Q8Row
21843 } else {
21844 cortiq_core::TensorDtype::Q8_2f
21845 };
21846 let udt = if ucf.is_empty() {
21847 cortiq_core::TensorDtype::Q8Row
21848 } else {
21849 cortiq_core::TensorDtype::Q8_2f
21850 };
21851 jobs.push(crate::gpu::MoeJob {
21852 gate: (gi, gr, gc, grs),
21853 up: (ui, ur, uc, urs),
21854 down: (di, dr, dc, drs),
21855 xs_gate: prescale(x, gcf, gdt).into_owned(),
21856 xs_up: prescale(x, ucf, udt).into_owned(),
21857 down_col: dcf,
21858 w,
21859 q1: gq1,
21860 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
21861 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
21862 gu_q2: gq2,
21863 swiglu_limit,
21864 act,
21865 });
21866 Some(())
21867}
21868
21869fn sparse_ffn_quant(
21876 d: &DenseFfn,
21877 x: &[f32],
21878 active: &[u16],
21879 hidden: usize,
21880 pool: Option<&Pool>,
21881) -> Vec<f32> {
21882 let n = active.len();
21883 let inter = d.gate_proj.rows();
21884 let mut act = vec![0.0f32; n];
21885 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
21888 let compute = |ai: usize| -> f32 {
21889 let idx = active[ai] as usize;
21890 if idx >= inter {
21891 return 0.0; }
21893 let mut s = if need_scratch {
21894 vec![0.0f32; hidden]
21895 } else {
21896 Vec::new()
21897 };
21898 let gate = d.gate_proj.row_dot(idx, x, &mut s);
21899 let up = d.up_proj.row_dot(idx, x, &mut s);
21900 d.act.combine(gate, up)
21901 };
21902 match pool {
21903 Some(p) if n >= 256 => {
21904 let ptr = SendMut(act.as_mut_ptr());
21905 p.run(&|widx, nw| {
21906 let chunk = n.div_ceil(nw);
21907 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
21908 for ai in s..e {
21909 unsafe { *ptr.at(ai) = compute(ai) };
21910 }
21911 });
21912 }
21913 _ => {
21914 for (ai, a) in act.iter_mut().enumerate() {
21915 *a = compute(ai);
21916 }
21917 }
21918 }
21919 let mut out = vec![0.0f32; hidden];
21921 for (ai, &idx) in active.iter().enumerate() {
21922 let w = act[ai];
21923 if w.abs() >= 1e-12 && (idx as usize) < inter {
21924 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
21925 }
21926 }
21927 out
21928}
21929
21930#[doc(hidden)]
21932pub fn sparse_ffn_quant_for_test(
21933 d: &DenseFfn,
21934 x: &[f32],
21935 active: &[u16],
21936 hidden: usize,
21937) -> Vec<f32> {
21938 sparse_ffn_quant(d, x, active, hidden, None)
21939}
21940
21941fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
21945 let deq = |t: &QTensor| -> Vec<f32> {
21946 let (rows, cols) = (t.rows(), t.cols());
21947 let mut out = vec![0.0f32; rows * cols];
21948 for r in 0..rows {
21949 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
21950 }
21951 out
21952 };
21953 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
21954}
21955
21956struct SendMut(*mut f32);
21958unsafe impl Send for SendMut {}
21959unsafe impl Sync for SendMut {}
21960impl SendMut {
21961 #[inline]
21962 #[allow(clippy::mut_from_ref)]
21965 unsafe fn at(&self, i: usize) -> &mut f32 {
21966 unsafe { &mut *self.0.add(i) }
21967 }
21968}
21969
21970pub(crate) fn moe_route(
21982 logits: &[f32],
21983 m: &MoeFfn,
21984 allowed: Option<&[bool]>,
21985) -> (Vec<usize>, Vec<f32>, f32) {
21986 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
21987}
21988
21989pub(crate) fn moe_route_with_eps(
21999 logits: &[f32],
22000 m: &MoeFfn,
22001 allowed: Option<&[bool]>,
22002 sigmoid_denom_eps: f32,
22003) -> (Vec<usize>, Vec<f32>, f32) {
22004 let ne = logits.len();
22005 let admit = |e: usize| {
22011 m.mask.as_ref().is_none_or(|mk| mk[e])
22012 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
22013 };
22014 if m.resonance.is_some() && m.top_k == 1 {
22027 let mut best: Option<usize> = None;
22028 for e in (0..ne).filter(|&e| admit(e)) {
22029 let l = logits[e];
22030 if l == f32::NEG_INFINITY || l.is_nan() {
22031 continue;
22032 }
22033 if best.is_none_or(|b| l > logits[b]) {
22034 best = Some(e);
22035 }
22036 }
22037 if let Some(b) = best {
22038 let mut p = vec![0.0f32; ne];
22039 p[b] = 1.0;
22040 return (vec![b], p, 1.0 / m.routed_scaling);
22041 }
22042 }
22043 let p: Vec<f32> = if m.router_sigmoid {
22049 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
22050 } else {
22051 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
22052 if mx == f32::NEG_INFINITY {
22053 vec![1.0 / ne.max(1) as f32; ne]
22054 } else {
22055 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
22056 let s: f32 = e.iter().sum();
22057 for v in &mut e {
22058 *v /= s;
22059 }
22060 e
22061 }
22062 };
22063 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
22064 match &m.expert_bias {
22066 Some(b) => idx.sort_unstable_by(|&x, &y| {
22067 (p[y] + b[y])
22068 .partial_cmp(&(p[x] + b[x]))
22069 .unwrap()
22070 .then(x.cmp(&y))
22071 }),
22072 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
22073 }
22074 idx.truncate(m.top_k);
22075 if let Some(tau) = m.route_tau {
22079 let total: f32 = idx.iter().map(|&e| p[e]).sum();
22080 if total > 0.0 {
22081 let mut acc = 0.0f32;
22082 let mut keep = idx.len();
22083 for (i, &e) in idx.iter().enumerate() {
22084 acc += p[e];
22085 if acc >= tau * total {
22086 keep = i + 1;
22087 break;
22088 }
22089 }
22090 idx.truncate(keep);
22091 }
22092 }
22093 let wsum: f32 = if m.norm_topk_prob {
22094 let s: f32 = idx.iter().map(|&e| p[e]).sum();
22095 (if m.router_sigmoid {
22099 s + sigmoid_denom_eps
22100 } else {
22101 s
22102 }) / m.routed_scaling
22103 } else {
22104 1.0 / m.routed_scaling
22105 };
22106 (idx, p, wsum)
22107}
22108
22109fn moe_trace(idx: &[usize]) {
22111 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
22112}
22113
22114pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
22117 use std::io::Write;
22118 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
22119 std::sync::OnceLock::new();
22120 let Some(f) = F.get_or_init(|| {
22121 let p = std::env::var("CMF_MOE_TRACE").ok()?;
22122 Some(std::sync::Mutex::new(
22123 std::fs::OpenOptions::new()
22124 .create(true)
22125 .append(true)
22126 .open(p)
22127 .ok()?,
22128 ))
22129 }) else {
22130 return;
22131 };
22132 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
22133 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
22134}
22135
22136pub(crate) fn moe_ffn(
22139 m: &MoeFfn,
22140 x: &[f32],
22141 pool: Option<&Pool>,
22142 allowed: Option<&[bool]>,
22143) -> Vec<f32> {
22144 let r = moe_ffn_route(m, x, pool, allowed);
22145 moe_ffn_experts(m, x, &r, pool)
22146}
22147
22148pub(crate) struct MoeRoute {
22152 pub idx: Vec<usize>,
22153 pub p: Vec<f32>,
22154 pub wsum: f32,
22155 pub logits: Vec<f32>,
22156}
22157
22158pub(crate) fn moe_ffn_route(
22164 m: &MoeFfn,
22165 x: &[f32],
22166 pool: Option<&Pool>,
22167 allowed: Option<&[bool]>,
22168) -> MoeRoute {
22169 accumulate_act(m, x, 1);
22170 let ne = m.experts.len();
22171 let mut logits = vec![0.0f32; ne];
22172 match &m.resonance {
22173 Some(r) => r.scores(x, &mut logits),
22174 None => m.router.matvec(x, &mut logits, pool),
22175 }
22176 let (idx, p, wsum) = moe_route(&logits, m, allowed);
22177 {
22178 let mut st = m.stats.borrow_mut();
22179 if st.len() < ne {
22180 st.resize(ne, 0);
22181 }
22182 for &e in &idx {
22183 st[e] += 1;
22184 }
22185 }
22186 moe_trace(&idx);
22192 MoeRoute {
22193 idx,
22194 p,
22195 wsum,
22196 logits,
22197 }
22198}
22199
22200#[cfg(target_os = "macos")]
22209fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
22210 const HIDDEN: usize = 2_048;
22211 const ROUTED_INTER: usize = 512;
22212 const SHARED_INTER: usize = 1_024;
22213 const EXPERTS: usize = 256;
22214 const TOP_K: usize = 6;
22215
22216 let Some(routed) = m.experts.first() else {
22217 return false;
22218 };
22219 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
22220 return false;
22221 };
22222 let Some((shared, shared_gate)) = &m.shared else {
22223 return false;
22224 };
22225 if shared_gate.is_some()
22226 || model.header.arch.arch_name != "flint"
22227 || model.header.arch.prism_hadamard.is_some()
22228 || model.header.arch.hidden_size != HIDDEN
22229 || m.experts.len() != EXPERTS
22230 || m.top_k != TOP_K
22231 || !m.router_sigmoid
22232 || !m.norm_topk_prob
22233 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
22234 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
22235 || m.route_tau.is_some()
22236 || m.mask.is_some()
22237 || m.per_expert_scale.is_some()
22238 || m.router_input_norm
22239 || m.resonance.is_some()
22240 || !m.grown.is_empty()
22241 {
22242 return false;
22243 }
22244
22245 let routed_q2 = routed.act == Act::Silu
22246 && routed.segs.is_empty()
22247 && routed.gate_proj.rows() == ROUTED_INTER
22248 && routed.gate_proj.cols() == HIDDEN
22249 && routed.up_proj.rows() == ROUTED_INTER
22250 && routed.up_proj.cols() == HIDDEN
22251 && routed.down_proj.rows() == HIDDEN
22252 && routed.down_proj.cols() == ROUTED_INTER
22253 && routed.up_proj.mapped_q2tp().is_some()
22254 && routed.down_proj.mapped_q4tp().is_some();
22255 let shared_q4 = shared.act == Act::Silu
22256 && shared.segs.is_empty()
22257 && shared.gate_proj.rows() == SHARED_INTER
22258 && shared.gate_proj.cols() == HIDDEN
22259 && shared.up_proj.rows() == SHARED_INTER
22260 && shared.up_proj.cols() == HIDDEN
22261 && shared.down_proj.rows() == HIDDEN
22262 && shared.down_proj.cols() == SHARED_INTER
22263 && shared.gate_proj.mapped_q4tp().is_some()
22264 && shared.up_proj.mapped_q4tp().is_some()
22265 && shared.down_proj.mapped_q4tp().is_some();
22266 routed_q2 && shared_q4
22267}
22268
22269pub(crate) fn moe_ffn_experts(
22272 m: &MoeFfn,
22273 x: &[f32],
22274 r: &MoeRoute,
22275 pool: Option<&Pool>,
22276) -> Vec<f32> {
22277 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
22278 if crate::gpu::enabled_here() {
22283 #[cfg(target_os = "macos")]
22284 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
22285 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
22291 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
22292 });
22293 }
22294 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
22295 crate::gpu::ProbeArm::Gpu => {
22296 let t0 = std::time::Instant::now();
22297 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
22298 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
22299 return out;
22300 }
22301 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
22302 }
22303 crate::gpu::ProbeArm::CpuTimed => {
22304 let t0 = std::time::Instant::now();
22305 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22306 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
22307 return out;
22308 }
22309 crate::gpu::ProbeArm::Cpu => {
22310 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
22311 }
22312 }
22313 }
22314 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
22315}
22316
22317fn moe_ffn_minicpm_tail(
22321 slot: &mut crate::minicpm_tail::MiniCpmTail,
22322 li: usize,
22323 total_layers: usize,
22324 m: &MoeFfn,
22325 x: &[f32],
22326 pool: Option<&Pool>,
22327) -> Vec<f32> {
22328 let route = moe_ffn_route(m, x, pool, None);
22329 match slot.forward(li, m, x, &route, pool, total_layers) {
22330 Some(out) => out,
22331 None => moe_ffn_experts(m, x, &route, pool),
22336 }
22337}
22338
22339fn moe_ffn_banked(
22343 slot: &mut crate::mimo_moe::Slot,
22344 li: usize,
22345 m: &MoeFfn,
22346 x: &[f32],
22347 pool: Option<&Pool>,
22348) -> Vec<f32> {
22349 let t0 = std::time::Instant::now();
22350 let r = moe_ffn_route(m, x, pool, None);
22351 slot.note_route(t0.elapsed().as_nanos() as u64);
22352 match slot.forward(li, m, x, &r, pool) {
22353 Some(out) => out,
22354 None => crate::qtensor::float_activations_scope(|| {
22355 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
22356 }),
22357 }
22358}
22359
22360fn moe_ffn_banked_rows(
22362 slot: &mut crate::mimo_moe::Slot,
22363 li: usize,
22364 m: &MoeFfn,
22365 xs: &[f32],
22366 b: usize,
22367 hidden: usize,
22368 pool: Option<&Pool>,
22369) -> Vec<f32> {
22370 let t0 = std::time::Instant::now();
22371 let routes: Vec<_> = xs
22372 .chunks_exact(hidden)
22373 .map(|x| moe_ffn_route(m, x, pool, None))
22374 .collect();
22375 slot.note_route(t0.elapsed().as_nanos() as u64);
22376 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
22377 return out;
22378 }
22379 let mut out = Vec::with_capacity(b * hidden);
22380 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
22381 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
22382 crate::qtensor::float_activations_scope(|| {
22384 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
22385 })
22386 });
22387 out.extend(row);
22388 }
22389 out
22390}
22391
22392fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
22397 use std::sync::atomic::{AtomicBool, Ordering};
22398 if built {
22399 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
22400 if total_layers > 0 && layers_run < total_layers {
22401 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
22402 } else {
22403 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
22404 }
22405 } else {
22406 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
22407 }
22408 static SAID: AtomicBool = AtomicBool::new(false);
22409 if !SAID.swap(true, Ordering::Relaxed) {
22410 if built {
22411 tracing::info!("wgpu whole-token graph: ACTIVE");
22412 } else {
22413 tracing::warn!("wgpu whole-token graph refused — per-op path");
22414 }
22415 }
22416}
22417
22418pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22422pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22423pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22427pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22429
22430pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22434pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22435pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
22436 std::sync::atomic::AtomicU64::new(0);
22437pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22438pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22439pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
22443 std::sync::atomic::AtomicU64::new(0);
22444pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22445pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
22446 std::sync::atomic::AtomicU64::new(0);
22447pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
22448 std::sync::atomic::AtomicU64::new(0);
22449
22450fn moe_batch_enabled() -> bool {
22453 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
22454 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
22455}
22456
22457fn moe_ffn_cpu_batched(
22463 m: &MoeFfn,
22464 x: &[f32],
22465 idx: &[usize],
22466 p: &[f32],
22467 wsum: f32,
22468 pool: Option<&Pool>,
22469) -> Option<Vec<f32>> {
22470 if idx.is_empty() || !moe_batch_enabled() {
22471 return None;
22472 }
22473 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
22477 return None;
22478 }
22479 let n = idx.len() + usize::from(m.shared.is_some());
22480 let mut pairs = Vec::with_capacity(n);
22481 let mut downs = Vec::with_capacity(n);
22482 let mut ws = Vec::with_capacity(n);
22483 for &e in idx {
22484 let d = &m.experts[e];
22485 if d.act != Act::Silu {
22486 return None;
22487 }
22488 pairs.push((&d.gate_proj, &d.up_proj));
22489 downs.push(&d.down_proj);
22490 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
22491 }
22492 if let Some((se, gate)) = &m.shared {
22495 if se.act != Act::Silu {
22496 return None;
22497 }
22498 let g = gate.as_ref().map_or(1.0, |gate| {
22499 let mut gl = [0.0f32; 1];
22500 gate.matvec(x, &mut gl, pool);
22501 1.0 / (1.0 + (-gl[0]).exp())
22502 });
22503 pairs.push((&se.gate_proj, &se.up_proj));
22504 downs.push(&se.down_proj);
22505 ws.push(g);
22506 }
22507 let inter = pairs[0].0.rows();
22508 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
22509 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
22510 return None;
22511 }
22512 let mut out = attention::take_buf(x.len());
22513 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
22514 attention::recycle_buf(&mut out);
22515 return None;
22516 }
22517 Some(out)
22518}
22519
22520pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22528pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22529pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22530pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22531pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
22532
22533pub(crate) fn moe_cold_experts_cpu(
22534 experts: &[(&DenseFfn, f32)],
22535 x: &[f32],
22536 pool: Option<&Pool>,
22537) -> Vec<f32> {
22538 let mut out = attention::take_buf(x.len());
22539 if experts.is_empty() {
22540 return out;
22541 }
22542 let pairs: Vec<_> = experts
22543 .iter()
22544 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
22545 .collect();
22546 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
22547 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
22548 let inter = experts[0].0.gate_proj.rows();
22549 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
22550 let silu_only = experts
22558 .iter()
22559 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
22560 if silu_only
22561 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
22562 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
22563 {
22564 return out;
22565 }
22566 out.fill(0.0);
22567 for &(expert, weight) in experts {
22568 let mut one = dense_ffn(expert, x, pool);
22569 for (o, v) in out.iter_mut().zip(&one) {
22570 *o += weight * v;
22571 }
22572 attention::recycle_buf(&mut one);
22573 }
22574 out
22575}
22576
22577pub(crate) fn moe_cold_experts_rows_cpu(
22581 jobs: &[Vec<(&DenseFfn, f32)>],
22582 xs: &[f32],
22583 hidden: usize,
22584 pool: Option<&Pool>,
22585) -> Vec<f32> {
22586 let mut out = vec![0.0; xs.len()];
22587 let mut experts: Vec<&DenseFfn> = Vec::new();
22588 let mut groups: Vec<Vec<usize>> = Vec::new();
22589 let mut terms = vec![Vec::new(); jobs.len()];
22590 for (r, row) in jobs.iter().enumerate() {
22591 for &(e, w) in row {
22592 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
22593 Some(g) => g,
22594 None => {
22595 experts.push(e);
22596 groups.push(Vec::new());
22597 groups.len() - 1
22598 }
22599 };
22600 terms[r].push((g, groups[g].len(), w));
22601 groups[g].push(r);
22602 }
22603 }
22604 if experts.is_empty() {
22605 return out;
22606 }
22607 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
22608 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
22609 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
22610 let count: usize = lens.iter().sum();
22611 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
22612 let mut ds = vec![vec![0.0; hidden]; count];
22613 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
22614 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
22615 {
22616 let mut offset = 0;
22617 let offsets: Vec<_> = lens
22618 .iter()
22619 .map(|&n| {
22620 let start = offset;
22621 offset += n;
22622 start
22623 })
22624 .collect();
22625 for (r, terms) in terms.iter().enumerate() {
22626 for &(g, slot, w) in terms {
22627 for (o, &v) in out[r * hidden..(r + 1) * hidden]
22628 .iter_mut()
22629 .zip(&ds[offsets[g] + slot])
22630 {
22631 *o += w * v;
22632 }
22633 }
22634 }
22635 } else {
22636 for (r, jobs) in jobs.iter().enumerate() {
22637 if !jobs.is_empty() {
22638 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
22639 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
22640 attention::recycle_buf(&mut row);
22641 }
22642 }
22643 }
22644 out
22645}
22646
22647fn moe_ffn_cpu(
22649 m: &MoeFfn,
22650 x: &[f32],
22651 idx: &[usize],
22652 p: &[f32],
22653 wsum: f32,
22654 pool: Option<&Pool>,
22655) -> Vec<f32> {
22656 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
22657 return out;
22658 }
22659 let mut out = attention::take_buf(x.len());
22660 for &e in idx {
22661 let mut eo = dense_ffn(&m.experts[e], x, pool);
22662 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
22663 for i in 0..out.len() {
22664 out[i] += w * eo[i];
22665 }
22666 attention::recycle_buf(&mut eo);
22667 }
22668 if let Some((se, gate)) = &m.shared {
22669 let mut so = dense_ffn(se, x, pool);
22670 let g = gate.as_ref().map_or(1.0, |gate| {
22671 let mut gl = [0.0f32; 1];
22672 gate.matvec(x, &mut gl, pool);
22673 1.0 / (1.0 + (-gl[0]).exp())
22674 });
22675 for i in 0..out.len() {
22676 out[i] += g * so[i];
22677 }
22678 attention::recycle_buf(&mut so);
22679 }
22680 out
22681}
22682
22683#[allow(clippy::too_many_arguments)]
22691pub(crate) fn mla_attention(
22692 w: &MlaWeights,
22693 normed: &[f32],
22694 cache: &mut crate::kv_cache::LayerKvCache,
22695 position: usize,
22696 inv_freq: &[f32],
22697 rope_scale: f32,
22698 eps: f64,
22699 pool: Option<&Pool>,
22700) -> Vec<f32> {
22701 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
22702 let hd = dr + dn;
22703 let mut q = vec![0.0f32; nh * hd];
22704 match (&w.q_a, &w.q_a_norm) {
22705 (Some(qa), Some(qn)) => {
22706 let mut t = vec![0.0f32; qa.rows()];
22707 qa.matvec(normed, &mut t, pool);
22708 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
22709 w.q_proj.matvec(&tn, &mut q, pool);
22710 }
22711 _ => w.q_proj.matvec(normed, &mut q, pool),
22712 }
22713 let mut ca = vec![0.0f32; lora + dr];
22714 w.kv_a.matvec(normed, &mut ca, pool);
22715 let (c_lat, k_rope) = ca.split_at_mut(lora);
22716 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
22717 let mut kvb = vec![0.0f32; nh * (dn + dv)];
22718 w.kv_b.matvec(&latn, &mut kvb, pool);
22719 if !w.nope {
22720 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
22721 }
22722 for h in 0..nh {
22723 if !w.nope {
22724 attention::rope_rotate_scaled(
22725 &mut q[h * hd..h * hd + dr],
22726 position,
22727 inv_freq,
22728 rope_scale,
22729 );
22730 }
22731 }
22732 let mut k = vec![0.0f32; nh * hd];
22733 let mut v = vec![0.0f32; nh * hd];
22734 for h in 0..nh {
22735 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
22736 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
22737 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
22738 }
22739 cache.append(&k, &v, &vec![true; nh]);
22740 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
22741 attention::recycle_buf(&mut imp);
22742 let mut ov = vec![0.0f32; nh * dv];
22743 for h in 0..nh {
22744 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
22745 }
22746 let mut out = vec![0.0f32; w.o_proj.rows()];
22747 w.o_proj.matvec(&ov, &mut out, pool);
22748 out
22749}
22750
22751fn dense_moe_ffn(
22758 dm: &DenseMoeFfn,
22759 x_normed: &[f32],
22760 h_raw: &[f32],
22761 eps: f64,
22762 norm_style: NormStyle,
22763 pool: Option<&Pool>,
22764) -> Vec<f32> {
22765 let mut d = dense_ffn(&dm.dense, x_normed, pool);
22766 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
22767 let m = &dm.moe;
22768 let ne = m.experts.len();
22769 let mut logits = vec![0.0f32; ne];
22770 if m.router_input_norm {
22771 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
22772 let inv = 1.0 / (ss + eps as f32).sqrt();
22773 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
22774 m.router.matvec(&xr, &mut logits, pool);
22775 } else {
22776 m.router.matvec(h_raw, &mut logits, pool);
22777 }
22778 let (idx, p, wsum) = moe_route(&logits, m, None);
22779 {
22780 let mut st = m.stats.borrow_mut();
22781 if st.len() < ne {
22782 st.resize(ne, 0);
22783 }
22784 for &e in &idx {
22785 st[e] += 1;
22786 }
22787 }
22788 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
22789 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
22790 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
22791 for (di, mi) in d.iter_mut().zip(&mo) {
22792 *di += mi;
22793 }
22794 d
22795}
22796
22797fn moe_gpu_refused(why: &'static str) {
22804 use std::sync::atomic::{AtomicBool, Ordering};
22805 static SAID: AtomicBool = AtomicBool::new(false);
22806 if !SAID.swap(true, Ordering::Relaxed) {
22807 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
22808 }
22809}
22810
22811fn moe_ffn_gpu(
22812 m: &MoeFfn,
22813 x: &[f32],
22814 idx: &[usize],
22815 p: &[f32],
22816 wsum: f32,
22817 pool: Option<&Pool>,
22818) -> Option<Vec<f32>> {
22819 use crate::gpu::MoeJob;
22820
22821 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
22822 let mut model_ref = None;
22823 for &e in idx {
22824 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
22825 moe_gpu_refused("push_job(expert)");
22826 return None;
22827 }
22828 }
22829 if let Some((se, gate)) = &m.shared {
22830 let g = gate.as_ref().map_or(1.0, |gate| {
22831 let mut gl = [0.0f32; 1];
22832 gate.matvec(x, &mut gl, pool);
22833 1.0 / (1.0 + (-gl[0]).exp())
22834 });
22835 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
22836 moe_gpu_refused("push_job(shared)");
22837 return None;
22838 }
22839 }
22840 let Some(model) = model_ref else {
22841 moe_gpu_refused("no model_ref");
22842 return None;
22843 };
22844 let hidden = jobs[0].down.1;
22845 let mut out = vec![0.0f32; hidden];
22846 if crate::gpu::moe_block(&model, &jobs, &mut out) {
22847 Some(out)
22848 } else {
22849 moe_gpu_refused("gpu::moe_block");
22850 None
22851 }
22852}
22853
22854fn ffn_forward(
22856 ffn: &FfnKind,
22857 x: &[f32],
22858 pool: Option<&Pool>,
22859 experts_allowed: Option<&[bool]>,
22860) -> Vec<f32> {
22861 match ffn {
22862 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
22863 FfnKind::Dense(d) => dense_ffn(d, x, pool),
22864 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
22865 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22869 }
22870}
22871
22872fn ffn_forward_pair(
22876 ffn: &FfnKind,
22877 x1: &[f32],
22878 x2: &[f32],
22879 pool: Option<&Pool>,
22880 experts_allowed: Option<&[bool]>,
22881) -> (Vec<f32>, Vec<f32>) {
22882 let d = match ffn {
22883 FfnKind::Dense(d) if !d.segs.is_empty() => {
22886 return (
22887 tube_ffn(d, x1, 1, pool, None),
22888 tube_ffn(d, x2, 1, pool, None),
22889 );
22890 }
22891 FfnKind::Dense(d) => d,
22892 FfnKind::Moe(m) => {
22893 return (
22894 moe_ffn(m, x1, pool, experts_allowed),
22895 moe_ffn(m, x2, pool, experts_allowed),
22896 );
22897 }
22898 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
22899 };
22900 let inter = d.gate_proj.rows();
22901 FFN_SCRATCH.with(|s| {
22902 let mut s = s.borrow_mut();
22903 let [g1, g2, u1, u2] = &mut *s;
22904 g1.resize(inter, 0.0);
22905 g2.resize(inter, 0.0);
22906 u1.resize(inter, 0.0);
22907 u2.resize(inter, 0.0);
22908 QTensor::matvec2_many(
22911 [&d.gate_proj, &d.up_proj],
22912 x1,
22913 x2,
22914 [g1.as_mut_slice(), u1.as_mut_slice()],
22915 [g2.as_mut_slice(), u2.as_mut_slice()],
22916 pool,
22917 );
22918 for i in 0..inter {
22919 g1[i] = d.act.combine(g1[i], u1[i]);
22920 g2[i] = d.act.combine(g2[i], u2[i]);
22921 }
22922 let mut o1 = attention::take_buf(d.down_proj.rows());
22923 let mut o2 = attention::take_buf(d.down_proj.rows());
22924 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
22925 (o1, o2)
22926 })
22927}
22928
22929#[cfg(test)]
22930mod tests {
22931 use std::ffi::OsString;
22932 use std::sync::Mutex;
22933
22934 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
22937
22938 struct ScopedTestEnv {
22939 previous: Vec<(&'static str, Option<OsString>)>,
22940 }
22941
22942 impl ScopedTestEnv {
22943 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
22944 let previous = vars
22945 .iter()
22946 .map(|(name, _)| (*name, std::env::var_os(name)))
22947 .collect();
22948 for &(name, value) in vars {
22949 unsafe {
22950 match value {
22951 Some(value) => std::env::set_var(name, value),
22952 None => std::env::remove_var(name),
22953 }
22954 }
22955 }
22956 Self { previous }
22957 }
22958 }
22959
22960 impl Drop for ScopedTestEnv {
22961 fn drop(&mut self) {
22962 for (name, value) in self.previous.drain(..).rev() {
22963 unsafe {
22964 match value {
22965 Some(value) => std::env::set_var(name, value),
22966 None => std::env::remove_var(name),
22967 }
22968 }
22969 }
22970 }
22971 }
22972
22973 #[test]
22974 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
22975 use super::pad_mtp_draft_logits;
22976
22977 let mut logits = vec![-3.0, -1.0, -2.0];
22978 pad_mtp_draft_logits(&mut logits, 6);
22979 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
22980 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
22981 assert_eq!(
22982 logits
22983 .iter()
22984 .enumerate()
22985 .max_by(|(_, a), (_, b)| a.total_cmp(b))
22986 .map(|(i, _)| i),
22987 Some(1),
22988 "an unevaluated row cannot win an all-negative shortlist"
22989 );
22990 }
22991
22992 #[test]
22993 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
22994 use super::{Pipeline, SpecMon, SpecTrial};
22995
22996 let mut mon = SpecMon::default();
22997 let start = std::time::Instant::now();
22998 mon.round(1.0, 1);
22999 let first = Pipeline::spec_trial_round(
23000 SpecTrial::Spec {
23001 t0: start,
23002 gen0: 0,
23003 rounds: 0,
23004 },
23005 &mut mon,
23006 1,
23007 );
23008 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
23009
23010 mon.round(1.0, 1);
23011 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
23012 assert!(
23013 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
23014 "two verifier-tip-only rounds must stop the losing MTP trial"
23015 );
23016
23017 let mut viable = SpecMon::default();
23020 viable.round(1.0, 1);
23021 viable.round(1.0, 2);
23022 let continued = Pipeline::spec_trial_round(
23023 SpecTrial::Spec {
23024 t0: start,
23025 gen0: 0,
23026 rounds: 1,
23027 },
23028 &mut viable,
23029 3,
23030 );
23031 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
23032 }
23033
23034 #[test]
23035 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
23036 let mut p = spark_test_pipeline(None);
23037 let head_rows = 248_070;
23038 p.draft_full_streak = 7;
23039 assert_eq!(p.draft_head_rows(head_rows), head_rows);
23040
23041 p.reset_fresh_mtp_draft_state();
23042 assert_eq!(
23043 p.draft_head_rows(head_rows),
23044 super::Pipeline::draft_vocab_rows(head_rows),
23045 "a fresh request returns to its configured shortlist"
23046 );
23047
23048 p.note_draft_id(u32::MAX);
23051 assert_eq!(p.draft_head_rows(head_rows), head_rows);
23052 }
23053
23054 #[test]
23055 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
23056 use super::graph_spec_wanted_by_policy;
23057
23058 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
23062 assert!(!graph_spec_wanted_by_policy(
23063 Some("anything-but-1"),
23064 true,
23065 false,
23066 false,
23067 true,
23068 ));
23069 assert!(graph_spec_wanted_by_policy(
23070 Some("1"),
23071 false,
23072 true,
23073 false,
23074 true,
23075 ));
23076 assert!(!graph_spec_wanted_by_policy(
23077 Some("0"),
23078 true,
23079 false,
23080 false,
23081 true,
23082 ));
23083
23084 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
23087 assert!(graph_spec_wanted_by_policy(
23088 Some("legacy-force"),
23089 false,
23090 true,
23091 true,
23092 false,
23093 ));
23094 }
23095
23096 #[test]
23097 fn encoder_hidden_states_are_normalized_and_reusable() {
23098 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
23099 let ids = [1, 7, 11, 19];
23100
23101 let first = p.encode_hidden_states(&ids).expect("encoder forward");
23102 let second = p
23103 .encode_hidden_states(&ids)
23104 .expect("reusable encoder forward");
23105 assert_eq!(first.len(), ids.len() * 8);
23106 assert!(first.iter().all(|v| v.is_finite()));
23107 assert_eq!(
23108 first, second,
23109 "encoder forward must start from a fresh state"
23110 );
23111
23112 assert!(p.forward_ids(&ids, None).is_ok());
23115 }
23116
23117 #[test]
23122 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
23123 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
23124 let dense_card = ChunkStackFacts {
23125 plain_dense: true,
23126 discrete: true,
23127 gpu_on: true,
23128 ..Default::default()
23129 };
23130 assert!(dense_card.dense_on_discrete());
23131 assert_eq!(
23133 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
23134 DISCRETE_DENSE_PREFILL_CHUNK
23135 );
23136 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
23137 for (label, facts) in [
23138 (
23139 "GDN hybrid / MoE / DeepSeek stack",
23140 ChunkStackFacts {
23141 plain_dense: false,
23142 ..dense_card
23143 },
23144 ),
23145 (
23146 "integrated GPU",
23147 ChunkStackFacts {
23148 discrete: false,
23149 ..dense_card
23150 },
23151 ),
23152 (
23153 "CPU only",
23154 ChunkStackFacts {
23155 gpu_on: false,
23156 discrete: false,
23157 ..dense_card
23158 },
23159 ),
23160 (
23161 "capacity split",
23162 ChunkStackFacts {
23163 capacity_split: true,
23164 ..dense_card
23165 },
23166 ),
23167 (
23168 "multi-GPU plan",
23169 ChunkStackFacts {
23170 multi_gpu: true,
23171 ..dense_card
23172 },
23173 ),
23174 (
23175 "O(1) layers",
23176 ChunkStackFacts {
23177 o1: true,
23178 ..dense_card
23179 },
23180 ),
23181 ] {
23182 assert!(!facts.dense_on_discrete(), "{label}");
23183 assert_eq!(
23184 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
23185 48,
23186 "{label} keeps the historical x86 chunk"
23187 );
23188 }
23189 for dense in [false, true] {
23191 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
23192 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
23193 }
23194 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
23196 for dense in [false, true] {
23197 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
23198 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
23199 }
23200 }
23201 }
23202
23203 #[test]
23204 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
23205 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
23206 let full = |host_rows, device_rows| ReuseLayer {
23207 full: true,
23208 host_rows,
23209 device_rows,
23210 device_state: false,
23211 };
23212 assert_eq!(
23215 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
23216 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
23217 );
23218 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
23220 assert_eq!(
23222 kv_reuse_plan(339, &[full(339, Some(345))]),
23223 ReusePlan::Ready
23224 );
23225 assert_eq!(
23227 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
23228 ReusePlan::Pull(vec![(0, 300, 339)])
23229 );
23230 assert_eq!(
23232 kv_reuse_plan(339, &[full(300, Some(320))]),
23233 ReusePlan::Fresh
23234 );
23235 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
23236 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
23237 let conv = |device_state| ReuseLayer {
23240 full: false,
23241 host_rows: 0,
23242 device_rows: None,
23243 device_state,
23244 };
23245 assert_eq!(
23246 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
23247 ReusePlan::Fresh
23248 );
23249 assert_eq!(
23250 kv_reuse_plan(339, &[conv(false), full(339, None)]),
23251 ReusePlan::Ready
23252 );
23253 }
23254
23255 #[test]
23256 fn nll_graph_policy_scopes_only_the_fused_head() {
23257 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
23258 ("vulkan graph", true, true, false, true, false),
23260 ("native Metal graph", true, true, true, true, true),
23262 ("masked", false, true, false, false, false),
23264 ("graph disabled", true, false, true, false, false),
23265 ] {
23266 let (graph_quality, graph_head_required) =
23267 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
23268 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
23269 assert_eq!(graph_head_required, want_head, "{label}: fused head");
23270 }
23271 }
23272
23273 #[test]
23274 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
23275 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
23276 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
23277 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
23278 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
23279 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
23280 }
23281
23282 #[test]
23283 fn cancel_flag_stops_generation() {
23284 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
23285 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23288 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
23289 assert_eq!(r.finish_reason, "cancelled");
23290 assert!(
23291 r.token_ids.is_empty(),
23292 "no tokens after cancel: {:?}",
23293 r.token_ids
23294 );
23295 assert_eq!(p.kv_cache.seq_len(), 0);
23296 assert!(p.kv_history.is_empty());
23297 assert!(!p.graph_want_logits);
23298 assert!(p.graph_logits.is_none());
23299 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
23301 assert_ne!(r2.finish_reason, "cancelled");
23302 }
23303 use super::*;
23304
23305 #[test]
23306 fn minimax_m3_arch_selects_checked_oai_activation_only() {
23307 let arch = |arch_name: &str, hidden_act: &str| {
23311 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
23312 "arch_name": arch_name,
23313 "hidden_size": 8,
23314 "intermediate_size": 16,
23315 "num_layers": 1,
23316 "num_attention_heads": 1,
23317 "num_kv_heads": 1,
23318 "head_dim": 8,
23319 "vocab_size": 16,
23320 "layer_types": ["FullAttention"],
23321 "rms_norm_eps": 1e-6,
23322 "max_position_embeddings": 16,
23323 "hidden_act": hidden_act
23324 }))
23325 .expect("minimal architecture")
23326 };
23327 let minimax = arch("minimax_m3_vl", "swigluoai");
23328 assert_eq!(
23329 Act::from_arch_full(&minimax),
23330 Act::SwiGluOai {
23331 alpha: 1.702,
23332 limit: 7.0,
23333 }
23334 );
23335
23336 let unrelated = arch("qwen2", "swigluoai");
23339 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
23340
23341 let non_oai = arch("minimax_m3_vl", "gelu");
23345 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
23346 }
23347
23348 #[test]
23349 fn minimax_swiglu_oai_act_matches_scalar_contract() {
23350 let act = Act::swiglu_oai(1.702, 7.0);
23351 let got = act.combine(10.0, -10.0);
23352 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
23353 assert!((got - expected).abs() < 1e-6);
23354 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
23355 assert_eq!(
23356 act.wgpu_graph_act(),
23357 Some(crate::gpu::GraphAct::SwiGluOaiM3),
23358 "the WGPU-only arm accepts only the frozen M3 constants"
23359 );
23360 assert_eq!(
23361 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
23362 None,
23363 "nearby OAI constants must not borrow MiniMax's kernel"
23364 );
23365 assert_eq!(
23366 act.moe_job_graph_contract(),
23367 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
23368 "the direct MoE path carries the frozen clamp with the marker"
23369 );
23370 assert_eq!(
23371 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
23372 None,
23373 "a nearby clamp must not borrow MiniMax's device contract"
23374 );
23375
23376 let gate_only = act.apply(-10.0);
23379 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
23380 assert!((gate_only - expected_gate).abs() < 1e-10);
23381 }
23382
23383 #[test]
23384 fn explicit_sliding_schedule_selects_local_rope_profile() {
23385 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
23390 p.swa = Some((1024, usize::MAX));
23391 p.sliding_layers = Some(vec![true, true, true, false]);
23392 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
23393 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
23394 p.rope_scale = 1.277_258_9;
23395 p.rope_scale_local = 1.0;
23396
23397 for li in 0..3 {
23398 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
23399 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
23400 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
23401 }
23402 assert_eq!(p.layer_window(3), None);
23403 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
23404 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
23405 }
23406
23407 #[test]
23415 fn dynamic_ffn_equals_the_zeroing_arm() {
23416 let (hidden, inter) = (8usize, 32usize);
23417 let synth = |n: usize, salt: usize| -> Vec<f32> {
23418 (0..n)
23419 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
23420 .collect()
23421 };
23422 let down = synth(hidden * inter, 3);
23423 let mut down_t = vec![0.0f32; inter * hidden];
23424 for r in 0..hidden {
23425 for c in 0..inter {
23426 down_t[c * hidden + r] = down[r * inter + c];
23427 }
23428 }
23429 let d = DenseFfn {
23430 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23431 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23432 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
23433 act: Act::Silu,
23434 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
23435 segs: Vec::new(),
23436 };
23437 let x = synth(hidden, 11);
23438 let k = 12usize;
23439 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
23440 let mut g = vec![0.0f32; inter];
23442 d.gate_proj.matvec(&x, &mut g, None);
23443 let mut u = vec![0.0f32; inter];
23444 d.up_proj.matvec(&x, &mut u, None);
23445 for v in g.iter_mut() {
23446 *v = inference::silu(*v);
23447 }
23448 keep_top_k(&mut g, k);
23449 for i in 0..inter {
23450 g[i] *= u[i];
23451 }
23452 let mut want = vec![0.0f32; hidden];
23453 d.down_proj.matvec(&g, &mut want, None);
23454 for (a, b) in want.iter().zip(&got) {
23455 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
23456 }
23457 }
23458
23459 #[test]
23465 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
23466 let (hidden, core, tube) = (8usize, 12usize, 8usize);
23467 let inter = core + tube;
23468 let synth = |n: usize, salt: usize| -> Vec<f32> {
23469 (0..n)
23470 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
23471 .collect()
23472 };
23473 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
23474 let d_all = synth(hidden * inter, 3);
23475 let dense = DenseFfn {
23477 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
23478 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
23479 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
23480 act: Act::Silu,
23481 down_t: None,
23482 segs: Vec::new(),
23483 };
23484 let rows =
23485 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
23486 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
23487 let mut o = Vec::with_capacity(hidden * (b - a));
23488 for r in 0..hidden {
23489 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
23490 }
23491 o
23492 };
23493 let tubed = DenseFfn {
23494 down_t: None,
23495 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
23496 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
23497 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
23498 act: Act::Silu,
23499 segs: vec![FfnSeg {
23500 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
23501 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
23502 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
23503 start: core,
23504 width: tube,
23505 }],
23506 };
23507 let x = synth(hidden, 7);
23508 let want = dense_ffn(&dense, &x, None);
23509 let got = tube_ffn(&tubed, &x, 1, None, None);
23510 for (a, b) in want.iter().zip(&got) {
23511 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
23512 }
23513 let mut bits = vec![0u8; inter.div_ceil(8)];
23515 for n in 0..core {
23516 bits[n / 8] |= 1 << (n % 8);
23517 }
23518 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23519 let masked = dense_ffn_masked(&dense, &x, None, &bits);
23520 for (a, b) in masked.iter().zip(&closed) {
23521 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
23522 }
23523 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
23525 for (a, b) in closed.iter().zip(&batch) {
23526 assert_eq!(a, b, "batch arm disagrees with decode arm");
23527 }
23528 }
23529
23530 #[test]
23532 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
23533 let (hidden, inter) = (16usize, 40usize);
23534 let synth = |n: usize, salt: usize| -> Vec<f32> {
23535 (0..n)
23536 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
23537 .collect()
23538 };
23539 let d = DenseFfn {
23540 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
23541 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
23542 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
23543 act: Act::Silu,
23544 down_t: None,
23545 segs: Vec::new(),
23546 };
23547 let x = synth(hidden, 9);
23548 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
23550
23551 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
23552
23553 let mut g = vec![0.0f32; inter];
23555 d.gate_proj.matvec(&x, &mut g, None);
23556 let mut u = vec![0.0f32; inter];
23557 d.up_proj.matvec(&x, &mut u, None);
23558 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
23559 for i in 0..inter {
23560 g[i] = if act_set.contains(&(i as u16)) {
23561 inference::silu(g[i]) * u[i]
23562 } else {
23563 0.0
23564 };
23565 }
23566 let mut reference = vec![0.0f32; hidden];
23567 d.down_proj.matvec(&g, &mut reference, None);
23568
23569 let max_d = sparse
23570 .iter()
23571 .zip(&reference)
23572 .map(|(a, b)| (a - b).abs())
23573 .fold(0.0f32, f32::max);
23574 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
23575 }
23576
23577 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
23580 let (h, inter, heads, kv, hd) = (
23581 p.hidden_size,
23582 inter,
23583 p.num_heads,
23584 p.num_kv_heads,
23585 p.head_dim,
23586 );
23587 let synth = |n: usize, salt: usize| -> Vec<f32> {
23588 (0..n)
23589 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
23590 .collect()
23591 };
23592 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23593 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23594 };
23595 p.mtp = Some(MtpModule {
23596 enorm: vec![1.0; h],
23597 hnorm: vec![1.0; h],
23598 eh_proj: qt(h, 2 * h, 301),
23599 layer: LayerWeights {
23600 input_norm: vec![1.0; h],
23601 post_norm: vec![1.0; h],
23602 attn_out_norm: None,
23603 ffn_out_norm: None,
23604 layer_scale: None,
23605 ffn: FfnKind::Dense(DenseFfn {
23606 gate_proj: qt(inter, h, 315),
23607 up_proj: qt(inter, h, 316),
23608 down_proj: qt(h, inter, 317),
23609 act: Act::Silu,
23610 down_t: None,
23611 segs: Vec::new(),
23612 }),
23613 attn: AttnKind::Full {
23614 bias: None,
23615 wq: qt(heads * hd, h, 311),
23616 wk: qt(kv * hd, h, 312),
23617 wv: qt(kv * hd, h, 313),
23618 wo: qt(h, heads * hd, 314),
23619 q_norm: None,
23620 k_norm: None,
23621 output_gate: false,
23622 softplus_gate: None,
23623 },
23624 },
23625 final_norm: vec![1.0; h],
23626 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
23627 });
23628 }
23629
23630 fn attach_test_mtp(p: &mut Pipeline) {
23632 attach_test_mtp_width(p, p.intermediate_size);
23633 }
23634
23635 #[test]
23636 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
23637 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
23638 attach_test_mtp_width(&mut p, 11);
23641 let ids = [3, 9, 4, 12, 7, 2];
23642 let trunk = p.probe_ffn_mass(&ids);
23643 assert_eq!(trunk[0].len(), p.intermediate_size);
23644
23645 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
23646 assert_eq!(got.len(), 1);
23647 assert_eq!(got[0].mass.len(), 11);
23648 assert_eq!(got[0].squared_mass.len(), 11);
23649 assert_eq!(got[0].observed_positions, ids.len() - 1);
23650 assert!(got[0].mass.iter().any(|&v| v > 0.0));
23651 assert!(got[0]
23652 .mass
23653 .iter()
23654 .zip(&got[0].squared_mass)
23655 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23656 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
23657 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
23658
23659 let h = p.hidden_size;
23663 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
23664 panic!("synthetic MTP must be dense")
23665 };
23666 dense.segs.push(FfnSeg {
23667 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23668 up: QTensor::from_f32(vec![0.0; h], 1, h),
23669 down: QTensor::from_f32(vec![0.0; h], h, 1),
23670 start: dense.gate_proj.rows(),
23671 width: 1,
23672 });
23673 let err = p.probe_mtp_ffn(&ids);
23674 assert!(err
23675 .unwrap_err()
23676 .contains("tube-segmented draft FFNs are not supported"));
23677 assert_eq!(p.kv_cache.seq_len(), 0);
23678 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
23679 }
23680
23681 #[test]
23682 fn speculative_equals_vanilla_greedy() {
23683 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23684 let _env = ScopedTestEnv::set(&[
23687 ("CMF_GPU", Some("0")),
23688 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23689 ("CMF_MTP_PAIR", Some("1")),
23690 ]);
23691 let run = |spec: bool| {
23692 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23693 p.sampler_config.temperature = 0.0;
23694 attach_test_mtp(&mut p);
23695 p.speculative = spec;
23696 let r = p.generate("abcdef", 12, None, None).unwrap();
23697 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23698 };
23699 let (vanilla, d0, _) = run(false);
23700 let (spec, d1, a1) = run(true);
23701 assert_eq!(d0, 0, "vanilla path must not draft");
23702 assert!(d1 > 0, "speculative path must draft");
23703 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
23704 assert_eq!(
23705 vanilla, spec,
23706 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
23707 );
23708 }
23709
23710 #[test]
23711 fn speculative_accepts_constant_oracle() {
23712 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23713 let _env = ScopedTestEnv::set(&[
23714 ("CMF_GPU", Some("0")),
23715 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23716 ("CMF_MTP_PAIR", Some("1")),
23717 ]);
23718 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23719 p.sampler_config.temperature = 0.0;
23720 p.sampler_config.repetition_penalty = 1.0;
23721 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
23724 attach_test_mtp(&mut p);
23725 p.speculative = true;
23726 let r = p.generate("abcd", 10, None, None).unwrap();
23727 assert!(r.mtp_drafted > 0);
23728 assert_eq!(
23729 r.mtp_accepted, r.mtp_drafted,
23730 "constant logits → every draft accepted"
23731 );
23732 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
23735 }
23736
23737 #[test]
23738 fn host_fused_mtp_pair_is_opt_in() {
23739 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23740 let _env = ScopedTestEnv::set(&[
23741 ("CMF_GPU", Some("0")),
23742 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23743 ("CMF_MTP_PAIR", None),
23744 ]);
23745 let run = |spec: bool| {
23746 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23747 p.sampler_config.temperature = 0.0;
23748 attach_test_mtp(&mut p);
23749 p.speculative = spec;
23750 let r = p.generate("abcdef", 12, None, None).unwrap();
23751 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
23752 };
23753 let (plain, d0, _) = run(false);
23754 let (guarded, d1, a1) = run(true);
23755 assert_eq!(d0, 0);
23756 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
23757 assert_eq!(a1, 0);
23758 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
23759 }
23760
23761 #[test]
23762 fn empty_prompt_is_an_error_not_a_panic() {
23763 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23764 let r = p.generate("", 4, None, None);
23765 assert!(r.is_err(), "empty prompt must be a clean error");
23766 }
23767
23768 #[test]
23769 fn every_token_enters_kv_exactly_once() {
23770 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23771 p.sampler_config.temperature = 0.0;
23773 let r = p.generate("abc", 2, None, None).unwrap();
23774 assert_eq!(r.prompt_tokens, 3);
23775 assert_eq!(
23779 p.kv_cache.seq_len(),
23780 3 + r.tokens_generated - 1,
23781 "each token must be cached exactly once (v1 cached the last prompt token twice)"
23782 );
23783 }
23784
23785 #[test]
23786 fn generation_is_reproducible_with_seed() {
23787 let run = || {
23788 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23789 p.generate("hello", 8, None, None).unwrap().token_ids
23790 };
23791 assert_eq!(run(), run());
23792 }
23793
23794 #[test]
23795 fn resetting_sampler_restarts_the_seeded_stream() {
23796 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23797 let config = SamplerConfig {
23798 seed: Some(1234),
23799 ..SamplerConfig::default()
23800 };
23801 p.set_sampler_config(config.clone());
23802 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
23803 p.set_sampler_config(config);
23804 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
23805 assert_eq!(first, second);
23806 }
23807
23808 #[test]
23809 fn eviction_bounds_the_cache() {
23810 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
23811 p.kv_cache.max_seq_len = 6;
23812 p.sampler_config.temperature = 0.0;
23813 let _ = p.generate("abcd", 12, None, None).unwrap();
23814 assert!(
23815 p.kv_cache.seq_len() <= 6 + 1,
23816 "cache must stay bounded by max_seq_len (got {})",
23817 p.kv_cache.seq_len()
23818 );
23819 }
23820
23821 #[test]
23822 fn confidence_matches_tokens_and_is_a_probability() {
23823 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23824 p.sampler_config.temperature = 0.0;
23825 p.sampler_config.repetition_penalty = 1.0;
23826 let r = p.generate("abcd", 10, None, None).unwrap();
23827 assert_eq!(
23828 r.token_confidence.len(),
23829 r.token_ids.len(),
23830 "one confidence per emitted token"
23831 );
23832 for &c in &r.token_confidence {
23833 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
23834 }
23835 let logits = [1.0f32, 3.0, 0.5, 3.0];
23837 let p0 = top1_prob_t(&logits, 1, 1.0);
23838 let p1 = top1_prob_t(&logits, 3, 1.0);
23839 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
23840 assert!(p0 > 0.0 && p0 < 1.0);
23841 let sharp = top1_prob_t(&logits, 1, 1.0);
23843 let soft = top1_prob_t(&logits, 1, 2.0);
23844 assert!(soft < sharp, "higher temperature lowers peak confidence");
23845 }
23846
23847 #[test]
23848 fn trace_is_opt_in_and_parallels_the_output() {
23849 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23851 p.sampler_config.temperature = 0.0;
23852 p.sampler_config.repetition_penalty = 1.0;
23853 let r = p.generate("abcd", 10, None, None).unwrap();
23854 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
23855
23856 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23858 p.sampler_config.temperature = 0.0;
23859 p.sampler_config.repetition_penalty = 1.0;
23860 p.set_trace(true);
23861 let r = p.generate("abcd", 10, None, None).unwrap();
23862 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
23863 for (i, tr) in r.traces.iter().enumerate() {
23864 assert_eq!(tr.t, i, "trace index is sequential");
23865 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
23866 assert_eq!(
23867 tr.confidence, r.token_confidence[i],
23868 "trace confidence matches the confidence channel"
23869 );
23870 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
23872 }
23873 }
23874
23875 #[test]
23876 fn explain_prefill_logits_match_greedy_first_token() {
23877 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23881 p.sampler_config.temperature = 0.0;
23882 p.sampler_config.repetition_penalty = 1.0;
23883 let ids = p.tokenizer.encode("abcd");
23884 let logits = p.prefill_next_logits(&ids, None);
23885 let argmax = logits
23886 .iter()
23887 .enumerate()
23888 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
23889 .unwrap()
23890 .0 as u32;
23891 let r = p.generate("abcd", 1, None, None).unwrap();
23892 assert_eq!(
23893 argmax, r.token_ids[0],
23894 "explain preview must match greedy emit"
23895 );
23896 }
23897
23898 #[test]
23899 fn laguna_shared_expert_is_unconditionally_added() {
23900 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
23901 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
23902 let zero_dense = || DenseFfn {
23903 gate_proj: matrix(vec![0.0; 4]),
23904 up_proj: matrix(vec![0.0; 4]),
23905 down_proj: matrix(vec![0.0; 4]),
23906 act: Act::Silu,
23907 down_t: None,
23908 segs: Vec::new(),
23909 };
23910 let shared = DenseFfn {
23911 gate_proj: identity(),
23912 up_proj: identity(),
23913 down_proj: identity(),
23914 act: Act::Silu,
23915 down_t: None,
23916 segs: Vec::new(),
23917 };
23918 let x = [1.0, 2.0];
23919 let expected = dense_ffn(&shared, &x, None);
23920 let moe = MoeFfn {
23921 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
23922 experts: vec![zero_dense()],
23923 top_k: 1,
23924 norm_topk_prob: true,
23925 router_sigmoid: true,
23926 sigmoid_denom_eps: 1e-6,
23927 expert_bias: None,
23928 routed_scaling: 1.0,
23929 route_tau: None,
23930 shared: Some((shared, None)),
23931 stats: std::cell::RefCell::new(Vec::new()),
23932 act_sq: std::cell::RefCell::new(Vec::new()),
23933 act_rows: std::cell::RefCell::new(Vec::new()),
23934 mask: None,
23935 per_expert_scale: None,
23936 router_input_norm: false,
23937 resonance: None,
23938 grown: Vec::new(),
23939 };
23940 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
23941 for (actual, expected) in actual.iter().zip(expected) {
23942 assert!((actual - expected).abs() < 1e-6);
23943 }
23944 }
23945
23946 fn mimo_test_pipeline() -> Pipeline {
23955 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
23956 let kvh = [1usize, 2, 2, 1];
23957 let synth = |n: usize, salt: usize| -> Vec<f32> {
23958 (0..n)
23959 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23960 .collect()
23961 };
23962 let qt = |rows: usize, cols: usize, salt: usize| {
23963 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23964 };
23965 let dense = |inter: usize, salt: usize| DenseFfn {
23966 gate_proj: qt(inter, hs, salt),
23967 up_proj: qt(inter, hs, salt + 1),
23968 down_proj: qt(hs, inter, salt + 2),
23969 act: Act::Silu,
23970 down_t: None,
23971 segs: Vec::new(),
23972 };
23973 let layers: Vec<LayerWeights> = (0..4)
23974 .map(|li| LayerWeights {
23975 input_norm: vec![1.0; hs],
23976 post_norm: vec![1.0; hs],
23977 attn_out_norm: None,
23978 ffn_out_norm: None,
23979 layer_scale: None,
23980 ffn: if li == 0 {
23981 FfnKind::Dense(dense(inter, 50))
23982 } else {
23983 FfnKind::Moe(MoeFfn {
23984 router: qt(4, hs, 60 + li),
23985 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
23986 top_k: 2,
23987 norm_topk_prob: true,
23988 router_sigmoid: true,
23989 sigmoid_denom_eps: 1e-6,
23990 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
23991 routed_scaling: 1.0,
23992 route_tau: None,
23993 shared: None,
23994 stats: std::cell::RefCell::new(Vec::new()),
23995 act_sq: std::cell::RefCell::new(Vec::new()),
23996 act_rows: std::cell::RefCell::new(Vec::new()),
23997 mask: None,
23998 per_expert_scale: None,
23999 router_input_norm: false,
24000 resonance: None,
24001 grown: Vec::new(),
24002 })
24003 },
24004 attn: AttnKind::Full {
24005 wq: qt(nh * hd, hs, li * 10 + 1),
24006 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
24007 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
24008 wo: qt(hs, nh * vd, li * 10 + 4),
24009 q_norm: None,
24010 k_norm: None,
24011 output_gate: false,
24012 softplus_gate: None,
24013 bias: None,
24014 },
24015 })
24016 .collect();
24017 let mut p = Pipeline::new(
24018 Tokenizer::byte_level(),
24019 PipelineWeights {
24020 embed_tokens: qt(vocab, hs, 100),
24021 layers,
24022 lm_head: qt(vocab, hs, 200),
24023 final_norm: vec![1.0; hs],
24024 },
24025 hs,
24026 inter,
24027 nh,
24028 1, hd,
24030 4,
24031 4,
24032 false,
24033 vocab,
24034 1e-6,
24035 1e7,
24036 NormStyle::Qwen,
24037 4096,
24038 SamplerConfig {
24039 seed: Some(7),
24040 ..Default::default()
24041 },
24042 );
24043 p.layer_dump = None;
24045 p.set_rotary(4, 1e7);
24046 p.sliding_layers = Some(vec![false, true, true, false]);
24047 p.swa = Some((3, usize::MAX));
24048 p.rotary_dim_local = Some(4);
24049 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
24050 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
24051 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
24052 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
24053 p
24054 }
24055
24056 #[test]
24057 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
24058 let mut p = mimo_test_pipeline();
24059 p.speculative = false;
24060 p.ignore_eos = true;
24061 p.sampler_config.temperature = 0.0;
24062 p.sampler_config.repetition_penalty = 1.0;
24063 let a = vec![3, 5, 7, 9, 11, 13];
24064 let b = vec![4, 8, 12, 16, 20, 24];
24065 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
24066 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
24067 let actual = p
24071 .generate_from_embeds(&a, &rows, 8, None, None)
24072 .unwrap()
24073 .token_ids;
24074 assert_eq!(actual, expected);
24075 assert!(p.kv_history.is_empty());
24076 let mut extended = a.clone();
24077 extended.push(17);
24078 let after_media = p
24079 .generate_from_ids(&extended, 8, None, None)
24080 .unwrap()
24081 .token_ids;
24082 p.reset_session();
24083 let fresh = p
24084 .generate_from_ids(&extended, 8, None, None)
24085 .unwrap()
24086 .token_ids;
24087 assert_eq!(after_media, fresh);
24088 assert!(
24089 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
24090 .is_err()
24091 );
24092 p.reset_session();
24096 p.generate_from_ids(&a, 1, None, None).unwrap();
24097 let mut media_ids = p.kv_history.clone();
24098 assert!(!media_ids.is_empty());
24099 media_ids.extend_from_slice(&[19, 21, 23]);
24100 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
24101 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
24102 let mut oracle = mimo_test_pipeline();
24103 oracle.speculative = false;
24104 oracle.ignore_eos = true;
24105 oracle.sampler_config.temperature = 0.0;
24106 oracle.sampler_config.repetition_penalty = 1.0;
24107 let expected = oracle
24108 .generate_from_ids(&source_ids, 8, None, None)
24109 .unwrap()
24110 .token_ids;
24111 assert_eq!(
24112 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
24113 .unwrap()
24114 .token_ids,
24115 expected
24116 );
24117 assert!(p.kv_history.is_empty());
24118 let mut bad = rows;
24119 bad[0] = f32::NAN;
24120 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
24121 }
24122
24123 fn f32_bits(v: &[f32]) -> Vec<u32> {
24124 v.iter().map(|x| x.to_bits()).collect()
24125 }
24126
24127 #[test]
24134 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
24135 let mut p = mimo_test_pipeline();
24136 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
24137 assert_eq!(kv, vec![1, 2, 2, 1]);
24138 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
24139 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
24140 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
24141 let hs = p.hidden_size;
24142 let mut decode = Vec::new();
24143 for (pos, &id) in ids.iter().enumerate() {
24144 let e = p.embed_single(id);
24145 let h = p.forward_layers(&e, pos, None);
24146 decode.push(p.logits_from_hidden(&h));
24147 }
24148 for l in &p.kv_cache.layers {
24149 assert_eq!(l.seq_len, 12);
24150 assert_eq!(l.head_values(0).len(), 12 * 8);
24152 }
24153 assert!(decode.iter().flatten().all(|v| v.is_finite()));
24154
24155 p.clear_sequence_state();
24156 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24157 for pos in 0..ids.len() {
24158 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
24159 assert_eq!(
24160 f32_bits(&decode[pos]),
24161 f32_bits(&lg),
24162 "whole prompt, pos {pos}"
24163 );
24164 }
24165
24166 p.clear_sequence_state();
24167 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
24168 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
24169 for pos in 0..ids.len() {
24170 let row = if pos < 5 {
24171 &a[pos * hs..(pos + 1) * hs]
24172 } else {
24173 &b[(pos - 5) * hs..(pos - 4) * hs]
24174 };
24175 let lg = p.logits_from_hidden(row);
24176 assert_eq!(
24177 f32_bits(&decode[pos]),
24178 f32_bits(&lg),
24179 "two chunks, pos {pos}"
24180 );
24181 }
24182
24183 let last = |p: &mut Pipeline| {
24186 p.clear_sequence_state();
24187 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24188 p.logits_from_hidden(&hb[11 * hs..12 * hs])
24189 };
24190 let base = last(&mut p);
24191 let mut no_sinks = mimo_test_pipeline();
24192 for l in &mut no_sinks.kv_cache.layers {
24193 l.sinks = None;
24194 }
24195 assert_ne!(
24196 f32_bits(&last(&mut no_sinks)),
24197 f32_bits(&base),
24198 "sinks are live"
24199 );
24200 let mut wide = mimo_test_pipeline();
24201 wide.swa = Some((64, usize::MAX));
24202 assert_ne!(
24203 f32_bits(&last(&mut wide)),
24204 f32_bits(&base),
24205 "window is live"
24206 );
24207
24208 p.clear_sequence_state();
24210 p.ignore_eos = true;
24211 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
24212 assert_eq!(r.token_ids.len(), 4);
24213 }
24214
24215 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
24218 let (hs, nh) = (16usize, 4usize);
24219 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
24220 p.layer_dump = None;
24221 p.swa = Some((6, 4));
24222 p.proj_gate_sigmoid = true;
24223 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
24224 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
24225 let g: Vec<f32> = (0..nh * hs)
24226 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
24227 .collect();
24228 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
24229 }
24230 }
24231 p.swa_trim = trim;
24232 p
24233 }
24234
24235 #[test]
24241 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
24242 let mut a = spark_test_pipeline(None);
24243 let mut b = spark_test_pipeline(Some((2, 4)));
24244 assert_eq!(
24245 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
24246 vec![Some(6), Some(6), Some(6), None]
24247 );
24248 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
24249 let mut pos = 0usize;
24250 for &n in [5usize, 13, 7, 16].iter().cycle() {
24251 if pos >= ids.len() {
24252 break;
24253 }
24254 let end = (pos + n).min(ids.len());
24255 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24256 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
24257 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
24258 pos = end;
24259 }
24260 for _ in 0..23 {
24261 let e = a.embed_single(((pos * 7) % 64) as u32);
24262 let ha = a.forward_layers(&e, pos, None);
24263 let hb = b.forward_layers(&e, pos, None);
24264 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
24265 for li in 0..3 {
24266 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
24267 }
24268 pos += 1;
24269 }
24270 for round in 0..9 {
24271 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
24272 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
24273 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
24274 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
24275 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
24276 if round % 2 == 1 {
24277 for p in [&mut a, &mut b] {
24279 for l in &mut p.kv_cache.layers {
24280 l.truncate_last(2);
24281 }
24282 }
24283 } else {
24284 pos += 2;
24285 }
24286 }
24287 let e = a.embed_single(9);
24288 let (ha, hb) = (
24289 a.forward_layers(&e, pos, None),
24290 b.forward_layers(&e, pos, None),
24291 );
24292 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
24293 for li in 0..3 {
24294 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
24295 assert!(lb.base() > 0, "layer {li} trimmed");
24296 assert_eq!(la.base(), 0);
24297 assert_eq!(lb.pos_len(), la.seq_len);
24298 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
24299 }
24300 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
24301 assert_eq!(
24302 (gb.base(), gb.seq_len),
24303 (0, ga.seq_len),
24304 "the global layer keeps all"
24305 );
24306 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
24307 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
24308 }
24309
24310 #[test]
24315 fn swa_trim_wire_handoff_continues_bitwise() {
24316 let mut a = spark_test_pipeline(None);
24317 let mut b = spark_test_pipeline(Some((2, 4)));
24318 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
24319 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24320 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
24321 assert_eq!(f32_bits(&ha), f32_bits(&hb));
24322 let mut pos = ids.len();
24323 for _ in 0..5 {
24324 let e = a.embed_single(((pos * 5) % 64) as u32);
24325 assert_eq!(
24326 f32_bits(&a.forward_layers(&e, pos, None)),
24327 f32_bits(&b.forward_layers(&e, pos, None))
24328 );
24329 pos += 1;
24330 }
24331 let mut c = spark_test_pipeline(Some((2, 4)));
24332 for li in 0..4 {
24333 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
24334 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
24335 }
24336 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
24337 assert_eq!(c.kv_cache.seq_len(), pos);
24338 for step in 0..17 {
24339 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
24340 let ha = a.forward_layers(&e, pos, None);
24341 let hc = c.forward_layers(&e, pos, None);
24342 assert_eq!(
24343 f32_bits(&ha),
24344 f32_bits(&hc),
24345 "after the hand-off, step {step}"
24346 );
24347 pos += 1;
24348 }
24349 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
24350 }
24351
24352 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
24357 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
24358 let synth = |len: usize, salt: usize| -> Vec<f32> {
24359 (0..len)
24360 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
24361 .collect()
24362 };
24363 let qt = |rows: usize, cols: usize, salt: usize| {
24364 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24365 };
24366 let layers = widths
24367 .iter()
24368 .copied()
24369 .enumerate()
24370 .map(|(k, inter)| {
24371 let s = 500 + k * 40;
24372 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
24373 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
24374 MtpModule {
24375 enorm: vec![1.0; hs],
24376 hnorm: vec![1.0; hs],
24377 eh_proj: qt(hs, 2 * hs, s),
24378 layer: LayerWeights {
24379 input_norm: vec![1.0; hs],
24380 post_norm: vec![1.0; hs],
24381 attn_out_norm: None,
24382 ffn_out_norm: None,
24383 layer_scale: None,
24384 attn: AttnKind::Full {
24385 wq: qt(nh * hd, hs, s + 1),
24386 wk: qt(nkv * hd, hs, s + 2),
24387 wv: qt(nkv * vd, hs, s + 3),
24388 wo: qt(hs, nh * vd, s + 4),
24389 q_norm: None,
24390 k_norm: None,
24391 output_gate: false,
24392 softplus_gate: None,
24393 bias: None,
24394 },
24395 ffn: FfnKind::Dense(DenseFfn {
24396 gate_proj: qt(inter, hs, s + 5),
24397 up_proj: qt(inter, hs, s + 6),
24398 down_proj: qt(hs, inter, s + 7),
24399 act: Act::Silu,
24400 down_t: None,
24401 segs: Vec::new(),
24402 }),
24403 },
24404 final_norm: vec![1.0; hs],
24405 kv,
24406 }
24407 })
24408 .collect();
24409 mimo_mtp::MimoMtp::from_layers(layers)
24410 }
24411
24412 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
24413 mimo_test_mtp_widths(&vec![24; n], gain)
24414 }
24415
24416 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
24417 p.clear_sequence_state();
24418 p.speculative = spec;
24419 p.ignore_eos = true;
24420 p.sampler_config.temperature = 0.0;
24421 p.generate_from_ids(ids, n, None, None).unwrap()
24422 }
24423
24424 #[test]
24425 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
24426 let mut p = mimo_test_pipeline();
24427 let widths = [7usize, 13, 29];
24428 assert_ne!(widths[0], p.intermediate_size);
24429 assert_ne!(widths[1], p.intermediate_size);
24430 assert_ne!(widths[2], p.intermediate_size);
24431 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
24432 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
24433
24434 let trunk = p.probe_ffn_mass(&ids);
24435 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
24436 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
24437 assert_eq!(got.len(), widths.len());
24438 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
24439 assert_eq!(stat.mass.len(), width, "draft layer {k}");
24440 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
24441 assert_eq!(stat.observed_positions, ids.len() - k - 1);
24442 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
24443 assert!(stat
24444 .mass
24445 .iter()
24446 .zip(&stat.squared_mass)
24447 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
24448 }
24449 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
24450 let st = p.mimo_mtp.as_ref().unwrap();
24451 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
24452
24453 let h = p.hidden_size;
24456 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
24457 panic!("synthetic MiMo MTP must be dense")
24458 };
24459 dense.segs.push(FfnSeg {
24460 gate: QTensor::from_f32(vec![0.0; h], 1, h),
24461 up: QTensor::from_f32(vec![0.0; h], 1, h),
24462 down: QTensor::from_f32(vec![0.0; h], h, 1),
24463 start: dense.gate_proj.rows(),
24464 width: 1,
24465 });
24466 assert!(p
24467 .probe_mtp_ffn(&ids)
24468 .unwrap_err()
24469 .contains("tube-segmented draft FFNs are not supported"));
24470 assert_eq!(p.kv_cache.seq_len(), 0);
24471 assert!(p
24472 .mimo_mtp
24473 .as_ref()
24474 .unwrap()
24475 .layers
24476 .iter()
24477 .all(|m| m.kv.seq_len == 0));
24478 }
24479
24480 #[test]
24481 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
24482 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
24483 p.clear_sequence_state();
24484 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
24485 let _exact = ExactMtpCalibrationGuard::enter();
24486 let got = crate::gpu::cpu_scope(|| {
24487 let trunk = p.probe_trunk_rows(ids)?;
24488 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
24489 })
24490 .expect("teacher-forced MiMo probe");
24491 st.reset();
24492 p.mimo_mtp = Some(st);
24493 p.reset_mtp_probe_state();
24494 got
24495 }
24496
24497 for post_norm_hidden in [false, true] {
24498 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
24499 let make = || {
24500 let mut p = mimo_test_pipeline();
24501 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
24502 st.post_norm_hidden = post_norm_hidden;
24503 p.mimo_mtp = Some(st);
24504 p
24505 };
24506 let mut full = make();
24507 let expected = collect(&mut full, &ids, usize::MAX);
24508 for rows in [1usize, 7, 128] {
24509 let mut chunked = make();
24510 let got = collect(&mut chunked, &ids, rows);
24511 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
24512 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
24513 assert_eq!(
24514 got.observed_positions, expected.observed_positions,
24515 "post={post_norm_hidden}, rows={rows}, layer={layer}"
24516 );
24517 assert_eq!(
24518 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24519 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
24520 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
24521 );
24522 assert_eq!(
24523 got.squared_mass
24524 .iter()
24525 .map(|v| v.to_bits())
24526 .collect::<Vec<_>>(),
24527 expected
24528 .squared_mass
24529 .iter()
24530 .map(|v| v.to_bits())
24531 .collect::<Vec<_>>(),
24532 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
24533 );
24534 }
24535 }
24536 }
24537 }
24538
24539 #[test]
24545 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
24546 for post in [false, true] {
24549 let mut p = mimo_test_pipeline();
24550 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
24552 let mut st0 = mimo_test_mtp(3, 1.0);
24553 st0.post_norm_hidden = post;
24554 p.mimo_mtp = Some(st0);
24555 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
24556 let hs = p.hidden_size;
24557 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24558 p.mimo_note_rows(&hb, 0);
24559 let mut st = p.mimo_mtp.take().unwrap();
24560 let k = 3;
24563 let mut inc = Vec::new();
24564 for t in 0..ids.len() - k - 1 {
24565 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
24566 }
24567 let s = ids.len();
24570 let mut reference = vec![vec![0u32; k]; s - k - 1];
24571 let mut fresh = mimo_test_mtp(3, 1.0);
24572 for (layer, m) in fresh.layers.iter_mut().enumerate() {
24573 let n = s - layer - 1;
24574 let mut cats = vec![0.0f32; n * 2 * hs];
24575 for j in 0..n {
24576 let e = p.embed_single(ids[j + layer + 1]);
24577 let raw = &hb[j * hs..(j + 1) * hs];
24578 let g = if post {
24579 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
24580 } else {
24581 raw.to_vec()
24582 };
24583 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
24584 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
24585 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
24586 }
24587 let mut x = vec![0.0f32; n * hs];
24588 m.eh_proj.matmat(&cats, n, &mut x, None);
24589 p.mimo_mtp_block(m, &mut x, n, 0);
24590 for (t, row) in reference.iter_mut().enumerate() {
24591 let y = inference::rms_norm(
24592 &x[t * hs..(t + 1) * hs],
24593 &m.final_norm,
24594 p.rms_eps,
24595 p.norm_style,
24596 );
24597 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
24598 }
24599 }
24600 assert_eq!(inc, reference, "post_norm_hidden = {post}");
24601 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
24603 assert!(distinct.len() > 3, "{inc:?}");
24604 let last_t = ids.len() - k - 2;
24606 for m in &st.layers {
24607 assert_eq!(m.kv.seq_len, last_t + 1);
24608 }
24609 }
24610 }
24611
24612 #[test]
24619 fn mimo_speculative_greedy_equals_plain_greedy() {
24620 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
24621 let _env = ScopedTestEnv::set(&[
24622 ("CMF_GPU", Some("0")),
24623 ("CMF_GPU_WGPU_GRAPH", Some("0")),
24624 ]);
24625 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
24626 let n = 24;
24627 let mut p = mimo_test_pipeline();
24628 let plain = mimo_greedy(&mut p, &ids, n, false);
24629 assert_eq!(plain.mtp_drafted, 0);
24630 assert_eq!(plain.token_ids.len(), n);
24631 let plain_kv = p.kv_cache.layers[0].seq_len;
24632
24633 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
24635 let spec = mimo_greedy(&mut p, &ids, n, true);
24636 assert!(spec.mtp_drafted > 0, "the round must draft");
24637 assert_eq!(spec.token_ids, plain.token_ids);
24638 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24639
24640 let mut truth: Vec<u32> = ids.clone();
24643 truth.extend(&plain.token_ids);
24644 let mut noisy = truth.clone();
24645 for (i, t) in noisy.iter_mut().enumerate() {
24646 if i % 5 == 0 {
24647 *t = (*t + 1) % 64;
24648 }
24649 }
24650 let mut st = mimo_test_mtp(3, 1.0);
24651 st.draft_override = Some(noisy);
24652 p.mimo_mtp = Some(st);
24653 let spec = mimo_greedy(&mut p, &ids, n, true);
24654 assert_eq!(spec.token_ids, plain.token_ids);
24655 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24656 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
24657 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
24658 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
24659 assert!(
24660 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
24661 "{:?}",
24662 stats.accept_hist
24663 );
24664 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
24665
24666 let mut st = mimo_test_mtp(3, 1.0);
24669 st.draft_override = Some(truth);
24670 p.mimo_mtp = Some(st);
24671 let spec = mimo_greedy(&mut p, &ids, n, true);
24672 assert_eq!(spec.token_ids, plain.token_ids);
24673 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
24674 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
24675
24676 let off = mimo_greedy(&mut p, &ids, n, false);
24678 assert_eq!(off.token_ids, plain.token_ids);
24679 assert_eq!(off.mtp_drafted, 0);
24680 }
24681
24682 #[test]
24691 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
24692 let p = mimo_test_pipeline();
24693 assert_eq!(
24694 p.graph_attn_decline_reason(),
24695 Some("per-layer KV head counts")
24696 );
24697 assert_eq!(p.wgpu_graph_attn_decline(), None);
24698 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
24699 assert_eq!(
24700 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
24701 (1, 4, 4, None, false)
24702 );
24703 assert_eq!(g0.invf, p.inv_freq.as_slice());
24704 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
24705 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
24706 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
24707 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
24708 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
24709 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
24710 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
24711
24712 let emb = p.embed_single(3);
24715 let mut lg = Vec::new();
24716 assert!(
24717 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
24718 .is_none()
24719 );
24720 let mut hid = emb.clone();
24721 assert_eq!(
24722 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
24723 crate::gpu::BatchGraphOutcome::Declined
24724 );
24725 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
24726 assert!(p.try_multi_burst(3, 0, 4).is_none());
24727 assert!(
24728 p.graph_declines().is_empty(),
24729 "no attention decline logged: {:?}",
24730 p.graph_declines()
24731 );
24732 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
24737 assert_eq!(plain().graph_attn_decline_reason(), None);
24738 assert_eq!(plain().wgpu_graph_attn_decline(), None);
24739 assert!(
24740 plain().graph_attn_geom(0).is_none(),
24741 "uniform models keep the historical arms"
24742 );
24743 let mut q = plain();
24744 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
24745 assert_eq!(
24746 q.graph_attn_decline_reason(),
24747 Some("learned attention sinks")
24748 );
24749 assert_eq!(
24750 q.graph_attn_geom(1).unwrap().sink,
24751 Some(&[0.25f32, -0.25][..])
24752 );
24753 let mut q = plain();
24754 q.set_attn_geometry(None, Some(2)).unwrap();
24755 assert_eq!(
24756 q.graph_attn_decline_reason(),
24757 Some("V heads narrower than Q/K heads")
24758 );
24759 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
24760 let mut q = plain();
24761 q.sliding_layers = Some(vec![true, false]);
24762 q.swa = Some((4, usize::MAX));
24763 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
24764 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
24765 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
24766
24767 let mut q = mimo_test_pipeline();
24771 q.rope_scale = 2.0;
24772 q.rope_scale_local = 1.25;
24773 assert_eq!(q.wgpu_graph_attn_decline(), None);
24774 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
24775 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
24776
24777 let mut q = plain();
24780 q.rope_scale = 2.0;
24781 assert_eq!(
24782 q.wgpu_graph_attn_decline(),
24783 Some("scaled RoPE without per-layer geometry")
24784 );
24785 }
24786
24787 #[cfg(target_os = "macos")]
24793 #[test]
24794 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
24795 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
24799 p.swa = Some((16, usize::MAX));
24800 p.sliding_layers = Some(vec![true, true]);
24801 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
24802 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
24803
24804 assert_eq!(
24805 p.graph_attn_decline_reason(),
24806 Some("learned attention sinks")
24807 );
24808 let swa_graph = p.metal_graph_swa();
24809 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
24810 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
24811 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
24812 assert!(
24813 native_sink_attend,
24814 "the q1 sink admission must follow the SWA door"
24815 );
24816 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
24819 }
24820
24821 #[test]
24822 fn mimo_verify_rewind_preserves_lagging_host_caches() {
24823 let mut p = mimo_test_pipeline();
24824 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
24825 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
24826 for _ in 0..if li == 0 { 2 } else { 12 } {
24827 layer.append(&row, &row, &[]);
24828 }
24829 }
24830 p.mimo_verify_rewind(9).unwrap();
24831 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
24832 for layer in &p.kv_cache.layers[1..] {
24833 assert_eq!(layer.seq_len, 9);
24834 }
24835 }
24836
24837 #[test]
24841 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
24842 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
24843 let _ = std::fs::remove_dir_all(&dir);
24844 let mut p = mimo_test_pipeline();
24845 let hs = p.hidden_size;
24846 let ids = [5u32, 9, 11, 2, 40];
24847 p.layer_dump = Some(dir.join("decode"));
24848 for (pos, &id) in ids.iter().enumerate() {
24849 let e = p.embed_single(id);
24850 let _ = p.forward_layers(&e, pos, None);
24851 }
24852 p.clear_sequence_state();
24853 p.layer_dump = Some(dir.join("prefill"));
24854 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24855 for pos in 0..ids.len() {
24856 for li in 0..p.num_layers {
24857 let name = format!("p{pos:06}_l{li:02}.f32");
24858 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
24859 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
24860 assert_eq!(a.len(), hs * 4, "{name}");
24861 assert_eq!(a, b, "{name}");
24862 }
24863 }
24864 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
24865 let vals: Vec<f32> = last
24866 .chunks(4)
24867 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
24868 .collect();
24869 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
24870 let _ = std::fs::remove_dir_all(&dir);
24871 }
24872
24873 #[test]
24874 fn attn_geometry_and_sinks_are_validated() {
24875 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
24876 assert!(
24877 p.set_attn_geometry(Some(vec![2]), None).is_err(),
24878 "one entry per layer"
24879 );
24880 assert!(
24881 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
24882 "3 does not divide 4"
24883 );
24884 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
24885 assert!(p.set_attn_geometry(None, Some(0)).is_err());
24886 assert!(
24887 p.set_attn_geometry(None, Some(5)).is_err(),
24888 "V wider than the head"
24889 );
24890 p.set_attn_geometry(None, Some(4)).unwrap();
24891 assert_eq!(
24892 p.v_head_dim, None,
24893 "v_head_dim == head_dim is the uniform case"
24894 );
24895 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
24896 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
24897 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
24898 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
24899 assert!(
24900 p.kv_cache.layers[1].sinks.is_some(),
24901 "a reshape keeps the layer's sinks"
24902 );
24903 assert_eq!(p.layer_geom(1).0, 4);
24904 assert!(
24905 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
24906 "one sink per Q head"
24907 );
24908 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
24909 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
24910 }
24911
24912 #[test]
24915 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
24916 let cfg = || {
24917 Some(crate::nystrom::O1Cfg {
24918 layers: crate::nystrom::O1Layers::All,
24919 m: 4,
24920 w: 8,
24921 sink: 2,
24922 rect: crate::nystrom::O1Rect::Aggregate,
24923 })
24924 };
24925 let mut p = mimo_test_pipeline();
24926 p.set_o1(cfg());
24927 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
24928 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
24929 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
24930 q.sliding_layers = Some(vec![false, false, true]);
24931 q.swa = Some((4, usize::MAX));
24932 q.set_o1(cfg());
24933 assert_eq!(q.o1_flags, vec![true, false, false]);
24934 }
24935
24936 #[test]
24937 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
24938 const B: usize = 19;
24939 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24940 p.set_o1(Some(crate::nystrom::O1Cfg {
24941 layers: crate::nystrom::O1Layers::All,
24942 m: 4,
24943 w: 8,
24944 sink: 2,
24945 rect: crate::nystrom::O1Rect::Aggregate,
24946 }));
24947 p.o1_begin_with_prefix(Some(B));
24948 let ids: Vec<u32> = (0..B as u32).collect();
24949 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
24950
24951 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
24952 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
24953 let next = p.embed_single(B as u32);
24954 let _ = p.forward_layers(&next, B, None);
24955 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
24956 }
24957
24958 #[test]
24959 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
24960 const B: usize = 19;
24961 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
24962 let gdn_cfg = crate::linear_core::GdnCfg {
24966 num_v_heads: 2,
24967 num_k_heads: 1,
24968 key_head_dim: 2,
24969 value_head_dim: 4,
24970 conv_kernel: 3,
24971 hidden_size: 8,
24972 rms_eps: 1e-6,
24973 output_gate_sigmoid: false,
24974 };
24975 let synth = |n: usize, salt: usize| -> Vec<f32> {
24976 (0..n)
24977 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
24978 .collect()
24979 };
24980 let qt = |rows: usize, cols: usize, salt: usize| {
24981 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
24982 };
24983 let c_dim = gdn_cfg.conv_dim();
24984 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
24985 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
24986 in_proj_qkv: qt(c_dim, 8, 1),
24987 in_proj_z: qt(vd, 8, 2),
24988 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
24989 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
24990 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
24991 a_log: vec![0.2, 0.5],
24992 dt_bias: synth(gdn_cfg.num_v_heads, 6),
24993 norm: vec![1.0; gdn_cfg.value_head_dim],
24994 out_proj: qt(8, vd, 7),
24995 });
24996 p.gdn_cfg = Some(gdn_cfg);
24997 p.set_o1(Some(crate::nystrom::O1Cfg {
24998 layers: crate::nystrom::O1Layers::All,
24999 m: 4,
25000 w: 8,
25001 sink: 2,
25002 rect: crate::nystrom::O1Rect::Aggregate,
25003 }));
25004 p.o1_begin_with_prefix(Some(B));
25005 for pos in 0..B - 2 {
25006 let emb = p.embed_single(pos as u32);
25007 let _ = p.forward_layers(&emb, pos, None);
25008 }
25009 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
25010
25011 let e1 = p.embed_single((B - 2) as u32);
25012 let e2 = p.embed_single((B - 1) as u32);
25013 let _ = p.forward_pair(&e1, &e2, B - 2);
25014
25015 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
25016 assert!(
25017 p.kv_cache
25018 .layers
25019 .iter()
25020 .enumerate()
25021 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
25022 );
25023 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
25024 assert_ne!(
25025 p.kv_cache.layers[0].linear_state, lane1_state,
25026 "real pair must commit GDN lane 2 before returning"
25027 );
25028 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
25029 let next = p.embed_single(B as u32);
25030 let _ = p.forward_layers(&next, B, None);
25031 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
25032 }
25033
25034 #[test]
25035 fn o1_error_observation_stays_terminal_until_reset() {
25036 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25037 p.set_o1(Some(crate::nystrom::O1Cfg {
25038 layers: crate::nystrom::O1Layers::All,
25039 m: 4,
25040 w: 8,
25041 sink: 2,
25042 rect: crate::nystrom::O1Rect::Aggregate,
25043 }));
25044 p.o1_begin();
25045 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
25046
25047 assert!(p.o1_seal_checked().is_err());
25048 assert!(
25049 p.o1_seal_checked().is_err(),
25050 "retry must see the sticky error"
25051 );
25052 let k = vec![0.2f32; 4];
25053 let v = vec![0.3f32; 4];
25054 p.kv_cache.layers[0].append(&k, &v, &[]);
25055 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
25056
25057 p.reset_session();
25058 p.o1_begin();
25059 p.kv_cache.layers[0].append(&k, &v, &[]);
25060 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
25061 }
25062
25063 #[test]
25064 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
25065 let ids = vec![1u32, 2, 3, 4, 5, 6];
25066 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25067 p.graph_logits = Some(vec![123.0]);
25068 p.graph_want_logits = true;
25069 p.graph_failed
25070 .store(true, std::sync::atomic::Ordering::Relaxed);
25071 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25072 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
25073 assert!(err.contains("before NLL"));
25074 assert!(p.graph_logits.is_none());
25075 assert!(!p.graph_want_logits);
25076 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25077 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25078
25079 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25080 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25081 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25082 assert_eq!(actual.1, expected.1);
25083 assert!((actual.0 - expected.0).abs() < 1e-9);
25084 }
25085
25086 #[test]
25087 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
25088 let ids = vec![1u32, 2, 3, 4, 5, 6];
25089 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25090 p.nll_test_fail_at = Some(1);
25091 let err = p
25092 .nll_ids_from(&ids, 0)
25093 .expect_err("one-shot forward failure");
25094 assert!(err.contains("forward") || err.contains("score row"));
25095 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25096 assert!(!p.graph_want_logits);
25097 assert!(p.graph_logits.is_none());
25098 assert!(p.kv_history.is_empty());
25099
25100 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25101 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
25102 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
25103 assert_eq!(actual.1, expected.1);
25104 assert!((actual.0 - expected.0).abs() < 1e-9);
25105 }
25106
25107 #[test]
25108 fn nll_serial_failure_before_first_row_is_reported() {
25109 let ids = vec![1u32, 2, 3, 4];
25110 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25111 p.nll_test_force_serial = true;
25112 p.nll_test_fail_at = Some(0);
25113 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
25114 assert!(err.contains("serial forward"));
25115 assert!(p.kv_history.is_empty());
25116 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25117 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25118 }
25119
25120 #[test]
25121 fn ffn_probe_failure_discards_recorder_and_state() {
25122 let ids = vec![1u32, 2, 3, 4];
25123 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25124 p.nll_test_fail_at = Some(0);
25125 let err = p
25126 .probe_ffn_mass_batch(&ids)
25127 .expect_err("probe forward failure");
25128 assert!(err.contains("NLL"));
25129 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
25130 assert!(p.kv_history.is_empty());
25131 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25132 }
25133
25134 #[test]
25135 fn nll_test_controls_are_pipeline_scoped() {
25136 let ids = vec![1u32, 2, 3, 4];
25137 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25138 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25139 failing.nll_test_force_serial = true;
25140 failing.nll_test_fail_at = Some(0);
25141
25142 assert!(!failing.can_prefill_batched());
25143 assert!(unaffected.can_prefill_batched());
25144 let expected = unaffected
25145 .nll_ids_from(&ids, 0)
25146 .expect("unaffected pipeline remains usable");
25147 let err = failing
25148 .nll_ids_from(&ids, 0)
25149 .expect_err("failure injection belongs to failing pipeline");
25150 assert!(err.contains("serial forward"));
25151 assert!(failing.nll_test_fail_at.is_none());
25152 assert!(unaffected.can_prefill_batched());
25153 let actual = unaffected
25154 .nll_ids_from(&ids, 0)
25155 .expect("unaffected pipeline remains reusable");
25156 assert_eq!(actual.1, expected.1);
25157 assert!((actual.0 - expected.0).abs() < 1e-9);
25158 }
25159
25160 #[test]
25161 fn forward_ids_failure_channel_is_terminal_and_reusable() {
25162 let ids = vec![1u32, 2, 3, 4, 5, 6];
25163 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
25164 p.graph_logits = Some(vec![123.0]);
25165 p.graph_want_logits = true;
25166 p.graph_failed
25167 .store(true, std::sync::atomic::Ordering::Relaxed);
25168 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
25169
25170 let err = p
25171 .forward_ids(&ids, None)
25172 .expect_err("a failed forward must not become a valid head result");
25173 assert!(err.contains("forward_ids setup"));
25174 assert!(p.graph_logits.is_none());
25175 assert!(!p.graph_want_logits);
25176 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
25177 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
25178 assert_eq!(p.kv_cache.seq_len(), 0);
25179
25180 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
25181 .forward_ids(&ids, None)
25182 .expect("fresh forward_ids");
25183 let actual = p
25184 .forward_ids(&ids, None)
25185 .expect("pipeline remains reusable after a failed forward");
25186 assert_eq!(actual.len(), expected.len());
25187 assert!(
25188 actual
25189 .iter()
25190 .zip(expected)
25191 .all(|(a, b)| (a - b).abs() < 1e-9)
25192 );
25193 assert_eq!(p.kv_cache.seq_len(), ids.len());
25194 }
25195
25196 #[test]
25197 fn sigmoid_router_floor_is_explicit_per_architecture() {
25198 let zero = || DenseFfn {
25204 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25205 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25206 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25207 act: Act::Silu,
25208 down_t: None,
25209 segs: Vec::new(),
25210 };
25211 let m = MoeFfn {
25212 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25213 experts: vec![zero(), zero()],
25214 top_k: 1,
25215 norm_topk_prob: true,
25216 router_sigmoid: true,
25217 sigmoid_denom_eps: 1e-20,
25218 expert_bias: None,
25219 routed_scaling: 2.5,
25220 route_tau: None,
25221 shared: None,
25222 stats: std::cell::RefCell::new(Vec::new()),
25223 act_sq: std::cell::RefCell::new(Vec::new()),
25224 act_rows: std::cell::RefCell::new(Vec::new()),
25225 mask: None,
25226 per_expert_scale: None,
25227 router_input_norm: false,
25228 resonance: None,
25229 grown: Vec::new(),
25230 };
25231 let logits = [-20.0f32, -20.0];
25232 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
25233 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25234 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
25235 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
25236 assert!((flint_wsum - reference_expected).abs() < 1e-15);
25237 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
25238 assert!(legacy_wsum > minimax_wsum * 100.0);
25239 }
25240
25241 #[test]
25242 fn resonance_scores_match_formula_and_stable_tie() {
25243 let r = Resonance {
25244 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
25246 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
25247 k: 1,
25248 bias: vec![1.5, 0.5, 0.0],
25249 shell: Vec::new(),
25250 };
25251 let x = [1.0f32, 1.0];
25252 let mut got = vec![0.0; 3];
25253 r.scores(&x, &mut got);
25254 assert!((got[0] - 0.5).abs() < 1e-6);
25258 assert!((got[1] - 0.5).abs() < 1e-6);
25259 assert!(got[2].abs() < 1e-6);
25260 let best = got
25261 .iter()
25262 .enumerate()
25263 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
25264 .map(|(i, _)| i);
25265 assert_eq!(best, Some(0));
25266 assert!(got.iter().all(|v| v.is_finite()));
25267 }
25268
25269 #[test]
25274 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
25275 let plain = Resonance {
25281 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
25282 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
25283 k: 1,
25284 bias: vec![1.5, 0.5, 0.0, 0.0],
25285 shell: Vec::new(),
25286 };
25287 let shelled = Resonance {
25288 mu: plain.mu.clone(),
25289 u: plain.u.clone(),
25290 k: 1,
25291 bias: plain.bias.clone(),
25292 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
25293 };
25294 assert!(!plain.has_shell());
25295 assert!(shelled.has_shell());
25296 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
25297 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
25298 set_growth_shell(Some(true));
25299 assert!(growth_shell_enabled());
25300 let x = [1.0f32, 1.0];
25303 plain.scores(&x, &mut a);
25304 shelled.scores(&x, &mut b);
25305 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
25306 assert!(a[2] == 0.0 && a[3] == 0.0);
25307 let xo = [3.0f32, 0.0];
25310 plain.scores(&xo, &mut a);
25311 shelled.scores(&xo, &mut b);
25312 assert_eq!(a[2], -6.0);
25313 assert_eq!(a[3], -6.0);
25314 assert_eq!(
25315 bits(&a[..3]),
25316 bits(&b[..3]),
25317 "trunk rows + the boundary row"
25318 );
25319 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
25320 assert_eq!(shelled.effective_shell(4), shelled.shell);
25321 set_growth_shell(Some(false));
25324 assert!(!growth_shell_enabled());
25325 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
25326 shelled.scores(&xo, &mut b);
25327 assert_eq!(bits(&a), bits(&b));
25328 set_growth_shell(None);
25329 let short = Resonance {
25332 shell: vec![f32::INFINITY, f32::INFINITY],
25333 ..shelled
25334 };
25335 set_growth_shell(Some(true));
25336 short.scores(&xo, &mut b);
25337 assert_eq!(bits(&a), bits(&b));
25338 set_growth_shell(None);
25339 }
25340
25341 #[test]
25345 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
25346 let zero = || DenseFfn {
25347 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25348 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25349 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25350 act: Act::Silu,
25351 down_t: None,
25352 segs: Vec::new(),
25353 };
25354 let moe = |sigmoid: bool| MoeFfn {
25355 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
25356 experts: vec![zero(), zero(), zero(), zero()],
25357 top_k: 1,
25358 norm_topk_prob: true,
25359 router_sigmoid: sigmoid,
25360 sigmoid_denom_eps: 1e-6,
25361 expert_bias: None,
25362 routed_scaling: 1.0,
25363 route_tau: None,
25364 shared: None,
25365 stats: std::cell::RefCell::new(Vec::new()),
25366 act_sq: std::cell::RefCell::new(Vec::new()),
25367 act_rows: std::cell::RefCell::new(Vec::new()),
25368 mask: None,
25369 per_expert_scale: None,
25370 router_input_norm: false,
25371 resonance: None,
25372 grown: Vec::new(),
25373 };
25374 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
25375 for sigmoid in [false, true] {
25376 let m = moe(sigmoid);
25377 let (idx, p, wsum) = moe_route(&logits, &m, None);
25378 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
25379 assert_eq!(p[1], 0.0);
25380 assert_eq!(p[3], 0.0);
25381 assert!(p[2] > p[0] && p[0] > 0.0);
25382 assert!(p.iter().all(|v| v.is_finite()));
25383 let w = p[2] / wsum;
25384 if sigmoid {
25385 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
25387 } else {
25388 assert_eq!(
25389 w, 1.0,
25390 "softmax: the renormalized top-1 weight is exactly 1"
25391 );
25392 }
25393 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
25396 assert_eq!(idx, vec![2]);
25397 }
25398 let m = moe(false);
25400 let (idx, _, _) = moe_route(
25401 &[
25402 f32::NEG_INFINITY,
25403 f32::NEG_INFINITY,
25404 f32::NEG_INFINITY,
25405 -9.0,
25406 ],
25407 &m,
25408 None,
25409 );
25410 assert_eq!(idx, vec![3]);
25411 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
25414 assert_eq!(idx, vec![0]);
25415 assert!(p.iter().all(|&v| v == 0.25));
25416 assert!(wsum.is_finite() && wsum > 0.0);
25417 }
25418
25419 #[cfg(feature = "gpu")]
25425 #[test]
25426 fn minimax_device_route_matches_host_sigmoid_contract() {
25427 match crate::gpu_wgpu::selected_and_up() {
25428 None => {
25429 crate::gpu_wgpu::skip_or_fail(module_path!());
25430 return;
25431 }
25432 Some(false) => {
25440 eprintln!(
25441 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25442 module_path!()
25443 );
25444 return;
25445 }
25446 Some(true) => {}
25447 }
25448 let n = 128usize;
25449 let top_k = 4usize;
25450 let logits: Vec<f32> = (0..n)
25451 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25452 .collect();
25453 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25454 let m = MoeFfn {
25455 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25456 experts: Vec::new(),
25457 top_k,
25458 norm_topk_prob: true,
25459 router_sigmoid: true,
25460 sigmoid_denom_eps: 1e-20,
25461 expert_bias: Some(bias.clone()),
25462 routed_scaling: 2.0,
25463 route_tau: None,
25464 shared: None,
25465 stats: std::cell::RefCell::new(Vec::new()),
25466 act_sq: std::cell::RefCell::new(Vec::new()),
25467 act_rows: std::cell::RefCell::new(Vec::new()),
25468 mask: None,
25469 per_expert_scale: None,
25470 router_input_norm: false,
25471 resonance: None,
25472 grown: Vec::new(),
25473 };
25474 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25475 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
25476
25477 for shared in [Some(n as u32), None] {
25478 let mut gi = Vec::new();
25479 let mut gw = Vec::new();
25480 assert!(
25481 crate::gpu_wgpu::minimax_moe_route_for_test(
25482 &logits,
25483 Some(&bias),
25484 top_k,
25485 2.0,
25486 shared,
25487 &mut gi,
25488 &mut gw,
25489 ),
25490 "the device did not come up — the test checked nothing"
25491 );
25492 let routed = if shared.is_some() { top_k } else { gi.len() };
25493 assert_eq!(
25494 &gi[..routed],
25495 &ci[..],
25496 "different experts or order (shared={shared:?})"
25497 );
25498 let d = cw
25499 .iter()
25500 .zip(&gw[..routed])
25501 .map(|(a, b)| (a - b).abs())
25502 .fold(0.0f32, f32::max);
25503 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
25504 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
25505 if let Some(s) = shared {
25506 assert_eq!(gi[top_k], s as usize, "shared pin slot");
25507 assert_eq!(gw[top_k], 1.0, "shared weight");
25508 }
25509 }
25510 }
25511
25512 #[cfg(feature = "gpu")]
25517 #[test]
25518 fn minimax_device_cold_weights_match_host_contract() {
25519 match crate::gpu_wgpu::selected_and_up() {
25520 None => {
25521 crate::gpu_wgpu::skip_or_fail(module_path!());
25522 return;
25523 }
25524 Some(false) => {
25532 eprintln!(
25533 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
25534 module_path!()
25535 );
25536 return;
25537 }
25538 Some(true) => {}
25539 }
25540 let n = 128usize;
25541 let top_k = 4usize;
25542 let logits: Vec<f32> = (0..n)
25543 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
25544 .collect();
25545 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
25546 let m = MoeFfn {
25547 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
25548 experts: Vec::new(),
25549 top_k,
25550 norm_topk_prob: true,
25551 router_sigmoid: true,
25552 sigmoid_denom_eps: 1e-20,
25553 expert_bias: Some(bias.clone()),
25554 routed_scaling: 2.0,
25555 route_tau: None,
25556 shared: None,
25557 stats: std::cell::RefCell::new(Vec::new()),
25558 act_sq: std::cell::RefCell::new(Vec::new()),
25559 act_rows: std::cell::RefCell::new(Vec::new()),
25560 mask: None,
25561 per_expert_scale: None,
25562 router_input_norm: false,
25563 resonance: None,
25564 grown: Vec::new(),
25565 };
25566 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
25567 assert_eq!(ci.len(), top_k);
25568
25569 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
25571 let mut cold = Vec::new();
25572 let mut idx = Vec::new();
25573 let mut dw = Vec::new();
25574 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25575 &logits,
25576 Some(&bias),
25577 top_k,
25578 2.0,
25579 &resident,
25580 n as u32,
25581 &mut cold,
25582 &mut idx,
25583 &mut dw,
25584 ));
25585 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
25586 for (r, &e) in ci.iter().enumerate() {
25591 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
25592 let host = p[e] / wsum;
25593 let d = (dw[r] - host).abs();
25594 assert!(
25595 d <= 1e-6 * host.abs().max(1.0),
25596 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
25597 dw[r]
25598 );
25599 }
25600 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
25601 assert_eq!(dw[top_k], 1.0, "shared weight");
25602
25603 let mut remap = resident.clone();
25605 remap[ci[0]] = u32::MAX;
25606 remap[ci[2]] = u32::MAX;
25607 let mut cold = Vec::new();
25608 let mut idx2 = Vec::new();
25609 let mut dw2 = Vec::new();
25610 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
25611 &logits,
25612 Some(&bias),
25613 top_k,
25614 2.0,
25615 &remap,
25616 n as u32,
25617 &mut cold,
25618 &mut idx2,
25619 &mut dw2,
25620 ));
25621 let mut resident_weight_checked = 0usize;
25629 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
25630 if dw2[r] == 0.0 {
25631 continue; }
25633 let e = slot as usize;
25634 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
25635 let host = p[e] / wsum;
25636 let d = (dw2[r] - host).abs();
25637 assert!(
25638 d <= 1e-6 * host.abs().max(1.0),
25639 "partial-bank resident weight expert {e}: Δ={d:.3e}"
25640 );
25641 resident_weight_checked += 1;
25642 }
25643 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
25644 println!("partial bank: checked {resident_weight_checked} resident weights");
25645 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
25646 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
25647 ids.sort_unstable();
25648 let mut want = vec![ci[0], ci[2]];
25649 want.sort_unstable();
25650 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
25651 for &(e, w) in &cold {
25652 let host = p[e] / wsum;
25653 let d = (w - host).abs();
25654 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
25655 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
25656 }
25657 }
25658
25659 #[test]
25664 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
25665 let zero = || DenseFfn {
25666 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25667 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25668 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
25669 act: Act::Silu,
25670 down_t: None,
25671 segs: Vec::new(),
25672 };
25673 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
25674 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
25675 experts: vec![zero(), zero(), zero()],
25676 top_k: 1,
25677 norm_topk_prob: norm_topk,
25678 router_sigmoid: false,
25679 sigmoid_denom_eps: 1e-6,
25680 expert_bias: None,
25681 routed_scaling: 1.0,
25682 route_tau: None,
25683 shared: None,
25684 stats: std::cell::RefCell::new(Vec::new()),
25685 act_sq: std::cell::RefCell::new(Vec::new()),
25686 act_rows: std::cell::RefCell::new(Vec::new()),
25687 mask: None,
25688 per_expert_scale: None,
25689 router_input_norm: false,
25690 resonance: resonant.then(|| Resonance {
25691 mu: vec![0.0; 6],
25692 u: Vec::new(),
25693 k: 0,
25694 bias: vec![0.0; 3],
25695 shell: Vec::new(),
25696 }),
25697 grown: Vec::new(),
25698 };
25699 let lo = -0.1f32;
25702 let hi = f32::from_bits(lo.to_bits() - 1);
25703 assert!(hi > lo && hi - lo < 2f32.powi(-25));
25704 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
25705 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
25707 assert_eq!(
25708 idx,
25709 vec![0],
25710 "softmax tie → lower index (the gated contract)"
25711 );
25712 for norm in [true, false] {
25715 let m = moe(true, norm);
25716 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
25717 assert_eq!(idx, vec![1], "norm_topk {norm}");
25718 assert_eq!(p, vec![0.0, 1.0, 0.0]);
25719 assert_eq!(p[1] / wsum, 1.0);
25720 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
25723 assert_eq!(idx, vec![0]);
25724 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
25726 assert_eq!(idx, vec![2]);
25727 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
25728 assert_eq!(idx, vec![0]);
25729 assert_eq!(p[0] / wsum, 1.0);
25730 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
25733 assert_eq!(idx, vec![0]);
25734 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
25735 }
25736 }
25737}