1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18use crate::kv_cache::KvCache;
19use crate::linear_core::{
20 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
21 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
22 vmf_phase_pair,
23};
24use crate::pool::Pool;
25use crate::qtensor::QTensor;
26use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
27use crate::tokenizer::Tokenizer;
28use cortiq_core::mask::TaskMask;
29use cortiq_core::types::NormStyle;
30
31pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
32 std::sync::atomic::AtomicBool::new(false);
33
34#[derive(Clone)]
41struct GraphMoeBank {
42 experts: Vec<(usize, usize, usize)>,
43 q4tp: Option<bool>,
44 gu_q2: Option<bool>,
45 q82: Option<bool>,
46 act: Option<crate::gpu::GraphAct>,
47 model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
48}
49
50type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
56
57thread_local! {
62 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
63}
64
65struct ExactMtpCalibrationGuard;
66
67impl ExactMtpCalibrationGuard {
68 fn enter() -> Self {
69 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
70 Self
71 }
72}
73
74impl Drop for ExactMtpCalibrationGuard {
75 fn drop(&mut self) {
76 EXACT_MTP_CALIBRATION.with(|depth| {
77 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
78 depth.set(depth.get().saturating_sub(1));
79 });
80 }
81}
82
83fn exact_mtp_calibration_active() -> bool {
84 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
85}
86
87fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
91 logits.resize(vocab_size, f32::NEG_INFINITY);
92}
93
94struct ForwardScratch {
98 n1: Vec<f32>,
99 n2: Vec<f32>,
100 p1: Vec<f32>,
101 p2: Vec<f32>,
102}
103
104impl ForwardScratch {
105 fn new(hidden: usize) -> Self {
106 Self {
107 n1: vec![0.0; hidden],
108 n2: vec![0.0; hidden],
109 p1: vec![0.0; hidden],
110 p2: vec![0.0; hidden],
111 }
112 }
113}
114
115pub struct Pipeline {
117 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
122 pub tokenizer: std::sync::Arc<Tokenizer>,
125 pub kv_cache: KvCache,
126 pub sampler_config: SamplerConfig,
127 pub weights: PipelineWeights,
128 pub hidden_size: usize,
129 pub intermediate_size: usize,
130 pub num_heads: usize,
131 pub num_kv_heads: usize,
132 pub head_dim: usize,
133 pub num_layers: usize,
135 pub physical_layers: usize,
137 pub loop_final_norm: bool,
139 pub vocab_size: usize,
140 pub rms_eps: f64,
141 pub rope_base: f32,
142 pub norm_style: NormStyle,
143 pub rotary_dim: usize,
145 pub attention_heads_per_layer: Option<Vec<usize>>,
147 pub kv_heads_per_layer: Option<Vec<usize>>,
152 pub v_head_dim: Option<usize>,
157 pub layer_dump: Option<std::path::PathBuf>,
170 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
173 pub(crate) mimo_moe: crate::mimo_moe::Slot,
176 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
181 pub vmf_cfg: Option<VmfPhaseCfg>,
183 pub gdn_cfg: Option<GdnCfg>,
185 pub logit_multiplier: Option<f32>,
187 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
192 graph_failed: std::sync::atomic::AtomicBool,
197 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
200 pub kv_history: Vec<u32>,
205 pub kv_history_device: bool,
208 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
210 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
213 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
216 pub dsv4: Option<
220 Box<(
221 crate::dsv4::Dsv4Globals,
222 Vec<crate::dsv4::Dsv4Layer>,
223 crate::dsv4::Dsv4Cfg,
224 crate::dsv4::Dsv4State,
225 )>,
226 >,
227 pub dsv41: Option<
231 Box<(
232 crate::dsv41::Dsv41Globals,
233 Vec<crate::dsv41::Dsv41Layer>,
234 crate::dsv41::Dsv41Cfg,
235 crate::dsv41::Dsv41State,
236 )>,
237 >,
238 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
240 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
242 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
247 clover_stats: clover::CloverStats,
248 pub qwen4_exp: Option<
249 Box<(
250 crate::qwen4_exp::Globals,
251 Vec<crate::qwen4_exp::Layer>,
252 crate::qwen4_exp::Cfg,
253 crate::qwen4_exp::State,
254 )>,
255 >,
256 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
260 pub dspark: Option<crate::dsv4::DsparkState>,
262 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
265 pub dspark_hist: Vec<usize>,
267 pub dspark_real: Vec<u32>,
271 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
275 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
277 pub dspark_draft_ns: u128,
281 m3_winners: Vec<Vec<u32>>,
283 m3_counts: Vec<u32>,
285 m3_n_exp: usize,
286 m3_tokens: u64,
287 m3_prewarmed: bool,
288 pub short_conv_cfg: Option<ShortConvCfg>,
291 pub mtp: Option<MtpModule>,
293 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
297 verify_exact_moe: bool,
300 pub speculative: bool,
302 pub ignore_eos: bool,
308 pub draft_full_streak: u32,
314 pub spec_k_adapt: Option<usize>,
322 pub spec_acc_ewma: f32,
324 rng: SplitMix64,
325 sampler_scratch: SamplerScratch,
326 spec_forced: Option<u32>,
332 spec_q: Vec<Vec<f32>>,
333 spec_p: Vec<f32>,
334 spec_res: Vec<f32>,
335 spec_qs: Vec<sampler::Sparse>,
337 spec_ps: sampler::Sparse,
338 spec_ress: sampler::Sparse,
339 mtp_graph_mode: Option<bool>,
346 #[cfg(target_os = "macos")]
349 metal_verify: Option<MetalVerifyPending>,
350 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
354 ws: ForwardScratch,
358 pool: Option<std::sync::Arc<Pool>>,
360 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
364 pub(crate) dyn_force_f32: bool,
366 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
371 pub(crate) dyn_active: Option<usize>,
377 pub(crate) dyn_blend_loaded: bool,
381 pub(crate) dyn_phi_layer: Option<usize>,
384 dyn_phi_ema: Vec<f32>,
386 dyn_phi_seen: usize,
387 pub dyn_router: Option<crate::swarm::DynRouter>,
390 o1_cfg: Option<crate::nystrom::O1Cfg>,
393 o1_epoch: u64,
396 o1_flags: Vec<bool>,
398 trace: bool,
401 calib_temp: f32,
404 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
406 graph_kv_id: u64,
407 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
410 graph_want_logits: bool,
411 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
415 graph_head_required: bool,
416 graph_logits: Option<Vec<f32>>,
419 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
422 graph_refused: std::sync::atomic::AtomicBool,
428 pub embed_multiplier: f32,
430 pub attn_scale: f32,
433 pub swa: Option<(usize, usize)>,
436 pub swa_trim: Option<(usize, usize)>,
443 swa_trim_anchor: usize,
447 pub chunk_min_rows: usize,
451 pub sliding_layers: Option<Vec<bool>>,
454 pub no_rope_layers: Option<Vec<bool>>,
459 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
465 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
468 pub kv_prefix: KvPrefix,
472 pub last_prefill_tokens: usize,
475 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
478 pub rotary_dim_local: Option<usize>,
479 pub rope_scale: f32,
480 pub rope_scale_local: f32,
481 pub global_attn: Option<(usize, usize)>,
484 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
487 pub attn_v_norm: bool,
489 pub qk_norm_after_rope: bool,
491 pub proj_gate_sigmoid: bool,
494 pub final_softcap: Option<f32>,
496 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
500 pub attn_softcap: f32,
502 confidence_on: bool,
506 #[cfg(test)]
509 nll_test_fail_at: Option<usize>,
510 #[cfg(test)]
513 nll_test_force_serial: bool,
514}
515
516#[cfg(target_os = "macos")]
517impl Drop for Pipeline {
518 fn drop(&mut self) {
519 let _ = crate::gpu_metal::wait_replay();
521 crate::gpu::kv_mirror_drop(self.graph_kv_id);
522 }
523}
524
525#[cfg(not(target_os = "macos"))]
526impl Drop for Pipeline {
527 fn drop(&mut self) {
528 crate::gpu::graph_kv_reset(self.graph_kv_id);
532 }
533}
534
535pub struct PipelineWeights {
540 pub embed_tokens: QTensor,
542 pub layers: Vec<LayerWeights>,
544 pub lm_head: QTensor,
546 pub final_norm: Vec<f32>,
548}
549
550pub struct LayerWeights {
552 pub input_norm: Vec<f32>,
553 pub post_norm: Vec<f32>,
556 pub attn_out_norm: Option<Vec<f32>>,
559 pub layer_scale: Option<f32>,
561 pub ffn_out_norm: Option<Vec<f32>>,
564 pub ffn: FfnKind,
565 pub attn: AttnKind,
566}
567
568#[derive(Clone, Copy, PartialEq, Debug, Default)]
572pub enum Act {
573 #[default]
574 Silu,
575 GeluTanh,
576 Gelu,
578 Situ {
581 beta: f32,
582 linear_beta: f32,
583 },
584 SwiGluOai {
589 alpha: f32,
590 limit: f32,
591 },
592}
593
594impl Act {
595 pub fn from_arch(name: &str) -> Self {
596 if name == "gelu_tanh" {
597 Self::GeluTanh
598 } else if name == "gelu" {
599 Self::Gelu
600 } else {
601 Self::Silu
602 }
603 }
604
605 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
607 match arch.hidden_act.as_str() {
608 "situ" => Self::Situ {
609 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
610 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
611 },
612 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
617 alpha: 1.702,
618 limit: 7.0,
619 },
620 other => Self::from_arch(other),
621 }
622 }
623
624 #[inline]
629 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
630 Self::SwiGluOai { alpha, limit }
631 }
632
633 #[inline]
634 pub fn apply(self, x: f32) -> f32 {
635 match self {
636 Self::Silu => inference::silu(x),
637 Self::GeluTanh => inference::gelu_tanh(x),
638 Self::Gelu => inference::gelu_erf(x),
639 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
640 Self::SwiGluOai { alpha, limit } => {
641 let gate = x.min(limit);
642 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
643 }
644 }
645 }
646
647 #[inline]
650 pub fn combine(self, g: f32, u: f32) -> f32 {
651 match self {
652 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
653 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
654 }
655 Self::SwiGluOai { alpha, limit } => {
656 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
657 }
658 _ => self.apply(g) * u,
659 }
660 }
661
662 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
666 match self {
667 Self::Silu => Some(crate::gpu::GraphAct::Silu),
668 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
669 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
670 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
671 }
672 }
673
674 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
680 self.moe_job_graph_contract().map(|(act, _)| act)
681 }
682
683 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
689 match self {
690 Self::SwiGluOai { alpha, limit }
691 if alpha.to_bits() == 1.702f32.to_bits()
692 && limit.to_bits() == 7.0f32.to_bits() =>
693 {
694 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
695 }
696 _ => self.graph_act().map(|act| (act, 0.0)),
697 }
698 }
699}
700
701pub struct DenseFfn {
703 pub gate_proj: QTensor,
704 pub up_proj: QTensor,
705 pub down_proj: QTensor,
706 pub act: Act,
708 pub down_t: Option<QTensor>,
714 pub segs: Vec<FfnSeg>,
721}
722
723pub struct FfnSeg {
728 pub gate: QTensor,
729 pub up: QTensor,
730 pub down: QTensor,
731 pub start: usize,
732 pub width: usize,
733}
734
735pub enum FfnKind {
738 Dense(DenseFfn),
739 Moe(MoeFfn),
743 DenseMoe(Box<DenseMoeFfn>),
750}
751
752pub struct DenseMoeFfn {
754 pub dense: DenseFfn,
755 pub moe: MoeFfn,
756 pub post_norm_1: Vec<f32>,
758 pub pre_norm_2: Vec<f32>,
761 pub post_norm_2: Vec<f32>,
763}
764
765pub struct MoeFfn {
766 pub router: QTensor,
768 pub experts: Vec<DenseFfn>,
769 pub top_k: usize,
770 pub norm_topk_prob: bool,
771 pub router_sigmoid: bool,
774 pub sigmoid_denom_eps: f32,
780 pub expert_bias: Option<Vec<f32>>,
784 pub routed_scaling: f32,
787 pub route_tau: Option<f32>,
793 pub shared: Option<(DenseFfn, Option<QTensor>)>,
796 pub stats: std::cell::RefCell<Vec<u64>>,
800 pub act_sq: std::cell::RefCell<Vec<f64>>,
807 pub act_rows: std::cell::RefCell<Vec<f32>>,
813 pub mask: Option<Vec<bool>>,
818 pub per_expert_scale: Option<Vec<f32>>,
821 pub router_input_norm: bool,
825 pub resonance: Option<Resonance>,
829 pub grown: Vec<GrownExpert>,
835}
836
837#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct GrownExpert {
840 pub record: String,
842 pub record_index: usize,
844 pub layer: usize,
845 pub expert: usize,
849}
850
851pub struct Resonance {
853 pub mu: Vec<f32>,
855 pub u: Vec<f32>,
857 pub k: usize,
858 pub bias: Vec<f32>,
860 pub shell: Vec<f32>,
866}
867
868static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
871
872pub fn growth_shell_enabled() -> bool {
877 use std::sync::atomic::Ordering;
878 match GROWTH_SHELL.load(Ordering::Relaxed) {
879 1 => true,
880 2 => false,
881 _ => {
882 let off = std::env::var("CMF_GROWTH_SHELL")
883 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
884 .unwrap_or(false);
885 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
886 !off
887 }
888 }
889}
890
891pub fn set_growth_shell(on: Option<bool>) {
896 GROWTH_SHELL.store(
897 match on {
898 Some(true) => 1,
899 Some(false) => 2,
900 None => 0,
901 },
902 std::sync::atomic::Ordering::Relaxed,
903 );
904}
905
906impl Resonance {
907 pub fn has_shell(&self) -> bool {
909 self.shell.iter().any(|s| s.is_finite())
910 }
911
912 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
915 let mut out = vec![f32::INFINITY; ne];
916 if growth_shell_enabled() {
917 for (o, s) in out.iter_mut().zip(&self.shell) {
918 *o = *s;
919 }
920 }
921 out
922 }
923
924 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
929 let h = x.len();
930 let ne = out.len();
931 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
932 for e in 0..ne {
933 let mu = &self.mu[e * h..(e + 1) * h];
934 let mut d2 = 0.0f32;
935 for j in 0..h {
936 let d = x[j] - mu[j];
937 d2 += d * d;
938 }
939 let mut proj = 0.0f32;
940 for i in 0..self.k {
941 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
942 let mut p = 0.0f32;
943 for j in 0..h {
944 p += (x[j] - mu[j]) * u[j];
945 }
946 proj += p * p;
947 }
948 let err = d2 - proj;
949 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
950 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
951 out[e] = f32::NEG_INFINITY;
952 }
953 }
954 }
955}
956
957pub enum AttnKind {
960 Full {
962 wq: QTensor,
963 wk: QTensor,
964 wv: QTensor,
965 wo: QTensor,
966 q_norm: Option<Vec<f32>>,
967 k_norm: Option<Vec<f32>>,
968 output_gate: bool,
969 softplus_gate: Option<(QTensor, bool)>,
973 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
975 },
976 Linear(VmfPhaseWeights),
978 LinearGdn(GdnWeights),
980 ShortConv(ShortConvWeights),
983 Mla(Box<MlaWeights>),
991 Kda(Box<crate::linear_core::KdaWeights>),
995 Bounded(Box<crate::bounded::BoundedWeights>),
1000}
1001
1002pub struct MlaWeights {
1004 pub q_proj: QTensor,
1008 pub q_a: Option<QTensor>,
1011 pub q_a_norm: Option<Vec<f32>>,
1012 pub kv_a: QTensor,
1014 pub kv_a_norm: Vec<f32>,
1016 pub kv_b: QTensor,
1018 pub o_proj: QTensor,
1020 pub nh: usize,
1021 pub qk_rope: usize,
1022 pub qk_nope: usize,
1023 pub v_dim: usize,
1024 pub lora: usize,
1025 pub scale: f32,
1027 pub nope: bool,
1029}
1030
1031pub struct MtpModule {
1036 pub enorm: Vec<f32>,
1037 pub hnorm: Vec<f32>,
1038 pub eh_proj: QTensor,
1040 pub layer: LayerWeights,
1041 pub final_norm: Vec<f32>,
1042 pub kv: crate::kv_cache::LayerKvCache,
1043}
1044
1045const MTP_PROBE_FFN_ROWS: usize = 128;
1054
1055#[derive(Clone, Debug, PartialEq)]
1060pub struct MtpFfnProbe {
1061 pub mass: Vec<f64>,
1062 pub squared_mass: Vec<f64>,
1063 pub observed_positions: usize,
1064}
1065
1066#[cfg(target_os = "macos")]
1073enum MetalRowsItem<'a> {
1074 Gdn {
1075 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1076 first: usize,
1077 },
1078 Attn {
1079 l: crate::gpu_metal::AttnGpuLayer<'a>,
1080 li: usize,
1081 q_norm: Option<&'a [f32]>,
1082 k_norm: Option<&'a [f32]>,
1083 output_gate: bool,
1084 },
1085}
1086
1087#[cfg(target_os = "macos")]
1088struct MetalVerifyPending {
1089 graph: crate::gpu_metal::VerifyGraph,
1090 gdn_layers: Vec<usize>,
1091 attn_layers: Vec<(usize, usize)>,
1092}
1093
1094#[cfg(target_os = "macos")]
1098struct MetalWarmPending {
1099 graph: crate::gpu_metal::VerifyGraph,
1100 cpu_stored: usize,
1101 b: usize,
1102}
1103
1104#[cfg(target_os = "macos")]
1105enum MetalRowsRun {
1106 Declined,
1108 Failed,
1111 Completed(MetalVerifyPending),
1112}
1113
1114#[cfg(target_os = "macos")]
1115enum MetalPrefillOutcome {
1116 Declined,
1117 Failed,
1118 Completed(Vec<f32>),
1119}
1120
1121#[cfg(target_os = "macos")]
1122enum MetalBatchNllOutcome {
1123 Declined,
1124 Failed(String),
1125 Completed(f64, usize),
1126}
1127
1128#[derive(Clone, Copy)]
1134enum SpecTrial {
1135 Spec {
1136 t0: std::time::Instant,
1137 gen0: usize,
1138 rounds: usize,
1139 },
1140 Plain {
1141 t0: std::time::Instant,
1142 gen0: usize,
1143 },
1144 Decided {
1145 spec: bool,
1146 recheck_at: usize,
1147 },
1148}
1149
1150pub(crate) fn spec_time_level() -> u8 {
1154 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1155 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1156 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1157 Err(_) => 0,
1158 })
1159}
1160
1161fn graph_moe_bank_on() -> bool {
1164 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1165 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1166}
1167
1168fn graph_spec_wanted_by_policy(
1176 spec_env: Option<&str>,
1177 spec_default_ok: bool,
1178 penalized: bool,
1179 metal_wgpu: bool,
1180 native_metal: bool,
1181) -> bool {
1182 match spec_env {
1183 Some("0") => false,
1184 Some("1") => true,
1189 Some(_) => !native_metal,
1190 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1193 }
1194}
1195
1196struct SpecStampLog {
1202 t_last: std::time::Instant,
1203 items: Vec<(&'static str, f32)>,
1204}
1205
1206static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1207
1208pub(crate) fn spec_stamp(name: &'static str) {
1209 if spec_time_level() == 0 {
1210 return;
1211 }
1212 if let Ok(mut g) = SPEC_STAMPS.lock() {
1213 if let Some(log) = g.as_mut() {
1214 let now = std::time::Instant::now();
1215 log.items
1216 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1217 log.t_last = now;
1218 }
1219 }
1220}
1221
1222fn spec_stamps_begin() {
1223 if spec_time_level() == 0 {
1224 return;
1225 }
1226 if let Ok(mut g) = SPEC_STAMPS.lock() {
1227 *g = Some(SpecStampLog {
1228 t_last: std::time::Instant::now(),
1229 items: Vec::with_capacity(64),
1230 });
1231 }
1232}
1233
1234fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1235 SPEC_STAMPS
1236 .lock()
1237 .ok()
1238 .and_then(|mut g| g.take())
1239 .map(|l| l.items)
1240 .unwrap_or_default()
1241}
1242
1243fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1246 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1247 for &(n, ms) in items {
1248 match agg.iter_mut().find(|e| e.0 == n) {
1249 Some(e) => {
1250 e.1 += ms;
1251 e.2 += 1;
1252 }
1253 None => agg.push((n, ms, 1)),
1254 }
1255 }
1256 let mut s = String::with_capacity(agg.len() * 16);
1257 for (n, ms, k) in agg {
1258 if k > 1 {
1259 s.push_str(&format!("{n} {ms:.1}/{k} "));
1260 } else {
1261 s.push_str(&format!("{n} {ms:.1} "));
1262 }
1263 }
1264 s
1265}
1266
1267#[derive(Default, Clone, Copy)]
1289struct SpecMon {
1290 round_ms: f64,
1291 tokens: f64,
1292 plain_ms: f64,
1293 n: u32,
1294 fails: u32,
1295 zero_accept_rounds: u8,
1300 metal: bool,
1301}
1302
1303const SPEC_PROXY_TOKENS: f64 = 3.5;
1306const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1309
1310impl SpecMon {
1311 fn round(&mut self, dt_ms: f64, produced: usize) {
1312 if produced <= 1 {
1313 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1314 } else {
1315 self.zero_accept_rounds = 0;
1316 }
1317 self.n += 1;
1318 if self.n == 1 {
1319 return; }
1321 let a = if self.n == 2 { 1.0 } else { 0.3 };
1322 self.round_ms += a * (dt_ms - self.round_ms);
1323 self.tokens += a * (produced as f64 - self.tokens);
1324 }
1325 fn pays(&self) -> bool {
1326 if self.plain_ms > 0.0 {
1327 self.tokens * self.plain_ms > self.round_ms * 1.03
1328 } else {
1329 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1330 }
1331 }
1332 fn repeated_zero_acceptance(&self) -> bool {
1333 self.zero_accept_rounds >= 2
1334 }
1335 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1337 let n = generated.saturating_sub(gen0);
1338 if n >= 8 {
1339 return true;
1340 }
1341 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1342 }
1343}
1344
1345pub const KV_PREFIX_TAIL: usize = 128;
1348
1349#[derive(Debug, Clone, Default)]
1355pub struct KvPrefix {
1356 len: usize,
1357 hash: u64,
1358 tail: Vec<u32>,
1359 device: bool,
1364}
1365
1366impl KvPrefix {
1367 #[inline]
1368 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1369 for &id in ids {
1370 h ^= id as u64;
1371 h = h.wrapping_mul(0x100000001b3);
1372 h ^= h >> 29;
1373 }
1374 h
1375 }
1376
1377 pub fn clear(&mut self) {
1378 self.len = 0;
1379 self.hash = 0xcbf29ce484222325;
1380 self.tail.clear();
1381 self.device = false;
1382 }
1383
1384 pub fn on_device(&self) -> bool {
1386 self.device
1387 }
1388
1389 pub fn set_on_device(&mut self, device: bool) {
1391 self.device = device;
1392 }
1393
1394 pub fn len(&self) -> usize {
1396 self.len
1397 }
1398
1399 pub fn is_empty(&self) -> bool {
1400 self.len == 0
1401 }
1402
1403 pub fn tail_len(&self) -> usize {
1405 self.tail.len()
1406 }
1407
1408 pub fn set(&mut self, ids: &[u32]) {
1410 self.clear();
1411 self.extend(ids);
1412 }
1413
1414 pub fn extend(&mut self, more: &[u32]) {
1416 if self.len == 0 && self.hash == 0 {
1417 self.hash = 0xcbf29ce484222325;
1418 }
1419 self.hash = Self::fold(self.hash, more);
1420 self.len += more.len();
1421 if more.len() >= KV_PREFIX_TAIL {
1422 self.tail.clear();
1423 self.tail
1424 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1425 } else {
1426 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1427 self.tail.drain(..drop);
1428 self.tail.extend_from_slice(more);
1429 }
1430 }
1431
1432 pub fn extension(&self, ids: &[u32]) -> usize {
1436 if self.len == 0 || ids.len() <= self.len {
1437 return 0;
1438 }
1439 let t = self.tail.len();
1440 if ids[self.len - t..self.len] != self.tail[..] {
1441 return 0;
1442 }
1443 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1444 return 0;
1445 }
1446 self.len
1447 }
1448}
1449
1450pub struct GenerateResult {
1452 pub text: String,
1453 pub token_ids: Vec<u32>,
1454 pub prompt_tokens: usize,
1455 pub tokens_generated: usize,
1456 pub finish_reason: String,
1457 pub mtp_drafted: usize,
1459 pub mtp_accepted: usize,
1460 pub token_confidence: Vec<f32>,
1465 pub traces: Vec<TokenTrace>,
1468}
1469
1470#[derive(Clone, Debug)]
1475pub struct TokenTrace {
1476 pub t: usize,
1478 pub token_id: u32,
1480 pub confidence: f32,
1482 pub active_skill: Option<String>,
1484 pub recon: Option<f32>,
1488 pub switched: bool,
1491}
1492
1493#[cfg_attr(not(test), allow(dead_code))]
1498fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1499 let t = if temp > 1e-3 { temp } else { 1.0 };
1500 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1501 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1502 if sum > 0.0 {
1503 (((logits[id as usize] - max) / t).exp()) / sum
1504 } else {
1505 0.0
1506 }
1507}
1508
1509fn prefill_batched() -> bool {
1512 std::env::var("CMF_PREFILL")
1513 .map(|v| v != "seq")
1514 .unwrap_or(true)
1515}
1516
1517#[inline]
1521fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1522 let graph_quality = unmasked && prefer_graph;
1523 let fused_head_quality = graph_quality && native_metal;
1524 (graph_quality, fused_head_quality)
1525}
1526
1527#[derive(Clone, Copy)]
1531enum PrefillIn<'a> {
1532 Ids(&'a [u32]),
1533 Hidden(&'a [f32]),
1534}
1535
1536impl Pipeline {
1543 pub(crate) fn can_prefill_batched(&self) -> bool {
1544 #[cfg(test)]
1545 let force_serial = self.nll_test_force_serial;
1546 #[cfg(not(test))]
1547 let force_serial = false;
1548 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1549 }
1550
1551 fn automatic_gpu_prefix(&self) -> Option<usize> {
1554 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1555 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1556 }
1557
1558 pub fn prefill_chunk(&self) -> usize {
1562 let env = env_prefill_chunk();
1563 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1564 return prefill_chunk_rule(env, ChunkHost::here(), false);
1565 }
1566 prefill_chunk_rule(
1567 None,
1568 ChunkHost::Other,
1569 self.chunk_stack_facts().dense_on_discrete(),
1570 )
1571 }
1572
1573 fn per_token_stack(&self) -> bool {
1577 self.g3n.is_some() || self.g4e.is_some()
1578 }
1579
1580 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1581 let plain_dense = !self.weights.layers.is_empty()
1582 && !self.per_token_stack()
1583 && self.dsv4.is_none()
1584 && self.dsv41.is_none()
1585 && self.qwen4_exp.is_none()
1586 && self.weights.layers.iter().all(|lw| {
1587 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1588 });
1589 let gpu_on = crate::gpu::enabled();
1590 ChunkStackFacts {
1591 plain_dense,
1592 discrete: gpu_on && crate::gpu::discrete(),
1593 gpu_on,
1594 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1597 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1598 multi_gpu: self.gpu_plan.is_some(),
1599 o1: self.o1_active(),
1600 }
1601 }
1602}
1603
1604pub fn prefill_chunk() -> usize {
1613 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1614}
1615
1616fn env_prefill_chunk() -> Option<usize> {
1617 std::env::var("CMF_PREFILL_CHUNK")
1618 .ok()
1619 .and_then(|v| v.parse::<usize>().ok())
1620}
1621
1622#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1624enum ChunkHost {
1625 Macos,
1626 Aarch64,
1628 Other,
1630}
1631
1632impl ChunkHost {
1633 fn here() -> Self {
1634 if cfg!(target_os = "macos") {
1635 ChunkHost::Macos
1636 } else if cfg!(target_arch = "aarch64") {
1637 ChunkHost::Aarch64
1638 } else {
1639 ChunkHost::Other
1640 }
1641 }
1642}
1643
1644const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1651
1652fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1658 if let Some(n) = env {
1659 return n.max(1);
1660 }
1661 match host {
1662 ChunkHost::Macos => 512,
1663 ChunkHost::Aarch64 => 256,
1666 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1667 ChunkHost::Other => 48,
1668 }
1669}
1670
1671#[derive(Clone, Copy, Debug, Default)]
1673struct ChunkStackFacts {
1674 plain_dense: bool,
1677 discrete: bool,
1679 gpu_on: bool,
1681 capacity_split: bool,
1683 multi_gpu: bool,
1685 o1: bool,
1687}
1688
1689impl ChunkStackFacts {
1690 fn dense_on_discrete(self) -> bool {
1691 self.plain_dense
1692 && self.discrete
1693 && self.gpu_on
1694 && !self.capacity_split
1695 && !self.multi_gpu
1696 && !self.o1
1697 }
1698}
1699
1700#[inline]
1706fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1707 if end <= start || start >= input_len {
1708 return 0;
1709 }
1710 let rows = (end.min(input_len) - start).min(input_len - start);
1711 if end < input_len {
1712 rows
1713 } else {
1714 rows.saturating_sub(1)
1715 }
1716}
1717
1718pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1720
1721#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1723pub(crate) struct ReuseLayer {
1724 pub full: bool,
1727 pub host_rows: usize,
1730 pub device_rows: Option<usize>,
1732 pub device_state: bool,
1734}
1735
1736#[derive(Debug, Clone, PartialEq, Eq)]
1738pub(crate) enum ReusePlan {
1739 Ready,
1741 Pull(Vec<(usize, usize, usize)>),
1744 Fresh,
1746}
1747
1748pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1758 let mut pulls = Vec::new();
1759 for (li, l) in layers.iter().enumerate() {
1760 if !l.full {
1761 if l.device_state {
1762 return ReusePlan::Fresh;
1763 }
1764 continue;
1765 }
1766 if l.host_rows == reuse_from {
1767 continue;
1768 }
1769 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1770 pulls.push((li, l.host_rows, reuse_from));
1771 continue;
1772 }
1773 return ReusePlan::Fresh;
1774 }
1775 if pulls.is_empty() {
1776 ReusePlan::Ready
1777 } else {
1778 ReusePlan::Pull(pulls)
1779 }
1780}
1781
1782impl Pipeline {
1783 fn reset_fresh_mtp_draft_state(&mut self) {
1786 self.draft_full_streak = 0;
1787 }
1788
1789 fn clear_sequence_state(&mut self) {
1797 #[cfg(target_os = "macos")]
1800 let _ = crate::gpu_metal::wait_replay();
1801 self.kv_cache.clear();
1802 self.clear_history();
1805 self.graph_logits = None;
1806 if let Some(b) = &mut self.dsv41 {
1807 b.3.clear();
1808 }
1809 crate::gpu::graph_kv_reset(self.graph_kv_id);
1810 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1815 }
1816
1817 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1824 if self.graph_prefill_preferred() {
1825 return true;
1826 }
1827 let kv_id = self.graph_kv_id;
1828 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1829 .map(|li| {
1830 let full = matches!(
1831 self.weights.layers[self.phys_layer(li)].attn,
1832 AttnKind::Full { .. }
1833 );
1834 ReuseLayer {
1835 full,
1836 host_rows: self.kv_cache.layers[li].pos_len(),
1839 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1840 device_state: crate::gpu::graph_state_resident(kv_id, li),
1841 }
1842 })
1843 .collect();
1844 if layers
1848 .iter()
1849 .all(|l| l.device_rows.is_none() && !l.device_state)
1850 {
1851 return true;
1852 }
1853 let plan = kv_reuse_plan(reuse_from, &layers);
1854 let (what, rows, n) = match &plan {
1855 ReusePlan::Ready => ("host ready", 0, 0),
1856 ReusePlan::Fresh => ("fresh", 0, 0),
1857 ReusePlan::Pull(p) => (
1858 "pull",
1859 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1860 p.len(),
1861 ),
1862 };
1863 let t0 = std::time::Instant::now();
1864 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1865 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1866 eprintln!(
1867 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1868 if ok { "" } else { " (failed → fresh)" },
1869 t0.elapsed().as_secs_f64() * 1e3
1870 );
1871 }
1872 ok
1873 }
1874
1875 fn apply_kv_reuse_plan(
1876 &mut self,
1877 reuse_from: usize,
1878 plan: ReusePlan,
1879 layers: &[ReuseLayer],
1880 ) -> bool {
1881 let kv_id = self.graph_kv_id;
1882 match plan {
1883 ReusePlan::Fresh => return false,
1884 ReusePlan::Ready => {}
1885 ReusePlan::Pull(pulls) => {
1886 let (nkv, hd) = {
1891 let c = &self.kv_cache.layers[pulls[0].0];
1892 (c.num_kv_heads, c.head_dim)
1893 };
1894 let uniform = pulls.iter().all(|&(li, _, _)| {
1895 let c = &self.kv_cache.layers[li];
1896 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1897 });
1898 let batched = if uniform {
1899 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1900 } else {
1901 None
1902 };
1903 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1904 Some(rows) => rows,
1905 None => {
1906 let mut rows = Vec::with_capacity(pulls.len());
1907 for &(li, from, to) in &pulls {
1908 let (lnkv, lhd) = {
1909 let c = &self.kv_cache.layers[li];
1910 (c.num_kv_heads, c.head_dim)
1911 };
1912 let Some((k, v, first_valid)) =
1913 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1914 else {
1915 return false;
1916 };
1917 let need_from = match self.layer_window(li) {
1921 Some(w) => from.max((to + 1).saturating_sub(w)),
1922 None => from,
1923 };
1924 if first_valid > need_from {
1925 return false;
1926 }
1927 rows.push((k, v));
1928 }
1929 rows
1930 }
1931 };
1932 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1933 let cache = &mut self.kv_cache.layers[li];
1934 let row = cache.num_kv_heads * cache.head_dim;
1935 for p in 0..to - from {
1936 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1937 }
1938 if cache.pos_len() != to {
1939 return false;
1940 }
1941 }
1942 }
1943 }
1944 for (li, l) in layers.iter().enumerate() {
1948 if l.full
1949 && l.device_rows.is_some_and(|d| d > reuse_from)
1950 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1951 {
1952 return false;
1953 }
1954 }
1955 true
1956 }
1957
1958 fn finish_generation(
1964 &mut self,
1965 mtp: &mut Option<MtpModule>,
1966 router: &mut Option<crate::swarm::DynRouter>,
1967 clear_sequence: bool,
1968 ) {
1969 if router.is_some() {
1973 let _ = self.set_active_skill(None);
1974 }
1975 #[cfg(target_os = "macos")]
1982 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1983 if clear_sequence {
1984 self.clear_sequence_state();
1985 if let Some(m) = mtp.as_mut() {
1986 m.kv.clear();
1992 }
1993 if let Some(m) = self.mtp.as_mut() {
1994 m.kv.clear();
1998 }
1999 }
2000 self.graph_want_logits = false;
2001 self.graph_head_required = false;
2002 self.graph_logits = None;
2003 self.graph_failed
2004 .store(false, std::sync::atomic::Ordering::Relaxed);
2005 self.cancel
2006 .store(false, std::sync::atomic::Ordering::Relaxed);
2007 self.dyn_router = router.take().or(self.dyn_router.take());
2008 self.mtp = mtp.take().or(self.mtp.take());
2009 self.mtp_graph_mode = None;
2010 self.spec_forced = None;
2011 }
2012
2013 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2017 if self
2018 .graph_failed
2019 .swap(false, std::sync::atomic::Ordering::Relaxed)
2020 {
2021 self.cancel
2022 .store(false, std::sync::atomic::Ordering::Relaxed);
2023 self.clear_sequence_state();
2024 self.graph_logits = None;
2025 self.graph_want_logits = false;
2026 self.graph_head_required = false;
2027 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2028 }
2029 Ok(())
2030 }
2031
2032 #[cfg(target_os = "macos")]
2033 fn fail_metal_graph(&mut self, reason: &str) {
2034 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2035 self.clear_sequence_state();
2036 self.graph_logits = None;
2037 self.graph_failed
2038 .store(true, std::sync::atomic::Ordering::Relaxed);
2039 self.cancel
2040 .store(true, std::sync::atomic::Ordering::Relaxed);
2041 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2042 }
2043
2044 fn nll_begin(&mut self) -> Result<(), String> {
2049 if self
2050 .graph_failed
2051 .swap(false, std::sync::atomic::Ordering::Relaxed)
2052 {
2053 self.cancel
2054 .store(false, std::sync::atomic::Ordering::Relaxed);
2055 self.clear_sequence_state();
2056 self.graph_logits = None;
2057 self.graph_want_logits = false;
2058 self.graph_head_required = false;
2059 return Err("GPU graph failed before NLL scoring".to_string());
2060 }
2061 self.clear_sequence_state();
2062 self.graph_logits = None;
2063 self.graph_want_logits = false;
2064 self.graph_head_required = false;
2065 Ok(())
2066 }
2067
2068 fn nll_end(&mut self) {
2072 self.clear_sequence_state();
2073 self.graph_logits = None;
2074 self.graph_want_logits = false;
2075 self.graph_head_required = false;
2076 self.graph_failed
2077 .store(false, std::sync::atomic::Ordering::Relaxed);
2078 }
2079
2080 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2083 #[cfg(test)]
2084 if self.nll_test_fail_at == Some(pos) {
2085 self.nll_test_fail_at = None;
2086 self.graph_failed
2087 .store(true, std::sync::atomic::Ordering::Relaxed);
2088 self.cancel
2089 .store(true, std::sync::atomic::Ordering::Relaxed);
2090 }
2091 if self
2092 .graph_failed
2093 .swap(false, std::sync::atomic::Ordering::Relaxed)
2094 {
2095 self.cancel
2096 .store(false, std::sync::atomic::Ordering::Relaxed);
2097 self.clear_sequence_state();
2098 self.graph_logits = None;
2099 self.graph_want_logits = false;
2100 return Err(format!(
2101 "GPU graph failed during NLL {phase} at position {pos}"
2102 ));
2103 }
2104 Ok(())
2105 }
2106
2107 #[inline]
2111 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2112 virtual_idx % self.physical_layers
2113 }
2114
2115 #[inline]
2118 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2119 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2120 }
2121
2122 #[allow(clippy::too_many_arguments)]
2124
2125 #[cfg(target_os = "macos")]
2144 fn graph_prefill_preferred(&self) -> bool {
2145 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2146 if !crate::gpu::enabled_here()
2147 || !graph_force
2148 || std::env::var("CMF_GPU_BLOCK")
2149 .map(|v| v == "0")
2150 .unwrap_or(false)
2151 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2154 {
2155 return false;
2156 }
2157 self.weights
2158 .layers
2159 .iter()
2160 .any(|lw| {
2161 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2162 })
2163 }
2164
2165 #[cfg(not(target_os = "macos"))]
2172 fn batch_prefix_prefill(&self) -> bool {
2173 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2174 Ok("0") => return false,
2175 Ok("1") => true,
2176 _ => false,
2177 };
2178 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2179 && crate::gpu::enabled_here()
2180 && !self.graph_refused()
2181 && (forced || self.graph_attn_decline_reason().is_some())
2182 && self.wgpu_graph_attn_decline().is_none()
2183 && self.attn_softcap == 0.0
2184 && self
2185 .weights
2186 .layers
2187 .iter()
2188 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2189 && self.automatic_gpu_prefix().is_some()
2190 }
2191
2192 fn g4_batch_prefill(&self) -> bool {
2198 #[cfg(target_os = "macos")]
2199 {
2200 false
2201 }
2202 #[cfg(not(target_os = "macos"))]
2203 {
2204 !self.weights.layers.is_empty()
2208 && !self.per_token_stack()
2209 && (self.global_attn.is_some() || self.attn_v_norm)
2210 && crate::gpu::g4_batch_on()
2211 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2212 && crate::gpu::enabled_here()
2213 && crate::gpu::discrete()
2214 && !self.graph_refused()
2215 && !self.o1_active()
2216 && self.attn_softcap == 0.0
2217 && self.wgpu_graph_attn_decline().is_none()
2218 && self.weights.layers.iter().all(|lw| {
2219 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2220 })
2221 && self.automatic_gpu_prefix().is_none()
2222 }
2223 }
2224
2225 #[cfg(not(target_os = "macos"))]
2232 fn g4_batch_ingest(
2233 &mut self,
2234 ids: &[u32],
2235 pos0: usize,
2236 keep_all: bool,
2237 ) -> Result<(usize, Vec<f32>), String> {
2238 let hs = self.hidden_size;
2239 let chunk = self.generation_batch_k().max(1);
2240 let end_all = pos0 + ids.len();
2241 let mut pos = pos0;
2242 let mut out = Vec::new();
2243 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2244 let end = (pos + chunk).min(end_all);
2245 let bk = end - pos;
2246 let mut hiddens = vec![0f32; bk * hs];
2247 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2248 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2249 }
2250 let positions: Vec<usize> = (pos..end).collect();
2251 let t_chunk = std::time::Instant::now();
2252 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2253 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2254 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2255 eprintln!(
2256 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2257 bk as f64 / (ms / 1000.0)
2258 );
2259 }
2260 match outcome {
2261 crate::gpu::BatchGraphOutcome::Completed => {
2262 if keep_all {
2263 out.extend_from_slice(&hiddens);
2264 } else {
2265 out = hiddens[(bk - 1) * hs..].to_vec();
2266 }
2267 pos = end;
2268 }
2269 crate::gpu::BatchGraphOutcome::Failed => {
2270 return Err("Gemma-4 batched prefill failed after admission".into());
2271 }
2272 crate::gpu::BatchGraphOutcome::Declined => {
2273 #[cfg(feature = "gpu")]
2274 if pos > pos0 {
2275 self.pull_lagging_host_kv(0, self.num_layers, pos);
2276 }
2277 break;
2278 }
2279 }
2280 }
2281 Ok((pos - pos0, out))
2282 }
2283
2284 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2289 #[cfg(not(target_os = "macos"))]
2290 {
2291 if ids.is_empty() || !self.g4_batch_prefill() {
2292 return None;
2293 }
2294 self.clear_sequence_state();
2295 match self.g4_batch_ingest(ids, 0, true) {
2296 Ok((n, rows)) if n == ids.len() => Some(rows),
2297 _ => {
2298 self.clear_sequence_state();
2299 None
2300 }
2301 }
2302 }
2303 #[cfg(target_os = "macos")]
2304 {
2305 let _ = ids;
2306 None
2307 }
2308 }
2309
2310 #[cfg(not(target_os = "macos"))]
2311 fn graph_prefill_preferred(&self) -> bool {
2312 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2320 if !graph_on || !crate::gpu::enabled_here() {
2321 return false;
2322 }
2323 if self.embryo_resident_eligible() {
2327 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2332 }
2333 if self.o1_active() {
2347 return false;
2348 }
2349 if self.wgpu_graph_attn_decline().is_some() {
2353 return false;
2354 }
2355 if self
2356 .weights
2357 .layers
2358 .iter()
2359 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2360 {
2361 return true;
2362 }
2363 self.weights
2374 .layers
2375 .iter()
2376 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2377 && self.automatic_gpu_prefix().is_none()
2378 }
2379
2380 #[cfg(target_os = "macos")]
2381 fn q1_graph_gpu(
2382 &mut self,
2383 start: usize,
2384 upto: Option<usize>,
2385 position: usize,
2386 h: &mut [f32],
2387 ) -> usize {
2388 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2390 const MOE_WORKING_SET_REASON: &str =
2391 "model weights exceed Metal working set for native MoE graph";
2392 if self
2396 .graph_declines
2397 .borrow()
2398 .iter()
2399 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2400 {
2401 return start;
2402 }
2403 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2404 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2406 || !crate::gpu::enabled_here()
2407 || !graph_force
2408 || std::env::var("CMF_GPU_BLOCK")
2409 .map(|v| v == "0")
2410 .unwrap_or(false)
2411 {
2412 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2413 eprintln!(
2414 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2415 self.attn_softcap > 0.0,
2416 crate::gpu::enabled_here(),
2417 graph_force,
2418 );
2419 }
2420 if self.graph_head_required {
2421 self.fail_metal_graph("native graph front gate refused");
2422 }
2423 return start;
2424 }
2425 let gemma4 = self.metal_gemma4();
2435 let swa_graph = self.metal_graph_swa() || gemma4;
2436 let graph_attn_reason = self.graph_attn_decline_reason();
2443 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2444 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2445 if (self.swa.is_some() && !swa_graph)
2446 || (self.global_attn.is_some() && !gemma4)
2447 || self.attention_heads_per_layer.is_some()
2448 || (self.attn_v_norm && !gemma4)
2449 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2452 || self.weights.layers.iter().any(|lw| {
2453 ((lw.attn_out_norm.is_some()
2454 || lw.ffn_out_norm.is_some()
2455 || lw.layer_scale.is_some())
2456 && !gemma4)
2457 || matches!(&lw.ffn, FfnKind::Dense(d)
2458 if !(matches!(d.act, Act::Silu | Act::Gelu)
2459 || (gemma4 && d.act == Act::GeluTanh)))
2460 })
2461 {
2462 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2465 self.note_graph_decline("metal block graph", reason);
2466 }
2467 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2468 eprintln!(
2469 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2470 self.swa.is_some(),
2471 self.global_attn.is_some(),
2472 self.attention_heads_per_layer.is_some(),
2473 self.attn_v_norm,
2474 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2475 );
2476 }
2477 if self.graph_head_required {
2478 self.fail_metal_graph("native graph architecture gate refused");
2479 }
2480 return start;
2481 }
2482 let limit = upto
2485 .map(|u| u + 1)
2486 .unwrap_or(self.num_layers)
2487 .min(self.num_layers);
2488
2489 enum Item<'a> {
2490 Gdn {
2491 run: Vec<GdnGpuLayer<'a>>,
2492 first: usize,
2493 },
2494 Attn {
2495 l: AttnGpuLayer<'a>,
2496 li: usize,
2497 q_norm: Option<&'a [f32]>,
2498 k_norm: Option<&'a [f32]>,
2499 output_gate: bool,
2500 proj_gate: Option<(&'a QTensor, bool)>,
2504 head_gate_w: Option<&'a [f32]>,
2507 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2508 full_gpu: bool,
2511 },
2512 }
2513
2514 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2521 let attend_contract = attend_mode != "0"
2522 && attend_mode != "off"
2523 && self.head_dim % 4 == 0
2524 && self.head_dim <= 256
2525 && (self.rotary_dim == 0
2529 || (self.rotary_dim >= 2
2530 && self.rotary_dim <= self.head_dim
2531 && (self.rotary_dim / 2) % 32 == 0))
2532 && self.num_kv_heads > 0
2533 && self.num_heads % self.num_kv_heads == 0;
2534
2535 let mut plan: Vec<Item> = Vec::new();
2536 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2537 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2539 let mut scan = start;
2540 while scan < limit {
2541 let lw = &self.weights.layers[self.phys_layer(scan)];
2542 let ffn = match &lw.ffn {
2543 FfnKind::Dense(d) if d.segs.is_empty() => {
2544 let (Some(g), Some(u), Some(dn)) = (
2545 d.gate_proj.metal_graph_parts(),
2546 d.up_proj.metal_graph_parts(),
2547 d.down_proj.metal_graph_parts(),
2548 ) else {
2549 if block_diag {
2550 eprintln!(
2551 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2552 );
2553 }
2554 break;
2555 };
2556 MetalFfn::Dense {
2557 gate: g,
2558 up: u,
2559 down: dn,
2560 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2563 }
2564 }
2565 FfnKind::Moe(m) => {
2566 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2572 {
2573 MetalFfn::FlintMoe(flint)
2574 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2575 MetalFfn::Moe(moe)
2576 } else {
2577 if block_diag {
2578 eprintln!(
2579 "block-graph: L{scan} MoE outside the graph contract — run ends"
2580 );
2581 }
2582 break;
2583 };
2584 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2585 model_ref.get_or_insert_with(|| model.clone());
2586 }
2587 ffn
2588 }
2589 _ => {
2590 if block_diag {
2591 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2592 }
2593 break;
2594 }
2595 };
2596 match &lw.attn {
2597 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2598 let parts = (
2599 w.in_proj_qkv.metal_graph_parts(),
2600 w.in_proj_z.metal_graph_parts(),
2601 w.in_proj_a.f32_parts(),
2602 w.in_proj_b.f32_parts(),
2603 w.out_proj.metal_graph_parts(),
2604 );
2605 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2606 if block_diag {
2607 eprintln!(
2608 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2609 w.in_proj_qkv.metal_graph_parts().is_some(),
2610 w.in_proj_z.metal_graph_parts().is_some(),
2611 w.in_proj_a.f32_parts().is_some(),
2612 w.in_proj_b.f32_parts().is_some(),
2613 w.out_proj.metal_graph_parts().is_some(),
2614 );
2615 }
2616 break;
2617 };
2618 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2619 model_ref.get_or_insert_with(|| model.clone());
2620 }
2621 let gl = GdnGpuLayer {
2622 attn_norm: &lw.input_norm,
2623 post_norm: &lw.post_norm,
2624 qkv,
2625 z,
2626 a,
2627 b,
2628 out,
2629 ffn,
2630 conv1d: &w.conv1d,
2631 a_log: &w.a_log,
2632 dt_bias: &w.dt_bias,
2633 gnorm: &w.norm,
2634 };
2635 match plan.last_mut() {
2636 Some(Item::Gdn { run, .. }) => run.push(gl),
2637 _ => plan.push(Item::Gdn {
2638 run: vec![gl],
2639 first: scan,
2640 }),
2641 }
2642 }
2643 AttnKind::Full {
2644 wq,
2645 wk,
2646 wv,
2647 wo,
2648 q_norm,
2649 k_norm,
2650 output_gate,
2651 softplus_gate,
2652 bias,
2653 } if (!self.kv_cache.layers[scan].o1_sealed()
2654 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2659 && softplus_gate
2663 .as_ref()
2664 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2665 {
2666 let parts = (
2667 wq.metal_graph_parts(),
2668 wk.metal_graph_parts(),
2669 wv.metal_graph_parts(),
2670 wo.metal_graph_parts(),
2671 );
2672 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2673 break;
2674 };
2675 if let QTensor::Mapped { model, .. } = wq {
2676 model_ref.get_or_insert_with(|| model.clone());
2677 }
2678 let cache = &self.kv_cache.layers[scan];
2679 let o1_metal = cache.o1.is_some()
2683 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2684 && cache.o1_views().is_some();
2685 let head_gate_w = softplus_gate
2689 .as_ref()
2690 .and_then(|(g, _)| g.f32_parts())
2691 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2692 .map(|(d, _, _)| d);
2693 let (nkv_g, hd_g) = if gemma4 {
2696 let (a, b, _) = self.layer_geom(scan);
2697 (a, b)
2698 } else {
2699 (self.num_kv_heads, self.head_dim)
2700 };
2701 let full_gpu = attend_contract
2702 && softplus_gate.is_none() == head_gate_w.is_none()
2703 && cache.mode == crate::kv_cache::KvMode::F32
2704 && (cache.o1.is_none() || o1_metal)
2705 && bias.is_none()
2706 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2707 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2708 && pk.1 == nkv_g * hd_g
2709 && pv.1 == nkv_g * hd_g
2710 && po.2 == self.num_heads * hd_g;
2711 plan.push(Item::Attn {
2712 l: AttnGpuLayer {
2713 attn_norm: &lw.input_norm,
2714 post_norm: &lw.post_norm,
2715 wq: pq,
2716 wk: pk,
2717 wv: pv,
2718 wo: po,
2719 ffn,
2720 attn_out_norm: lw.attn_out_norm.as_deref(),
2721 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2722 layer_scale: lw.layer_scale,
2723 },
2724 li: scan,
2725 q_norm: q_norm.as_deref(),
2726 k_norm: k_norm.as_deref(),
2727 output_gate: *output_gate,
2728 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2729 head_gate_w,
2730 bias: bias
2731 .as_ref()
2732 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2733 full_gpu,
2734 });
2735 }
2736 _ => break,
2737 }
2738 scan += 1;
2739 }
2740 let Some(model) = model_ref else {
2741 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2742 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2743 }
2744 if self.graph_head_required {
2745 self.fail_metal_graph("native graph has no mapped model reference");
2746 }
2747 return start;
2748 };
2749 if plan.is_empty() {
2750 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2751 eprintln!("q1-graph: empty plan at layer {start}");
2752 }
2753 if self.graph_head_required {
2754 self.fail_metal_graph("native graph plan is empty");
2755 }
2756 return start;
2757 }
2758 let has_moe = plan.iter().any(|it| match it {
2759 Item::Gdn { run, .. } => run
2760 .iter()
2761 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2762 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2763 });
2764 let has_flint_moe = plan.iter().any(|it| match it {
2765 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2766 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2767 });
2768 let metal_working_set = crate::gpu_metal::working_set_bytes();
2769 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2770 let model_bytes = model.primary_bytes().len() as u64;
2771 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2777 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2778 let pager_cap = if !has_moe {
2787 None
2788 } else if has_flint_moe {
2789 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2793 } else {
2794 crate::gpu_metal::moe_pager_cap_bytes()
2795 };
2796 let pager_eligible =
2797 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2798 let mut pager_span: Option<(usize, usize)> = None;
2799 if pager_eligible {
2800 let cap = pager_cap.expect("checked above");
2801 let tail_span = (self.graph_want_logits
2805 && upto.is_none()
2806 && scan == self.num_layers
2807 && std::env::var("CMF_GPU_LMHEAD")
2808 .map(|v| v != "0")
2809 .unwrap_or(true))
2810 .then(|| self.weights.lm_head.metal_graph_parts())
2811 .flatten()
2812 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2813 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2814 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2815 Some(match span {
2816 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2817 None => (lo, hi),
2818 })
2819 };
2820 let page_span = |span: (usize, usize), include_tail: bool| {
2821 let (mut lo, mut hi) = span;
2822 if include_tail {
2823 if let Some((tail_lo, tail_hi)) = tail_span {
2824 lo = lo.min(tail_lo);
2825 hi = hi.max(tail_hi);
2826 }
2827 }
2828 crate::gpu_metal::page_round_span(lo, hi)
2829 };
2830 let mut selected = 0usize;
2831 let mut selected_span: Option<(usize, usize)> = None;
2832 let mut gdn_prefix: Option<(usize, usize)> = None;
2840 'items: for (i, item) in plan.iter().enumerate() {
2841 match item {
2842 Item::Attn { li, .. } => {
2843 let Some(span) = add_layer_span(selected_span, *li) else {
2844 break;
2845 };
2846 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2847 break;
2848 };
2849 if page_hi - page_lo > cap {
2850 break;
2851 }
2852 selected = i + 1;
2853 selected_span = Some((page_lo, page_hi));
2854 }
2855 Item::Gdn { first, run } => {
2856 let mut whole = None;
2863 for take in 1..=run.len() {
2864 whole = add_layer_span(whole, *first + take - 1);
2865 if whole.is_none() {
2866 break;
2867 }
2868 }
2869 let whole_fits_empty_page = whole
2870 .and_then(|span| page_span(span, false))
2871 .is_some_and(|(lo, hi)| hi - lo <= cap);
2872 if whole_fits_empty_page {
2873 let mut span = selected_span;
2874 for take in 1..=run.len() {
2875 let Some(next) = add_layer_span(span, *first + take - 1) else {
2876 break 'items;
2877 };
2878 span = Some(next);
2879 }
2880 let Some(span) = span else {
2881 break;
2882 };
2883 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2884 else {
2885 break;
2886 };
2887 if page_hi - page_lo > cap {
2888 break;
2889 }
2890 selected = i + 1;
2891 selected_span = Some((page_lo, page_hi));
2892 continue;
2893 }
2894 if selected != 0 {
2895 break;
2896 }
2897 let mut span = None;
2898 let mut best: Option<(usize, (usize, usize))> = None;
2899 for take in 1..=run.len() {
2900 let Some(next) = add_layer_span(span, *first + take - 1) else {
2901 break;
2902 };
2903 let Some((page_lo, page_hi)) = page_span(next, false) else {
2906 break;
2907 };
2908 if page_hi - page_lo > cap {
2909 break;
2910 }
2911 span = Some((page_lo, page_hi));
2912 best = Some((take, (page_lo, page_hi)));
2913 }
2914 let Some((take, page)) = best else {
2915 break;
2916 };
2917 selected = i + 1;
2918 selected_span = Some(page);
2919 if take != run.len() {
2920 gdn_prefix = Some((i, take));
2921 break 'items;
2922 }
2923 }
2924 }
2925 }
2926 if selected != 0 {
2927 if let Some((item, take)) = gdn_prefix {
2928 match &mut plan[item] {
2929 Item::Gdn { run, .. } => run.truncate(take),
2930 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2931 }
2932 }
2933 let (page_lo, page_hi) = selected_span.expect("selected page span");
2934 tracing::info!(
2935 page_start = page_lo,
2936 page_end = page_hi,
2937 page_mb = (page_hi - page_lo) / (1024 * 1024),
2938 cap_mb = cap / (1024 * 1024),
2939 graph_items = selected,
2940 "native Metal MoE graph uses transient bounded weight page"
2941 );
2942 plan.truncate(selected);
2943 pager_span = Some((page_lo, page_hi));
2944 } else {
2945 tracing::warn!(
2946 cap_mb = cap / (1024 * 1024),
2947 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2948 );
2949 }
2950 }
2951 if has_moe && needs_bounded_pager && pager_span.is_none() {
2960 tracing::info!(
2961 model_bytes,
2962 metal_working_set,
2963 "native Metal MoE graph declined before allocation"
2964 );
2965 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2966 if self.graph_head_required {
2967 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2968 }
2969 return start;
2970 }
2971 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2972 let dev_attend = attend_contract
2973 && (self.head_dim <= 128
2974 || has_moe
2975 || (self.head_dim <= 256 && has_gdn)
2981 || (self.head_dim <= 256 && swa_graph)
2985 || attend_mode == "force"
2986 || attend_mode == "256");
2987 if !dev_attend {
2988 for it in &mut plan {
2989 if let Item::Attn { li, full_gpu, .. } = it {
2990 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2993 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2994 if !keep_o1 {
2995 *full_gpu = false;
2996 }
2997 }
2998 }
2999 }
3000 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3001 use std::sync::atomic::{AtomicBool, Ordering};
3002 static SAID: AtomicBool = AtomicBool::new(false);
3003 if !SAID.swap(true, Ordering::Relaxed) {
3004 let fg = plan
3005 .iter()
3006 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
3007 .count();
3008 let att = plan
3009 .iter()
3010 .filter(|it| matches!(it, Item::Attn { .. }))
3011 .count();
3012 eprintln!(
3013 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
3014 plan.len(),
3015 self.head_dim,
3016 self.rotary_dim,
3017 self.num_kv_heads,
3018 self.num_heads,
3019 );
3020 }
3021 }
3022 let dims = GraphDims {
3023 hidden: self.hidden_size,
3024 eps: self.rms_eps as f32,
3025 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3026 };
3027 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
3031 crate::gpu_metal::MoePagerPageScope::new(
3032 _mt0,
3033 position,
3034 start,
3035 page_lo,
3036 page_hi,
3037 pager_cap.unwrap_or(0),
3041 )
3042 });
3043 let graph_new = match pager_span {
3044 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3045 None => TokenGraph::new(&model, dims, h),
3046 };
3047 let Some(mut graph) = graph_new else {
3048 if self.graph_head_required {
3049 self.fail_metal_graph("native TokenGraph allocation refused");
3050 }
3051 return start;
3052 };
3053 if swa_graph && self.head_dim > 128 {
3054 graph.set_attend_blk(1, 32);
3064 }
3065 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3066 nv: cfg.num_v_heads,
3067 nk: cfg.num_k_heads,
3068 dk: cfg.key_head_dim,
3069 dv: cfg.value_head_dim,
3070 kk: cfg.conv_kernel,
3071 hidden: self.hidden_size,
3072 inter: self.intermediate_size,
3073 c_dim: cfg.conv_dim(),
3074 eps: cfg.rms_eps as f32,
3075 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3076 });
3077 let mut valid = 0usize;
3081 let mut end = start;
3082 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3084 static ONCE: std::sync::Once = std::sync::Once::new();
3085 ONCE.call_once(|| {
3086 for it in &plan {
3087 match it {
3088 Item::Gdn { first, run } => {
3089 eprintln!("plan: Gdn first={first} len={}", run.len())
3090 }
3091 Item::Attn { li, full_gpu, .. } => {
3092 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3093 }
3094 }
3095 }
3096 });
3097 }
3098 for item in &plan {
3099 let ok = match item {
3100 Item::Gdn { run, .. } => gcfg
3101 .as_ref()
3102 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3103 .unwrap_or(false),
3104 Item::Attn { l, .. } => graph.attn_ok(l),
3105 };
3106 if !ok {
3107 if block_diag {
3108 eprintln!(
3109 "block-graph: plan item {} ({}) failed graph preflight",
3110 valid,
3111 match item {
3112 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3113 Item::Attn { li, .. } => format!("Attn L{li}"),
3114 }
3115 );
3116 }
3117 break;
3118 }
3119 valid += 1;
3120 end += match item {
3121 Item::Gdn { run, .. } => run.len(),
3122 Item::Attn { .. } => 1,
3123 };
3124 }
3125 plan.truncate(valid);
3126 if plan.is_empty() {
3127 if self.graph_head_required {
3128 self.fail_metal_graph("native graph preflight produced no valid items");
3129 }
3130 return start;
3131 }
3132
3133 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3134 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3135 return start;
3136 }
3137
3138 let one_pass = |t: (usize, usize, usize)| {
3147 use cortiq_core::TensorDtype as D;
3148 matches!(
3149 model.tensors[t.0].dtype,
3150 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3151 )
3152 };
3153 let dense_fast = plan.iter().all(|it| match it {
3154 Item::Attn {
3155 l, li, full_gpu, ..
3156 } => {
3157 *full_gpu
3158 && self.kv_cache.layers[*li].o1.is_none()
3159 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3160 && match l.ffn {
3161 MetalFfn::Dense { gate, up, down, .. } => {
3162 one_pass(gate) && one_pass(up) && one_pass(down)
3163 }
3164 _ => false,
3165 }
3166 }
3167 Item::Gdn { .. } => false,
3168 });
3169 let moe_plan = !dense_fast
3173 && plan.iter().all(|it| match it {
3174 Item::Attn {
3175 l, li, full_gpu, ..
3176 } => {
3177 *full_gpu
3178 && self.kv_cache.layers[*li].o1.is_none()
3179 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3180 }
3181 Item::Gdn { .. } => false,
3182 });
3183 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3184 if block_diag {
3185 use std::sync::atomic::{AtomicBool, Ordering};
3186 static SAID_AB: AtomicBool = AtomicBool::new(false);
3187 if !SAID_AB.swap(true, Ordering::Relaxed) {
3188 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3189 }
3190 }
3191 let _mv_fast = match ab {
3192 Some((bits, _)) => {
3193 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3194 crate::gpu_metal::MvFastGuard::set_raw(bits)
3195 }
3196 None if moe_plan => {
3197 graph.set_dense_concurrent(
3198 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3199 );
3200 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3201 }
3202 None => {
3203 graph.set_dense_concurrent(dense_fast);
3204 let q8r4 = if swa_graph {
3207 crate::gpu_metal::DENSE_Q8R4
3208 } else {
3209 0
3210 };
3211 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3212 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3213 } else {
3214 0
3215 })
3216 }
3217 };
3218
3219 let pool = self.pool.clone();
3222 let (nh, nkv, hd, hs, eps) = (
3223 self.num_heads,
3224 self.num_kv_heads,
3225 self.head_dim,
3226 self.hidden_size,
3227 self.rms_eps,
3228 );
3229 let norm_style = self.norm_style;
3230 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3231 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3232 let kv_id = self.graph_kv_id;
3233 let mut pending: Vec<(usize, usize)> = Vec::new();
3236 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3239 for item in &plan {
3240 let _xt0 = std::time::Instant::now();
3241 let _xkind: u32 = match item {
3242 Item::Gdn { .. } => 2,
3243 Item::Attn { .. } => 3,
3244 };
3245 if self.loop_final_norm {
3247 let item_start = match item {
3248 Item::Gdn { first, .. } => *first,
3249 Item::Attn { li, .. } => *li,
3250 };
3251 if item_start > start && self.is_loop_end(item_start - 1) {
3252 graph.encode_loop_norm(&self.weights.final_norm);
3253 }
3254 }
3255 match item {
3256 Item::Gdn { run, first } => {
3257 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3258 if l.linear_state.len() != want {
3259 l.linear_state = vec![0f32; want];
3260 }
3261 }
3262 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3263 .iter()
3264 .map(|l| l.linear_state.as_slice())
3265 .collect();
3266 let _ig = std::time::Instant::now();
3267 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3268 tracing::error!("q1 graph: GDN run refused after validation");
3270 return start;
3271 }
3272 graph.commit_kind = 2;
3275 graph.commit();
3276 crate::gpu::stageprof(0, _ig.elapsed());
3277 pending.push((*first, run.len()));
3278 }
3279 Item::Attn {
3280 l,
3281 li,
3282 q_norm,
3283 k_norm,
3284 output_gate,
3285 proj_gate,
3286 head_gate_w,
3287 bias,
3288 full_gpu,
3289 } => {
3290 let _ia = std::time::Instant::now();
3291 let inv_freq_l = self.layer_inv_freq(*li);
3297 let rd_l = self.layer_geom(*li).2;
3298 let window_l = self.layer_window(*li);
3299 let head_gate_w = *head_gate_w;
3300 let (nkv, hd) = if gemma4 {
3303 let (a, b, _) = self.layer_geom(*li);
3304 (a, b)
3305 } else {
3306 (nkv, hd)
3307 };
3308 if *full_gpu {
3310 let cache = &self.kv_cache.layers[*li];
3311 let o1p = if cache.o1.is_some() {
3312 match cache.o1_views() {
3313 Some(views) => Some(crate::gpu::O1AttnParams {
3314 views,
3315 epoch: self.o1_epoch,
3316 }),
3317 None => None,
3319 }
3320 } else {
3321 None
3322 };
3323 let o1_layer = cache.o1.is_some();
3324 if o1_layer && o1p.is_none() {
3325 }
3327 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3328 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3329 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3330 debug_assert!(
3333 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3334 );
3335 let p = crate::gpu::AttnDeviceParams {
3336 kv_id,
3337 layer: *li,
3338 nh,
3339 nkv,
3340 hd,
3341 rd: rd_l,
3342 position,
3343 scale: self.attn_scale,
3344 eps: eps as f32,
3345 gemma,
3346 late_qk_norm: self.qk_norm_after_rope,
3347 output_gate: *output_gate,
3348 q_norm: *q_norm,
3349 k_norm: *k_norm,
3350 inv_freq: &inv_freq_l,
3351 rope_scale: self.layer_rope_scale(*li),
3352 cpu_k,
3353 cpu_v,
3354 cpu_stored,
3355 cpu_gen: cache.generation(),
3356 o1: o1p,
3357 window: window_l,
3358 softmax_sink: cache.sinks.as_deref(),
3359 head_gate: head_gate_w,
3360 v_norm: self.attn_v_norm,
3361 };
3362 let o1_bad = o1_layer && p.o1.is_none();
3363 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3364 {
3365 if p.o1.is_none() {
3367 dev_attn.push((*li, nkv, hd));
3368 }
3369 graph.commit_kind = 3;
3370 graph.commit();
3371 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3375 continue;
3376 }
3377 }
3379 graph.encode_attn_prefix(l);
3380 if let Err(err) = graph.sync_checked() {
3381 self.fail_metal_graph(&err);
3382 return start;
3383 }
3384 if !pending.is_empty() {
3385 let idxs: Vec<usize> =
3386 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3387 let mut outs: Vec<&mut [f32]> = self
3388 .kv_cache
3389 .layers
3390 .iter_mut()
3391 .enumerate()
3392 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3393 .map(|(_, s)| s.linear_state.as_mut_slice())
3394 .collect();
3395 graph.read_states(&mut outs);
3396 }
3397 let mut q_raw = attention::take_buf(l.wq.1);
3398 let mut k = attention::take_buf(l.wk.1);
3399 let mut v = attention::take_buf(l.wv.1);
3400 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3401 let mut gate_raw = proj_gate.map(|(gp, _)| {
3404 let mut normed = attention::take_buf(hs);
3405 graph.read_normed(&mut normed);
3406 let mut raw = attention::take_buf(gp.rows());
3407 gp.matvec(&normed, &mut raw, pool.as_deref());
3408 attention::recycle_buf(&mut normed);
3409 raw
3410 });
3411 let cfg = QwenAttnCfg {
3412 num_heads: nh,
3413 num_kv_heads: nkv,
3414 head_dim: hd,
3415 hidden_size: hs,
3416 position,
3417 inv_freq: &inv_freq_l,
3418 rotary_dim: rd_l,
3419 scale: self.attn_scale,
3420 softcap: self.attn_softcap,
3421 window: window_l,
3422 v_norm: self.attn_v_norm,
3425 qk_norm_after_rope: self.qk_norm_after_rope,
3426 gate_sigmoid: self.proj_gate_sigmoid,
3427 q_norm: *q_norm,
3428 k_norm: *k_norm,
3429 output_gate: *output_gate,
3430 softplus_gate: None,
3431 rope_scale: self.layer_rope_scale(*li),
3432 bias: *bias,
3433 rms_eps: eps,
3434 norm_style,
3435 pool: pool.as_deref(),
3436 v_head_dim: hd,
3437 };
3438 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3441 || std::env::var("CMF_ATTN_DUMP").is_ok();
3442 let _ = full_gpu;
3443 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3444 let mut ao = attention::qwen_attention_core(
3445 q_raw,
3446 k,
3447 v,
3448 &mut self.kv_cache.layers[*li],
3449 &cfg,
3450 );
3451 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3455 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3456 let (cq, _cg, _ck, _cv) =
3457 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3458 let cache = &self.kv_cache.layers[*li];
3459 let n = cache.head_keys(0).len() / hd;
3460 let mut bytes: Vec<u8> = Vec::new();
3461 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3462 bytes.extend_from_slice(&v.to_le_bytes());
3463 }
3464 for v in &cq {
3465 bytes.extend_from_slice(&v.to_le_bytes());
3466 }
3467 for g in 0..nkv {
3468 for v in cache.head_keys(g) {
3469 bytes.extend_from_slice(&v.to_le_bytes());
3470 }
3471 }
3472 for g in 0..nkv {
3473 for v in cache.head_values(g) {
3474 bytes.extend_from_slice(&v.to_le_bytes());
3475 }
3476 }
3477 let _ =
3478 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3479 }
3480 }
3481 if let Some((qr0, k0, v0)) =
3482 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3483 {
3484 let (cq, _cg, ck, cv) =
3485 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3486 let mut h_now = vec![0f32; hs];
3487 graph.read_h(&mut h_now);
3488 let cache = &self.kv_cache.layers[*li];
3489 let n_after = cache.head_keys(0).len() / hd;
3490 let stored = n_after.saturating_sub(1);
3494 let cpu_k: Vec<&[f32]> = (0..nkv)
3495 .map(|g| &cache.head_keys(g)[..stored * hd])
3496 .collect();
3497 let cpu_v: Vec<&[f32]> = (0..nkv)
3498 .map(|g| &cache.head_values(g)[..stored * hd])
3499 .collect();
3500 let p = crate::gpu::AttnDeviceParams {
3501 kv_id,
3502 layer: *li,
3503 nh,
3504 nkv,
3505 hd,
3506 rd: rd_l,
3510 position,
3511 scale: self.attn_scale,
3512 eps: eps as f32,
3513 gemma,
3514 late_qk_norm: self.qk_norm_after_rope,
3515 output_gate: *output_gate,
3516 q_norm: *q_norm,
3517 k_norm: *k_norm,
3518 inv_freq: &inv_freq_l,
3519 rope_scale: self.layer_rope_scale(*li),
3520 cpu_k,
3521 cpu_v,
3522 cpu_stored: stored,
3523 cpu_gen: cache.generation(),
3524 o1: None,
3525 window: window_l,
3526 softmax_sink: cache.sinks.as_deref(),
3527 head_gate: None,
3528 v_norm: self.attn_v_norm,
3529 };
3530 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3531 let md = |a: &[f32], b: &[f32]| {
3532 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3533 };
3534 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3535 eprintln!(
3536 "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}",
3537 nn(&cq),
3538 md(&cq, &dq),
3539 nn(&ck),
3540 md(&ck, &dk),
3541 nn(&cv),
3542 md(&cv, &dv),
3543 nn(&ao),
3544 md(&ao, &dao)
3545 );
3546 } else {
3547 eprintln!("attn-oracle L{li}: device probe declined");
3548 }
3549 }
3550 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3551 attention::apply_projected_gate(
3554 &mut ao,
3555 raw,
3556 per_head,
3557 hd,
3558 self.proj_gate_sigmoid,
3559 );
3560 }
3561 if let Some(mut raw) = gate_raw.take() {
3562 attention::recycle_buf(&mut raw);
3563 }
3564 graph.encode_attn_suffix(l, &ao);
3565 graph.commit();
3568 attention::recycle_buf(&mut ao);
3569 }
3570 }
3571
3572 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3573 }
3574 let mut lm_rows = None;
3579 if self.graph_want_logits
3580 && upto.is_none()
3581 && end == self.num_layers
3582 && std::env::var("CMF_GPU_LMHEAD")
3583 .map(|v| v != "0")
3584 .unwrap_or(true)
3585 {
3586 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3587 if graph.lm_head_ok(lm) {
3588 graph.encode_lm_head(&self.weights.final_norm, lm);
3589 lm_rows = Some(lm.1);
3590 }
3591 }
3592 }
3593 if self.graph_head_required && lm_rows.is_none() {
3594 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3595 self.fail_metal_graph("fused graph head was requested but not encodable");
3596 return start;
3597 }
3598 let _sy0 = std::time::Instant::now();
3599 if let Err(err) = graph.sync_checked() {
3600 if let Some(profile) = pager_profile_page.as_mut() {
3601 profile.finish(start, graph.pager_page_stats(), "sync-error");
3602 }
3603 self.fail_metal_graph(&err);
3604 return start;
3605 }
3606 let _rs0 = std::time::Instant::now();
3607 if !pending.is_empty() {
3608 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3609 let mut outs: Vec<&mut [f32]> = self
3610 .kv_cache
3611 .layers
3612 .iter_mut()
3613 .enumerate()
3614 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3615 .map(|(_, s)| s.linear_state.as_mut_slice())
3616 .collect();
3617 graph.read_states(&mut outs);
3618 }
3619 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3620 use std::sync::atomic::{AtomicU64, Ordering};
3621 static SY: AtomicU64 = AtomicU64::new(0);
3622 static RS: AtomicU64 = AtomicU64::new(0);
3623 static N: AtomicU64 = AtomicU64::new(0);
3624 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3625 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3626 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3627 if n % 100 == 0 {
3628 eprintln!(
3629 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3630 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3631 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3632 );
3633 }
3634 }
3635 if let Some(rows) = lm_rows {
3636 crate::gpu::hostprof_encode_done(_mt0);
3637 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3638 graph.read_logits(&mut lg);
3639 crate::gpu::hostprof_total(_mt0);
3640 lg.resize(self.vocab_size, 0.0);
3641 if let Some(c) = self.final_softcap {
3642 softcap_in_place(&mut lg, c, self.pool.as_deref());
3643 }
3644 self.graph_logits = Some(lg);
3645 }
3646 graph.read_h(h);
3647 if self.graph_head_required && self.graph_logits.is_none() {
3648 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3649 self.fail_metal_graph("fused graph head completed without logits readback");
3650 return start;
3651 }
3652 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3653 METAL_GRAPH_LAYERS.fetch_add(
3654 end.saturating_sub(start) as u64,
3655 std::sync::atomic::Ordering::Relaxed,
3656 );
3657 if self.graph_head_required {
3658 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3659 }
3660 for (li, nkv, hd) in dev_attn {
3664 let mut krow = attention::take_buf(nkv * hd);
3665 let mut vrow = attention::take_buf(nkv * hd);
3666 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3667 let cache = &mut self.kv_cache.layers[li];
3668 cache.append(&krow, &vrow, &[]);
3669 let n = cache.seq_len;
3670 let mut imp = attention::take_buf(n);
3671 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3672 cache.accumulate_imp(&imp);
3673 attention::recycle_buf(&mut imp);
3674 }
3675 attention::recycle_buf(&mut krow);
3676 attention::recycle_buf(&mut vrow);
3677 }
3678 if let Some(profile) = pager_profile_page.as_mut() {
3679 profile.finish(end, graph.pager_page_stats(), "page-complete");
3680 }
3681 if let Some((_, arm)) = ab {
3682 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3683 }
3684 end
3685 }
3686
3687 pub fn new(
3688 tokenizer: Tokenizer,
3689 weights: PipelineWeights,
3690 hidden_size: usize,
3691 intermediate_size: usize,
3692 num_heads: usize,
3693 num_kv_heads: usize,
3694 head_dim: usize,
3695 num_layers: usize,
3696 physical_layers: usize,
3697 loop_final_norm: bool,
3698 vocab_size: usize,
3699 rms_eps: f64,
3700 rope_base: f32,
3701 norm_style: NormStyle,
3702 max_seq_len: usize,
3703 sampler_config: SamplerConfig,
3704 ) -> Self {
3705 let rng = match sampler_config.seed {
3706 Some(s) => SplitMix64::new(s),
3707 None => SplitMix64::from_entropy(),
3708 };
3709 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3710 let pool = Pool::from_env();
3711 if let Some(p) = &pool {
3712 tracing::info!("worker pool: {} threads", p.n_workers());
3713 if let Some(model) = weights
3715 .lm_head
3716 .model_arc()
3717 .or_else(|| weights.embed_tokens.model_arc())
3718 {
3719 let regions: Vec<&[u8]> =
3720 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3721 p.bind_numa(®ions);
3722 }
3723 }
3724 Self {
3725 gpu_plan: None,
3726 tokenizer: std::sync::Arc::new(tokenizer),
3727 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3728 sampler_config,
3729 weights,
3730 hidden_size,
3731 intermediate_size,
3732 num_heads,
3733 num_kv_heads,
3734 head_dim,
3735 num_layers,
3736 physical_layers,
3737 loop_final_norm,
3738 vocab_size,
3739 rms_eps,
3740 rope_base,
3741 norm_style,
3742 rotary_dim: head_dim,
3743 attention_heads_per_layer: None,
3744 kv_heads_per_layer: None,
3745 v_head_dim: None,
3746 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3747 .filter(|v| !v.is_empty())
3748 .map(std::path::PathBuf::from),
3749 graph_declines: std::cell::RefCell::new(Vec::new()),
3750 mimo_moe: Default::default(),
3751 minicpm_tail: Default::default(),
3752 vmf_cfg: None,
3753 gdn_cfg: None,
3754 kda_cfg: None,
3755 g3n: None,
3756 g4e: None,
3757 dsv4: None,
3758 dsv41: None,
3759 dsv41_vision: None,
3760 dsv41_prefill: None,
3761 qwen4_exp: None,
3762 clover: None,
3763 clover_stats: clover::CloverStats::default(),
3764 dsv4_mtp: Vec::new(),
3765 dspark: None,
3766 dspark_pending: Vec::new(),
3767 dspark_hist: Vec::new(),
3768 dspark_real: Vec::new(),
3769 dspark_trunk_picks: Vec::new(),
3770 dspark_exp: Vec::new(),
3771 dspark_draft_ns: 0,
3772 m3_winners: Vec::new(),
3773 m3_counts: Vec::new(),
3774 m3_n_exp: 0,
3775 m3_tokens: 0,
3776 m3_prewarmed: false,
3777 logit_multiplier: None,
3778 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3779 graph_failed: std::sync::atomic::AtomicBool::new(false),
3780 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
3781 kv_history: Vec::new(),
3782 kv_history_device: false,
3783 short_conv_cfg: None,
3784 mtp: None,
3785 mimo_mtp: None,
3786 verify_exact_moe: false,
3787 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3788 ignore_eos: false,
3789 draft_full_streak: 0,
3790 spec_k_adapt: None,
3791 spec_acc_ewma: 0.7,
3792 rng,
3793 sampler_scratch: SamplerScratch::default(),
3794 spec_forced: None,
3795 spec_q: Vec::new(),
3796 spec_p: Vec::new(),
3797 spec_res: Vec::new(),
3798 spec_qs: Vec::new(),
3799 spec_ps: Vec::new(),
3800 spec_ress: Vec::new(),
3801 mtp_graph_mode: None,
3802 #[cfg(target_os = "macos")]
3803 metal_verify: None,
3804 inv_freq,
3805 ws: ForwardScratch::new(hidden_size),
3806 pool,
3807 model: None,
3808 dyn_force_f32: false,
3809 dyn_skill_layers: Vec::new(),
3810 dyn_active: None,
3811 dyn_blend_loaded: false,
3812 dyn_phi_layer: None,
3813 dyn_phi_ema: Vec::new(),
3814 dyn_phi_seen: 0,
3815 dyn_router: None,
3816 o1_cfg: None,
3817 o1_epoch: 0,
3818 o1_flags: Vec::new(),
3819 trace: false,
3820 calib_temp: 1.0,
3821 confidence_on: true,
3822 embed_multiplier: 1.0,
3823 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3824 swa: None,
3825 swa_trim: None,
3826 swa_trim_anchor: usize::MAX,
3827 chunk_min_rows: 32,
3828 sliding_layers: None,
3829 no_rope_layers: None,
3830 anchor_core: None,
3831 bounded_rope: None,
3832 kv_prefix: KvPrefix::default(),
3833 last_prefill_tokens: 0,
3834 inv_freq_local: None,
3835 rotary_dim_local: None,
3836 rope_scale: 1.0,
3837 rope_scale_local: 1.0,
3838 global_attn: None,
3839 inv_freq_global: None,
3840 attn_v_norm: false,
3841 qk_norm_after_rope: false,
3842 proj_gate_sigmoid: false,
3843 final_softcap: None,
3844 head_clusters: None,
3845 attn_softcap: 0.0,
3846 graph_want_logits: false,
3847 graph_head_required: false,
3848 graph_logits: None,
3849 embryo_graph: None,
3850 graph_refused: std::sync::atomic::AtomicBool::new(false),
3851 graph_kv_id: {
3852 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3853 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3854 },
3855 #[cfg(test)]
3856 nll_test_fail_at: None,
3857 #[cfg(test)]
3858 nll_test_force_serial: false,
3859 }
3860 }
3861
3862 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3870 if let Err(e) = self.try_set_o1(cfg) {
3871 tracing::error!("{e}");
3872 }
3873 }
3874
3875 pub fn bounded_native(&self) -> bool {
3879 self.anchor_core.is_some()
3880 }
3881
3882 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3885 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3886 }
3887
3888 pub fn o1_refusal(&self) -> Option<String> {
3890 self.anchor_core.as_ref().map(|ac| {
3891 format!(
3892 "--o1 / CMF_O1 refused: the anchor is native bounded \
3893 (anchor_core kind={} window={} sink={}); the file's operator \
3894 is executed as-is and no post-hoc Nyström overlay applies",
3895 ac.kind, ac.window, ac.sink
3896 )
3897 })
3898 }
3899
3900 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3902 if let Some(c) = &cfg {
3903 if let Some(why) = self.o1_refusal() {
3904 self.o1_flags = Vec::new();
3905 self.o1_cfg = None;
3906 return Err(why);
3907 }
3908 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3909 self.o1_flags.clear();
3910 self.o1_cfg = None;
3911 return Err(format!(
3912 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3913 c.w, c.sink
3914 ));
3915 }
3916 }
3917 self.o1_flags = match &cfg {
3918 Some(c) => {
3919 let mut flags = c.layer_flags(self.num_layers);
3920 for (li, f) in flags.iter_mut().enumerate() {
3921 if *f
3927 && (!matches!(
3928 self.weights.layers[self.phys_layer(li)].attn,
3929 AttnKind::Full { .. }
3930 ) || self.layer_window(li).is_some()
3931 || self.kv_cache.layers[li].sinks.is_some()
3932 || self.layer_v_dim(li) != self.layer_geom(li).1)
3933 {
3934 *f = false;
3935 }
3936 }
3937 flags
3938 }
3939 None => Vec::new(),
3940 };
3941 if let Some(c) = &cfg {
3942 let n = self.o1_flags.iter().filter(|&&f| f).count();
3943 tracing::info!(
3944 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3945 self.num_layers,
3946 c.m,
3947 c.w,
3948 c.sink,
3949 c.rect
3950 );
3951 }
3952 self.o1_cfg = cfg;
3953 Ok(())
3954 }
3955
3956 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3962 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3963 return Err(format!(
3964 "anchor_core kind '{}' is not executable by this runtime",
3965 cfg.kind
3966 ));
3967 }
3968 if cfg.window == 0 {
3969 return Err("anchor_core.window must be >= 1".into());
3970 }
3971 let mut n = 0usize;
3972 for li in 0..self.num_layers {
3973 let pl = self.phys_layer(li);
3974 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3975 if w.window != cfg.window || w.sink != cfg.sink {
3976 return Err(format!(
3977 "layer {li}: bounded weights (window {} sink {}) disagree with \
3978 anchor_core (window {} sink {})",
3979 w.window, w.sink, cfg.window, cfg.sink
3980 ));
3981 }
3982 self.kv_cache.layers[li].install_bounded(cfg.window);
3983 n += 1;
3984 }
3985 }
3986 if n == 0 {
3987 return Err("anchor_core is present but no layer executes it".into());
3988 }
3989 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3990 self.bounded_rope = Some(std::sync::Arc::new(rope));
3991 self.anchor_core = Some(cfg.clone());
3992 self.embryo_graph = None;
3993 tracing::info!(
3994 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3995 cfg.kind,
3996 cfg.window,
3997 cfg.sink,
3998 self.kv_cache
3999 .layers
4000 .iter()
4001 .map(|l| l.bounded_state_bytes())
4002 .max()
4003 .unwrap_or(0)
4004 );
4005 Ok(())
4006 }
4007
4008 pub fn install_wire_identity(&mut self, identity: u64) {
4012 for li in 0..self.kv_cache.layers.len() {
4013 let pl = self.phys_layer(li);
4014 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
4015 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
4016 Some(AttnKind::Linear(_))
4017 | Some(AttnKind::LinearGdn(_))
4018 | Some(AttnKind::ShortConv(_))
4019 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
4020 _ => crate::kv_cache::WireKind::Full,
4021 };
4022 let l = &mut self.kv_cache.layers[li];
4023 l.wire_kind = kind;
4024 l.wire_identity = identity;
4025 l.wire_layer = li as u32;
4028 }
4029 }
4030
4031 pub fn clear_history(&mut self) {
4033 self.kv_history.clear();
4034 self.kv_history_device = false;
4035 self.kv_prefix.clear();
4036 }
4037
4038 pub fn graph_refused(&self) -> bool {
4042 self.graph_refused
4043 .load(std::sync::atomic::Ordering::Relaxed)
4044 }
4045
4046 pub fn mark_graph_refused(&self) {
4048 if !self
4049 .graph_refused
4050 .swap(true, std::sync::atomic::Ordering::Relaxed)
4051 {
4052 tracing::info!(
4053 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4054 self.graph_kv_id
4055 );
4056 }
4057 }
4058
4059 pub fn device_sequence_position(&self) -> Option<usize> {
4063 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4064 }
4065
4066 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4071 let dev = self.device_sequence_position();
4072 if recorded_on_device {
4073 dev == Some(n) && self.embryo_resident_wanted()
4074 } else {
4075 dev.is_none()
4076 }
4077 }
4078
4079 pub(crate) fn invalidate_for_weight_change(&mut self) {
4087 self.clear_sequence_state();
4088 self.embryo_graph = None;
4089 }
4090
4091 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4097 let (n, on_device) = if self.bounded_native() {
4098 (
4099 self.kv_prefix.extension(input_ids),
4100 self.kv_prefix.on_device(),
4101 )
4102 } else {
4103 let h = &self.kv_history;
4104 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4105 (h.len(), self.kv_history_device)
4106 } else if !h.is_empty() && !self.kv_history_device {
4107 (self.rollback_prefix_len(input_ids), false)
4108 } else {
4109 (0, false)
4110 }
4111 };
4112 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4115 tracing::warn!(
4116 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4117 the device now holds {:?} — re-prefilling from zero",
4118 if on_device { "resident device" } else { "host" },
4119 self.device_sequence_position()
4120 );
4121 return 0;
4122 }
4123 n
4124 }
4125
4126 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4137 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4138 || self.device_sequence_position().is_some()
4139 {
4140 return 0;
4141 }
4142 let h = &self.kv_history;
4143 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4144 let n = lcp.min(input_ids.len().saturating_sub(1));
4145 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4146 return 0;
4147 }
4148 let kv_id = self.graph_kv_id;
4149 for li in 0..self.num_layers {
4150 let l = &self.kv_cache.layers[li];
4151 let tail_ok = l.base() == 0
4152 || self
4153 .layer_window(li)
4154 .is_some_and(|w| l.base() + w <= n + 1);
4155 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4156 || l.o1.is_some()
4157 || l.bounded.is_some()
4158 || !l.linear_state.is_empty()
4159 || l.mode != crate::kv_cache::KvMode::F32
4160 || l.pos_len() != h.len()
4161 || !tail_ok
4162 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4163 || crate::gpu::graph_state_resident(kv_id, li)
4164 {
4165 return 0;
4166 }
4167 }
4168 n
4169 }
4170
4171 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4174 self.cached_prefix_len(input_ids)
4175 }
4176
4177 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4183 let on_device = self.device_sequence_position().is_some();
4184 if self.bounded_native() {
4185 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4186 let prev_device = self.kv_prefix.on_device();
4187 self.kv_history.clear();
4188 self.kv_history_device = false;
4189 if keep && prev_device == on_device {
4190 self.kv_prefix.extend(&consumed[reused..]);
4191 } else {
4192 self.kv_prefix.set(consumed);
4193 }
4194 self.kv_prefix.set_on_device(on_device);
4195 } else {
4196 self.kv_history = consumed.to_vec();
4197 self.kv_history_device = on_device;
4198 }
4199 }
4200
4201 pub fn o1_active(&self) -> bool {
4203 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4204 }
4205
4206 pub fn generation_batch_k(&self) -> usize {
4224 if let Some(k) = std::env::var("CMF_BATCH_K")
4225 .ok()
4226 .and_then(|v| v.parse::<usize>().ok())
4227 {
4228 return k;
4229 }
4230 if self.g4_batch_prefill() {
4233 return 128;
4234 }
4235 #[cfg(not(target_os = "macos"))]
4236 if self.graph_prefill_preferred() && !self.o1_active() {
4237 let moe = self
4241 .weights
4242 .layers
4243 .iter()
4244 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4245 let q82 = crate::gpu::moe_q82_graph_on()
4251 && self.weights.layers.iter().any(|lw| {
4252 matches!(&lw.ffn, FfnKind::Moe(m)
4253 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4254 });
4255 let q4tp_moe = self.weights.layers.iter().any(|lw| {
4262 matches!(&lw.ffn, FfnKind::Moe(m)
4263 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
4264 });
4265 return if q82 {
4266 128
4267 } else if q4tp_moe {
4268 256
4269 } else if moe {
4270 32
4271 } else {
4272 128
4273 };
4274 }
4275 0
4276 }
4277
4278 pub fn generation_graph_prefill(&self) -> bool {
4279 let graph = self.graph_prefill_preferred();
4280 #[cfg(not(target_os = "macos"))]
4291 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4292 return false;
4293 }
4294 graph
4295 }
4296
4297 pub fn o1_device_stats(&self) -> (usize, u64) {
4302 crate::gpu::o1_device_stats(self.graph_kv_id)
4303 }
4304
4305 pub fn o1_begin(&mut self) {
4310 self.o1_begin_with_prefix(None);
4311 }
4312
4313 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4317 if let Some(c) = &self.o1_cfg {
4318 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4319 let boundary = requested_prefix.map(|p| {
4320 p.max(
4321 crate::nystrom::o1_deferred_boundary(w, sink)
4322 .expect("o1 config boundary validated in set_o1"),
4323 )
4324 });
4325 for (li, &f) in self.o1_flags.iter().enumerate() {
4326 if f {
4327 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4328 }
4329 }
4330 }
4331 }
4332
4333 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4335 self.o1_cfg.as_ref().and_then(|c| {
4336 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4337 .map(|floor| requested_prefix.max(floor))
4338 })
4339 }
4340
4341 fn o1_note_transition(&mut self) {
4342 let mut transitioned = false;
4346 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4347 if flagged {
4348 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4349 }
4350 }
4351 if transitioned {
4352 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4353 }
4354 }
4355
4356 fn o1_pending(&self) -> bool {
4357 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4358 f && self.kv_cache.layers[li].seq_len > 0
4359 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4360 })
4361 }
4362
4363 fn o1_fail(&mut self, err: String) {
4364 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4365 self.clear_sequence_state();
4366 self.graph_failed
4367 .store(true, std::sync::atomic::Ordering::Relaxed);
4368 self.cancel
4369 .store(true, std::sync::atomic::Ordering::Relaxed);
4370 }
4371
4372 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4377 if self.o1_cfg.is_none() {
4378 return Ok(false);
4379 }
4380 let mut participating = false;
4381 for li in 0..self.num_layers {
4382 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4383 continue;
4384 }
4385 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4386 return Err(err);
4387 }
4388 if self.kv_cache.layers[li].seq_len == 0 {
4389 continue;
4390 }
4391 participating = true;
4392 let num_heads = self.layer_num_heads(li);
4393 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4394 }
4395 self.o1_note_transition();
4396 for li in 0..self.num_layers {
4397 if self.o1_flags.get(li).copied().unwrap_or(false) {
4398 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4399 return Err(err);
4400 }
4401 }
4402 }
4403 Ok(participating
4404 && (0..self.num_layers).all(|li| {
4405 !self.o1_flags.get(li).copied().unwrap_or(false)
4406 || self.kv_cache.layers[li].seq_len == 0
4407 || self.kv_cache.layers[li].o1_sealed()
4408 }))
4409 }
4410
4411 fn o1_progress(&mut self) {
4414 if !self.o1_active() {
4415 return;
4416 }
4417 for li in 0..self.num_layers {
4418 if self.o1_flags.get(li).copied().unwrap_or(false) {
4419 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4420 self.o1_fail(err);
4421 return;
4422 }
4423 }
4424 }
4425 self.o1_note_transition();
4429 if !self.o1_pending() {
4430 return;
4431 }
4432 if let Err(err) = self.o1_seal_checked() {
4433 self.o1_fail(err);
4434 }
4435 }
4436
4437 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4442 if self
4443 .graph_failed
4444 .swap(false, std::sync::atomic::Ordering::Relaxed)
4445 {
4446 self.cancel
4447 .store(false, std::sync::atomic::Ordering::Relaxed);
4448 self.clear_sequence_state();
4449 return Err(format!("{phase}: deferred O(1) transition failed"));
4450 }
4451 Ok(())
4452 }
4453
4454 pub fn o1_seal(&mut self) {
4458 if let Err(err) = self.o1_seal_checked() {
4459 self.o1_fail(err);
4460 }
4461 }
4462
4463 pub fn set_trace(&mut self, on: bool) {
4465 self.trace = on;
4466 }
4467
4468 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4471 self.rng = match config.seed {
4472 Some(seed) => SplitMix64::new(seed),
4473 None => SplitMix64::from_entropy(),
4474 };
4475 self.sampler_config = config;
4476 }
4477
4478 pub fn set_confidence(&mut self, on: bool) {
4483 self.confidence_on = on;
4484 }
4485
4486 pub fn set_calib_temp(&mut self, t: f32) {
4489 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4490 }
4491
4492 pub fn calib_temp(&self) -> f32 {
4494 self.calib_temp
4495 }
4496
4497 pub fn rope_inv_freq(&self) -> &[f32] {
4501 self.inv_freq.as_slice()
4502 }
4503
4504 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4507 self.rotary_dim = rotary_dim.min(self.head_dim);
4508 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4509 self.embryo_graph = None;
4513 }
4514
4515 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4516 QwenAttnCfg {
4517 num_heads: self.num_heads,
4518 num_kv_heads: self.num_kv_heads,
4519 head_dim: self.head_dim,
4520 hidden_size: self.hidden_size,
4521 position,
4522 inv_freq: &self.inv_freq,
4523 rotary_dim: self.rotary_dim,
4524 scale: self.attn_scale,
4525 softcap: self.attn_softcap,
4526 window: None,
4527 v_norm: false,
4528 qk_norm_after_rope: self.qk_norm_after_rope,
4529 gate_sigmoid: self.proj_gate_sigmoid,
4530 q_norm: None,
4531 k_norm: None,
4532 output_gate: false,
4533 softplus_gate: None,
4534 rope_scale: self.rope_scale,
4535 bias: None,
4536 rms_eps: self.rms_eps,
4537 norm_style: self.norm_style,
4538 pool: self.pool.as_deref(),
4539 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4540 }
4541 }
4542
4543 pub fn generate(
4545 &mut self,
4546 prompt: &str,
4547 max_tokens: usize,
4548 task_mask: Option<&TaskMask>,
4549 on_token: Option<TokenCallback>,
4550 ) -> Result<GenerateResult, String> {
4551 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4552 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4553 }
4554
4555 pub fn generate_from_vl(
4558 &mut self,
4559 input: &crate::dsv41_vision::PreparedVlInputs,
4560 max_tokens: usize,
4561 task_mask: Option<&TaskMask>,
4562 on_token: Option<TokenCallback>,
4563 ) -> Result<GenerateResult, String> {
4564 let Some(dsv41) = &self.dsv41 else {
4565 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4566 };
4567 if input.token_ids.is_empty() {
4568 return Err("empty V4.1 multimodal prompt".into());
4569 }
4570 if input.token_types.len() != input.token_ids.len() {
4571 return Err(format!(
4572 "V4.1 token type count {} != token count {}",
4573 input.token_types.len(),
4574 input.token_ids.len()
4575 ));
4576 }
4577 let dim = dsv41.2.dim;
4578 let mut embeddings = vec![None; input.token_ids.len()];
4579 let mut participates = vec![true; input.token_ids.len()];
4580 if !input.images.is_empty() {
4581 let vision = self
4582 .dsv41_vision
4583 .as_ref()
4584 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4585 for image in &input.images {
4586 let end = image.start.saturating_add(image.types.len());
4587 if end > input.token_ids.len() {
4588 return Err(format!(
4589 "V4.1 image span {}..{} exceeds prompt length {}",
4590 image.start,
4591 end,
4592 input.token_ids.len()
4593 ));
4594 }
4595 let mut span = vec![0.0f32; image.types.len() * dim];
4596 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4597 for (offset, &kind) in image.types.iter().enumerate() {
4598 let pos = image.start + offset;
4599 if input.token_types[pos] != kind {
4600 return Err(format!(
4601 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4602 input.token_types[pos]
4603 ));
4604 }
4605 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4606 participates[pos] = false;
4607 }
4608 }
4609 }
4610 for (pos, &kind) in input.token_types.iter().enumerate() {
4611 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4612 return Err(format!("V4.1 text position {pos} has an image embedding"));
4613 }
4614 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4615 return Err(format!("V4.1 image position {pos} has no image embedding"));
4616 }
4617 }
4618 self.dsv41_prefill = Some((embeddings, participates));
4619 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4620 self.dsv41_prefill = None;
4621 result
4622 }
4623
4624 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4626 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4627 }
4628
4629 pub fn generate_from_ids(
4637 &mut self,
4638 input_ids: &[u32],
4639 max_tokens: usize,
4640 task_mask: Option<&TaskMask>,
4641 on_token: Option<TokenCallback>,
4642 ) -> Result<GenerateResult, String> {
4643 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4644 }
4645
4646 pub fn generate_from_embeds(
4652 &mut self,
4653 input_ids: &[u32],
4654 prompt_rows: &[f32],
4655 max_tokens: usize,
4656 task_mask: Option<&TaskMask>,
4657 on_token: Option<TokenCallback>,
4658 ) -> Result<GenerateResult, String> {
4659 if input_ids.is_empty()
4660 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4661 {
4662 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4663 }
4664 if prompt_rows.iter().any(|x| !x.is_finite()) {
4665 return Err("embedded prompt contains non-finite values".into());
4666 }
4667 if !self.can_prefill_batched()
4668 || self.dyn_router.is_some()
4669 || self.o1_active()
4670 || self.mtp.is_some()
4671 || self.gpu_plan.is_some()
4672 {
4673 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4674 }
4675 self.generate_with_prompt_rows(
4676 input_ids,
4677 Some(prompt_rows),
4678 max_tokens,
4679 task_mask,
4680 on_token,
4681 )
4682 }
4683
4684 fn generate_with_prompt_rows(
4685 &mut self,
4686 input_ids: &[u32],
4687 prompt_rows: Option<&[f32]>,
4688 max_tokens: usize,
4689 task_mask: Option<&TaskMask>,
4690 mut on_token: Option<TokenCallback>,
4691 ) -> Result<GenerateResult, String> {
4692 #[cfg(target_os = "macos")]
4693 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4694 if std::env::var("CMF_TRACE_H").is_ok() {
4695 eprintln!("input_ids: {input_ids:?}");
4696 }
4697 if input_ids.is_empty() {
4698 return Err("empty prompt: nothing to generate from".to_string());
4699 }
4700 self.graph_failed
4704 .store(false, std::sync::atomic::Ordering::Relaxed);
4705 let task_mask = self.drop_open_mask(task_mask);
4710 if self.clover.is_some() {
4713 if prompt_rows.is_some() || task_mask.is_some() {
4714 return Err("block-diffusion generation takes token prompts without task masks".into());
4715 }
4716 return self.clover_generate(input_ids, max_tokens, on_token);
4717 }
4718
4719 let mut reuse_from = {
4727 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4728 if on
4729 && prompt_rows.is_none()
4730 && task_mask.is_none()
4731 && self.mtp.is_none()
4732 && !(self.mimo_mtp.is_some() && self.speculative)
4733 && self.o1_cfg.is_none()
4734 && self.dsv41.is_none()
4735 {
4736 self.cached_prefix_len(input_ids)
4737 } else {
4738 0
4739 }
4740 };
4741 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4744 reuse_from = 0;
4745 }
4746 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4749 for l in &mut self.kv_cache.layers {
4750 let d = l.pos_len() - reuse_from;
4751 l.truncate_last(d);
4752 }
4753 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4754 eprintln!(
4755 "kv-reuse: rolled back {} answer position(s) to the common prefix",
4756 self.kv_history.len() - reuse_from
4757 );
4758 }
4759 self.kv_history.truncate(reuse_from);
4760 self.graph_logits = None;
4761 }
4762 self.swa_trim_anchor = input_ids.len();
4763 self.last_prefill_tokens = input_ids.len() - reuse_from;
4764 let bounded_native = self.bounded_native();
4765 if reuse_from == 0 {
4766 self.reset_fresh_mtp_draft_state();
4771 self.clear_sequence_state();
4772 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4773 eprintln!(
4774 "kv-reuse: {} of {} prompt positions already cached",
4775 reuse_from,
4776 input_ids.len()
4777 );
4778 }
4779 crate::gpu::graph_race_begin_generation();
4780 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4784 std::env::var("CMF_O1_PREFILL")
4785 .ok()
4786 .and_then(|v| v.parse::<usize>().ok())
4787 .filter(|&p| p > 0)
4788 } else {
4789 None
4790 };
4791 if task_mask.is_none() {
4792 self.o1_begin_with_prefix(o1_prefill);
4793 }
4794
4795 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4801 #[cfg(target_os = "macos")]
4839 let metal_graph = crate::gpu::q1_force()
4840 && crate::gpu::enabled_here()
4841 && std::env::var("CMF_GPU_BLOCK")
4842 .map(|v| v != "0")
4843 .unwrap_or(true);
4844 #[cfg(target_os = "macos")]
4850 let native_metal_backend = crate::gpu::q1_force();
4851 #[cfg(not(target_os = "macos"))]
4852 let metal_graph = false;
4853 #[cfg(not(target_os = "macos"))]
4854 let native_metal_backend = false;
4855 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4856 let spec_cheap_round =
4860 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4861 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4862 || match spec_sample_env.as_deref() {
4863 Some("1") => true,
4864 Some(_) => false,
4865 None => metal_graph && spec_cheap_round,
4866 };
4867 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4883 for lw in &self.weights.layers {
4884 if let FfnKind::Dense(d) = &lw.ffn {
4885 dense_n += 1;
4886 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4887 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4888 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4889 {
4890 dense_q4tp += 1;
4891 }
4892 }
4893 }
4894 #[cfg(feature = "gpu")]
4903 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| self.mtp_graph_ok(m));
4904 #[cfg(not(feature = "gpu"))]
4905 let mtp_on_graph = true;
4906 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
4907 let penalized = !metal_graph
4919 && (self.sampler_config.repetition_penalty != 1.0
4920 || self.sampler_config.presence_penalty != 0.0
4921 || !self.sampler_config.suppress_tokens.is_empty());
4922 #[cfg(feature = "gpu")]
4927 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4928 #[cfg(not(feature = "gpu"))]
4929 let metal_wgpu = false;
4930 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4931 let spec_wanted = graph_spec_wanted_by_policy(
4932 spec_env.as_deref(),
4933 spec_default_ok,
4934 penalized,
4935 metal_wgpu,
4936 native_metal_backend,
4937 );
4938 if spec_wanted && metal_wgpu {
4939 tracing::warn!(
4940 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4941 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4942 );
4943 }
4944 let graph_spec = self.speculative
4948 && (graph_on || metal_graph)
4949 && self.mtp.is_some()
4950 && task_mask.is_none()
4951 && !self.o1_active()
4952 && spec_sampling_ok
4953 && spec_wanted;
4954 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4967 let pair_pays = host_pair_opt_in
4968 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4969 #[cfg(target_os = "macos")]
4973 if metal_graph {
4974 static SAID: std::sync::Once = std::sync::Once::new();
4975 SAID.call_once(|| {
4976 let spec = if graph_spec {
4977 let k = std::env::var("CMF_GRAPH_SPEC_K")
4978 .ok()
4979 .and_then(|v| v.parse::<usize>().ok())
4980 .filter(|&v| (1..=8).contains(&v))
4981 .unwrap_or(7);
4982 let arm = if self.sampler_config.temperature < 1e-6 {
4983 "greedy"
4984 } else {
4985 "sampling"
4986 };
4987 format!(
4988 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4989 Self::draft_vocab_rows(usize::MAX)
4990 )
4991 } else if !self.speculative {
4992 "spec off (CMF_MTP=0)".to_string()
4993 } else if self.mtp.is_none() {
4994 "spec off (no MTP head)".to_string()
4995 } else if !spec_sampling_ok {
4996 if spec_cheap_round {
4997 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4998 } else {
4999 "spec off (sampling without a top-k: the dense chain \
5000 costs more than it saves)"
5001 .to_string()
5002 }
5003 } else if !spec_wanted {
5004 if spec_env.is_none() {
5005 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
5006 } else {
5007 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
5008 }
5009 } else if task_mask.is_some() {
5010 "spec off (task mask)".to_string()
5011 } else if !graph_spec && !pair_pays {
5012 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
5013 .to_string()
5014 } else {
5015 "spec off (O(1) attention)".to_string()
5016 };
5017 let on = |var: &str| {
5018 if std::env::var(var).as_deref() == Ok("0") {
5019 "off"
5020 } else {
5021 "on"
5022 }
5023 };
5024 tracing::info!(
5025 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
5026 MTP graph {}, attend {}, probe {}",
5027 if crate::gpu_metal::state4_on() {
5028 "on"
5029 } else {
5030 "off"
5031 },
5032 if crate::gpu_metal::async_replay_on() {
5033 "on"
5034 } else {
5035 "off"
5036 },
5037 on("CMF_METAL_PREFILL"),
5038 on("CMF_MTP_GRAPH"),
5039 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
5040 if crate::gpu::probe_enabled() {
5041 "bypassed (q1 force)"
5042 } else {
5043 "off"
5044 },
5045 );
5046 });
5047 }
5048 let spec_active = self.speculative
5049 && self.mtp.is_some()
5050 && task_mask.is_none()
5051 && !self.o1_active()
5052 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
5053 let mut mtp = if spec_active { self.mtp.take() } else { None };
5056 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
5057 eprintln!(
5058 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
5059 mtp.is_some(),
5060 self.speculative,
5061 self.sampler_config.temperature < 1e-6,
5062 );
5063 }
5064 if let Some(m) = &mut mtp {
5065 m.kv.clear();
5066 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5068 self.mtp_graph_mode = None;
5069 }
5070 let mimo_spec = self.speculative
5074 && self.mimo_mtp.is_some()
5075 && task_mask.is_none()
5076 && !self.o1_active()
5077 && self.dyn_router.is_none()
5078 && self.sampler_config.temperature < 1e-6
5079 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5080 if let Some(st) = self.mimo_mtp.as_mut() {
5081 st.reset();
5082 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5083 Self::mimo_mtp_hist_cap(st, input_ids.len());
5084 }
5085 }
5086 let mut router = if mtp.is_none() {
5090 self.dyn_router.take()
5091 } else {
5092 None
5093 };
5094 let mut reuse_from = reuse_from;
5095 if let Some(r) = &mut router {
5096 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5099 let _ = self.set_active_skill(None);
5102 reuse_from = 0;
5103 self.last_prefill_tokens = input_ids.len();
5104 }
5105 }
5106
5107 let mut all_ids = input_ids.to_vec();
5108 let mut generated = 0usize;
5109 let mut finish_reason = "max_tokens".to_string();
5110 let mut drafted = 0usize;
5111 let mut accepted = 0usize;
5112 let mut dsv4_spec_bad = 0usize;
5119 let mut dsv4_spec_retry_at = 0usize;
5120 let mut confidence: Vec<f32> = Vec::new();
5121 let trace_on = self.trace;
5122 let calib_temp = self.calib_temp;
5123 let mut traces: Vec<TokenTrace> = Vec::new();
5124
5125 let mut hidden = vec![0.0f32; self.hidden_size];
5131 let mut pos = reuse_from;
5132 let fuse_lm = mtp.is_none()
5141 && router.is_none()
5142 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5143 self.graph_logits = None;
5144 self.graph_want_logits = false;
5145 let _tpf = std::time::Instant::now();
5146 let batch_k = self.generation_batch_k();
5147 if let Some(rows) = prompt_rows {
5148 let hs = self.hidden_size;
5149 let chunk = self.prefill_chunk().max(1);
5150 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5151 let end = (pos + chunk).min(input_ids.len());
5152 let hb = match self.prefill_input_rows(
5153 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5154 pos,
5155 task_mask,
5156 ) {
5157 Ok(hb) => hb,
5158 Err(err) => {
5159 self.finish_generation(&mut mtp, &mut router, true);
5160 return Err(err);
5161 }
5162 };
5163 if mimo_spec {
5164 self.mimo_note_rows(&hb, pos);
5165 }
5166 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5167 pos = end;
5168 }
5169 }
5170 while self.qwen4_exp.is_some()
5181 && mtp.is_none()
5182 && pos < input_ids.len()
5183 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5184 {
5185 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5188 let want_logits = end == input_ids.len();
5189 let mut lg = Vec::new();
5190 if let Some(b) = &mut self.qwen4_exp {
5191 crate::qwen4_exp::forward_tokens(
5192 &b.0,
5193 &b.1,
5194 &b.2,
5195 &mut b.3,
5196 &input_ids[pos..end],
5197 pos,
5198 &self.inv_freq,
5199 self.pool.as_deref(),
5200 &mut lg,
5201 want_logits,
5202 );
5203 }
5204 if want_logits {
5205 self.graph_logits = Some(lg);
5206 }
5207 pos = end;
5208 hidden.fill(0.0);
5209 }
5210 while self.dsv4.is_some()
5211 && mtp.is_none()
5212 && pos < input_ids.len()
5213 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5214 {
5215 let end = (pos + prefill_chunk()).min(input_ids.len());
5216 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5217 let mut lg = Vec::new();
5218 if let Some(b) = &mut self.dsv4 {
5219 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5220 crate::dsv4::forward_chunk(
5221 g,
5222 layers,
5223 &cfg,
5224 st,
5225 &ids,
5226 pos,
5227 &self.inv_freq,
5228 self.pool.as_deref(),
5229 &mut lg,
5230 end == input_ids.len(),
5231 );
5232 }
5233 if end == input_ids.len() {
5234 self.graph_logits = Some(lg);
5235 }
5236 pos = end;
5237 hidden = vec![0.0; self.hidden_size];
5238 }
5239 let dsv41_prefill = self.dsv41_prefill.take();
5240 while self.dsv41.is_some()
5241 && mtp.is_none()
5242 && pos < input_ids.len()
5243 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5244 {
5245 let end = (pos + prefill_chunk()).min(input_ids.len());
5246 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5247 let mut lg = Vec::new();
5248 if let Some(b) = &mut self.dsv41 {
5249 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5250 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5251 crate::dsv41::forward_chunk_masked_with_embeddings(
5252 g,
5253 layers,
5254 cfg,
5255 st,
5256 &ids,
5257 pos,
5258 &embeddings[pos..end],
5259 &participates[pos..end],
5260 self.pool.as_deref(),
5261 &mut lg,
5262 );
5263 } else {
5264 crate::dsv41::forward_chunk(
5265 g,
5266 layers,
5267 cfg,
5268 st,
5269 &ids,
5270 pos,
5271 self.pool.as_deref(),
5272 &mut lg,
5273 );
5274 }
5275 }
5276 if end == input_ids.len() {
5277 self.graph_logits = Some(lg);
5278 }
5279 pos = end;
5280 hidden = vec![0.0; self.hidden_size];
5281 }
5282 let dyn_prefill = router.is_some();
5287 let o1_prefill_limit = o1_prefill
5295 .and_then(|requested| self.o1_effective_boundary(requested))
5296 .map(|boundary| boundary.min(input_ids.len()));
5297 let mut o1_sealed = false;
5298 if let Some(limit) = o1_prefill_limit {
5299 if self.can_prefill_batched() && limit > 2 {
5302 let chunk = self.prefill_chunk();
5303 let hs = self.hidden_size;
5304 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5305 let end = (pos + chunk).min(limit);
5306 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5307 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5308 pos = end;
5309 }
5310 } else {
5311 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5312 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5313 pos += 1;
5314 }
5315 }
5316 if pos >= limit {
5317 o1_sealed = match self.o1_seal_checked() {
5318 Ok(sealed) => sealed,
5319 Err(err) => {
5320 self.finish_generation(&mut mtp, &mut router, true);
5321 return Err(err);
5322 }
5323 };
5324 tracing::info!(
5325 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5326 o1_prefill.unwrap_or(0),
5327 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5328 .unwrap_or(limit),
5329 limit,
5330 input_ids.len()
5331 );
5332 }
5333 }
5334 let graph_prefill = self.graph_prefill_preferred();
5340 #[cfg(target_os = "macos")]
5348 if task_mask.is_none()
5349 && !dyn_prefill
5350 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5351 && crate::gpu::enabled_here()
5352 && self.gdn_cfg.is_some()
5353 && !self.per_token_stack()
5354 && input_ids.len() > 8
5355 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5356 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5357 {
5358 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5359 .ok()
5360 .and_then(|v| v.parse().ok())
5361 .filter(|&v| (16..=512).contains(&v))
5362 .unwrap_or(256);
5363 let hs = self.hidden_size;
5364 let _tp = std::time::Instant::now();
5365 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5366 let end = (pos + chunk).min(input_ids.len());
5367 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5368 MetalPrefillOutcome::Completed(hb) => hb,
5369 MetalPrefillOutcome::Declined => break,
5370 MetalPrefillOutcome::Failed => {
5371 self.finish_generation(&mut mtp, &mut router, true);
5372 return Err("ordinary Metal prefill failed after admission".into());
5373 }
5374 };
5375 if let Some(m) = &mut mtp {
5376 let n_pairs = if end < input_ids.len() {
5377 end - pos
5378 } else {
5379 end - pos - 1
5380 };
5381 if n_pairs > 0 {
5382 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5383 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5384 .collect();
5385 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5386 for (j, (h, t)) in pairs.iter().enumerate() {
5387 let h = h.to_vec();
5388 let _ = self.mtp_step(m, &h, *t, pos + j);
5389 }
5390 }
5391 }
5392 }
5393 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5394 pos = end;
5395 }
5396 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5397 eprintln!(
5398 "metal-prefill: {} of {} tokens in {:.1} ms",
5399 pos,
5400 input_ids.len(),
5401 _tp.elapsed().as_secs_f64() * 1e3
5402 );
5403 }
5404 }
5405 #[cfg(not(target_os = "macos"))]
5410 if task_mask.is_none()
5411 && !dyn_prefill
5412 && mtp.is_none()
5413 && o1_prefill.is_none()
5414 && batch_k > 0
5415 && pos + 1 < input_ids.len()
5416 && self.g4_batch_prefill()
5417 {
5418 let t_g4 = std::time::Instant::now();
5419 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5420 Ok((n, last)) => {
5421 if n > 0 {
5422 hidden.copy_from_slice(&last);
5423 }
5424 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5425 eprintln!(
5426 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5427 input_ids.len() - pos,
5428 t_g4.elapsed().as_secs_f64() * 1e3
5429 );
5430 }
5431 pos += n;
5432 }
5433 Err(e) => {
5434 self.finish_generation(&mut mtp, &mut router, true);
5435 return Err(e);
5436 }
5437 }
5438 }
5439 self.mimo_moe_prepare();
5440 #[cfg(not(target_os = "macos"))]
5446 if task_mask.is_none()
5447 && !dyn_prefill
5448 && !graph_prefill
5449 && mtp.is_none()
5450 && o1_prefill.is_none()
5451 && !self.o1_active()
5452 && input_ids.len() > 2
5453 && self.batch_prefix_prefill()
5454 {
5455 let chunk = self.prefill_chunk().max(1);
5456 let hs = self.hidden_size;
5457 let t_bp = std::time::Instant::now();
5458 let pos0 = pos;
5459 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5460 let end = (pos + chunk).min(input_ids.len());
5461 let bk = end - pos;
5462 let mut hiddens = vec![0f32; bk * hs];
5463 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5464 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5465 }
5466 let positions: Vec<usize> = (pos..end).collect();
5467 let mut run = 0usize;
5468 let outcome = self.try_batch_graph_wgpu_prefix(
5469 &mut hiddens,
5470 &positions,
5471 bk,
5472 None,
5473 Some(&mut run),
5474 );
5475 match outcome {
5476 crate::gpu::BatchGraphOutcome::Completed => {
5477 let hb = if run < self.num_layers {
5478 self.prefill_batch_span(
5479 PrefillIn::Hidden(&hiddens),
5480 pos,
5481 None,
5482 run,
5483 self.num_layers,
5484 )
5485 } else {
5486 hiddens
5487 };
5488 if mimo_spec {
5489 self.mimo_note_rows(&hb, pos);
5490 }
5491 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5492 pos = end;
5493 }
5494 crate::gpu::BatchGraphOutcome::Failed => {
5495 self.finish_generation(&mut mtp, &mut router, true);
5496 return Err("batched prefix prefill failed after admission".into());
5497 }
5498 crate::gpu::BatchGraphOutcome::Declined => {
5499 #[cfg(feature = "gpu")]
5502 if pos > pos0 {
5503 self.pull_lagging_host_kv(0, self.num_layers, pos);
5504 }
5505 break;
5506 }
5507 }
5508 }
5509 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5510 eprintln!(
5511 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5512 pos - pos0,
5513 input_ids.len(),
5514 t_bp.elapsed().as_secs_f64() * 1e3
5515 );
5516 }
5517 }
5518 if task_mask.is_none()
5519 && !dyn_prefill
5520 && !graph_prefill
5521 && self.can_prefill_batched()
5522 && !self.per_token_stack()
5523 && o1_prefill.is_none()
5524 && input_ids.len() > 2
5525 {
5526 let chunk = self.prefill_chunk();
5532 let hs = self.hidden_size;
5533 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5534 let end = (pos + chunk).min(input_ids.len());
5535 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5536 if mimo_spec {
5537 self.mimo_note_rows(&hb, pos);
5538 }
5539 if let Some(m) = &mut mtp {
5540 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5541 .ok()
5542 .and_then(|v| v.parse().ok())
5543 .unwrap_or(0);
5544 for p in pos..end {
5545 if p + 1 < input_ids.len() {
5546 if probe >= 1 && p + 2 < input_ids.len() {
5547 let (d1, mut hx) = self.mtp_step_h(
5551 m,
5552 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5553 input_ids[p + 1],
5554 p,
5555 );
5556 let mut ok = d1 == input_ids[p + 2];
5557 Self::chain_probe_note(0, ok);
5558 let mut d_prev = d1;
5559 let mut extra = 0usize;
5560 for j in 1..probe {
5561 if p + 2 + j >= input_ids.len() {
5562 break;
5563 }
5564 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5565 extra += 1;
5566 ok = ok && dj == input_ids[p + 2 + j];
5567 Self::chain_probe_note(j, ok);
5568 d_prev = dj;
5569 hx = hj;
5570 }
5571 m.kv.truncate_last(extra);
5572 } else {
5573 let _ = self.mtp_step(
5574 m,
5575 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5576 input_ids[p + 1],
5577 p,
5578 );
5579 }
5580 }
5581 }
5582 }
5583 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5584 pos = end;
5585 }
5586 }
5587 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5588 if task_mask.is_none()
5589 && !dyn_prefill
5590 && !graph_prefill
5591 && !pair_off
5592 && self.pair_supported()
5593 && o1_prefill.is_none()
5594 {
5595 while pos + 1 < input_ids.len()
5596 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5597 {
5598 let e1 = self.embed_single(input_ids[pos]);
5599 let e2 = self.embed_single(input_ids[pos + 1]);
5600 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5601 if mimo_spec {
5602 self.mimo_note_rows(&h1, pos);
5603 self.mimo_note_rows(&h2, pos + 1);
5604 }
5605 self.commit_linear_scratch();
5607 if let Some(m) = &mut mtp {
5608 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5609 if pos + 2 < input_ids.len() {
5610 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5611 .ok()
5612 .and_then(|v| v.parse().ok())
5613 .unwrap_or(0);
5614 if probe >= 1 && pos + 3 < input_ids.len() {
5615 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5619 let mut ok = d1 == input_ids[pos + 3];
5620 Self::chain_probe_note(0, ok);
5621 let mut d_prev = d1;
5622 let mut extra = 0usize;
5623 for j in 1..probe {
5624 if pos + 3 + j >= input_ids.len() {
5625 break;
5626 }
5627 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5628 extra += 1;
5629 ok = ok && dj == input_ids[pos + 3 + j];
5630 Self::chain_probe_note(j, ok);
5631 d_prev = dj;
5632 hx = hj;
5633 }
5634 m.kv.truncate_last(extra);
5635 } else {
5636 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5637 }
5638 }
5639 }
5640 hidden = h2;
5641 pos += 2;
5642 }
5643 }
5644 let o1_batch_ready = o1_sealed
5657 && o1_prefill.is_some()
5658 && mtp.is_none()
5659 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5660 && (0..self.num_layers).all(|li| {
5661 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5662 cache.o1.is_none() || cache.o1_views().is_some()
5663 });
5664 let mtp_batch_prefill = mtp.is_some()
5669 && graph_prefill
5670 && task_mask.is_none()
5671 && !dyn_prefill
5672 && !self.o1_active()
5673 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5674 if batch_k > 0
5675 && (graph_prefill || o1_batch_ready)
5676 && task_mask.is_none()
5677 && (!self.o1_active() || o1_batch_ready)
5678 && (mtp.is_none() || mtp_batch_prefill)
5679 && !dyn_prefill
5680 && pos + 1 < input_ids.len()
5681 {
5682 let hs = self.hidden_size;
5683 let chunk = batch_k;
5684 while pos < input_ids.len() {
5685 let end = (pos + chunk).min(input_ids.len());
5686 let bk = end - pos;
5687 let mut hiddens = vec![0f32; bk * hs];
5688 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5689 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5690 }
5691 let positions: Vec<usize> = (pos..end).collect();
5692 let t_chunk = std::time::Instant::now();
5693 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5694 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5695 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5696 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5697 eprintln!(
5698 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5699 if o1_batch_ready {
5700 "o1"
5701 } else if mtp_batch_prefill {
5702 "ordinary_mtp"
5703 } else {
5704 "ordinary"
5705 },
5706 bk as f64 / (ms / 1000.0)
5707 );
5708 }
5709 {
5710 use std::sync::atomic::{AtomicBool, Ordering};
5711 static SAID: AtomicBool = AtomicBool::new(false);
5712 if !SAID.swap(true, Ordering::Relaxed) {
5713 if ok_b {
5714 tracing::info!(
5715 "batched prefill: ACTIVE mode={} (k={bk})",
5716 if o1_batch_ready {
5717 "o1"
5718 } else if mtp_batch_prefill {
5719 "ordinary_mtp"
5720 } else {
5721 "ordinary"
5722 }
5723 );
5724 } else {
5725 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5726 }
5727 }
5728 }
5729 if ok_b {
5730 if mimo_spec {
5731 self.mimo_note_rows(&hiddens, pos);
5732 }
5733 if mtp_batch_prefill {
5734 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5735 if n_pairs > 0 {
5736 let rows: Vec<Vec<f32>> = (0..n_pairs)
5742 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5743 .collect();
5744 let pairs: Vec<(&[f32], u32)> = rows
5745 .iter()
5746 .enumerate()
5747 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5748 .collect();
5749 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5750 eprintln!(
5751 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5752 pos,
5753 n_pairs,
5754 pos + n_pairs - 1,
5755 );
5756 }
5757 let warm_error = if let Some(m) = mtp.as_mut() {
5758 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5759 } else {
5760 None
5761 };
5762 if let Some(err) = warm_error {
5763 self.finish_generation(&mut mtp, &mut router, true);
5768 return Err(err.to_string());
5769 }
5770 }
5771 }
5772 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5773 pos = end;
5774 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5775 self.finish_generation(&mut mtp, &mut router, true);
5780 return Err(if o1_batch_ready {
5781 "sealed O(1) batch graph failed after admission".to_string()
5782 } else {
5783 "ordinary recurrent batch graph failed after admission".to_string()
5784 });
5785 } else {
5786 break; }
5788 }
5789 }
5790 if graph_prefill
5794 && task_mask.is_none()
5795 && mtp.is_none()
5796 && !dyn_prefill
5797 && pos == 0
5798 && input_ids.len() > 1
5799 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5800 {
5801 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5802 self.graph_logits = Some(lg);
5803 hidden = vec![0.0; self.hidden_size];
5804 pos = input_ids.len();
5805 }
5806 }
5807 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5808 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5809 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5810 if mimo_spec {
5811 self.mimo_note_rows(&hidden, pos);
5812 }
5813 if let Some(m) = &mut mtp {
5814 if pos + 1 < input_ids.len() {
5815 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5821 .ok()
5822 .and_then(|v| v.parse().ok())
5823 .unwrap_or(0);
5824 if probe >= 1 && pos + 2 < input_ids.len() {
5825 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5826 let mut ok = d1 == input_ids[pos + 2];
5827 Self::chain_probe_note(0, ok);
5828 let mut d_prev = d1;
5829 let mut extra = 0usize;
5830 for j in 1..probe {
5831 if pos + 2 + j >= input_ids.len() {
5832 break;
5833 }
5834 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5835 extra += 1;
5836 ok = ok && dj == input_ids[pos + 2 + j];
5837 Self::chain_probe_note(j, ok);
5838 d_prev = dj;
5839 hx = hj;
5840 }
5841 m.kv.truncate_last(extra);
5844 } else {
5845 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5846 }
5847 }
5848 }
5849 pos += 1;
5850 }
5851 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5852 eprintln!(
5853 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5854 input_ids.len(),
5855 _tpf.elapsed().as_secs_f64() * 1000.0
5856 );
5857 }
5858 if self
5859 .graph_failed
5860 .swap(false, std::sync::atomic::Ordering::Relaxed)
5861 {
5862 self.finish_generation(&mut mtp, &mut router, true);
5867 return Err("GPU token graph failed during prefill".to_string());
5868 }
5869 if self
5872 .cancel
5873 .swap(false, std::sync::atomic::Ordering::Relaxed)
5874 {
5875 self.finish_generation(&mut mtp, &mut router, true);
5879 return Ok(GenerateResult {
5880 text: String::new(),
5881 token_ids: Vec::new(),
5882 prompt_tokens: input_ids.len(),
5883 tokens_generated: 0,
5884 finish_reason: "cancelled".to_string(),
5885 mtp_drafted: 0,
5886 mtp_accepted: 0,
5887 token_confidence: Vec::new(),
5888 traces: Vec::new(),
5889 });
5890 }
5891
5892 if !o1_sealed {
5895 match self.o1_seal_checked() {
5896 Ok(_) => {}
5897 Err(err) => {
5898 self.finish_generation(&mut mtp, &mut router, true);
5899 return Err(err);
5900 }
5901 }
5902 }
5903
5904 macro_rules! commit {
5906 ($id:expr) => {{
5907 all_ids.push($id);
5908 generated += 1;
5909 self.note_draft_id($id);
5910 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5911 finish_reason = "stop".to_string();
5912 false
5913 } else {
5914 let token_text = self.tokenizer.decode_token($id);
5915 let mut go = true;
5916 if let Some(ref mut cb) = on_token {
5917 if !cb(&token_text) {
5918 finish_reason = "cancelled".to_string();
5919 go = false;
5920 }
5921 }
5922 go
5923 }
5924 }};
5925 }
5926
5927 let mut spec_trial = SpecTrial::Spec {
5938 t0: std::time::Instant::now(),
5939 gen0: generated,
5940 rounds: 0,
5941 };
5942 let mut spec_mon = SpecMon {
5948 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5949 ..SpecMon::default()
5950 };
5951 let mut spec_watchdog_off = false;
5952 let mut spec_walls: Vec<f32> = Vec::new();
5955 let mut spec_round_end: Option<std::time::Instant> = None;
5958 if mimo_spec {
5959 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5960 if let Some(mut st) = self.mimo_mtp.take() {
5961 self.mimo_mtp_probe(&mut st, input_ids, &path);
5962 self.mimo_mtp = Some(st);
5963 }
5964 }
5965 }
5966 let mut next_pos = input_ids.len();
5968 'decode: while generated < max_tokens {
5969 if self
5970 .graph_failed
5971 .swap(false, std::sync::atomic::Ordering::Relaxed)
5972 {
5973 self.finish_generation(&mut mtp, &mut router, true);
5978 return Err("GPU token graph failed during decode".to_string());
5979 }
5980 if self
5981 .cancel
5982 .swap(false, std::sync::atomic::Ordering::Relaxed)
5983 {
5984 finish_reason = "cancelled".to_string();
5985 break 'decode;
5986 }
5987 if mimo_spec && next_pos > 0 {
5992 self.mimo_note_rows(&hidden, next_pos - 1);
5995 }
5996 let forced = self.spec_forced.take();
5997 let mut logits = match (forced, self.graph_logits.take()) {
5998 (Some(_), _) => Vec::new(),
5999 (None, Some(lg)) => lg,
6000 (None, None) => {
6001 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
6002 #[cfg(feature = "gpu")]
6007 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
6008 lg
6009 } else {
6010 inference::rms_norm_into(
6011 &hidden,
6012 &self.weights.final_norm,
6013 self.rms_eps,
6014 self.norm_style,
6015 &mut self.ws.n1,
6016 );
6017 self.lm_head_forward(&self.ws.n1)
6018 };
6019 #[cfg(not(feature = "gpu"))]
6020 let logits_out = {
6021 inference::rms_norm_into(
6022 &hidden,
6023 &self.weights.final_norm,
6024 self.rms_eps,
6025 self.norm_style,
6026 &mut self.ws.n1,
6027 );
6028 self.lm_head_forward(&self.ws.n1)
6029 };
6030 logits_out
6031 }
6032 };
6033 if generated
6036 == std::env::var("CMF_LOGIT_DUMP_STEP")
6037 .ok()
6038 .and_then(|v| v.parse().ok())
6039 .unwrap_or(0)
6040 {
6041 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
6042 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
6043 for v in hidden.iter().chain(logits.iter()) {
6044 bytes.extend_from_slice(&v.to_le_bytes());
6045 }
6046 if let Err(e) = std::fs::write(&path, &bytes) {
6047 eprintln!("logit dump: failed to write {path}: {e}");
6048 self.finish_generation(&mut mtp, &mut router, true);
6049 return Err(format!("logit dump write failed: {e}"));
6050 }
6051 }
6052 }
6053 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
6057 if !logits.is_empty() {
6058 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
6059 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
6060 if let Err(e) =
6061 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6062 {
6063 eprintln!("logit dump: failed to write {}: {e}", path.display());
6064 }
6065 }
6066 }
6067 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6068 Some(c) => c,
6069 None => {
6070 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6071 sampler::sample_with_scratch_pool(
6072 &logits,
6073 &self.sampler_config,
6074 self.sampler_config.penalty_past(&all_ids, bounded_native),
6075 &mut self.rng,
6076 &mut self.sampler_scratch,
6077 self.pool.as_deref(),
6078 )
6079 }
6080 };
6081 if self.confidence_on {
6082 confidence.push(if logits.is_empty() {
6083 0.0
6084 } else {
6085 sampler::top1_prob_pool(
6086 self.pool.as_deref(),
6087 &mut self.sampler_scratch,
6088 &logits,
6089 t_next,
6090 calib_temp,
6091 )
6092 });
6093 }
6094 if !logits.is_empty() {
6095 attention::recycle_buf(&mut logits);
6096 }
6097 if trace_on {
6098 let skill = router.as_ref().and_then(|r| r.active_id());
6102 traces.push(TokenTrace {
6103 t: generated,
6104 token_id: t_next,
6105 confidence: confidence.last().copied().unwrap_or(0.0),
6106 active_skill: skill,
6107 recon: None,
6108 switched: false,
6109 });
6110 }
6111 if !commit!(t_next) {
6112 break 'decode;
6113 }
6114 if generated >= max_tokens {
6115 break 'decode;
6116 }
6117
6118 self.swa_trim_tails();
6121 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6122 static SAID: std::sync::Once = std::sync::Once::new();
6128 SAID.call_once(|| {
6129 tracing::warn!(
6130 "KV cache full at {} positions — evicting half; quality \
6131 will degrade. Raise CMF_MAX_SEQ.",
6132 self.kv_cache.max_seq_len,
6133 );
6134 });
6135 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6136 self.kv_cache.evict(keep);
6137 }
6138
6139 if graph_spec {
6142 match spec_trial {
6143 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6144 spec_mon.plain_ms =
6145 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6146 let keep = spec_mon.pays();
6147 tracing::info!(
6148 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6149 spec_mon.tokens,
6150 spec_mon.round_ms,
6151 spec_mon.plain_ms,
6152 if keep { "speculating" } else { "plain" }
6153 );
6154 spec_mon.fails = 0;
6155 spec_trial = SpecTrial::Decided {
6156 spec: keep,
6157 recheck_at: if keep { usize::MAX } else { generated + 128 },
6158 };
6159 }
6160 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6161 spec_mon.n = 0;
6162 spec_mon.zero_accept_rounds = 0;
6163 spec_trial = SpecTrial::Spec {
6164 t0: std::time::Instant::now(),
6165 gen0: generated,
6166 rounds: 0,
6167 };
6168 }
6169 _ => {}
6170 }
6171 spec_watchdog_off = matches!(
6172 spec_trial,
6173 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6174 );
6175 }
6176 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6178 let budget = max_tokens - generated - 1;
6179 if let Some(mut st) = self.mimo_mtp.take() {
6180 let k = st.depth.min(budget);
6181 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6182 self.mimo_mtp = Some(st);
6183 let r = match r {
6184 Ok(r) => r,
6185 Err(err) => {
6186 self.finish_generation(&mut mtp, &mut router, true);
6187 return Err(err);
6188 }
6189 };
6190 if let Some(r) = r {
6191 drafted += r.drafted;
6192 accepted += r.accepted.len();
6193 let mut stopped = false;
6194 for &id in &r.accepted {
6195 if self.confidence_on {
6196 confidence.push(0.0);
6197 }
6198 if !commit!(id) {
6199 stopped = true;
6200 break;
6201 }
6202 }
6203 if stopped {
6204 break 'decode;
6205 }
6206 next_pos += r.accepted.len() + 1;
6207 hidden = r.hidden;
6208 self.graph_logits = Some(r.logits);
6211 continue 'decode;
6212 }
6213 }
6214 }
6215 #[cfg(feature = "gpu")]
6218 if self.speculative
6219 && self.qwen4_exp.is_some()
6220 && task_mask.is_none()
6221 && self.sampler_config.temperature < 1e-6
6222 && generated + 1 < max_tokens
6223 && next_pos > 0
6224 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6225 {
6226 let r = match &mut self.qwen4_exp {
6227 Some(b) => crate::qwen4_exp::spec_round(
6228 &b.0,
6229 &b.1,
6230 &b.2,
6231 &mut b.3,
6232 next_pos,
6233 &all_ids,
6234 &self.inv_freq,
6235 self.pool.as_deref(),
6236 ),
6237 None => None,
6238 };
6239 if let Some(r) = r {
6240 drafted += r.drafted;
6241 accepted += r.accepted.len();
6242 let mut stopped = false;
6243 for &id in &r.accepted {
6244 if self.confidence_on {
6245 confidence.push(0.0);
6246 }
6247 if !commit!(id) {
6248 stopped = true;
6249 break;
6250 }
6251 }
6252 if stopped {
6253 break 'decode;
6254 }
6255 next_pos += r.accepted.len() + 1;
6256 hidden.fill(0.0);
6257 self.graph_logits = Some(r.logits);
6258 continue 'decode;
6259 }
6260 }
6261 match &mut mtp {
6262 #[cfg(feature = "gpu")]
6264 Some(m)
6265 if graph_spec
6266 && !spec_watchdog_off
6267 && generated + 1 < max_tokens
6268 && next_pos > 0 =>
6269 {
6270 let t_round = std::time::Instant::now();
6271 if spec_time_level() >= 2 {
6272 if let Some(t) = spec_round_end.take() {
6273 eprintln!(
6274 "spec-gap {:.2} ms (host between rounds)",
6275 t.elapsed().as_secs_f64() * 1e3
6276 );
6277 }
6278 }
6279 spec_stamps_begin();
6280 #[cfg(target_os = "macos")]
6285 let allocs0 =
6286 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6287 #[cfg(not(target_os = "macos"))]
6288 let allocs0 = 0u64;
6289 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6290 m,
6291 &hidden,
6292 t_next,
6293 next_pos,
6294 &mut drafted,
6295 &mut accepted,
6296 &mut all_ids,
6297 max_tokens - generated,
6298 ) {
6299 next_pos = n_pos;
6300 hidden = new_h;
6301 let level = spec_time_level();
6302 if level > 0 {
6303 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6304 let stamps = spec_stamps_take();
6305 let median = if spec_walls.len() >= 3 {
6308 let mut s = spec_walls.clone();
6309 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6310 Some(s[s.len() / 2])
6311 } else {
6312 None
6313 };
6314 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6315 #[cfg(target_os = "macos")]
6316 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6317 .load(std::sync::atomic::Ordering::Relaxed)
6318 - allocs0;
6319 #[cfg(not(target_os = "macos"))]
6320 let allocs = allocs0;
6321 eprintln!(
6322 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6323 extra.len() + 1,
6324 if allocs > 0 {
6325 format!(" [{allocs} new device buffers]")
6326 } else {
6327 String::new()
6328 },
6329 match (outlier, median) {
6330 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6331 _ => String::new(),
6332 }
6333 );
6334 if level >= 2 || outlier {
6335 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6336 eprintln!(
6337 "spec-stamps: {}| untracked {:.1}",
6338 spec_stamps_format(&stamps),
6339 wall - sum
6340 );
6341 }
6342 if spec_mon.n >= 1 {
6343 spec_walls.push(wall);
6344 }
6345 }
6346 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6350 spec_trial = Self::spec_trial_round(
6353 spec_trial,
6354 &mut spec_mon,
6355 generated + extra.len() + 1,
6356 );
6357 let mut stopped = false;
6358 for &id in &extra {
6359 if self.confidence_on {
6360 confidence.push(0.0);
6361 }
6362 if trace_on {
6375 traces.push(TokenTrace {
6376 t: generated,
6377 token_id: id,
6378 confidence: confidence.last().copied().unwrap_or(0.0),
6379 active_skill: None,
6380 recon: None,
6381 switched: false,
6382 });
6383 }
6384 if !commit!(id) {
6385 stopped = true;
6386 break;
6387 }
6388 }
6389 if stopped {
6390 break 'decode;
6391 }
6392 if spec_time_level() >= 2 {
6393 spec_round_end = Some(std::time::Instant::now());
6394 }
6395 continue 'decode;
6396 }
6397 if self
6398 .graph_failed
6399 .swap(false, std::sync::atomic::Ordering::Relaxed)
6400 {
6401 self.finish_generation(&mut mtp, &mut router, true);
6407 return Err("GPU MTP graph failed during speculative decode".to_string());
6408 }
6409 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6420 spec_mon.tokens = 0.0;
6421 spec_mon.fails = 3;
6422 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6423 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6424 next_pos += 1;
6425 continue 'decode;
6426 }
6427 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6429 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6430 drafted += 1;
6431 let emb1 = self.embed_single(t_next);
6432 let emb2 = self.embed_single(draft);
6433 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6434
6435 inference::rms_norm_into(
6436 &h1,
6437 &self.weights.final_norm,
6438 self.rms_eps,
6439 self.norm_style,
6440 &mut self.ws.n1,
6441 );
6442 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6443 let t_after = sampler::sample_with_scratch_pool(
6444 &logits1,
6445 &self.sampler_config,
6446 self.sampler_config.penalty_past(&all_ids, bounded_native),
6447 &mut self.rng,
6448 &mut self.sampler_scratch,
6449 self.pool.as_deref(),
6450 );
6451 if self.confidence_on {
6452 confidence.push(sampler::top1_prob_pool(
6453 self.pool.as_deref(),
6454 &mut self.sampler_scratch,
6455 &logits1,
6456 t_after,
6457 calib_temp,
6458 ));
6459 }
6460 attention::recycle_buf(&mut logits1);
6461 if trace_on {
6462 traces.push(TokenTrace {
6465 t: generated,
6466 token_id: t_after,
6467 confidence: confidence.last().copied().unwrap_or(0.0),
6468 active_skill: None,
6469 recon: None,
6470 switched: false,
6471 });
6472 }
6473 let stop = !commit!(t_after);
6474
6475 if t_after == draft {
6476 accepted += 1;
6477 self.commit_linear_scratch();
6478 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6479 hidden = h2;
6480 next_pos += 2;
6481 } else {
6482 for layer in &mut self.kv_cache.layers {
6484 layer.truncate_last(1);
6485 }
6486 if !stop {
6487 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6488 hidden = self.forward_layers(
6489 &self.embed_single(t_after),
6490 next_pos + 1,
6491 None,
6492 );
6493 }
6494 next_pos += 2;
6495 }
6496 if stop {
6497 break 'decode;
6498 }
6499 }
6500 _ => {
6502 #[cfg(feature = "gpu")]
6507 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6508 static SAID: std::sync::Once = std::sync::Once::new();
6509 SAID.call_once(|| {
6510 eprintln!(
6511 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6512 !self.dsv4_mtp.is_empty(),
6513 task_mask.is_none(),
6514 router.is_none(),
6515 !trace_on,
6516 self.sampler_config.temperature < 1e-6,
6517 self.sampler_config.repetition_penalty == 1.0,
6518 );
6519 });
6520 }
6521 #[cfg(feature = "gpu")]
6522 if Self::dsv4_spec_on()
6523 && self.dsv4.is_some()
6524 && !self.dsv4_mtp.is_empty()
6525 && task_mask.is_none()
6526 && router.is_none()
6527 && !trace_on
6528 && self.sampler_config.temperature < 1e-6
6529 && self.sampler_config.repetition_penalty == 1.0
6530 && generated + 1 < max_tokens
6531 && all_ids.len() >= 2
6532 && generated >= dsv4_spec_retry_at
6533 {
6534 let tip_token = all_ids[all_ids.len() - 2];
6535 let drafted0 = drafted;
6536 let round = self.dsv4_spec_step(
6537 tip_token,
6538 t_next,
6539 next_pos,
6540 max_tokens.saturating_sub(generated),
6541 &mut drafted,
6542 &mut accepted,
6543 );
6544 if drafted > drafted0 {
6545 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6546 if useful {
6547 dsv4_spec_bad = 0;
6548 } else {
6549 dsv4_spec_bad += 1;
6550 if dsv4_spec_bad >= 2 {
6551 dsv4_spec_bad = 0;
6552 dsv4_spec_retry_at = generated.saturating_add(32);
6553 tracing::info!(
6554 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6555 );
6556 }
6557 }
6558 }
6559 if let Some((extra, n_pos)) = round {
6560 next_pos = n_pos;
6561 let mut stopped = false;
6562 for &id in &extra {
6563 if self.confidence_on {
6564 confidence.push(0.0);
6565 }
6566 if !commit!(id) {
6567 stopped = true;
6568 break;
6569 }
6570 }
6571 if stopped {
6572 break 'decode;
6573 }
6574 continue 'decode;
6575 }
6576 }
6577 self.graph_want_logits = fuse_lm;
6578 let mut t_fwd = t_next;
6584 let pure_greedy = self.sampler_config.temperature < 1e-6
6585 && self.sampler_config.repetition_penalty == 1.0
6586 && self.sampler_config.presence_penalty == 0.0
6587 && self.sampler_config.suppress_tokens.is_empty();
6588 let burst_k = std::env::var("CMF_MULTISTEP")
6593 .ok()
6594 .and_then(|v| v.parse::<usize>().ok())
6595 .unwrap_or(0);
6596 if pure_greedy
6597 && burst_k >= 1
6598 && fuse_lm
6599 && task_mask.is_none()
6600 && router.is_none()
6601 && !trace_on
6602 && !self.confidence_on
6603 && std::env::var_os("CMF_FORCE_IDS").is_none()
6608 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6609 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6610 {
6611 let mut stopped = false;
6612 loop {
6613 if self
6618 .cancel
6619 .swap(false, std::sync::atomic::Ordering::Relaxed)
6620 {
6621 finish_reason = "cancelled".to_string();
6622 stopped = true;
6623 break;
6624 }
6625 let room = max_tokens.saturating_sub(generated);
6626 if room <= 2 {
6627 break;
6628 }
6629 let k = burst_k.min(room - 1);
6630 if k < 1 {
6631 break;
6632 }
6633 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6634 if self
6635 .graph_failed
6636 .swap(false, std::sync::atomic::Ordering::Relaxed)
6637 {
6638 self.finish_generation(&mut mtp, &mut router, true);
6639 return Err(
6640 "GPU token graph failed during greedy burst".to_string()
6641 );
6642 }
6643 break;
6644 };
6645 next_pos += k;
6646 for &id in &ids {
6647 if !commit!(id) {
6648 stopped = true;
6649 break;
6650 }
6651 }
6652 if stopped {
6653 break;
6654 }
6655 t_fwd = *ids.last().unwrap();
6656 }
6657 if stopped {
6658 break 'decode;
6659 }
6660 }
6661 #[cfg(target_os = "macos")]
6671 if graph_spec
6672 && spec_watchdog_off
6673 && next_pos > 0
6674 && self.mtp_graph_mode == Some(true)
6675 && crate::gpu::q1_force()
6676 {
6677 if let Some(m) = mtp.as_mut() {
6678 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6679 }
6680 }
6681 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6682 next_pos += 1;
6683 if let Some(r) = &mut router {
6686 let phi = self.dyn_phi_ema.clone();
6687 let decision = r.step(&phi, generated);
6688 if let Some(new_active) = decision {
6689 let _ = self.set_active_skill(new_active);
6690 }
6691 if trace_on {
6694 if let Some(last) = traces.last_mut() {
6695 let e = r.last_best_e();
6696 last.recon = e.is_finite().then_some(e);
6697 last.switched = decision.is_some();
6698 }
6699 }
6700 }
6701 }
6702 }
6703 }
6704
6705 let cancelled = finish_reason == "cancelled";
6706 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6710 if mimo_spec {
6711 if let Some(st) = self.mimo_mtp.as_ref() {
6712 let line = st.stats.line();
6713 tracing::info!("{line}");
6714 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6715 eprintln!("{line}");
6716 }
6717 }
6718 }
6719 self.finish_generation(&mut mtp, &mut router, cancelled);
6720
6721 let output_ids = &all_ids[input_ids.len()..];
6722 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6726 let consumed = std::mem::take(&mut all_ids);
6731 if dyn_switched {
6732 self.clear_sequence_state();
6733 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6734 self.clear_history();
6735 } else {
6736 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6737 }
6738 all_ids = consumed;
6739 let output_ids = &all_ids[input_ids.len()..];
6740 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6742 Ok(GenerateResult {
6743 text: self.tokenizer.decode(output_ids),
6744 token_ids: output_ids.to_vec(),
6745 prompt_tokens: input_ids.len(),
6746 tokens_generated: generated,
6747 finish_reason,
6748 mtp_drafted: drafted,
6749 mtp_accepted: accepted,
6750 token_confidence: confidence,
6751 traces,
6752 })
6753 }
6754
6755 fn mtp_step(
6759 &mut self,
6760 m: &mut MtpModule,
6761 hidden: &[f32],
6762 next_token: u32,
6763 position: usize,
6764 ) -> u32 {
6765 self.mtp_step_h(m, hidden, next_token, position).0
6766 }
6767
6768 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6772 use std::sync::Mutex;
6773 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6774 let mut t = T.lock().unwrap();
6775 if t.len() <= depth {
6776 t.resize(depth + 1, (0, 0));
6777 }
6778 t[depth].0 += 1;
6779 t[depth].1 += prefix_ok as u64;
6780 if depth == 0 && t[0].0 % 128 == 0 {
6781 let line: Vec<String> = t
6782 .iter()
6783 .enumerate()
6784 .map(|(d, (n, k))| {
6785 format!(
6786 "d{}={:.0}%({n})",
6787 d + 1,
6788 100.0 * *k as f64 / (*n).max(1) as f64
6789 )
6790 })
6791 .collect();
6792 eprintln!("mtp-chain: {}", line.join(" "));
6793 }
6794 }
6795
6796 fn mtp_step_hl(
6804 &mut self,
6805 m: &mut MtpModule,
6806 hidden: &[f32],
6807 next_token: u32,
6808 position: usize,
6809 ) -> (Vec<f32>, Vec<f32>) {
6810 #[cfg(target_os = "macos")]
6815 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6816 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6817 self.mtp_graph_mode = Some(true);
6818 return r;
6819 }
6820 if self.mtp_graph_mode == Some(true) {
6821 tracing::error!("mtp Metal graph failed after admission");
6822 self.clear_sequence_state();
6823 self.graph_failed
6824 .store(true, std::sync::atomic::Ordering::Relaxed);
6825 self.cancel
6826 .store(true, std::sync::atomic::Ordering::Relaxed);
6827 return (Vec::new(), Vec::new());
6828 }
6829 self.mtp_graph_mode = Some(false);
6830 }
6831 #[cfg(feature = "gpu")]
6832 if self.mtp_graph_mode != Some(false) {
6833 if !self.mtp_graph_ok(m) {
6834 if self.mtp_graph_mode == Some(true) {
6835 tracing::error!("mtp graph became unavailable after admission");
6840 self.clear_sequence_state();
6841 self.graph_failed
6842 .store(true, std::sync::atomic::Ordering::Relaxed);
6843 self.cancel
6844 .store(true, std::sync::atomic::Ordering::Relaxed);
6845 return (Vec::new(), Vec::new());
6846 }
6847 self.mtp_graph_mode = Some(false);
6848 } else {
6849 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6850 self.mtp_graph_mode = Some(true);
6851 return r;
6852 }
6853 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6854 return (Vec::new(), Vec::new());
6861 }
6862 tracing::error!("mtp graph failed or declined after admission");
6866 self.clear_sequence_state();
6867 self.graph_failed
6868 .store(true, std::sync::atomic::Ordering::Relaxed);
6869 self.cancel
6870 .store(true, std::sync::atomic::Ordering::Relaxed);
6871 return (Vec::new(), Vec::new());
6872 }
6873 }
6874 let e = self.embed_single(next_token);
6878 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6879 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6880 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6881 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6882 let mut x = vec![0.0f32; self.hidden_size];
6883 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6884
6885 let lw = &m.layer;
6887 inference::rms_norm_into(
6888 &x,
6889 &lw.input_norm,
6890 self.rms_eps,
6891 self.norm_style,
6892 &mut self.ws.n1,
6893 );
6894 let attn = match &lw.attn {
6895 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6897 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6898 AttnKind::Full {
6899 wq,
6900 wk,
6901 wv,
6902 wo,
6903 q_norm,
6904 k_norm,
6905 output_gate,
6906 softplus_gate,
6907 bias,
6908 } => {
6909 let mut cfg = self.attn_cfg(position);
6910 cfg.q_norm = q_norm.as_deref();
6911 cfg.k_norm = k_norm.as_deref();
6912 cfg.output_gate = *output_gate;
6913 cfg.softplus_gate = softplus_gate
6914 .as_ref()
6915 .map(|(gate, per_head)| (gate, *per_head));
6916 cfg.bias = bias
6917 .as_ref()
6918 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6919 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6920 }
6921 AttnKind::Linear(_)
6922 | AttnKind::LinearGdn(_)
6923 | AttnKind::ShortConv(_)
6924 | AttnKind::Bounded(_) => {
6925 unreachable!("MTP block is full attention")
6926 }
6927 };
6928 for (i, &a) in attn.iter().enumerate() {
6929 x[i] += a;
6930 }
6931 inference::rms_norm_into(
6932 &x,
6933 &lw.post_norm,
6934 self.rms_eps,
6935 self.norm_style,
6936 &mut self.ws.p1,
6937 );
6938 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6939 for (i, &f) in ffn.iter().enumerate() {
6940 x[i] += f;
6941 }
6942
6943 inference::rms_norm_into(
6944 &x,
6945 &m.final_norm,
6946 self.rms_eps,
6947 self.norm_style,
6948 &mut self.ws.n1,
6949 );
6950 let lg = self.lm_head_forward(&self.ws.n1);
6951 (lg, x)
6952 }
6953
6954 fn mtp_step_h(
6956 &mut self,
6957 m: &mut MtpModule,
6958 hidden: &[f32],
6959 next_token: u32,
6960 position: usize,
6961 ) -> (u32, Vec<f32>) {
6962 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6963 let draft = sampler::argmax(&lg);
6964 attention::recycle_buf(&mut lg);
6965 (draft, x)
6966 }
6967
6968 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6974 match trial {
6975 SpecTrial::Spec { t0, gen0, rounds } => {
6976 let rounds = rounds + 1;
6977 if mon.repeated_zero_acceptance() {
6985 tracing::info!(
6986 "speculation trial: no accepted drafts in two rounds — timing plain"
6987 );
6988 return SpecTrial::Plain {
6989 t0: std::time::Instant::now(),
6990 gen0: generated,
6991 };
6992 }
6993 if rounds >= 5 {
6994 if mon.plain_ms > 0.0 {
6995 let keep = mon.pays();
6996 mon.fails = 0;
6997 tracing::info!(
6998 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6999 mon.tokens,
7000 mon.round_ms,
7001 mon.plain_ms,
7002 if keep { "speculating" } else { "plain" }
7003 );
7004 SpecTrial::Decided {
7005 spec: keep,
7006 recheck_at: if keep { usize::MAX } else { generated + 128 },
7007 }
7008 } else if mon.pays() {
7009 mon.fails = 0;
7014 tracing::info!(
7015 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
7016 mon.tokens,
7017 mon.round_ms,
7018 );
7019 SpecTrial::Decided {
7020 spec: true,
7021 recheck_at: usize::MAX,
7022 }
7023 } else {
7024 SpecTrial::Plain {
7025 t0: std::time::Instant::now(),
7026 gen0: generated,
7027 }
7028 }
7029 } else {
7030 SpecTrial::Spec { t0, gen0, rounds }
7031 }
7032 }
7033 SpecTrial::Decided { spec: true, .. } => {
7034 if mon.pays() {
7035 mon.fails = 0;
7036 trial
7037 } else {
7038 mon.fails += 1;
7039 if mon.fails >= 4 {
7040 if mon.plain_ms <= 0.0 {
7041 tracing::info!(
7045 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
7046 mon.tokens,
7047 mon.round_ms,
7048 );
7049 return SpecTrial::Plain {
7050 t0: std::time::Instant::now(),
7051 gen0: generated,
7052 };
7053 }
7054 tracing::info!(
7055 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
7056 mon.tokens,
7057 mon.round_ms,
7058 mon.plain_ms
7059 );
7060 SpecTrial::Decided {
7061 spec: false,
7062 recheck_at: generated + 128,
7063 }
7064 } else {
7065 trial
7066 }
7067 }
7068 }
7069 other => other,
7070 }
7071 }
7072
7073 fn mtp_kv_id(&self) -> u64 {
7076 self.graph_kv_id | (1u64 << 40)
7077 }
7078
7079 const MTP_LAYER_BASE: usize = 0;
7084
7085 #[cfg(feature = "gpu")]
7092 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7093 self.mtp_graph_mode != Some(true)
7094 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7095 }
7096
7097 #[cfg(feature = "gpu")]
7103 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7104 let mut ok = true;
7105 let mut expected = false;
7106 for li in 0..self.num_layers {
7107 if matches!(
7108 self.weights.layers[self.phys_layer(li)].attn,
7109 AttnKind::Full { .. }
7110 ) {
7111 expected = true;
7112 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7113 }
7114 }
7115 !expected || ok
7116 }
7117
7118 fn graph_gdn_layer_count(&self) -> usize {
7122 (0..self.num_layers)
7123 .filter(|&li| {
7124 matches!(
7125 &self.weights.layers[self.phys_layer(li)].attn,
7126 AttnKind::LinearGdn(_)
7127 )
7128 })
7129 .count()
7130 }
7131
7132 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7135 let e = self.embed_single(next_token);
7136 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7137 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7138 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7139 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7140 let mut x = vec![0.0f32; self.hidden_size];
7141 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7142 x
7143 }
7144
7145 #[cfg(feature = "gpu")]
7148 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7149 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7150 return false;
7151 }
7152 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7153 || !crate::gpu::enabled_here()
7154 || self.attn_softcap > 0.0
7155 || self.attention_heads_per_layer.is_some()
7156 || self.v_head_dim.is_some()
7159 {
7160 return false;
7161 }
7162 matches!(
7163 &m.layer.attn,
7164 AttnKind::Full {
7165 softplus_gate: None,
7166 ..
7167 }
7168 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7169 }
7170
7171 #[cfg(feature = "gpu")]
7175 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7176 if !self.mtp_block_graph_ok(m) {
7177 return false;
7178 }
7179 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7180 return false;
7181 };
7182 let FfnKind::Dense(d) = &m.layer.ffn else {
7183 return false;
7184 };
7185 d.segs.is_empty()
7186 && wq.graph_weight().is_some()
7187 && wk.graph_weight().is_some()
7188 && wv.graph_weight().is_some()
7189 && wo.graph_weight().is_some()
7190 && d.gate_proj.graph_weight().is_some()
7191 && d.up_proj.graph_weight().is_some()
7192 && d.down_proj.graph_weight().is_some()
7193 && self.weights.lm_head.graph_weight().is_some()
7194 }
7195
7196 #[cfg(feature = "gpu")]
7202 fn mtp_step_graph(
7203 &mut self,
7204 m: &mut MtpModule,
7205 hidden: &[f32],
7206 next_token: u32,
7207 position: usize,
7208 ) -> Option<(Vec<f32>, Vec<f32>)> {
7209 if !self.mtp_graph_ok(m) {
7210 return None;
7211 }
7212 let lw = &m.layer;
7213 let AttnKind::Full {
7214 wq,
7215 wk,
7216 wv,
7217 wo,
7218 q_norm,
7219 k_norm,
7220 output_gate,
7221 softplus_gate,
7222 bias,
7223 } = &lw.attn
7224 else {
7225 return None;
7226 };
7227 if softplus_gate.is_some() {
7228 return None;
7229 }
7230 let FfnKind::Dense(d) = &lw.ffn else {
7231 return None;
7232 };
7233 if !d.segs.is_empty() {
7234 return None; }
7236 let mut x = self.mtp_block_input(m, hidden, next_token);
7239 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7240 let (_, i, kind, rs) = t.graph_weight()?;
7241 Some(crate::gpu::GraphW {
7242 idx: i,
7243 kind,
7244 row_scale: rs,
7245 data: &[],
7246 prism: crate::gpu::GraphPrismOp::None,
7247 affine: false,
7248 })
7249 }
7250 let (model, _, _, _) = wq.graph_weight()?;
7251 let model = model.clone();
7252 let (lm_gw, lm_rows) = {
7253 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7254 let rows = if kind == 6 {
7258 self.draft_head_rows(self.weights.lm_head.rows())
7259 } else {
7260 self.weights.lm_head.rows()
7261 };
7262 (
7263 crate::gpu::GraphW {
7264 idx: i,
7265 kind,
7266 row_scale: rs,
7267 data: &[],
7268 prism: crate::gpu::GraphPrismOp::None,
7269 affine: false,
7270 },
7271 rows,
7272 )
7273 };
7274 let layer = crate::gpu::GraphLayer {
7275 input_norm: &lw.input_norm,
7276 attn: crate::gpu::GraphAttn::Full {
7277 wq: gw(wq)?,
7278 wk: gw(wk)?,
7279 wv: gw(wv)?,
7280 wo: gw(wo)?,
7281 q_norm: q_norm.as_deref(),
7282 k_norm: k_norm.as_deref(),
7283 late_qk_norm: self.qk_norm_after_rope,
7284 bias: bias
7285 .as_ref()
7286 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7287 output_gate: *output_gate,
7288 cpu_k: m.kv.k_heads(),
7289 cpu_v: m.kv.v_heads(),
7290 cpu_base: m.kv.base(),
7291 geom: None,
7292 head_gate: None,
7293 },
7294 post_norm: &lw.post_norm,
7295 attn_out_norm: None,
7296 ffn_out_norm: None,
7297 layer_scale: None,
7298 ffn: crate::gpu::GraphFfn::Dense {
7299 gate: gw(&d.gate_proj)?,
7300 up: gw(&d.up_proj)?,
7301 down: gw(&d.down_proj)?,
7302 act: d.act.graph_act()?,
7303 },
7304 };
7305 let nh = self.num_heads;
7306 let (nkv, hd, rd) = self.layer_geom(0);
7307 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7308 let mut logits = Vec::new();
7309 let ok = crate::gpu::forward_token_graph(
7310 &model,
7311 self.mtp_kv_id(),
7312 std::slice::from_ref(&layer),
7313 &[None],
7314 self.o1_epoch,
7315 &self.inv_freq,
7316 &mut x,
7317 nh,
7318 nkv,
7319 hd,
7320 self.attn_scale,
7321 rd,
7322 self.hidden_size,
7323 d.gate_proj.rows(),
7327 position,
7328 self.kv_cache.max_seq_len,
7329 gemma,
7330 self.rms_eps as f32,
7331 Some((&lm_gw, lm_rows)),
7332 &m.final_norm,
7333 &mut logits,
7334 &[],
7335 1,
7336 None,
7337 None,
7338 None,
7339 Self::MTP_LAYER_BASE,
7340 true,
7341 );
7342 match ok {
7343 crate::gpu::TokenGraphOutcome::Completed => {}
7344 crate::gpu::TokenGraphOutcome::Declined => return None,
7345 crate::gpu::TokenGraphOutcome::Failed => {
7346 self.clear_sequence_state();
7350 self.graph_failed
7351 .store(true, std::sync::atomic::Ordering::Relaxed);
7352 self.cancel
7353 .store(true, std::sync::atomic::Ordering::Relaxed);
7354 return None;
7355 }
7356 }
7357 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7361 Some((logits, x))
7362 }
7363
7364 #[cfg(feature = "gpu")]
7372 fn mtp_warm_graph(
7373 &mut self,
7374 m: &mut MtpModule,
7375 pairs: &[(&[f32], u32)],
7376 first_pos: usize,
7377 ) -> crate::gpu::BatchGraphOutcome {
7378 if pairs.is_empty() {
7379 return crate::gpu::BatchGraphOutcome::Completed;
7380 }
7381 if !self.mtp_block_graph_ok(m) {
7382 return crate::gpu::BatchGraphOutcome::Declined;
7383 }
7384 let hs = self.hidden_size;
7385 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7388 for (h, t) in pairs {
7389 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7390 }
7391 let lw = &m.layer;
7392 let AttnKind::Full {
7393 wq,
7394 wk,
7395 wv,
7396 wo,
7397 q_norm,
7398 k_norm,
7399 output_gate,
7400 bias,
7401 ..
7402 } = &lw.attn
7403 else {
7404 return crate::gpu::BatchGraphOutcome::Declined;
7405 };
7406 let FfnKind::Dense(d) = &lw.ffn else {
7407 return crate::gpu::BatchGraphOutcome::Declined;
7408 };
7409 if !d.segs.is_empty() {
7410 return crate::gpu::BatchGraphOutcome::Declined; }
7412 let (
7415 AttnKind::Full {
7416 softplus_gate: None,
7417 ..
7418 },
7419 Some(gact),
7420 ) = (&lw.attn, d.act.graph_act())
7421 else {
7422 return crate::gpu::BatchGraphOutcome::Declined;
7423 };
7424 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7425 let (_, i, kind, rs) = t.graph_weight()?;
7426 Some(crate::gpu::GraphW {
7427 idx: i,
7428 kind,
7429 row_scale: rs,
7430 data: &[],
7431 prism: crate::gpu::GraphPrismOp::None,
7432 affine: false,
7433 })
7434 }
7435 let Some((model, _, _, _)) = wq.graph_weight() else {
7436 return crate::gpu::BatchGraphOutcome::Declined;
7437 };
7438 let model = model.clone();
7439 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7440 gw(wq),
7441 gw(wk),
7442 gw(wv),
7443 gw(wo),
7444 gw(&d.gate_proj),
7445 gw(&d.up_proj),
7446 gw(&d.down_proj),
7447 ) else {
7448 return crate::gpu::BatchGraphOutcome::Declined;
7449 };
7450 let layer = crate::gpu::GraphLayer {
7451 input_norm: &lw.input_norm,
7452 attn: crate::gpu::GraphAttn::Full {
7453 wq: gwq,
7454 wk: gwk,
7455 wv: gwv,
7456 wo: gwo,
7457 q_norm: q_norm.as_deref(),
7458 k_norm: k_norm.as_deref(),
7459 late_qk_norm: self.qk_norm_after_rope,
7460 bias: bias
7461 .as_ref()
7462 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7463 output_gate: *output_gate,
7464 cpu_k: m.kv.k_heads(),
7465 cpu_v: m.kv.v_heads(),
7466 cpu_base: m.kv.base(),
7467 geom: None,
7468 head_gate: None,
7469 },
7470 post_norm: &lw.post_norm,
7471 attn_out_norm: None,
7472 ffn_out_norm: None,
7473 layer_scale: None,
7474 ffn: crate::gpu::GraphFfn::Dense {
7475 gate: gg,
7476 up: gu,
7477 down: gd,
7478 act: gact,
7479 },
7480 };
7481 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7482 let nh = self.num_heads;
7483 let (nkv, hd, rd) = self.layer_geom(0);
7484 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7485 crate::gpu::forward_batch_graph(
7486 &model,
7487 self.mtp_kv_id(),
7488 std::slice::from_ref(&layer),
7489 &self.inv_freq,
7490 &mut hiddens,
7491 nh,
7492 nkv,
7493 hd,
7494 rd,
7495 hs,
7496 d.gate_proj.rows(),
7499 &positions,
7500 self.kv_cache.max_seq_len,
7501 gemma,
7502 self.rms_eps as f32,
7503 self.attn_scale,
7504 pairs.len(),
7505 &[],
7506 0,
7507 None,
7508 None,
7509 )
7510 }
7511
7512 #[cfg(feature = "gpu")]
7519 fn mtp_warm_graph_fallback(
7520 &mut self,
7521 m: &mut MtpModule,
7522 pairs: &[(&[f32], u32)],
7523 first_pos: usize,
7524 ) -> bool {
7525 if pairs.is_empty() {
7526 return true;
7527 }
7528 let graphable = self.mtp_block_graph_ok(m);
7529 if !graphable {
7530 if self.mtp_graph_mode == Some(true) {
7534 return false;
7535 }
7536 self.mtp_graph_mode = Some(false);
7537 for (j, (h, t)) in pairs.iter().enumerate() {
7538 self.mtp_warm(m, h, *t, first_pos + j);
7539 }
7540 return true;
7541 }
7542
7543 for (j, (h, t)) in pairs.iter().enumerate() {
7548 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7549 return false;
7550 }
7551 }
7552 self.mtp_graph_mode = Some(true);
7553 true
7554 }
7555
7556 #[cfg(feature = "gpu")]
7561 fn mtp_warm_prefill_pairs(
7562 &mut self,
7563 m: &mut MtpModule,
7564 pairs: &[(&[f32], u32)],
7565 first_pos: usize,
7566 ) -> Result<(), &'static str> {
7567 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7572 if self.mtp_graph_mode == Some(true) {
7573 return Err("MTP token graph became unavailable after admission");
7574 }
7575 self.mtp_graph_mode = Some(false);
7576 for (j, (h, t)) in pairs.iter().enumerate() {
7577 self.mtp_warm(m, h, *t, first_pos + j);
7578 }
7579 return Ok(());
7580 }
7581 match self.mtp_warm_graph(m, pairs, first_pos) {
7582 crate::gpu::BatchGraphOutcome::Completed => {
7583 if !pairs.is_empty() {
7584 self.mtp_graph_mode = Some(true);
7585 }
7586 Ok(())
7587 }
7588 crate::gpu::BatchGraphOutcome::Declined => {
7589 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7590 Ok(())
7591 } else {
7592 Err("MTP warm-up fallback failed after device admission")
7593 }
7594 }
7595 crate::gpu::BatchGraphOutcome::Failed => {
7596 Err("MTP warm batch graph failed after admission")
7597 }
7598 }
7599 }
7600
7601 #[cfg(not(feature = "gpu"))]
7602 fn mtp_warm_prefill_pairs(
7603 &mut self,
7604 m: &mut MtpModule,
7605 pairs: &[(&[f32], u32)],
7606 first_pos: usize,
7607 ) -> Result<(), &'static str> {
7608 for (j, (h, t)) in pairs.iter().enumerate() {
7609 self.mtp_warm(m, h, *t, first_pos + j);
7610 }
7611 Ok(())
7612 }
7613
7614 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7618 let e = self.embed_single(next_token);
7619 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7620 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7621 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7622 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7623 let mut x = vec![0.0f32; self.hidden_size];
7624 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7625 inference::rms_norm_into(
7626 &x,
7627 &m.layer.input_norm,
7628 self.rms_eps,
7629 self.norm_style,
7630 &mut self.ws.n1,
7631 );
7632 let attn = match &m.layer.attn {
7633 AttnKind::Full {
7634 wq,
7635 wk,
7636 wv,
7637 wo,
7638 q_norm,
7639 k_norm,
7640 output_gate,
7641 softplus_gate,
7642 bias,
7643 } => {
7644 let mut cfg = self.attn_cfg(position);
7645 cfg.q_norm = q_norm.as_deref();
7646 cfg.k_norm = k_norm.as_deref();
7647 cfg.output_gate = *output_gate;
7648 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7649 cfg.bias = bias
7650 .as_ref()
7651 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7652 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7653 }
7654 _ => return,
7655 };
7656 let _ = attn;
7657 }
7658
7659 #[cfg(feature = "gpu")]
7666 #[allow(clippy::too_many_arguments)]
7667 fn graph_spec_step(
7668 &mut self,
7669 m: &mut MtpModule,
7670 hidden: &[f32],
7671 t_next: u32,
7672 next_pos: usize,
7673 drafted: &mut usize,
7674 accepted: &mut usize,
7675 all_ids: &mut Vec<u32>,
7679 room: usize,
7684 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7685 #[cfg(target_os = "macos")]
7696 let metal_native = crate::gpu::q1_force();
7697 #[cfg(not(target_os = "macos"))]
7698 let metal_native = false;
7699 #[cfg(feature = "gpu")]
7700 let k_default = if metal_native {
7701 7
7704 } else if crate::gpu_wgpu::verify_i8_on() {
7705 5
7706 } else {
7707 4
7708 };
7709 #[cfg(not(feature = "gpu"))]
7710 let k_default = 4;
7711 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7712 .ok()
7713 .and_then(|v| v.parse().ok())
7714 .filter(|&v| (1..=8).contains(&v));
7715 let (k_start, k_max) = if metal_native {
7720 (7, 7)
7721 } else {
7722 (3, k_default.max(5))
7723 };
7724 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7725 let k_spec = k_full.min(room).max(1);
7726 let k_capped = k_spec < k_full;
7729 if next_pos == 0 {
7730 return None;
7731 }
7732 let t_round = std::time::Instant::now();
7733 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7749 let sub0 = subs();
7750 let cfg = self.sampler_config.clone();
7755 let penalized = !(cfg.repetition_penalty == 1.0
7756 && cfg.presence_penalty == 0.0
7757 && cfg.suppress_tokens.is_empty());
7758 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7763 let sampling = cfg.temperature >= 1e-6;
7764 let sparse = sampling && sampler::sparse_ok(&cfg);
7770 let base_len = all_ids.len();
7771 if sampling && !sparse && self.spec_q.len() < k_spec {
7772 self.spec_q.resize_with(k_spec, Vec::new);
7773 }
7774 if sparse && self.spec_qs.len() < k_spec {
7775 self.spec_qs.resize_with(k_spec, Vec::new);
7776 }
7777 let mut drafts = Vec::with_capacity(k_spec);
7782 let mut hx = hidden.to_vec();
7783 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7786 spec_stamp("pro");
7787 #[cfg(target_os = "macos")]
7793 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7794 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7795 Ok(ids) => {
7796 self.mtp_graph_mode = Some(true);
7797 drafts = ids;
7798 }
7799 Err(true) => {
7800 tracing::error!("mtp Metal draft chain failed after commit");
7801 self.clear_sequence_state();
7802 self.graph_failed
7803 .store(true, std::sync::atomic::Ordering::Relaxed);
7804 self.cancel
7805 .store(true, std::sync::atomic::Ordering::Relaxed);
7806 return None;
7807 }
7808 Err(false) => {}
7809 }
7810 }
7811 for j in drafts.len()..k_spec {
7812 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7813 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7814 if spec_dbg {
7815 let saved = self.mtp_graph_mode;
7816 self.mtp_graph_mode = Some(false);
7817 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7818 self.mtp_graph_mode = saved;
7819 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7820 return None;
7821 }
7822 m.kv.truncate_last(1);
7823 dbg_ref = Some(r);
7824 }
7825 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7826 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7827 return None;
7828 }
7829 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7830 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7831 let dl = lg
7832 .iter()
7833 .zip(&lg_cpu)
7834 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7835 let dh = hj
7836 .iter()
7837 .zip(&h_cpu)
7838 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7839 eprintln!(
7840 "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 {}",
7841 next_pos - 1 + j,
7842 sampler::argmax(&lg_cpu),
7843 sampler::argmax(&lg),
7844 n(&h_cpu),
7845 n(&hj),
7846 m.kv.seq_len
7847 );
7848 }
7849 let dj = if sparse {
7850 let mut q = std::mem::take(&mut self.spec_qs[j]);
7851 let ok = sampler::sparse_distribution_into(
7852 &lg,
7853 &cfg,
7854 all_ids,
7855 &mut self.sampler_scratch,
7856 self.pool.as_deref(),
7857 &mut q,
7858 );
7859 let d = if ok {
7860 sampler::draw_sparse(&q, &mut self.rng)
7861 } else {
7862 let t = sampler::argmax(&lg);
7864 q.clear();
7865 q.push((t, 1.0));
7866 t
7867 };
7868 self.spec_qs[j] = q;
7869 all_ids.push(d);
7870 d
7871 } else if sampling {
7872 let mut q = std::mem::take(&mut self.spec_q[j]);
7873 sampler::distribution_into(
7874 &lg,
7875 &cfg,
7876 all_ids,
7877 &mut self.sampler_scratch,
7878 self.pool.as_deref(),
7879 &mut q,
7880 );
7881 let d = sampler::draw(&q, &mut self.rng);
7882 self.spec_q[j] = q;
7883 all_ids.push(d); d
7885 } else if greedy_pen {
7886 let d = sampler::argmax_penalized(
7887 &lg,
7888 &cfg,
7889 all_ids,
7890 &mut self.sampler_scratch,
7891 self.pool.as_deref(),
7892 );
7893 all_ids.push(d);
7894 d
7895 } else {
7896 sampler::argmax(&lg)
7897 };
7898 attention::recycle_buf(&mut lg);
7899 drafts.push(dj);
7900 hx = hj;
7901 spec_stamp("d.pick");
7902 }
7903 all_ids.truncate(base_len);
7904 *drafted += k_spec;
7905 let t_draft = t_round.elapsed();
7906 let sub_draft = subs();
7907 let b = k_spec + 1;
7910 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7911 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7912 let e = self.embed_single(t);
7913 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7914 }
7915 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7916 spec_stamp("v.emb");
7917 let (lm_gw, lm_rows) = {
7918 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7919 (
7920 crate::gpu::GraphW {
7921 idx: i,
7922 kind,
7923 row_scale: rs,
7924 data: &[],
7925 prism: crate::gpu::GraphPrismOp::None,
7926 affine: false,
7927 },
7928 self.weights.lm_head.rows(),
7929 )
7930 };
7931 let mut logits = Vec::new();
7932 let final_norm = self.weights.final_norm.clone();
7933 #[cfg(target_os = "macos")]
7942 let greedy_dev = metal_native
7943 && !sampling
7944 && !greedy_pen
7945 && !self.confidence_on
7946 && self.final_softcap.is_none()
7947 && self.vocab_size == lm_rows
7953 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7954 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7955 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7956 #[cfg(not(target_os = "macos"))]
7957 let greedy_dev = false;
7958 let mut dev_ids: Vec<u32> = Vec::new();
7959 #[cfg(target_os = "macos")]
7960 let verify_outcome = if metal_native {
7961 let lm = self.weights.lm_head.q1_parts()?;
7962 let n_score = self.vocab_size.min(lm_rows);
7963 self.try_batch_graph_metal(
7964 &mut hiddens,
7965 &positions,
7966 b,
7967 Some((lm, &final_norm, &mut logits)),
7968 if greedy_dev {
7969 Some((n_score, &mut dev_ids))
7970 } else {
7971 None
7972 },
7973 )
7974 } else {
7975 self.try_batch_graph_wgpu(
7976 &mut hiddens,
7977 &positions,
7978 b,
7979 Some(crate::gpu::SpecTail {
7980 lm: lm_gw,
7981 lm_rows,
7982 final_norm: &final_norm,
7983 logits_out: &mut logits,
7984 pick: None,
7985 }),
7986 )
7987 };
7988 #[cfg(not(target_os = "macos"))]
7989 let verify_outcome = self.try_batch_graph_wgpu(
7990 &mut hiddens,
7991 &positions,
7992 b,
7993 Some(crate::gpu::SpecTail {
7994 lm: lm_gw,
7995 lm_rows,
7996 final_norm: &final_norm,
7997 logits_out: &mut logits,
7998 pick: None,
7999 }),
8000 );
8001 match verify_outcome {
8002 crate::gpu::BatchGraphOutcome::Completed => {}
8003 crate::gpu::BatchGraphOutcome::Declined => {
8004 m.kv.truncate_last(k_spec);
8008 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
8009 self.clear_sequence_state();
8010 self.graph_failed
8011 .store(true, std::sync::atomic::Ordering::Relaxed);
8012 self.cancel
8013 .store(true, std::sync::atomic::Ordering::Relaxed);
8014 tracing::error!("MTP graph mirror rewind failed after verify decline");
8015 }
8016 return None;
8017 }
8018 crate::gpu::BatchGraphOutcome::Failed => {
8019 self.clear_sequence_state();
8023 self.graph_failed
8024 .store(true, std::sync::atomic::Ordering::Relaxed);
8025 self.cancel
8026 .store(true, std::sync::atomic::Ordering::Relaxed);
8027 tracing::error!("MTP verify batch graph failed after admission");
8028 return None;
8029 }
8030 }
8031 #[cfg(target_os = "macos")]
8037 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
8038 let snap: Vec<Vec<f32>> = self
8039 .kv_cache
8040 .layers
8041 .iter()
8042 .map(|l| l.linear_state.clone())
8043 .collect();
8044 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8045 let toks: Vec<u32> = std::iter::once(t_next)
8046 .chain(drafts.iter().copied())
8047 .collect();
8048 let want_save = self.graph_want_logits;
8049 self.graph_want_logits = false;
8050 for (i, &t) in toks.iter().enumerate() {
8051 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8052 let _ = self.graph_logits.take();
8053 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
8057 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
8058 }
8059 let ref_lg = self.logits_from_hidden(&hi);
8060 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
8061 let ra = sampler::argmax(&ref_lg);
8062 let va = sampler::argmax(row);
8063 let mut md = 0f32;
8064 let mut rms = 0f64;
8065 for j in 0..lm_rows.min(ref_lg.len()) {
8066 let d = (ref_lg[j] - row[j]).abs();
8067 md = md.max(d);
8068 rms += (d as f64) * (d as f64);
8069 }
8070 let mut hd = 0f32;
8071 for j in 0..self.hidden_size {
8072 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8073 }
8074 eprintln!(
8075 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8076 next_pos + i,
8077 if ra == va { "OK" } else { "MISMATCH" },
8078 (rms / lm_rows as f64).sqrt()
8079 );
8080 }
8081 self.graph_want_logits = want_save;
8082 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8085 if l.linear_state.len() == st.len() {
8086 l.linear_state.copy_from_slice(&st);
8087 } else {
8088 l.linear_state = st;
8089 }
8090 }
8091 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8092 let extra = l.seq_len.saturating_sub(n0);
8093 if extra > 0 {
8094 l.truncate_last(extra);
8095 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8096 }
8097 }
8098 }
8099 let t_verify = t_round.elapsed();
8100 let sub_verify = subs();
8101 let mut a = 0usize;
8106 let mut forced: Option<u32> = None;
8107 let ids: Vec<u32> = if sparse {
8108 let mut p = std::mem::take(&mut self.spec_ps);
8109 let mut res = std::mem::take(&mut self.spec_ress);
8110 while a < k_spec {
8111 let ok = sampler::sparse_distribution_into(
8112 &logits[a * lm_rows..(a + 1) * lm_rows],
8113 &cfg,
8114 all_ids,
8115 &mut self.sampler_scratch,
8116 self.pool.as_deref(),
8117 &mut p,
8118 );
8119 if !ok {
8120 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8121 p.clear();
8122 p.push((t, 1.0));
8123 }
8124 match sampler::spec_accept_or_correct_sparse(
8125 &p,
8126 &self.spec_qs[a],
8127 drafts[a],
8128 &mut self.rng,
8129 &mut res,
8130 ) {
8131 None => {
8132 all_ids.push(drafts[a]);
8133 a += 1;
8134 }
8135 Some(c) => {
8136 forced = Some(c);
8137 break;
8138 }
8139 }
8140 }
8141 all_ids.truncate(base_len);
8142 self.spec_ps = p;
8143 self.spec_ress = res;
8144 drafts.clone()
8145 } else if sampling {
8146 let mut p = std::mem::take(&mut self.spec_p);
8147 let mut res = std::mem::take(&mut self.spec_res);
8148 while a < k_spec {
8149 sampler::distribution_into(
8150 &logits[a * lm_rows..(a + 1) * lm_rows],
8151 &cfg,
8152 all_ids,
8153 &mut self.sampler_scratch,
8154 self.pool.as_deref(),
8155 &mut p,
8156 );
8157 match sampler::spec_accept_or_correct(
8158 &p,
8159 &self.spec_q[a],
8160 drafts[a],
8161 &mut self.rng,
8162 &mut res,
8163 self.pool.as_deref(),
8164 ) {
8165 None => {
8166 all_ids.push(drafts[a]);
8167 a += 1;
8168 }
8169 Some(c) => {
8170 forced = Some(c);
8171 break;
8172 }
8173 }
8174 }
8175 all_ids.truncate(base_len);
8176 self.spec_p = p;
8177 self.spec_res = res;
8178 drafts.clone()
8180 } else if greedy_pen {
8181 let mut ids: Vec<u32> = Vec::with_capacity(b);
8185 for i in 0..b {
8186 let t = sampler::argmax_penalized(
8187 &logits[i * lm_rows..(i + 1) * lm_rows],
8188 &cfg,
8189 all_ids,
8190 &mut self.sampler_scratch,
8191 self.pool.as_deref(),
8192 );
8193 ids.push(t);
8194 if i < k_spec && t == drafts[i] {
8195 all_ids.push(t);
8196 } else {
8197 break;
8198 }
8199 }
8200 all_ids.truncate(base_len);
8201 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8202 a += 1;
8203 }
8204 ids
8207 } else if greedy_dev && dev_ids.len() == b {
8208 let ids = std::mem::take(&mut dev_ids);
8209 while a < k_spec && ids[a] == drafts[a] {
8210 a += 1;
8211 }
8212 ids
8213 } else {
8214 if logits.len() < b * lm_rows {
8215 self.clear_sequence_state();
8218 self.graph_failed
8219 .store(true, std::sync::atomic::Ordering::Relaxed);
8220 self.cancel
8221 .store(true, std::sync::atomic::Ordering::Relaxed);
8222 tracing::error!("Metal verify returned neither logits nor argmax ids");
8223 return None;
8224 }
8225 let ids: Vec<u32> = (0..b)
8226 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8227 .collect();
8228 while a < k_spec && ids[a] == drafts[a] {
8229 a += 1;
8230 }
8231 ids
8232 };
8233 spec_stamp("acc");
8234 if spec_dbg {
8235 eprintln!(
8236 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8237 drafts, ids
8238 );
8239 }
8240 #[cfg(target_os = "macos")]
8244 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8245 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8246 {
8247 let snap: Vec<Vec<f32>> = self
8248 .kv_cache
8249 .layers
8250 .iter()
8251 .map(|l| l.linear_state.clone())
8252 .collect();
8253 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8254 let toks: Vec<u32> = std::iter::once(t_next)
8255 .chain(drafts.iter().copied())
8256 .collect();
8257 let want_save = self.graph_want_logits;
8258 self.graph_want_logits = false;
8259 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8260 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8261 let _ = self.graph_logits.take();
8262 }
8263 self.graph_want_logits = want_save;
8264 let plain_states: Vec<Vec<f32>> = self
8265 .kv_cache
8266 .layers
8267 .iter()
8268 .map(|l| l.linear_state.clone())
8269 .collect();
8270 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8271 let mut rows = Vec::new();
8272 for (li, (l, n0)) in self
8273 .kv_cache
8274 .layers
8275 .iter_mut()
8276 .zip(attn_lens.iter())
8277 .enumerate()
8278 {
8279 let extra = l.seq_len.saturating_sub(*n0);
8280 if extra > 0 {
8281 let mut kk = Vec::new();
8282 let mut vv = Vec::new();
8283 for g in 0..nkv {
8284 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8285 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8286 }
8287 rows.push((li, kk, vv));
8288 l.truncate_last(extra);
8289 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8290 }
8291 }
8292 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8293 if l.linear_state.len() == st.len() {
8294 l.linear_state.copy_from_slice(&st);
8295 } else {
8296 l.linear_state = st;
8297 }
8298 }
8299 Some((plain_states, rows))
8300 } else {
8301 None
8302 };
8303 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8304 #[cfg(target_os = "macos")]
8313 let mut warm_pending: Option<MetalWarmPending> = None;
8314 #[cfg(target_os = "macos")]
8315 if metal_native {
8316 m.kv.truncate_last(k_spec.saturating_sub(1));
8317 if self.mtp_graph_mode == Some(true) {
8318 crate::gpu_metal::kv_mirror_set_stored(
8321 self.mtp_kv_id(),
8322 Self::MTP_LAYER_BASE,
8323 m.kv.seq_len,
8324 );
8325 if !warm_off && a > 0 {
8326 let pairs: Vec<(&[f32], u32)> = (0..a)
8327 .map(|j| {
8328 (
8329 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8330 ids[j],
8331 )
8332 })
8333 .collect();
8334 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8335 }
8336 }
8337 spec_stamp("c.wsub");
8338 }
8339 #[cfg(target_os = "macos")]
8341 if metal_native {
8342 if !self.metal_verify_commit(a) {
8345 self.clear_sequence_state();
8346 self.graph_failed
8347 .store(true, std::sync::atomic::Ordering::Relaxed);
8348 self.cancel
8349 .store(true, std::sync::atomic::Ordering::Relaxed);
8350 tracing::error!("Metal verify state/KV handoff failed after admission");
8351 return None;
8352 }
8353 if let Some((plain_states, rows)) = commit_ref {
8354 crate::gpu_metal::queue_fence();
8355 let _ = crate::gpu_metal::wait_replay();
8358 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8359 let mut worst_s = 0f32;
8360 let mut worst_li = 0usize;
8361 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8362 if l.linear_state.len() != ps.len() || ps.is_empty() {
8363 continue;
8364 }
8365 let d = l
8366 .linear_state
8367 .iter()
8368 .zip(ps)
8369 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8370 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8371 let rel = d / n.max(1e-6);
8372 if rel > worst_s {
8373 worst_s = rel;
8374 worst_li = li;
8375 }
8376 }
8377 let mut worst_k = 0f32;
8378 for (li, kk, vv) in &rows {
8379 let l = &self.kv_cache.layers[*li];
8380 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8381 let mut ck = Vec::new();
8382 let mut cv = Vec::new();
8383 for g in 0..nkv {
8384 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8385 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8386 }
8387 if ck.len() == kk.len() {
8388 let dk = ck
8389 .iter()
8390 .zip(kk)
8391 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8392 let dv = cv
8393 .iter()
8394 .zip(vv)
8395 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8396 worst_k = worst_k.max(dk).max(dv);
8397 } else {
8398 eprintln!(
8399 "commit-check L{li}: kv row count mismatch {} vs {}",
8400 ck.len(),
8401 kk.len()
8402 );
8403 }
8404 }
8405 eprintln!(
8406 "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}"
8407 );
8408 }
8409 }
8410 if !metal_native && a + 1 < b {
8411 let expected_gdn_layers = self.graph_gdn_layer_count();
8412 if expected_gdn_layers > 0
8413 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8414 {
8415 self.clear_sequence_state();
8416 self.graph_failed
8417 .store(true, std::sync::atomic::Ordering::Relaxed);
8418 self.cancel
8419 .store(true, std::sync::atomic::Ordering::Relaxed);
8420 tracing::error!("GDN speculative restore failed after verify");
8421 return None;
8422 }
8423 }
8424 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8425 self.clear_sequence_state();
8430 self.graph_failed
8431 .store(true, std::sync::atomic::Ordering::Relaxed);
8432 self.cancel
8433 .store(true, std::sync::atomic::Ordering::Relaxed);
8434 tracing::error!("trunk graph KV rewind failed after speculative verify");
8435 return None;
8436 }
8437 *accepted += a;
8438 if !metal_native {
8449 m.kv.truncate_last(k_spec.saturating_sub(1));
8451 }
8452 spec_stamp("c.trunc");
8453 if !metal_native
8454 && self.mtp_graph_mode == Some(true)
8455 && !self.rewind_mtp_graph_mirror(next_pos)
8456 {
8457 self.clear_sequence_state();
8461 self.graph_failed
8462 .store(true, std::sync::atomic::Ordering::Relaxed);
8463 self.cancel
8464 .store(true, std::sync::atomic::Ordering::Relaxed);
8465 tracing::error!("MTP graph mirror rewind failed after verify commit");
8466 return None;
8467 }
8468 if !warm_off && a > 0 {
8469 let mut warmed = false;
8472 #[cfg(target_os = "macos")]
8473 if metal_native && self.mtp_graph_mode == Some(true) {
8474 warmed = match warm_pending.take() {
8478 Some(p) => self.mtp_warm_batch_finish(m, p),
8479 None => false,
8480 };
8481 if !warmed {
8482 warmed = true;
8483 for j in 0..a {
8484 let row =
8485 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8486 if self
8487 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8488 .is_none()
8489 {
8490 warmed = false;
8491 break;
8492 }
8493 }
8494 }
8495 }
8496 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8497 let rows: Vec<Vec<f32>> = (0..a)
8498 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8499 .collect();
8500 let pairs: Vec<(&[f32], u32)> = rows
8501 .iter()
8502 .zip(ids.iter())
8503 .map(|(r, &t)| (r.as_slice(), t))
8504 .collect();
8505 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8506 Ok(()) => warmed = true,
8507 Err(err) => {
8508 tracing::error!("{err}");
8514 self.clear_sequence_state();
8515 self.graph_failed
8516 .store(true, std::sync::atomic::Ordering::Relaxed);
8517 self.cancel
8518 .store(true, std::sync::atomic::Ordering::Relaxed);
8519 return None;
8520 }
8521 }
8522 }
8523 if !warmed {
8524 for j in 0..a {
8525 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8526 let row = row.to_vec();
8527 self.mtp_warm(m, &row, ids[j], next_pos + j);
8528 }
8529 }
8530 }
8531 spec_stamp("c.warm");
8535 if let Some(c) = forced {
8536 self.spec_forced = Some(c);
8537 self.graph_logits = None;
8538 } else if greedy_dev && logits.is_empty() {
8539 self.spec_forced = Some(ids[a]);
8542 self.graph_logits = None;
8543 } else {
8544 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8545 row.resize(self.vocab_size, 0.0);
8546 if let Some(c) = self.final_softcap {
8547 for l in row.iter_mut() {
8548 *l = c * (*l / c).tanh();
8549 }
8550 }
8551 self.graph_logits = Some(row);
8552 }
8553 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8554 spec_stamp("c.row");
8555 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8561 let end = subs();
8562 eprintln!(
8563 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8564 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8565 t_draft.as_secs_f64() * 1e3,
8566 sub_draft - sub0,
8567 (t_verify - t_draft).as_secs_f64() * 1e3,
8568 sub_verify - sub_draft,
8569 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8570 end - sub_verify,
8571 self.draft_full_streak,
8572 );
8573 }
8574 if k_env.is_none() && !metal_native && !k_capped {
8579 let f = a as f32 / k_spec.max(1) as f32;
8583 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8584 let mut k_next = k_spec;
8585 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8586 k_next = k_spec + 1;
8587 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8588 k_next = k_spec - 1;
8589 }
8590 if k_next != k_spec {
8591 self.spec_acc_ewma = 0.6;
8592 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8593 eprintln!("spec-k: {k_spec} → {k_next}");
8594 }
8595 }
8596 self.spec_k_adapt = Some(k_next);
8597 }
8598 spec_stamp("end");
8599 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8600 }
8601
8602 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8611 if !self.pair_supported() {
8612 return (0.0, 0.0);
8613 }
8614 if self.gdn_cfg.is_some()
8622 && self.graph_prefill_preferred()
8623 && self.automatic_gpu_prefix().is_some()
8624 {
8625 tracing::info!(
8626 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8627 );
8628 return (0.0, 0.0);
8629 }
8630 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8637 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8638 let emb1 = self.embed_single(1);
8639 let emb2 = self.embed_single(2);
8640 let pos = self.kv_cache.seq_len();
8641
8642 let t0 = std::time::Instant::now();
8643 for _ in 0..iters {
8644 let _ = self.forward_layers(&emb1, pos, None);
8645 let _ = self.forward_layers(&emb2, pos + 1, None);
8646 for l in &mut self.kv_cache.layers {
8647 l.truncate_last(2);
8648 }
8649 }
8650 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8651
8652 let t1 = std::time::Instant::now();
8653 for _ in 0..iters {
8654 let _ = self.forward_pair(&emb1, &emb2, pos);
8655 for l in &mut self.kv_cache.layers {
8656 l.truncate_last(2);
8657 }
8658 }
8659 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8660 match graph_env {
8661 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8662 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8663 }
8664 (singles_ms, pair_ms)
8665 }
8666
8667 fn pair_supported(&self) -> bool {
8675 !self.weights.layers.is_empty()
8682 && !self.per_token_stack()
8683 && !self
8684 .weights
8685 .layers
8686 .iter()
8687 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8688 }
8689
8690 fn forward_pair(
8691 &mut self,
8692 emb1: &[f32],
8693 emb2: &[f32],
8694 position: usize,
8695 ) -> (Vec<f32>, Vec<f32>) {
8696 self.mimo_moe_prepare();
8699 let mut h1 = emb1.to_vec();
8700 let mut h2 = emb2.to_vec();
8701 let (_nkv, _hd, hs, _rd, eps) = (
8702 self.num_kv_heads,
8703 self.head_dim,
8704 self.hidden_size,
8705 self.rotary_dim,
8706 self.rms_eps,
8707 );
8708 let pool = self.pool.clone();
8709 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8716
8717 for li in 0..self.num_layers {
8718 let _capacity_tail = automatic_gpu_prefix
8719 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8720 .map(|_| crate::gpu::enter_cpu_scope());
8721 crate::gpu::set_layer(li as i64);
8724 let lw = &self.weights.layers[self.phys_layer(li)];
8725 inference::rms_norm_into(
8728 &h1,
8729 &lw.input_norm,
8730 self.rms_eps,
8731 self.norm_style,
8732 &mut self.ws.n1,
8733 );
8734 inference::rms_norm_into(
8735 &h2,
8736 &lw.input_norm,
8737 self.rms_eps,
8738 self.norm_style,
8739 &mut self.ws.n2,
8740 );
8741
8742 let (a1, a2) = match &lw.attn {
8743 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8744 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8745 AttnKind::Bounded(w) => {
8746 let rope = self
8749 .bounded_rope
8750 .clone()
8751 .expect("bounded layer without an installed rotation table");
8752 let cfg = crate::bounded::BoundedAttnCfg {
8753 num_heads: self.num_heads,
8754 num_kv_heads: self.num_kv_heads,
8755 head_dim: self.head_dim,
8756 hidden_size: hs,
8757 scale: self.attn_scale,
8758 rope: &rope,
8759 pool: pool.as_deref(),
8760 };
8761 let a1 = crate::bounded::bounded_attention(
8762 &self.ws.n1,
8763 w,
8764 &mut self.kv_cache.layers[li],
8765 &cfg,
8766 );
8767 let a2 = crate::bounded::bounded_attention(
8768 &self.ws.n2,
8769 w,
8770 &mut self.kv_cache.layers[li],
8771 &cfg,
8772 );
8773 (a1, a2)
8774 }
8775 AttnKind::Linear(w) => {
8776 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8777 let layer = &mut self.kv_cache.layers[li];
8778 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8779 vmf_phase_pair(
8780 &self.ws.n1,
8781 &self.ws.n2,
8782 w,
8783 &cfg,
8784 state,
8785 scratch,
8786 self.pool.as_deref(),
8787 )
8788 }
8789 AttnKind::LinearGdn(w) => {
8790 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8791 let layer = &mut self.kv_cache.layers[li];
8792 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8793 gdn_pair(
8794 &self.ws.n1,
8795 &self.ws.n2,
8796 w,
8797 &cfg,
8798 state,
8799 scratch,
8800 self.pool.as_deref(),
8801 )
8802 }
8803 AttnKind::ShortConv(w) => {
8804 let cfg = self
8805 .short_conv_cfg
8806 .expect("short-conv layer without short_conv_cfg");
8807 let layer = &mut self.kv_cache.layers[li];
8808 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8809 short_conv_pair(
8810 &self.ws.n1,
8811 &self.ws.n2,
8812 w,
8813 &cfg,
8814 state,
8815 scratch,
8816 self.pool.as_deref(),
8817 )
8818 }
8819 AttnKind::Full {
8820 wq,
8821 wk,
8822 wv,
8823 wo,
8824 q_norm,
8825 k_norm,
8826 output_gate,
8827 softplus_gate,
8828 bias,
8829 } => {
8830 let inv_freq_l = self.layer_inv_freq(li);
8831 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8832 let cfg = QwenAttnCfg {
8833 num_heads: self.layer_num_heads(li),
8834 num_kv_heads: nkv_l,
8835 head_dim: hd_l,
8836 hidden_size: hs,
8837 position,
8838 inv_freq: &inv_freq_l,
8839 rotary_dim: rd_l,
8840 scale: self.attn_scale,
8841 softcap: self.attn_softcap,
8842 window: self.layer_window(li),
8843 v_norm: self.attn_v_norm,
8844 qk_norm_after_rope: self.qk_norm_after_rope,
8845 gate_sigmoid: self.proj_gate_sigmoid,
8846 q_norm: q_norm.as_deref(),
8847 k_norm: k_norm.as_deref(),
8848 output_gate: *output_gate,
8849 softplus_gate: softplus_gate
8850 .as_ref()
8851 .map(|(gate, per_head)| (gate, *per_head)),
8852 rope_scale: self.layer_rope_scale(li),
8853 bias: bias
8854 .as_ref()
8855 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8856 rms_eps: eps,
8857 norm_style: self.norm_style,
8858 pool: pool.as_deref(),
8859 v_head_dim: self.layer_v_dim(li),
8860 };
8861 attention::qwen_attention_pair(
8862 &self.ws.n1,
8863 &self.ws.n2,
8864 wq,
8865 wk,
8866 wv,
8867 wo,
8868 &mut self.kv_cache.layers[li],
8869 &cfg,
8870 )
8871 }
8872 };
8873 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8874 Some(w) => (
8875 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8876 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8877 ),
8878 None => (a1, a2),
8879 };
8880 for i in 0..self.hidden_size {
8881 h1[i] += a1[i];
8882 h2[i] += a2[i];
8883 }
8884 let (mut a1, mut a2) = (a1, a2);
8885 attention::recycle_buf(&mut a1);
8886 attention::recycle_buf(&mut a2);
8887
8888 let lw = &self.weights.layers[self.phys_layer(li)];
8889 inference::rms_norm_into(
8890 &h1,
8891 &lw.post_norm,
8892 self.rms_eps,
8893 self.norm_style,
8894 &mut self.ws.p1,
8895 );
8896 inference::rms_norm_into(
8897 &h2,
8898 &lw.post_norm,
8899 self.rms_eps,
8900 self.norm_style,
8901 &mut self.ws.p2,
8902 );
8903 let (f1, f2) = match &lw.ffn {
8904 FfnKind::DenseMoe(dm) => (
8907 dense_moe_ffn(
8908 dm,
8909 &self.ws.p1,
8910 &h1,
8911 self.rms_eps,
8912 self.norm_style,
8913 self.pool.as_deref(),
8914 ),
8915 dense_moe_ffn(
8916 dm,
8917 &self.ws.p2,
8918 &h2,
8919 self.rms_eps,
8920 self.norm_style,
8921 self.pool.as_deref(),
8922 ),
8923 ),
8924 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8925 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8926 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8927 ),
8928 _ => ffn_forward_pair(
8929 &lw.ffn,
8930 &self.ws.p1,
8931 &self.ws.p2,
8932 self.pool.as_deref(),
8933 None,
8934 ),
8935 };
8936 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8937 Some(w) => (
8938 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8939 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8940 ),
8941 None => (f1, f2),
8942 };
8943 for i in 0..self.hidden_size {
8944 h1[i] += f1[i];
8945 h2[i] += f2[i];
8946 }
8947 let (mut f1, mut f2) = (f1, f2);
8948 attention::recycle_buf(&mut f1);
8949 attention::recycle_buf(&mut f2);
8950 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8951 for i in 0..self.hidden_size {
8952 h1[i] *= sc;
8953 h2[i] *= sc;
8954 }
8955 }
8956 if self.is_loop_end(li) && li + 1 < self.num_layers {
8958 h1 = inference::rms_norm(
8959 &h1,
8960 &self.weights.final_norm,
8961 self.rms_eps,
8962 self.norm_style,
8963 );
8964 h2 = inference::rms_norm(
8965 &h2,
8966 &self.weights.final_norm,
8967 self.rms_eps,
8968 self.norm_style,
8969 );
8970 }
8971 }
8972 crate::gpu::set_layer(-1); if self.o1_active() {
8979 self.commit_linear_scratch();
8980 }
8981 self.o1_progress();
8982 self.swa_trim_tails();
8983 (h1, h2)
8984 }
8985
8986 fn commit_linear_scratch(&mut self) {
8988 for layer in &mut self.kv_cache.layers {
8989 if !layer.linear_scratch.is_empty() {
8990 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8991 layer.linear_scratch.clear();
8992 }
8993 }
8994 }
8995
8996 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9004 #[cfg(target_os = "macos")]
9005 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9006 if ids.is_empty() {
9007 return Err("empty id sequence".to_string());
9008 }
9009
9010 self.nll_begin()?;
9011 let result = (|| {
9012 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9013 self.nll_check_graph("encoder prefill", 0)?;
9014 let expected = ids.len() * self.hidden_size;
9015 if hidden.len() != expected {
9016 return Err(format!(
9017 "encoder prefill returned {} values for {} rows of hidden size {}",
9018 hidden.len(),
9019 ids.len(),
9020 self.hidden_size
9021 ));
9022 }
9023
9024 let mut out = vec![0.0f32; expected];
9025 for (src, dst) in hidden
9026 .chunks_exact(self.hidden_size)
9027 .zip(out.chunks_exact_mut(self.hidden_size))
9028 {
9029 inference::rms_norm_into(
9030 src,
9031 &self.weights.final_norm,
9032 self.rms_eps,
9033 self.norm_style,
9034 dst,
9035 );
9036 }
9037 Ok(out)
9038 })();
9039 self.nll_end();
9040 result
9041 }
9042
9043 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9048 #[cfg(target_os = "macos")]
9049 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9050 if ids.is_empty() {
9051 return Err("empty id sequence".to_string());
9052 }
9053 self.nll_begin()?;
9054 let result = (|| {
9055 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
9056 self.nll_check_graph("hidden prefill", 0)?;
9057 if hidden.len() != ids.len() * self.hidden_size {
9058 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
9059 }
9060 Ok(hidden)
9061 })();
9062 self.nll_end();
9063 result
9064 }
9065
9066 pub fn forward_ids(
9069 &mut self,
9070 ids: &[u32],
9071 task_mask: Option<&TaskMask>,
9072 ) -> Result<Vec<f32>, String> {
9073 #[cfg(target_os = "macos")]
9074 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9075 if ids.is_empty() {
9076 return Err("empty id sequence".to_string());
9077 }
9078 self.clear_sequence_state();
9079 self.check_forward_graph("forward_ids setup", 0)?;
9080 if task_mask.is_none() {
9081 self.o1_begin();
9082 }
9083 let mut hidden = vec![0.0f32; self.hidden_size];
9084 let mut pos = 0usize;
9085 if let Some(b) = &mut self.dsv41 {
9086 let pool = self.pool.clone();
9087 let mut logits = Vec::new();
9088 crate::dsv41::forward_chunk(
9089 &b.0,
9090 &b.1,
9091 &b.2,
9092 &mut b.3,
9093 ids,
9094 0,
9095 pool.as_deref(),
9096 &mut logits,
9097 );
9098 if let Err(err) = self.o1_seal_checked() {
9099 self.clear_sequence_state();
9100 return Err(err);
9101 }
9102 return Ok(logits);
9103 }
9104 #[cfg(not(target_os = "macos"))]
9115 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9116 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9117 Ok(r) => r,
9118 Err(e) => {
9119 self.clear_sequence_state();
9120 return Err(e);
9121 }
9122 };
9123 if n > 0 {
9124 hidden.copy_from_slice(&last);
9125 pos = n;
9126 }
9127 }
9128 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9129 let chunk = self.prefill_chunk();
9133 let hs = self.hidden_size;
9134 while pos < ids.len() {
9135 let end = (pos + chunk).min(ids.len());
9136 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9137 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9138 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9139 pos = end;
9140 }
9141 }
9142 if task_mask.is_none()
9151 && !self.graph_prefill_preferred()
9152 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9153 && self.pair_supported()
9154 {
9155 while pos + 1 < ids.len() {
9156 let e1 = self.embed_single(ids[pos]);
9157 let e2 = self.embed_single(ids[pos + 1]);
9158 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9159 self.check_forward_graph("forward_ids pair", pos + 1)?;
9160 self.commit_linear_scratch();
9161 hidden = h2;
9162 pos += 2;
9163 }
9164 }
9165 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9168 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9169 self.graph_logits = Some(lg);
9170 hidden = vec![0.0; self.hidden_size];
9171 pos = ids.len();
9172 }
9173 }
9174 while pos < ids.len() {
9175 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9176 self.check_forward_graph("forward_ids", pos)?;
9177 pos += 1;
9178 }
9179 if let Some(logits) = self.graph_logits.take() {
9180 if let Err(err) = self.o1_seal_checked() {
9184 self.clear_sequence_state();
9185 return Err(err);
9186 }
9187 return Ok(logits);
9188 }
9189 if let Err(err) = self.o1_seal_checked() {
9193 self.clear_sequence_state();
9194 return Err(err);
9195 }
9196 let normed = inference::rms_norm(
9197 &hidden,
9198 &self.weights.final_norm,
9199 self.rms_eps,
9200 self.norm_style,
9201 );
9202 Ok(self.lm_head_forward(&normed))
9203 }
9204
9205 #[doc(hidden)]
9209 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9210 #[cfg(target_os = "macos")]
9211 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9212 if ids.is_empty() {
9213 return Err("empty id sequence".to_string());
9214 }
9215 self.clear_sequence_state();
9216 self.dsv41
9217 .as_ref()
9218 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9219 self.o1_begin();
9220 let rows = {
9221 let pool = self.pool.clone();
9222 let b = self
9223 .dsv41
9224 .as_mut()
9225 .expect("dsv41 checked above; state cannot change during forward");
9226 let mut rows = Vec::with_capacity(ids.len());
9227 for (position, &id) in ids.iter().enumerate() {
9228 let mut logits = Vec::new();
9229 crate::dsv41::forward_token(
9230 &b.0,
9231 &b.1,
9232 &b.2,
9233 &mut b.3,
9234 id,
9235 position,
9236 pool.as_deref(),
9237 &mut logits,
9238 );
9239 rows.push(logits);
9240 }
9241 rows
9242 };
9243 self.o1_seal();
9244 Ok(rows)
9245 }
9246
9247 pub fn dsv41_moe_capture(
9251 &mut self,
9252 ids: &[u32],
9253 lo: usize,
9254 hi: usize,
9255 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9256 #[cfg(target_os = "macos")]
9257 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9258 if ids.is_empty() {
9259 return Err("empty id sequence".to_string());
9260 }
9261 self.dsv41
9262 .as_ref()
9263 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9264 self.clear_sequence_state();
9265 let res = {
9266 let pool = self.pool.clone();
9267 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9268 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9269 };
9270 self.clear_sequence_state();
9271 res
9272 }
9273
9274 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9281 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9282 Ok((nll / cnt.max(1) as f64).exp())
9283 }
9284
9285 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9292 if !self.can_prefill_batched() {
9293 return self.nll_ids_from(ids, 0).map(|_| ());
9294 }
9295 self.nll_begin()?;
9296 let result: Result<(), String> = (|| {
9297 let chunk = chunk.max(1);
9298 let mut pos = 0usize;
9299 while pos < ids.len() {
9300 let end = (pos + chunk).min(ids.len());
9301 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9302 self.nll_check_graph("calibration prefill", pos)?;
9303 pos = end;
9304 }
9305 Ok(())
9306 })();
9307 self.nll_end();
9308 result
9309 }
9310
9311 pub fn calib_window_span(
9320 &mut self,
9321 ids: &[u32],
9322 input: Option<&[f32]>,
9323 from: usize,
9324 mid: usize,
9325 upto: usize,
9326 ) -> Result<Vec<f32>, String> {
9327 if !self.can_prefill_batched() {
9328 return Err("calib_window_span: this architecture has no batched prefill".into());
9329 }
9330 self.nll_begin()?;
9331 let result: Result<Vec<f32>, String> = (|| {
9332 let h = if mid > from {
9333 let inp = match input {
9334 Some(h) => PrefillIn::Hidden(h),
9335 None => PrefillIn::Ids(ids),
9336 };
9337 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9338 self.nll_check_graph("calibration span", 0)?;
9339 h
9340 } else {
9341 input.map(<[f32]>::to_vec).unwrap_or_default()
9342 };
9343 if upto > mid {
9344 let inp = if mid > from || input.is_some() {
9345 PrefillIn::Hidden(&h)
9346 } else {
9347 PrefillIn::Ids(ids)
9348 };
9349 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9350 self.nll_check_graph("calibration span", 0)?;
9351 }
9352 Ok(h)
9353 })();
9354 self.nll_end();
9355 result
9356 }
9357
9358 #[cfg(feature = "gpu")]
9362 pub fn qwen4_capture(
9363 &mut self,
9364 ids: &[u32],
9365 lo: usize,
9366 hi: usize,
9367 ) -> Result<Vec<Vec<f32>>, String> {
9368 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9369 crate::qwen4_exp::capture_window(
9370 &b.0,
9371 &b.1,
9372 &b.2,
9373 &mut b.3,
9374 ids,
9375 lo,
9376 hi,
9377 &self.inv_freq,
9378 self.pool.as_deref(),
9379 )
9380 }
9381
9382 #[cfg(feature = "gpu")]
9388 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9389 use crate::gpu_wgpu::clover as cap;
9390 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9391 let ne = (lo..hi)
9392 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9393 FfnKind::Moe(m) => Some(m.experts.len()),
9394 _ => None,
9395 })
9396 .ok_or("no MoE layer in the capture range")?;
9397 let chunk = self.prefill_chunk().max(1);
9398 self.clear_sequence_state();
9399 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9400 return Err("capture unavailable (no wgpu device)".into());
9401 }
9402 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9403 let res = (|| {
9404 for (ci, part) in ids.chunks(chunk).enumerate() {
9405 let mut hiddens = Vec::with_capacity(part.len() * hs);
9406 for &id in part {
9407 hiddens.extend_from_slice(&self.embed_single(id));
9408 }
9409 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9410 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9411 crate::gpu::BatchGraphOutcome::Completed => {}
9412 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9413 }
9414 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9415 if written < hi - lo {
9416 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9417 }
9418 for (o, r) in out.iter_mut().zip(rows) {
9419 o.extend_from_slice(&r);
9420 }
9421 }
9422 Ok(())
9423 })();
9424 cap::moe_capture_end();
9425 self.clear_sequence_state();
9426 res.map(|_| out)
9427 }
9428
9429 pub fn moe_expert_dirs(
9432 &self,
9433 li: usize,
9434 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9435 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9436 return None;
9437 };
9438 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9439 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9440 let shared = match &m.shared {
9441 Some((se, _)) => Some(dirs(se)?),
9442 None => None,
9443 };
9444 Some((routed, shared))
9445 }
9446
9447 pub fn qwen4_expert_dirs(
9449 &self,
9450 li: usize,
9451 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9452 let b = self.qwen4_exp.as_ref()?;
9453 crate::qwen4_exp::expert_dirs(&b.1, li)
9454 }
9455
9456 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9471 self.reset_mtp_probe_state();
9472 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9478 let result = if ids.len() < 2 {
9479 Err("MTP FFN probe needs at least two token ids".into())
9480 } else if ids
9481 .iter()
9482 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9483 {
9484 Err("MTP FFN probe received a token id outside the embedding table".into())
9485 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9486 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9489 } else if self.mimo_mtp.is_some() {
9490 let mut st = self
9491 .mimo_mtp
9492 .take()
9493 .expect("checked MiMo MTP before detaching it");
9494 let result = match self.probe_trunk_rows(ids) {
9495 Ok(trunk) => crate::gpu::cpu_scope(|| {
9496 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
9497 }),
9498 Err(e) => Err(e),
9499 };
9500 st.reset();
9503 self.mimo_mtp = Some(st);
9504 result
9505 } else if self.mtp.is_some() {
9506 let mut mtp = self
9507 .mtp
9508 .take()
9509 .expect("checked canonical MTP before detaching it");
9510 let result = match self.probe_trunk_rows(ids) {
9511 Ok(trunk) => crate::gpu::cpu_scope(|| {
9512 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
9513 }),
9514 Err(e) => Err(e),
9515 };
9516 mtp.kv.clear();
9517 self.mtp = Some(mtp);
9518 result
9519 } else {
9520 Err("MTP FFN probe: this pipeline has no draft stack".into())
9521 };
9522 self.reset_mtp_probe_state();
9525 result
9526 }
9527
9528 fn reset_mtp_probe_state(&mut self) {
9530 self.clear_sequence_state();
9531 if let Some(mtp) = self.mtp.as_mut() {
9532 mtp.kv.clear();
9533 }
9534 if let Some(mimo) = self.mimo_mtp.as_mut() {
9535 mimo.reset();
9536 }
9537 self.mtp_graph_mode = None;
9538 }
9539
9540 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
9546 if ids.len() < 2 {
9547 return Err("MTP FFN probe needs at least two token ids".into());
9548 }
9549 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
9550 self.clear_sequence_state();
9551 Ok(rows)
9552 }
9553
9554 fn mtp_ffn_probe_teacher_forced(
9560 &mut self,
9561 mtp: &mut MtpModule,
9562 ids: &[u32],
9563 trunk: &[f32],
9564 ) -> Result<Vec<MtpFfnProbe>, String> {
9565 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9566 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9567 };
9568 let inter = dense.gate_proj.rows();
9569 let (hs, n) = (self.hidden_size, ids.len() - 1);
9570 if trunk.len() != n * hs {
9571 return Err(format!(
9572 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
9573 trunk.len()
9574 ));
9575 }
9576 let mut mass = vec![0.0f64; inter];
9577 let mut squared_mass = vec![0.0f64; inter];
9578 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
9579 for position in 0..n {
9580 self.mtp_probe_ffn_input(
9581 mtp,
9582 &trunk[position * hs..(position + 1) * hs],
9583 ids[position + 1],
9584 position,
9585 )?;
9586 inputs.extend_from_slice(&self.ws.p1[..hs]);
9587 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
9588 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9589 unreachable!("checked dense above");
9590 };
9591 accumulate_dense_ffn_activation_mass(
9592 dense,
9593 &inputs,
9594 inputs.len() / hs,
9595 self.pool.as_deref(),
9596 &mut mass,
9597 &mut squared_mass,
9598 )?;
9599 inputs.clear();
9600 }
9601 }
9602 Ok(vec![MtpFfnProbe {
9603 mass,
9604 squared_mass,
9605 observed_positions: n,
9606 }])
9607 }
9608
9609 fn mtp_probe_ffn_input(
9615 &mut self,
9616 mtp: &mut MtpModule,
9617 hidden: &[f32],
9618 next_token: u32,
9619 position: usize,
9620 ) -> Result<(), String> {
9621 let e = self.embed_single(next_token);
9622 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9623 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9624 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9625 inference::rms_norm_into(
9626 hidden,
9627 &mtp.hnorm,
9628 self.rms_eps,
9629 self.norm_style,
9630 cat_h,
9631 );
9632 let mut x = vec![0.0f32; self.hidden_size];
9633 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9634
9635 let lw = &mtp.layer;
9636 inference::rms_norm_into(
9637 &x,
9638 &lw.input_norm,
9639 self.rms_eps,
9640 self.norm_style,
9641 &mut self.ws.n1,
9642 );
9643 let attn = match &lw.attn {
9644 AttnKind::Full {
9645 wq,
9646 wk,
9647 wv,
9648 wo,
9649 q_norm,
9650 k_norm,
9651 output_gate,
9652 softplus_gate,
9653 bias,
9654 } => {
9655 let mut cfg = self.attn_cfg(position);
9656 cfg.q_norm = q_norm.as_deref();
9657 cfg.k_norm = k_norm.as_deref();
9658 cfg.output_gate = *output_gate;
9659 cfg.softplus_gate = softplus_gate
9660 .as_ref()
9661 .map(|(gate, per_head)| (gate, *per_head));
9662 cfg.bias = bias
9663 .as_ref()
9664 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9665 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9666 }
9667 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9668 };
9669 for (dst, src) in x.iter_mut().zip(attn) {
9670 *dst += src;
9671 }
9672 inference::rms_norm_into(
9673 &x,
9674 &lw.post_norm,
9675 self.rms_eps,
9676 self.norm_style,
9677 &mut self.ws.p1,
9678 );
9679 Ok(())
9680 }
9681
9682 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9687 self.clear_sequence_state();
9688 FFN_PROBE.with(|p| {
9689 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9690 });
9691 crate::gpu::cpu_scope(|| {
9692 for (pos, &id) in ids.iter().enumerate() {
9693 let emb = self.embed_single(id);
9694 let _ = self.forward_layers(&emb, pos, None);
9695 }
9696 });
9697 self.clear_sequence_state();
9698 FFN_PROBE
9699 .with(|p| p.borrow_mut().take())
9700 .unwrap_or_default()
9701 }
9702
9703 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9707 if let Err(err) = self.nll_begin() {
9708 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9712 self.nll_end();
9713 return Err(err);
9714 }
9715 FFN_PROBE.with(|p| {
9716 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9717 });
9718 let result: Result<(), String> = (|| {
9719 for chunk in ids.chunks(256) {
9720 if chunk.len() < 2 {
9721 continue;
9722 }
9723 self.nll_ids_masked(chunk, 0, None)?;
9724 }
9725 Ok(())
9726 })();
9727 self.nll_end();
9728 let probe = FFN_PROBE
9729 .with(|p| p.borrow_mut().take())
9730 .unwrap_or_default();
9731 match result {
9732 Ok(()) => Ok(probe),
9733 Err(err) => {
9734 drop(probe);
9735 Err(err)
9736 }
9737 }
9738 }
9739
9740 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9744 self.nll_begin()?;
9745 let result: Result<f64, String> = (|| {
9746 let mut nll = 0f64;
9747 let mut cnt = 0usize;
9748 let mut hidden = vec![0f32; self.hidden_size];
9749 for (pos, &id) in ids.iter().enumerate() {
9750 if pos > 0 {
9751 inference::rms_norm_into(
9752 &hidden,
9753 &self.weights.final_norm,
9754 self.rms_eps,
9755 self.norm_style,
9756 &mut self.ws.n1,
9757 );
9758 let mut logits = self.lm_head_forward(&self.ws.n1);
9759 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9760 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9761 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9762 nll -= p.max(1e-300).ln();
9763 cnt += 1;
9764 attention::recycle_buf(&mut logits);
9765 }
9766 let emb = self.embed_single(id);
9767 hidden = self.forward_layers(&emb, pos, Some(mask));
9768 self.nll_check_graph("masked serial forward", pos)?;
9769 let _ = self.graph_logits.take();
9773 }
9774 Ok((nll / cnt.max(1) as f64).exp())
9775 })();
9776 self.nll_end();
9777 result
9778 }
9779
9780 pub fn nll_ids_masked(
9799 &mut self,
9800 ids: &[u32],
9801 start: usize,
9802 task_mask: Option<&TaskMask>,
9803 ) -> Result<(f64, usize), String> {
9804 let task_mask = self.drop_open_mask(task_mask);
9805 self.nll_ids_inner(ids, start, task_mask)
9806 }
9807
9808 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9809 self.nll_ids_inner(ids, start, None)
9810 }
9811
9812 fn nll_ids_inner(
9813 &mut self,
9814 ids: &[u32],
9815 start: usize,
9816 task_mask: Option<&TaskMask>,
9817 ) -> Result<(f64, usize), String> {
9818 self.nll_begin()?;
9819 #[cfg(target_os = "macos")]
9821 if task_mask.is_none() && self.is_block_diffusion() {
9822 if let Some(r) = self.clover_metal_nll(ids, start) {
9823 self.nll_end();
9824 return r;
9825 }
9826 }
9827 let result: Result<(f64, usize), String> = (|| {
9828 let mut nll = 0f64;
9829 let mut cnt = 0usize;
9830 let (graph_quality, fused_head_quality) = nll_graph_policy(
9843 task_mask.is_none(),
9844 self.graph_prefill_preferred(),
9845 crate::gpu::q1_force(),
9846 );
9847 self.graph_head_required = fused_head_quality;
9848 self.graph_want_logits = fused_head_quality;
9849 #[cfg(target_os = "macos")]
9850 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9851 match self.nll_batch_metal(ids, start) {
9852 MetalBatchNllOutcome::Completed(nll, count) => {
9853 return Ok((nll, count));
9854 }
9855 MetalBatchNllOutcome::Declined => {}
9856 MetalBatchNllOutcome::Failed(err) => return Err(err),
9857 }
9858 }
9859 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9864 if self.can_prefill_batched() && !graph_quality && !force_serial {
9865 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9874 .ok()
9875 .and_then(|v| v.parse().ok())
9876 .filter(|&c: &usize| c > 0)
9877 .unwrap_or(128);
9878 const LM_SUB: usize = 32;
9879 let n = ids.len().saturating_sub(1);
9880 let hs = self.hidden_size;
9881 let rows = self.weights.lm_head.rows();
9882 let mut pos = 0usize;
9883 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9884 while pos < n {
9885 let end = (pos + chunk_len).min(n);
9886 let bsz = end - pos;
9887 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9888 self.nll_check_graph("batched prefill", pos)?;
9889 if state_trace && end % 256 == 0 {
9890 self.trace_recurrent_state(end);
9891 }
9892 let mut k0 = 0usize;
9893 while k0 < bsz {
9894 let k1 = (k0 + LM_SUB).min(bsz);
9895 let sb = k1 - k0;
9896 if pos + k1 <= start {
9899 k0 = k1;
9900 continue;
9901 }
9902 let mut normed = vec![0.0f32; sb * hs];
9903 for k in 0..sb {
9904 let r = inference::rms_norm(
9905 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9906 &self.weights.final_norm,
9907 self.rms_eps,
9908 self.norm_style,
9909 );
9910 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9911 }
9912 let mut logits = vec![0.0f32; sb * rows];
9913 self.weights
9914 .lm_head
9915 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9916 for k in 0..sb {
9917 if pos + k0 + k < start {
9918 continue;
9919 }
9920 self.nll_check_graph("batched score row", pos + k0 + k)?;
9921 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9922 if let Some(mu) = self.logit_multiplier {
9923 for v in lg.iter_mut() {
9924 *v *= mu;
9925 }
9926 }
9927 if let Some(c) = self.final_softcap {
9931 softcap_in_place(lg, c, self.pool.as_deref());
9932 }
9933 if let Some(cm) = self.head_clusters.clone() {
9936 self.hierarchical_head_logprobs(
9937 &normed[k * hs..(k + 1) * hs],
9938 &cm,
9939 lg,
9940 );
9941 }
9942 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9943 let target = ids[pos + k0 + k + 1] as usize;
9944 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9945 let lse: f64 = lg
9946 .iter()
9947 .map(|&v| ((v - max) as f64).exp())
9948 .sum::<f64>()
9949 .ln()
9950 + max as f64;
9951 nll += lse - lg[target] as f64;
9952 cnt += 1;
9953 if std::env::var("CMF_PPL_TRACE").is_ok() {
9954 let top = lg
9955 .iter()
9956 .enumerate()
9957 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9958 .map(|(i, _)| i)
9959 .unwrap_or(0);
9960 eprintln!(
9961 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9962 pos + k0 + k,
9963 target,
9964 lse - lg[target] as f64,
9965 top,
9966 lg[target],
9967 lg[top]
9968 );
9969 }
9970 }
9971 k0 = k1;
9972 }
9973 pos = end;
9974 }
9975 return Ok((nll, cnt));
9976 }
9977 for pos in 0..ids.len().saturating_sub(1) {
9978 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9979 self.nll_check_graph("serial forward", pos)?;
9980 let out_of_band = self.graph_logits.take();
9988 if self.graph_head_required && out_of_band.is_none() {
9989 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9990 return Err(format!(
9991 "fused Metal graph head did not complete at NLL position {pos}"
9992 ));
9993 }
9994 if pos < start {
9995 continue;
9996 }
9997 let logits = match out_of_band {
9998 Some(lg) => lg,
9999 None => {
10000 let normed = inference::rms_norm(
10001 &hidden,
10002 &self.weights.final_norm,
10003 self.rms_eps,
10004 self.norm_style,
10005 );
10006 self.lm_head_forward(&normed)
10010 }
10011 };
10012 let target = ids[pos + 1] as usize;
10013 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10014 let lse: f64 = logits
10015 .iter()
10016 .map(|&v| ((v - max) as f64).exp())
10017 .sum::<f64>()
10018 .ln()
10019 + max as f64;
10020 let tok_nll = lse - logits[target] as f64;
10021 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10022 let top = logits
10023 .iter()
10024 .enumerate()
10025 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10026 .map(|(i, _)| i)
10027 .unwrap_or(0);
10028 eprintln!(
10029 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10030 logits[target], logits[top]
10031 );
10032 }
10033 nll += tok_nll;
10034 cnt += 1;
10035 }
10036 Ok((nll, cnt))
10037 })();
10038 self.nll_end();
10039 result
10040 }
10041
10042 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
10047 let normed = inference::rms_norm(
10048 hidden,
10049 &self.weights.final_norm,
10050 self.rms_eps,
10051 self.norm_style,
10052 );
10053 let mut logits = self.lm_head_forward(&normed);
10056 let target = target as usize;
10057 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10058 let lse: f64 = logits
10059 .iter()
10060 .map(|&v| ((v - max) as f64).exp())
10061 .sum::<f64>()
10062 .ln()
10063 + max as f64;
10064 let tok_nll = lse - logits[target] as f64;
10065 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10066 let top = logits
10067 .iter()
10068 .enumerate()
10069 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10070 .map(|(i, _)| i)
10071 .unwrap_or(0);
10072 eprintln!(
10073 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10074 logits[target], logits[top]
10075 );
10076 }
10077 attention::recycle_buf(&mut logits);
10078 tok_nll
10079 }
10080
10081 fn trace_recurrent_state(&self, pos: usize) {
10089 let stats = |v: &[f32]| -> (f64, f64) {
10090 if v.is_empty() {
10091 return (0.0, 0.0);
10092 }
10093 let (mut ss, mut mx) = (0f64, 0f64);
10094 for &x in v {
10095 ss += (x as f64) * (x as f64);
10096 mx = mx.max((x as f64).abs());
10097 }
10098 ((ss / v.len() as f64).sqrt(), mx)
10099 };
10100 for (li, l) in self.kv_cache.layers.iter().enumerate() {
10101 let lw = &self.weights.layers[self.phys_layer(li)];
10102 let (kind, s_len) = match &lw.attn {
10103 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10104 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10105 AttnKind::Bounded(_) => ("bounded", 0),
10106 AttnKind::Full { .. } => ("full", 0),
10107 _ => ("other", 0),
10108 };
10109 let (rms, max) = stats(&l.linear_state);
10110 let s_part = if kind == "vmf" {
10111 &l.linear_state[..s_len.min(l.linear_state.len())]
10112 } else if kind == "gdn" {
10113 let ring = l
10114 .linear_state
10115 .len()
10116 .saturating_sub(s_len)
10117 .min(l.linear_state.len());
10118 &l.linear_state[ring..]
10119 } else {
10120 &l.linear_state[..0]
10121 };
10122 let (s_rms, s_max) = stats(s_part);
10123 let (ring_rms, ring_len) = match &l.bounded {
10124 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10125 None => (0.0, 0),
10126 };
10127 eprintln!(
10128 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10129 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10130 l.linear_state.len(),
10131 l.seq_len
10132 );
10133 }
10134 }
10135
10136 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10154 self.nll_begin()?;
10159 let requested_prefix = (prefill > 0).then_some(prefill);
10160 self.o1_begin_with_prefix(requested_prefix);
10161 let n = ids.len().saturating_sub(1);
10162 let requested_start = prefill.min(n);
10163 let exact_end = if self.o1_active() {
10168 match requested_prefix {
10169 Some(requested) => self.o1_effective_boundary(requested),
10170 None => self
10171 .o1_cfg
10172 .as_ref()
10173 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10174 }
10175 .unwrap_or(requested_start)
10176 .min(n)
10177 } else {
10178 requested_start
10179 };
10180 let mut nll = 0f64;
10181 let mut cnt = 0usize;
10182
10183 let mut pos = 0usize;
10187 if self.can_prefill_batched() {
10188 const CHUNK: usize = 128;
10189 while pos < exact_end {
10190 let end = (pos + CHUNK).min(exact_end);
10191 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10192 if self
10193 .graph_failed
10194 .swap(false, std::sync::atomic::Ordering::Relaxed)
10195 {
10196 self.cancel
10197 .store(false, std::sync::atomic::Ordering::Relaxed);
10198 self.nll_end();
10199 return Err("GPU graph failed during O(1) NLL prefix".into());
10200 }
10201 for row in 0..end - pos {
10202 let score_pos = pos + row;
10203 if score_pos >= requested_start && score_pos < n {
10204 nll += self.nll_from_hidden(
10205 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10206 ids[score_pos + 1],
10207 score_pos,
10208 );
10209 cnt += 1;
10210 }
10211 }
10212 pos = end;
10213 }
10214 } else {
10215 while pos < exact_end {
10216 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10217 if self
10218 .graph_failed
10219 .swap(false, std::sync::atomic::Ordering::Relaxed)
10220 {
10221 self.cancel
10222 .store(false, std::sync::atomic::Ordering::Relaxed);
10223 self.nll_end();
10224 return Err("GPU graph failed during O(1) NLL prefix".into());
10225 }
10226 if pos >= requested_start && pos < n {
10227 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10228 cnt += 1;
10229 }
10230 pos += 1;
10231 }
10232 }
10233 self.o1_seal_checked().map_err(|err| {
10234 self.nll_end();
10235 err
10236 })?;
10237
10238 let batch_k = std::env::var("CMF_BATCH_K")
10247 .ok()
10248 .and_then(|v| v.parse::<usize>().ok())
10249 .unwrap_or(0);
10250 let batch_admitted = batch_k > 0
10251 && self.can_prefill_batched()
10252 && self.o1_active()
10253 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10254 && (0..self.num_layers).all(|li| {
10255 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10256 cache.o1.is_none() || cache.o1_views().is_some()
10257 });
10258 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10259 eprintln!(
10260 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10261 batch_admitted,
10262 batch_k,
10263 n.saturating_sub(exact_end),
10264 );
10265 }
10266 let mut batch_completed = false;
10267 if batch_admitted && exact_end < n {
10268 let hs = self.hidden_size;
10269 let mut batch_pos = exact_end;
10270 while batch_pos < n {
10271 let end = (batch_pos + batch_k).min(n);
10272 let bk = end - batch_pos;
10273 let mut hiddens = vec![0.0f32; bk * hs];
10274 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10275 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10276 }
10277 let positions: Vec<usize> = (batch_pos..end).collect();
10278 let t_batch = std::time::Instant::now();
10279 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10280 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10281 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10282 eprintln!(
10283 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10284 batch_pos,
10285 end.saturating_sub(1),
10286 bk as f64 / (ms / 1000.0),
10287 );
10288 }
10289 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10290 self.nll_end();
10291 return Err(err);
10292 }
10293 match outcome {
10294 crate::gpu::BatchGraphOutcome::Completed => {
10295 batch_completed = true;
10296 for row in 0..bk {
10297 nll += self.nll_from_hidden(
10298 &hiddens[row * hs..(row + 1) * hs],
10299 ids[batch_pos + row + 1],
10300 batch_pos + row,
10301 );
10302 cnt += 1;
10303 }
10304 batch_pos = end;
10305 }
10306 crate::gpu::BatchGraphOutcome::Declined => {
10307 if batch_completed {
10308 self.nll_end();
10309 return Err(format!(
10310 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10311 ));
10312 }
10313 break;
10314 }
10315 crate::gpu::BatchGraphOutcome::Failed => {
10316 self.nll_end();
10317 return Err(format!(
10318 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10319 ));
10320 }
10321 }
10322 }
10323 if batch_completed && cnt == n.saturating_sub(requested_start) {
10324 self.nll_end();
10325 return Ok((nll, cnt));
10326 }
10327 }
10328
10329 for pos in exact_end..n {
10334 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10335 if self
10336 .graph_failed
10337 .swap(false, std::sync::atomic::Ordering::Relaxed)
10338 {
10339 self.cancel
10340 .store(false, std::sync::atomic::Ordering::Relaxed);
10341 self.nll_end();
10342 return Err(format!(
10343 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10344 ));
10345 }
10346 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10347 cnt += 1;
10348 }
10349 self.nll_end();
10350 Ok((nll, cnt))
10351 }
10352
10353 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10361 self.clear_sequence_state();
10362 let n = ids.len().saturating_sub(1);
10363 let mut correct = Vec::with_capacity(n);
10364 let mut pmax = Vec::with_capacity(n);
10365 for pos in 0..n {
10366 let emb = self.embed_single(ids[pos]);
10367 let hidden = self.forward_layers(&emb, pos, None);
10368 let logits = if let Some(logits) = self.graph_logits.take() {
10369 logits
10370 } else {
10371 let normed = inference::rms_norm(
10372 &hidden,
10373 &self.weights.final_norm,
10374 self.rms_eps,
10375 self.norm_style,
10376 );
10377 self.lm_head_forward(&normed)
10381 };
10382 let target = ids[pos + 1] as usize;
10383 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10384 for (i, &v) in logits.iter().enumerate() {
10385 if v > mval {
10386 mval = v;
10387 amax = i;
10388 }
10389 }
10390 correct.push(amax == target);
10391 let row: Vec<f32> = temps
10392 .iter()
10393 .map(|&t| {
10394 let tt = t.max(1e-3);
10395 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10396 1.0 / s.max(1e-12) })
10398 .collect();
10399 pmax.push(row);
10400 }
10401 self.clear_sequence_state();
10402 (correct, pmax)
10403 }
10404
10405 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10412 if self.dyn_router.is_none() {
10413 return Ok((self.ppl_ids(ids)?, 0));
10414 }
10415 self.nll_begin()?;
10416 let saved_active = self.dyn_active;
10417 let mut router = self
10418 .dyn_router
10419 .take()
10420 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10421 router.reset();
10422 self.dyn_phi_seen = 0;
10423 let _ = self.set_active_skill(None);
10424
10425 let result: Result<(f64, usize), String> = (|| {
10426 let mut nll = 0f64;
10427 let mut cnt = 0usize;
10428 for pos in 0..ids.len().saturating_sub(1) {
10429 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10430 self.nll_check_graph("dynamic serial forward", pos)?;
10431 let out_of_band = self.graph_logits.take();
10432 let mut logits = match out_of_band {
10433 Some(lg) => lg,
10434 None => {
10435 let normed = inference::rms_norm(
10436 &hidden,
10437 &self.weights.final_norm,
10438 self.rms_eps,
10439 self.norm_style,
10440 );
10441 self.lm_head_forward(&normed)
10445 }
10446 };
10447 let target = ids[pos + 1] as usize;
10448 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10449 let lse: f64 = logits
10450 .iter()
10451 .map(|&v| ((v - max) as f64).exp())
10452 .sum::<f64>()
10453 .ln()
10454 + max as f64;
10455 let tok_nll = lse - logits[target] as f64;
10456 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10457 let top = logits
10458 .iter()
10459 .enumerate()
10460 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10461 .map(|(i, _)| i)
10462 .unwrap_or(0);
10463 eprintln!(
10464 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10465 logits[target], logits[top]
10466 );
10467 }
10468 nll += tok_nll;
10469 cnt += 1;
10470 attention::recycle_buf(&mut logits);
10471 let phi = self.dyn_phi_ema.clone();
10473 if let Some(new_active) = router.step(&phi, pos) {
10474 let _ = self.set_active_skill(new_active);
10475 }
10476 }
10477 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10478 })();
10479
10480 let _ = self.set_active_skill(saved_active);
10483 self.dyn_router = Some(router);
10484 self.nll_end();
10485 result
10486 }
10487
10488 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10490 self.clear_sequence_state();
10491 let mut acc = vec![0f32; self.hidden_size];
10492 for (pos, &id) in ids.iter().enumerate() {
10493 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10494 for (a, v) in acc.iter_mut().zip(&h) {
10495 *a += v;
10496 }
10497 }
10498 let n = ids.len().max(1) as f32;
10499 for a in acc.iter_mut() {
10500 *a /= n;
10501 }
10502 self.clear_sequence_state();
10503 acc
10504 }
10505
10506 pub fn probe_phi_span(
10520 &mut self,
10521 ids: &[u32],
10522 layer: usize,
10523 span: std::ops::Range<usize>,
10524 ) -> Vec<f32> {
10525 #[cfg(target_os = "macos")]
10526 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10527 let end = span.end.min(ids.len());
10528 let start = span.start.min(end);
10529 let reset = |p: &mut Self| p.clear_sequence_state();
10530 reset(self);
10531 let mut acc = vec![0f32; self.hidden_size];
10532 for (pos, &id) in ids[..end].iter().enumerate() {
10533 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10534 if pos >= start {
10535 for (a, v) in acc.iter_mut().zip(&h) {
10536 *a += v;
10537 }
10538 }
10539 }
10540 let n = end - start;
10541 if n > 0 {
10542 let n = n as f32;
10543 for a in acc.iter_mut() {
10544 *a /= n;
10545 }
10546 }
10547 reset(self);
10548 acc
10549 }
10550
10551 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10558 #[cfg(target_os = "macos")]
10559 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10560 self.graph_logits = None;
10561 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10562 if let Some(logits) = self.graph_logits.take() {
10563 return logits;
10564 }
10565 inference::rms_norm_into(
10566 &hidden,
10567 &self.weights.final_norm,
10568 self.rms_eps,
10569 self.norm_style,
10570 &mut self.ws.n1,
10571 );
10572 self.lm_head_forward(&self.ws.n1)
10573 }
10574
10575 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10581 self.prefill_batch_masked(ids, start_pos, None)
10582 }
10583
10584 fn prefill_batch_masked(
10590 &mut self,
10591 ids: &[u32],
10592 start_pos: usize,
10593 task_mask: Option<&TaskMask>,
10594 ) -> Vec<f32> {
10595 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10596 }
10597
10598 fn prefill_rows(
10606 &mut self,
10607 ids: &[u32],
10608 pos: usize,
10609 task_mask: Option<&TaskMask>,
10610 ) -> Result<Vec<f32>, String> {
10611 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10612 }
10613
10614 fn prefill_input_rows(
10615 &mut self,
10616 input: PrefillIn<'_>,
10617 pos: usize,
10618 task_mask: Option<&TaskMask>,
10619 ) -> Result<Vec<f32>, String> {
10620 self.mimo_moe_prepare();
10621 let hs = self.hidden_size;
10622 let bk = match input {
10623 PrefillIn::Ids(ids) => ids.len(),
10624 PrefillIn::Hidden(rows) => rows.len() / hs,
10625 };
10626 #[cfg(not(target_os = "macos"))]
10627 if task_mask.is_none()
10628 && !self.o1_active()
10629 && bk > 1
10630 && (self.batch_prefix_prefill()
10631 || (self.verify_exact_moe
10632 && crate::gpu::enabled_here()
10633 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10634 {
10635 let mut hiddens = match input {
10636 PrefillIn::Hidden(rows) => rows.to_vec(),
10637 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10638 };
10639 let positions: Vec<usize> = (pos..pos + bk).collect();
10640 let mut run = 0usize;
10641 match self.try_batch_graph_wgpu_prefix(
10642 &mut hiddens,
10643 &positions,
10644 bk,
10645 None,
10646 Some(&mut run),
10647 ) {
10648 crate::gpu::BatchGraphOutcome::Completed => {
10649 let out = if run < self.num_layers {
10650 self.prefill_batch_span(
10651 PrefillIn::Hidden(&hiddens),
10652 pos,
10653 None,
10654 run,
10655 self.num_layers,
10656 )
10657 } else {
10658 hiddens
10659 };
10660 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10661 Err("MiMo attention graph failed after admission".into())
10662 } else {
10663 Ok(out)
10664 };
10665 }
10666 crate::gpu::BatchGraphOutcome::Failed => {
10667 return Err("batched prefix prefill failed after admission".into());
10668 }
10669 crate::gpu::BatchGraphOutcome::Declined => {
10670 #[cfg(feature = "gpu")]
10672 self.pull_lagging_host_kv(0, self.num_layers, pos);
10673 }
10674 }
10675 }
10676 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10677 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10678 Err("batch tail graph failed after admission".into())
10679 } else {
10680 Ok(out)
10681 }
10682 }
10683
10684 fn prefill_batch_span(
10690 &mut self,
10691 input: PrefillIn<'_>,
10692 start_pos: usize,
10693 task_mask: Option<&TaskMask>,
10694 from: usize,
10695 upto_excl: usize,
10696 ) -> Vec<f32> {
10697 let hs = self.hidden_size;
10698 let b = match input {
10699 PrefillIn::Ids(ids) => ids.len(),
10700 PrefillIn::Hidden(hb) => hb.len() / hs,
10701 };
10702 let upto_excl = upto_excl.min(self.num_layers);
10703 let mut h: Vec<f32>;
10707 let mut h_ready;
10708 match input {
10709 PrefillIn::Ids(_) => {
10710 h = vec![0.0; b * hs];
10711 h_ready = false;
10712 }
10713 PrefillIn::Hidden(hb) => {
10714 h = hb.to_vec();
10715 h_ready = true;
10716 }
10717 }
10718 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10719 if let PrefillIn::Ids(ids) = input {
10720 for (bi, &id) in ids.iter().enumerate() {
10721 let e = me.embed_single(id);
10722 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10723 }
10724 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10725 if let Ok(t) = tp.parse::<usize>() {
10726 if t >= start_pos && t < start_pos + ids.len() {
10727 let bi = t - start_pos;
10728 let row = &h[bi * hs..(bi + 1) * hs];
10729 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10730 eprintln!(
10731 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10732 ids[bi],
10733 row[0],
10734 row[1],
10735 ids.len(),
10736 &ids[..ids.len().min(8)]
10737 );
10738 }
10739 }
10740 }
10741 }
10742 };
10743 let (_nkv, _hd, _rd, eps) = (
10744 self.num_kv_heads,
10745 self.head_dim,
10746 self.rotary_dim,
10747 self.rms_eps,
10748 );
10749 let pool = self.pool.clone();
10750 let norm_style = self.norm_style;
10751 self.mimo_moe_prepare();
10752 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10753
10754 #[cfg(target_os = "macos")]
10755 let mut chunk_skip_until = 0usize;
10756 for li in from..upto_excl {
10757 let _capacity_tail = automatic_gpu_prefix
10758 .filter(|&prefix| {
10759 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10760 })
10761 .map(|_| crate::gpu::enter_cpu_scope());
10762 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10767 let _fast_gemm = self
10768 .proj_gate_sigmoid
10769 .then(crate::gpu::enter_prefill_fast_gemm);
10770 #[cfg(target_os = "macos")]
10776 if task_mask.is_none() {
10777 if li < chunk_skip_until {
10778 continue;
10779 }
10780 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10786 fill_h(&mut h, self);
10787 h_ready = true;
10788 }
10789 let ids_for_embed = match input {
10790 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10791 PrefillIn::Hidden(_) => None,
10792 };
10793 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10794 if end > li {
10795 h_ready = true;
10796 chunk_skip_until = end;
10797 if self.is_loop_end(end - 1) && end < self.num_layers {
10800 for bi in 0..b {
10801 let normed = inference::rms_norm(
10802 &h[bi * hs..(bi + 1) * hs],
10803 &self.weights.final_norm,
10804 eps,
10805 norm_style,
10806 );
10807 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10808 }
10809 }
10810 continue;
10811 }
10812 }
10813 if !h_ready {
10814 fill_h(&mut h, self);
10815 h_ready = true;
10816 }
10817 if task_mask.is_none() && self.verify_exact_moe {
10818 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10819 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10820 crate::gpu::BatchGraphOutcome::Completed => continue,
10821 crate::gpu::BatchGraphOutcome::Failed => return h,
10822 crate::gpu::BatchGraphOutcome::Declined => {}
10823 }
10824 }
10825 #[cfg(feature = "gpu")]
10826 self.pull_lagging_host_kv(li, li + 1, start_pos);
10827 let lw = &self.weights.layers[self.phys_layer(li)];
10828 let t_attn = std::time::Instant::now();
10829 match &lw.attn {
10831 AttnKind::Kda(w) => {
10832 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10834 let mut normed = vec![0.0f32; b * hs];
10835 for bi in 0..b {
10836 inference::rms_norm_into(
10837 &h[bi * hs..(bi + 1) * hs],
10838 &lw.input_norm,
10839 eps,
10840 norm_style,
10841 &mut normed[bi * hs..(bi + 1) * hs],
10842 );
10843 }
10844 let attn = crate::linear_core::kda_forward_batch(
10845 &normed,
10846 b,
10847 w,
10848 &cfg,
10849 &mut self.kv_cache.layers[li].linear_state,
10850 pool.as_deref(),
10851 );
10852 for (dst, &a) in h.iter_mut().zip(&attn) {
10853 *dst += a;
10854 }
10855 }
10856 AttnKind::LinearGdn(w) => {
10857 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10859 let mut normed = vec![0.0f32; b * hs];
10860 for bi in 0..b {
10861 let r = inference::rms_norm(
10862 &h[bi * hs..(bi + 1) * hs],
10863 &lw.input_norm,
10864 eps,
10865 norm_style,
10866 );
10867 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10868 }
10869 let attn = crate::linear_core::gdn_forward_batch(
10870 &normed,
10871 b,
10872 w,
10873 &cfg,
10874 &mut self.kv_cache.layers[li].linear_state,
10875 pool.as_deref(),
10876 );
10877 for (dst, &a) in h.iter_mut().zip(&attn) {
10878 *dst += a;
10879 }
10880 }
10881 AttnKind::ShortConv(w) => {
10882 let cfg = self
10885 .short_conv_cfg
10886 .expect("short-conv layer without short_conv_cfg");
10887 let mut normed = vec![0.0f32; b * hs];
10888 for bi in 0..b {
10889 inference::rms_norm_into(
10890 &h[bi * hs..(bi + 1) * hs],
10891 &lw.input_norm,
10892 eps,
10893 norm_style,
10894 &mut normed[bi * hs..(bi + 1) * hs],
10895 );
10896 }
10897 let attn = short_conv_forward_batch(
10898 &normed,
10899 b,
10900 w,
10901 &cfg,
10902 &mut self.kv_cache.layers[li].linear_state,
10903 pool.as_deref(),
10904 );
10905 for (dst, &a) in h.iter_mut().zip(&attn) {
10906 *dst += a;
10907 }
10908 }
10909 AttnKind::Mla(w) => {
10910 let inv_freq_l = self.layer_inv_freq(li);
10913 let rs = self.layer_rope_scale(li);
10914 let mut normed = vec![0.0f32; hs];
10915 for bi in 0..b {
10916 inference::rms_norm_into(
10917 &h[bi * hs..(bi + 1) * hs],
10918 &lw.input_norm,
10919 eps,
10920 norm_style,
10921 &mut normed,
10922 );
10923 let ao = mla_attention(
10924 w,
10925 &normed,
10926 &mut self.kv_cache.layers[li],
10927 start_pos + bi,
10928 &inv_freq_l,
10929 rs,
10930 eps,
10931 pool.as_deref(),
10932 );
10933 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10934 *dst += a;
10935 }
10936 }
10937 }
10938 AttnKind::Full {
10939 wq,
10940 wk,
10941 wv,
10942 wo,
10943 q_norm,
10944 k_norm,
10945 output_gate,
10946 softplus_gate,
10947 bias,
10948 } => {
10949 let mut normed = vec![0.0f32; b * hs];
10953 for bi in 0..b {
10954 inference::rms_norm_into(
10955 &h[bi * hs..(bi + 1) * hs],
10956 &lw.input_norm,
10957 eps,
10958 norm_style,
10959 &mut normed[bi * hs..(bi + 1) * hs],
10960 );
10961 }
10962 let inv_freq_l = self.layer_inv_freq(li);
10963 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10964 let cfg = QwenAttnCfg {
10965 num_heads: self.layer_num_heads(li),
10966 num_kv_heads: nkv_l,
10967 head_dim: hd_l,
10968 hidden_size: hs,
10969 position: start_pos,
10970 inv_freq: &inv_freq_l,
10971 rotary_dim: rd_l,
10972 scale: self.attn_scale,
10973 softcap: self.attn_softcap,
10974 window: self.layer_window(li),
10975 v_norm: self.attn_v_norm,
10976 qk_norm_after_rope: self.qk_norm_after_rope,
10977 gate_sigmoid: self.proj_gate_sigmoid,
10978 q_norm: q_norm.as_deref(),
10979 k_norm: k_norm.as_deref(),
10980 output_gate: *output_gate,
10981 softplus_gate: softplus_gate
10982 .as_ref()
10983 .map(|(gate, per_head)| (gate, *per_head)),
10984 rope_scale: self.layer_rope_scale(li),
10985 bias: bias
10986 .as_ref()
10987 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10988 rms_eps: eps,
10989 norm_style,
10990 pool: pool.as_deref(),
10991 v_head_dim: self.layer_v_dim(li),
10992 };
10993 #[cfg(feature = "gpu")]
10994 let _mirror = self
10995 .prefill_mirror_target(li)
10996 .map(crate::gpu::enter_prefill_mirror);
10997 let mut attn = attention::qwen_attention_batch(
10998 &normed,
10999 b,
11000 wq,
11001 wk,
11002 wv,
11003 wo,
11004 &mut self.kv_cache.layers[li],
11005 &cfg,
11006 );
11007 if let Some(w) = &lw.attn_out_norm {
11008 for bi in 0..b {
11009 inference::rms_norm_into(
11010 &attn[bi * hs..(bi + 1) * hs],
11011 w,
11012 eps,
11013 norm_style,
11014 &mut normed[bi * hs..(bi + 1) * hs],
11015 );
11016 }
11017 attn.copy_from_slice(&normed);
11018 }
11019 for (dst, &a) in h.iter_mut().zip(&attn) {
11020 *dst += a;
11021 }
11022 }
11023 AttnKind::Bounded(w) => {
11024 let mut normed = vec![0.0f32; b * hs];
11027 for bi in 0..b {
11028 inference::rms_norm_into(
11029 &h[bi * hs..(bi + 1) * hs],
11030 &lw.input_norm,
11031 eps,
11032 norm_style,
11033 &mut normed[bi * hs..(bi + 1) * hs],
11034 );
11035 }
11036 let rope = self
11037 .bounded_rope
11038 .clone()
11039 .expect("bounded layer without an installed rotation table");
11040 let cfg = crate::bounded::BoundedAttnCfg {
11041 num_heads: self.num_heads,
11042 num_kv_heads: self.num_kv_heads,
11043 head_dim: self.head_dim,
11044 hidden_size: hs,
11045 scale: self.attn_scale,
11046 rope: &rope,
11047 pool: pool.as_deref(),
11048 };
11049 let mut attn = crate::bounded::bounded_attention_batch(
11050 &normed,
11051 b,
11052 w,
11053 &mut self.kv_cache.layers[li],
11054 &cfg,
11055 );
11056 if let Some(wn) = &lw.attn_out_norm {
11057 for bi in 0..b {
11058 inference::rms_norm_into(
11059 &attn[bi * hs..(bi + 1) * hs],
11060 wn,
11061 eps,
11062 norm_style,
11063 &mut normed[bi * hs..(bi + 1) * hs],
11064 );
11065 }
11066 attn.copy_from_slice(&normed);
11067 }
11068 for (dst, &a) in h.iter_mut().zip(&attn) {
11069 *dst += a;
11070 }
11071 attention::recycle_buf(&mut attn);
11072 }
11073 AttnKind::Linear(w) => {
11074 for bi in 0..b {
11075 let normed = inference::rms_norm(
11076 &h[bi * hs..(bi + 1) * hs],
11077 &lw.input_norm,
11078 eps,
11079 norm_style,
11080 );
11081 vmf_phase_forward(
11082 &normed,
11083 w,
11084 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11085 &mut self.kv_cache.layers[li].linear_state,
11086 pool.as_deref(),
11087 )
11088 .iter()
11089 .enumerate()
11090 .for_each(|(i, &a)| h[bi * hs + i] += a);
11091 }
11092 }
11093 }
11094
11095 let lw = &self.weights.layers[self.phys_layer(li)];
11097 let mut post = vec![0.0f32; b * hs];
11098 for bi in 0..b {
11099 let r =
11100 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11101 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11102 }
11103 let mask_row = task_mask
11106 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11107 .and_then(|m| m.ffn_masks.get(li))
11108 .map(|v| v.as_slice());
11109 let attn_ns = t_attn.elapsed().as_nanos() as u64;
11110 let t_ffn = std::time::Instant::now();
11111 let mut ffn = match &lw.ffn {
11112 FfnKind::Dense(d) if !d.segs.is_empty() => {
11113 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11114 }
11115 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11116 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11117 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11118 }
11119 FfnKind::Moe(m) if self.verify_exact_moe => {
11120 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11121 }
11122 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11125 let before = m.stats.borrow().clone();
11126 let out = crate::gpu::cpu_scope(|| {
11127 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11128 });
11129 self.mimo_moe.prime(li, m, &before);
11130 out
11131 }
11132 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11133 FfnKind::DenseMoe(dm) => {
11136 let mut out = vec![0.0f32; b * hs];
11137 for bi in 0..b {
11138 let r = dense_moe_ffn(
11139 dm,
11140 &post[bi * hs..(bi + 1) * hs],
11141 &h[bi * hs..(bi + 1) * hs],
11142 eps,
11143 norm_style,
11144 pool.as_deref(),
11145 );
11146 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11147 }
11148 out
11149 }
11150 };
11151 if prefill_prof_on() {
11152 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11153 PREFILL_SPLIT[1].fetch_add(
11154 t_ffn.elapsed().as_nanos() as u64,
11155 std::sync::atomic::Ordering::Relaxed,
11156 );
11157 }
11158 if let Some(w) = &lw.ffn_out_norm {
11159 for bi in 0..b {
11160 inference::rms_norm_into(
11161 &ffn[bi * hs..(bi + 1) * hs],
11162 w,
11163 eps,
11164 norm_style,
11165 &mut post[bi * hs..(bi + 1) * hs],
11166 );
11167 }
11168 ffn.copy_from_slice(&post);
11169 }
11170 for (dst, &f) in h.iter_mut().zip(&ffn) {
11171 *dst += f;
11172 }
11173 if let Some(sc) = lw.layer_scale {
11174 for v in h.iter_mut() {
11175 *v *= sc;
11176 }
11177 }
11178 if self.layer_dump.is_some() {
11180 for bi in 0..b {
11181 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11182 }
11183 }
11184 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11185 if let Ok(t) = tp.parse::<usize>() {
11186 if t >= start_pos && t < start_pos + b {
11187 let bi = t - start_pos;
11188 let row = &h[bi * hs..(bi + 1) * hs];
11189 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11190 eprintln!(
11191 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11192 row[0], row[1]
11193 );
11194 }
11195 }
11196 }
11197 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11201 let row = &h[(b - 1) * hs..b * hs];
11202 let rms =
11203 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11204 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11205 eprintln!(
11206 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11207 match &self.weights.layers[self.phys_layer(li)].attn {
11208 AttnKind::LinearGdn(_) => "gdn",
11209 AttnKind::Linear(_) => "vmf",
11210 AttnKind::ShortConv(_) => "conv",
11211 _ => "attn",
11212 },
11213 match &lw.ffn {
11214 FfnKind::Moe(_) => "moe",
11215 FfnKind::Dense(_) => "dense",
11216 FfnKind::DenseMoe(_) => "dense+moe",
11217 },
11218 );
11219 }
11220 if self.is_loop_end(li) && li + 1 < self.num_layers {
11222 for bi in 0..b {
11223 let normed = inference::rms_norm(
11224 &h[bi * hs..(bi + 1) * hs],
11225 &self.weights.final_norm,
11226 eps,
11227 norm_style,
11228 );
11229 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11230 }
11231 }
11232 if std::env::var("CMF_TRACE_H").is_ok() {
11233 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11234 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11235 eprintln!(
11236 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11237 lw.layer_scale
11238 );
11239 }
11240 }
11241 crate::gpu::set_layer(-1); if prefill_prof_on() {
11243 let ms = |a: &std::sync::atomic::AtomicU64| {
11244 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11245 };
11246 let sp = &attention::ATTN_SPLIT;
11247 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11248 eprintln!(
11249 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11250 host gate {:.1}], host loop {:.1}, attend {:.1} \
11251 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11252 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11253 ms(&PREFILL_SPLIT[0]),
11254 ms(&sp[0]),
11255 qkv_enq,
11256 qkv_wait,
11257 ms(&sp[6]),
11258 ms(&sp[1]),
11259 ms(&sp[2]),
11260 ms(&sp[4]),
11261 ms(&sp[5]),
11262 ca_wait,
11263 ms(&sp[3]),
11264 ms(&PREFILL_SPLIT[1]),
11265 );
11266 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11267 eprintln!(
11268 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11269 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11270 fallbacks (cumulative)",
11271 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11272 );
11273 }
11274 self.o1_progress();
11279 self.swa_trim_tails();
11281 h
11282 }
11283
11284 fn embed_single(&self, id: u32) -> Vec<f32> {
11286 let mut out = vec![0.0f32; self.hidden_size];
11287 if (id as usize) < self.weights.embed_tokens.rows() {
11288 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11289 }
11290 if self.embed_multiplier != 1.0 {
11291 for v in out.iter_mut() {
11292 *v *= self.embed_multiplier;
11293 }
11294 }
11295 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11299 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11300 v[0] = id as f32;
11301 return v;
11302 }
11303 if let Some(b) = &self.g4e {
11307 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11308 }
11309 if let Some(b) = &self.g3n {
11310 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11311 }
11312 out
11313 }
11314
11315 #[cfg(target_os = "macos")]
11321 fn chunk_run_gpu(
11322 &mut self,
11323 li0: usize,
11324 h: &mut [f32],
11325 b: usize,
11326 pos0: usize,
11327 embed_ids: Option<&[u32]>,
11328 cap: usize,
11329 ) -> usize {
11330 let gemma4 =
11337 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11338 if !crate::gpu::enabled_here()
11339 || std::env::var("CMF_GPU_CHUNK")
11340 .map(|v| v == "0")
11341 .unwrap_or(false)
11342 || b < self.chunk_min_rows
11343 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11346 || (self.global_attn.is_some() && !gemma4)
11347 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11351 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11354 || self.o1_active()
11357 || (self.attn_v_norm && !gemma4)
11358 || !self.metal_rope_scale_ok()
11359 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11360 {
11361 return li0;
11362 }
11363 let Some(model) = self.model.clone() else {
11364 return li0;
11365 };
11366 let (nh, nkv, hd, hs) = (
11367 self.num_heads,
11368 self.num_kv_heads,
11369 self.head_dim,
11370 self.hidden_size,
11371 );
11372 let loop_end = if self.loop_final_norm {
11376 ((li0 / self.physical_layers) + 1) * self.physical_layers
11377 } else {
11378 self.num_layers
11379 };
11380 let run_end = self.num_layers.min(loop_end).min(cap);
11381 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11386 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11387 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11388 let mut stored_at: Vec<usize> = Vec::new();
11389 for li in li0..run_end {
11390 let lw = &self.weights.layers[self.phys_layer(li)];
11391 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11392 && !gemma4
11393 {
11394 break;
11395 }
11396 let AttnKind::Full {
11397 wq,
11398 wk,
11399 wv,
11400 wo,
11401 q_norm,
11402 k_norm,
11403 output_gate: false,
11404 softplus_gate,
11405 bias,
11406 } = &lw.attn
11407 else {
11408 break;
11409 };
11410 let head_gate = match softplus_gate {
11412 None => None,
11413 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11414 Some((d, r, c)) if r == nh && c == hs => Some(d),
11415 _ => break,
11416 },
11417 Some(_) => break,
11418 };
11419 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11423 FfnKind::Dense(d)
11424 if (matches!(d.act, Act::Silu | Act::Gelu)
11425 || (gemma4 && d.act == Act::GeluTanh))
11426 && d.segs.is_empty() =>
11427 {
11428 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11429 }
11430 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11431 Some((router, experts, q8)) => {
11432 let e0 = &m.experts[0];
11433 let route = Box::new(move |lg: &[f32]| {
11434 let (idx, p, wsum) = moe_route(lg, m, None);
11436 let mut st = m.stats.borrow_mut();
11437 if st.len() < m.experts.len() {
11438 st.resize(m.experts.len(), 0);
11439 }
11440 for &e in &idx {
11441 st[e] += 1;
11442 }
11443 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11444 (idx, w)
11445 });
11446 (
11447 &e0.gate_proj,
11448 &e0.up_proj,
11449 &e0.down_proj,
11450 Act::Silu,
11451 Some(crate::gpu_metal::ChunkMoe {
11452 router,
11453 experts,
11454 q8,
11455 route,
11456 }),
11457 )
11458 }
11459 None => break,
11460 },
11461 _ => break,
11462 };
11463 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11469 t.q8_row_parts()
11470 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11471 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11472 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11473 }
11474 fn cf(t: &QTensor) -> &[f32] {
11475 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11476 }
11477 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11478 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
11479 if moe.is_none()
11489 && !gemma4
11490 && ffn_act != Act::Gelu
11491 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
11492 {
11493 break;
11494 }
11495 let parts = (
11496 cw(wq),
11497 cw(wk),
11498 cw(wv),
11499 cw(wo),
11500 cw(gate_t),
11501 cw(up_t),
11502 cw(down_t),
11503 );
11504 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11505 else {
11506 break;
11507 };
11508 let layer = &self.kv_cache.layers[li];
11509 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11510 break;
11511 }
11512 debug_assert!(
11514 layer.base() == 0
11515 || self
11516 .layer_window(li)
11517 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11518 );
11519 stored_at.push(layer.head_len(0));
11520 let (nkv, hd) = if gemma4 {
11523 let (a, bb, _) = self.layer_geom(li);
11524 (a, bb)
11525 } else {
11526 (nkv, hd)
11527 };
11528 layers.push(crate::gpu_metal::ChunkLayer {
11529 model: &model,
11530 kv_id: self.graph_kv_id,
11531 layer: li,
11532 wq: pq,
11533 wk: pk,
11534 wv: pv,
11535 wo: po,
11536 gate: pg,
11537 up: pu,
11538 down: pd,
11539 input_norm: &lw.input_norm,
11540 post_norm: &lw.post_norm,
11541 bias: bias
11542 .as_ref()
11543 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11544 q_norm: q_norm.as_deref(),
11545 k_norm: k_norm.as_deref(),
11546 inv_freq: &tables[li - li0],
11547 rope_scale: self.layer_rope_scale(li),
11548 rd: self.layer_geom(li).2,
11549 nh,
11550 nkv,
11551 hd,
11552 hs,
11553 inter: gate_t.rows(),
11554 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11555 late_qk_norm: self.qk_norm_after_rope,
11556 eps: self.rms_eps as f32,
11557 window: self.layer_window(li),
11558 head_gate,
11559 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11560 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11561 v_norm: self.attn_v_norm,
11562 scale: self.attn_scale,
11563 attn_out_norm: lw.attn_out_norm.as_deref(),
11564 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11565 layer_scale: lw.layer_scale,
11566 }),
11567 col,
11568 ffn_col: if moe.is_none() && !gemma4 {
11571 ffn_col
11572 } else {
11573 [&[][..]; 3]
11574 },
11575 moe,
11576 });
11577 }
11578 if layers.is_empty() {
11579 return li0;
11580 }
11581 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11583 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11584 .iter()
11585 .zip(&rows_kv)
11586 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11587 .collect();
11588 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11589 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11590 let li = layers[i].layer;
11591 let layer = &self.kv_cache.layers[li];
11592 let nkv = rows_kv[i].0;
11593 io.push(crate::gpu_metal::ChunkIo {
11594 cpu_stored: stored_at[i],
11595 cpu_gen: layer.generation(),
11596 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11597 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11598 out_k: ok,
11599 out_v: ov,
11600 imp: oi,
11601 });
11602 }
11603 let n_run = layers.len();
11604 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11605 let ep = embed_ids.and_then(|ids| {
11608 self.weights
11609 .embed_tokens
11610 .q8_row_parts()
11611 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11612 idx,
11613 rows,
11614 row_scale: rs,
11615 ids,
11616 mult: self.embed_multiplier,
11617 })
11618 });
11619 if embed_ids.is_some() && ep.is_none() {
11620 return li0;
11621 }
11622 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11623 return li0;
11624 }
11625 drop(io);
11626 drop(layers);
11627 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11630 let li = li0 + i;
11631 let row = rows_kv[i].1;
11632 let layer = &mut self.kv_cache.layers[li];
11633 for bi in 0..b {
11634 layer.append(
11635 &ok[bi * row..(bi + 1) * row],
11636 &ov[bi * row..(bi + 1) * row],
11637 &[],
11638 );
11639 }
11640 layer.accumulate_imp(oi);
11641 }
11642 last
11643 }
11644
11645 fn layer_is_local(&self, li: usize) -> bool {
11648 if let Some(layers) = &self.sliding_layers {
11649 return layers.get(li).copied().unwrap_or(false);
11650 }
11651 match self.swa {
11652 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11653 None => false,
11654 }
11655 }
11656
11657 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11660 if self.layer_is_local(li) {
11661 if let Some(f) = &self.inv_freq_local {
11662 return f.clone();
11663 }
11664 } else if let Some(f) = &self.inv_freq_global {
11665 return f.clone();
11666 }
11667 self.inv_freq.clone()
11668 }
11669
11670 fn layer_window(&self, li: usize) -> Option<usize> {
11672 self.swa
11673 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11674 }
11675
11676 fn swa_trim_tails(&mut self) {
11685 let Some((slack, align)) = self.swa_trim else {
11686 return;
11687 };
11688 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11689 return;
11690 }
11691 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11692 if let Some(w) = self.layer_window(li) {
11693 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
11694 }
11695 }
11696 }
11697
11698 fn layer_num_heads(&self, li: usize) -> usize {
11699 self.attention_heads_per_layer
11700 .as_ref()
11701 .and_then(|v| v.get(li).copied())
11702 .unwrap_or(self.num_heads)
11703 }
11704
11705 fn layer_rope_scale(&self, li: usize) -> f32 {
11706 if self.layer_is_local(li) {
11707 self.rope_scale_local
11708 } else {
11709 self.rope_scale
11710 }
11711 }
11712
11713 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11716 if !self.layer_is_local(li) {
11717 if let Some((ghd, gkv)) = self.global_attn {
11718 return (gkv, ghd, ghd);
11719 }
11720 }
11721 let rd = if self.layer_is_local(li) {
11722 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11723 } else {
11724 self.rotary_dim
11725 };
11726 let rd = if self
11728 .no_rope_layers
11729 .as_ref()
11730 .and_then(|v| v.get(li))
11731 .copied()
11732 == Some(true)
11733 {
11734 0
11735 } else {
11736 rd
11737 };
11738 (self.layer_num_kv_heads(li), self.head_dim, rd)
11739 }
11740
11741 fn layer_num_kv_heads(&self, li: usize) -> usize {
11744 self.kv_heads_per_layer
11745 .as_ref()
11746 .and_then(|v| v.get(self.phys_layer(li)).copied())
11747 .unwrap_or(self.num_kv_heads)
11748 }
11749
11750 fn layer_v_dim(&self, li: usize) -> usize {
11752 let (_, hd, _) = self.layer_geom(li);
11753 self.v_head_dim.unwrap_or(hd).min(hd)
11754 }
11755
11756 pub fn set_attn_geometry(
11765 &mut self,
11766 kv_heads_per_layer: Option<Vec<usize>>,
11767 v_head_dim: Option<usize>,
11768 ) -> Result<(), String> {
11769 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11770 if self.global_attn.is_some() {
11771 return Err(
11772 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11773 attention geometry"
11774 .into(),
11775 );
11776 }
11777 if self
11778 .weights
11779 .layers
11780 .iter()
11781 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11782 {
11783 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11784 }
11785 }
11786 if let Some(vd) = v_head_dim {
11787 if vd == 0 || vd > self.head_dim {
11788 return Err(format!(
11789 "v_head_dim {vd} must be in 1..={} (head_dim)",
11790 self.head_dim
11791 ));
11792 }
11793 }
11794 if let Some(v) = &kv_heads_per_layer {
11795 if v.len() != self.physical_layers {
11796 return Err(format!(
11797 "kv_heads_per_layer has {} entries, expected {} layers",
11798 v.len(),
11799 self.physical_layers
11800 ));
11801 }
11802 for (li, &nkv) in v.iter().enumerate() {
11803 let is_attn = matches!(
11804 self.weights.layers.get(li).map(|lw| &lw.attn),
11805 Some(AttnKind::Full { .. }) | None
11806 );
11807 if !is_attn {
11808 continue;
11809 }
11810 let nh = self
11811 .attention_heads_per_layer
11812 .as_ref()
11813 .and_then(|h| h.get(li).copied())
11814 .unwrap_or(self.num_heads);
11815 if nkv == 0 || nh % nkv != 0 {
11816 return Err(format!(
11817 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11818 ));
11819 }
11820 }
11821 }
11822 self.kv_heads_per_layer = kv_heads_per_layer;
11823 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11824 if self.kv_heads_per_layer.is_some() {
11825 for li in 0..self.kv_cache.layers.len() {
11826 let full = matches!(
11827 self.weights
11828 .layers
11829 .get(self.phys_layer(li))
11830 .map(|lw| &lw.attn),
11831 Some(AttnKind::Full { .. })
11832 );
11833 let nkv = self.layer_num_kv_heads(li);
11834 let cache = &self.kv_cache.layers[li];
11835 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11836 let sinks = cache.sinks.clone();
11837 self.kv_cache.layers[li] =
11838 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11839 self.kv_cache.layers[li].sinks = sinks;
11840 }
11841 }
11842 }
11843 Ok(())
11844 }
11845
11846 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11850 let Some(lw) = self.weights.layers.get(phys) else {
11851 return Err(format!("sinks for layer {phys}: no such layer"));
11852 };
11853 if !matches!(lw.attn, AttnKind::Full { .. }) {
11854 return Err(format!(
11855 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11856 ));
11857 }
11858 let nh = self
11859 .attention_heads_per_layer
11860 .as_ref()
11861 .and_then(|h| h.get(phys).copied())
11862 .unwrap_or(self.num_heads);
11863 if sinks.len() != nh {
11864 return Err(format!(
11865 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11866 sinks.len()
11867 ));
11868 }
11869 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11870 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11871 }
11872 for li in 0..self.kv_cache.layers.len() {
11873 if self.phys_layer(li) == phys {
11874 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11875 }
11876 }
11877 Ok(())
11878 }
11879
11880 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11890 if self.kv_heads_per_layer.is_some() {
11891 return Some("per-layer KV head counts");
11892 }
11893 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11894 return Some("V heads narrower than Q/K heads");
11895 }
11896 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11897 return Some("learned attention sinks");
11898 }
11899 if self.swa.is_some() || self.sliding_layers.is_some() {
11900 return Some("sliding-window layers");
11901 }
11902 None
11903 }
11904
11905 #[cfg(target_os = "macos")]
11913 fn metal_rope_scale_ok(&self) -> bool {
11914 !(self.qk_norm_after_rope
11915 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11916 }
11917
11918 #[cfg(target_os = "macos")]
11935 fn metal_graph_swa(&self) -> bool {
11936 (self.swa.is_some() || self.sliding_layers.is_some())
11937 && self.attention_heads_per_layer.is_none()
11938 && !self.attn_v_norm
11939 && self.attn_softcap == 0.0
11940 && self.kv_heads_per_layer.is_none()
11941 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11942 && self.global_attn.is_none()
11943 && self.inv_freq_global.is_none()
11944 && self.metal_rope_scale_ok()
11945 && !(0..self.num_layers).any(|li| {
11946 self.layer_is_local(li)
11947 && self.inv_freq_local.is_none()
11948 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11949 })
11950 }
11951
11952 #[cfg(target_os = "macos")]
11960 fn metal_gemma4(&self) -> bool {
11961 crate::gpu_metal::gemma4::enabled()
11962 && (self.global_attn.is_some() || self.attn_v_norm)
11963 && self.attention_heads_per_layer.is_none()
11964 && self.kv_heads_per_layer.is_none()
11965 && self.v_head_dim.is_none()
11966 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11967 && self.attn_softcap == 0.0
11968 && !self.qk_norm_after_rope
11969 && self.metal_rope_scale_ok()
11970 && self
11971 .weights
11972 .layers
11973 .iter()
11974 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11975 && !(0..self.num_layers).any(|li| {
11976 self.layer_is_local(li)
11977 && self.inv_freq_local.is_none()
11978 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11979 })
11980 }
11981
11982 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11989 let geometry_reason = self.graph_attn_decline_reason();
11993 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11994 if has_scaled_rope && geometry_reason.is_none() {
11995 return Some("scaled RoPE without per-layer geometry");
11996 }
11997 if has_scaled_rope && self.qk_norm_after_rope {
12002 return Some("scaled RoPE with post-RoPE Q/K norm");
12003 }
12004 geometry_reason?;
12005 if self.attention_heads_per_layer.is_some() {
12011 return Some("per-layer Q head counts with per-layer geometry");
12012 }
12013 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
12017 if !gemma4
12018 && self.weights.layers.iter().any(|lw| {
12019 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12020 })
12021 {
12022 return Some("sandwich norms / layer scale with per-layer geometry");
12023 }
12024 if self.weights.layers.iter().any(|lw| {
12028 matches!(
12029 &lw.attn,
12030 AttnKind::Full {
12031 output_gate: true,
12032 ..
12033 }
12034 )
12035 }) && self.v_head_dim.is_some()
12036 {
12037 return Some("gated attention with V narrower than K");
12038 }
12039 if (0..self.num_layers).any(|li| {
12040 self.layer_is_local(li)
12041 && self.inv_freq_local.is_none()
12042 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
12043 }) {
12044 return Some("local rotary width without a local RoPE table");
12045 }
12046 None
12047 }
12048
12049 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
12053 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
12056 return None;
12057 }
12058 if self.global_attn.is_some() {
12059 return Some("per-layer head width (Gemma-4 global layers)");
12060 }
12061 if self.attn_v_norm {
12062 return Some("V norm");
12063 }
12064 if self.weights.layers.iter().any(|lw| {
12065 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
12066 }) {
12067 return Some("sandwich norms / layer scale");
12068 }
12069 None
12070 }
12071
12072 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
12077 self.graph_attn_decline_reason()?;
12078 let (nkv, hd, rd) = self.layer_geom(li);
12079 let invf: &[f32] = if self.layer_is_local(li) {
12080 match &self.inv_freq_local {
12081 Some(f) => f.as_slice(),
12082 None => self.inv_freq.as_slice(),
12083 }
12084 } else {
12085 match &self.inv_freq_global {
12086 Some(f) => f.as_slice(),
12087 None => self.inv_freq.as_slice(),
12088 }
12089 };
12090 Some(crate::gpu::GraphAttnGeom {
12091 nkv,
12092 hd,
12093 dv: self.layer_v_dim(li),
12094 rd,
12095 invf,
12096 rope_scale: self.layer_rope_scale(li),
12097 window: self.layer_window(li),
12098 sink: self.kv_cache.layers[li].sinks.as_deref(),
12099 v_norm: self.attn_v_norm,
12100 })
12101 }
12102
12103 #[cfg(feature = "gpu")]
12115 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12116 if !self.proj_gate_sigmoid
12117 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12118 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12119 || self.graph_refused()
12120 || self.o1_active()
12121 || self.attn_softcap > 0.0
12122 || self.wgpu_graph_attn_decline().is_some()
12123 {
12124 return None;
12125 }
12126 let g = self.graph_attn_geom(li)?;
12127 let (nkv, hd, _) = self.layer_geom(li);
12128 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12130 return None;
12131 }
12132 Some(crate::gpu::PrefillMirror {
12133 kv_id: self.graph_kv_id,
12134 layer: li,
12135 limit: self.kv_cache.max_seq_len,
12136 })
12137 }
12138
12139 #[cfg(feature = "gpu")]
12148 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12149 let kv_id = self.graph_kv_id;
12150 for li in from..upto.min(self.num_layers) {
12151 if !matches!(
12152 self.weights.layers[self.phys_layer(li)].attn,
12153 AttnKind::Full { .. }
12154 ) {
12155 continue;
12156 }
12157 let host = self.kv_cache.layers[li].pos_len();
12159 if host >= position {
12160 continue;
12161 }
12162 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12163 continue;
12164 };
12165 let to = dev.min(position);
12166 if to <= host {
12167 continue;
12168 }
12169 let (nkv, hd) = {
12170 let c = &self.kv_cache.layers[li];
12171 (c.num_kv_heads, c.head_dim)
12172 };
12173 let Some((k, v, first_valid)) =
12174 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
12175 else {
12176 continue;
12177 };
12178 let need_from = match self.layer_window(li) {
12181 Some(w) => host.max((position + 1).saturating_sub(w)),
12182 None => host,
12183 };
12184 if first_valid > need_from {
12185 tracing::warn!(
12186 "layer {li}: device KV rows {host}..{to} no longer resident \
12187 (from {first_valid}); host attention will miss them"
12188 );
12189 }
12190 let row = nkv * hd;
12191 let cache = &mut self.kv_cache.layers[li];
12192 for p in 0..to - host {
12193 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12194 }
12195 }
12196 }
12197
12198 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12201 let mut seen = self.graph_declines.borrow_mut();
12202 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12203 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12204 seen.push((site, reason));
12205 }
12206 }
12207
12208 pub fn graph_declines(&self) -> Vec<String> {
12211 self.graph_declines
12212 .borrow()
12213 .iter()
12214 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12215 .collect()
12216 }
12217
12218 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12227 let Some(dir) = &self.layer_dump else {
12228 return;
12229 };
12230 let mut bytes = Vec::with_capacity(row.len() * 4);
12231 for v in row {
12232 bytes.extend_from_slice(&v.to_le_bytes());
12233 }
12234 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12235 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12236 use std::sync::atomic::{AtomicBool, Ordering};
12237 static SAID: AtomicBool = AtomicBool::new(false);
12238 if !SAID.swap(true, Ordering::Relaxed) {
12239 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12240 }
12241 }
12242 }
12243
12244 fn mimo_moe_prepare(&mut self) {
12247 if !self.mimo_moe.is_undecided() {
12248 return;
12249 }
12250 let slot = {
12251 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12252 .filter_map(
12253 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12254 FfnKind::Moe(m) => Some((li, m)),
12255 _ => None,
12256 },
12257 )
12258 .collect();
12259 if layers.is_empty()
12262 || self.physical_layers != self.num_layers
12263 || self.gpu_plan.is_some()
12264 {
12265 crate::mimo_moe::Slot::Off
12266 } else {
12267 let graph_prefix =
12271 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12272 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12273 }
12274 };
12275 self.mimo_moe = slot;
12276 }
12277
12278 #[cfg(test)]
12279 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12280 self.graph_kv_id
12281 }
12282
12283 pub(crate) fn mimo_graph_layer_rows(
12287 &mut self,
12288 li: usize,
12289 h: &mut [f32],
12290 positions: &[usize],
12291 ) -> crate::gpu::BatchGraphOutcome {
12292 use crate::gpu::BatchGraphOutcome as Out;
12293 let b = positions.len();
12294 if !(1..=4).contains(&b)
12295 || h.len() != b * self.hidden_size
12296 || !self.mimo_moe.is_dynamic(li, true)
12297 || !crate::gpu::enabled_here()
12298 || !crate::gpu::wgpu_active()
12299 || self.o1_active()
12300 || self.physical_layers != self.num_layers
12301 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12305 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12306 || self.wgpu_graph_attn_decline().is_some()
12307 {
12308 return Out::Declined;
12309 }
12310 let attn_started = std::time::Instant::now();
12311 let outcome = {
12312 let lw = &self.weights.layers[li];
12313 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12314 return Out::Declined;
12315 }
12316 let FfnKind::Moe(m) = &lw.ffn else {
12317 return Out::Declined;
12318 };
12319 let AttnKind::Full {
12320 wq,
12321 wk,
12322 wv,
12323 wo,
12324 q_norm,
12325 k_norm,
12326 output_gate,
12327 softplus_gate,
12328 bias,
12329 } = &lw.attn
12330 else {
12331 return Out::Declined;
12332 };
12333 if *output_gate || softplus_gate.is_some() {
12334 return Out::Declined;
12335 }
12336 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12337 m.experts
12338 .first()?
12339 .gate_proj
12340 .mapped_q4tp()
12341 .map(|(m, _)| m.clone())
12342 }) else {
12343 return Out::Declined;
12344 };
12345 fn gw<'a>(
12346 t: &'a QTensor,
12347 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12348 ) -> Option<crate::gpu::GraphW<'a>> {
12349 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12350 if m.uid() != owner.uid() || t.has_prism_contract() {
12351 return None;
12352 }
12353 return Some(crate::gpu::GraphW {
12354 idx,
12355 kind,
12356 row_scale: rs,
12357 data: &[],
12358 prism: crate::gpu::GraphPrismOp::None,
12359 affine: false,
12360 });
12361 }
12362 t.as_f32().map(|data| crate::gpu::GraphW {
12363 idx: 0,
12364 kind: 4,
12365 row_scale: &[],
12366 data,
12367 prism: crate::gpu::GraphPrismOp::None,
12368 affine: false,
12369 })
12370 }
12371 let (Some(q), Some(k), Some(v), Some(o)) = (
12372 gw(wq, &model),
12373 gw(wk, &model),
12374 gw(wv, &model),
12375 gw(wo, &model),
12376 ) else {
12377 return Out::Declined;
12378 };
12379 let layer = crate::gpu::GraphLayer {
12380 input_norm: &lw.input_norm,
12381 post_norm: &lw.post_norm,
12382 attn_out_norm: None,
12383 ffn_out_norm: None,
12384 layer_scale: None,
12385 ffn: crate::gpu::GraphFfn::AttentionOnly,
12386 attn: crate::gpu::GraphAttn::Full {
12387 wq: q,
12388 wk: k,
12389 wv: v,
12390 wo: o,
12391 q_norm: q_norm.as_deref(),
12392 k_norm: k_norm.as_deref(),
12393 late_qk_norm: self.qk_norm_after_rope,
12394 bias: bias
12395 .as_ref()
12396 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12397 output_gate: false,
12398 cpu_k: self.kv_cache.layers[li].k_heads(),
12399 cpu_v: self.kv_cache.layers[li].v_heads(),
12400 cpu_base: self.kv_cache.layers[li].base(),
12401 geom: self.graph_attn_geom(li),
12402 head_gate: None,
12403 },
12404 };
12405 let (nkv, hd, rd) = self.layer_geom(li);
12406 crate::gpu::forward_batch_graph_at(
12407 &model,
12408 self.graph_kv_id,
12409 li,
12410 &[layer],
12411 &self.inv_freq,
12412 h,
12413 self.layer_num_heads(li),
12414 nkv,
12415 hd,
12416 rd,
12417 self.hidden_size,
12418 1,
12419 positions,
12420 self.kv_cache.max_seq_len,
12421 self.norm_style == cortiq_core::NormStyle::Gemma,
12422 self.rms_eps as f32,
12423 self.attn_scale,
12424 b,
12425 &[],
12426 self.o1_epoch,
12427 None,
12428 None,
12429 )
12430 };
12431 match outcome {
12432 Out::Completed => {}
12433 Out::Declined => return Out::Declined,
12434 Out::Failed => {
12435 self.graph_failed
12436 .store(true, std::sync::atomic::Ordering::Relaxed);
12437 return Out::Failed;
12438 }
12439 }
12440 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12441 let hs = self.hidden_size;
12442 let lw = &self.weights.layers[li];
12443 let FfnKind::Moe(m) = &lw.ffn else {
12444 unreachable!()
12445 };
12446 let mut post = vec![0.0; h.len()];
12447 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12448 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12449 }
12450 let mut ffn = if b == 1 {
12451 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12452 } else {
12453 moe_ffn_banked_rows(
12454 &mut self.mimo_moe,
12455 li,
12456 m,
12457 &post,
12458 b,
12459 hs,
12460 self.pool.as_deref(),
12461 )
12462 };
12463 for (x, &f) in h.iter_mut().zip(&ffn) {
12464 *x += f;
12465 }
12466 attention::recycle_buf(&mut ffn);
12467 if self.layer_dump.is_some() {
12468 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12469 self.dump_layer_row(pos, li, row);
12470 }
12471 }
12472 crate::mimo_moe::note_attention_graph(b, attn_ns);
12473 Out::Completed
12474 }
12475
12476 fn layer_attn_plain(&self, li: usize) -> bool {
12477 self.kv_heads_per_layer.is_none()
12478 && self.v_head_dim.is_none()
12479 && self.global_attn.is_none()
12480 && self.layer_window(li).is_none()
12481 && self.kv_cache.layers[li].sinks.is_none()
12482 }
12483
12484 fn forward_layers(
12486 &mut self,
12487 hidden: &[f32],
12488 position: usize,
12489 task_mask: Option<&TaskMask>,
12490 ) -> Vec<f32> {
12491 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12492 self.o1_progress();
12493 self.swa_trim_tails();
12494 out
12495 }
12496
12497 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12505 self.embed_single(id)
12506 }
12507
12508 pub fn split_supported(&self) -> Result<(), String> {
12512 if self.dsv4.is_some() {
12513 return Err(
12514 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12515 );
12516 }
12517 if self.dsv41.is_some() {
12518 return Err(
12519 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12520 .into(),
12521 );
12522 }
12523 if self.qwen4_exp.is_some() {
12524 return Err(
12525 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12526 );
12527 }
12528 if self.per_token_stack() {
12529 return Err(
12530 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12531 );
12532 }
12533 Ok(())
12534 }
12535
12536 pub fn forward_span(
12541 &mut self,
12542 hidden: &[f32],
12543 position: usize,
12544 from: usize,
12545 upto: usize,
12546 task_mask: Option<&TaskMask>,
12547 ) -> Result<Vec<f32>, String> {
12548 self.split_supported()?;
12549 if from > upto || upto >= self.num_layers {
12550 return Err(format!(
12551 "forward_span: layer range {from}..={upto} outside 0..{}",
12552 self.num_layers
12553 ));
12554 }
12555 if hidden.len() != self.hidden_size {
12556 return Err(format!(
12557 "forward_span: hidden len {} ≠ hidden_size {}",
12558 hidden.len(),
12559 self.hidden_size
12560 ));
12561 }
12562 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12563 self.o1_progress();
12564 self.swa_trim_tails();
12565 if self
12566 .graph_failed
12567 .swap(false, std::sync::atomic::Ordering::Relaxed)
12568 {
12569 self.cancel
12570 .store(false, std::sync::atomic::Ordering::Relaxed);
12571 self.clear_sequence_state();
12572 return Err("forward_span: deferred O(1) transition failed".into());
12573 }
12574 Ok(out)
12575 }
12576
12577 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12580 let normed = inference::rms_norm(
12581 hidden,
12582 &self.weights.final_norm,
12583 self.rms_eps,
12584 self.norm_style,
12585 );
12586 self.lm_head_forward(&normed)
12587 }
12588
12589 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12591 sampler::sample_with_scratch(
12592 logits,
12593 &self.sampler_config,
12594 past_tokens,
12595 &mut self.rng,
12596 &mut self.sampler_scratch,
12597 )
12598 }
12599
12600 pub fn reset_session(&mut self) {
12602 self.clear_sequence_state();
12603 }
12604
12605 pub fn prefill_span_ids(
12611 &mut self,
12612 ids: &[u32],
12613 start_pos: usize,
12614 upto: usize,
12615 task_mask: Option<&TaskMask>,
12616 ) -> Result<Vec<f32>, String> {
12617 self.split_supported()?;
12618 if upto >= self.num_layers {
12619 return Err(format!(
12620 "prefill_span_ids: upto {upto} outside 0..{}",
12621 self.num_layers
12622 ));
12623 }
12624 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12628 let out =
12629 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12630 self.check_o1_progress_failure("prefill_span_ids")?;
12631 Ok(out)
12632 } else {
12633 let hs = self.hidden_size;
12634 let mut out = Vec::with_capacity(ids.len() * hs);
12635 for (i, &id) in ids.iter().enumerate() {
12636 let emb = self.embed_id(id);
12637 out.extend_from_slice(&self.forward_span(
12638 &emb,
12639 start_pos + i,
12640 0,
12641 upto,
12642 task_mask,
12643 )?);
12644 }
12645 Ok(out)
12646 }
12647 }
12648
12649 pub fn prefill_span_hidden(
12652 &mut self,
12653 hidden: &[f32],
12654 start_pos: usize,
12655 from: usize,
12656 upto: usize,
12657 task_mask: Option<&TaskMask>,
12658 ) -> Result<Vec<f32>, String> {
12659 self.split_supported()?;
12660 let hs = self.hidden_size;
12661 if hidden.is_empty() || hidden.len() % hs != 0 {
12662 return Err(format!(
12663 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12664 hidden.len()
12665 ));
12666 }
12667 if from > upto || upto >= self.num_layers {
12668 return Err(format!(
12669 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12670 self.num_layers
12671 ));
12672 }
12673 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12674 let out = self.prefill_batch_span(
12675 PrefillIn::Hidden(hidden),
12676 start_pos,
12677 task_mask,
12678 from,
12679 upto + 1,
12680 );
12681 self.check_o1_progress_failure("prefill_span_hidden")?;
12682 Ok(out)
12683 } else {
12684 let b = hidden.len() / hs;
12685 let mut out = Vec::with_capacity(hidden.len());
12686 for i in 0..b {
12687 let h = self.forward_span(
12688 &hidden[i * hs..(i + 1) * hs],
12689 start_pos + i,
12690 from,
12691 upto,
12692 task_mask,
12693 )?;
12694 out.extend_from_slice(&h);
12695 }
12696 Ok(out)
12697 }
12698 }
12699
12700 fn try_token_graph_wgpu(
12704 &self,
12705 hidden: &[f32],
12706 position: usize,
12707 logits_out: &mut Vec<f32>,
12708 layers_run: &mut usize,
12709 ) -> Option<Result<Vec<f32>, ()>> {
12710 self.try_token_graph_wgpu_steps(
12711 hidden,
12712 position,
12713 logits_out,
12714 1,
12715 None,
12716 Some(layers_run),
12717 0,
12718 self.num_layers,
12719 )
12720 }
12721
12722 fn try_token_graph_wgpu_span(
12726 &self,
12727 hidden: &[f32],
12728 position: usize,
12729 logits_out: &mut Vec<f32>,
12730 from: usize,
12731 upto_excl: usize,
12732 layers_run: &mut usize,
12733 ) -> Option<Result<Vec<f32>, ()>> {
12734 self.try_token_graph_wgpu_steps(
12735 hidden,
12736 position,
12737 logits_out,
12738 1,
12739 None,
12740 Some(layers_run),
12741 from,
12742 upto_excl,
12743 )
12744 }
12745
12746 #[cfg(feature = "gpu")]
12750 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12751 if !crate::minicpm_tail::minimax_pool_on()
12752 || !crate::gpu::enabled_here()
12753 || self.head_clusters.is_some()
12754 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12755 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12756 {
12757 return None;
12758 }
12759 let (model, i, kind, rs) = self
12760 .weights
12761 .lm_head
12762 .graph_weight()
12763 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12764 let name = &model.tensors[i].name;
12765 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12766 crate::gpu::GraphPrismOp::InverseEmbedding
12767 } else if crate::prism::is_forward_weight(&model, name) {
12768 crate::gpu::GraphPrismOp::Forward
12769 } else {
12770 crate::gpu::GraphPrismOp::None
12771 };
12772 let gw = crate::gpu::GraphW {
12773 idx: i,
12774 kind,
12775 row_scale: rs,
12776 data: &[],
12777 prism,
12778 affine: crate::prism::is_affine_target(&model, name),
12779 };
12780 let rows = self.weights.lm_head.rows();
12781 let mut h = hidden.to_vec();
12782 let mut lg = Vec::new();
12783 let mut layers_run = 0usize;
12784 let outcome = crate::gpu::forward_token_graph(
12785 &model,
12786 self.graph_kv_id,
12787 &[],
12788 &[],
12789 self.o1_epoch,
12790 &self.inv_freq,
12791 &mut h,
12792 self.num_heads,
12793 self.num_kv_heads,
12794 self.head_dim,
12795 self.attn_scale,
12796 self.head_dim,
12797 self.hidden_size,
12798 self.intermediate_size,
12799 0,
12800 self.kv_cache.max_seq_len,
12801 self.norm_style == cortiq_core::NormStyle::Gemma,
12802 self.rms_eps as f32,
12803 Some((&gw, rows)),
12804 &self.weights.final_norm,
12805 &mut lg,
12806 &[],
12807 1,
12808 None,
12809 None,
12810 Some(&mut layers_run),
12811 0,
12812 false,
12813 );
12814 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12815 return None;
12816 }
12817 lg.resize(self.vocab_size, 0.0);
12820 if let Some(m) = self.logit_multiplier {
12821 for l in lg.iter_mut() {
12822 *l *= m;
12823 }
12824 }
12825 if let Some(c) = self.final_softcap {
12826 softcap_in_place(&mut lg, c, self.pool.as_deref());
12827 }
12828 Some(lg)
12829 }
12830
12831 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12834 if winners.is_empty() {
12835 return;
12836 }
12837 if self.m3_n_exp == 0 {
12838 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12839 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12840 }
12841 for &e in winners {
12842 let e = e as usize;
12843 if e < self.m3_n_exp && li < self.num_layers {
12844 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12845 }
12846 }
12847 if li + 1 == self.num_layers {
12848 self.m3_tokens += 1;
12849 if self.m3_tokens % 16 == 0 {
12850 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12851 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12852 let _ = std::fs::write(&path, bytes);
12853 }
12854 }
12855 }
12856 }
12857
12858 fn m3_prewarm(&mut self) {
12863 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12864 return;
12865 };
12866 let Ok(bytes) = std::fs::read(&path) else {
12867 tracing::warn!("m3 prewarm: cannot read profile {path}");
12868 return;
12869 };
12870 let counts: Vec<u32> = bytes
12871 .chunks_exact(4)
12872 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12873 .collect();
12874 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12875 if counts.len() != self.num_layers * n_exp {
12876 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12877 return;
12878 }
12879 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12880 .ok()
12881 .and_then(|v| v.parse().ok())
12882 .unwrap_or(300);
12883 let mut ranked: Vec<(u32, usize, usize)> = counts
12884 .iter()
12885 .enumerate()
12886 .filter(|(_, c)| **c > 0)
12887 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12888 .collect();
12889 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12890 ranked.truncate(limit);
12891 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12892 for (_, li, e) in ranked {
12893 per_layer[li].push(e);
12894 }
12895 let mut admitted = 0usize;
12896 for (li, ids) in per_layer.iter().enumerate() {
12897 if ids.is_empty() {
12898 continue;
12899 }
12900 let lw = &self.weights.layers[self.phys_layer(li)];
12901 let FfnKind::Moe(m) = &lw.ffn else {
12902 continue;
12903 };
12904 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12906 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12907 admitted += ids.len();
12908 }
12909 }
12910 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12911 }
12912
12913 #[cfg(feature = "gpu")]
12924 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12925 if self.o1_active() || self.attn_softcap > 0.0 {
12926 return Ok(false);
12927 }
12928 if !self.m3_prewarmed {
12929 self.m3_prewarmed = true;
12930 self.m3_prewarm();
12931 }
12932 let frame = {
12933 let lw = &self.weights.layers[self.phys_layer(li)];
12934 let FfnKind::Moe(m) = &lw.ffn else {
12935 return Ok(false);
12936 };
12937 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12938 return Ok(false);
12939 }
12940 let t_arm = std::time::Instant::now();
12941 if self.m3_winners.len() != self.num_layers {
12942 self.m3_winners = vec![Vec::new(); self.num_layers];
12943 }
12944 let pred = std::mem::take(&mut self.m3_winners[li]);
12945 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12946 if armed.is_none() {
12947 self.m3_winners[li] = pred; }
12949 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12950 match armed {
12951 Some(frame) => frame,
12952 None => return Ok(false),
12955 }
12956 };
12957 crate::gpu_wgpu::pooled_frame_enter(frame);
12958 let want_logits = self.graph_want_logits;
12962 self.graph_want_logits = false;
12963 let mut lg = Vec::new();
12964 let mut gl = 0usize;
12965 let t_span = std::time::Instant::now();
12966 let span = crate::gpu::gpu_scope(|| {
12967 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12968 });
12969 self.graph_want_logits = want_logits;
12970 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12971 let hh = match span {
12972 Some(Ok(hh)) => hh,
12973 Some(Err(())) => return Err(()),
12974 None => {
12975 return Ok(false);
12978 }
12979 };
12980 if gl != 1 {
12981 return if gl == 0 { Ok(false) } else { Err(()) };
12984 }
12985 *h = hh;
12986 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12987 self.m3_note_route(li, &winners);
12988 if self.m3_winners.len() != self.num_layers {
12989 self.m3_winners = vec![Vec::new(); self.num_layers];
12990 }
12991 self.m3_winners[li] = winners;
12992 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12993 cold.retain(|&(_, w)| {
12996 if w.is_finite() {
12997 true
12998 } else {
12999 tracing::error!("pooled tail: non-finite cold weight from expert route");
13000 false
13001 }
13002 });
13003 if cold.is_empty() {
13004 return Ok(true);
13005 }
13006 let x = crate::gpu_wgpu::pooled_frame_take_input();
13007 if x.len() != self.hidden_size {
13008 return Err(());
13009 }
13010 let lw = &self.weights.layers[self.phys_layer(li)];
13016 let FfnKind::Moe(m) = &lw.ffn else {
13017 return Err(());
13018 };
13019 let t_cold = std::time::Instant::now();
13020 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
13021 let mut out = crate::attention::take_buf(self.hidden_size);
13022 let on_device = !force_host
13023 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
13024 if !on_device {
13025 crate::attention::recycle_buf(&mut out);
13026 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
13027 .iter()
13028 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
13029 .collect::<Result<_, ()>>()?;
13030 out = crate::gpu::cpu_scope(|| {
13031 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
13032 });
13033 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
13034 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
13035 }
13036 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
13037 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13038 if out.len() != h.len() {
13039 crate::attention::recycle_buf(&mut out);
13040 return Err(());
13041 }
13042 for (dst, src) in h.iter_mut().zip(out.iter()) {
13043 *dst += src;
13044 }
13045 crate::attention::recycle_buf(&mut out);
13046 if on_device {
13047 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
13048 }
13049 Ok(true)
13050 }
13051
13052 #[cfg(not(feature = "gpu"))]
13053 fn m3_pooled_layer(
13054 &mut self,
13055 _li: usize,
13056 _position: usize,
13057 _h: &mut Vec<f32>,
13058 ) -> Result<bool, ()> {
13059 Ok(false)
13060 }
13061
13062 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
13066 if self.o1_active() || self.attn_softcap > 0.0 {
13067 return None;
13068 }
13069 if let Some(reason) = self.wgpu_graph_attn_decline() {
13072 self.note_graph_decline("wgpu multi-burst", reason);
13073 return None;
13074 }
13075 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
13076 if !graph_on || self.graph_refused() {
13077 return None;
13084 }
13085 let emb = self.embed_single(t_next);
13086 let mut lg = Vec::new();
13087 let mut ids = Vec::new();
13088 match self.try_token_graph_wgpu_steps(
13089 &emb,
13090 position,
13091 &mut lg,
13092 k,
13093 Some(&mut ids),
13094 None,
13095 0,
13096 self.num_layers,
13097 ) {
13098 Some(Ok(_)) => {}
13099 Some(Err(())) => {
13100 self.graph_failed
13105 .store(true, std::sync::atomic::Ordering::Relaxed);
13106 return None;
13107 }
13108 None => return None,
13109 }
13110 (ids.len() == k).then_some(ids)
13111 }
13112
13113 fn try_token_graph_wgpu_steps(
13117 &self,
13118 hidden: &[f32],
13119 position: usize,
13120 logits_out: &mut Vec<f32>,
13121 steps: usize,
13122 ids_out: Option<&mut Vec<u32>>,
13123 layers_run: Option<&mut usize>,
13124 from: usize,
13125 upto_excl: usize,
13126 ) -> Option<Result<Vec<f32>, ()>> {
13127 let upto_excl = match self.mimo_moe.graph_prefix_end() {
13130 Some(end) if end < upto_excl => {
13131 if steps != 1 || layers_run.is_none() || from >= end {
13132 return None;
13133 }
13134 end
13135 }
13136 _ => upto_excl,
13137 };
13138 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13141 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13142 return None;
13146 }
13147 if let Some(reason) = self.wgpu_graph_attn_decline() {
13154 self.note_graph_decline("wgpu token graph", reason);
13155 return None;
13156 }
13157 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13162 .map(|li| {
13163 if !o1_gpu {
13164 return None;
13165 }
13166 self.kv_cache.layers[self.phys_layer(li)].o1_views()
13167 })
13168 .collect();
13169 if self.o1_active() && o1_gpu {
13170 let want: usize = (from..upto_excl)
13173 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
13174 .count();
13175 let have = o1_views.iter().filter(|v| v.is_some()).count();
13176 if want == 0 || have != want {
13177 use std::sync::atomic::{AtomicUsize, Ordering};
13187 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13188 let code = have * 1000 + want;
13189 if LAST.swap(code, Ordering::Relaxed) != code {
13190 tracing::warn!(
13191 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13192 );
13193 }
13194 return None;
13195 }
13196 }
13197 let nh = self.num_heads;
13198 let (nkv, hd, rd) = self.layer_geom(0);
13199 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13200 let mut layers = Vec::with_capacity(upto_excl - from);
13201 let mut model = None;
13202 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13203 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13204 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13206 let name = &m.tensors[i].name;
13207 let prism = if crate::prism::is_inverse_embedding(m, name) {
13208 crate::gpu::GraphPrismOp::InverseEmbedding
13209 } else if crate::prism::is_forward_weight(m, name) {
13210 crate::gpu::GraphPrismOp::Forward
13211 } else {
13212 crate::gpu::GraphPrismOp::None
13213 };
13214 return Some(crate::gpu::GraphW {
13215 idx: i,
13216 kind,
13217 row_scale: rs,
13218 data: &[],
13219 prism,
13220 affine: crate::prism::is_affine_target(m, name),
13221 });
13222 }
13223 match t.as_f32() {
13225 Some(d) => Some(crate::gpu::GraphW {
13226 idx: 0,
13227 kind: 4,
13228 row_scale: &[],
13229 data: d,
13230 prism: crate::gpu::GraphPrismOp::None,
13231 affine: false,
13232 }),
13233 None => {
13234 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13235 eprintln!("batch graph: weight has no graph/f32 representation");
13236 }
13237 None
13238 }
13239 }
13240 }
13241 for li in from..upto_excl {
13242 let lw = &self.weights.layers[self.phys_layer(li)];
13243 if dbg {
13244 let ak = match &lw.attn {
13245 AttnKind::Mla(_) => "Mla".into(),
13246 AttnKind::Full {
13247 output_gate, bias, ..
13248 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13249 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13250 AttnKind::Kda(_) => "Kda".into(),
13251 AttnKind::Linear(_) => "Linear".into(),
13252 AttnKind::ShortConv(_) => "ShortConv".into(),
13253 AttnKind::Bounded(_) => "Bounded".into(),
13254 };
13255 let fk = match &lw.ffn {
13256 FfnKind::Dense(_) => "Dense",
13257 FfnKind::Moe(_) => "Moe",
13258 FfnKind::DenseMoe(_) => "DenseMoe",
13259 };
13260 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13261 }
13262 let gffn = match &lw.ffn {
13263 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13267 FfnKind::Dense(d) => {
13268 let Some(act) = d.act.wgpu_graph_act() else {
13272 self.note_graph_decline(
13273 "wgpu token graph",
13274 "dense FFN activation without a graph kernel",
13275 );
13276 return None;
13277 };
13278 crate::gpu::GraphFfn::Dense {
13279 gate: gw(&d.gate_proj)?,
13280 up: gw(&d.up_proj)?,
13281 down: gw(&d.down_proj)?,
13282 act,
13283 }
13284 }
13285 FfnKind::Moe(m) => {
13286 if m.route_tau.is_some() || m.mask.is_some() {
13294 return None;
13295 }
13296 let shared = m.shared.as_ref();
13297 let inter = m.experts.first()?.gate_proj.rows();
13303 let detached_shared = match shared {
13311 Some((se, None))
13312 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13313 {
13314 if !matches!(se.act, Act::Silu)
13315 || se.gate_proj.mapped_q4tp().is_none()
13316 || se.up_proj.mapped_q4tp().is_none()
13317 || se.down_proj.mapped_q4tp().is_none()
13318 {
13319 return None;
13320 }
13321 Some(crate::gpu::GraphMoeDetachedShared {
13322 gate: gw(&se.gate_proj)?,
13323 up: gw(&se.up_proj)?,
13324 down: gw(&se.down_proj)?,
13325 width: se.gate_proj.rows(),
13326 })
13327 }
13328 Some((se, Some(_)))
13332 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13333 {
13334 return None;
13335 }
13336 _ => None,
13337 };
13338 let has_shared = shared.is_some() && detached_shared.is_none();
13339 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13340 let sgate = match shared {
13341 Some((_, Some(sg))) if has_shared => gw(sg)?,
13342 _ => gw(&m.router)?,
13346 };
13347 let router = gw(&m.router)?;
13348 if router.prism != crate::gpu::GraphPrismOp::None
13354 || sgate.prism != crate::gpu::GraphPrismOp::None
13355 || router.affine
13356 || sgate.affine
13357 {
13358 tracing::warn!(
13359 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13360 );
13361 return None;
13362 }
13363 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
13364 let edge = |t: &QTensor| {
13365 t.graph_weight()
13366 .or_else(|| t.graph_weight_descriptor())
13367 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
13368 };
13369 let bank_key: Option<GraphMoeKey> = match (
13370 m.experts.first().and_then(|e| edge(&e.gate_proj)),
13371 m.experts.last().and_then(|e| edge(&e.down_proj)),
13372 ) {
13373 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
13374 self.phys_layer(li),
13375 m.experts.as_ptr() as usize,
13376 m.experts.len(),
13377 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
13378 fm,
13379 fi,
13380 di,
13381 )),
13382 _ => None,
13383 };
13384 let cached =
13385 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
13386 let bank = if let Some(b) = cached {
13387 b
13388 } else {
13389 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13390 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13391 let mut q4tp: Option<bool> = None;
13394 let mut gu_q2: Option<bool> = None;
13397 let mut q82: Option<bool> = None;
13399 let mut moe_act = None;
13403 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
13404 let Some(e_act) = e.act.wgpu_graph_act() else {
13405 return None;
13406 };
13407 match moe_act {
13408 Some(existing) if existing != e_act => return None,
13409 None => moe_act = Some(e_act),
13410 _ => {}
13411 }
13412 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13413 return None;
13414 }
13415 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13420 let Some((em, ei, _, _)) = expert_weight
13421 .graph_weight()
13422 .or_else(|| expert_weight.graph_weight_descriptor())
13423 else {
13424 return None;
13425 };
13426 let name = &em.tensors[ei].name;
13427 if crate::prism::is_forward_weight(em, name)
13428 || crate::prism::is_inverse_embedding(em, name)
13429 || crate::prism::is_affine_target(em, name)
13430 {
13431 tracing::warn!(
13432 "resident MoE declined: expert Prism/affine transform is not implemented"
13433 );
13434 return None;
13435 }
13436 }
13437 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13438 Some((mm, gi)) => (
13439 mm,
13440 gi,
13441 e.up_proj.mapped_q4t()?.1,
13442 e.down_proj.mapped_q4t()?.1,
13443 false,
13444 false,
13445 false,
13446 ),
13447 None => match e.gate_proj.mapped_q2tp() {
13448 Some((mm, gi)) => (
13449 mm,
13450 gi,
13451 e.up_proj.mapped_q2tp()?.1,
13452 e.down_proj.mapped_q4tp()?.1,
13453 true,
13454 true,
13455 false,
13456 ),
13457 None => match e.gate_proj.mapped_q4tp() {
13458 Some((mm, gi)) => (
13459 mm,
13460 gi,
13461 e.up_proj.mapped_q4tp()?.1,
13462 e.down_proj.mapped_q4tp()?.1,
13463 true,
13464 false,
13465 false,
13466 ),
13467 None => {
13470 if !crate::gpu::moe_q82_graph_on() {
13471 return None;
13472 }
13473 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13474 (
13475 mm,
13476 gi,
13477 e.up_proj.mapped_q8_2f()?.1,
13478 e.down_proj.mapped_q8_2f()?.1,
13479 false,
13480 false,
13481 true,
13482 )
13483 }
13484 },
13485 },
13486 };
13487 if *q4tp.get_or_insert(is_p) != is_p
13488 || *gu_q2.get_or_insert(is_q2) != is_q2
13489 || *q82.get_or_insert(is_8) != is_8
13490 {
13491 tracing::warn!(
13497 "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."
13498 );
13499 return None;
13500 }
13501 bank_model.get_or_insert_with(|| mm.clone());
13502 experts.push((gi, ui, di));
13503 }
13504 let b = GraphMoeBank {
13505 experts,
13506 q4tp,
13507 gu_q2,
13508 q82,
13509 act: moe_act,
13510 model: bank_model,
13511 };
13512 if let Some(k) = bank_key {
13513 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
13514 }
13515 b
13516 };
13517 if let Some(mm) = &bank.model {
13518 model.get_or_insert_with(|| mm.clone());
13519 }
13520 crate::gpu::GraphFfn::Moe {
13521 router,
13522 shared_gate: sgate,
13523 experts: bank.experts,
13524 n_exp: m.experts.len(),
13525 top_k: std::env::var("CMF_TOPK_PROBE")
13531 .ok()
13532 .and_then(|v| v.parse::<usize>().ok())
13533 .filter(|k| *k > 0 && *k <= m.top_k)
13534 .unwrap_or(m.top_k),
13535 inter,
13536 act: bank.act?,
13537 norm_topk: m.norm_topk_prob,
13538 q4tp: bank.q4tp?,
13539 gu_q2: bank.gu_q2.unwrap_or(false),
13540 q82: bank.q82.unwrap_or(false),
13541 sigmoid: m.router_sigmoid,
13542 sigmoid_denom_eps: m.sigmoid_denom_eps,
13543 bias: m.expert_bias.as_deref(),
13544 has_shared,
13545 detached_shared,
13546 shared_gated,
13547 route_scale: m.routed_scaling,
13548 }
13549 }
13550 };
13551 let attn = match &lw.attn {
13552 AttnKind::Full {
13553 wq,
13554 wk,
13555 wv,
13556 wo,
13557 q_norm,
13558 k_norm,
13559 output_gate,
13560 softplus_gate,
13561 bias,
13562 } => {
13563 if self.attention_heads_per_layer.is_some() {
13564 return None;
13565 }
13566 let head_gate = match softplus_gate {
13570 None => None,
13571 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13572 Some(_) => {
13573 self.note_graph_decline(
13574 "wgpu token graph",
13575 "projected softplus / per-element output gate",
13576 );
13577 return None;
13578 }
13579 };
13580 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13581 model = Some(m.clone());
13582 crate::gpu::GraphAttn::Full {
13583 wq: gw(wq)?,
13584 wk: gw(wk)?,
13585 wv: gw(wv)?,
13586 wo: gw(wo)?,
13587 q_norm: q_norm.as_deref(),
13588 k_norm: k_norm.as_deref(),
13589 late_qk_norm: self.qk_norm_after_rope,
13590 bias: bias
13591 .as_ref()
13592 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13593 output_gate: *output_gate,
13594 cpu_k: self.kv_cache.layers[li].k_heads(),
13595 cpu_v: self.kv_cache.layers[li].v_heads(),
13596 cpu_base: self.kv_cache.layers[li].base(),
13597 geom: self.graph_attn_geom(li),
13598 head_gate,
13599 }
13600 }
13601 AttnKind::LinearGdn(w) => {
13602 let cfg = self.gdn_cfg?;
13603 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13604 model = Some(m.clone());
13605 crate::gpu::GraphAttn::Gdn {
13606 qkv: gw(&w.in_proj_qkv)?,
13607 z: gw(&w.in_proj_z)?,
13608 a: gw(&w.in_proj_a)?,
13609 b: gw(&w.in_proj_b)?,
13610 out: gw(&w.out_proj)?,
13611 conv1d: &w.conv1d,
13612 a_log: &w.a_log,
13613 dt_bias: &w.dt_bias,
13614 norm: &w.norm,
13615 nv: cfg.num_v_heads,
13616 nk: cfg.num_k_heads,
13617 dk: cfg.key_head_dim,
13618 dv: cfg.value_head_dim,
13619 kk: cfg.conv_kernel,
13620 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13621 preview: None,
13622 }
13623 }
13624 AttnKind::ShortConv(w) => {
13625 let cfg = self.short_conv_cfg?;
13626 let (m, _, _, _) = w
13627 .in_proj
13628 .graph_weight()
13629 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13630 model = Some(m.clone());
13631 crate::gpu::GraphAttn::ShortConv {
13632 inp: gw(&w.in_proj)?,
13633 out: gw(&w.out_proj)?,
13634 taps: &w.conv,
13635 kernel: cfg.kernel,
13636 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13637 }
13638 }
13639 _ => return None,
13640 };
13641 layers.push(crate::gpu::GraphLayer {
13642 input_norm: &lw.input_norm,
13643 attn,
13644 post_norm: &lw.post_norm,
13645 attn_out_norm: lw.attn_out_norm.as_deref(),
13646 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13647 layer_scale: lw.layer_scale,
13648 ffn: gffn,
13649 });
13650 }
13651 let model = model?;
13652 let lm_gw = if upto_excl == self.num_layers
13658 && self.graph_want_logits
13659 && std::env::var("CMF_GPU_LMHEAD")
13660 .map(|v| v != "0")
13661 .unwrap_or(true)
13662 {
13663 self.weights
13664 .lm_head
13665 .graph_weight()
13666 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13667 .map(|(m, i, kind, rs)| {
13668 let name = &m.tensors[i].name;
13669 let prism = if crate::prism::is_inverse_embedding(m, name) {
13670 crate::gpu::GraphPrismOp::InverseEmbedding
13671 } else if crate::prism::is_forward_weight(m, name) {
13672 crate::gpu::GraphPrismOp::Forward
13673 } else {
13674 crate::gpu::GraphPrismOp::None
13675 };
13676 (
13677 crate::gpu::GraphW {
13678 idx: i,
13679 kind,
13680 row_scale: rs,
13681 data: &[],
13682 prism,
13683 affine: crate::prism::is_affine_target(m, name),
13684 },
13685 self.weights.lm_head.rows(),
13686 )
13687 })
13688 } else {
13689 None
13690 };
13691 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13692 let emb_gw = if steps > 1 {
13694 self.weights
13695 .embed_tokens
13696 .graph_weight()
13697 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13698 .map(|(m, i, kind, rs)| {
13699 let name = &m.tensors[i].name;
13700 let prism = if crate::prism::is_inverse_embedding(m, name) {
13701 crate::gpu::GraphPrismOp::InverseEmbedding
13702 } else if crate::prism::is_forward_weight(m, name) {
13703 crate::gpu::GraphPrismOp::Forward
13704 } else {
13705 crate::gpu::GraphPrismOp::None
13706 };
13707 (
13708 crate::gpu::GraphW {
13709 idx: i,
13710 kind,
13711 row_scale: rs,
13712 data: &[],
13713 prism,
13714 affine: crate::prism::is_affine_target(m, name),
13715 },
13716 self.weights.embed_tokens.rows(),
13717 self.embed_multiplier,
13718 )
13719 })
13720 } else {
13721 None
13722 };
13723
13724 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13730 (from..upto_excl.min(self.num_layers - 1))
13731 .filter(|&li| (li + 1) % self.physical_layers == 0)
13732 .map(|li| li - from)
13733 .collect()
13734 } else {
13735 Vec::new()
13736 };
13737 let mut h = hidden.to_vec();
13738 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13744 let outcome = crate::gpu::forward_token_graph(
13745 &model,
13746 self.graph_kv_id,
13747 &layers,
13748 &o1_views,
13749 self.o1_epoch,
13750 &self.inv_freq,
13751 &mut h,
13752 nh,
13753 nkv,
13754 hd,
13755 self.attn_scale,
13756 rd,
13757 self.hidden_size,
13758 self.intermediate_size,
13759 position,
13760 self.kv_cache.max_seq_len,
13761 gemma,
13762 self.rms_eps as f32,
13763 lm,
13764 &self.weights.final_norm,
13765 logits_out,
13766 &loop_norm_at,
13767 steps,
13768 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13769 ids_out,
13770 layers_run,
13771 from,
13772 dump_hidden,
13773 );
13774 match outcome {
13775 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13776 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13777 crate::gpu::TokenGraphOutcome::Declined => None,
13778 }
13779 }
13780
13781 #[cfg(target_os = "macos")]
13790 #[allow(clippy::type_complexity)]
13791 fn metal_rows_plan(
13792 &self,
13793 ) -> Option<(
13794 Vec<MetalRowsItem<'_>>,
13795 std::sync::Arc<cortiq_core::CmfModel>,
13796 Option<crate::gpu_metal::GdnGpuCfg>,
13797 )> {
13798 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13799 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13800 if !graph_force
13801 || !crate::gpu::enabled_here()
13802 || std::env::var("CMF_GPU_BLOCK")
13803 .map(|v| v == "0")
13804 .unwrap_or(false)
13805 || self.attn_softcap > 0.0
13806 || self.o1_active()
13807 || self.swa.is_some()
13808 || self.global_attn.is_some()
13809 || self.attention_heads_per_layer.is_some()
13810 || self.graph_attn_decline_reason().is_some()
13812 || self.attn_v_norm
13813 || self.loop_final_norm
13814 || !self.metal_rope_scale_ok()
13815 {
13816 return None;
13817 }
13818 let attend_contract = self.head_dim % 4 == 0
13819 && self.head_dim <= 256
13820 && self.rotary_dim >= 2
13821 && self.rotary_dim <= self.head_dim
13822 && (self.rotary_dim / 2) % 32 == 0
13823 && self.num_kv_heads > 0
13824 && self.num_heads % self.num_kv_heads == 0;
13825 if !attend_contract {
13826 return None;
13827 }
13828 let mut plan: Vec<MetalRowsItem> = Vec::new();
13829 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13830 for li in 0..self.num_layers {
13831 let lw = &self.weights.layers[self.phys_layer(li)];
13832 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13833 return None;
13834 }
13835 let ffn = match &lw.ffn {
13836 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13837 let (Some(g), Some(u), Some(dn)) = (
13838 d.gate_proj.metal_graph_parts(),
13839 d.up_proj.metal_graph_parts(),
13840 d.down_proj.metal_graph_parts(),
13841 ) else {
13842 return None;
13843 };
13844 MetalFfn::Dense {
13845 gate: g,
13846 up: u,
13847 down: dn,
13848 act: 0, }
13850 }
13851 _ => return None,
13852 };
13853 match &lw.attn {
13854 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13855 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13856 w.in_proj_qkv.metal_graph_parts(),
13857 w.in_proj_z.metal_graph_parts(),
13858 w.in_proj_a.f32_parts(),
13859 w.in_proj_b.f32_parts(),
13860 w.out_proj.metal_graph_parts(),
13861 ) else {
13862 return None;
13863 };
13864 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13865 model_ref.get_or_insert_with(|| model.clone());
13866 }
13867 let gl = GdnGpuLayer {
13868 attn_norm: &lw.input_norm,
13869 post_norm: &lw.post_norm,
13870 qkv,
13871 z,
13872 a,
13873 b: bb,
13874 out,
13875 ffn,
13876 conv1d: &w.conv1d,
13877 a_log: &w.a_log,
13878 dt_bias: &w.dt_bias,
13879 gnorm: &w.norm,
13880 };
13881 match plan.last_mut() {
13882 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13883 _ => plan.push(MetalRowsItem::Gdn {
13884 run: vec![gl],
13885 first: li,
13886 }),
13887 }
13888 }
13889 AttnKind::Full {
13890 wq,
13891 wk,
13892 wv,
13893 wo,
13894 q_norm,
13895 k_norm,
13896 output_gate,
13897 softplus_gate: None,
13898 bias: None,
13899 } => {
13900 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13901 wq.metal_graph_parts(),
13902 wk.metal_graph_parts(),
13903 wv.metal_graph_parts(),
13904 wo.metal_graph_parts(),
13905 ) else {
13906 return None;
13907 };
13908 if let QTensor::Mapped { model, .. } = wq {
13909 model_ref.get_or_insert_with(|| model.clone());
13910 }
13911 let cache = &self.kv_cache.layers[li];
13912 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13913 return None;
13914 }
13915 plan.push(MetalRowsItem::Attn {
13916 l: AttnGpuLayer {
13917 attn_norm: &lw.input_norm,
13918 post_norm: &lw.post_norm,
13919 wq: pq,
13920 wk: pk,
13921 wv: pv,
13922 wo: po,
13923 ffn,
13924 attn_out_norm: None,
13925 ffn_out_norm: None,
13926 layer_scale: None,
13927 },
13928 li,
13929 q_norm: q_norm.as_deref(),
13930 k_norm: k_norm.as_deref(),
13931 output_gate: *output_gate,
13932 });
13933 }
13934 _ => return None,
13935 }
13936 }
13937 let model = model_ref?;
13938 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13939 nv: cfg.num_v_heads,
13940 nk: cfg.num_k_heads,
13941 dk: cfg.key_head_dim,
13942 dv: cfg.value_head_dim,
13943 kk: cfg.conv_kernel,
13944 hidden: self.hidden_size,
13945 inter: self.intermediate_size,
13946 c_dim: cfg.conv_dim(),
13947 eps: cfg.rms_eps as f32,
13948 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13949 });
13950 Some((plan, model, gcfg))
13951 }
13952
13953 #[cfg(target_os = "macos")]
13955 #[allow(clippy::too_many_arguments)]
13956 fn metal_attn_params<'a>(
13957 li: usize,
13958 cache: &'a crate::kv_cache::LayerKvCache,
13959 q_norm: Option<&'a [f32]>,
13960 k_norm: Option<&'a [f32]>,
13961 output_gate: bool,
13962 inv_freq: &'a [f32],
13963 rope_scale: f32,
13964 geom: (usize, usize, usize, usize),
13965 pos0: usize,
13966 kv_id: u64,
13967 scale: f32,
13968 eps: f32,
13969 gemma: bool,
13970 late_qk_norm: bool,
13971 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13972 let (nh, nkv, hd, rd) = geom;
13973 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13974 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13975 let cpu_stored = cpu_k[0].len() / hd;
13976 (
13977 crate::gpu_metal::AttnDeviceParams {
13978 kv_id,
13979 layer: li,
13980 nh,
13981 nkv,
13982 hd,
13983 rd,
13984 position: pos0,
13985 scale,
13986 eps,
13987 gemma,
13988 late_qk_norm,
13989 output_gate,
13990 q_norm,
13991 k_norm,
13992 inv_freq,
13993 rope_scale,
13994 cpu_k,
13995 cpu_v,
13996 cpu_stored,
13997 cpu_gen: cache.generation(),
13998 o1: None,
13999 window: None,
14000 softmax_sink: cache.sinks.as_deref(),
14001 head_gate: None,
14002 v_norm: false,
14003 },
14004 cpu_stored,
14005 )
14006 }
14007
14008 #[cfg(target_os = "macos")]
14013 #[allow(clippy::type_complexity)]
14014 fn metal_rows_run(
14015 &mut self,
14016 hiddens: &mut [f32],
14017 pos0: usize,
14018 b: usize,
14019 prefill: bool,
14020 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14021 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
14025 ) -> MetalRowsRun {
14026 use crate::gpu_metal::{GraphDims, VerifyGraph};
14027 if !crate::gpu_metal::wait_replay() {
14033 tracing::error!("Metal rows graph: the pending async replay failed");
14034 return MetalRowsRun::Failed;
14035 }
14036 spec_stamp("v.wait");
14037 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
14043 if want > 0 {
14044 let phys = self.physical_layers.max(1);
14045 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
14046 let is_gdn = self
14047 .weights
14048 .layers
14049 .get(li % phys)
14050 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
14051 if is_gdn && l.linear_state.len() != want {
14052 l.linear_state = vec![0f32; want];
14053 }
14054 }
14055 }
14056 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
14057 return MetalRowsRun::Declined;
14058 };
14059 spec_stamp("v.plan");
14060 let dims = GraphDims {
14061 hidden: self.hidden_size,
14062 eps: self.rms_eps as f32,
14063 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14064 };
14065 let Some(mut graph) = (if prefill {
14066 VerifyGraph::new_prefill(&model, dims, hiddens, b)
14067 } else {
14068 VerifyGraph::new(&model, dims, hiddens, b)
14069 }) else {
14070 return MetalRowsRun::Declined;
14071 };
14072 let geom = (
14073 self.num_heads,
14074 self.num_kv_heads,
14075 self.head_dim,
14076 self.rotary_dim,
14077 );
14078 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
14079 let eps = self.rms_eps as f32;
14080 let kv_id = self.graph_kv_id;
14081 let inv_freq = self.inv_freq.clone();
14082 for item in &plan {
14083 let ok = match item {
14084 MetalRowsItem::Gdn { run, .. } => gcfg
14085 .as_ref()
14086 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
14087 .unwrap_or(false),
14088 MetalRowsItem::Attn {
14089 l,
14090 li,
14091 q_norm,
14092 k_norm,
14093 output_gate,
14094 } => {
14095 let (p, _) = Self::metal_attn_params(
14096 *li,
14097 &self.kv_cache.layers[*li],
14098 *q_norm,
14099 *k_norm,
14100 *output_gate,
14101 &inv_freq,
14102 self.layer_rope_scale(*li),
14103 geom,
14104 pos0,
14105 kv_id,
14106 self.attn_scale,
14107 eps,
14108 gemma,
14109 self.qk_norm_after_rope,
14110 );
14111 graph.attn_ok(l, &p)
14112 }
14113 };
14114 if !ok {
14115 use std::sync::atomic::{AtomicBool, Ordering};
14116 static SAID: AtomicBool = AtomicBool::new(false);
14117 if !SAID.swap(true, Ordering::Relaxed) {
14118 tracing::warn!("metal rows graph: a layer failed preflight — declining");
14119 }
14120 return MetalRowsRun::Declined;
14121 }
14122 }
14123 let lm = match &spec {
14124 Some((lm, _, _)) => {
14125 if !graph.lm_head_ok(*lm) {
14126 return MetalRowsRun::Declined;
14127 }
14128 Some(*lm)
14129 }
14130 None => None,
14131 };
14132 let mut gdn_layers = Vec::new();
14133 let mut attn_layers = Vec::new();
14134 for item in &plan {
14135 match item {
14136 MetalRowsItem::Gdn { run, first } => {
14137 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14138 .iter()
14139 .map(|l| l.linear_state.as_slice())
14140 .collect();
14141 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14142 return MetalRowsRun::Declined;
14143 }
14144 gdn_layers.extend(*first..*first + run.len());
14145 }
14146 MetalRowsItem::Attn {
14147 l,
14148 li,
14149 q_norm,
14150 k_norm,
14151 output_gate,
14152 } => {
14153 let (p, cpu_stored) = Self::metal_attn_params(
14154 *li,
14155 &self.kv_cache.layers[*li],
14156 *q_norm,
14157 *k_norm,
14158 *output_gate,
14159 &inv_freq,
14160 self.layer_rope_scale(*li),
14161 geom,
14162 pos0,
14163 kv_id,
14164 self.attn_scale,
14165 eps,
14166 gemma,
14167 self.qk_norm_after_rope,
14168 );
14169 if !graph.encode_attn_b(l, &p) {
14170 return MetalRowsRun::Declined;
14171 }
14172 attn_layers.push((*li, cpu_stored));
14173 }
14174 }
14175 }
14176 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
14177 if !graph.encode_lm_head_b(final_norm, lm) {
14178 return MetalRowsRun::Declined;
14179 }
14180 if let Some((n, _)) = argmax_out.as_ref() {
14185 if !graph.encode_argmax_b(*n) {
14186 argmax_out = None;
14187 }
14188 }
14189 }
14190 spec_stamp("v.enc");
14191 if !graph.sync() {
14192 return MetalRowsRun::Failed;
14193 }
14194 spec_stamp("v.gpu");
14195 match (spec, argmax_out) {
14196 (Some(_), Some((_, ids))) => {
14197 ids.resize(b, 0);
14198 if !graph.read_argmax(ids) {
14199 return MetalRowsRun::Failed;
14200 }
14201 spec_stamp("v.am");
14202 }
14203 (Some((lm, _, logits)), None) => {
14204 logits.resize(b * lm.1, 0.0);
14205 if !graph.read_logits(logits) {
14206 return MetalRowsRun::Failed;
14207 }
14208 spec_stamp("v.lg");
14209 }
14210 (None, _) => {}
14211 }
14212 if !graph.read_hidden(hiddens) {
14213 return MetalRowsRun::Failed;
14214 }
14215 spec_stamp("v.hid");
14216 MetalRowsRun::Completed(MetalVerifyPending {
14217 graph,
14218 gdn_layers,
14219 attn_layers,
14220 })
14221 }
14222
14223 #[cfg(target_os = "macos")]
14229 fn try_batch_graph_metal(
14230 &mut self,
14231 hiddens: &mut [f32],
14232 positions: &[usize],
14233 b: usize,
14234 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14235 argmax_out: Option<(usize, &mut Vec<u32>)>,
14236 ) -> crate::gpu::BatchGraphOutcome {
14237 let _t0 = std::time::Instant::now();
14238 if positions.len() != b
14239 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14240 || hiddens.len() != b * self.hidden_size
14241 {
14242 return crate::gpu::BatchGraphOutcome::Declined;
14243 }
14244 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14245 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14246 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14247 MetalRowsRun::Completed(pending) => pending,
14248 };
14249 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14250 eprintln!(
14251 "metal-verify: {:.1} ms | b={b}",
14252 _t0.elapsed().as_secs_f64() * 1e3
14253 );
14254 }
14255 self.metal_verify = Some(pending);
14256 crate::gpu::BatchGraphOutcome::Completed
14257 }
14258
14259 #[cfg(target_os = "macos")]
14264 fn prefill_rows_metal(
14265 &mut self,
14266 ids: &[u32],
14267 start_pos: usize,
14268 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14269 ) -> MetalPrefillOutcome {
14270 let b = ids.len();
14271 if b == 0 || b > 512 {
14272 return MetalPrefillOutcome::Declined;
14273 }
14274 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14275 let with_head = spec.is_some();
14276 let hs = self.hidden_size;
14277 let mut hiddens = vec![0f32; b * hs];
14278 for (j, &id) in ids.iter().enumerate() {
14279 let e = self.embed_single(id);
14280 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14281 }
14282 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14283 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14284 MetalRowsRun::Failed => {
14285 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14286 return MetalPrefillOutcome::Failed;
14287 }
14288 MetalRowsRun::Completed(pending) => pending,
14289 };
14290 let idxs = pending.gdn_layers.clone();
14292 let mut outs: Vec<&mut [f32]> = self
14293 .kv_cache
14294 .layers
14295 .iter_mut()
14296 .enumerate()
14297 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14298 .map(|(_, l)| l.linear_state.as_mut_slice())
14299 .collect();
14300 if !pending.graph.finish_states(&mut outs) {
14301 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14302 return MetalPrefillOutcome::Failed;
14303 }
14304 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14305 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14309 for (li, cpu_stored) in &pending.attn_layers {
14310 let mut kbuf = vec![0f32; b * nkv * hd];
14311 let mut vbuf = vec![0f32; b * nkv * hd];
14312 if !crate::gpu_metal::kv_mirror_read_rows(
14313 self.graph_kv_id,
14314 *li,
14315 nkv,
14316 hd,
14317 *cpu_stored,
14318 b,
14319 &mut kbuf,
14320 &mut vbuf,
14321 ) {
14322 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14323 return MetalPrefillOutcome::Failed;
14324 }
14325 rows.push((*li, *cpu_stored, kbuf, vbuf));
14326 }
14327 for (li, cpu_stored, kbuf, vbuf) in rows {
14328 let cache = &mut self.kv_cache.layers[li];
14329 for r in 0..b {
14330 cache.append(
14331 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14332 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14333 &[],
14334 );
14335 }
14336 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14337 }
14338 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14339 if with_head {
14340 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14341 }
14342 MetalPrefillOutcome::Completed(hiddens)
14343 }
14344
14345 #[cfg(target_os = "macos")]
14346 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14347 self.prefill_rows_metal(ids, start_pos, None)
14348 }
14349
14350 #[cfg(target_os = "macos")]
14355 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14356 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14357 return MetalBatchNllOutcome::Declined;
14358 }
14359 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14360 return MetalBatchNllOutcome::Declined;
14361 };
14362 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14363 .ok()
14364 .and_then(|v| v.parse::<usize>().ok())
14365 .filter(|&v| (1..=512).contains(&v))
14366 .unwrap_or(32);
14367 let final_norm = self.weights.final_norm.clone();
14368 let mut nll = 0.0f64;
14369 let mut count = 0usize;
14370 let mut pos = 0usize;
14371 let mut completed = 0usize;
14372 while pos < ids.len() {
14373 let end = (pos + chunk).min(ids.len());
14374 let mut logits = Vec::new();
14375 let outcome =
14376 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14377 match outcome {
14378 MetalPrefillOutcome::Declined => {
14379 return if completed == 0 {
14380 MetalBatchNllOutcome::Declined
14381 } else {
14382 MetalBatchNllOutcome::Failed(format!(
14383 "ordinary Metal NLL batch declined after {completed} chunks"
14384 ))
14385 };
14386 }
14387 MetalPrefillOutcome::Failed => {
14388 return MetalBatchNllOutcome::Failed(
14389 "ordinary Metal NLL batch failed after admission".to_string(),
14390 );
14391 }
14392 MetalPrefillOutcome::Completed(_) => {}
14393 }
14394 completed += 1;
14395 let vocab = self.vocab_size.min(lm.1);
14396 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14397 return MetalBatchNllOutcome::Failed(
14398 "ordinary Metal NLL head returned an invalid shape".to_string(),
14399 );
14400 }
14401 for row in 0..(end - pos) {
14402 let absolute = pos + row;
14403 if absolute < start || absolute + 1 >= ids.len() {
14404 continue;
14405 }
14406 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14407 if let Some(mu) = self.logit_multiplier {
14408 for v in lg.iter_mut() {
14409 *v *= mu;
14410 }
14411 }
14412 if let Some(c) = self.final_softcap {
14413 softcap_in_place(lg, c, self.pool.as_deref());
14414 }
14415 let target = ids[absolute + 1] as usize;
14416 if target >= vocab {
14417 return MetalBatchNllOutcome::Failed(format!(
14418 "target token {target} exceeds Metal head rows {vocab}"
14419 ));
14420 }
14421 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14422 let lse: f64 = lg
14423 .iter()
14424 .map(|&v| ((v - max) as f64).exp())
14425 .sum::<f64>()
14426 .ln()
14427 + max as f64;
14428 nll += lse - lg[target] as f64;
14429 count += 1;
14430 }
14431 pos = end;
14432 }
14433 MetalBatchNllOutcome::Completed(nll, count)
14434 }
14435
14436 #[cfg(target_os = "macos")]
14440 fn metal_verify_commit(&mut self, a: usize) -> bool {
14441 let Some(mut pending) = self.metal_verify.take() else {
14442 return false;
14443 };
14444 let n = a + 1;
14445 let idxs = pending.gdn_layers.clone();
14447 let mut outs: Vec<&mut [f32]> = self
14448 .kv_cache
14449 .layers
14450 .iter_mut()
14451 .enumerate()
14452 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14453 .map(|(_, l)| l.linear_state.as_mut_slice())
14454 .collect();
14455 if !pending.graph.commit(n, &mut outs) {
14456 return false;
14457 }
14458 spec_stamp("c.replay");
14459 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14460 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14464 for (li, cpu_stored) in &pending.attn_layers {
14465 let mut kbuf = vec![0f32; n * nkv * hd];
14466 let mut vbuf = vec![0f32; n * nkv * hd];
14467 if !crate::gpu_metal::kv_mirror_read_rows(
14468 self.graph_kv_id,
14469 *li,
14470 nkv,
14471 hd,
14472 *cpu_stored,
14473 n,
14474 &mut kbuf,
14475 &mut vbuf,
14476 ) {
14477 return false;
14478 }
14479 rows.push((*li, *cpu_stored, kbuf, vbuf));
14480 }
14481 for (li, cpu_stored, kbuf, vbuf) in rows {
14482 let cache = &mut self.kv_cache.layers[li];
14483 for r in 0..n {
14484 cache.append(
14485 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14486 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14487 &[],
14488 );
14489 }
14490 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14491 }
14492 spec_stamp("c.kv");
14493 true
14494 }
14495
14496 #[cfg(target_os = "macos")]
14503 fn mtp_warm_batch_submit(
14504 &mut self,
14505 m: &mut MtpModule,
14506 pairs: &[(&[f32], u32)],
14507 first_pos: usize,
14508 ) -> Option<MetalWarmPending> {
14509 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14510 let b = pairs.len();
14511 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14512 return None;
14513 }
14514 let AttnKind::Full {
14515 wq,
14516 wk,
14517 wv,
14518 wo,
14519 q_norm,
14520 k_norm,
14521 output_gate,
14522 softplus_gate: None,
14523 bias: None,
14524 } = &m.layer.attn
14525 else {
14526 return None;
14527 };
14528 let FfnKind::Dense(d) = &m.layer.ffn else {
14529 return None;
14530 };
14531 if !d.segs.is_empty() {
14532 return None;
14533 }
14534 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14535 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14536 else {
14537 return None;
14538 };
14539 let (Some(g), Some(u), Some(dn)) = (
14540 d.gate_proj.q1_parts(),
14541 d.up_proj.q1_parts(),
14542 d.down_proj.q1_parts(),
14543 ) else {
14544 return None;
14545 };
14546 let Some(eh) = m.eh_proj.q1_parts() else {
14547 return None;
14548 };
14549 let QTensor::Mapped { model, .. } = wq else {
14550 return None;
14551 };
14552 let model = model.clone();
14553 let hs = self.hidden_size;
14554 let mut cat = vec![0f32; b * 2 * hs];
14556 for (j, (h, tok)) in pairs.iter().enumerate() {
14557 let e = self.embed_single(*tok);
14558 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14559 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14560 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14561 }
14562 let dims = GraphDims {
14563 hidden: hs,
14564 eps: self.rms_eps as f32,
14565 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14566 };
14567 spec_stamp("w.cat");
14568 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14569 return None;
14570 };
14571 spec_stamp("w.new");
14572 let l = AttnGpuLayer {
14573 attn_norm: &m.layer.input_norm,
14574 post_norm: &m.layer.post_norm,
14575 wq: pq,
14576 wk: pk,
14577 wv: pv,
14578 wo: po,
14579 ffn: MetalFfn::Dense {
14580 gate: g,
14581 up: u,
14582 down: dn,
14583 act: (d.act == Act::Gelu) as u8,
14584 },
14585 attn_out_norm: None,
14586 ffn_out_norm: None,
14587 layer_scale: None,
14588 };
14589 let (nh, nkv, hd, rd) = (
14590 self.num_heads,
14591 self.num_kv_heads,
14592 self.head_dim,
14593 self.rotary_dim,
14594 );
14595 let inv_freq = self.inv_freq.clone();
14596 let cpu_stored;
14597 {
14598 let cache = &m.kv;
14599 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14600 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14601 cpu_stored = cpu_k[0].len() / hd;
14602 if cpu_stored > first_pos {
14607 spec_stamp("w.decl");
14608 return None;
14609 }
14610 let p = AttnDeviceParams {
14611 kv_id: self.mtp_kv_id(),
14612 layer: Self::MTP_LAYER_BASE,
14613 nh,
14614 nkv,
14615 hd,
14616 rd,
14617 position: first_pos,
14618 scale: self.attn_scale,
14619 eps: self.rms_eps as f32,
14620 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14621 late_qk_norm: self.qk_norm_after_rope,
14622 output_gate: *output_gate,
14623 q_norm: q_norm.as_deref(),
14624 k_norm: k_norm.as_deref(),
14625 inv_freq: &inv_freq,
14626 rope_scale: self.rope_scale,
14627 cpu_k,
14628 cpu_v,
14629 cpu_stored,
14630 cpu_gen: cache.generation(),
14631 o1: None,
14632 window: None,
14633 softmax_sink: None,
14634 head_gate: None,
14635 v_norm: false,
14636 };
14637 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14638 return None;
14639 }
14640 }
14641 spec_stamp("w.enc");
14642 if !graph.submit() {
14643 return None;
14644 }
14645 spec_stamp("w.sub");
14646 Some(MetalWarmPending {
14647 graph,
14648 cpu_stored,
14649 b,
14650 })
14651 }
14652
14653 #[cfg(target_os = "macos")]
14656 fn mtp_warm_batch_metal(
14657 &mut self,
14658 m: &mut MtpModule,
14659 pairs: &[(&[f32], u32)],
14660 first_pos: usize,
14661 ) -> bool {
14662 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14663 Some(p) => self.mtp_warm_batch_finish(m, p),
14664 None => false,
14665 }
14666 }
14667
14668 #[cfg(target_os = "macos")]
14673 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14674 let MetalWarmPending {
14675 mut graph,
14676 cpu_stored,
14677 b,
14678 } = pending;
14679 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14680 if !graph.sync() {
14681 return false;
14682 }
14683 spec_stamp("w.gpu");
14684 let mut kbuf = vec![0f32; b * nkv * hd];
14685 let mut vbuf = vec![0f32; b * nkv * hd];
14686 if !crate::gpu_metal::kv_mirror_read_rows(
14687 self.mtp_kv_id(),
14688 Self::MTP_LAYER_BASE,
14689 nkv,
14690 hd,
14691 cpu_stored,
14692 b,
14693 &mut kbuf,
14694 &mut vbuf,
14695 ) {
14696 return false;
14697 }
14698 for r in 0..b {
14699 m.kv.append(
14700 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14701 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14702 &[],
14703 );
14704 }
14705 crate::gpu_metal::kv_mirror_set_stored(
14706 self.mtp_kv_id(),
14707 Self::MTP_LAYER_BASE,
14708 cpu_stored + b,
14709 );
14710 spec_stamp("w.kv");
14711 true
14712 }
14713
14714 pub(crate) fn note_draft_id(&mut self, id: u32) {
14721 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14722 if (id as usize) >= cut {
14723 self.draft_full_streak = 16;
14724 } else {
14725 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14726 }
14727 }
14728
14729 fn draft_head_rows(&self, head_rows: usize) -> usize {
14732 if self.draft_full_streak > 0 {
14733 head_rows
14734 } else {
14735 Self::draft_vocab_rows(head_rows)
14736 }
14737 }
14738
14739 fn draft_vocab_rows(head_rows: usize) -> usize {
14742 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14743 let n = *N.get_or_init(|| {
14744 std::env::var("CMF_DRAFT_VOCAB")
14745 .ok()
14746 .and_then(|v| v.parse().ok())
14747 .unwrap_or(65536)
14748 });
14749 if n == 0 { head_rows } else { n.min(head_rows) }
14750 }
14751
14752 #[cfg(target_os = "macos")]
14757 fn mtp_step_metal(
14758 &mut self,
14759 m: &mut MtpModule,
14760 hidden: &[f32],
14761 next_token: u32,
14762 position: usize,
14763 want_logits: bool,
14764 ) -> Option<(Vec<f32>, Vec<f32>)> {
14765 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14766 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14767 || !crate::gpu::q1_force()
14768 || !crate::gpu::enabled_here()
14769 || self.attn_softcap > 0.0
14770 || self.attention_heads_per_layer.is_some()
14771 || m.kv.mode != crate::kv_cache::KvMode::F32
14772 || m.kv.o1.is_some()
14773 {
14774 return None;
14775 }
14776 let AttnKind::Full {
14777 wq,
14778 wk,
14779 wv,
14780 wo,
14781 q_norm,
14782 k_norm,
14783 output_gate,
14784 softplus_gate: None,
14785 bias: None,
14786 } = &m.layer.attn
14787 else {
14788 return None;
14789 };
14790 let FfnKind::Dense(d) = &m.layer.ffn else {
14791 return None;
14792 };
14793 if d.act != Act::Silu || !d.segs.is_empty() {
14794 return None;
14795 }
14796 let (pq, pk, pv, po) = (
14797 wq.q1_parts()?,
14798 wk.q1_parts()?,
14799 wv.q1_parts()?,
14800 wo.q1_parts()?,
14801 );
14802 let (g, u, dn) = (
14803 d.gate_proj.q1_parts()?,
14804 d.up_proj.q1_parts()?,
14805 d.down_proj.q1_parts()?,
14806 );
14807 let QTensor::Mapped { model, .. } = wq else {
14808 return None;
14809 };
14810 let model = model.clone();
14811 let lm = if want_logits {
14812 Some(self.weights.lm_head.q1_parts()?)
14813 } else {
14814 None
14815 };
14816 let dims = GraphDims {
14817 hidden: self.hidden_size,
14818 eps: self.rms_eps as f32,
14819 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14820 };
14821 let hs = self.hidden_size;
14824 let mut x = vec![0f32; hs];
14825 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14832 crate::gpu_metal::DENSE_MV,
14833 );
14834 let mut graph = TokenGraph::new(&model, dims, &x)?;
14835 let mut folded = false;
14836 if let Some(eh) = m.eh_proj.q1_parts() {
14837 let e = self.embed_single(next_token);
14838 let mut cat = vec![0.0f32; 2 * hs];
14839 let (cat_e, cat_h) = cat.split_at_mut(hs);
14840 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14841 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14842 folded = graph.encode_input_proj(eh, &cat);
14843 }
14844 if !folded {
14845 x = self.mtp_block_input(m, hidden, next_token);
14846 graph = TokenGraph::new(&model, dims, &x)?;
14847 }
14848 spec_stamp("d.in");
14849 let l = AttnGpuLayer {
14850 attn_norm: &m.layer.input_norm,
14851 post_norm: &m.layer.post_norm,
14852 wq: pq,
14853 wk: pk,
14854 wv: pv,
14855 wo: po,
14856 ffn: MetalFfn::Dense {
14857 gate: g,
14858 up: u,
14859 down: dn,
14860 act: (d.act == Act::Gelu) as u8,
14861 },
14862 attn_out_norm: None,
14863 ffn_out_norm: None,
14864 layer_scale: None,
14865 };
14866 let (nh, nkv, hd, rd) = (
14867 self.num_heads,
14868 self.num_kv_heads,
14869 self.head_dim,
14870 self.rotary_dim,
14871 );
14872 let inv_freq = self.inv_freq.clone();
14873 {
14874 let cache = &m.kv;
14875 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14876 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14877 let cpu_stored = cpu_k[0].len() / hd;
14878 let p = AttnDeviceParams {
14879 kv_id: self.mtp_kv_id(),
14880 layer: Self::MTP_LAYER_BASE,
14881 nh,
14882 nkv,
14883 hd,
14884 rd,
14885 position,
14886 scale: self.attn_scale,
14887 eps: self.rms_eps as f32,
14888 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14889 late_qk_norm: self.qk_norm_after_rope,
14890 output_gate: *output_gate,
14891 q_norm: q_norm.as_deref(),
14892 k_norm: k_norm.as_deref(),
14893 inv_freq: &inv_freq,
14894 rope_scale: self.rope_scale,
14895 cpu_k,
14896 cpu_v,
14897 cpu_stored,
14898 cpu_gen: cache.generation(),
14899 o1: None,
14900 window: None,
14901 softmax_sink: None,
14902 head_gate: None,
14903 v_norm: false,
14904 };
14905 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14906 return None;
14907 }
14908 }
14909 let draft_rows = if let Some(lm) = lm {
14915 self.draft_head_rows(lm.1)
14916 } else {
14917 0
14918 };
14919 if let Some(lm) = lm {
14920 if !graph.lm_head_ok(lm) {
14921 return None;
14922 }
14923 if draft_rows < lm.1 {
14924 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14925 return None;
14926 }
14927 } else {
14928 graph.encode_lm_head(&m.final_norm, lm);
14929 }
14930 }
14931 spec_stamp("d.enc");
14932 if graph.sync_checked().is_err() {
14933 return None;
14934 }
14935 spec_stamp("d.gpu");
14936 let mut logits = Vec::new();
14937 if let Some(lm) = lm {
14938 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14939 logits = attention::take_buf(n_read);
14940 graph.read_logits(&mut logits);
14941 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14943 }
14944 graph.finish(&mut x);
14945 let mut krow = attention::take_buf(nkv * hd);
14946 let mut vrow = attention::take_buf(nkv * hd);
14947 if crate::gpu_metal::kv_mirror_read_last(
14948 self.mtp_kv_id(),
14949 Self::MTP_LAYER_BASE,
14950 nkv,
14951 hd,
14952 &mut krow,
14953 &mut vrow,
14954 ) {
14955 m.kv.append(&krow, &vrow, &[]);
14956 }
14957 attention::recycle_buf(&mut krow);
14958 attention::recycle_buf(&mut vrow);
14959 spec_stamp("d.rd");
14960 Some((logits, x))
14961 }
14962
14963 fn mtp_chain_on() -> bool {
14976 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14977 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14978 }
14979
14980 #[cfg(target_os = "macos")]
14992 fn mtp_draft_chain_metal(
14993 &mut self,
14994 m: &mut MtpModule,
14995 hidden: &[f32],
14996 t_next: u32,
14997 position: usize,
14998 k: usize,
14999 ) -> Result<Vec<u32>, bool> {
15000 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
15001 if k == 0
15002 || k > 64
15003 || !Self::mtp_chain_on()
15004 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
15005 || !crate::gpu::q1_force()
15006 || !crate::gpu::enabled_here()
15007 || self.attn_softcap > 0.0
15008 || self.attention_heads_per_layer.is_some()
15009 || m.kv.mode != crate::kv_cache::KvMode::F32
15010 || m.kv.o1.is_some()
15011 || self.dsv4.is_some()
15013 || self.dsv41.is_some()
15014 || self.qwen4_exp.is_some()
15015 || self.per_token_stack()
15016 {
15017 return Err(false);
15018 }
15019 let AttnKind::Full {
15020 wq,
15021 wk,
15022 wv,
15023 wo,
15024 q_norm,
15025 k_norm,
15026 output_gate,
15027 softplus_gate: None,
15028 bias: None,
15029 } = &m.layer.attn
15030 else {
15031 return Err(false);
15032 };
15033 let FfnKind::Dense(d) = &m.layer.ffn else {
15034 return Err(false);
15035 };
15036 if d.act != Act::Silu || !d.segs.is_empty() {
15037 return Err(false);
15038 }
15039 let (Some(pq), Some(pk), Some(pv), Some(po)) =
15040 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
15041 else {
15042 return Err(false);
15043 };
15044 let (Some(g), Some(u), Some(dn)) = (
15045 d.gate_proj.q1_parts(),
15046 d.up_proj.q1_parts(),
15047 d.down_proj.q1_parts(),
15048 ) else {
15049 return Err(false);
15050 };
15051 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
15052 return Err(false);
15053 };
15054 let QTensor::Mapped { model, .. } = wq else {
15055 return Err(false);
15056 };
15057 let model = model.clone();
15058 let QTensor::Mapped {
15061 model: em,
15062 idx: eidx,
15063 dtype: cortiq_core::TensorDtype::Q4TiledP,
15064 ..
15065 } = &self.weights.embed_tokens
15066 else {
15067 return Err(false);
15068 };
15069 if !std::sync::Arc::ptr_eq(em, &model)
15070 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
15071 {
15072 return Err(false);
15073 }
15074 let embed = (
15075 *eidx,
15076 self.weights.embed_tokens.rows(),
15077 self.weights.embed_tokens.cols(),
15078 );
15079 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
15080 return Err(false);
15081 }
15082 let dims = GraphDims {
15083 hidden: self.hidden_size,
15084 eps: self.rms_eps as f32,
15085 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15086 };
15087 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
15091 crate::gpu_metal::DENSE_MV,
15092 );
15093 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
15094 return Err(false);
15095 };
15096 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
15097 return Err(false);
15098 }
15099 let l = AttnGpuLayer {
15100 attn_norm: &m.layer.input_norm,
15101 post_norm: &m.layer.post_norm,
15102 wq: pq,
15103 wk: pk,
15104 wv: pv,
15105 wo: po,
15106 ffn: MetalFfn::Dense {
15107 gate: g,
15108 up: u,
15109 down: dn,
15110 act: (d.act == Act::Gelu) as u8,
15111 },
15112 attn_out_norm: None,
15113 ffn_out_norm: None,
15114 layer_scale: None,
15115 };
15116 let (nh, nkv, hd, rd) = (
15117 self.num_heads,
15118 self.num_kv_heads,
15119 self.head_dim,
15120 self.rotary_dim,
15121 );
15122 let inv_freq = self.inv_freq.clone();
15123 let draft_rows = self.draft_head_rows(lm.1);
15124 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15125 if n_arg == 0 {
15126 return Err(false);
15127 }
15128 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15135 let t_chain = std::time::Instant::now();
15136 graph.chain_ids_init(t_next, k);
15137 let cpu_stored;
15138 {
15139 let cache = &m.kv;
15140 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15141 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15142 cpu_stored = cpu_k[0].len() / hd;
15143 for j in 0..k {
15144 if !graph.encode_chain_input(
15145 embed,
15146 j as u32,
15147 &m.enorm,
15148 &m.hnorm,
15149 self.embed_multiplier,
15150 eh,
15151 ) {
15152 return Err(false);
15153 }
15154 let p = AttnDeviceParams {
15158 kv_id: self.mtp_kv_id(),
15159 layer: Self::MTP_LAYER_BASE,
15160 nh,
15161 nkv,
15162 hd,
15163 rd,
15164 position: position + j,
15165 scale: self.attn_scale,
15166 eps: self.rms_eps as f32,
15167 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15168 late_qk_norm: self.qk_norm_after_rope,
15169 output_gate: *output_gate,
15170 q_norm: q_norm.as_deref(),
15171 k_norm: k_norm.as_deref(),
15172 inv_freq: &inv_freq,
15173 rope_scale: self.rope_scale,
15174 cpu_k: cpu_k.clone(),
15175 cpu_v: cpu_v.clone(),
15176 cpu_stored: cpu_stored + j,
15177 cpu_gen: cache.generation(),
15178 o1: None,
15179 window: None,
15180 softmax_sink: None,
15181 head_gate: None,
15182 v_norm: false,
15183 };
15184 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15185 return Err(false);
15186 }
15187 if draft_rows < lm.1 {
15188 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15189 return Err(false);
15190 }
15191 } else {
15192 graph.encode_lm_head(&m.final_norm, lm);
15193 }
15194 if !graph.encode_argmax(n_arg, j as u32 + 1) {
15195 return Err(false);
15196 }
15197 if split {
15198 graph.commit();
15201 }
15202 }
15203 }
15204 let t_enc = t_chain.elapsed();
15205 if graph.sync_checked().is_err() {
15206 return Err(true);
15207 }
15208 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
15209 eprintln!(
15210 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
15211 t_enc.as_secs_f64() * 1e3,
15212 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
15213 if split { ", split" } else { "" }
15214 );
15215 }
15216 let mut ids = vec![0u32; k];
15217 if !graph.chain_ids_read(&mut ids) {
15218 return Err(true);
15219 }
15220 let mut kbuf = vec![0f32; k * nkv * hd];
15221 let mut vbuf = vec![0f32; k * nkv * hd];
15222 if !crate::gpu_metal::kv_mirror_read_rows(
15223 self.mtp_kv_id(),
15224 Self::MTP_LAYER_BASE,
15225 nkv,
15226 hd,
15227 cpu_stored,
15228 k,
15229 &mut kbuf,
15230 &mut vbuf,
15231 ) {
15232 return Err(true);
15233 }
15234 for r in 0..k {
15235 m.kv.append(
15236 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15237 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15238 &[],
15239 );
15240 }
15241 Ok(ids)
15242 }
15243
15244 fn try_batch_graph_wgpu(
15245 &self,
15246 hiddens: &mut [f32],
15247 positions: &[usize],
15248 k: usize,
15249 spec: Option<crate::gpu::SpecTail<'_>>,
15250 ) -> crate::gpu::BatchGraphOutcome {
15251 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15252 }
15253
15254 fn try_batch_graph_wgpu_prefix(
15259 &self,
15260 hiddens: &mut [f32],
15261 positions: &[usize],
15262 k: usize,
15263 spec: Option<crate::gpu::SpecTail<'_>>,
15264 layers_run: Option<&mut usize>,
15265 ) -> crate::gpu::BatchGraphOutcome {
15266 let graph_end = match self.mimo_moe.graph_prefix_end() {
15267 Some(end) if end < self.num_layers => {
15268 if layers_run.is_none() || spec.is_some() || end == 0 {
15269 return crate::gpu::BatchGraphOutcome::Declined;
15270 }
15271 end
15272 }
15273 _ => self.num_layers,
15274 };
15275 let _tb = std::time::Instant::now();
15276 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15277 if self.attn_softcap > 0.0 {
15278 return crate::gpu::BatchGraphOutcome::Declined; }
15280 if let Some(reason) = self
15283 .wgpu_graph_attn_decline()
15284 .or_else(|| self.batch_graph_gemma_decline())
15285 {
15286 self.note_graph_decline("wgpu batch graph", reason);
15287 return crate::gpu::BatchGraphOutcome::Declined;
15288 }
15289 let nh = self.num_heads;
15290 let (nkv, hd, rd) = self.layer_geom(0);
15291 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15292 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15293 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15294 {
15295 let name = &m.tensors[i].name;
15296 let prism = if crate::prism::is_inverse_embedding(m, name) {
15297 crate::gpu::GraphPrismOp::InverseEmbedding
15298 } else if crate::prism::is_forward_weight(m, name) {
15299 crate::gpu::GraphPrismOp::Forward
15300 } else {
15301 crate::gpu::GraphPrismOp::None
15302 };
15303 return Some(crate::gpu::GraphW {
15304 idx: i,
15305 kind,
15306 row_scale: rs,
15307 data: &[],
15308 prism,
15309 affine: crate::prism::is_affine_target(m, name),
15310 });
15311 }
15312 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15313 eprintln!(
15314 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15315 t.rows(),
15316 t.cols()
15317 );
15318 }
15319 t.as_f32().map(|d| crate::gpu::GraphW {
15320 idx: 0,
15321 kind: 4,
15322 row_scale: &[],
15323 data: d,
15324 prism: crate::gpu::GraphPrismOp::None,
15325 affine: false,
15326 })
15327 }
15328 let built: Option<(
15329 Vec<crate::gpu::GraphLayer<'_>>,
15330 std::sync::Arc<cortiq_core::CmfModel>,
15331 )> = (|| {
15332 let mut layers = Vec::with_capacity(graph_end);
15333 let mut model = None;
15334 for li in 0..graph_end {
15335 let lw = &self.weights.layers[self.phys_layer(li)];
15336 let gffn = match &lw.ffn {
15343 FfnKind::Dense(d) if !d.segs.is_empty() => {
15344 if batch_debug {
15345 eprintln!("batch graph: dense segmented FFN at layer {li}");
15346 }
15347 return None;
15348 }
15349 FfnKind::Dense(d) => {
15350 let Some(act) = d.act.wgpu_graph_act() else {
15351 if batch_debug {
15352 eprintln!(
15353 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15354 d.act
15355 );
15356 }
15357 return None;
15358 };
15359 crate::gpu::GraphFfn::Dense {
15360 gate: gw(&d.gate_proj)?,
15361 up: gw(&d.up_proj)?,
15362 down: gw(&d.down_proj)?,
15363 act,
15364 }
15365 }
15366 FfnKind::Moe(m) => {
15367 if m.route_tau.is_some() || m.mask.is_some() {
15374 return None;
15375 }
15376 let shared = m.shared.as_ref();
15380 let has_shared = shared.is_some();
15381 let shared_gated = matches!(shared, Some((_, Some(_))));
15382 let sgate = match shared {
15383 Some((_, Some(sg))) => gw(sg)?,
15384 _ => gw(&m.router)?,
15388 };
15389 let router = gw(&m.router)?;
15390 if router.prism != crate::gpu::GraphPrismOp::None
15396 || router.affine
15397 || sgate.prism != crate::gpu::GraphPrismOp::None
15398 || sgate.affine
15399 {
15400 return None;
15401 }
15402 let inter = m.experts.first()?.gate_proj.rows();
15403 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15404 let mut q4tp: Option<bool> = None;
15405 let mut gu_q2: Option<bool> = None;
15406 let mut q82: Option<bool> = None;
15407 let mut moe_act = None;
15411 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15412 let Some(e_act) = e.act.wgpu_graph_act() else {
15413 return None;
15414 };
15415 match moe_act {
15416 Some(existing) if existing != e_act => return None,
15417 None => moe_act = Some(e_act),
15418 _ => {}
15419 }
15420 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15421 return None;
15422 }
15423 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15427 {
15428 Some((mm, gi)) => (
15429 mm,
15430 gi,
15431 e.up_proj.mapped_q4t()?.1,
15432 e.down_proj.mapped_q4t()?.1,
15433 false,
15434 false,
15435 false,
15436 ),
15437 None => match e.gate_proj.mapped_q2tp() {
15438 Some((mm, gi)) => (
15439 mm,
15440 gi,
15441 e.up_proj.mapped_q2tp()?.1,
15442 e.down_proj.mapped_q4tp()?.1,
15443 true,
15444 true,
15445 false,
15446 ),
15447 None => match e.gate_proj.mapped_q4tp() {
15448 Some((mm, gi)) => (
15449 mm,
15450 gi,
15451 e.up_proj.mapped_q4tp()?.1,
15452 e.down_proj.mapped_q4tp()?.1,
15453 true,
15454 false,
15455 false,
15456 ),
15457 None => {
15458 if !crate::gpu::moe_q82_graph_on() {
15459 return None;
15460 }
15461 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15462 (
15463 mm,
15464 gi,
15465 e.up_proj.mapped_q8_2f()?.1,
15466 e.down_proj.mapped_q8_2f()?.1,
15467 false,
15468 false,
15469 true,
15470 )
15471 }
15472 },
15473 },
15474 };
15475 if *q4tp.get_or_insert(is_p) != is_p
15476 || *gu_q2.get_or_insert(is_q2) != is_q2
15477 || *q82.get_or_insert(is_8) != is_8
15478 {
15479 return None;
15480 }
15481 if [gi, ui, di].into_iter().any(|idx| {
15482 mm.tensors.get(idx).is_some_and(|t| {
15483 crate::prism::is_forward_weight(mm, &t.name)
15484 || crate::prism::is_affine_target(mm, &t.name)
15485 })
15486 }) {
15487 return None;
15488 }
15489 model.get_or_insert_with(|| mm.clone());
15490 experts.push((gi, ui, di));
15491 }
15492 crate::gpu::GraphFfn::Moe {
15493 router,
15494 shared_gate: sgate,
15495 experts,
15496 n_exp: m.experts.len(),
15497 top_k: m.top_k,
15498 inter,
15499 act: moe_act?,
15500 norm_topk: m.norm_topk_prob,
15501 q4tp: q4tp?,
15502 gu_q2: gu_q2.unwrap_or(false),
15503 q82: q82.unwrap_or(false),
15504 sigmoid: m.router_sigmoid,
15505 sigmoid_denom_eps: m.sigmoid_denom_eps,
15506 bias: m.expert_bias.as_deref(),
15507 has_shared,
15508 detached_shared: None,
15509 shared_gated,
15510 route_scale: m.routed_scaling,
15511 }
15512 }
15513 _ => return None,
15514 };
15515 let attn = match &lw.attn {
15516 AttnKind::Full {
15517 wq,
15518 wk,
15519 wv,
15520 wo,
15521 q_norm,
15522 k_norm,
15523 output_gate,
15524 softplus_gate,
15525 bias,
15526 } => {
15527 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15528 if batch_debug {
15529 eprintln!(
15530 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15531 softplus_gate.is_some(),
15532 self.attention_heads_per_layer.is_some()
15533 );
15534 }
15535 return None;
15536 }
15537 let (m, _, _, _) =
15538 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15539 model = Some(m.clone());
15540 crate::gpu::GraphAttn::Full {
15541 wq: gw(wq)?,
15542 wk: gw(wk)?,
15543 wv: gw(wv)?,
15544 wo: gw(wo)?,
15545 q_norm: q_norm.as_deref(),
15546 k_norm: k_norm.as_deref(),
15547 late_qk_norm: self.qk_norm_after_rope,
15548 bias: bias
15549 .as_ref()
15550 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15551 output_gate: *output_gate,
15552 cpu_k: self.kv_cache.layers[li].k_heads(),
15553 cpu_v: self.kv_cache.layers[li].v_heads(),
15554 cpu_base: self.kv_cache.layers[li].base(),
15555 geom: self.graph_attn_geom(li),
15556 head_gate: None,
15559 }
15560 }
15561 AttnKind::LinearGdn(w) => {
15562 let Some(cfg) = self.gdn_cfg else {
15563 if batch_debug {
15564 eprintln!("batch graph: no GDN config at layer {li}");
15565 }
15566 return None;
15567 };
15568 let (m, _, _, _) = w
15569 .in_proj_qkv
15570 .graph_weight()
15571 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15572 model = Some(m.clone());
15573 crate::gpu::GraphAttn::Gdn {
15574 qkv: gw(&w.in_proj_qkv)?,
15575 z: gw(&w.in_proj_z)?,
15576 a: gw(&w.in_proj_a)?,
15577 b: gw(&w.in_proj_b)?,
15578 out: gw(&w.out_proj)?,
15579 conv1d: &w.conv1d,
15580 a_log: &w.a_log,
15581 dt_bias: &w.dt_bias,
15582 norm: &w.norm,
15583 nv: cfg.num_v_heads,
15584 nk: cfg.num_k_heads,
15585 dk: cfg.key_head_dim,
15586 dv: cfg.value_head_dim,
15587 kk: cfg.conv_kernel,
15588 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15589 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15590 }
15591 }
15592 _ => return None,
15593 };
15594 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15597 layers.push(crate::gpu::GraphLayer {
15598 input_norm: &lw.input_norm,
15599 attn,
15600 post_norm: &lw.post_norm,
15601 attn_out_norm: if g4 {
15602 lw.attn_out_norm.as_deref()
15603 } else {
15604 None
15605 },
15606 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15607 layer_scale: if g4 { lw.layer_scale } else { None },
15608 ffn: gffn,
15609 });
15610 }
15611 Some((layers, model?))
15612 })();
15613 let Some((layers, model)) = built else {
15614 {
15615 use std::sync::atomic::{AtomicBool, Ordering};
15616 static SAID: AtomicBool = AtomicBool::new(false);
15617 if !SAID.swap(true, Ordering::Relaxed) {
15618 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15619 }
15620 }
15621 return crate::gpu::BatchGraphOutcome::Declined;
15622 };
15623 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15624 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15625 }
15626 crate::gpu::forward_batch_graph(
15627 &model,
15628 self.graph_kv_id,
15629 &layers,
15630 &self.inv_freq,
15631 hiddens,
15632 nh,
15633 nkv,
15634 hd,
15635 rd,
15636 self.hidden_size,
15637 self.intermediate_size,
15638 positions,
15639 self.kv_cache.max_seq_len,
15640 gemma,
15641 self.rms_eps as f32,
15642 self.attn_scale,
15643 k,
15644 &(0..graph_end)
15645 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15646 .collect::<Vec<_>>(),
15647 self.o1_epoch,
15648 spec,
15649 layers_run,
15650 )
15651 }
15652
15653 fn draft_probe() -> bool {
15657 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15658 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15659 }
15660
15661 #[cfg(feature = "gpu")]
15673 fn dsv4_spec_on() -> bool {
15674 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15675 *ON.get_or_init(|| {
15676 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15680 return v != "0";
15681 }
15682 std::env::var("CMF_DSV4_SPEC")
15689 .map(|v| v != "0")
15690 .unwrap_or_else(|_| {
15691 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15692 })
15693 })
15694 }
15695
15696 #[cfg(feature = "gpu")]
15703 fn dsv4_spec_step(
15704 &mut self,
15705 tip_token: u32,
15706 t_next: u32,
15707 next_pos: usize,
15708 max_extra: usize,
15709 drafted: &mut usize,
15710 accepted_ctr: &mut usize,
15711 ) -> Option<(Vec<u32>, usize)> {
15712 let t_all = std::time::Instant::now();
15713 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15714 thread_local! {
15715 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15716 const { std::cell::Cell::new(None) };
15717 }
15718 LAST.with(|l| {
15719 if let Some(prev) = l.get() {
15720 eprintln!(
15721 "between rounds {:.1} ms",
15722 prev.elapsed().as_secs_f64() * 1e3
15723 );
15724 }
15725 l.set(Some(std::time::Instant::now()));
15726 });
15727 }
15728 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15729 eprintln!("spec_step: enter pos={next_pos}");
15730 }
15731 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15732 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15733 if self.dspark.is_none() {
15735 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15736 if t.is_empty() {
15737 return None;
15738 }
15739 crate::dsv4::dspark_arm(&t, cfg.dim);
15740 self.dspark = Some(crate::dsv4::DsparkState::new(
15741 self.dsv4_mtp.len(),
15742 &cfg,
15743 t.len(),
15744 ));
15745 }
15746 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15747 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15748 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15749 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15750 }
15751 let pack = pack?;
15752 let block = crate::dsv4::dspark_block();
15753 let b_box = self.dsv4.as_mut()?;
15754 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15755 let ds = self.dspark.as_mut()?;
15756 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15759 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15760 if dbg {
15761 eprintln!("spec_step: no capture");
15762 }
15763 return None;
15764 }
15765 ds.have_hidden = true;
15766 let tip_pos = next_pos.checked_sub(1)?;
15767 let draft_started = std::time::Instant::now();
15768 let mut conf = Vec::new();
15769 let props = crate::dsv4::dspark_draft_gpu(
15770 g,
15771 &self.dsv4_mtp,
15772 &cfg,
15773 ds,
15774 pack,
15775 st.kv_id,
15776 tip_token,
15777 tip_pos,
15778 self.pool.as_deref(),
15779 &mut conf,
15780 );
15781 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15782 *drafted += block;
15783 if props.is_empty() || props[0] != t_next {
15784 if dbg {
15785 eprintln!(
15786 "spec_step: draft {} (props0={:?} t_next={t_next})",
15787 if props.is_empty() {
15788 "empty"
15789 } else {
15790 "miss"
15791 },
15792 props.first()
15793 );
15794 }
15795 return None;
15796 }
15797 let mut k_verify = crate::dsv4::dspark_verify_k()
15804 .min(props.len())
15805 .min(max_extra.saturating_add(1));
15806 let conf_min = {
15812 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15813 *M.get_or_init(|| {
15814 std::env::var("CMF_DSPARK_CONF_MIN")
15815 .ok()
15816 .and_then(|v| v.parse().ok())
15817 .unwrap_or(0.0)
15818 })
15819 };
15820 if conf_min > 0.0 && conf.len() >= props.len() {
15821 let mut keep = 1usize;
15822 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15823 keep += 1;
15824 }
15825 k_verify = k_verify.min(keep.max(2));
15826 }
15827 if k_verify < 2 {
15828 return None;
15829 }
15830 let mut fed = Vec::with_capacity(k_verify);
15831 fed.push(t_next);
15832 fed.extend_from_slice(&props[1..k_verify]);
15833 let mut argmax = Vec::new();
15834 let mut logits_all = Vec::new();
15835 let mut walked = Vec::new();
15836 let txn = crate::dsv4::dsv4_verify_chunk(
15837 g,
15838 layers,
15839 &cfg,
15840 st,
15841 &fed,
15842 next_pos,
15843 &self.inv_freq,
15844 self.pool.as_deref(),
15845 &targets,
15846 &mut argmax,
15847 &mut logits_all,
15848 &mut walked,
15849 );
15850 if txn.is_none() && dbg {
15851 eprintln!("spec_step: verify declined");
15852 }
15853 let txn = txn?;
15854 let spec_gpu_end = txn.gpu_end;
15855 let b = fed.len();
15856 let mut accepted = 1usize;
15857 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15858 accepted += 1;
15859 }
15860 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15865 accepted = 1;
15866 }
15867 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15868 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15869 }
15870 let t_fin = std::time::Instant::now();
15871 if !crate::dsv4::dsv4_spec_finish(
15872 g,
15873 layers,
15874 &cfg,
15875 st,
15876 txn,
15877 accepted,
15878 &fed,
15879 &self.inv_freq,
15880 self.pool.as_deref(),
15881 ) {
15882 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15883 return None;
15884 }
15885 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15886 eprintln!(
15887 "finish(k={accepted}): {:.1} ms",
15888 t_fin.elapsed().as_secs_f64() * 1e3
15889 );
15890 }
15891 *accepted_ctr += accepted - 1;
15892 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15897 let dev_caps: Vec<usize> = targets
15902 .iter()
15903 .copied()
15904 .filter(|&t| t < spec_gpu_end)
15905 .collect();
15906 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15907 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15908 return None;
15909 }
15910 for t in 0..accepted {
15911 let tip = t + 1 == accepted;
15912 for (slot, &tl) in targets.iter().enumerate() {
15913 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15914 let lo = (di * b + t) * hc * dim;
15915 crate::dsv4::dspark_capture(
15916 &caps_all[lo..lo + hc * dim],
15917 &cfg,
15918 slot,
15919 &mut ds.main_hidden,
15920 );
15921 } else if tip
15922 && crate::dsv4::dspark_peek_slot(slot, dim, {
15923 let lo = slot * dim;
15924 &mut ds.main_hidden[lo..lo + dim]
15925 })
15926 {
15927 } else {
15932 crate::dsv4::dspark_capture(
15936 &walked[t * hc * dim..(t + 1) * hc * dim],
15937 &cfg,
15938 slot,
15939 &mut ds.main_hidden,
15940 );
15941 }
15942 }
15943 crate::dsv4::dspark_ring_append(
15944 g,
15945 &self.dsv4_mtp,
15946 &cfg,
15947 ds,
15948 next_pos + t,
15949 self.pool.as_deref(),
15950 );
15951 }
15952 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15953 self.graph_logits = Some(row);
15954 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15959 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15960 crate::dsv4::pick_tally_arm();
15961 }
15962 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15963 eprintln!(
15964 "spec_step total {:.1} ms (k={accepted})",
15965 t_all.elapsed().as_secs_f64() * 1e3
15966 );
15967 }
15968 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15969 }
15970
15971 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15972 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15973 return;
15974 }
15975 let trunk_now = crate::dsv4::pick_tally_take();
15977 crate::dsv4::trunk_freq_note(&trunk_now);
15978 if !trunk_now.is_empty() {
15979 self.dspark_trunk_picks.push(trunk_now);
15980 let keep = crate::dsv4::dspark_block();
15981 if self.dspark_trunk_picks.len() > keep {
15982 self.dspark_trunk_picks.remove(0);
15983 }
15984 }
15985 for p in std::mem::take(&mut self.dspark_pending) {
15988 let Some(i) = position.checked_sub(p.0 + 1) else {
15989 continue;
15990 };
15991 let mut p = p;
15992 if i < p.1.len() {
15993 if p.2 && p.1[i] == token_id {
15994 p.3 = i + 1;
15995 } else {
15996 p.2 = false;
15997 }
15998 if i + 1 < p.1.len() {
15999 self.dspark_pending.push(p);
16000 continue;
16001 }
16002 }
16003 self.dspark_hist.push(p.3);
16004 self.dspark_real.push(token_id);
16005 }
16006 let Some(b) = &mut self.dsv4 else { return };
16007 let (g, layers, cfg) = (&b.0, &b.1, b.2);
16008 let n_layers = layers.len();
16009 if self.dspark.is_none() {
16010 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
16011 if t.is_empty() {
16012 return;
16013 }
16014 eprintln!(
16015 "DSpark: capture from layers {t:?}, block {}",
16016 crate::dsv4::dspark_block()
16017 );
16018 crate::dsv4::dspark_arm(&t, cfg.dim);
16019 self.dspark = Some(crate::dsv4::DsparkState::new(
16020 self.dsv4_mtp.len(),
16021 &cfg,
16022 t.len(),
16023 ));
16024 }
16025 let ds = self.dspark.as_mut().unwrap();
16026 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
16027 return; }
16029 let mut conf = Vec::new();
16030 crate::dsv4::pick_tally_arm();
16031 let draft_started = std::time::Instant::now();
16036 #[cfg(feature = "gpu")]
16037 let gpu_draft = crate::dsv4::dspark_gpu_on();
16038 #[cfg(not(feature = "gpu"))]
16039 let gpu_draft = false;
16040 let props = if gpu_draft {
16041 #[cfg(feature = "gpu")]
16042 {
16043 let kv_id = b.3.kv_id;
16044 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
16045 Some(pk) => crate::dsv4::dspark_draft_gpu(
16046 g,
16047 &self.dsv4_mtp,
16048 &cfg,
16049 ds,
16050 pk,
16051 kv_id,
16052 token_id,
16053 position,
16054 self.pool.as_deref(),
16055 &mut conf,
16056 ),
16057 None => Vec::new(),
16058 }
16059 }
16060 #[cfg(not(feature = "gpu"))]
16061 Vec::new()
16062 } else {
16063 crate::gpu::cpu_scope(|| {
16064 crate::dsv4::dspark_draft(
16065 g,
16066 &self.dsv4_mtp,
16067 &cfg,
16068 ds,
16069 token_id,
16070 position,
16071 self.pool.as_deref(),
16072 &mut conf,
16073 )
16074 })
16075 };
16076 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
16077 let draft_picks = crate::dsv4::pick_tally_take();
16078 crate::dsv4::dspark_freq_note(&draft_picks);
16079 crate::dsv4::pick_tally_arm();
16082 if !props.is_empty() {
16083 let (tu, tt) = {
16087 let flat: Vec<(usize, Vec<usize>)> = self
16088 .dspark_trunk_picks
16089 .iter()
16090 .flat_map(|v| v.iter().cloned())
16091 .collect();
16092 let mut per: std::collections::HashMap<usize, Vec<usize>> =
16094 std::collections::HashMap::new();
16095 for (li, picks) in flat {
16096 per.entry(li).or_default().extend(picks);
16097 }
16098 let n = per.len().max(1);
16099 let mut u = 0usize;
16100 let mut t = 0usize;
16101 for (_, v) in per {
16102 t += v.len();
16103 u += v.iter().collect::<std::collections::HashSet<_>>().len();
16104 }
16105 (u / n, t / n)
16106 };
16107 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
16108 self.dspark_exp.push((tu, tt, du, dt));
16109 self.dspark_pending.push((position, props, true, 0));
16110 }
16111 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
16112 let n = self.dspark_hist.len() as f32;
16113 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
16114 let block = crate::dsv4::dspark_block();
16115 let mut at = vec![0usize; block + 1];
16116 for &k in &self.dspark_hist {
16117 at[k] += 1;
16118 }
16119 let mut surv = Vec::with_capacity(block);
16121 for i in 1..=block {
16122 let k = at[i..].iter().sum::<usize>() as f32 / n;
16123 surv.push(format!("{k:.2}"));
16124 }
16125 let distinct = self
16126 .dspark_real
16127 .iter()
16128 .collect::<std::collections::HashSet<_>>()
16129 .len();
16130 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16131 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16132 });
16133 let m = self.dspark_exp.len().max(1);
16134 eprintln!(
16135 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16136 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16137 self.dspark_hist.len(),
16138 mean + 1.0,
16139 surv.join(" ")
16140 );
16141 eprintln!(
16142 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16143 trunk experts {}/{} per layer over {block} tokens, \
16144 draft {}/{} per block, draft {:.2} ms/block",
16145 self.dspark_real.len(),
16146 tu / m,
16147 tt / m,
16148 du / m,
16149 dt / m,
16150 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16151 );
16152 }
16153 }
16154
16155 fn forward_layers_upto(
16156 &mut self,
16157 hidden: &[f32],
16158 position: usize,
16159 task_mask: Option<&TaskMask>,
16160 upto: Option<usize>,
16161 ) -> Vec<f32> {
16162 if let Some(plan) = self.gpu_plan.clone() {
16168 if upto.is_none() && plan.len() > 1 {
16169 let mut h = hidden.to_vec();
16170 for &(dev, from, upto_incl) in plan.iter() {
16171 h = crate::gpu::with_device(dev, || {
16172 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
16173 });
16174 }
16175 return h;
16176 }
16177 }
16178 self.forward_layers_span(hidden, position, task_mask, 0, upto)
16179 }
16180
16181 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
16186 self.set_gpu_plan_at(devices, None)
16187 }
16188
16189 pub fn set_gpu_plan_at(
16193 &mut self,
16194 devices: Option<&[usize]>,
16195 at: Option<usize>,
16196 ) -> Result<(), String> {
16197 let Some(devs) = devices.filter(|d| d.len() > 1) else {
16198 self.gpu_plan = None;
16199 return Ok(());
16200 };
16201 self.split_supported()?;
16202 let n = self.num_layers;
16203 if devs.len() > n {
16204 return Err(format!("{} devices for {n} layers", devs.len()));
16205 }
16206 if let Some(k) = at {
16207 if k == 0 || k >= n {
16208 return Err(format!("split at {k}: the model has {n} layers"));
16209 }
16210 if devs.len() == 2 {
16211 self.gpu_plan = Some(std::sync::Arc::new(vec![
16212 (devs[0], 0, k - 1),
16213 (devs[1], k, n - 1),
16214 ]));
16215 return Ok(());
16216 }
16217 return Err(format!(
16218 "an explicit split point takes exactly 2 devices, got {}",
16219 devs.len()
16220 ));
16221 }
16222 let per = n.div_ceil(devs.len());
16223 let mut plan = Vec::with_capacity(devs.len());
16224 let mut from = 0usize;
16225 for &d in devs {
16226 if from >= n {
16227 break;
16228 }
16229 let upto = (from + per - 1).min(n - 1);
16230 plan.push((d, from, upto));
16231 from = upto + 1;
16232 }
16233 self.gpu_plan = Some(std::sync::Arc::new(plan));
16234 Ok(())
16235 }
16236
16237 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16239 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16240 }
16241
16242 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16248 if let Some(x) = t.as_f32() {
16249 return x.to_vec();
16250 }
16251 let mut out = vec![0.0; t.rows() * t.cols()];
16252 for r in 0..t.rows() {
16253 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16254 }
16255 out
16256 }
16257
16258 fn embryo_resident_eligible(&self) -> bool {
16259 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16262 || self.num_layers != self.physical_layers
16263 || self.loop_final_norm
16264 || self.weights.layers.len() != self.num_layers
16265 || self.head_clusters.is_none()
16266 || self.final_softcap.is_some()
16267 || self.logit_multiplier.is_some()
16268 || self.attn_softcap != 0.0
16269 || self.mtp.is_some()
16270 || self.per_token_stack()
16271 || self.dsv4.is_some()
16272 || self.dsv41.is_some()
16273 || self.qwen4_exp.is_some()
16274 || self.dyn_router.is_some()
16281 || self.dyn_phi_layer.is_some()
16282 || self.dyn_blend_loaded
16283 || self.o1_cfg.is_some()
16284 || self.swa.is_some()
16285 || self.sliding_layers.is_some()
16286 || self.global_attn.is_some()
16287 || self.attention_heads_per_layer.is_some()
16288 || self.attn_v_norm
16289 || self
16290 .kv_cache
16291 .layers
16292 .iter()
16293 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16294 || self.rope_scale != 1.0
16295 || self.rope_scale_local != 1.0
16296 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16297 || self.hidden_size == 0
16298 || self.hidden_size > 1024
16299 || self.intermediate_size > 1024
16300 || self.num_heads == 0
16301 || self.num_kv_heads == 0
16302 || self.num_heads % self.num_kv_heads != 0
16303 || self.num_heads.saturating_mul(self.head_dim) > 1024
16304 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16305 || self.vocab_size == 0
16306 || self.kv_cache.max_seq_len == 0
16307 || self.rotary_dim == 0
16308 || self.rotary_dim > self.head_dim
16309 || self.rotary_dim % 2 != 0
16310 || self.inv_freq.len() < self.rotary_dim / 2
16311 {
16312 return false;
16313 }
16314 if self.weights.lm_head.as_f32().is_none()
16325 || self.weights.embed_tokens.as_f32().is_none()
16326 || self.weights.lm_head.rows() < self.vocab_size
16327 || self.weights.lm_head.cols() != self.hidden_size
16328 || self.weights.embed_tokens.rows() < self.vocab_size
16329 || self.weights.embed_tokens.cols() != self.hidden_size
16330 || self.weights.final_norm.len() != self.hidden_size
16331 {
16332 return false;
16333 }
16334 if let Some(cfg) = self.vmf_cfg {
16335 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16336 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16337 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16338 || cfg.state_len() == 0
16339 {
16340 return false;
16341 }
16342 }
16343 if let Some(g) = self.gdn_cfg {
16344 if g.num_v_heads == 0
16349 || g.num_k_heads == 0
16350 || g.num_v_heads % g.num_k_heads != 0
16351 || g.key_head_dim == 0
16352 || g.key_head_dim > 128
16353 || g.value_head_dim == 0
16354 || g.value_head_dim > 256
16355 || g.value_head_dim % 4 != 0
16356 || g.conv_kernel == 0
16357 || g.num_v_heads > 512
16358 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16359 || g.conv_dim() > 2048
16360 || g.conv_dim() % 4 != 0
16361 || g.hidden_size != self.hidden_size
16362 || g.output_gate_sigmoid
16363 || g.rms_eps != self.rms_eps
16364 || g.state_len() == 0
16365 {
16366 return false;
16367 }
16368 }
16369 let mut full_seen = false;
16370 for lw in &self.weights.layers {
16371 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16372 return false;
16373 }
16374 match &lw.attn {
16375 AttnKind::LinearGdn(w) => {
16376 let Some(g) = self.gdn_cfg else {
16377 return false;
16378 };
16379 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16380 if w.in_proj_qkv.rows() != g.conv_dim()
16381 || w.in_proj_qkv.cols() != self.hidden_size
16382 || w.in_proj_qkv.as_f32().is_none()
16383 || w.in_proj_z.rows() != nv * dv
16384 || w.in_proj_z.cols() != self.hidden_size
16385 || w.in_proj_z.as_f32().is_none()
16386 || w.in_proj_a.rows() != nv
16387 || w.in_proj_a.cols() != self.hidden_size
16388 || w.in_proj_a.as_f32().is_none()
16389 || w.in_proj_b.rows() != nv
16390 || w.in_proj_b.cols() != self.hidden_size
16391 || w.in_proj_b.as_f32().is_none()
16392 || w.conv1d.len() != g.conv_dim() * kk
16393 || w.a_log.len() != nv
16394 || w.dt_bias.len() != nv
16395 || w.norm.len() != dv
16396 || w.out_proj.rows() != self.hidden_size
16397 || w.out_proj.cols() != nv * dv
16398 || w.out_proj.as_f32().is_none()
16399 {
16400 return false;
16401 }
16402 }
16403 AttnKind::Linear(w) => {
16404 let Some(cfg) = self.vmf_cfg else {
16405 return false;
16406 };
16407 if w.thq.rows() != cfg.num_heads * cfg.nphase
16408 || w.thq.cols() != self.hidden_size
16409 || w.thq.as_f32().is_none()
16410 || w.thk.rows() != cfg.num_heads * cfg.nphase
16411 || w.thk.cols() != self.hidden_size
16412 || w.thk.as_f32().is_none()
16413 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16414 || w.v_proj.cols() != self.hidden_size
16415 || w.v_proj.as_f32().is_none()
16416 || w.out_proj.rows() != self.hidden_size
16417 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16418 || w.out_proj.as_f32().is_none()
16419 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16420 {
16421 return false;
16422 }
16423 if let Some((kg, kb)) = &w.k_gate {
16424 if kg.rows() != cfg.num_heads
16425 || kg.cols() != self.hidden_size
16426 || kg.as_f32().is_none()
16427 || kb.len() != cfg.num_heads
16428 {
16429 return false;
16430 }
16431 }
16432 if let Some(conv) = &w.conv {
16433 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16434 return false;
16435 }
16436 }
16437 }
16438 AttnKind::Full {
16439 wq,
16440 wk,
16441 wv,
16442 wo,
16443 q_norm,
16444 k_norm,
16445 output_gate,
16446 softplus_gate,
16447 bias,
16448 } => {
16449 if full_seen
16450 || q_norm.is_some()
16451 || k_norm.is_some()
16452 || *output_gate
16453 || softplus_gate.is_some()
16454 || bias.is_some()
16455 || wq.as_f32().is_none()
16456 || wk.as_f32().is_none()
16457 || wv.as_f32().is_none()
16458 || wo.as_f32().is_none()
16459 || wq.rows() != self.num_heads * self.head_dim
16460 || wk.rows() != self.num_kv_heads * self.head_dim
16461 || wv.rows() != self.num_kv_heads * self.head_dim
16462 || wq.cols() != self.hidden_size
16463 || wk.cols() != self.hidden_size
16464 || wv.cols() != self.hidden_size
16465 || wo.rows() != self.hidden_size
16466 || wo.cols() != self.num_heads * self.head_dim
16467 {
16468 return false;
16469 }
16470 full_seen = true;
16471 }
16472 AttnKind::Bounded(w) => {
16473 let Some(ac) = self.anchor_core.as_ref() else {
16476 return false;
16477 };
16478 if self.bounded_rope.is_none()
16479 || w.window != ac.window
16480 || w.sink != ac.sink
16481 || w.window == 0
16482 || w.window + w.sink > 256
16483 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16484 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16485 || w.wq.as_f32().is_none()
16486 || w.wk.as_f32().is_none()
16487 || w.wv.as_f32().is_none()
16488 || w.wo.as_f32().is_none()
16489 || w.wq.rows() != self.num_heads * self.head_dim
16490 || w.wk.rows() != self.num_kv_heads * self.head_dim
16491 || w.wv.rows() != self.num_kv_heads * self.head_dim
16492 || w.wq.cols() != self.hidden_size
16493 || w.wk.cols() != self.hidden_size
16494 || w.wv.cols() != self.hidden_size
16495 || w.wo.rows() != self.hidden_size
16496 || w.wo.cols() != self.num_heads * self.head_dim
16497 {
16498 return false;
16499 }
16500 }
16501 _ => return false,
16502 }
16503 match &lw.ffn {
16504 FfnKind::Dense(d) => {
16505 if d.act != Act::Silu
16506 || !d.segs.is_empty()
16507 || d.gate_proj.as_f32().is_none()
16508 || d.up_proj.as_f32().is_none()
16509 || d.down_proj.as_f32().is_none()
16510 || d.gate_proj.rows() != self.intermediate_size
16511 || d.gate_proj.cols() != self.hidden_size
16512 || d.up_proj.rows() != self.intermediate_size
16513 || d.up_proj.cols() != self.hidden_size
16514 || d.down_proj.rows() != self.hidden_size
16515 || d.down_proj.cols() != self.intermediate_size
16516 {
16517 return false;
16518 }
16519 }
16520 FfnKind::Moe(m) => {
16521 if m.resonance.is_none()
16522 || m.top_k != 1
16523 || m.router_sigmoid
16524 || !m.norm_topk_prob
16525 || m.expert_bias.is_some()
16526 || m.routed_scaling != 1.0
16527 || m.route_tau.is_some()
16528 || m.shared.is_none()
16529 || m.mask.is_some()
16530 || m.per_expert_scale.is_some()
16531 || m.router_input_norm
16532 || m.experts.is_empty()
16533 || m.experts.len() > 8
16534 {
16535 return false;
16536 }
16537 let r = m.resonance.as_ref().unwrap();
16538 if r.mu.len() != m.experts.len() * self.hidden_size
16539 || r.bias.len() != m.experts.len()
16540 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16541 || r.k > 128
16542 {
16543 return false;
16544 }
16545 let Some((shared, gate)) = &m.shared else {
16546 return false;
16547 };
16548 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16549 return false;
16550 }
16551 if shared.gate_proj.as_f32().is_none()
16552 || shared.up_proj.as_f32().is_none()
16553 || shared.down_proj.as_f32().is_none()
16554 || shared.gate_proj.rows() != self.intermediate_size
16555 || shared.gate_proj.cols() != self.hidden_size
16556 || shared.up_proj.rows() != self.intermediate_size
16557 || shared.up_proj.cols() != self.hidden_size
16558 || shared.down_proj.rows() != self.hidden_size
16559 || shared.down_proj.cols() != self.intermediate_size
16560 {
16561 return false;
16562 }
16563 for e in &m.experts {
16564 if e.act != Act::Silu
16565 || !e.segs.is_empty()
16566 || e.gate_proj.as_f32().is_none()
16567 || e.up_proj.as_f32().is_none()
16568 || e.down_proj.as_f32().is_none()
16569 || e.gate_proj.rows() != self.intermediate_size
16570 || e.gate_proj.cols() != self.hidden_size
16571 || e.up_proj.rows() != self.intermediate_size
16572 || e.up_proj.cols() != self.hidden_size
16573 || e.down_proj.rows() != self.hidden_size
16574 || e.down_proj.cols() != self.intermediate_size
16575 {
16576 return false;
16577 }
16578 }
16579 }
16580 FfnKind::DenseMoe(_) => return false,
16581 }
16582 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16583 return false;
16584 }
16585 }
16586 if full_seen && self.anchor_core.is_some() {
16587 return false;
16588 }
16589 full_seen || self.num_layers > 0
16590 }
16591
16592 fn embryo_resident_wanted(&self) -> bool {
16597 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16598 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16599 && matches!(
16600 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16601 Ok("1") | Ok("parallel")
16602 )
16603 && crate::gpu::enabled_here()
16604 && !self.graph_refused()
16605 && self.embryo_resident_eligible()
16606 }
16607
16608 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16616 if ids.len() < 2
16617 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16618 || !self.embryo_resident_wanted()
16619 {
16620 return None;
16621 }
16622 let model = self.ensure_embryo_graph()?;
16623 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16624 .ok()
16625 .and_then(|v| v.parse::<usize>().ok())
16626 .filter(|&v| v >= 1)
16627 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16628 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16629 let hs = self.hidden_size;
16630 let n = ids.len();
16631 let mut pos = start;
16632 let mut last = None;
16633 let mut rows = Vec::with_capacity(cmax * hs);
16634 while pos < n {
16635 let end = (pos + cmax).min(n);
16636 rows.clear();
16637 for &id in &ids[pos..end] {
16638 rows.extend_from_slice(&self.embed_single(id));
16639 }
16640 let mut lg = Vec::new();
16641 if !crate::gpu::forward_embryo_graph_chunk(
16642 &model,
16643 self.graph_kv_id,
16644 &rows,
16645 pos,
16646 end - pos,
16647 &mut lg,
16648 ) {
16649 if pos == start {
16650 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16651 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16652 }
16653 return None;
16654 }
16655 self.kv_cache.clear();
16659 self.clear_history();
16660 crate::gpu::graph_kv_reset(self.graph_kv_id);
16661 panic!(
16662 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16663 );
16664 }
16665 last = Some(lg);
16666 pos = end;
16667 }
16668 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16669 eprintln!(
16670 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16671 n - start,
16672 (n - start).div_ceil(cmax)
16673 );
16674 }
16675 last
16676 }
16677
16678 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16679 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16680 const UMAX: u32 = u32::MAX;
16681 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16682 const REC: usize = 64;
16683 struct Pack {
16684 data: Vec<f32>,
16685 }
16686 impl Pack {
16687 fn put(&mut self, x: &[f32]) -> u32 {
16688 if x.is_empty() {
16689 return u32::MAX;
16690 }
16691 let off = self.data.len();
16692 self.data.extend_from_slice(x);
16693 off as u32
16694 }
16695 }
16696 let vmf = self.vmf_cfg;
16700 let gdn = self.gdn_cfg;
16701 let mut pack = Pack { data: Vec::new() };
16702 let mut meta = vec![0u32; HEADER];
16703 meta[0] = self.hidden_size as u32;
16704 meta[1] = self.intermediate_size as u32;
16705 meta[2] = self.vocab_size as u32;
16706 meta[3] = self.num_layers as u32;
16707 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16708 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16709 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16710 if let Some(g) = gdn {
16711 meta[24] = g.num_v_heads as u32;
16712 meta[25] = g.num_k_heads as u32;
16713 meta[26] = g.key_head_dim as u32;
16714 meta[27] = g.value_head_dim as u32;
16715 meta[28] = g.conv_kernel as u32;
16716 meta[29] = g.conv_dim() as u32;
16717 }
16718 meta[7] = self.num_heads as u32;
16719 meta[8] = self.num_kv_heads as u32;
16720 meta[9] = self.head_dim as u32;
16721 meta[10] = self.kv_cache.max_seq_len as u32;
16722 let clusters = self.head_clusters.as_ref().unwrap();
16723 let cluster_count = clusters.len() / self.hidden_size;
16724 if clusters.len() % self.hidden_size != 0
16725 || cluster_count == 0
16726 || cluster_count > 1024
16727 || self.vocab_size % cluster_count != 0
16728 || self.weights.lm_head.rows() < self.vocab_size
16729 || self.weights.final_norm.len() != self.hidden_size
16730 {
16731 return None;
16732 }
16733 meta[11] = cluster_count as u32;
16734 meta[12] = (self.vocab_size / cluster_count) as u32;
16735 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16736 meta[16] = self.rotary_dim as u32;
16737 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16738 meta[19] = (self.rms_eps as f32).to_bits();
16739 let max_conv = self
16740 .weights
16741 .layers
16742 .iter()
16743 .filter_map(|lw| match &lw.attn {
16744 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16745 _ => None,
16746 })
16747 .max()
16748 .unwrap_or(1);
16749 let phase_stride = vmf
16755 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16756 .unwrap_or(0);
16757 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16758 let state_stride = phase_stride.max(gdn_stride);
16759 let bounded = self.anchor_core.clone();
16763 let (anchor_window, anchor_sink) = bounded
16764 .as_ref()
16765 .map(|ac| (ac.window, ac.sink))
16766 .unwrap_or((0, 0));
16767 let kv_stride = if bounded.is_some() {
16768 2usize
16769 .saturating_mul(self.num_kv_heads)
16770 .saturating_mul(anchor_window)
16771 .saturating_mul(self.head_dim)
16772 } else {
16773 2usize
16774 .saturating_mul(self.num_kv_heads)
16775 .saturating_mul(self.kv_cache.max_seq_len)
16776 .saturating_mul(self.head_dim)
16777 };
16778 meta[14] = state_stride as u32;
16779 meta[15] = kv_stride as u32;
16780 meta[18] = anchor_window as u32;
16781 meta[20] = anchor_sink as u32;
16782 meta[21] = match &self.bounded_rope {
16783 Some(rope) => {
16784 let off = pack.put(&rope.cos);
16786 let _ = pack.put(&rope.sin);
16787 off
16788 }
16789 None => UMAX,
16790 };
16791 let mut full_seen = false;
16792 let mut bounded_seen = 0usize;
16793 let mut phase_seen = 0usize;
16797 let mut gdn_seen = 0usize;
16798 for (li, lw) in self.weights.layers.iter().enumerate() {
16799 let base = meta.len();
16800 meta.resize(base + REC, UMAX);
16801 meta[base] = match &lw.attn {
16802 AttnKind::Linear(w) if w.phase_delta => 1,
16803 AttnKind::Linear(_) => 0,
16804 AttnKind::Full { .. } => 2,
16805 AttnKind::Bounded(_) => 3,
16806 AttnKind::LinearGdn(_) => 4,
16807 _ => UMAX,
16808 };
16809 meta[base + 1] = pack.put(&lw.input_norm);
16810 meta[base + 2] = pack.put(&lw.post_norm);
16811 meta[base + 25] = match &lw.attn {
16812 AttnKind::Linear(_) => {
16813 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16814 phase_seen += 1;
16815 off
16816 }
16817 AttnKind::LinearGdn(_) => {
16818 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16819 gdn_seen += 1;
16820 off
16821 }
16822 _ => UMAX,
16823 };
16824 match &lw.attn {
16825 AttnKind::LinearGdn(w) => {
16826 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16829 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16830 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16831 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16832 meta[base + 60] = pack.put(&w.conv1d);
16833 meta[base + 61] = pack.put(&w.a_log);
16834 meta[base + 62] = pack.put(&w.dt_bias);
16835 meta[base + 63] = pack.put(&w.norm);
16836 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16837 meta[base + 24] = 0;
16838 }
16839 AttnKind::Linear(w) => {
16840 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16841 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16842 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16843 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16844 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16845 meta[base + 7] = pack.put(&decay);
16846 if let Some((kg, kb)) = &w.k_gate {
16847 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16848 meta[base + 9] = pack.put(kb);
16849 }
16850 if let Some(conv) = &w.conv {
16851 meta[base + 10] = pack.put(conv);
16852 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16853 } else {
16854 meta[base + 24] = 0;
16855 }
16856 }
16857 AttnKind::Full { wq, wk, wv, wo, .. } => {
16858 full_seen = true;
16859 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16860 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16861 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16862 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16863 meta[base + 26] = (li * kv_stride) as u32;
16864 }
16865 AttnKind::Bounded(w) => {
16866 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16867 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16868 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16869 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16870 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16872 meta[base + 27] = pack.put(&w.sink_k);
16873 meta[base + 28] = pack.put(&w.sink_v);
16874 bounded_seen += 1;
16875 }
16876 _ => return None,
16877 }
16878 match &lw.ffn {
16879 FfnKind::Dense(d) => {
16880 meta[base + 15] = 0;
16881 meta[base + 16] = 0;
16882 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16883 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16884 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16885 }
16886 FfnKind::Moe(m) => {
16887 let r = m.resonance.as_ref().unwrap();
16888 let (shared, _) = m.shared.as_ref().unwrap();
16889 meta[base + 15] = 1;
16890 meta[base + 16] = m.experts.len() as u32;
16891 meta[base + 17] = pack.put(&r.mu);
16892 meta[base + 18] = pack.put(&r.u);
16893 meta[base + 19] = pack.put(&r.bias);
16894 meta[base + 20] = r.k as u32;
16895 let mut shell = r.effective_shell(m.experts.len());
16904 shell.push(f32::NEG_INFINITY);
16905 meta[base + 30] = pack.put(&shell);
16906 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16907 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16908 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16909 for (e, ex) in m.experts.iter().enumerate() {
16910 meta[base + 32 + e * 3] =
16911 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16912 meta[base + 33 + e * 3] =
16913 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16914 meta[base + 34 + e * 3] =
16915 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16916 }
16917 }
16918 FfnKind::DenseMoe(_) => return None,
16919 }
16920 }
16921 if !full_seen && self.num_layers == 0 {
16922 return None;
16923 }
16924 let id = {
16925 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16926 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16927 };
16928 let model = crate::gpu::EmbryoGraphModel {
16929 id,
16930 hidden: self.hidden_size,
16931 intermediate: self.intermediate_size,
16932 vocab: self.vocab_size,
16933 layers: self.num_layers,
16934 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16935 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16936 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16937 anchor_q_heads: self.num_heads,
16938 anchor_kv_heads: self.num_kv_heads,
16939 anchor_head_dim: self.head_dim,
16940 rotary_dim: self.rotary_dim,
16941 max_seq: self.kv_cache.max_seq_len,
16942 cluster_count,
16943 cluster_size: self.vocab_size / cluster_count,
16944 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16945 state_stride,
16946 kv_stride,
16947 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16948 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16949 weights: pack.data,
16950 meta,
16951 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16952 clusters: clusters.as_ref().clone(),
16953 final_norm: self.weights.final_norm.clone(),
16954 inv_freq: self.inv_freq.as_ref().clone(),
16955 bounded: bounded.is_some(),
16956 kv_layers: if bounded.is_some() {
16957 bounded_seen
16958 } else {
16959 self.num_layers
16960 },
16961 state_layers: phase_seen + gdn_seen,
16962 anchor_window,
16963 anchor_sink,
16964 phase_layers: phase_seen,
16965 gdn_layers: gdn_seen,
16966 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16967 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16968 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16969 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16970 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16971 };
16972 self.embryo_graph = Some(std::sync::Arc::new(model));
16973 }
16974 self.embryo_graph.clone()
16975 }
16976
16977 fn forward_layers_span(
16978 &mut self,
16979 hidden: &[f32],
16980 position: usize,
16981 task_mask: Option<&TaskMask>,
16982 from: usize,
16983 upto: Option<usize>,
16984 ) -> Vec<f32> {
16985 #[cfg(target_os = "macos")]
16986 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16987 position,
16988 from,
16989 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16990 );
16991 debug_assert!(
16992 from == 0
16993 || (self.dsv4.is_none()
16994 && self.dsv41.is_none()
16995 && self.qwen4_exp.is_none()
16996 && !self.per_token_stack())
16997 );
16998 #[cfg(target_os = "macos")]
17004 if !crate::gpu_metal::wait_replay() {
17005 self.fail_metal_graph("the pending async replay failed before a plain forward");
17006 return vec![0.0; self.hidden_size];
17007 }
17008 if let Some(b) = &mut self.qwen4_exp {
17009 let _ = (task_mask, upto);
17010 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17011 let mut logits = Vec::new();
17012 crate::qwen4_exp::forward_token(
17013 &b.0,
17014 &b.1,
17015 &b.2,
17016 &mut b.3,
17017 token_id,
17018 position,
17019 &self.inv_freq,
17020 self.pool.as_deref(),
17021 &mut logits,
17022 true,
17023 );
17024 self.graph_logits = Some(logits);
17025 return vec![0.0; self.hidden_size];
17026 }
17027 if let Some(b) = &mut self.dsv4 {
17033 let _ = (task_mask, upto);
17034 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17035 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
17036 st.pos = position;
17037 let mut logits = Vec::new();
17038 crate::dsv4::forward_token(
17039 g,
17040 layers,
17041 &cfg,
17042 st,
17043 token_id,
17044 &self.inv_freq,
17045 self.pool.as_deref(),
17046 &mut logits,
17047 );
17048 self.graph_logits = Some(logits);
17049 self.dspark_probe(position, token_id);
17050 return vec![0.0; self.hidden_size];
17053 }
17054 if let Some(b) = &mut self.dsv41 {
17056 let _ = (task_mask, upto);
17057 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
17058 let mut logits = Vec::new();
17059 crate::dsv41::forward_token(
17060 &b.0,
17061 &b.1,
17062 &b.2,
17063 &mut b.3,
17064 token_id,
17065 position,
17066 self.pool.as_deref(),
17067 &mut logits,
17068 );
17069 self.graph_logits = Some(logits);
17070 return vec![0.0; self.hidden_size];
17071 }
17072 if let Some(b) = &self.g4e {
17078 let _ = (task_mask, upto);
17079 return crate::g4e::g4e_forward(
17080 &b.0,
17081 &b.1,
17082 hidden,
17083 position,
17084 &mut self.kv_cache.layers,
17085 self.num_heads,
17086 self.num_kv_heads,
17087 self.pool.as_deref(),
17088 );
17089 }
17090 if let Some(b) = &self.g3n {
17091 let _ = (task_mask, upto);
17092 return crate::g3n::g3n_forward(
17093 &b.0,
17094 &b.1,
17095 hidden,
17096 position,
17097 &mut self.kv_cache.layers,
17098 self.num_heads,
17099 self.num_kv_heads,
17100 self.head_dim,
17101 self.pool.as_deref(),
17102 );
17103 }
17104 if from == 0
17111 && upto.is_none()
17112 && task_mask.is_none()
17113 && self.anchor_core.is_some()
17114 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
17115 {
17116 static ONCE: std::sync::Once = std::sync::Once::new();
17117 ONCE.call_once(|| {
17118 eprintln!(
17119 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
17120 unsupported={} eligible={}",
17121 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17122 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17123 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17124 crate::gpu::enabled_here(),
17125 self.graph_refused(),
17126 self.embryo_resident_eligible(),
17127 );
17128 });
17129 }
17130 if from == 0
17131 && upto.is_none()
17132 && task_mask.is_none()
17133 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17137 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17138 && matches!(
17142 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17143 Ok("1") | Ok("parallel")
17144 )
17145 && crate::gpu::enabled_here()
17146 && !self.graph_refused()
17147 && (position == 0 || self.device_sequence_position().is_some())
17153 && self.embryo_resident_eligible()
17154 && let Some(model) = self.ensure_embryo_graph()
17155 {
17156 let mut lg = Vec::new();
17157 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17158 {
17159 self.graph_logits = Some(lg);
17160 return vec![0.0; self.hidden_size];
17161 }
17162 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17163 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17164 }
17165 self.mark_graph_refused();
17171 if position != 0 {
17172 self.kv_cache.clear();
17176 self.clear_history();
17177 crate::gpu::graph_kv_reset(self.graph_kv_id);
17178 panic!(
17179 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
17180 );
17181 }
17182 }
17183 let mut h = hidden.to_vec();
17184 self.mimo_moe_prepare();
17187 let _mimo_q8 = self
17188 .mimo_moe
17189 .is_on()
17190 .then(crate::qtensor::enter_full_gpu_q8_scope);
17191 let (nh, _nkv, _hd, hs, _rd, eps) = (
17194 self.num_heads,
17195 self.num_kv_heads,
17196 self.head_dim,
17197 self.hidden_size,
17198 self.rotary_dim,
17199 self.rms_eps,
17200 );
17201 let pool = self.pool.clone();
17202 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
17214 let graph_on = match graph_env.as_deref() {
17215 Some("0") => false,
17216 Some("prefill") => false, Some(_) => true,
17218 None => crate::gpu::wgpu_graph_default(),
17224 };
17225 let graph_trusted =
17226 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17227 let race_eligible =
17228 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17229 let mut tail_start = 0usize;
17230 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17231 let t_graph = std::time::Instant::now();
17232 let mut lg = Vec::new();
17233 let mut gl = 0usize;
17234 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17235 let declined = built.is_none();
17236 let built = match built {
17237 Some(Ok(hh)) => Some(hh),
17238 Some(Err(())) => {
17239 self.clear_sequence_state();
17243 self.graph_failed
17244 .store(true, std::sync::atomic::Ordering::Relaxed);
17245 self.cancel
17246 .store(true, std::sync::atomic::Ordering::Relaxed);
17247 tracing::error!("token graph failed after admission; sequence state cleared");
17248 return vec![0.0; self.hidden_size];
17249 }
17250 None => None,
17251 };
17252 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17257 self.mark_graph_refused();
17258 }
17259 graph_note(built.is_some(), gl, self.num_layers);
17260 if let Some(hh) = built {
17261 let dur = t_graph.elapsed();
17262 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17263 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17264 }
17265 if gl > 0 && gl < self.num_layers {
17266 h = hh;
17272 tail_start = gl;
17273 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17274 if !graph_trusted {
17275 crate::gpu::graph_race_record(true, dur);
17276 }
17277 if !lg.is_empty() {
17278 lg.resize(self.vocab_size, 0.0);
17281 if let Some(c) = self.final_softcap {
17282 softcap_in_place(&mut lg, c, self.pool.as_deref());
17283 }
17284 self.graph_logits = Some(lg);
17285 }
17286 return hh;
17287 }
17288 }
17294 }
17295 let span = from > 0 || upto.is_some();
17319 if span && graph_on && task_mask.is_none() && graph_trusted {
17320 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17321 let mut lg = Vec::new();
17322 let mut gl = 0usize;
17323 let span_res =
17324 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17325 let span_res = match span_res {
17326 Some(Ok(hh)) => Some(hh),
17327 Some(Err(())) => {
17328 self.clear_sequence_state();
17329 self.graph_failed
17330 .store(true, std::sync::atomic::Ordering::Relaxed);
17331 self.cancel
17332 .store(true, std::sync::atomic::Ordering::Relaxed);
17333 tracing::error!(
17334 "span token graph failed after admission; sequence state cleared"
17335 );
17336 return vec![0.0; self.hidden_size];
17337 }
17338 None => None,
17339 };
17340 graph_note(span_res.is_some(), gl, upto_excl - from);
17341 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17342 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17346 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17347 eprintln!(
17348 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17349 upto_excl - from,
17350 span_res.is_some()
17351 );
17352 }
17353 }
17354 if let Some(hh) = span_res {
17355 if gl == upto_excl - from {
17356 if !lg.is_empty() {
17357 lg.resize(self.vocab_size, 0.0);
17358 if let Some(c) = self.final_softcap {
17359 softcap_in_place(&mut lg, c, self.pool.as_deref());
17360 }
17361 self.graph_logits = Some(lg);
17362 }
17363 crate::gpu::set_layer(-1);
17364 return hh;
17365 }
17366 h = hh;
17368 tail_start = from + gl;
17369 }
17370 }
17371 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17376
17377 let host_tail = tail_start > from;
17387 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17388 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17389
17390 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17391 #[cfg(target_os = "macos")]
17392 let mut gpu_skip_until = 0usize;
17393 for li in tail_start.max(from)..self.num_layers {
17394 let _capacity_tail = automatic_gpu_prefix
17395 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17396 .map(|_| crate::gpu::enter_cpu_scope());
17397 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17399 if li > u {
17400 break;
17401 }
17402 }
17403 if let Some(mask) = task_mask {
17404 if !mask.layer_alive(li) {
17405 continue; }
17407 }
17408 if host_tail && task_mask.is_none() {
17413 match self.m3_pooled_layer(li, position, &mut h) {
17414 Ok(true) => {
17415 crate::gpu::set_layer(-1);
17416 continue;
17417 }
17418 Ok(false) => {}
17419 Err(()) => {
17420 self.clear_sequence_state();
17421 self.graph_failed
17422 .store(true, std::sync::atomic::Ordering::Relaxed);
17423 self.cancel
17424 .store(true, std::sync::atomic::Ordering::Relaxed);
17425 tracing::error!(
17426 "pooled tail token graph failed after admission; sequence state cleared"
17427 );
17428 return vec![0.0; self.hidden_size];
17429 }
17430 }
17431 }
17432 #[cfg(target_os = "macos")]
17436 {
17437 if li < gpu_skip_until {
17438 continue;
17439 }
17440 if task_mask.is_none() {
17441 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17442 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17443 return vec![0.0; self.hidden_size];
17447 }
17448 if end > li {
17449 gpu_skip_until = end;
17450 if self.is_loop_end(end - 1) && end < self.num_layers {
17453 h = inference::rms_norm(
17454 &h,
17455 &self.weights.final_norm,
17456 self.rms_eps,
17457 self.norm_style,
17458 );
17459 }
17460 continue;
17461 }
17462 }
17463 }
17464
17465 if task_mask.is_none() {
17466 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17467 crate::gpu::BatchGraphOutcome::Completed => continue,
17468 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17469 crate::gpu::BatchGraphOutcome::Declined => {}
17470 }
17471 }
17472 #[cfg(feature = "gpu")]
17473 self.pull_lagging_host_kv(li, li + 1, position);
17474 let lw = &self.weights.layers[self.phys_layer(li)];
17475 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17476 if tp.parse::<usize>().ok() == Some(position) {
17477 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17478 eprintln!(
17479 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17480 h[0], h[1]
17481 );
17482 }
17483 }
17484 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17487 inference::rms_norm_into(
17488 &h,
17489 &lw.input_norm,
17490 self.rms_eps,
17491 self.norm_style,
17492 &mut self.ws.n1,
17493 );
17494 drop(prof);
17495
17496 let attn_out = match &lw.attn {
17497 AttnKind::Mla(w) => {
17498 let inv_freq_l = self.layer_inv_freq(li);
17499 let rs = self.layer_rope_scale(li);
17500 let eps = self.rms_eps;
17501 let pool = self.pool.clone();
17502 mla_attention(
17503 w,
17504 &self.ws.n1,
17505 &mut self.kv_cache.layers[li],
17506 position,
17507 &inv_freq_l,
17508 rs,
17509 eps,
17510 pool.as_deref(),
17511 )
17512 }
17513 AttnKind::Linear(w) => {
17514 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17515 vmf_phase_forward(
17516 &self.ws.n1,
17517 w,
17518 &cfg,
17519 &mut self.kv_cache.layers[li].linear_state,
17520 self.pool.as_deref(),
17521 )
17522 }
17523 AttnKind::Kda(w) => {
17524 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17525 crate::linear_core::kda_forward(
17526 &self.ws.n1,
17527 w,
17528 &cfg,
17529 &mut self.kv_cache.layers[li].linear_state,
17530 self.pool.as_deref(),
17531 )
17532 }
17533 AttnKind::LinearGdn(w) => {
17534 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17535 gdn_forward(
17536 &self.ws.n1,
17537 w,
17538 &cfg,
17539 &mut self.kv_cache.layers[li].linear_state,
17540 self.pool.as_deref(),
17541 )
17542 }
17543 AttnKind::ShortConv(w) => {
17544 let cfg = self
17545 .short_conv_cfg
17546 .expect("short-conv layer without short_conv_cfg");
17547 short_conv_forward(
17548 &self.ws.n1,
17549 w,
17550 &cfg,
17551 &mut self.kv_cache.layers[li].linear_state,
17552 self.pool.as_deref(),
17553 )
17554 }
17555 AttnKind::Bounded(w) => {
17556 let rope = self
17559 .bounded_rope
17560 .clone()
17561 .expect("bounded layer without an installed rotation table");
17562 let cfg = crate::bounded::BoundedAttnCfg {
17563 num_heads: self.num_heads,
17564 num_kv_heads: self.num_kv_heads,
17565 head_dim: self.head_dim,
17566 hidden_size: hs,
17567 scale: self.attn_scale,
17568 rope: &rope,
17569 pool: pool.as_deref(),
17570 };
17571 crate::bounded::bounded_attention(
17572 &self.ws.n1,
17573 w,
17574 &mut self.kv_cache.layers[li],
17575 &cfg,
17576 )
17577 }
17578 AttnKind::Full {
17579 wq,
17580 wk,
17581 wv,
17582 wo,
17583 q_norm,
17584 k_norm,
17585 output_gate,
17586 softplus_gate,
17587 bias,
17588 } if self.kv_cache.layers[li].o1_sealed() => {
17589 let inv_freq_l = self.layer_inv_freq(li);
17592 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17593 let cfg = QwenAttnCfg {
17594 num_heads: self.layer_num_heads(li),
17595 num_kv_heads: nkv_l,
17596 head_dim: hd_l,
17597 hidden_size: hs,
17598 position,
17599 inv_freq: &inv_freq_l,
17600 rotary_dim: rd_l,
17601 scale: self.attn_scale,
17602 softcap: self.attn_softcap,
17603 window: None,
17604 v_norm: self.attn_v_norm,
17605 qk_norm_after_rope: self.qk_norm_after_rope,
17606 gate_sigmoid: self.proj_gate_sigmoid,
17607 q_norm: q_norm.as_deref(),
17608 k_norm: k_norm.as_deref(),
17609 output_gate: *output_gate,
17610 softplus_gate: softplus_gate
17611 .as_ref()
17612 .map(|(gate, per_head)| (gate, *per_head)),
17613 rope_scale: self.layer_rope_scale(li),
17614 bias: bias
17615 .as_ref()
17616 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17617 rms_eps: eps,
17618 norm_style: self.norm_style,
17619 pool: pool.as_deref(),
17620 v_head_dim: self.layer_v_dim(li),
17621 };
17622 attention::qwen_attention_nystrom(
17623 &self.ws.n1,
17624 wq,
17625 wk,
17626 wv,
17627 wo,
17628 &mut self.kv_cache.layers[li],
17629 &cfg,
17630 )
17631 }
17632 AttnKind::Full {
17633 wq,
17634 wk,
17635 wv,
17636 wo,
17637 q_norm,
17638 k_norm,
17639 output_gate,
17640 softplus_gate,
17641 bias,
17642 } => 'attn: {
17643 let dropin_reason =
17648 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17649 if let Some(reason) = dropin_reason {
17650 self.note_graph_decline("wgpu attn dropin", reason);
17651 }
17652 if graph_on
17653 && dropin_reason.is_none()
17654 && !*output_gate
17655 && softplus_gate.is_none()
17656 && self.attention_heads_per_layer.is_none()
17657 && bias.is_none()
17658 && task_mask.is_none()
17659 {
17660 let inv_freq_l = self.layer_inv_freq(li);
17661 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17662 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17663 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17664 wq.mapped_q1(),
17665 wk.mapped_q1(),
17666 wv.mapped_q1(),
17667 wo.mapped_q1(),
17668 ) {
17669 let gm = gm.clone();
17670 let mut out = vec![0f32; hs];
17671 let cache = &self.kv_cache.layers[li];
17672 if crate::gpu::attn_dropin(
17673 &gm,
17674 self.graph_kv_id,
17675 li,
17676 &self.ws.n1,
17677 qi,
17678 ki,
17679 vi,
17680 oi,
17681 q_norm.as_deref(),
17682 k_norm.as_deref(),
17683 self.qk_norm_after_rope,
17684 &inv_freq_l,
17685 nh,
17686 nkv_l,
17687 hd_l,
17688 rd_l,
17689 hs,
17690 position,
17691 self.kv_cache.max_seq_len,
17692 gemma,
17693 eps as f32,
17694 cache.k_heads(),
17695 cache.v_heads(),
17696 &mut out,
17697 ) {
17698 break 'attn out;
17699 }
17700 }
17701 }
17702 let masked = task_mask
17703 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17704 .unwrap_or(false);
17705 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17706 let plain = self.layer_attn_plain(li);
17709 match (masked, f32_view) {
17710 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17713 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17714 attention::multi_head_attention(
17715 &self.ws.n1,
17716 q,
17717 k,
17718 v,
17719 o,
17720 &mut self.kv_cache.layers[li],
17721 self.num_heads,
17722 self.num_kv_heads,
17723 self.head_dim,
17724 self.hidden_size,
17725 position,
17726 &active_heads,
17727 &self.inv_freq,
17728 )
17729 }
17730 (masked, _) => {
17731 if masked {
17732 tracing::warn!(
17733 "layer {li}: head mask on quantized weights or on a \
17734 window/sink/per-layer-geometry layer not supported \
17735 yet — executing dense"
17736 );
17737 }
17738 let inv_freq_l = self.layer_inv_freq(li);
17739 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17740 let cfg = QwenAttnCfg {
17741 num_heads: self.layer_num_heads(li),
17742 num_kv_heads: nkv_l,
17743 head_dim: hd_l,
17744 hidden_size: hs,
17745 position,
17746 inv_freq: &inv_freq_l,
17747 rotary_dim: rd_l,
17748 scale: self.attn_scale,
17749 softcap: self.attn_softcap,
17750 window: self.layer_window(li),
17751 v_norm: self.attn_v_norm,
17752 qk_norm_after_rope: self.qk_norm_after_rope,
17753 gate_sigmoid: self.proj_gate_sigmoid,
17754 q_norm: q_norm.as_deref(),
17755 k_norm: k_norm.as_deref(),
17756 output_gate: *output_gate,
17757 softplus_gate: softplus_gate
17758 .as_ref()
17759 .map(|(gate, per_head)| (gate, *per_head)),
17760 rope_scale: self.layer_rope_scale(li),
17761 bias: bias
17762 .as_ref()
17763 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17764 rms_eps: eps,
17765 norm_style: self.norm_style,
17766 pool: pool.as_deref(),
17767 v_head_dim: self.layer_v_dim(li),
17768 };
17769 attention::qwen_attention(
17770 &self.ws.n1,
17771 wq,
17772 wk,
17773 wv,
17774 wo,
17775 &mut self.kv_cache.layers[li],
17776 &cfg,
17777 )
17778 }
17779 }
17780 }
17781 };
17782 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17785 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17786 None => attn_out,
17787 };
17788 let lw = &self.weights.layers[self.phys_layer(li)];
17789 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17790 inference::add_rmsnorm_fused_into(
17791 &mut h,
17792 &attn_out,
17793 &lw.post_norm,
17794 self.rms_eps,
17795 self.norm_style,
17796 &mut self.ws.p1,
17797 );
17798 drop(prof);
17799 let mut attn_out = attn_out;
17800 attention::recycle_buf(&mut attn_out);
17801 let post_normed = &self.ws.p1;
17802
17803 let ffn_masked = task_mask
17804 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17805 .unwrap_or(false);
17806 let ffn_out = match (ffn_masked, &lw.ffn) {
17818 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17822 let row = task_mask
17823 .and_then(|tm| tm.ffn_masks.get(li))
17824 .map(|v| v.as_slice());
17825 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17826 }
17827 (true, FfnKind::Dense(d)) => {
17828 let tm = task_mask.unwrap();
17829 let alive = tm.ffn_active_count(li);
17830 let deep = alive * 2 <= self.intermediate_size;
17831 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17832 let active = tm.ffn_active_indices(li);
17833 sparse_ffn_quant(
17834 d,
17835 post_normed,
17836 &active,
17837 self.hidden_size,
17838 self.pool.as_deref(),
17839 )
17840 } else if deep
17841 && let (Some(g), Some(u), Some(dn)) = (
17842 d.gate_proj.as_f32(),
17843 d.up_proj.as_f32(),
17844 d.down_proj.as_f32(),
17845 )
17846 {
17847 let active = tm.ffn_active_indices(li);
17848 inference::sparse_ffn_forward(
17849 post_normed,
17850 g,
17851 u,
17852 dn,
17853 self.hidden_size,
17854 self.intermediate_size,
17855 &active,
17856 self.pool.as_deref(),
17857 )
17858 } else {
17859 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17860 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17861 }
17862 }
17863 (true, FfnKind::Moe(m)) => {
17864 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17868 ffn_forward(
17869 &lw.ffn,
17870 post_normed,
17871 self.pool.as_deref(),
17872 allowed.as_deref(),
17873 )
17874 }
17875 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17876 dm,
17877 post_normed,
17878 &h,
17879 self.rms_eps,
17880 self.norm_style,
17881 self.pool.as_deref(),
17882 ),
17883 (false, _) => match &lw.ffn {
17884 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17885 dm,
17886 post_normed,
17887 &h,
17888 self.rms_eps,
17889 self.norm_style,
17890 self.pool.as_deref(),
17891 ),
17892 FfnKind::Moe(m)
17893 if task_mask.is_none()
17894 && host_tail
17895 && li >= tail_start
17896 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17897 {
17898 moe_ffn_minicpm_tail(
17899 &mut self.minicpm_tail,
17900 li,
17901 self.num_layers,
17902 m,
17903 post_normed,
17904 self.pool.as_deref(),
17905 )
17906 }
17907 FfnKind::Moe(m)
17908 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17909 {
17910 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17911 }
17912 _ => {
17913 let allowed = match (&lw.ffn, task_mask) {
17914 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17915 _ => None,
17916 };
17917 ffn_forward(
17918 &lw.ffn,
17919 post_normed,
17920 self.pool.as_deref(),
17921 allowed.as_deref(),
17922 )
17923 }
17924 },
17925 };
17926 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17927 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17928 None => ffn_out,
17929 };
17930 for (i, &f) in ffn_out.iter().enumerate() {
17931 h[i] += f;
17932 }
17933 let mut ffn_out = ffn_out;
17934 attention::recycle_buf(&mut ffn_out);
17935
17936 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17938 for v in h.iter_mut() {
17939 *v *= sc;
17940 }
17941 }
17942 if self.layer_dump.is_some() {
17944 self.dump_layer_row(position, li, &h);
17945 }
17946
17947 if self.is_loop_end(li) && li + 1 < self.num_layers {
17950 h = inference::rms_norm(
17951 &h,
17952 &self.weights.final_norm,
17953 self.rms_eps,
17954 self.norm_style,
17955 );
17956 }
17957
17958 if self.dyn_phi_layer == Some(li) {
17962 self.update_dyn_phi(&h);
17963 }
17964 }
17965 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17967 crate::gpu::graph_race_record(false, t.elapsed());
17968 }
17969
17970 h
17971 }
17972
17973 fn update_dyn_phi(&mut self, h: &[f32]) {
17976 const A: f32 = 0.2;
17977 if self.dyn_phi_ema.len() != h.len() {
17978 self.dyn_phi_ema = vec![0.0; h.len()];
17979 self.dyn_phi_seen = 0;
17980 }
17981 if self.dyn_phi_seen == 0 {
17982 self.dyn_phi_ema.copy_from_slice(h);
17983 } else {
17984 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17985 *e = (1.0 - A) * *e + A * v;
17986 }
17987 }
17988 self.dyn_phi_seen += 1;
17989 }
17990
17991 pub fn dyn_phi(&self) -> &[f32] {
17993 &self.dyn_phi_ema
17994 }
17995
17996 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17998 self.dyn_phi_layer = layer;
17999 self.dyn_phi_ema.clear();
18000 self.dyn_phi_seen = 0;
18001 }
18002
18003 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
18005 let Some(model) = &self.model else {
18006 return Vec::new();
18007 };
18008 model
18009 .header
18010 .skills
18011 .iter()
18012 .enumerate()
18013 .filter_map(|(i, sk)| {
18014 if sk.is_v2() {
18018 return None;
18019 }
18020 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
18021 let sel = sk.selection.as_ref()?;
18022 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
18023 })
18024 .collect()
18025 }
18026
18027 pub fn active_skill(&self) -> Option<usize> {
18029 self.dyn_active
18030 }
18031
18032 pub fn enable_dynamic_routing(&mut self) -> usize {
18037 use crate::swarm::{DynRouter, RoutableSkill};
18038 let Some(model) = self.model.clone() else {
18039 return 0;
18040 };
18041 if let Some(r) = &model.header.router {
18048 tracing::warn!(
18049 "dynamic routing disabled: this file declares router policy '{}' with \
18050 granularity \"{}\" — the request-level decision applies instead",
18051 r.policy,
18052 r.granularity
18053 );
18054 return 0;
18055 }
18056 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
18062 || model.header.skills.iter().any(|s| s.is_v2())
18063 {
18064 tracing::warn!(
18065 "dynamic routing disabled: this file carries format-v2 skill records \
18066 (SKILLS_V2) — they route per request through a router policy only"
18067 );
18068 return 0;
18069 }
18070 if self.dyn_blend_loaded {
18073 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
18074 return 0;
18075 }
18076 if let Some(a) = self.dyn_active {
18080 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
18081 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
18082 return 0;
18083 }
18084 }
18085 let hidden = self.hidden_size;
18086 let mut skills = Vec::new();
18087 for (idx, id, _phi) in self.dynamic_skills() {
18088 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
18089 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
18090 skills.push(rs);
18091 }
18092 }
18093 }
18094 if skills.is_empty() {
18095 return 0;
18096 }
18097 let phi = skills[0].phi_layer;
18099 if skills.iter().any(|s| s.phi_layer != phi) {
18100 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
18101 }
18102 let n = skills.len();
18103 self.set_dyn_phi_layer(Some(phi));
18104 self.dyn_router = Some(DynRouter::new(skills));
18105 n
18106 }
18107
18108 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
18110 self.dyn_router
18111 .as_ref()
18112 .map(|r| r.switches.clone())
18113 .unwrap_or_default()
18114 }
18115
18116 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
18119 let _mimo_q8 = self
18120 .mimo_moe
18121 .is_on()
18122 .then(crate::qtensor::enter_full_gpu_q8_scope);
18123 let rows = self.weights.lm_head.rows();
18124 let mut logits = attention::take_buf(rows.min(self.vocab_size));
18125 let served = self.mimo_moe.is_on()
18129 && crate::gpu::mimo_q8_short_enabled()
18130 && rows == self.vocab_size
18131 && !self.weights.lm_head.has_prism_contract()
18132 && self
18133 .weights
18134 .lm_head
18135 .graph_weight()
18136 .is_some_and(|(model, idx, kind, _)| {
18137 kind == 7
18138 && crate::gpu::q82_short_rows(
18139 model,
18140 idx,
18141 hidden,
18142 1,
18143 rows,
18144 self.hidden_size,
18145 &mut logits,
18146 )
18147 });
18148 if !served {
18149 self.weights
18150 .lm_head
18151 .matvec(hidden, &mut logits, self.pool.as_deref());
18152 }
18153 logits.resize(self.vocab_size, 0.0);
18154 if let Some(m) = self.logit_multiplier {
18155 for l in logits.iter_mut() {
18156 *l *= m;
18157 }
18158 }
18159 if let Some(c) = self.final_softcap {
18160 softcap_in_place(&mut logits, c, self.pool.as_deref());
18161 }
18162 if let Some(cm) = self.head_clusters.as_ref() {
18163 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18164 }
18165 logits
18166 }
18167
18168 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18171 let h = hidden.len();
18172 let ncl = cm.len() / h.max(1);
18173 if ncl == 0 || logits.len() % ncl != 0 {
18174 return;
18175 }
18176 let cs = logits.len() / ncl;
18177 let mut lc = vec![0.0f32; ncl];
18179 for c in 0..ncl {
18180 let row = &cm[c * h..(c + 1) * h];
18181 let mut s = 0.0f32;
18182 for j in 0..h {
18183 s += row[j] * hidden[j];
18184 }
18185 lc[c] = s;
18186 }
18187 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18188 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
18189 for c in 0..ncl {
18190 let blk = &mut logits[c * cs..(c + 1) * cs];
18191 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18192 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
18193 let add = lc[c] - lse - bl;
18194 for v in blk.iter_mut() {
18195 *v += add;
18196 }
18197 }
18198 }
18199
18200 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
18205 #[cfg(target_os = "macos")]
18206 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
18207 self.clear_sequence_state();
18208 crate::gpu::graph_race_begin_generation();
18212 if task_mask.is_none() {
18213 self.o1_begin();
18214 }
18215 let mut hidden = vec![0.0f32; self.hidden_size];
18216 for (pos, &id) in ids.iter().enumerate() {
18217 let emb = self.embed_single(id);
18218 hidden = self.forward_layers(&emb, pos, task_mask);
18219 }
18220 if let Err(err) = self.o1_seal_checked() {
18221 self.o1_fail(err);
18222 }
18223 if let Some(logits) = self.graph_logits.take() {
18226 return logits;
18227 }
18228 inference::rms_norm_into(
18229 &hidden,
18230 &self.weights.final_norm,
18231 self.rms_eps,
18232 self.norm_style,
18233 &mut self.ws.n1,
18234 );
18235 self.lm_head_forward(&self.ws.n1)
18236 }
18237}
18238
18239pub fn create_test_pipeline(
18241 hidden_size: usize,
18242 intermediate_size: usize,
18243 num_heads: usize,
18244 num_kv_heads: usize,
18245 head_dim: usize,
18246 num_layers: usize,
18247 vocab_size: usize,
18248) -> Pipeline {
18249 let synth = |n: usize, salt: usize| -> Vec<f32> {
18252 (0..n)
18253 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18254 .collect()
18255 };
18256 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18257 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18258 };
18259 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18260 .map(|li| LayerWeights {
18261 input_norm: vec![1.0; hidden_size],
18262 post_norm: vec![1.0; hidden_size],
18263 attn_out_norm: None,
18264 ffn_out_norm: None,
18265 layer_scale: None,
18266 ffn: FfnKind::Dense(DenseFfn {
18267 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18268 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18269 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18270 act: Act::Silu,
18271 down_t: None,
18272 segs: Vec::new(),
18273 }),
18274 attn: AttnKind::Full {
18275 bias: None,
18276 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18277 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18278 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18279 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18280 q_norm: None,
18281 k_norm: None,
18282 output_gate: false,
18283 softplus_gate: None,
18284 },
18285 })
18286 .collect();
18287
18288 Pipeline::new(
18289 Tokenizer::byte_level(),
18290 PipelineWeights {
18291 embed_tokens: qt(vocab_size, hidden_size, 100),
18292 layers: layer_weights,
18293 lm_head: qt(vocab_size, hidden_size, 200),
18294 final_norm: vec![1.0; hidden_size],
18295 },
18296 hidden_size,
18297 intermediate_size,
18298 num_heads,
18299 num_kv_heads,
18300 head_dim,
18301 num_layers,
18302 num_layers, false, vocab_size,
18305 1e-6,
18306 10_000.0,
18307 NormStyle::Qwen,
18308 4096,
18309 SamplerConfig {
18310 seed: Some(42),
18311 ..Default::default()
18312 },
18313 )
18314}
18315
18316#[inline]
18321fn mask_bit(row: &[u8], j: usize) -> bool {
18322 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18323}
18324
18325fn mask_gain() -> f32 {
18336 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18337 *G.get_or_init(|| {
18338 std::env::var("CMF_FFN_MASK_GAIN")
18339 .ok()
18340 .and_then(|v| v.parse().ok())
18341 .unwrap_or(1.0)
18342 })
18343}
18344
18345fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18346 let fill = meanfill().and_then(|(i, v)| {
18349 let li = crate::gpu::cur_layer();
18350 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18351 });
18352 for r in 0..rows {
18353 let base = r * inter;
18354 for (bi, &byte) in row.iter().enumerate() {
18355 if byte == 0xFF {
18356 continue;
18357 }
18358 let j0 = bi * 8;
18359 for bit in 0..8 {
18360 let j = j0 + bit;
18361 if j < inter && byte & (1 << bit) == 0 {
18362 g[base + j] = fill.map_or(0.0, |f| f[j]);
18363 }
18364 }
18365 }
18366 }
18367 let gain = mask_gain();
18368 if gain != 1.0 {
18369 for v in g[..rows * inter].iter_mut() {
18370 *v *= gain;
18371 }
18372 }
18373}
18374
18375#[inline]
18377fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18378 row.is_none_or(|r| mask_bit(r, i))
18379}
18380
18381fn all_bits_on(row: &[u8], n: usize) -> bool {
18384 (0..n).all(|i| mask_bit(row, i))
18385}
18386
18387fn tube_topk() -> usize {
18395 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18396 *K.get_or_init(|| {
18397 std::env::var("CMF_TUBE_TOPK")
18398 .ok()
18399 .and_then(|v| v.parse().ok())
18400 .unwrap_or(0)
18401 })
18402}
18403
18404fn tube_score_oracle() -> bool {
18405 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18406 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18407}
18408
18409fn tube_ffn_routed(
18416 d: &DenseFfn,
18417 xs: &[f32],
18418 b: usize,
18419 pool: Option<&Pool>,
18420 mask_row: Option<&[u8]>,
18421 k: usize,
18422) -> Vec<f32> {
18423 let hidden = d.down_proj.rows();
18424 let core = d.gate_proj.rows();
18425 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18426 let mut out = match (b, core_full, mask_row) {
18427 (1, true, _) => dense_ffn(d, xs, pool),
18428 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18429 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18430 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18431 };
18432 let cand: Vec<usize> = (0..d.segs.len())
18433 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18434 .collect();
18435 if cand.is_empty() {
18436 return out;
18437 }
18438 let oracle = tube_score_oracle();
18442 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18443 let mut scores = vec![0f32; b * cand.len()];
18444 for (ci, &i) in cand.iter().enumerate() {
18445 let seg = &d.segs[i];
18446 let w = seg.width;
18447 let mut g = vec![0.0f32; b * w];
18448 if b == 1 {
18449 seg.gate.matvec(xs, &mut g, pool);
18450 } else {
18451 seg.gate.matmat(xs, b, &mut g, pool);
18452 }
18453 for v in g.iter_mut() {
18454 *v = Act::Silu.combine(*v, 1.0);
18455 }
18456 if !oracle {
18457 for t in 0..b {
18458 scores[t * cand.len() + ci] =
18459 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18460 }
18461 }
18462 if oracle || b > 1 {
18463 let mut u = vec![0.0f32; b * w];
18464 if b == 1 {
18465 seg.up.matvec(xs, &mut u, pool);
18466 } else {
18467 seg.up.matmat(xs, b, &mut u, pool);
18468 }
18469 for (a, &v) in g.iter_mut().zip(u.iter()) {
18470 *a *= v;
18471 }
18472 if oracle {
18473 for t in 0..b {
18474 scores[t * cand.len() + ci] =
18475 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18476 }
18477 }
18478 }
18479 acts.push(g);
18480 }
18481 let keep = k.min(cand.len());
18483 let mut scratch: Vec<f32> = Vec::new();
18484 for t in 0..b {
18485 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18486 .map(|ci| (scores[t * cand.len() + ci], ci))
18487 .collect();
18488 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18489 let mut alive = vec![false; cand.len()];
18490 for &(_, ci) in sc.iter().take(keep) {
18491 alive[ci] = true;
18492 }
18493 if b > 1 {
18494 for (ci, a) in acts.iter_mut().enumerate() {
18495 if !alive[ci] {
18496 let w = d.segs[cand[ci]].width;
18497 a[t * w..(t + 1) * w].fill(0.0);
18498 }
18499 }
18500 } else {
18501 for (ci, &i) in cand.iter().enumerate() {
18505 if !alive[ci] {
18506 continue;
18507 }
18508 let seg = &d.segs[i];
18509 let w = seg.width;
18510 let g = &mut acts[ci];
18511 if !tube_score_oracle() {
18512 scratch.clear();
18513 scratch.resize(w, 0.0);
18514 seg.up.matvec(xs, &mut scratch, pool);
18515 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18516 *a *= v;
18517 }
18518 }
18519 let mut acc = vec![0.0f32; hidden];
18520 seg.down.matvec(g, &mut acc, pool);
18521 for (o, a) in out.iter_mut().zip(&acc) {
18522 *o += *a;
18523 }
18524 }
18525 }
18526 }
18527 if b > 1 {
18528 for (ci, &i) in cand.iter().enumerate() {
18529 let seg = &d.segs[i];
18530 let mut acc = vec![0.0f32; b * hidden];
18531 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18532 for (o, a) in out.iter_mut().zip(&acc) {
18533 *o += *a;
18534 }
18535 }
18536 }
18537 out
18538}
18539
18540fn tube_ffn(
18546 d: &DenseFfn,
18547 xs: &[f32],
18548 b: usize,
18549 pool: Option<&Pool>,
18550 mask_row: Option<&[u8]>,
18551) -> Vec<f32> {
18552 if tube_topk() > 0 {
18553 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18554 }
18555 let hidden = d.down_proj.rows();
18556 let core = d.gate_proj.rows();
18557 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18558 let mut out = match (b, core_full, mask_row) {
18559 (1, true, _) => dense_ffn(d, xs, pool),
18560 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18561 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18562 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18563 };
18564 TUBE_SCRATCH.with(|sc| {
18565 let mut sc = sc.borrow_mut();
18566 let [g, u, acc] = &mut *sc;
18567 for seg in &d.segs {
18568 if !tube_bit(mask_row, seg.start) {
18569 continue;
18570 }
18571 let w = seg.width;
18572 g.resize(b * w, 0.0);
18573 if b == 1
18574 && d.act == Act::Silu
18575 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18576 {
18577 } else {
18579 u.resize(b * w, 0.0);
18580 if b == 1 {
18581 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18582 } else {
18583 seg.gate.matmat(xs, b, g, pool);
18584 seg.up.matmat(xs, b, u, pool);
18585 }
18586 for i in 0..b * w {
18587 g[i] = d.act.combine(g[i], u[i]);
18588 }
18589 }
18590 acc.resize(b * hidden, 0.0);
18591 acc.fill(0.0);
18592 if b == 1 {
18593 seg.down.matvec(g, acc, pool);
18594 } else {
18595 seg.down.matmat(g, b, acc, pool);
18596 }
18597 for (o, a) in out.iter_mut().zip(acc.iter()) {
18598 *o += *a;
18599 }
18600 }
18601 out
18602 })
18603}
18604
18605thread_local! {
18606 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18610 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18611}
18612
18613static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18616 std::sync::atomic::AtomicU64::new(0),
18617 std::sync::atomic::AtomicU64::new(0),
18618];
18619
18620fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18624 #[cfg(feature = "gpu")]
18625 {
18626 use std::sync::atomic::Ordering::Relaxed;
18627 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18628 (
18629 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18630 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18631 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18632 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18633 )
18634 }
18635 #[cfg(not(feature = "gpu"))]
18636 (0.0, 0.0, 0.0, 0)
18637}
18638
18639fn wgpu_mirror_counters() -> [u64; 6] {
18642 #[cfg(feature = "gpu")]
18643 {
18644 crate::gpu_wgpu::MIRROR_EVENTS
18645 .each_ref()
18646 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18647 }
18648 #[cfg(not(feature = "gpu"))]
18649 [0; 6]
18650}
18651
18652fn prefill_prof_on() -> bool {
18653 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18654 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18655}
18656
18657pub(crate) fn accumulate_dense_ffn_activation_mass(
18666 d: &DenseFfn,
18667 xs: &[f32],
18668 b: usize,
18669 pool: Option<&Pool>,
18670 mass: &mut [f64],
18671 squared_mass: &mut [f64],
18672) -> Result<(), String> {
18673 let inter = d.gate_proj.rows();
18674 let hidden = d.gate_proj.cols();
18675 if mass.len() != inter || squared_mass.len() != inter {
18676 return Err(format!(
18677 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18678 mass.len(),
18679 squared_mass.len()
18680 ));
18681 }
18682 if d.up_proj.rows() != inter
18683 || d.up_proj.cols() != hidden
18684 || d.down_proj.cols() != inter
18685 || d.down_proj.rows() != hidden
18686 {
18687 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18688 }
18689 if !d.segs.is_empty() {
18690 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18691 }
18692 if xs.len() != b.saturating_mul(hidden) {
18693 return Err(format!(
18694 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18695 xs.len()
18696 ));
18697 }
18698 if b == 0 {
18699 return Ok(());
18700 }
18701 let mut gate = vec![0.0f32; b * inter];
18702 let mut up = vec![0.0f32; b * inter];
18703 d.gate_proj.matmat(xs, b, &mut gate, pool);
18704 d.up_proj.matmat(xs, b, &mut up, pool);
18705 for row in 0..b {
18706 for i in 0..inter {
18707 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18708 let a = a as f64;
18709 mass[i] += a.abs();
18710 squared_mass[i] += a * a;
18711 }
18712 }
18713 Ok(())
18714}
18715
18716fn dense_ffn_batch(
18717 d: &DenseFfn,
18718 xs: &[f32],
18719 b: usize,
18720 pool: Option<&Pool>,
18721 mask_row: Option<&[u8]>,
18722) -> Vec<f32> {
18723 let inter = d.gate_proj.rows();
18724 let hidden = d.down_proj.rows();
18725 let fused_act = d.act.wgpu_graph_act();
18737 if mask_row.is_none()
18738 && fused_act.is_some()
18739 && b >= 32
18740 && crate::gpu::enabled_here()
18741 && !crate::gpu::mm_killed()
18742 && refit_dir().is_none()
18747 && !ffn_probe_active()
18752 && !crate::gptq_capture::capturing()
18756 {
18757 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18758 d.gate_proj.mapped_q4t(),
18759 d.up_proj.mapped_q4t(),
18760 d.down_proj.mapped_q4t(),
18761 ) {
18762 let mut out = vec![0.0f32; b * hidden];
18763 let act = fused_act.expect("checked above");
18764 if crate::gpu::q4_ffn_act(
18765 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18766 ) {
18767 return out;
18768 }
18769 }
18770 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18775 d.gate_proj.mapped_q4tp(),
18776 d.up_proj.mapped_q4tp(),
18777 d.down_proj.mapped_q4tp(),
18778 ) {
18779 let mut out = vec![0.0f32; b * hidden];
18780 let act = fused_act.expect("checked above");
18781 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18782 {
18783 return out;
18784 }
18785 }
18786 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18792 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18793 d.gate_proj.mapped_device_gemm(),
18794 d.up_proj.mapped_device_gemm(),
18795 d.down_proj.mapped_device_gemm(),
18796 ) {
18797 let mut out = vec![0.0f32; b * hidden];
18798 let act = fused_act.expect("checked above");
18799 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18800 return out;
18801 }
18802 }
18803 }
18804 let mut g = vec![0.0f32; b * inter];
18805 d.gate_proj.matmat(xs, b, &mut g, pool);
18806 let mut u = vec![0.0f32; b * inter];
18807 d.up_proj.matmat(xs, b, &mut u, pool);
18808 if gate_topk() > 0 && d.act == Act::Silu {
18809 for t in 0..b {
18810 let row = &mut g[t * inter..(t + 1) * inter];
18811 for v in row.iter_mut() {
18812 *v = Act::Silu.combine(*v, 1.0);
18813 }
18814 keep_top_k(row, gate_topk());
18815 }
18816 for i in 0..b * inter {
18817 g[i] *= u[i];
18818 }
18819 } else {
18820 for i in 0..b * inter {
18821 g[i] = d.act.combine(g[i], u[i]);
18822 }
18823 }
18824 if let Some(row) = mask_row {
18825 zero_masked_cols(&mut g, b, inter, row);
18826 }
18827 if oracle_topk() > 0 {
18828 for t in 0..b {
18829 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18830 }
18831 }
18832 let mut out = vec![0.0f32; b * hidden];
18833 d.down_proj.matmat(&g, b, &mut out, pool);
18834 if refit_dir().is_some() {
18835 let li = crate::gpu::cur_layer();
18836 if li >= 0 {
18837 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18838 }
18839 }
18840 FFN_PROBE.with(|pr| {
18844 if let Some(acc) = pr.borrow_mut().as_mut() {
18845 let li = crate::gpu::cur_layer();
18846 if li < 0 {
18847 return;
18848 }
18849 let Some(row) = acc.get_mut(li as usize) else {
18850 return;
18851 };
18852 let sq = probe_sq();
18853 for t in 0..b {
18854 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18855 *a += if sq {
18856 (v as f64) * (v as f64)
18857 } else {
18858 (v as f64).abs()
18859 };
18860 }
18861 }
18862 }
18863 });
18864 out
18865}
18866
18867fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18872 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18873 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18874 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18875 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18876 if (!on && !dump) || b == 0 {
18877 return;
18878 }
18879 let hidden = xs.len() / b;
18880 if on {
18881 let mut acc = m.act_sq.borrow_mut();
18882 if acc.len() < hidden {
18883 acc.resize(hidden, 0.0);
18884 }
18885 for t in 0..b {
18886 let row = &xs[t * hidden..(t + 1) * hidden];
18887 for (a, &v) in acc.iter_mut().zip(row) {
18888 *a += (v as f64) * (v as f64);
18889 }
18890 }
18891 }
18892 if dump {
18893 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18896 .ok()
18897 .and_then(|v| v.parse().ok())
18898 .unwrap_or(4096);
18899 let mut rows = m.act_rows.borrow_mut();
18900 if rows.len() < cap * hidden {
18901 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18902 rows.extend_from_slice(&xs[..take * hidden]);
18903 }
18904 }
18905}
18906
18907#[derive(Clone, Copy)]
18910struct SendVecs(*mut Vec<f32>);
18911unsafe impl Send for SendVecs {}
18912unsafe impl Sync for SendVecs {}
18913impl SendVecs {
18914 #[inline]
18915 fn at(self, i: usize) -> *mut Vec<f32> {
18916 unsafe { self.0.add(i) }
18917 }
18918}
18919
18920fn moe_ffn_batch(
18921 m: &MoeFfn,
18922 xs: &[f32],
18923 b: usize,
18924 hidden: usize,
18925 pool: Option<&Pool>,
18926 allowed: Option<&[bool]>,
18927) -> Vec<f32> {
18928 accumulate_act(m, xs, b);
18929 let ne = m.experts.len();
18930 let mut logits = vec![0.0f32; b * ne];
18931 match &m.resonance {
18932 Some(r) => {
18933 let hdim = xs.len() / b.max(1);
18934 for bi in 0..b {
18935 r.scores(
18936 &xs[bi * hdim..(bi + 1) * hdim],
18937 &mut logits[bi * ne..(bi + 1) * ne],
18938 );
18939 }
18940 }
18941 None => m.router.matmat(xs, b, &mut logits, pool),
18942 }
18943
18944 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18947 {
18948 let mut st = m.stats.borrow_mut();
18949 if st.len() < ne {
18950 st.resize(ne, 0);
18951 }
18952 for bi in 0..b {
18953 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18954 for &e in &idx {
18955 st[e] += 1;
18956 assign[e].push((bi, p[e] / wsum));
18957 }
18958 }
18959 }
18960
18961 let mut out = vec![0.0f32; b * hidden];
18962 let cols = m.experts[0].gate_proj.cols();
18963 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18964 let sb = list.len();
18965 let mut sub = vec![0.0f32; sb * cols];
18966 for (k, &(bi, _)) in list.iter().enumerate() {
18967 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18968 }
18969 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18970 for (k, &(bi, w)) in list.iter().enumerate() {
18971 for i in 0..hidden {
18972 out[bi * hidden + i] += w * eo[k * hidden + i];
18973 }
18974 }
18975 };
18976 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18982 if pool.is_some() && active.len() >= 8 {
18983 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18984 {
18985 let panel_ptr = SendVecs(panels.as_mut_ptr());
18986 let experts = &m.experts;
18989 let (active_r, assign_r) = (&active, &assign);
18990 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18991 let run = |start: usize, end: usize| {
18992 let _cpu_scope = inherit_cpu();
18993 #[cfg(target_os = "macos")]
19001 let _metal_cpu = crate::gpu::enter_cpu_scope();
19002 for ai in start..end {
19003 let e = active_r[ai];
19004 let list = &assign_r[e];
19005 let sb = list.len();
19006 let mut sub = vec![0.0f32; sb * cols];
19007 for (k, &(bi, _)) in list.iter().enumerate() {
19008 sub[k * cols..(k + 1) * cols]
19009 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
19010 }
19011 unsafe {
19013 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
19014 }
19015 }
19016 };
19017 match pool {
19018 Some(p) => p.run_rows(active.len(), &run),
19019 None => run(0, active.len()),
19020 }
19021 }
19022 for (ai, &e) in active.iter().enumerate() {
19023 for (k, &(bi, w)) in assign[e].iter().enumerate() {
19024 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
19025 for i in 0..hidden {
19026 out[bi * hidden + i] += w * eo[i];
19027 }
19028 }
19029 }
19030 } else {
19031 for &e in &active {
19032 run_expert(&m.experts[e], &assign[e], &mut out);
19033 }
19034 }
19035 if let Some((se, gate)) = &m.shared {
19036 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
19037 let mut gl = vec![0.0f32; b];
19038 gate.matmat(xs, b, &mut gl, pool);
19039 (0..b)
19040 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
19041 .collect()
19042 } else {
19043 (0..b).map(|bi| (bi, 1.0)).collect()
19044 };
19045 run_expert(se, &all, &mut out);
19046 }
19047 out
19048}
19049
19050fn moe_ffn_rows_exact(
19062 m: &MoeFfn,
19063 xs: &[f32],
19064 b: usize,
19065 hidden: usize,
19066 pool: Option<&Pool>,
19067) -> Vec<f32> {
19068 let mut out = vec![0.0f32; b * hidden];
19069 let per_row = |out: &mut [f32]| {
19070 for r in 0..b {
19071 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
19072 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
19073 }
19074 };
19075 let covered = !crate::gpu::enabled_here()
19076 && moe_batch_enabled()
19077 && m.shared.is_none()
19078 && m.resonance.is_none()
19079 && FFN_PROBE.with(|pr| pr.borrow().is_none())
19080 && m.experts.iter().all(|d| d.act == Act::Silu);
19081 if !covered {
19082 per_row(&mut out);
19083 return out;
19084 }
19085 let ne = m.experts.len();
19086 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
19088 for r in 0..b {
19089 let x = &xs[r * hidden..(r + 1) * hidden];
19090 accumulate_act(m, x, 1);
19091 let mut logits = vec![0.0f32; ne];
19092 m.router.matvec(x, &mut logits, pool);
19093 let (idx, p, wsum) = moe_route(&logits, m, None);
19094 {
19095 let mut st = m.stats.borrow_mut();
19096 if st.len() < ne {
19097 st.resize(ne, 0);
19098 }
19099 for &e in &idx {
19100 st[e] += 1;
19101 }
19102 }
19103 let w: Vec<f32> = idx
19104 .iter()
19105 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
19106 .collect();
19107 routes.push((idx, w));
19108 }
19109 if routes.iter().any(|(idx, _)| idx.is_empty()) {
19110 per_row(&mut out);
19111 return out;
19112 }
19113 let mut experts: Vec<usize> = Vec::new();
19115 let mut groups: Vec<Vec<usize>> = Vec::new();
19116 for (r, (idx, _)) in routes.iter().enumerate() {
19117 for &e in idx {
19118 match experts.iter().position(|&x| x == e) {
19119 Some(g) => groups[g].push(r),
19120 None => {
19121 experts.push(e);
19122 groups.push(vec![r]);
19123 }
19124 }
19125 }
19126 }
19127 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19128 let inter = m.experts[experts[0]].gate_proj.rows();
19129 let pairs: Vec<(&QTensor, &QTensor)> = experts
19130 .iter()
19131 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19132 .collect();
19133 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19134 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19135 per_row(&mut out);
19136 return out;
19137 }
19138 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19139 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19140 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19141 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19142 per_row(&mut out);
19143 return out;
19144 }
19145 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19147 let mut p = 0usize;
19148 for (g, &e) in experts.iter().enumerate() {
19149 for &r in &groups[g] {
19150 slot.insert((r, e), p);
19151 p += 1;
19152 }
19153 }
19154 for (r, (idx, w)) in routes.iter().enumerate() {
19155 let terms: Vec<(&[f32], f32)> = idx
19156 .iter()
19157 .zip(w)
19158 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19159 .collect();
19160 let row = &mut out[r * hidden..(r + 1) * hidden];
19161 for (i, dst) in row.iter_mut().enumerate() {
19162 let mut acc = 0f32;
19164 for (d, we) in &terms {
19165 acc += we * d[i];
19166 }
19167 *dst = acc;
19168 }
19169 }
19170 out
19171}
19172
19173pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
19177 match pool {
19178 Some(p) if v.len() >= 1 << 15 => {
19179 let base = v.as_mut_ptr() as usize;
19180 p.run_rows(v.len(), &|s, e| {
19181 let sl =
19184 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
19185 for x in sl {
19186 *x = c * (*x / c).tanh();
19187 }
19188 });
19189 }
19190 _ => {
19191 for x in v.iter_mut() {
19192 *x = c * (*x / c).tanh();
19193 }
19194 }
19195 }
19196}
19197
19198thread_local! {
19199 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
19203 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
19204}
19205
19206fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19208 if gate_topk() > 0
19211 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
19212 {
19213 return out;
19214 }
19215 let prism_body = d.gate_proj.has_prism_contract()
19231 || d.up_proj.has_prism_contract()
19232 || d.down_proj.has_prism_contract();
19233 if !prism_body
19234 && crate::gpu::enabled_here()
19235 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19236 {
19237 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19238 crate::gpu::ProbeArm::Gpu
19239 } else {
19240 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19241 };
19242 match arm {
19243 crate::gpu::ProbeArm::Gpu => {
19244 let t0 = std::time::Instant::now();
19245 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19246 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19247 return out;
19248 }
19249 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19253 }
19254 crate::gpu::ProbeArm::CpuTimed => {
19255 let t0 = std::time::Instant::now();
19256 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19257 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19258 return out;
19259 }
19260 crate::gpu::ProbeArm::Cpu => {
19261 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19262 }
19263 }
19264 }
19265 dense_ffn_cpu(d, x, pool)
19266}
19267
19268fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19270 let inter = d.gate_proj.rows();
19271 FFN_SCRATCH.with(|s| {
19272 let mut s = s.borrow_mut();
19273 let [g, u, ..] = &mut *s;
19274 g.resize(inter, 0.0);
19275 if gate_topk() > 0 {
19278 u.resize(inter, 0.0);
19282 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19283 for i in 0..inter {
19284 g[i] = Act::Silu.combine(g[i], 1.0);
19285 }
19286 keep_top_k(g, gate_topk());
19287 for i in 0..inter {
19288 g[i] *= u[i];
19289 }
19290 } else if d.act == Act::Silu && {
19291 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19292 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19293 } {
19294 } else {
19296 u.resize(inter, 0.0);
19297 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19299 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19300 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19301 for i in 0..inter {
19302 g[i] = d.act.combine(g[i], u[i]);
19303 }
19304 }
19305 FFN_PROBE.with(|pr| {
19313 if let Some(acc) = pr.borrow_mut().as_mut() {
19314 let li = crate::gpu::cur_layer();
19315 if li >= 0 {
19316 if let Some(row) = acc.get_mut(li as usize) {
19317 match probe_topk() {
19318 0 if probe_sq() => {
19319 for (a, &v) in row.iter_mut().zip(g.iter()) {
19320 *a += (v as f64) * (v as f64);
19321 }
19322 }
19323 0 if probe_signed() => {
19324 for (a, &v) in row.iter_mut().zip(g.iter()) {
19325 *a += v as f64;
19326 }
19327 }
19328 0 => {
19329 for (a, &v) in row.iter_mut().zip(g.iter()) {
19330 *a += (v as f64).abs();
19331 }
19332 }
19333 k => {
19334 let n = g.len();
19335 let k = k.min(n);
19336 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19337 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19338 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19339 });
19340 let thr = *kth;
19341 for (a, &v) in row.iter_mut().zip(g.iter()) {
19342 if v.abs() >= thr {
19343 *a += 1.0;
19344 }
19345 }
19346 }
19347 }
19348 }
19349 }
19350 }
19351 });
19352 if oracle_topk() > 0 {
19353 keep_top_k(g, oracle_topk());
19354 }
19355 {
19356 let li = crate::gpu::cur_layer();
19357 if li >= 0 {
19358 adump_row(li as usize, g);
19359 }
19360 }
19361 let mut out = attention::take_buf(d.down_proj.rows());
19362 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19363 d.down_proj.matvec(g, &mut out, pool);
19364 out
19365 })
19366}
19367
19368pub struct RefitAcc {
19381 pub support: Vec<u32>,
19382 pub gss: Vec<f32>,
19383 pub ya: Vec<f32>,
19384 pub hidden: usize,
19385 pub tokens: u64,
19386 pub buf_g: Vec<f32>,
19392 pub buf_o: Vec<f32>,
19393 pub buf_t: usize,
19394}
19395
19396type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19400
19401static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19402 std::sync::OnceLock::new();
19403
19404fn ffn_probe_active() -> bool {
19407 FFN_PROBE.with(|p| p.borrow().is_some())
19408}
19409
19410fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19411 REFIT
19412 .get_or_init(|| {
19413 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19414 (
19415 d,
19416 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19417 )
19418 })
19419 })
19420 .as_ref()
19421}
19422
19423fn refit_accumulate(
19425 li: usize,
19426 g: &[f32],
19427 b: usize,
19428 inter: usize,
19429 out: &[f32],
19430 hidden: usize,
19431 pool: Option<&Pool>,
19432) {
19433 let Some((dir, map)) = refit_dir() else {
19434 return;
19435 };
19436 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19437 let (from, to) = *SPAN.get_or_init(|| {
19438 let g = |k: &str, d: usize| {
19439 std::env::var(k)
19440 .ok()
19441 .and_then(|v| v.parse().ok())
19442 .unwrap_or(d)
19443 };
19444 (
19445 g("CMF_FFN_REFIT_FROM", 0),
19446 g("CMF_FFN_REFIT_TO", usize::MAX),
19447 )
19448 });
19449 if li < from || li > to {
19450 return;
19451 }
19452 let mut guard = map.lock().unwrap();
19453 let (map, shared) = &mut *guard;
19454 let acc = match map.entry(li) {
19455 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19456 std::collections::hash_map::Entry::Vacant(e) => {
19457 let path = format!("{dir}/support.{li}.u32");
19458 let Ok(bytes) = std::fs::read(&path) else {
19459 eprintln!("refit: no {path} — layer {li} skipped");
19460 return;
19461 };
19462 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19463 let support: Vec<u32> = bytes[4..4 + n * 4]
19464 .chunks_exact(4)
19465 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19466 .collect();
19467 eprintln!(
19468 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19469 (n * n + hidden * n) as f64 * 4.0 / 1e6
19470 );
19471 e.insert(RefitAcc {
19472 gss: vec![0.0; n * n],
19473 ya: vec![0.0; hidden * n],
19474 buf_g: Vec::new(),
19475 buf_o: Vec::new(),
19476 buf_t: 0,
19477 support,
19478 hidden,
19479 tokens: 0,
19480 })
19481 }
19482 };
19483 let ns = acc.support.len();
19484 let cap = refit_batch();
19486 if acc.buf_g.is_empty() {
19487 acc.buf_g = vec![0.0; ns * cap];
19488 acc.buf_o = vec![0.0; hidden * cap];
19489 }
19490 let take = b.min(cap - acc.buf_t);
19491 for t in 0..take {
19492 let col = acc.buf_t + t;
19493 for (j, &n) in acc.support.iter().enumerate() {
19494 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19495 }
19496 for h in 0..hidden {
19497 acc.buf_o[h * cap + col] = out[t * hidden + h];
19498 }
19499 }
19500 acc.buf_t += take;
19501 acc.tokens += take as u64;
19502 if acc.buf_t < cap {
19503 return;
19504 }
19505 let bt = acc.buf_t;
19506 acc.buf_t = 0;
19507 let RefitAcc {
19517 gss,
19518 ya,
19519 buf_g,
19520 buf_o,
19521 ..
19522 } = acc;
19523 let need = (ns * ns).max(hidden * ns);
19524 if shared.len() < need {
19525 shared.resize(need, 0.0);
19526 }
19527 let scratch = &mut shared[..];
19528 let _ = bt;
19529 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19530 add_into(gss, &scratch[..ns * ns], pool);
19531 if crate::gpu::gemm_nt_f32_transient(
19532 buf_o,
19533 buf_g,
19534 &mut scratch[..hidden * ns],
19535 hidden,
19536 cap,
19537 ns,
19538 ) {
19539 add_into(ya, &scratch[..hidden * ns], pool);
19540 } else {
19541 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19542 }
19543 } else {
19544 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19545 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19546 }
19547 }
19551
19552fn refit_batch() -> usize {
19554 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19555 *B.get_or_init(|| {
19556 std::env::var("CMF_FFN_REFIT_BATCH")
19557 .ok()
19558 .and_then(|v| v.parse().ok())
19559 .unwrap_or(4096)
19560 })
19561}
19562
19563fn accum_outer_t(
19566 c: &mut [f32],
19567 m: usize,
19568 n: usize,
19569 b: usize,
19570 left: &[f32],
19571 right: &[f32],
19572 pool: Option<&Pool>,
19573) {
19574 let ptr = SendMut(c.as_mut_ptr());
19575 let body = |i: usize| {
19576 let ptr = &ptr;
19577 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19578 for t in 0..b {
19579 let a = left[i * b + t];
19580 if a == 0.0 {
19581 continue;
19582 }
19583 for (j, o) in row.iter_mut().enumerate() {
19584 *o += a * right[j * b + t];
19585 }
19586 }
19587 };
19588 match pool {
19589 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19590 for i in s..e {
19591 body(i);
19592 }
19593 }),
19594 _ => {
19595 for i in 0..m {
19596 body(i);
19597 }
19598 }
19599 }
19600}
19601
19602fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19605 let n = dst.len().min(src.len());
19606 match pool {
19607 Some(p) if n >= 1 << 16 => {
19608 let ptr = SendMut(dst.as_mut_ptr());
19609 let f = |s: usize, e: usize| {
19610 let ptr = &ptr;
19611 for blk in s..e {
19612 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19613 for i in a..b {
19614 unsafe { *ptr.0.add(i) += src[i] };
19615 }
19616 }
19617 };
19618 p.run_rows(n.div_ceil(4096), &f);
19619 }
19620 _ => {
19621 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19622 *d += *v;
19623 }
19624 }
19625 }
19626}
19627
19628fn accum_outer(
19633 c: &mut [f32],
19634 m: usize,
19635 n: usize,
19636 b: usize,
19637 left: &[f32],
19638 right: &[f32],
19639 pool: Option<&Pool>,
19640) {
19641 const TILE: usize = 32;
19642 let tiles = m.div_ceil(TILE);
19643 let cp = SendMut(c.as_mut_ptr());
19644 let body = |ti: usize| {
19645 let cp = &cp;
19646 let i0 = ti * TILE;
19647 let i1 = (i0 + TILE).min(m);
19648 for t in 0..b {
19649 let r = &right[t * n..t * n + n];
19650 for i in i0..i1 {
19651 let a = left[i * b + t];
19652 if a == 0.0 {
19653 continue;
19654 }
19655 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19657 for (o, v) in row.iter_mut().zip(r) {
19658 *o += a * *v;
19659 }
19660 }
19661 }
19662 };
19663 match pool {
19664 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19665 for ti in s..e {
19666 body(ti);
19667 }
19668 }),
19669 _ => {
19670 for ti in 0..tiles {
19671 body(ti);
19672 }
19673 }
19674 }
19675}
19676
19677pub fn refit_flush() -> usize {
19679 let Some((dir, map)) = refit_dir() else {
19680 return 0;
19681 };
19682 let guard = map.lock().unwrap();
19683 let mut n = 0;
19684 for (li, acc) in guard.0.iter() {
19685 let w = |name: &str, v: &[f32]| {
19688 let path = format!("{dir}/{name}.{li}.f32");
19689 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19690 match std::fs::write(&path, &bytes) {
19691 Ok(()) => {}
19692 Err(e) => eprintln!(
19693 "refit: FAILED to write {path} ({} MB): {e}",
19694 bytes.len() / 1_000_000
19695 ),
19696 }
19697 };
19698 w("gss", &acc.gss);
19699 w("ya", &acc.ya);
19700 println!(
19701 "refit L{li}: {} support, {} tokens, hidden {}",
19702 acc.support.len(),
19703 acc.tokens,
19704 acc.hidden
19705 );
19706 n += 1;
19707 }
19708 n
19709}
19710
19711fn adump_row(li: usize, g: &[f32]) {
19716 use std::io::Write as _;
19717 static FILES: std::sync::OnceLock<
19718 Option<(
19719 String,
19720 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19721 )>,
19722 > = std::sync::OnceLock::new();
19723 let Some((prefix, map)) = FILES
19724 .get_or_init(|| {
19725 std::env::var("CMF_FFN_ADUMP")
19726 .ok()
19727 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19728 })
19729 .as_ref()
19730 else {
19731 return;
19732 };
19733 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19736 let (from, to) = *SPAN.get_or_init(|| {
19737 let g = |k: &str, d: usize| {
19738 std::env::var(k)
19739 .ok()
19740 .and_then(|v| v.parse().ok())
19741 .unwrap_or(d)
19742 };
19743 (
19744 g("CMF_FFN_ADUMP_FROM", 0),
19745 g("CMF_FFN_ADUMP_TO", usize::MAX),
19746 )
19747 });
19748 if li < from || li > to {
19749 return;
19750 }
19751 let mut map = map.lock().unwrap();
19752 let f = map.entry(li).or_insert_with(|| {
19753 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19754 });
19755 let mut bytes = Vec::with_capacity(g.len() * 2);
19756 for v in g {
19757 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19758 }
19759 let _ = f.write_all(&bytes);
19760}
19761
19762fn oracle_topk() -> usize {
19768 if exact_mtp_calibration_active() {
19769 return 0;
19770 }
19771 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19772 *K.get_or_init(|| {
19773 std::env::var("CMF_FFN_ORACLE_TOPK")
19774 .ok()
19775 .and_then(|v| v.parse().ok())
19776 .unwrap_or(0)
19777 })
19778}
19779
19780fn gate_topk() -> usize {
19786 if exact_mtp_calibration_active() {
19787 return 0;
19788 }
19789 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19790 *K.get_or_init(|| {
19791 std::env::var("CMF_FFN_GATE_TOPK")
19792 .ok()
19793 .and_then(|v| v.parse().ok())
19794 .unwrap_or(0)
19795 })
19796}
19797
19798fn gate_block() -> usize {
19805 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19806 *B.get_or_init(|| {
19807 std::env::var("CMF_FFN_GATE_BLOCK")
19808 .ok()
19809 .and_then(|v| v.parse().ok())
19810 .unwrap_or(1)
19811 })
19812}
19813
19814fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19816 let n = g.len();
19817 let nb = n.div_ceil(block);
19818 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19819 if kb >= nb {
19820 return;
19821 }
19822 let mut score: Vec<f32> = (0..nb)
19823 .map(|b| {
19824 g[b * block..((b + 1) * block).min(n)]
19825 .iter()
19826 .map(|v| v * v)
19827 .sum::<f32>()
19828 })
19829 .collect();
19830 let mut ord = score.clone();
19831 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19832 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19833 });
19834 let thr = *kth;
19835 for b in 0..nb {
19836 if score[b] < thr {
19837 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19838 }
19839 }
19840 score.clear();
19841}
19842
19843fn keep_top_k(g: &mut [f32], k: usize) {
19845 if gate_block() > 1 {
19846 return keep_top_blocks(g, k, gate_block());
19847 }
19848 let n = g.len();
19849 if k == 0 || k >= n {
19850 return;
19851 }
19852 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19853 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19854 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19855 });
19856 let thr = *kth;
19857 for v in g.iter_mut() {
19858 if v.abs() < thr {
19859 *v = 0.0;
19860 }
19861 }
19862}
19863
19864fn probe_sq() -> bool {
19868 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19869 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19870}
19871
19872fn probe_signed() -> bool {
19876 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19877 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19878}
19879
19880fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19888 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19889 M.get_or_init(|| {
19890 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19891 let b = std::fs::read(&p).ok()?;
19892 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19893 let vals: Vec<f32> = b[8..]
19894 .chunks_exact(4)
19895 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19896 .collect();
19897 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19898 Some((inter, vals))
19899 })
19900 .as_ref()
19901}
19902
19903fn probe_topk() -> usize {
19906 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19907 *K.get_or_init(|| {
19908 std::env::var("CMF_FFN_PROBE_TOPK")
19909 .ok()
19910 .and_then(|v| v.parse().ok())
19911 .unwrap_or(0)
19912 })
19913}
19914
19915thread_local! {
19916 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19919 const { std::cell::RefCell::new(None) };
19920}
19921
19922fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19935 if d.gate_proj.has_prism_contract()
19940 || d.up_proj.has_prism_contract()
19941 || d.down_proj.has_prism_contract()
19942 {
19943 return None;
19944 }
19945 let dt = d.down_t.as_ref()?;
19946 let inter = d.gate_proj.rows();
19947 let hidden = dt.cols();
19948 if k == 0 || k >= inter || d.act != Act::Silu {
19949 return None;
19950 }
19951 DYN_SCRATCH.with(|sc| {
19952 let mut sc = sc.borrow_mut();
19953 let DynScratch {
19954 g,
19955 mag,
19956 live,
19957 parts,
19958 } = &mut *sc;
19959 g.resize(inter, 0.0);
19960 d.gate_proj.matvec(x, g, pool);
19961 for v in g.iter_mut() {
19962 *v = inference::silu(*v);
19963 }
19964 mag.clear();
19967 mag.extend(g.iter().map(|v| v.abs()));
19968 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19969 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19970 });
19971 let thr = *kth;
19972 live.clear();
19973 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19974 let mut out = vec![0.0f32; hidden];
19975 match pool {
19976 Some(p) if live.len() >= 64 => {
19977 let nw = p.n_workers() + 1;
19978 parts.clear();
19979 parts.resize(nw * hidden, 0.0);
19980 let ptr = SendMut(parts.as_mut_ptr());
19981 let n = live.len();
19982 let live_ref: &[u32] = live;
19983 let g_ref: &[f32] = g;
19984 p.run(&|w, workers| {
19985 let chunk = n.div_ceil(workers);
19986 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19987 if s >= e {
19988 return;
19989 }
19990 WORKER_SCRATCH.with(|ws| {
19991 let mut ws = ws.borrow_mut();
19992 let [scratch, acc] = &mut *ws;
19993 scratch.resize(hidden.max(x.len()), 0.0);
19994 acc.clear();
19995 acc.resize(hidden, 0.0);
19996 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19997 if let Some(&nx) = live_ref[s..e].get(o + 1) {
20000 d.up_proj.prefetch_row(nx as usize);
20001 dt.prefetch_row(nx as usize);
20002 }
20003 let idx = nrm as usize;
20004 let up = d.up_proj.row_dot(idx, x, scratch);
20005 let a = g_ref[idx] * up;
20006 if a != 0.0 {
20007 dt.add_row_scaled(idx, a, acc, scratch);
20008 }
20009 }
20010 for (j, v) in acc.iter().enumerate() {
20011 unsafe { *ptr.at(w * hidden + j) = *v };
20012 }
20013 });
20014 });
20015 for w in 0..nw {
20016 for (j, o) in out.iter_mut().enumerate() {
20017 *o += parts[w * hidden + j];
20018 }
20019 }
20020 }
20021 _ => {
20022 WORKER_SCRATCH.with(|ws| {
20023 let mut ws = ws.borrow_mut();
20024 let [scratch, _acc] = &mut *ws;
20025 scratch.resize(hidden.max(x.len()), 0.0);
20026 for &nrm in live.iter() {
20027 let idx = nrm as usize;
20028 let up = d.up_proj.row_dot(idx, x, scratch);
20029 let a = g[idx] * up;
20030 if a != 0.0 {
20031 dt.add_row_scaled(idx, a, &mut out, scratch);
20032 }
20033 }
20034 });
20035 }
20036 }
20037 Some(out)
20038 })
20039}
20040
20041struct DynScratch {
20044 g: Vec<f32>,
20045 mag: Vec<f32>,
20046 live: Vec<u32>,
20047 parts: Vec<f32>,
20048}
20049
20050thread_local! {
20051 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
20052 std::cell::RefCell::new(DynScratch {
20053 g: Vec::new(),
20054 mag: Vec::new(),
20055 live: Vec::new(),
20056 parts: Vec::new(),
20057 })
20058 };
20059 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
20061 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
20062}
20063
20064fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
20069 let inter = d.gate_proj.rows();
20070 FFN_SCRATCH.with(|s| {
20071 let mut s = s.borrow_mut();
20072 let [g, u, ..] = &mut *s;
20073 g.resize(inter, 0.0);
20074 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
20075 } else {
20077 u.resize(inter, 0.0);
20078 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
20079 for i in 0..inter {
20080 g[i] = d.act.combine(g[i], u[i]);
20081 }
20082 }
20083 zero_masked_cols(g, 1, inter, mask_row);
20084 let mut out = attention::take_buf(d.down_proj.rows());
20085 d.down_proj.matvec(g, &mut out, pool);
20086 out
20087 })
20088}
20089
20090fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
20096 if d.gate_proj.has_prism_contract()
20097 || d.up_proj.has_prism_contract()
20098 || d.down_proj.has_prism_contract()
20099 {
20100 return None;
20101 }
20102 d.act.moe_job_graph_contract()?;
20105 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
20108 return None;
20109 }
20110 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
20111 let mut model_ref = None;
20112 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
20113 let model = model_ref?;
20114 let hidden = jobs[0].down.1;
20115 let mut out = attention::take_buf(hidden);
20116 if crate::gpu::moe_block(&model, &jobs, &mut out) {
20117 Some(out)
20118 } else {
20119 let mut out = out;
20120 attention::recycle_buf(&mut out);
20121 None
20122 }
20123}
20124
20125#[allow(clippy::type_complexity)]
20130#[allow(clippy::type_complexity)]
20131pub(crate) fn moe_parts(
20132 t: &QTensor,
20133) -> Option<(
20134 &std::sync::Arc<cortiq_core::CmfModel>,
20135 usize,
20136 usize,
20137 usize,
20138 &[f32],
20139 &[f32],
20140 bool,
20141 bool,
20142 bool,
20143)> {
20144 match t {
20145 QTensor::Mapped {
20146 model,
20147 idx,
20148 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20149 rows,
20150 cols,
20151 row_scale,
20152 col_field,
20153 ..
20154 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20155 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20156 )),
20157 QTensor::Mapped {
20159 model,
20160 idx,
20161 dtype: cortiq_core::TensorDtype::Q1,
20162 rows,
20163 cols,
20164 ..
20165 } => Some((
20166 model,
20167 *idx,
20168 *rows,
20169 *cols,
20170 &[][..],
20171 &[][..],
20172 true,
20173 false,
20174 false,
20175 )),
20176 QTensor::Mapped {
20178 model,
20179 idx,
20180 dtype: cortiq_core::TensorDtype::Q4Tiled,
20181 rows,
20182 cols,
20183 ..
20184 } => Some((
20185 model,
20186 *idx,
20187 *rows,
20188 *cols,
20189 &[][..],
20190 &[][..],
20191 false,
20192 true,
20193 false,
20194 )),
20195 QTensor::Mapped {
20197 model,
20198 idx,
20199 dtype: cortiq_core::TensorDtype::Q4TiledP,
20200 rows,
20201 cols,
20202 ..
20203 } => Some((
20204 model,
20205 *idx,
20206 *rows,
20207 *cols,
20208 &[][..],
20209 &[][..],
20210 false,
20211 true,
20212 false,
20213 )),
20214 QTensor::Mapped {
20218 model,
20219 idx,
20220 dtype: cortiq_core::TensorDtype::Q2TiledP,
20221 rows,
20222 cols,
20223 ..
20224 } => Some((
20225 model,
20226 *idx,
20227 *rows,
20228 *cols,
20229 &[][..],
20230 &[][..],
20231 false,
20232 true,
20233 true,
20234 )),
20235 _ => None,
20236 }
20237}
20238
20239#[cfg(target_os = "macos")]
20246#[allow(clippy::type_complexity)]
20247fn chunk_moe_parts(
20248 m: &MoeFfn,
20249 hidden: usize,
20250) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20251 if m.shared.is_some()
20252 || m.per_expert_scale.is_some()
20253 || m.resonance.is_some()
20254 || m.route_tau.is_some()
20255 || m.router_input_norm
20256 || m.experts.is_empty()
20257 || m.top_k == 0
20258 || std::env::var("CMF_RMS_TRACE").is_ok()
20259 || std::env::var("CMF_ACT_DUMP").is_ok()
20260 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20261 {
20262 return None;
20263 }
20264 let (rf, rr, rc) = m.router.f32_parts()?;
20265 if rr != m.experts.len() || rc != hidden {
20266 return None;
20267 }
20268 let inter = m.experts[0].gate_proj.rows();
20269 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20272 let experts = m
20273 .experts
20274 .iter()
20275 .map(|e| {
20276 if e.act != Act::Silu
20277 || !e.segs.is_empty()
20278 || e.gate_proj.rows() != inter
20279 || e.up_proj.rows() != inter
20280 || e.down_proj.cols() != inter
20281 || e.gate_proj.cols() != hidden
20282 || e.down_proj.rows() != hidden
20283 {
20284 return None;
20285 }
20286 if q8 {
20287 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20288 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20289 }
20290 Some((
20291 e.gate_proj.mapped_q4tp()?.1,
20292 e.up_proj.mapped_q4tp()?.1,
20293 e.down_proj.mapped_q4tp()?.1,
20294 ))
20295 })
20296 .collect::<Option<Vec<_>>>()?;
20297 Some((rf, experts, q8))
20298}
20299
20300#[cfg(target_os = "macos")]
20313fn metal_flint_moe_graph_parts(
20314 m: &MoeFfn,
20315 hidden: usize,
20316 scratch_key: usize,
20317) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20318 use std::sync::Arc;
20319
20320 const HIDDEN: usize = 2_048;
20321 const ROUTED_INTER: usize = 512;
20322 const SHARED_INTER: usize = 1_024;
20323 const EXPERTS: usize = 256;
20324 const TOP_K: usize = 6;
20325
20326 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20327 return None;
20328 }
20329 let routed0 = m.experts.first()?;
20330 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20331 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20332 if router_rows != EXPERTS || router_cols != HIDDEN {
20333 return None;
20334 }
20335 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20336 return None;
20337 }
20338 let same_q2 = |t: &QTensor| -> Option<usize> {
20339 let (owner, idx) = t.mapped_q2tp()?;
20340 Arc::ptr_eq(&owner, &model).then_some(idx)
20341 };
20342 let same_q4 = |t: &QTensor| -> Option<usize> {
20343 let (owner, idx) = t.mapped_q4tp()?;
20344 Arc::ptr_eq(&owner, &model).then_some(idx)
20345 };
20346 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20347 if e.act != Act::Silu
20348 || !e.segs.is_empty()
20349 || e.gate_proj.rows() != ROUTED_INTER
20350 || e.gate_proj.cols() != HIDDEN
20351 || e.up_proj.rows() != ROUTED_INTER
20352 || e.up_proj.cols() != HIDDEN
20353 || e.down_proj.rows() != HIDDEN
20354 || e.down_proj.cols() != ROUTED_INTER
20355 {
20356 return None;
20357 }
20358 Some((
20359 same_q2(&e.gate_proj)?,
20360 same_q2(&e.up_proj)?,
20361 same_q4(&e.down_proj)?,
20362 ))
20363 };
20364 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20365 if e.act != Act::Silu
20366 || !e.segs.is_empty()
20367 || e.gate_proj.rows() != SHARED_INTER
20368 || e.gate_proj.cols() != HIDDEN
20369 || e.up_proj.rows() != SHARED_INTER
20370 || e.up_proj.cols() != HIDDEN
20371 || e.down_proj.rows() != HIDDEN
20372 || e.down_proj.cols() != SHARED_INTER
20373 {
20374 return None;
20375 }
20376 Some((
20377 same_q4(&e.gate_proj)?,
20378 same_q4(&e.up_proj)?,
20379 same_q4(&e.down_proj)?,
20380 ))
20381 };
20382 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20383 if experts.len() != EXPERTS {
20384 return None;
20385 }
20386 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20387 if shared_gate.is_some() {
20388 return None;
20389 }
20390 Some(crate::gpu::GpuFlintMoe {
20391 router,
20392 bias: m.expert_bias.as_deref(),
20393 experts,
20394 scratch_key,
20395 shared: shared(shared_ffn)?,
20396 top_k: m.top_k,
20397 norm_topk: m.norm_topk_prob,
20398 route_scale: m.routed_scaling,
20399 sigmoid: m.router_sigmoid,
20400 sigmoid_denom_eps: m.sigmoid_denom_eps,
20401 })
20402}
20403
20404#[cfg(target_os = "macos")]
20405fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20406 if m.router_input_norm
20407 || m.route_tau.is_some()
20408 || m.mask.is_some()
20409 || m.per_expert_scale.is_some()
20410 || m.experts.is_empty()
20411 || m.top_k == 0
20412 || m.resonance.is_some()
20413 {
20414 return None;
20415 }
20416 let (sh, sg) = match &m.shared {
20421 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20422 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20423 None => return None,
20424 };
20425 let (rf, rr, rc) = m.router.f32_parts()?;
20426 if rr != m.experts.len() || rc != hidden {
20427 return None;
20428 }
20429 let shared_gated = sg.is_some();
20430 let sf = match sg {
20431 Some(sg) => {
20432 let (sf, sr, sc) = sg.f32_parts()?;
20433 if sr * sc != hidden {
20434 return None;
20435 }
20436 sf
20437 }
20438 None => &rf[..hidden],
20441 };
20442 if let Some(b) = &m.expert_bias {
20443 if b.len() != m.experts.len() {
20444 return None;
20445 }
20446 }
20447 let inter = m.experts[0].gate_proj.rows();
20448 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20451 let q8 = crate::gpu_metal::moe_q8::enabled()
20455 && m.shared.is_none()
20456 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20457 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20458 if e.act != Act::Silu
20459 || e.gate_proj.rows() != inter
20460 || e.gate_proj.cols() != hidden
20461 || e.up_proj.rows() != inter
20462 || e.up_proj.cols() != hidden
20463 || e.down_proj.rows() != hidden
20464 || e.down_proj.cols() != inter
20465 {
20466 return None;
20467 }
20468 if q8 {
20469 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20470 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20471 }
20472 let pick = |t: &QTensor| -> Option<usize> {
20473 if gu_q2 {
20474 t.mapped_q2tp().map(|(_, i)| i)
20475 } else {
20476 t.mapped_q4tp().map(|(_, i)| i)
20477 }
20478 };
20479 Some((
20480 pick(&e.gate_proj)?,
20481 pick(&e.up_proj)?,
20482 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20483 ))
20484 };
20485 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20486 let shared = match sh {
20487 Some(sh) => Some(trio(sh)?),
20488 None => None,
20489 };
20490 Some(crate::gpu::GpuMoe {
20491 router: rf,
20492 sgate: sf,
20493 experts,
20494 shared,
20495 n_exp: m.experts.len(),
20496 top_k: m.top_k,
20497 inter,
20498 norm_topk: m.norm_topk_prob,
20499 route_scale: m.routed_scaling,
20500 gu_q2,
20501 q8,
20502 sigmoid: m.router_sigmoid,
20503 sigmoid_denom_eps: m.sigmoid_denom_eps,
20504 bias: m.expert_bias.as_deref(),
20505 shared_gated,
20506 })
20507}
20508
20509fn forced_ids_at(step: usize) -> Option<u32> {
20515 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20516 IDS.get_or_init(|| {
20517 std::env::var("CMF_FORCE_IDS")
20518 .ok()
20519 .and_then(|p| std::fs::read_to_string(p).ok())
20520 .map(|s| {
20521 s.split_whitespace()
20522 .filter_map(|t| t.parse().ok())
20523 .collect()
20524 })
20525 .unwrap_or_default()
20526 })
20527 .get(step)
20528 .copied()
20529}
20530
20531pub(crate) fn moe_push_job_parts<'a>(
20535 gate: &'a QTensor,
20536 up: &'a QTensor,
20537 down: &'a QTensor,
20538 x: &[f32],
20539 w: f32,
20540 swiglu_limit: f32,
20541 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20542 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20543) -> Option<()> {
20544 use crate::qtensor::prescale;
20545 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20546 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20547 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20548 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20549 return None; }
20551 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20554 return None;
20555 }
20556 if !gq2 && dq2 {
20557 return None;
20558 }
20559 model_ref.get_or_insert_with(|| gm.clone());
20560 let dt = |cf: &[f32]| {
20561 if cf.is_empty() {
20562 cortiq_core::TensorDtype::Q8Row
20563 } else {
20564 cortiq_core::TensorDtype::Q8_2f
20565 }
20566 };
20567 jobs.push(crate::gpu::MoeJob {
20568 gate: (gi, gr, gc, grs),
20569 up: (ui, ur, uc, urs),
20570 down: (di, dr, dc, drs),
20571 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20572 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20573 down_col: dcf,
20574 w,
20575 q1: gq1,
20576 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20577 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20578 gu_q2: gq2,
20579 swiglu_limit,
20580 act: 0,
20581 });
20582 Some(())
20583}
20584
20585fn moe_push_job<'a>(
20587 d: &'a DenseFfn,
20588 x: &[f32],
20589 w: f32,
20590 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20591 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20592) -> Option<()> {
20593 use crate::qtensor::prescale;
20594 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20599 let act = graph_act.code();
20600 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20601 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20602 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20603 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20604 return None; }
20606 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20607 return None;
20608 }
20609 if !gq2 && dq2 {
20610 return None;
20611 }
20612 model_ref.get_or_insert_with(|| gm.clone());
20613 let gdt = if gcf.is_empty() {
20614 cortiq_core::TensorDtype::Q8Row
20615 } else {
20616 cortiq_core::TensorDtype::Q8_2f
20617 };
20618 let udt = if ucf.is_empty() {
20619 cortiq_core::TensorDtype::Q8Row
20620 } else {
20621 cortiq_core::TensorDtype::Q8_2f
20622 };
20623 jobs.push(crate::gpu::MoeJob {
20624 gate: (gi, gr, gc, grs),
20625 up: (ui, ur, uc, urs),
20626 down: (di, dr, dc, drs),
20627 xs_gate: prescale(x, gcf, gdt).into_owned(),
20628 xs_up: prescale(x, ucf, udt).into_owned(),
20629 down_col: dcf,
20630 w,
20631 q1: gq1,
20632 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20633 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20634 gu_q2: gq2,
20635 swiglu_limit,
20636 act,
20637 });
20638 Some(())
20639}
20640
20641fn sparse_ffn_quant(
20648 d: &DenseFfn,
20649 x: &[f32],
20650 active: &[u16],
20651 hidden: usize,
20652 pool: Option<&Pool>,
20653) -> Vec<f32> {
20654 let n = active.len();
20655 let inter = d.gate_proj.rows();
20656 let mut act = vec![0.0f32; n];
20657 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20660 let compute = |ai: usize| -> f32 {
20661 let idx = active[ai] as usize;
20662 if idx >= inter {
20663 return 0.0; }
20665 let mut s = if need_scratch {
20666 vec![0.0f32; hidden]
20667 } else {
20668 Vec::new()
20669 };
20670 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20671 let up = d.up_proj.row_dot(idx, x, &mut s);
20672 d.act.combine(gate, up)
20673 };
20674 match pool {
20675 Some(p) if n >= 256 => {
20676 let ptr = SendMut(act.as_mut_ptr());
20677 p.run(&|widx, nw| {
20678 let chunk = n.div_ceil(nw);
20679 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20680 for ai in s..e {
20681 unsafe { *ptr.at(ai) = compute(ai) };
20682 }
20683 });
20684 }
20685 _ => {
20686 for (ai, a) in act.iter_mut().enumerate() {
20687 *a = compute(ai);
20688 }
20689 }
20690 }
20691 let mut out = vec![0.0f32; hidden];
20693 for (ai, &idx) in active.iter().enumerate() {
20694 let w = act[ai];
20695 if w.abs() >= 1e-12 && (idx as usize) < inter {
20696 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20697 }
20698 }
20699 out
20700}
20701
20702#[doc(hidden)]
20704pub fn sparse_ffn_quant_for_test(
20705 d: &DenseFfn,
20706 x: &[f32],
20707 active: &[u16],
20708 hidden: usize,
20709) -> Vec<f32> {
20710 sparse_ffn_quant(d, x, active, hidden, None)
20711}
20712
20713fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20717 let deq = |t: &QTensor| -> Vec<f32> {
20718 let (rows, cols) = (t.rows(), t.cols());
20719 let mut out = vec![0.0f32; rows * cols];
20720 for r in 0..rows {
20721 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20722 }
20723 out
20724 };
20725 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20726}
20727
20728struct SendMut(*mut f32);
20730unsafe impl Send for SendMut {}
20731unsafe impl Sync for SendMut {}
20732impl SendMut {
20733 #[inline]
20734 #[allow(clippy::mut_from_ref)]
20737 unsafe fn at(&self, i: usize) -> &mut f32 {
20738 unsafe { &mut *self.0.add(i) }
20739 }
20740}
20741
20742pub(crate) fn moe_route(
20754 logits: &[f32],
20755 m: &MoeFfn,
20756 allowed: Option<&[bool]>,
20757) -> (Vec<usize>, Vec<f32>, f32) {
20758 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20759}
20760
20761pub(crate) fn moe_route_with_eps(
20771 logits: &[f32],
20772 m: &MoeFfn,
20773 allowed: Option<&[bool]>,
20774 sigmoid_denom_eps: f32,
20775) -> (Vec<usize>, Vec<f32>, f32) {
20776 let ne = logits.len();
20777 let admit = |e: usize| {
20783 m.mask.as_ref().is_none_or(|mk| mk[e])
20784 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20785 };
20786 if m.resonance.is_some() && m.top_k == 1 {
20799 let mut best: Option<usize> = None;
20800 for e in (0..ne).filter(|&e| admit(e)) {
20801 let l = logits[e];
20802 if l == f32::NEG_INFINITY || l.is_nan() {
20803 continue;
20804 }
20805 if best.is_none_or(|b| l > logits[b]) {
20806 best = Some(e);
20807 }
20808 }
20809 if let Some(b) = best {
20810 let mut p = vec![0.0f32; ne];
20811 p[b] = 1.0;
20812 return (vec![b], p, 1.0 / m.routed_scaling);
20813 }
20814 }
20815 let p: Vec<f32> = if m.router_sigmoid {
20821 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20822 } else {
20823 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20824 if mx == f32::NEG_INFINITY {
20825 vec![1.0 / ne.max(1) as f32; ne]
20826 } else {
20827 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20828 let s: f32 = e.iter().sum();
20829 for v in &mut e {
20830 *v /= s;
20831 }
20832 e
20833 }
20834 };
20835 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20836 match &m.expert_bias {
20838 Some(b) => idx.sort_unstable_by(|&x, &y| {
20839 (p[y] + b[y])
20840 .partial_cmp(&(p[x] + b[x]))
20841 .unwrap()
20842 .then(x.cmp(&y))
20843 }),
20844 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20845 }
20846 idx.truncate(m.top_k);
20847 if let Some(tau) = m.route_tau {
20851 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20852 if total > 0.0 {
20853 let mut acc = 0.0f32;
20854 let mut keep = idx.len();
20855 for (i, &e) in idx.iter().enumerate() {
20856 acc += p[e];
20857 if acc >= tau * total {
20858 keep = i + 1;
20859 break;
20860 }
20861 }
20862 idx.truncate(keep);
20863 }
20864 }
20865 let wsum: f32 = if m.norm_topk_prob {
20866 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20867 (if m.router_sigmoid {
20871 s + sigmoid_denom_eps
20872 } else {
20873 s
20874 }) / m.routed_scaling
20875 } else {
20876 1.0 / m.routed_scaling
20877 };
20878 (idx, p, wsum)
20879}
20880
20881fn moe_trace(idx: &[usize]) {
20883 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20884}
20885
20886pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20889 use std::io::Write;
20890 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20891 std::sync::OnceLock::new();
20892 let Some(f) = F.get_or_init(|| {
20893 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20894 Some(std::sync::Mutex::new(
20895 std::fs::OpenOptions::new()
20896 .create(true)
20897 .append(true)
20898 .open(p)
20899 .ok()?,
20900 ))
20901 }) else {
20902 return;
20903 };
20904 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20905 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20906}
20907
20908pub(crate) fn moe_ffn(
20911 m: &MoeFfn,
20912 x: &[f32],
20913 pool: Option<&Pool>,
20914 allowed: Option<&[bool]>,
20915) -> Vec<f32> {
20916 let r = moe_ffn_route(m, x, pool, allowed);
20917 moe_ffn_experts(m, x, &r, pool)
20918}
20919
20920pub(crate) struct MoeRoute {
20924 pub idx: Vec<usize>,
20925 pub p: Vec<f32>,
20926 pub wsum: f32,
20927 pub logits: Vec<f32>,
20928}
20929
20930pub(crate) fn moe_ffn_route(
20936 m: &MoeFfn,
20937 x: &[f32],
20938 pool: Option<&Pool>,
20939 allowed: Option<&[bool]>,
20940) -> MoeRoute {
20941 accumulate_act(m, x, 1);
20942 let ne = m.experts.len();
20943 let mut logits = vec![0.0f32; ne];
20944 match &m.resonance {
20945 Some(r) => r.scores(x, &mut logits),
20946 None => m.router.matvec(x, &mut logits, pool),
20947 }
20948 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20949 {
20950 let mut st = m.stats.borrow_mut();
20951 if st.len() < ne {
20952 st.resize(ne, 0);
20953 }
20954 for &e in &idx {
20955 st[e] += 1;
20956 }
20957 }
20958 moe_trace(&idx);
20964 MoeRoute {
20965 idx,
20966 p,
20967 wsum,
20968 logits,
20969 }
20970}
20971
20972#[cfg(target_os = "macos")]
20981fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20982 const HIDDEN: usize = 2_048;
20983 const ROUTED_INTER: usize = 512;
20984 const SHARED_INTER: usize = 1_024;
20985 const EXPERTS: usize = 256;
20986 const TOP_K: usize = 6;
20987
20988 let Some(routed) = m.experts.first() else {
20989 return false;
20990 };
20991 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20992 return false;
20993 };
20994 let Some((shared, shared_gate)) = &m.shared else {
20995 return false;
20996 };
20997 if shared_gate.is_some()
20998 || model.header.arch.arch_name != "flint"
20999 || model.header.arch.prism_hadamard.is_some()
21000 || model.header.arch.hidden_size != HIDDEN
21001 || m.experts.len() != EXPERTS
21002 || m.top_k != TOP_K
21003 || !m.router_sigmoid
21004 || !m.norm_topk_prob
21005 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
21006 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
21007 || m.route_tau.is_some()
21008 || m.mask.is_some()
21009 || m.per_expert_scale.is_some()
21010 || m.router_input_norm
21011 || m.resonance.is_some()
21012 || !m.grown.is_empty()
21013 {
21014 return false;
21015 }
21016
21017 let routed_q2 = routed.act == Act::Silu
21018 && routed.segs.is_empty()
21019 && routed.gate_proj.rows() == ROUTED_INTER
21020 && routed.gate_proj.cols() == HIDDEN
21021 && routed.up_proj.rows() == ROUTED_INTER
21022 && routed.up_proj.cols() == HIDDEN
21023 && routed.down_proj.rows() == HIDDEN
21024 && routed.down_proj.cols() == ROUTED_INTER
21025 && routed.up_proj.mapped_q2tp().is_some()
21026 && routed.down_proj.mapped_q4tp().is_some();
21027 let shared_q4 = shared.act == Act::Silu
21028 && shared.segs.is_empty()
21029 && shared.gate_proj.rows() == SHARED_INTER
21030 && shared.gate_proj.cols() == HIDDEN
21031 && shared.up_proj.rows() == SHARED_INTER
21032 && shared.up_proj.cols() == HIDDEN
21033 && shared.down_proj.rows() == HIDDEN
21034 && shared.down_proj.cols() == SHARED_INTER
21035 && shared.gate_proj.mapped_q4tp().is_some()
21036 && shared.up_proj.mapped_q4tp().is_some()
21037 && shared.down_proj.mapped_q4tp().is_some();
21038 routed_q2 && shared_q4
21039}
21040
21041pub(crate) fn moe_ffn_experts(
21044 m: &MoeFfn,
21045 x: &[f32],
21046 r: &MoeRoute,
21047 pool: Option<&Pool>,
21048) -> Vec<f32> {
21049 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
21050 if crate::gpu::enabled_here() {
21055 #[cfg(target_os = "macos")]
21056 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
21057 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
21063 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
21064 });
21065 }
21066 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
21067 crate::gpu::ProbeArm::Gpu => {
21068 let t0 = std::time::Instant::now();
21069 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
21070 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
21071 return out;
21072 }
21073 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
21074 }
21075 crate::gpu::ProbeArm::CpuTimed => {
21076 let t0 = std::time::Instant::now();
21077 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21078 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
21079 return out;
21080 }
21081 crate::gpu::ProbeArm::Cpu => {
21082 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
21083 }
21084 }
21085 }
21086 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
21087}
21088
21089fn moe_ffn_minicpm_tail(
21093 slot: &mut crate::minicpm_tail::MiniCpmTail,
21094 li: usize,
21095 total_layers: usize,
21096 m: &MoeFfn,
21097 x: &[f32],
21098 pool: Option<&Pool>,
21099) -> Vec<f32> {
21100 let route = moe_ffn_route(m, x, pool, None);
21101 match slot.forward(li, m, x, &route, pool, total_layers) {
21102 Some(out) => out,
21103 None => moe_ffn_experts(m, x, &route, pool),
21108 }
21109}
21110
21111fn moe_ffn_banked(
21115 slot: &mut crate::mimo_moe::Slot,
21116 li: usize,
21117 m: &MoeFfn,
21118 x: &[f32],
21119 pool: Option<&Pool>,
21120) -> Vec<f32> {
21121 let t0 = std::time::Instant::now();
21122 let r = moe_ffn_route(m, x, pool, None);
21123 slot.note_route(t0.elapsed().as_nanos() as u64);
21124 match slot.forward(li, m, x, &r, pool) {
21125 Some(out) => out,
21126 None => crate::qtensor::float_activations_scope(|| {
21127 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21128 }),
21129 }
21130}
21131
21132fn moe_ffn_banked_rows(
21134 slot: &mut crate::mimo_moe::Slot,
21135 li: usize,
21136 m: &MoeFfn,
21137 xs: &[f32],
21138 b: usize,
21139 hidden: usize,
21140 pool: Option<&Pool>,
21141) -> Vec<f32> {
21142 let t0 = std::time::Instant::now();
21143 let routes: Vec<_> = xs
21144 .chunks_exact(hidden)
21145 .map(|x| moe_ffn_route(m, x, pool, None))
21146 .collect();
21147 slot.note_route(t0.elapsed().as_nanos() as u64);
21148 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21149 return out;
21150 }
21151 let mut out = Vec::with_capacity(b * hidden);
21152 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21153 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21154 crate::qtensor::float_activations_scope(|| {
21156 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21157 })
21158 });
21159 out.extend(row);
21160 }
21161 out
21162}
21163
21164fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21169 use std::sync::atomic::{AtomicBool, Ordering};
21170 if built {
21171 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
21172 if total_layers > 0 && layers_run < total_layers {
21173 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
21174 } else {
21175 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
21176 }
21177 } else {
21178 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
21179 }
21180 static SAID: AtomicBool = AtomicBool::new(false);
21181 if !SAID.swap(true, Ordering::Relaxed) {
21182 if built {
21183 tracing::info!("wgpu whole-token graph: ACTIVE");
21184 } else {
21185 tracing::warn!("wgpu whole-token graph refused — per-op path");
21186 }
21187 }
21188}
21189
21190pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21194pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21195pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21199pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21201
21202pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21206pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21207pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
21208 std::sync::atomic::AtomicU64::new(0);
21209pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21210pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21211pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
21215 std::sync::atomic::AtomicU64::new(0);
21216pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21217pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21218 std::sync::atomic::AtomicU64::new(0);
21219pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21220 std::sync::atomic::AtomicU64::new(0);
21221
21222fn moe_batch_enabled() -> bool {
21225 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21226 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21227}
21228
21229fn moe_ffn_cpu_batched(
21235 m: &MoeFfn,
21236 x: &[f32],
21237 idx: &[usize],
21238 p: &[f32],
21239 wsum: f32,
21240 pool: Option<&Pool>,
21241) -> Option<Vec<f32>> {
21242 if idx.is_empty() || !moe_batch_enabled() {
21243 return None;
21244 }
21245 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21249 return None;
21250 }
21251 let n = idx.len() + usize::from(m.shared.is_some());
21252 let mut pairs = Vec::with_capacity(n);
21253 let mut downs = Vec::with_capacity(n);
21254 let mut ws = Vec::with_capacity(n);
21255 for &e in idx {
21256 let d = &m.experts[e];
21257 if d.act != Act::Silu {
21258 return None;
21259 }
21260 pairs.push((&d.gate_proj, &d.up_proj));
21261 downs.push(&d.down_proj);
21262 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21263 }
21264 if let Some((se, gate)) = &m.shared {
21267 if se.act != Act::Silu {
21268 return None;
21269 }
21270 let g = gate.as_ref().map_or(1.0, |gate| {
21271 let mut gl = [0.0f32; 1];
21272 gate.matvec(x, &mut gl, pool);
21273 1.0 / (1.0 + (-gl[0]).exp())
21274 });
21275 pairs.push((&se.gate_proj, &se.up_proj));
21276 downs.push(&se.down_proj);
21277 ws.push(g);
21278 }
21279 let inter = pairs[0].0.rows();
21280 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21281 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21282 return None;
21283 }
21284 let mut out = attention::take_buf(x.len());
21285 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21286 attention::recycle_buf(&mut out);
21287 return None;
21288 }
21289 Some(out)
21290}
21291
21292pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21300pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21301pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21302pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21303pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21304
21305pub(crate) fn moe_cold_experts_cpu(
21306 experts: &[(&DenseFfn, f32)],
21307 x: &[f32],
21308 pool: Option<&Pool>,
21309) -> Vec<f32> {
21310 let mut out = attention::take_buf(x.len());
21311 if experts.is_empty() {
21312 return out;
21313 }
21314 let pairs: Vec<_> = experts
21315 .iter()
21316 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21317 .collect();
21318 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21319 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21320 let inter = experts[0].0.gate_proj.rows();
21321 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21322 let silu_only = experts
21330 .iter()
21331 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21332 if silu_only
21333 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21334 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21335 {
21336 return out;
21337 }
21338 out.fill(0.0);
21339 for &(expert, weight) in experts {
21340 let mut one = dense_ffn(expert, x, pool);
21341 for (o, v) in out.iter_mut().zip(&one) {
21342 *o += weight * v;
21343 }
21344 attention::recycle_buf(&mut one);
21345 }
21346 out
21347}
21348
21349pub(crate) fn moe_cold_experts_rows_cpu(
21353 jobs: &[Vec<(&DenseFfn, f32)>],
21354 xs: &[f32],
21355 hidden: usize,
21356 pool: Option<&Pool>,
21357) -> Vec<f32> {
21358 let mut out = vec![0.0; xs.len()];
21359 let mut experts: Vec<&DenseFfn> = Vec::new();
21360 let mut groups: Vec<Vec<usize>> = Vec::new();
21361 let mut terms = vec![Vec::new(); jobs.len()];
21362 for (r, row) in jobs.iter().enumerate() {
21363 for &(e, w) in row {
21364 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21365 Some(g) => g,
21366 None => {
21367 experts.push(e);
21368 groups.push(Vec::new());
21369 groups.len() - 1
21370 }
21371 };
21372 terms[r].push((g, groups[g].len(), w));
21373 groups[g].push(r);
21374 }
21375 }
21376 if experts.is_empty() {
21377 return out;
21378 }
21379 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21380 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21381 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21382 let count: usize = lens.iter().sum();
21383 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21384 let mut ds = vec![vec![0.0; hidden]; count];
21385 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21386 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21387 {
21388 let mut offset = 0;
21389 let offsets: Vec<_> = lens
21390 .iter()
21391 .map(|&n| {
21392 let start = offset;
21393 offset += n;
21394 start
21395 })
21396 .collect();
21397 for (r, terms) in terms.iter().enumerate() {
21398 for &(g, slot, w) in terms {
21399 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21400 .iter_mut()
21401 .zip(&ds[offsets[g] + slot])
21402 {
21403 *o += w * v;
21404 }
21405 }
21406 }
21407 } else {
21408 for (r, jobs) in jobs.iter().enumerate() {
21409 if !jobs.is_empty() {
21410 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21411 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21412 attention::recycle_buf(&mut row);
21413 }
21414 }
21415 }
21416 out
21417}
21418
21419fn moe_ffn_cpu(
21421 m: &MoeFfn,
21422 x: &[f32],
21423 idx: &[usize],
21424 p: &[f32],
21425 wsum: f32,
21426 pool: Option<&Pool>,
21427) -> Vec<f32> {
21428 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21429 return out;
21430 }
21431 let mut out = attention::take_buf(x.len());
21432 for &e in idx {
21433 let mut eo = dense_ffn(&m.experts[e], x, pool);
21434 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21435 for i in 0..out.len() {
21436 out[i] += w * eo[i];
21437 }
21438 attention::recycle_buf(&mut eo);
21439 }
21440 if let Some((se, gate)) = &m.shared {
21441 let mut so = dense_ffn(se, x, pool);
21442 let g = gate.as_ref().map_or(1.0, |gate| {
21443 let mut gl = [0.0f32; 1];
21444 gate.matvec(x, &mut gl, pool);
21445 1.0 / (1.0 + (-gl[0]).exp())
21446 });
21447 for i in 0..out.len() {
21448 out[i] += g * so[i];
21449 }
21450 attention::recycle_buf(&mut so);
21451 }
21452 out
21453}
21454
21455#[allow(clippy::too_many_arguments)]
21463pub(crate) fn mla_attention(
21464 w: &MlaWeights,
21465 normed: &[f32],
21466 cache: &mut crate::kv_cache::LayerKvCache,
21467 position: usize,
21468 inv_freq: &[f32],
21469 rope_scale: f32,
21470 eps: f64,
21471 pool: Option<&Pool>,
21472) -> Vec<f32> {
21473 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21474 let hd = dr + dn;
21475 let mut q = vec![0.0f32; nh * hd];
21476 match (&w.q_a, &w.q_a_norm) {
21477 (Some(qa), Some(qn)) => {
21478 let mut t = vec![0.0f32; qa.rows()];
21479 qa.matvec(normed, &mut t, pool);
21480 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21481 w.q_proj.matvec(&tn, &mut q, pool);
21482 }
21483 _ => w.q_proj.matvec(normed, &mut q, pool),
21484 }
21485 let mut ca = vec![0.0f32; lora + dr];
21486 w.kv_a.matvec(normed, &mut ca, pool);
21487 let (c_lat, k_rope) = ca.split_at_mut(lora);
21488 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21489 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21490 w.kv_b.matvec(&latn, &mut kvb, pool);
21491 if !w.nope {
21492 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21493 }
21494 for h in 0..nh {
21495 if !w.nope {
21496 attention::rope_rotate_scaled(
21497 &mut q[h * hd..h * hd + dr],
21498 position,
21499 inv_freq,
21500 rope_scale,
21501 );
21502 }
21503 }
21504 let mut k = vec![0.0f32; nh * hd];
21505 let mut v = vec![0.0f32; nh * hd];
21506 for h in 0..nh {
21507 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21508 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21509 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21510 }
21511 cache.append(&k, &v, &vec![true; nh]);
21512 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21513 attention::recycle_buf(&mut imp);
21514 let mut ov = vec![0.0f32; nh * dv];
21515 for h in 0..nh {
21516 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21517 }
21518 let mut out = vec![0.0f32; w.o_proj.rows()];
21519 w.o_proj.matvec(&ov, &mut out, pool);
21520 out
21521}
21522
21523fn dense_moe_ffn(
21530 dm: &DenseMoeFfn,
21531 x_normed: &[f32],
21532 h_raw: &[f32],
21533 eps: f64,
21534 norm_style: NormStyle,
21535 pool: Option<&Pool>,
21536) -> Vec<f32> {
21537 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21538 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21539 let m = &dm.moe;
21540 let ne = m.experts.len();
21541 let mut logits = vec![0.0f32; ne];
21542 if m.router_input_norm {
21543 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21544 let inv = 1.0 / (ss + eps as f32).sqrt();
21545 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21546 m.router.matvec(&xr, &mut logits, pool);
21547 } else {
21548 m.router.matvec(h_raw, &mut logits, pool);
21549 }
21550 let (idx, p, wsum) = moe_route(&logits, m, None);
21551 {
21552 let mut st = m.stats.borrow_mut();
21553 if st.len() < ne {
21554 st.resize(ne, 0);
21555 }
21556 for &e in &idx {
21557 st[e] += 1;
21558 }
21559 }
21560 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21561 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21562 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21563 for (di, mi) in d.iter_mut().zip(&mo) {
21564 *di += mi;
21565 }
21566 d
21567}
21568
21569fn moe_gpu_refused(why: &'static str) {
21576 use std::sync::atomic::{AtomicBool, Ordering};
21577 static SAID: AtomicBool = AtomicBool::new(false);
21578 if !SAID.swap(true, Ordering::Relaxed) {
21579 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21580 }
21581}
21582
21583fn moe_ffn_gpu(
21584 m: &MoeFfn,
21585 x: &[f32],
21586 idx: &[usize],
21587 p: &[f32],
21588 wsum: f32,
21589 pool: Option<&Pool>,
21590) -> Option<Vec<f32>> {
21591 use crate::gpu::MoeJob;
21592
21593 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21594 let mut model_ref = None;
21595 for &e in idx {
21596 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21597 moe_gpu_refused("push_job(expert)");
21598 return None;
21599 }
21600 }
21601 if let Some((se, gate)) = &m.shared {
21602 let g = gate.as_ref().map_or(1.0, |gate| {
21603 let mut gl = [0.0f32; 1];
21604 gate.matvec(x, &mut gl, pool);
21605 1.0 / (1.0 + (-gl[0]).exp())
21606 });
21607 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21608 moe_gpu_refused("push_job(shared)");
21609 return None;
21610 }
21611 }
21612 let Some(model) = model_ref else {
21613 moe_gpu_refused("no model_ref");
21614 return None;
21615 };
21616 let hidden = jobs[0].down.1;
21617 let mut out = vec![0.0f32; hidden];
21618 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21619 Some(out)
21620 } else {
21621 moe_gpu_refused("gpu::moe_block");
21622 None
21623 }
21624}
21625
21626fn ffn_forward(
21628 ffn: &FfnKind,
21629 x: &[f32],
21630 pool: Option<&Pool>,
21631 experts_allowed: Option<&[bool]>,
21632) -> Vec<f32> {
21633 match ffn {
21634 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21635 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21636 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21637 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21641 }
21642}
21643
21644fn ffn_forward_pair(
21648 ffn: &FfnKind,
21649 x1: &[f32],
21650 x2: &[f32],
21651 pool: Option<&Pool>,
21652 experts_allowed: Option<&[bool]>,
21653) -> (Vec<f32>, Vec<f32>) {
21654 let d = match ffn {
21655 FfnKind::Dense(d) if !d.segs.is_empty() => {
21658 return (
21659 tube_ffn(d, x1, 1, pool, None),
21660 tube_ffn(d, x2, 1, pool, None),
21661 );
21662 }
21663 FfnKind::Dense(d) => d,
21664 FfnKind::Moe(m) => {
21665 return (
21666 moe_ffn(m, x1, pool, experts_allowed),
21667 moe_ffn(m, x2, pool, experts_allowed),
21668 );
21669 }
21670 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21671 };
21672 let inter = d.gate_proj.rows();
21673 FFN_SCRATCH.with(|s| {
21674 let mut s = s.borrow_mut();
21675 let [g1, g2, u1, u2] = &mut *s;
21676 g1.resize(inter, 0.0);
21677 g2.resize(inter, 0.0);
21678 u1.resize(inter, 0.0);
21679 u2.resize(inter, 0.0);
21680 QTensor::matvec2_many(
21683 [&d.gate_proj, &d.up_proj],
21684 x1,
21685 x2,
21686 [g1.as_mut_slice(), u1.as_mut_slice()],
21687 [g2.as_mut_slice(), u2.as_mut_slice()],
21688 pool,
21689 );
21690 for i in 0..inter {
21691 g1[i] = d.act.combine(g1[i], u1[i]);
21692 g2[i] = d.act.combine(g2[i], u2[i]);
21693 }
21694 let mut o1 = attention::take_buf(d.down_proj.rows());
21695 let mut o2 = attention::take_buf(d.down_proj.rows());
21696 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21697 (o1, o2)
21698 })
21699}
21700
21701#[cfg(test)]
21702mod tests {
21703 use std::ffi::OsString;
21704 use std::sync::Mutex;
21705
21706 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21709
21710 struct ScopedTestEnv {
21711 previous: Vec<(&'static str, Option<OsString>)>,
21712 }
21713
21714 impl ScopedTestEnv {
21715 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21716 let previous = vars
21717 .iter()
21718 .map(|(name, _)| (*name, std::env::var_os(name)))
21719 .collect();
21720 for &(name, value) in vars {
21721 unsafe {
21722 match value {
21723 Some(value) => std::env::set_var(name, value),
21724 None => std::env::remove_var(name),
21725 }
21726 }
21727 }
21728 Self { previous }
21729 }
21730 }
21731
21732 impl Drop for ScopedTestEnv {
21733 fn drop(&mut self) {
21734 for (name, value) in self.previous.drain(..).rev() {
21735 unsafe {
21736 match value {
21737 Some(value) => std::env::set_var(name, value),
21738 None => std::env::remove_var(name),
21739 }
21740 }
21741 }
21742 }
21743 }
21744
21745 #[test]
21746 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21747 use super::pad_mtp_draft_logits;
21748
21749 let mut logits = vec![-3.0, -1.0, -2.0];
21750 pad_mtp_draft_logits(&mut logits, 6);
21751 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21752 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21753 assert_eq!(
21754 logits
21755 .iter()
21756 .enumerate()
21757 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21758 .map(|(i, _)| i),
21759 Some(1),
21760 "an unevaluated row cannot win an all-negative shortlist"
21761 );
21762 }
21763
21764 #[test]
21765 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21766 use super::{Pipeline, SpecMon, SpecTrial};
21767
21768 let mut mon = SpecMon::default();
21769 let start = std::time::Instant::now();
21770 mon.round(1.0, 1);
21771 let first = Pipeline::spec_trial_round(
21772 SpecTrial::Spec {
21773 t0: start,
21774 gen0: 0,
21775 rounds: 0,
21776 },
21777 &mut mon,
21778 1,
21779 );
21780 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21781
21782 mon.round(1.0, 1);
21783 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21784 assert!(
21785 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21786 "two verifier-tip-only rounds must stop the losing MTP trial"
21787 );
21788
21789 let mut viable = SpecMon::default();
21792 viable.round(1.0, 1);
21793 viable.round(1.0, 2);
21794 let continued = Pipeline::spec_trial_round(
21795 SpecTrial::Spec {
21796 t0: start,
21797 gen0: 0,
21798 rounds: 1,
21799 },
21800 &mut viable,
21801 3,
21802 );
21803 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21804 }
21805
21806 #[test]
21807 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21808 let mut p = spark_test_pipeline(None);
21809 let head_rows = 248_070;
21810 p.draft_full_streak = 7;
21811 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21812
21813 p.reset_fresh_mtp_draft_state();
21814 assert_eq!(
21815 p.draft_head_rows(head_rows),
21816 super::Pipeline::draft_vocab_rows(head_rows),
21817 "a fresh request returns to its configured shortlist"
21818 );
21819
21820 p.note_draft_id(u32::MAX);
21823 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21824 }
21825
21826 #[test]
21827 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21828 use super::graph_spec_wanted_by_policy;
21829
21830 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21834 assert!(!graph_spec_wanted_by_policy(
21835 Some("anything-but-1"),
21836 true,
21837 false,
21838 false,
21839 true,
21840 ));
21841 assert!(graph_spec_wanted_by_policy(
21842 Some("1"),
21843 false,
21844 true,
21845 false,
21846 true,
21847 ));
21848 assert!(!graph_spec_wanted_by_policy(
21849 Some("0"),
21850 true,
21851 false,
21852 false,
21853 true,
21854 ));
21855
21856 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21859 assert!(graph_spec_wanted_by_policy(
21860 Some("legacy-force"),
21861 false,
21862 true,
21863 true,
21864 false,
21865 ));
21866 }
21867
21868 #[test]
21869 fn encoder_hidden_states_are_normalized_and_reusable() {
21870 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21871 let ids = [1, 7, 11, 19];
21872
21873 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21874 let second = p
21875 .encode_hidden_states(&ids)
21876 .expect("reusable encoder forward");
21877 assert_eq!(first.len(), ids.len() * 8);
21878 assert!(first.iter().all(|v| v.is_finite()));
21879 assert_eq!(
21880 first, second,
21881 "encoder forward must start from a fresh state"
21882 );
21883
21884 assert!(p.forward_ids(&ids, None).is_ok());
21887 }
21888
21889 #[test]
21894 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21895 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21896 let dense_card = ChunkStackFacts {
21897 plain_dense: true,
21898 discrete: true,
21899 gpu_on: true,
21900 ..Default::default()
21901 };
21902 assert!(dense_card.dense_on_discrete());
21903 assert_eq!(
21905 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21906 DISCRETE_DENSE_PREFILL_CHUNK
21907 );
21908 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21909 for (label, facts) in [
21910 (
21911 "GDN hybrid / MoE / DeepSeek stack",
21912 ChunkStackFacts {
21913 plain_dense: false,
21914 ..dense_card
21915 },
21916 ),
21917 (
21918 "integrated GPU",
21919 ChunkStackFacts {
21920 discrete: false,
21921 ..dense_card
21922 },
21923 ),
21924 (
21925 "CPU only",
21926 ChunkStackFacts {
21927 gpu_on: false,
21928 discrete: false,
21929 ..dense_card
21930 },
21931 ),
21932 (
21933 "capacity split",
21934 ChunkStackFacts {
21935 capacity_split: true,
21936 ..dense_card
21937 },
21938 ),
21939 (
21940 "multi-GPU plan",
21941 ChunkStackFacts {
21942 multi_gpu: true,
21943 ..dense_card
21944 },
21945 ),
21946 (
21947 "O(1) layers",
21948 ChunkStackFacts {
21949 o1: true,
21950 ..dense_card
21951 },
21952 ),
21953 ] {
21954 assert!(!facts.dense_on_discrete(), "{label}");
21955 assert_eq!(
21956 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21957 48,
21958 "{label} keeps the historical x86 chunk"
21959 );
21960 }
21961 for dense in [false, true] {
21963 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21964 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21965 }
21966 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21968 for dense in [false, true] {
21969 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21970 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21971 }
21972 }
21973 }
21974
21975 #[test]
21976 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21977 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21978 let full = |host_rows, device_rows| ReuseLayer {
21979 full: true,
21980 host_rows,
21981 device_rows,
21982 device_state: false,
21983 };
21984 assert_eq!(
21987 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21988 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21989 );
21990 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21992 assert_eq!(
21994 kv_reuse_plan(339, &[full(339, Some(345))]),
21995 ReusePlan::Ready
21996 );
21997 assert_eq!(
21999 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
22000 ReusePlan::Pull(vec![(0, 300, 339)])
22001 );
22002 assert_eq!(
22004 kv_reuse_plan(339, &[full(300, Some(320))]),
22005 ReusePlan::Fresh
22006 );
22007 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
22008 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
22009 let conv = |device_state| ReuseLayer {
22012 full: false,
22013 host_rows: 0,
22014 device_rows: None,
22015 device_state,
22016 };
22017 assert_eq!(
22018 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
22019 ReusePlan::Fresh
22020 );
22021 assert_eq!(
22022 kv_reuse_plan(339, &[conv(false), full(339, None)]),
22023 ReusePlan::Ready
22024 );
22025 }
22026
22027 #[test]
22028 fn nll_graph_policy_scopes_only_the_fused_head() {
22029 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
22030 ("vulkan graph", true, true, false, true, false),
22032 ("native Metal graph", true, true, true, true, true),
22034 ("masked", false, true, false, false, false),
22036 ("graph disabled", true, false, true, false, false),
22037 ] {
22038 let (graph_quality, graph_head_required) =
22039 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
22040 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
22041 assert_eq!(graph_head_required, want_head, "{label}: fused head");
22042 }
22043 }
22044
22045 #[test]
22046 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
22047 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
22048 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
22049 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
22050 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
22051 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
22052 }
22053
22054 #[test]
22055 fn cancel_flag_stops_generation() {
22056 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
22057 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
22060 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
22061 assert_eq!(r.finish_reason, "cancelled");
22062 assert!(
22063 r.token_ids.is_empty(),
22064 "no tokens after cancel: {:?}",
22065 r.token_ids
22066 );
22067 assert_eq!(p.kv_cache.seq_len(), 0);
22068 assert!(p.kv_history.is_empty());
22069 assert!(!p.graph_want_logits);
22070 assert!(p.graph_logits.is_none());
22071 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
22073 assert_ne!(r2.finish_reason, "cancelled");
22074 }
22075 use super::*;
22076
22077 #[test]
22078 fn minimax_m3_arch_selects_checked_oai_activation_only() {
22079 let arch = |arch_name: &str, hidden_act: &str| {
22083 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
22084 "arch_name": arch_name,
22085 "hidden_size": 8,
22086 "intermediate_size": 16,
22087 "num_layers": 1,
22088 "num_attention_heads": 1,
22089 "num_kv_heads": 1,
22090 "head_dim": 8,
22091 "vocab_size": 16,
22092 "layer_types": ["FullAttention"],
22093 "rms_norm_eps": 1e-6,
22094 "max_position_embeddings": 16,
22095 "hidden_act": hidden_act
22096 }))
22097 .expect("minimal architecture")
22098 };
22099 let minimax = arch("minimax_m3_vl", "swigluoai");
22100 assert_eq!(
22101 Act::from_arch_full(&minimax),
22102 Act::SwiGluOai {
22103 alpha: 1.702,
22104 limit: 7.0,
22105 }
22106 );
22107
22108 let unrelated = arch("qwen2", "swigluoai");
22111 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
22112
22113 let non_oai = arch("minimax_m3_vl", "gelu");
22117 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
22118 }
22119
22120 #[test]
22121 fn minimax_swiglu_oai_act_matches_scalar_contract() {
22122 let act = Act::swiglu_oai(1.702, 7.0);
22123 let got = act.combine(10.0, -10.0);
22124 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22125 assert!((got - expected).abs() < 1e-6);
22126 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22127 assert_eq!(
22128 act.wgpu_graph_act(),
22129 Some(crate::gpu::GraphAct::SwiGluOaiM3),
22130 "the WGPU-only arm accepts only the frozen M3 constants"
22131 );
22132 assert_eq!(
22133 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22134 None,
22135 "nearby OAI constants must not borrow MiniMax's kernel"
22136 );
22137 assert_eq!(
22138 act.moe_job_graph_contract(),
22139 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22140 "the direct MoE path carries the frozen clamp with the marker"
22141 );
22142 assert_eq!(
22143 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22144 None,
22145 "a nearby clamp must not borrow MiniMax's device contract"
22146 );
22147
22148 let gate_only = act.apply(-10.0);
22151 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22152 assert!((gate_only - expected_gate).abs() < 1e-10);
22153 }
22154
22155 #[test]
22156 fn explicit_sliding_schedule_selects_local_rope_profile() {
22157 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22162 p.swa = Some((1024, usize::MAX));
22163 p.sliding_layers = Some(vec![true, true, true, false]);
22164 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22165 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22166 p.rope_scale = 1.277_258_9;
22167 p.rope_scale_local = 1.0;
22168
22169 for li in 0..3 {
22170 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22171 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
22172 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
22173 }
22174 assert_eq!(p.layer_window(3), None);
22175 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
22176 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
22177 }
22178
22179 #[test]
22187 fn dynamic_ffn_equals_the_zeroing_arm() {
22188 let (hidden, inter) = (8usize, 32usize);
22189 let synth = |n: usize, salt: usize| -> Vec<f32> {
22190 (0..n)
22191 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
22192 .collect()
22193 };
22194 let down = synth(hidden * inter, 3);
22195 let mut down_t = vec![0.0f32; inter * hidden];
22196 for r in 0..hidden {
22197 for c in 0..inter {
22198 down_t[c * hidden + r] = down[r * inter + c];
22199 }
22200 }
22201 let d = DenseFfn {
22202 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22203 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22204 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
22205 act: Act::Silu,
22206 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
22207 segs: Vec::new(),
22208 };
22209 let x = synth(hidden, 11);
22210 let k = 12usize;
22211 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
22212 let mut g = vec![0.0f32; inter];
22214 d.gate_proj.matvec(&x, &mut g, None);
22215 let mut u = vec![0.0f32; inter];
22216 d.up_proj.matvec(&x, &mut u, None);
22217 for v in g.iter_mut() {
22218 *v = inference::silu(*v);
22219 }
22220 keep_top_k(&mut g, k);
22221 for i in 0..inter {
22222 g[i] *= u[i];
22223 }
22224 let mut want = vec![0.0f32; hidden];
22225 d.down_proj.matvec(&g, &mut want, None);
22226 for (a, b) in want.iter().zip(&got) {
22227 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22228 }
22229 }
22230
22231 #[test]
22237 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22238 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22239 let inter = core + tube;
22240 let synth = |n: usize, salt: usize| -> Vec<f32> {
22241 (0..n)
22242 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22243 .collect()
22244 };
22245 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22246 let d_all = synth(hidden * inter, 3);
22247 let dense = DenseFfn {
22249 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22250 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22251 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22252 act: Act::Silu,
22253 down_t: None,
22254 segs: Vec::new(),
22255 };
22256 let rows =
22257 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22258 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22259 let mut o = Vec::with_capacity(hidden * (b - a));
22260 for r in 0..hidden {
22261 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22262 }
22263 o
22264 };
22265 let tubed = DenseFfn {
22266 down_t: None,
22267 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22268 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22269 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22270 act: Act::Silu,
22271 segs: vec![FfnSeg {
22272 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22273 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22274 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22275 start: core,
22276 width: tube,
22277 }],
22278 };
22279 let x = synth(hidden, 7);
22280 let want = dense_ffn(&dense, &x, None);
22281 let got = tube_ffn(&tubed, &x, 1, None, None);
22282 for (a, b) in want.iter().zip(&got) {
22283 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22284 }
22285 let mut bits = vec![0u8; inter.div_ceil(8)];
22287 for n in 0..core {
22288 bits[n / 8] |= 1 << (n % 8);
22289 }
22290 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22291 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22292 for (a, b) in masked.iter().zip(&closed) {
22293 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22294 }
22295 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22297 for (a, b) in closed.iter().zip(&batch) {
22298 assert_eq!(a, b, "batch arm disagrees with decode arm");
22299 }
22300 }
22301
22302 #[test]
22304 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22305 let (hidden, inter) = (16usize, 40usize);
22306 let synth = |n: usize, salt: usize| -> Vec<f32> {
22307 (0..n)
22308 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22309 .collect()
22310 };
22311 let d = DenseFfn {
22312 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22313 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22314 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22315 act: Act::Silu,
22316 down_t: None,
22317 segs: Vec::new(),
22318 };
22319 let x = synth(hidden, 9);
22320 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22322
22323 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22324
22325 let mut g = vec![0.0f32; inter];
22327 d.gate_proj.matvec(&x, &mut g, None);
22328 let mut u = vec![0.0f32; inter];
22329 d.up_proj.matvec(&x, &mut u, None);
22330 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22331 for i in 0..inter {
22332 g[i] = if act_set.contains(&(i as u16)) {
22333 inference::silu(g[i]) * u[i]
22334 } else {
22335 0.0
22336 };
22337 }
22338 let mut reference = vec![0.0f32; hidden];
22339 d.down_proj.matvec(&g, &mut reference, None);
22340
22341 let max_d = sparse
22342 .iter()
22343 .zip(&reference)
22344 .map(|(a, b)| (a - b).abs())
22345 .fold(0.0f32, f32::max);
22346 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22347 }
22348
22349 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22352 let (h, inter, heads, kv, hd) = (
22353 p.hidden_size,
22354 inter,
22355 p.num_heads,
22356 p.num_kv_heads,
22357 p.head_dim,
22358 );
22359 let synth = |n: usize, salt: usize| -> Vec<f32> {
22360 (0..n)
22361 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22362 .collect()
22363 };
22364 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22365 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22366 };
22367 p.mtp = Some(MtpModule {
22368 enorm: vec![1.0; h],
22369 hnorm: vec![1.0; h],
22370 eh_proj: qt(h, 2 * h, 301),
22371 layer: LayerWeights {
22372 input_norm: vec![1.0; h],
22373 post_norm: vec![1.0; h],
22374 attn_out_norm: None,
22375 ffn_out_norm: None,
22376 layer_scale: None,
22377 ffn: FfnKind::Dense(DenseFfn {
22378 gate_proj: qt(inter, h, 315),
22379 up_proj: qt(inter, h, 316),
22380 down_proj: qt(h, inter, 317),
22381 act: Act::Silu,
22382 down_t: None,
22383 segs: Vec::new(),
22384 }),
22385 attn: AttnKind::Full {
22386 bias: None,
22387 wq: qt(heads * hd, h, 311),
22388 wk: qt(kv * hd, h, 312),
22389 wv: qt(kv * hd, h, 313),
22390 wo: qt(h, heads * hd, 314),
22391 q_norm: None,
22392 k_norm: None,
22393 output_gate: false,
22394 softplus_gate: None,
22395 },
22396 },
22397 final_norm: vec![1.0; h],
22398 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22399 });
22400 }
22401
22402 fn attach_test_mtp(p: &mut Pipeline) {
22404 attach_test_mtp_width(p, p.intermediate_size);
22405 }
22406
22407 #[test]
22408 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22409 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22410 attach_test_mtp_width(&mut p, 11);
22413 let ids = [3, 9, 4, 12, 7, 2];
22414 let trunk = p.probe_ffn_mass(&ids);
22415 assert_eq!(trunk[0].len(), p.intermediate_size);
22416
22417 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22418 assert_eq!(got.len(), 1);
22419 assert_eq!(got[0].mass.len(), 11);
22420 assert_eq!(got[0].squared_mass.len(), 11);
22421 assert_eq!(got[0].observed_positions, ids.len() - 1);
22422 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22423 assert!(got[0]
22424 .mass
22425 .iter()
22426 .zip(&got[0].squared_mass)
22427 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22428 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22429 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22430
22431 let h = p.hidden_size;
22435 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22436 panic!("synthetic MTP must be dense")
22437 };
22438 dense.segs.push(FfnSeg {
22439 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22440 up: QTensor::from_f32(vec![0.0; h], 1, h),
22441 down: QTensor::from_f32(vec![0.0; h], h, 1),
22442 start: dense.gate_proj.rows(),
22443 width: 1,
22444 });
22445 let err = p.probe_mtp_ffn(&ids);
22446 assert!(err
22447 .unwrap_err()
22448 .contains("tube-segmented draft FFNs are not supported"));
22449 assert_eq!(p.kv_cache.seq_len(), 0);
22450 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22451 }
22452
22453 #[test]
22454 fn speculative_equals_vanilla_greedy() {
22455 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22456 let _env = ScopedTestEnv::set(&[
22459 ("CMF_GPU", Some("0")),
22460 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22461 ("CMF_MTP_PAIR", Some("1")),
22462 ]);
22463 let run = |spec: bool| {
22464 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22465 p.sampler_config.temperature = 0.0;
22466 attach_test_mtp(&mut p);
22467 p.speculative = spec;
22468 let r = p.generate("abcdef", 12, None, None).unwrap();
22469 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22470 };
22471 let (vanilla, d0, _) = run(false);
22472 let (spec, d1, a1) = run(true);
22473 assert_eq!(d0, 0, "vanilla path must not draft");
22474 assert!(d1 > 0, "speculative path must draft");
22475 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22476 assert_eq!(
22477 vanilla, spec,
22478 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22479 );
22480 }
22481
22482 #[test]
22483 fn speculative_accepts_constant_oracle() {
22484 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22485 let _env = ScopedTestEnv::set(&[
22486 ("CMF_GPU", Some("0")),
22487 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22488 ("CMF_MTP_PAIR", Some("1")),
22489 ]);
22490 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22491 p.sampler_config.temperature = 0.0;
22492 p.sampler_config.repetition_penalty = 1.0;
22493 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22496 attach_test_mtp(&mut p);
22497 p.speculative = true;
22498 let r = p.generate("abcd", 10, None, None).unwrap();
22499 assert!(r.mtp_drafted > 0);
22500 assert_eq!(
22501 r.mtp_accepted, r.mtp_drafted,
22502 "constant logits → every draft accepted"
22503 );
22504 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22507 }
22508
22509 #[test]
22510 fn host_fused_mtp_pair_is_opt_in() {
22511 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22512 let _env = ScopedTestEnv::set(&[
22513 ("CMF_GPU", Some("0")),
22514 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22515 ("CMF_MTP_PAIR", None),
22516 ]);
22517 let run = |spec: bool| {
22518 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22519 p.sampler_config.temperature = 0.0;
22520 attach_test_mtp(&mut p);
22521 p.speculative = spec;
22522 let r = p.generate("abcdef", 12, None, None).unwrap();
22523 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22524 };
22525 let (plain, d0, _) = run(false);
22526 let (guarded, d1, a1) = run(true);
22527 assert_eq!(d0, 0);
22528 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22529 assert_eq!(a1, 0);
22530 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22531 }
22532
22533 #[test]
22534 fn empty_prompt_is_an_error_not_a_panic() {
22535 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22536 let r = p.generate("", 4, None, None);
22537 assert!(r.is_err(), "empty prompt must be a clean error");
22538 }
22539
22540 #[test]
22541 fn every_token_enters_kv_exactly_once() {
22542 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22543 p.sampler_config.temperature = 0.0;
22545 let r = p.generate("abc", 2, None, None).unwrap();
22546 assert_eq!(r.prompt_tokens, 3);
22547 assert_eq!(
22551 p.kv_cache.seq_len(),
22552 3 + r.tokens_generated - 1,
22553 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22554 );
22555 }
22556
22557 #[test]
22558 fn generation_is_reproducible_with_seed() {
22559 let run = || {
22560 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22561 p.generate("hello", 8, None, None).unwrap().token_ids
22562 };
22563 assert_eq!(run(), run());
22564 }
22565
22566 #[test]
22567 fn resetting_sampler_restarts_the_seeded_stream() {
22568 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22569 let config = SamplerConfig {
22570 seed: Some(1234),
22571 ..SamplerConfig::default()
22572 };
22573 p.set_sampler_config(config.clone());
22574 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22575 p.set_sampler_config(config);
22576 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22577 assert_eq!(first, second);
22578 }
22579
22580 #[test]
22581 fn eviction_bounds_the_cache() {
22582 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22583 p.kv_cache.max_seq_len = 6;
22584 p.sampler_config.temperature = 0.0;
22585 let _ = p.generate("abcd", 12, None, None).unwrap();
22586 assert!(
22587 p.kv_cache.seq_len() <= 6 + 1,
22588 "cache must stay bounded by max_seq_len (got {})",
22589 p.kv_cache.seq_len()
22590 );
22591 }
22592
22593 #[test]
22594 fn confidence_matches_tokens_and_is_a_probability() {
22595 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22596 p.sampler_config.temperature = 0.0;
22597 p.sampler_config.repetition_penalty = 1.0;
22598 let r = p.generate("abcd", 10, None, None).unwrap();
22599 assert_eq!(
22600 r.token_confidence.len(),
22601 r.token_ids.len(),
22602 "one confidence per emitted token"
22603 );
22604 for &c in &r.token_confidence {
22605 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22606 }
22607 let logits = [1.0f32, 3.0, 0.5, 3.0];
22609 let p0 = top1_prob_t(&logits, 1, 1.0);
22610 let p1 = top1_prob_t(&logits, 3, 1.0);
22611 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22612 assert!(p0 > 0.0 && p0 < 1.0);
22613 let sharp = top1_prob_t(&logits, 1, 1.0);
22615 let soft = top1_prob_t(&logits, 1, 2.0);
22616 assert!(soft < sharp, "higher temperature lowers peak confidence");
22617 }
22618
22619 #[test]
22620 fn trace_is_opt_in_and_parallels_the_output() {
22621 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22623 p.sampler_config.temperature = 0.0;
22624 p.sampler_config.repetition_penalty = 1.0;
22625 let r = p.generate("abcd", 10, None, None).unwrap();
22626 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22627
22628 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22630 p.sampler_config.temperature = 0.0;
22631 p.sampler_config.repetition_penalty = 1.0;
22632 p.set_trace(true);
22633 let r = p.generate("abcd", 10, None, None).unwrap();
22634 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22635 for (i, tr) in r.traces.iter().enumerate() {
22636 assert_eq!(tr.t, i, "trace index is sequential");
22637 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22638 assert_eq!(
22639 tr.confidence, r.token_confidence[i],
22640 "trace confidence matches the confidence channel"
22641 );
22642 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22644 }
22645 }
22646
22647 #[test]
22648 fn explain_prefill_logits_match_greedy_first_token() {
22649 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22653 p.sampler_config.temperature = 0.0;
22654 p.sampler_config.repetition_penalty = 1.0;
22655 let ids = p.tokenizer.encode("abcd");
22656 let logits = p.prefill_next_logits(&ids, None);
22657 let argmax = logits
22658 .iter()
22659 .enumerate()
22660 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22661 .unwrap()
22662 .0 as u32;
22663 let r = p.generate("abcd", 1, None, None).unwrap();
22664 assert_eq!(
22665 argmax, r.token_ids[0],
22666 "explain preview must match greedy emit"
22667 );
22668 }
22669
22670 #[test]
22671 fn laguna_shared_expert_is_unconditionally_added() {
22672 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22673 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22674 let zero_dense = || DenseFfn {
22675 gate_proj: matrix(vec![0.0; 4]),
22676 up_proj: matrix(vec![0.0; 4]),
22677 down_proj: matrix(vec![0.0; 4]),
22678 act: Act::Silu,
22679 down_t: None,
22680 segs: Vec::new(),
22681 };
22682 let shared = DenseFfn {
22683 gate_proj: identity(),
22684 up_proj: identity(),
22685 down_proj: identity(),
22686 act: Act::Silu,
22687 down_t: None,
22688 segs: Vec::new(),
22689 };
22690 let x = [1.0, 2.0];
22691 let expected = dense_ffn(&shared, &x, None);
22692 let moe = MoeFfn {
22693 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22694 experts: vec![zero_dense()],
22695 top_k: 1,
22696 norm_topk_prob: true,
22697 router_sigmoid: true,
22698 sigmoid_denom_eps: 1e-6,
22699 expert_bias: None,
22700 routed_scaling: 1.0,
22701 route_tau: None,
22702 shared: Some((shared, None)),
22703 stats: std::cell::RefCell::new(Vec::new()),
22704 act_sq: std::cell::RefCell::new(Vec::new()),
22705 act_rows: std::cell::RefCell::new(Vec::new()),
22706 mask: None,
22707 per_expert_scale: None,
22708 router_input_norm: false,
22709 resonance: None,
22710 grown: Vec::new(),
22711 };
22712 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22713 for (actual, expected) in actual.iter().zip(expected) {
22714 assert!((actual - expected).abs() < 1e-6);
22715 }
22716 }
22717
22718 fn mimo_test_pipeline() -> Pipeline {
22727 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22728 let kvh = [1usize, 2, 2, 1];
22729 let synth = |n: usize, salt: usize| -> Vec<f32> {
22730 (0..n)
22731 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22732 .collect()
22733 };
22734 let qt = |rows: usize, cols: usize, salt: usize| {
22735 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22736 };
22737 let dense = |inter: usize, salt: usize| DenseFfn {
22738 gate_proj: qt(inter, hs, salt),
22739 up_proj: qt(inter, hs, salt + 1),
22740 down_proj: qt(hs, inter, salt + 2),
22741 act: Act::Silu,
22742 down_t: None,
22743 segs: Vec::new(),
22744 };
22745 let layers: Vec<LayerWeights> = (0..4)
22746 .map(|li| LayerWeights {
22747 input_norm: vec![1.0; hs],
22748 post_norm: vec![1.0; hs],
22749 attn_out_norm: None,
22750 ffn_out_norm: None,
22751 layer_scale: None,
22752 ffn: if li == 0 {
22753 FfnKind::Dense(dense(inter, 50))
22754 } else {
22755 FfnKind::Moe(MoeFfn {
22756 router: qt(4, hs, 60 + li),
22757 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22758 top_k: 2,
22759 norm_topk_prob: true,
22760 router_sigmoid: true,
22761 sigmoid_denom_eps: 1e-6,
22762 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22763 routed_scaling: 1.0,
22764 route_tau: None,
22765 shared: None,
22766 stats: std::cell::RefCell::new(Vec::new()),
22767 act_sq: std::cell::RefCell::new(Vec::new()),
22768 act_rows: std::cell::RefCell::new(Vec::new()),
22769 mask: None,
22770 per_expert_scale: None,
22771 router_input_norm: false,
22772 resonance: None,
22773 grown: Vec::new(),
22774 })
22775 },
22776 attn: AttnKind::Full {
22777 wq: qt(nh * hd, hs, li * 10 + 1),
22778 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22779 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22780 wo: qt(hs, nh * vd, li * 10 + 4),
22781 q_norm: None,
22782 k_norm: None,
22783 output_gate: false,
22784 softplus_gate: None,
22785 bias: None,
22786 },
22787 })
22788 .collect();
22789 let mut p = Pipeline::new(
22790 Tokenizer::byte_level(),
22791 PipelineWeights {
22792 embed_tokens: qt(vocab, hs, 100),
22793 layers,
22794 lm_head: qt(vocab, hs, 200),
22795 final_norm: vec![1.0; hs],
22796 },
22797 hs,
22798 inter,
22799 nh,
22800 1, hd,
22802 4,
22803 4,
22804 false,
22805 vocab,
22806 1e-6,
22807 1e7,
22808 NormStyle::Qwen,
22809 4096,
22810 SamplerConfig {
22811 seed: Some(7),
22812 ..Default::default()
22813 },
22814 );
22815 p.layer_dump = None;
22817 p.set_rotary(4, 1e7);
22818 p.sliding_layers = Some(vec![false, true, true, false]);
22819 p.swa = Some((3, usize::MAX));
22820 p.rotary_dim_local = Some(4);
22821 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22822 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22823 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22824 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22825 p
22826 }
22827
22828 #[test]
22829 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22830 let mut p = mimo_test_pipeline();
22831 p.speculative = false;
22832 p.ignore_eos = true;
22833 p.sampler_config.temperature = 0.0;
22834 p.sampler_config.repetition_penalty = 1.0;
22835 let a = vec![3, 5, 7, 9, 11, 13];
22836 let b = vec![4, 8, 12, 16, 20, 24];
22837 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22838 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22839 let actual = p
22843 .generate_from_embeds(&a, &rows, 8, None, None)
22844 .unwrap()
22845 .token_ids;
22846 assert_eq!(actual, expected);
22847 assert!(p.kv_history.is_empty());
22848 let mut extended = a.clone();
22849 extended.push(17);
22850 let after_media = p
22851 .generate_from_ids(&extended, 8, None, None)
22852 .unwrap()
22853 .token_ids;
22854 p.reset_session();
22855 let fresh = p
22856 .generate_from_ids(&extended, 8, None, None)
22857 .unwrap()
22858 .token_ids;
22859 assert_eq!(after_media, fresh);
22860 assert!(
22861 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22862 .is_err()
22863 );
22864 p.reset_session();
22868 p.generate_from_ids(&a, 1, None, None).unwrap();
22869 let mut media_ids = p.kv_history.clone();
22870 assert!(!media_ids.is_empty());
22871 media_ids.extend_from_slice(&[19, 21, 23]);
22872 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22873 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22874 let mut oracle = mimo_test_pipeline();
22875 oracle.speculative = false;
22876 oracle.ignore_eos = true;
22877 oracle.sampler_config.temperature = 0.0;
22878 oracle.sampler_config.repetition_penalty = 1.0;
22879 let expected = oracle
22880 .generate_from_ids(&source_ids, 8, None, None)
22881 .unwrap()
22882 .token_ids;
22883 assert_eq!(
22884 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22885 .unwrap()
22886 .token_ids,
22887 expected
22888 );
22889 assert!(p.kv_history.is_empty());
22890 let mut bad = rows;
22891 bad[0] = f32::NAN;
22892 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22893 }
22894
22895 fn f32_bits(v: &[f32]) -> Vec<u32> {
22896 v.iter().map(|x| x.to_bits()).collect()
22897 }
22898
22899 #[test]
22906 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22907 let mut p = mimo_test_pipeline();
22908 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22909 assert_eq!(kv, vec![1, 2, 2, 1]);
22910 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22911 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22912 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22913 let hs = p.hidden_size;
22914 let mut decode = Vec::new();
22915 for (pos, &id) in ids.iter().enumerate() {
22916 let e = p.embed_single(id);
22917 let h = p.forward_layers(&e, pos, None);
22918 decode.push(p.logits_from_hidden(&h));
22919 }
22920 for l in &p.kv_cache.layers {
22921 assert_eq!(l.seq_len, 12);
22922 assert_eq!(l.head_values(0).len(), 12 * 8);
22924 }
22925 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22926
22927 p.clear_sequence_state();
22928 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22929 for pos in 0..ids.len() {
22930 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22931 assert_eq!(
22932 f32_bits(&decode[pos]),
22933 f32_bits(&lg),
22934 "whole prompt, pos {pos}"
22935 );
22936 }
22937
22938 p.clear_sequence_state();
22939 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22940 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22941 for pos in 0..ids.len() {
22942 let row = if pos < 5 {
22943 &a[pos * hs..(pos + 1) * hs]
22944 } else {
22945 &b[(pos - 5) * hs..(pos - 4) * hs]
22946 };
22947 let lg = p.logits_from_hidden(row);
22948 assert_eq!(
22949 f32_bits(&decode[pos]),
22950 f32_bits(&lg),
22951 "two chunks, pos {pos}"
22952 );
22953 }
22954
22955 let last = |p: &mut Pipeline| {
22958 p.clear_sequence_state();
22959 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22960 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22961 };
22962 let base = last(&mut p);
22963 let mut no_sinks = mimo_test_pipeline();
22964 for l in &mut no_sinks.kv_cache.layers {
22965 l.sinks = None;
22966 }
22967 assert_ne!(
22968 f32_bits(&last(&mut no_sinks)),
22969 f32_bits(&base),
22970 "sinks are live"
22971 );
22972 let mut wide = mimo_test_pipeline();
22973 wide.swa = Some((64, usize::MAX));
22974 assert_ne!(
22975 f32_bits(&last(&mut wide)),
22976 f32_bits(&base),
22977 "window is live"
22978 );
22979
22980 p.clear_sequence_state();
22982 p.ignore_eos = true;
22983 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22984 assert_eq!(r.token_ids.len(), 4);
22985 }
22986
22987 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22990 let (hs, nh) = (16usize, 4usize);
22991 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22992 p.layer_dump = None;
22993 p.swa = Some((6, 4));
22994 p.proj_gate_sigmoid = true;
22995 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22996 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22997 let g: Vec<f32> = (0..nh * hs)
22998 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22999 .collect();
23000 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
23001 }
23002 }
23003 p.swa_trim = trim;
23004 p
23005 }
23006
23007 #[test]
23013 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
23014 let mut a = spark_test_pipeline(None);
23015 let mut b = spark_test_pipeline(Some((2, 4)));
23016 assert_eq!(
23017 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
23018 vec![Some(6), Some(6), Some(6), None]
23019 );
23020 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
23021 let mut pos = 0usize;
23022 for &n in [5usize, 13, 7, 16].iter().cycle() {
23023 if pos >= ids.len() {
23024 break;
23025 }
23026 let end = (pos + n).min(ids.len());
23027 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23028 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
23029 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
23030 pos = end;
23031 }
23032 for _ in 0..23 {
23033 let e = a.embed_single(((pos * 7) % 64) as u32);
23034 let ha = a.forward_layers(&e, pos, None);
23035 let hb = b.forward_layers(&e, pos, None);
23036 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
23037 for li in 0..3 {
23038 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
23039 }
23040 pos += 1;
23041 }
23042 for round in 0..9 {
23043 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
23044 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
23045 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
23046 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
23047 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
23048 if round % 2 == 1 {
23049 for p in [&mut a, &mut b] {
23051 for l in &mut p.kv_cache.layers {
23052 l.truncate_last(2);
23053 }
23054 }
23055 } else {
23056 pos += 2;
23057 }
23058 }
23059 let e = a.embed_single(9);
23060 let (ha, hb) = (
23061 a.forward_layers(&e, pos, None),
23062 b.forward_layers(&e, pos, None),
23063 );
23064 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
23065 for li in 0..3 {
23066 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
23067 assert!(lb.base() > 0, "layer {li} trimmed");
23068 assert_eq!(la.base(), 0);
23069 assert_eq!(lb.pos_len(), la.seq_len);
23070 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
23071 }
23072 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
23073 assert_eq!(
23074 (gb.base(), gb.seq_len),
23075 (0, ga.seq_len),
23076 "the global layer keeps all"
23077 );
23078 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
23079 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
23080 }
23081
23082 #[test]
23087 fn swa_trim_wire_handoff_continues_bitwise() {
23088 let mut a = spark_test_pipeline(None);
23089 let mut b = spark_test_pipeline(Some((2, 4)));
23090 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
23091 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23092 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
23093 assert_eq!(f32_bits(&ha), f32_bits(&hb));
23094 let mut pos = ids.len();
23095 for _ in 0..5 {
23096 let e = a.embed_single(((pos * 5) % 64) as u32);
23097 assert_eq!(
23098 f32_bits(&a.forward_layers(&e, pos, None)),
23099 f32_bits(&b.forward_layers(&e, pos, None))
23100 );
23101 pos += 1;
23102 }
23103 let mut c = spark_test_pipeline(Some((2, 4)));
23104 for li in 0..4 {
23105 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
23106 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
23107 }
23108 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
23109 assert_eq!(c.kv_cache.seq_len(), pos);
23110 for step in 0..17 {
23111 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
23112 let ha = a.forward_layers(&e, pos, None);
23113 let hc = c.forward_layers(&e, pos, None);
23114 assert_eq!(
23115 f32_bits(&ha),
23116 f32_bits(&hc),
23117 "after the hand-off, step {step}"
23118 );
23119 pos += 1;
23120 }
23121 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23122 }
23123
23124 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23129 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23130 let synth = |len: usize, salt: usize| -> Vec<f32> {
23131 (0..len)
23132 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23133 .collect()
23134 };
23135 let qt = |rows: usize, cols: usize, salt: usize| {
23136 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23137 };
23138 let layers = widths
23139 .iter()
23140 .copied()
23141 .enumerate()
23142 .map(|(k, inter)| {
23143 let s = 500 + k * 40;
23144 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23145 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23146 MtpModule {
23147 enorm: vec![1.0; hs],
23148 hnorm: vec![1.0; hs],
23149 eh_proj: qt(hs, 2 * hs, s),
23150 layer: LayerWeights {
23151 input_norm: vec![1.0; hs],
23152 post_norm: vec![1.0; hs],
23153 attn_out_norm: None,
23154 ffn_out_norm: None,
23155 layer_scale: None,
23156 attn: AttnKind::Full {
23157 wq: qt(nh * hd, hs, s + 1),
23158 wk: qt(nkv * hd, hs, s + 2),
23159 wv: qt(nkv * vd, hs, s + 3),
23160 wo: qt(hs, nh * vd, s + 4),
23161 q_norm: None,
23162 k_norm: None,
23163 output_gate: false,
23164 softplus_gate: None,
23165 bias: None,
23166 },
23167 ffn: FfnKind::Dense(DenseFfn {
23168 gate_proj: qt(inter, hs, s + 5),
23169 up_proj: qt(inter, hs, s + 6),
23170 down_proj: qt(hs, inter, s + 7),
23171 act: Act::Silu,
23172 down_t: None,
23173 segs: Vec::new(),
23174 }),
23175 },
23176 final_norm: vec![1.0; hs],
23177 kv,
23178 }
23179 })
23180 .collect();
23181 mimo_mtp::MimoMtp::from_layers(layers)
23182 }
23183
23184 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
23185 mimo_test_mtp_widths(&vec![24; n], gain)
23186 }
23187
23188 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
23189 p.clear_sequence_state();
23190 p.speculative = spec;
23191 p.ignore_eos = true;
23192 p.sampler_config.temperature = 0.0;
23193 p.generate_from_ids(ids, n, None, None).unwrap()
23194 }
23195
23196 #[test]
23197 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
23198 let mut p = mimo_test_pipeline();
23199 let widths = [7usize, 13, 29];
23200 assert_ne!(widths[0], p.intermediate_size);
23201 assert_ne!(widths[1], p.intermediate_size);
23202 assert_ne!(widths[2], p.intermediate_size);
23203 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
23204 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
23205
23206 let trunk = p.probe_ffn_mass(&ids);
23207 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
23208 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
23209 assert_eq!(got.len(), widths.len());
23210 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
23211 assert_eq!(stat.mass.len(), width, "draft layer {k}");
23212 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
23213 assert_eq!(stat.observed_positions, ids.len() - k - 1);
23214 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
23215 assert!(stat
23216 .mass
23217 .iter()
23218 .zip(&stat.squared_mass)
23219 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23220 }
23221 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23222 let st = p.mimo_mtp.as_ref().unwrap();
23223 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23224
23225 let h = p.hidden_size;
23228 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23229 panic!("synthetic MiMo MTP must be dense")
23230 };
23231 dense.segs.push(FfnSeg {
23232 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23233 up: QTensor::from_f32(vec![0.0; h], 1, h),
23234 down: QTensor::from_f32(vec![0.0; h], h, 1),
23235 start: dense.gate_proj.rows(),
23236 width: 1,
23237 });
23238 assert!(p
23239 .probe_mtp_ffn(&ids)
23240 .unwrap_err()
23241 .contains("tube-segmented draft FFNs are not supported"));
23242 assert_eq!(p.kv_cache.seq_len(), 0);
23243 assert!(p
23244 .mimo_mtp
23245 .as_ref()
23246 .unwrap()
23247 .layers
23248 .iter()
23249 .all(|m| m.kv.seq_len == 0));
23250 }
23251
23252 #[test]
23253 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23254 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23255 p.clear_sequence_state();
23256 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23257 let _exact = ExactMtpCalibrationGuard::enter();
23258 let got = crate::gpu::cpu_scope(|| {
23259 let trunk = p.probe_trunk_rows(ids)?;
23260 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
23261 })
23262 .expect("teacher-forced MiMo probe");
23263 st.reset();
23264 p.mimo_mtp = Some(st);
23265 p.reset_mtp_probe_state();
23266 got
23267 }
23268
23269 for post_norm_hidden in [false, true] {
23270 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23271 let make = || {
23272 let mut p = mimo_test_pipeline();
23273 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23274 st.post_norm_hidden = post_norm_hidden;
23275 p.mimo_mtp = Some(st);
23276 p
23277 };
23278 let mut full = make();
23279 let expected = collect(&mut full, &ids, usize::MAX);
23280 for rows in [1usize, 7, 128] {
23281 let mut chunked = make();
23282 let got = collect(&mut chunked, &ids, rows);
23283 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23284 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23285 assert_eq!(
23286 got.observed_positions, expected.observed_positions,
23287 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23288 );
23289 assert_eq!(
23290 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23291 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23292 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23293 );
23294 assert_eq!(
23295 got.squared_mass
23296 .iter()
23297 .map(|v| v.to_bits())
23298 .collect::<Vec<_>>(),
23299 expected
23300 .squared_mass
23301 .iter()
23302 .map(|v| v.to_bits())
23303 .collect::<Vec<_>>(),
23304 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23305 );
23306 }
23307 }
23308 }
23309 }
23310
23311 #[test]
23317 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23318 for post in [false, true] {
23321 let mut p = mimo_test_pipeline();
23322 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23324 let mut st0 = mimo_test_mtp(3, 1.0);
23325 st0.post_norm_hidden = post;
23326 p.mimo_mtp = Some(st0);
23327 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23328 let hs = p.hidden_size;
23329 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23330 p.mimo_note_rows(&hb, 0);
23331 let mut st = p.mimo_mtp.take().unwrap();
23332 let k = 3;
23335 let mut inc = Vec::new();
23336 for t in 0..ids.len() - k - 1 {
23337 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23338 }
23339 let s = ids.len();
23342 let mut reference = vec![vec![0u32; k]; s - k - 1];
23343 let mut fresh = mimo_test_mtp(3, 1.0);
23344 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23345 let n = s - layer - 1;
23346 let mut cats = vec![0.0f32; n * 2 * hs];
23347 for j in 0..n {
23348 let e = p.embed_single(ids[j + layer + 1]);
23349 let raw = &hb[j * hs..(j + 1) * hs];
23350 let g = if post {
23351 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23352 } else {
23353 raw.to_vec()
23354 };
23355 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23356 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23357 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23358 }
23359 let mut x = vec![0.0f32; n * hs];
23360 m.eh_proj.matmat(&cats, n, &mut x, None);
23361 p.mimo_mtp_block(m, &mut x, n, 0);
23362 for (t, row) in reference.iter_mut().enumerate() {
23363 let y = inference::rms_norm(
23364 &x[t * hs..(t + 1) * hs],
23365 &m.final_norm,
23366 p.rms_eps,
23367 p.norm_style,
23368 );
23369 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23370 }
23371 }
23372 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23373 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23375 assert!(distinct.len() > 3, "{inc:?}");
23376 let last_t = ids.len() - k - 2;
23378 for m in &st.layers {
23379 assert_eq!(m.kv.seq_len, last_t + 1);
23380 }
23381 }
23382 }
23383
23384 #[test]
23391 fn mimo_speculative_greedy_equals_plain_greedy() {
23392 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23393 let _env = ScopedTestEnv::set(&[
23394 ("CMF_GPU", Some("0")),
23395 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23396 ]);
23397 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23398 let n = 24;
23399 let mut p = mimo_test_pipeline();
23400 let plain = mimo_greedy(&mut p, &ids, n, false);
23401 assert_eq!(plain.mtp_drafted, 0);
23402 assert_eq!(plain.token_ids.len(), n);
23403 let plain_kv = p.kv_cache.layers[0].seq_len;
23404
23405 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23407 let spec = mimo_greedy(&mut p, &ids, n, true);
23408 assert!(spec.mtp_drafted > 0, "the round must draft");
23409 assert_eq!(spec.token_ids, plain.token_ids);
23410 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23411
23412 let mut truth: Vec<u32> = ids.clone();
23415 truth.extend(&plain.token_ids);
23416 let mut noisy = truth.clone();
23417 for (i, t) in noisy.iter_mut().enumerate() {
23418 if i % 5 == 0 {
23419 *t = (*t + 1) % 64;
23420 }
23421 }
23422 let mut st = mimo_test_mtp(3, 1.0);
23423 st.draft_override = Some(noisy);
23424 p.mimo_mtp = Some(st);
23425 let spec = mimo_greedy(&mut p, &ids, n, true);
23426 assert_eq!(spec.token_ids, plain.token_ids);
23427 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23428 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23429 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23430 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23431 assert!(
23432 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23433 "{:?}",
23434 stats.accept_hist
23435 );
23436 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23437
23438 let mut st = mimo_test_mtp(3, 1.0);
23441 st.draft_override = Some(truth);
23442 p.mimo_mtp = Some(st);
23443 let spec = mimo_greedy(&mut p, &ids, n, true);
23444 assert_eq!(spec.token_ids, plain.token_ids);
23445 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23446 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23447
23448 let off = mimo_greedy(&mut p, &ids, n, false);
23450 assert_eq!(off.token_ids, plain.token_ids);
23451 assert_eq!(off.mtp_drafted, 0);
23452 }
23453
23454 #[test]
23463 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23464 let p = mimo_test_pipeline();
23465 assert_eq!(
23466 p.graph_attn_decline_reason(),
23467 Some("per-layer KV head counts")
23468 );
23469 assert_eq!(p.wgpu_graph_attn_decline(), None);
23470 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23471 assert_eq!(
23472 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23473 (1, 4, 4, None, false)
23474 );
23475 assert_eq!(g0.invf, p.inv_freq.as_slice());
23476 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23477 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23478 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23479 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23480 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23481 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23482 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23483
23484 let emb = p.embed_single(3);
23487 let mut lg = Vec::new();
23488 assert!(
23489 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23490 .is_none()
23491 );
23492 let mut hid = emb.clone();
23493 assert_eq!(
23494 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23495 crate::gpu::BatchGraphOutcome::Declined
23496 );
23497 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23498 assert!(p.try_multi_burst(3, 0, 4).is_none());
23499 assert!(
23500 p.graph_declines().is_empty(),
23501 "no attention decline logged: {:?}",
23502 p.graph_declines()
23503 );
23504 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23509 assert_eq!(plain().graph_attn_decline_reason(), None);
23510 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23511 assert!(
23512 plain().graph_attn_geom(0).is_none(),
23513 "uniform models keep the historical arms"
23514 );
23515 let mut q = plain();
23516 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23517 assert_eq!(
23518 q.graph_attn_decline_reason(),
23519 Some("learned attention sinks")
23520 );
23521 assert_eq!(
23522 q.graph_attn_geom(1).unwrap().sink,
23523 Some(&[0.25f32, -0.25][..])
23524 );
23525 let mut q = plain();
23526 q.set_attn_geometry(None, Some(2)).unwrap();
23527 assert_eq!(
23528 q.graph_attn_decline_reason(),
23529 Some("V heads narrower than Q/K heads")
23530 );
23531 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23532 let mut q = plain();
23533 q.sliding_layers = Some(vec![true, false]);
23534 q.swa = Some((4, usize::MAX));
23535 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23536 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23537 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23538
23539 let mut q = mimo_test_pipeline();
23543 q.rope_scale = 2.0;
23544 q.rope_scale_local = 1.25;
23545 assert_eq!(q.wgpu_graph_attn_decline(), None);
23546 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23547 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23548
23549 let mut q = plain();
23552 q.rope_scale = 2.0;
23553 assert_eq!(
23554 q.wgpu_graph_attn_decline(),
23555 Some("scaled RoPE without per-layer geometry")
23556 );
23557 }
23558
23559 #[cfg(target_os = "macos")]
23565 #[test]
23566 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23567 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23571 p.swa = Some((16, usize::MAX));
23572 p.sliding_layers = Some(vec![true, true]);
23573 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23574 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23575
23576 assert_eq!(
23577 p.graph_attn_decline_reason(),
23578 Some("learned attention sinks")
23579 );
23580 let swa_graph = p.metal_graph_swa();
23581 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23582 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23583 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23584 assert!(
23585 native_sink_attend,
23586 "the q1 sink admission must follow the SWA door"
23587 );
23588 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23591 }
23592
23593 #[test]
23594 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23595 let mut p = mimo_test_pipeline();
23596 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23597 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23598 for _ in 0..if li == 0 { 2 } else { 12 } {
23599 layer.append(&row, &row, &[]);
23600 }
23601 }
23602 p.mimo_verify_rewind(9).unwrap();
23603 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23604 for layer in &p.kv_cache.layers[1..] {
23605 assert_eq!(layer.seq_len, 9);
23606 }
23607 }
23608
23609 #[test]
23613 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23614 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23615 let _ = std::fs::remove_dir_all(&dir);
23616 let mut p = mimo_test_pipeline();
23617 let hs = p.hidden_size;
23618 let ids = [5u32, 9, 11, 2, 40];
23619 p.layer_dump = Some(dir.join("decode"));
23620 for (pos, &id) in ids.iter().enumerate() {
23621 let e = p.embed_single(id);
23622 let _ = p.forward_layers(&e, pos, None);
23623 }
23624 p.clear_sequence_state();
23625 p.layer_dump = Some(dir.join("prefill"));
23626 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23627 for pos in 0..ids.len() {
23628 for li in 0..p.num_layers {
23629 let name = format!("p{pos:06}_l{li:02}.f32");
23630 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23631 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23632 assert_eq!(a.len(), hs * 4, "{name}");
23633 assert_eq!(a, b, "{name}");
23634 }
23635 }
23636 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23637 let vals: Vec<f32> = last
23638 .chunks(4)
23639 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23640 .collect();
23641 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23642 let _ = std::fs::remove_dir_all(&dir);
23643 }
23644
23645 #[test]
23646 fn attn_geometry_and_sinks_are_validated() {
23647 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23648 assert!(
23649 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23650 "one entry per layer"
23651 );
23652 assert!(
23653 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23654 "3 does not divide 4"
23655 );
23656 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23657 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23658 assert!(
23659 p.set_attn_geometry(None, Some(5)).is_err(),
23660 "V wider than the head"
23661 );
23662 p.set_attn_geometry(None, Some(4)).unwrap();
23663 assert_eq!(
23664 p.v_head_dim, None,
23665 "v_head_dim == head_dim is the uniform case"
23666 );
23667 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23668 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23669 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23670 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23671 assert!(
23672 p.kv_cache.layers[1].sinks.is_some(),
23673 "a reshape keeps the layer's sinks"
23674 );
23675 assert_eq!(p.layer_geom(1).0, 4);
23676 assert!(
23677 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23678 "one sink per Q head"
23679 );
23680 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23681 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23682 }
23683
23684 #[test]
23687 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23688 let cfg = || {
23689 Some(crate::nystrom::O1Cfg {
23690 layers: crate::nystrom::O1Layers::All,
23691 m: 4,
23692 w: 8,
23693 sink: 2,
23694 rect: crate::nystrom::O1Rect::Aggregate,
23695 })
23696 };
23697 let mut p = mimo_test_pipeline();
23698 p.set_o1(cfg());
23699 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23700 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23701 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23702 q.sliding_layers = Some(vec![false, false, true]);
23703 q.swa = Some((4, usize::MAX));
23704 q.set_o1(cfg());
23705 assert_eq!(q.o1_flags, vec![true, false, false]);
23706 }
23707
23708 #[test]
23709 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23710 const B: usize = 19;
23711 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23712 p.set_o1(Some(crate::nystrom::O1Cfg {
23713 layers: crate::nystrom::O1Layers::All,
23714 m: 4,
23715 w: 8,
23716 sink: 2,
23717 rect: crate::nystrom::O1Rect::Aggregate,
23718 }));
23719 p.o1_begin_with_prefix(Some(B));
23720 let ids: Vec<u32> = (0..B as u32).collect();
23721 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23722
23723 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23724 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23725 let next = p.embed_single(B as u32);
23726 let _ = p.forward_layers(&next, B, None);
23727 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23728 }
23729
23730 #[test]
23731 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23732 const B: usize = 19;
23733 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23734 let gdn_cfg = crate::linear_core::GdnCfg {
23738 num_v_heads: 2,
23739 num_k_heads: 1,
23740 key_head_dim: 2,
23741 value_head_dim: 4,
23742 conv_kernel: 3,
23743 hidden_size: 8,
23744 rms_eps: 1e-6,
23745 output_gate_sigmoid: false,
23746 };
23747 let synth = |n: usize, salt: usize| -> Vec<f32> {
23748 (0..n)
23749 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23750 .collect()
23751 };
23752 let qt = |rows: usize, cols: usize, salt: usize| {
23753 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23754 };
23755 let c_dim = gdn_cfg.conv_dim();
23756 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23757 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23758 in_proj_qkv: qt(c_dim, 8, 1),
23759 in_proj_z: qt(vd, 8, 2),
23760 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23761 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23762 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23763 a_log: vec![0.2, 0.5],
23764 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23765 norm: vec![1.0; gdn_cfg.value_head_dim],
23766 out_proj: qt(8, vd, 7),
23767 });
23768 p.gdn_cfg = Some(gdn_cfg);
23769 p.set_o1(Some(crate::nystrom::O1Cfg {
23770 layers: crate::nystrom::O1Layers::All,
23771 m: 4,
23772 w: 8,
23773 sink: 2,
23774 rect: crate::nystrom::O1Rect::Aggregate,
23775 }));
23776 p.o1_begin_with_prefix(Some(B));
23777 for pos in 0..B - 2 {
23778 let emb = p.embed_single(pos as u32);
23779 let _ = p.forward_layers(&emb, pos, None);
23780 }
23781 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23782
23783 let e1 = p.embed_single((B - 2) as u32);
23784 let e2 = p.embed_single((B - 1) as u32);
23785 let _ = p.forward_pair(&e1, &e2, B - 2);
23786
23787 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23788 assert!(
23789 p.kv_cache
23790 .layers
23791 .iter()
23792 .enumerate()
23793 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23794 );
23795 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23796 assert_ne!(
23797 p.kv_cache.layers[0].linear_state, lane1_state,
23798 "real pair must commit GDN lane 2 before returning"
23799 );
23800 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23801 let next = p.embed_single(B as u32);
23802 let _ = p.forward_layers(&next, B, None);
23803 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23804 }
23805
23806 #[test]
23807 fn o1_error_observation_stays_terminal_until_reset() {
23808 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23809 p.set_o1(Some(crate::nystrom::O1Cfg {
23810 layers: crate::nystrom::O1Layers::All,
23811 m: 4,
23812 w: 8,
23813 sink: 2,
23814 rect: crate::nystrom::O1Rect::Aggregate,
23815 }));
23816 p.o1_begin();
23817 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23818
23819 assert!(p.o1_seal_checked().is_err());
23820 assert!(
23821 p.o1_seal_checked().is_err(),
23822 "retry must see the sticky error"
23823 );
23824 let k = vec![0.2f32; 4];
23825 let v = vec![0.3f32; 4];
23826 p.kv_cache.layers[0].append(&k, &v, &[]);
23827 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23828
23829 p.reset_session();
23830 p.o1_begin();
23831 p.kv_cache.layers[0].append(&k, &v, &[]);
23832 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23833 }
23834
23835 #[test]
23836 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23837 let ids = vec![1u32, 2, 3, 4, 5, 6];
23838 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23839 p.graph_logits = Some(vec![123.0]);
23840 p.graph_want_logits = true;
23841 p.graph_failed
23842 .store(true, std::sync::atomic::Ordering::Relaxed);
23843 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23844 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23845 assert!(err.contains("before NLL"));
23846 assert!(p.graph_logits.is_none());
23847 assert!(!p.graph_want_logits);
23848 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23849 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23850
23851 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23852 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23853 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23854 assert_eq!(actual.1, expected.1);
23855 assert!((actual.0 - expected.0).abs() < 1e-9);
23856 }
23857
23858 #[test]
23859 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23860 let ids = vec![1u32, 2, 3, 4, 5, 6];
23861 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23862 p.nll_test_fail_at = Some(1);
23863 let err = p
23864 .nll_ids_from(&ids, 0)
23865 .expect_err("one-shot forward failure");
23866 assert!(err.contains("forward") || err.contains("score row"));
23867 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23868 assert!(!p.graph_want_logits);
23869 assert!(p.graph_logits.is_none());
23870 assert!(p.kv_history.is_empty());
23871
23872 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23873 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23874 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23875 assert_eq!(actual.1, expected.1);
23876 assert!((actual.0 - expected.0).abs() < 1e-9);
23877 }
23878
23879 #[test]
23880 fn nll_serial_failure_before_first_row_is_reported() {
23881 let ids = vec![1u32, 2, 3, 4];
23882 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23883 p.nll_test_force_serial = true;
23884 p.nll_test_fail_at = Some(0);
23885 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23886 assert!(err.contains("serial forward"));
23887 assert!(p.kv_history.is_empty());
23888 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23889 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23890 }
23891
23892 #[test]
23893 fn ffn_probe_failure_discards_recorder_and_state() {
23894 let ids = vec![1u32, 2, 3, 4];
23895 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23896 p.nll_test_fail_at = Some(0);
23897 let err = p
23898 .probe_ffn_mass_batch(&ids)
23899 .expect_err("probe forward failure");
23900 assert!(err.contains("NLL"));
23901 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23902 assert!(p.kv_history.is_empty());
23903 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23904 }
23905
23906 #[test]
23907 fn nll_test_controls_are_pipeline_scoped() {
23908 let ids = vec![1u32, 2, 3, 4];
23909 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23910 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23911 failing.nll_test_force_serial = true;
23912 failing.nll_test_fail_at = Some(0);
23913
23914 assert!(!failing.can_prefill_batched());
23915 assert!(unaffected.can_prefill_batched());
23916 let expected = unaffected
23917 .nll_ids_from(&ids, 0)
23918 .expect("unaffected pipeline remains usable");
23919 let err = failing
23920 .nll_ids_from(&ids, 0)
23921 .expect_err("failure injection belongs to failing pipeline");
23922 assert!(err.contains("serial forward"));
23923 assert!(failing.nll_test_fail_at.is_none());
23924 assert!(unaffected.can_prefill_batched());
23925 let actual = unaffected
23926 .nll_ids_from(&ids, 0)
23927 .expect("unaffected pipeline remains reusable");
23928 assert_eq!(actual.1, expected.1);
23929 assert!((actual.0 - expected.0).abs() < 1e-9);
23930 }
23931
23932 #[test]
23933 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23934 let ids = vec![1u32, 2, 3, 4, 5, 6];
23935 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23936 p.graph_logits = Some(vec![123.0]);
23937 p.graph_want_logits = true;
23938 p.graph_failed
23939 .store(true, std::sync::atomic::Ordering::Relaxed);
23940 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23941
23942 let err = p
23943 .forward_ids(&ids, None)
23944 .expect_err("a failed forward must not become a valid head result");
23945 assert!(err.contains("forward_ids setup"));
23946 assert!(p.graph_logits.is_none());
23947 assert!(!p.graph_want_logits);
23948 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23949 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23950 assert_eq!(p.kv_cache.seq_len(), 0);
23951
23952 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23953 .forward_ids(&ids, None)
23954 .expect("fresh forward_ids");
23955 let actual = p
23956 .forward_ids(&ids, None)
23957 .expect("pipeline remains reusable after a failed forward");
23958 assert_eq!(actual.len(), expected.len());
23959 assert!(
23960 actual
23961 .iter()
23962 .zip(expected)
23963 .all(|(a, b)| (a - b).abs() < 1e-9)
23964 );
23965 assert_eq!(p.kv_cache.seq_len(), ids.len());
23966 }
23967
23968 #[test]
23969 fn sigmoid_router_floor_is_explicit_per_architecture() {
23970 let zero = || DenseFfn {
23976 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23977 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23978 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23979 act: Act::Silu,
23980 down_t: None,
23981 segs: Vec::new(),
23982 };
23983 let m = MoeFfn {
23984 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23985 experts: vec![zero(), zero()],
23986 top_k: 1,
23987 norm_topk_prob: true,
23988 router_sigmoid: true,
23989 sigmoid_denom_eps: 1e-20,
23990 expert_bias: None,
23991 routed_scaling: 2.5,
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 let logits = [-20.0f32, -20.0];
24004 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
24005 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24006 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
24007 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
24008 assert!((flint_wsum - reference_expected).abs() < 1e-15);
24009 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
24010 assert!(legacy_wsum > minimax_wsum * 100.0);
24011 }
24012
24013 #[test]
24014 fn resonance_scores_match_formula_and_stable_tie() {
24015 let r = Resonance {
24016 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
24018 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
24019 k: 1,
24020 bias: vec![1.5, 0.5, 0.0],
24021 shell: Vec::new(),
24022 };
24023 let x = [1.0f32, 1.0];
24024 let mut got = vec![0.0; 3];
24025 r.scores(&x, &mut got);
24026 assert!((got[0] - 0.5).abs() < 1e-6);
24030 assert!((got[1] - 0.5).abs() < 1e-6);
24031 assert!(got[2].abs() < 1e-6);
24032 let best = got
24033 .iter()
24034 .enumerate()
24035 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
24036 .map(|(i, _)| i);
24037 assert_eq!(best, Some(0));
24038 assert!(got.iter().all(|v| v.is_finite()));
24039 }
24040
24041 #[test]
24046 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
24047 let plain = Resonance {
24053 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
24054 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
24055 k: 1,
24056 bias: vec![1.5, 0.5, 0.0, 0.0],
24057 shell: Vec::new(),
24058 };
24059 let shelled = Resonance {
24060 mu: plain.mu.clone(),
24061 u: plain.u.clone(),
24062 k: 1,
24063 bias: plain.bias.clone(),
24064 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
24065 };
24066 assert!(!plain.has_shell());
24067 assert!(shelled.has_shell());
24068 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
24069 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
24070 set_growth_shell(Some(true));
24071 assert!(growth_shell_enabled());
24072 let x = [1.0f32, 1.0];
24075 plain.scores(&x, &mut a);
24076 shelled.scores(&x, &mut b);
24077 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
24078 assert!(a[2] == 0.0 && a[3] == 0.0);
24079 let xo = [3.0f32, 0.0];
24082 plain.scores(&xo, &mut a);
24083 shelled.scores(&xo, &mut b);
24084 assert_eq!(a[2], -6.0);
24085 assert_eq!(a[3], -6.0);
24086 assert_eq!(
24087 bits(&a[..3]),
24088 bits(&b[..3]),
24089 "trunk rows + the boundary row"
24090 );
24091 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
24092 assert_eq!(shelled.effective_shell(4), shelled.shell);
24093 set_growth_shell(Some(false));
24096 assert!(!growth_shell_enabled());
24097 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
24098 shelled.scores(&xo, &mut b);
24099 assert_eq!(bits(&a), bits(&b));
24100 set_growth_shell(None);
24101 let short = Resonance {
24104 shell: vec![f32::INFINITY, f32::INFINITY],
24105 ..shelled
24106 };
24107 set_growth_shell(Some(true));
24108 short.scores(&xo, &mut b);
24109 assert_eq!(bits(&a), bits(&b));
24110 set_growth_shell(None);
24111 }
24112
24113 #[test]
24117 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
24118 let zero = || DenseFfn {
24119 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24120 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24121 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24122 act: Act::Silu,
24123 down_t: None,
24124 segs: Vec::new(),
24125 };
24126 let moe = |sigmoid: bool| MoeFfn {
24127 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24128 experts: vec![zero(), zero(), zero(), zero()],
24129 top_k: 1,
24130 norm_topk_prob: true,
24131 router_sigmoid: sigmoid,
24132 sigmoid_denom_eps: 1e-6,
24133 expert_bias: None,
24134 routed_scaling: 1.0,
24135 route_tau: None,
24136 shared: None,
24137 stats: std::cell::RefCell::new(Vec::new()),
24138 act_sq: std::cell::RefCell::new(Vec::new()),
24139 act_rows: std::cell::RefCell::new(Vec::new()),
24140 mask: None,
24141 per_expert_scale: None,
24142 router_input_norm: false,
24143 resonance: None,
24144 grown: Vec::new(),
24145 };
24146 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24147 for sigmoid in [false, true] {
24148 let m = moe(sigmoid);
24149 let (idx, p, wsum) = moe_route(&logits, &m, None);
24150 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24151 assert_eq!(p[1], 0.0);
24152 assert_eq!(p[3], 0.0);
24153 assert!(p[2] > p[0] && p[0] > 0.0);
24154 assert!(p.iter().all(|v| v.is_finite()));
24155 let w = p[2] / wsum;
24156 if sigmoid {
24157 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24159 } else {
24160 assert_eq!(
24161 w, 1.0,
24162 "softmax: the renormalized top-1 weight is exactly 1"
24163 );
24164 }
24165 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24168 assert_eq!(idx, vec![2]);
24169 }
24170 let m = moe(false);
24172 let (idx, _, _) = moe_route(
24173 &[
24174 f32::NEG_INFINITY,
24175 f32::NEG_INFINITY,
24176 f32::NEG_INFINITY,
24177 -9.0,
24178 ],
24179 &m,
24180 None,
24181 );
24182 assert_eq!(idx, vec![3]);
24183 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
24186 assert_eq!(idx, vec![0]);
24187 assert!(p.iter().all(|&v| v == 0.25));
24188 assert!(wsum.is_finite() && wsum > 0.0);
24189 }
24190
24191 #[cfg(feature = "gpu")]
24197 #[test]
24198 fn minimax_device_route_matches_host_sigmoid_contract() {
24199 match crate::gpu_wgpu::selected_and_up() {
24200 None => {
24201 crate::gpu_wgpu::skip_or_fail(module_path!());
24202 return;
24203 }
24204 Some(false) => {
24212 eprintln!(
24213 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24214 module_path!()
24215 );
24216 return;
24217 }
24218 Some(true) => {}
24219 }
24220 let n = 128usize;
24221 let top_k = 4usize;
24222 let logits: Vec<f32> = (0..n)
24223 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24224 .collect();
24225 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24226 let m = MoeFfn {
24227 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24228 experts: Vec::new(),
24229 top_k,
24230 norm_topk_prob: true,
24231 router_sigmoid: true,
24232 sigmoid_denom_eps: 1e-20,
24233 expert_bias: Some(bias.clone()),
24234 routed_scaling: 2.0,
24235 route_tau: None,
24236 shared: None,
24237 stats: std::cell::RefCell::new(Vec::new()),
24238 act_sq: std::cell::RefCell::new(Vec::new()),
24239 act_rows: std::cell::RefCell::new(Vec::new()),
24240 mask: None,
24241 per_expert_scale: None,
24242 router_input_norm: false,
24243 resonance: None,
24244 grown: Vec::new(),
24245 };
24246 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24247 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24248
24249 for shared in [Some(n as u32), None] {
24250 let mut gi = Vec::new();
24251 let mut gw = Vec::new();
24252 assert!(
24253 crate::gpu_wgpu::minimax_moe_route_for_test(
24254 &logits,
24255 Some(&bias),
24256 top_k,
24257 2.0,
24258 shared,
24259 &mut gi,
24260 &mut gw,
24261 ),
24262 "the device did not come up — the test checked nothing"
24263 );
24264 let routed = if shared.is_some() { top_k } else { gi.len() };
24265 assert_eq!(
24266 &gi[..routed],
24267 &ci[..],
24268 "different experts or order (shared={shared:?})"
24269 );
24270 let d = cw
24271 .iter()
24272 .zip(&gw[..routed])
24273 .map(|(a, b)| (a - b).abs())
24274 .fold(0.0f32, f32::max);
24275 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24276 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24277 if let Some(s) = shared {
24278 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24279 assert_eq!(gw[top_k], 1.0, "shared weight");
24280 }
24281 }
24282 }
24283
24284 #[cfg(feature = "gpu")]
24289 #[test]
24290 fn minimax_device_cold_weights_match_host_contract() {
24291 match crate::gpu_wgpu::selected_and_up() {
24292 None => {
24293 crate::gpu_wgpu::skip_or_fail(module_path!());
24294 return;
24295 }
24296 Some(false) => {
24304 eprintln!(
24305 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24306 module_path!()
24307 );
24308 return;
24309 }
24310 Some(true) => {}
24311 }
24312 let n = 128usize;
24313 let top_k = 4usize;
24314 let logits: Vec<f32> = (0..n)
24315 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24316 .collect();
24317 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24318 let m = MoeFfn {
24319 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24320 experts: Vec::new(),
24321 top_k,
24322 norm_topk_prob: true,
24323 router_sigmoid: true,
24324 sigmoid_denom_eps: 1e-20,
24325 expert_bias: Some(bias.clone()),
24326 routed_scaling: 2.0,
24327 route_tau: None,
24328 shared: None,
24329 stats: std::cell::RefCell::new(Vec::new()),
24330 act_sq: std::cell::RefCell::new(Vec::new()),
24331 act_rows: std::cell::RefCell::new(Vec::new()),
24332 mask: None,
24333 per_expert_scale: None,
24334 router_input_norm: false,
24335 resonance: None,
24336 grown: Vec::new(),
24337 };
24338 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24339 assert_eq!(ci.len(), top_k);
24340
24341 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24343 let mut cold = Vec::new();
24344 let mut idx = Vec::new();
24345 let mut dw = Vec::new();
24346 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24347 &logits,
24348 Some(&bias),
24349 top_k,
24350 2.0,
24351 &resident,
24352 n as u32,
24353 &mut cold,
24354 &mut idx,
24355 &mut dw,
24356 ));
24357 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24358 for (r, &e) in ci.iter().enumerate() {
24363 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24364 let host = p[e] / wsum;
24365 let d = (dw[r] - host).abs();
24366 assert!(
24367 d <= 1e-6 * host.abs().max(1.0),
24368 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24369 dw[r]
24370 );
24371 }
24372 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24373 assert_eq!(dw[top_k], 1.0, "shared weight");
24374
24375 let mut remap = resident.clone();
24377 remap[ci[0]] = u32::MAX;
24378 remap[ci[2]] = u32::MAX;
24379 let mut cold = Vec::new();
24380 let mut idx2 = Vec::new();
24381 let mut dw2 = Vec::new();
24382 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24383 &logits,
24384 Some(&bias),
24385 top_k,
24386 2.0,
24387 &remap,
24388 n as u32,
24389 &mut cold,
24390 &mut idx2,
24391 &mut dw2,
24392 ));
24393 let mut resident_weight_checked = 0usize;
24401 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24402 if dw2[r] == 0.0 {
24403 continue; }
24405 let e = slot as usize;
24406 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24407 let host = p[e] / wsum;
24408 let d = (dw2[r] - host).abs();
24409 assert!(
24410 d <= 1e-6 * host.abs().max(1.0),
24411 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24412 );
24413 resident_weight_checked += 1;
24414 }
24415 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24416 println!("partial bank: checked {resident_weight_checked} resident weights");
24417 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24418 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24419 ids.sort_unstable();
24420 let mut want = vec![ci[0], ci[2]];
24421 want.sort_unstable();
24422 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24423 for &(e, w) in &cold {
24424 let host = p[e] / wsum;
24425 let d = (w - host).abs();
24426 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24427 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24428 }
24429 }
24430
24431 #[test]
24436 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24437 let zero = || DenseFfn {
24438 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24439 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24440 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24441 act: Act::Silu,
24442 down_t: None,
24443 segs: Vec::new(),
24444 };
24445 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24446 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24447 experts: vec![zero(), zero(), zero()],
24448 top_k: 1,
24449 norm_topk_prob: norm_topk,
24450 router_sigmoid: false,
24451 sigmoid_denom_eps: 1e-6,
24452 expert_bias: None,
24453 routed_scaling: 1.0,
24454 route_tau: None,
24455 shared: None,
24456 stats: std::cell::RefCell::new(Vec::new()),
24457 act_sq: std::cell::RefCell::new(Vec::new()),
24458 act_rows: std::cell::RefCell::new(Vec::new()),
24459 mask: None,
24460 per_expert_scale: None,
24461 router_input_norm: false,
24462 resonance: resonant.then(|| Resonance {
24463 mu: vec![0.0; 6],
24464 u: Vec::new(),
24465 k: 0,
24466 bias: vec![0.0; 3],
24467 shell: Vec::new(),
24468 }),
24469 grown: Vec::new(),
24470 };
24471 let lo = -0.1f32;
24474 let hi = f32::from_bits(lo.to_bits() - 1);
24475 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24476 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24477 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24479 assert_eq!(
24480 idx,
24481 vec![0],
24482 "softmax tie → lower index (the gated contract)"
24483 );
24484 for norm in [true, false] {
24487 let m = moe(true, norm);
24488 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24489 assert_eq!(idx, vec![1], "norm_topk {norm}");
24490 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24491 assert_eq!(p[1] / wsum, 1.0);
24492 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24495 assert_eq!(idx, vec![0]);
24496 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24498 assert_eq!(idx, vec![2]);
24499 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24500 assert_eq!(idx, vec![0]);
24501 assert_eq!(p[0] / wsum, 1.0);
24502 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24505 assert_eq!(idx, vec![0]);
24506 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24507 }
24508 }
24509}