1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18use crate::kv_cache::KvCache;
19use crate::linear_core::{
20 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
21 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
22 vmf_phase_pair,
23};
24use crate::pool::Pool;
25use crate::qtensor::QTensor;
26use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
27use crate::tokenizer::Tokenizer;
28use cortiq_core::mask::TaskMask;
29use cortiq_core::types::NormStyle;
30
31pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
32 std::sync::atomic::AtomicBool::new(false);
33
34thread_local! {
39 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
40}
41
42struct ExactMtpCalibrationGuard;
43
44impl ExactMtpCalibrationGuard {
45 fn enter() -> Self {
46 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
47 Self
48 }
49}
50
51impl Drop for ExactMtpCalibrationGuard {
52 fn drop(&mut self) {
53 EXACT_MTP_CALIBRATION.with(|depth| {
54 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
55 depth.set(depth.get().saturating_sub(1));
56 });
57 }
58}
59
60fn exact_mtp_calibration_active() -> bool {
61 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
62}
63
64fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
68 logits.resize(vocab_size, f32::NEG_INFINITY);
69}
70
71struct ForwardScratch {
75 n1: Vec<f32>,
76 n2: Vec<f32>,
77 p1: Vec<f32>,
78 p2: Vec<f32>,
79}
80
81impl ForwardScratch {
82 fn new(hidden: usize) -> Self {
83 Self {
84 n1: vec![0.0; hidden],
85 n2: vec![0.0; hidden],
86 p1: vec![0.0; hidden],
87 p2: vec![0.0; hidden],
88 }
89 }
90}
91
92pub struct Pipeline {
94 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
99 pub tokenizer: std::sync::Arc<Tokenizer>,
102 pub kv_cache: KvCache,
103 pub sampler_config: SamplerConfig,
104 pub weights: PipelineWeights,
105 pub hidden_size: usize,
106 pub intermediate_size: usize,
107 pub num_heads: usize,
108 pub num_kv_heads: usize,
109 pub head_dim: usize,
110 pub num_layers: usize,
112 pub physical_layers: usize,
114 pub loop_final_norm: bool,
116 pub vocab_size: usize,
117 pub rms_eps: f64,
118 pub rope_base: f32,
119 pub norm_style: NormStyle,
120 pub rotary_dim: usize,
122 pub attention_heads_per_layer: Option<Vec<usize>>,
124 pub kv_heads_per_layer: Option<Vec<usize>>,
129 pub v_head_dim: Option<usize>,
134 pub layer_dump: Option<std::path::PathBuf>,
147 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
150 pub(crate) mimo_moe: crate::mimo_moe::Slot,
153 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
158 pub vmf_cfg: Option<VmfPhaseCfg>,
160 pub gdn_cfg: Option<GdnCfg>,
162 pub logit_multiplier: Option<f32>,
164 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
169 graph_failed: std::sync::atomic::AtomicBool,
174 pub kv_history: Vec<u32>,
179 pub kv_history_device: bool,
182 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
184 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
187 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
190 pub dsv4: Option<
194 Box<(
195 crate::dsv4::Dsv4Globals,
196 Vec<crate::dsv4::Dsv4Layer>,
197 crate::dsv4::Dsv4Cfg,
198 crate::dsv4::Dsv4State,
199 )>,
200 >,
201 pub dsv41: Option<
205 Box<(
206 crate::dsv41::Dsv41Globals,
207 Vec<crate::dsv41::Dsv41Layer>,
208 crate::dsv41::Dsv41Cfg,
209 crate::dsv41::Dsv41State,
210 )>,
211 >,
212 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
214 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
216 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
221 clover_stats: clover::CloverStats,
222 pub qwen4_exp: Option<
223 Box<(
224 crate::qwen4_exp::Globals,
225 Vec<crate::qwen4_exp::Layer>,
226 crate::qwen4_exp::Cfg,
227 crate::qwen4_exp::State,
228 )>,
229 >,
230 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
234 pub dspark: Option<crate::dsv4::DsparkState>,
236 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
239 pub dspark_hist: Vec<usize>,
241 pub dspark_real: Vec<u32>,
245 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
249 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
251 pub dspark_draft_ns: u128,
255 m3_winners: Vec<Vec<u32>>,
257 m3_counts: Vec<u32>,
259 m3_n_exp: usize,
260 m3_tokens: u64,
261 m3_prewarmed: bool,
262 pub short_conv_cfg: Option<ShortConvCfg>,
265 pub mtp: Option<MtpModule>,
267 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
271 verify_exact_moe: bool,
274 pub speculative: bool,
276 pub ignore_eos: bool,
282 pub draft_full_streak: u32,
288 pub spec_k_adapt: Option<usize>,
296 pub spec_acc_ewma: f32,
298 rng: SplitMix64,
299 sampler_scratch: SamplerScratch,
300 spec_forced: Option<u32>,
306 spec_q: Vec<Vec<f32>>,
307 spec_p: Vec<f32>,
308 spec_res: Vec<f32>,
309 spec_qs: Vec<sampler::Sparse>,
311 spec_ps: sampler::Sparse,
312 spec_ress: sampler::Sparse,
313 mtp_graph_mode: Option<bool>,
320 #[cfg(target_os = "macos")]
323 metal_verify: Option<MetalVerifyPending>,
324 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
328 ws: ForwardScratch,
332 pool: Option<std::sync::Arc<Pool>>,
334 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
338 pub(crate) dyn_force_f32: bool,
340 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
345 pub(crate) dyn_active: Option<usize>,
351 pub(crate) dyn_blend_loaded: bool,
355 pub(crate) dyn_phi_layer: Option<usize>,
358 dyn_phi_ema: Vec<f32>,
360 dyn_phi_seen: usize,
361 pub dyn_router: Option<crate::swarm::DynRouter>,
364 o1_cfg: Option<crate::nystrom::O1Cfg>,
367 o1_epoch: u64,
370 o1_flags: Vec<bool>,
372 trace: bool,
375 calib_temp: f32,
378 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
380 graph_kv_id: u64,
381 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
384 graph_want_logits: bool,
385 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
389 graph_head_required: bool,
390 graph_logits: Option<Vec<f32>>,
393 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
396 graph_refused: std::sync::atomic::AtomicBool,
402 pub embed_multiplier: f32,
404 pub attn_scale: f32,
407 pub swa: Option<(usize, usize)>,
410 pub swa_trim: Option<(usize, usize)>,
417 swa_trim_anchor: usize,
421 pub chunk_min_rows: usize,
425 pub sliding_layers: Option<Vec<bool>>,
428 pub no_rope_layers: Option<Vec<bool>>,
433 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
439 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
442 pub kv_prefix: KvPrefix,
446 pub last_prefill_tokens: usize,
449 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
452 pub rotary_dim_local: Option<usize>,
453 pub rope_scale: f32,
454 pub rope_scale_local: f32,
455 pub global_attn: Option<(usize, usize)>,
458 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
461 pub attn_v_norm: bool,
463 pub qk_norm_after_rope: bool,
465 pub proj_gate_sigmoid: bool,
468 pub final_softcap: Option<f32>,
470 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
474 pub attn_softcap: f32,
476 confidence_on: bool,
480 #[cfg(test)]
483 nll_test_fail_at: Option<usize>,
484 #[cfg(test)]
487 nll_test_force_serial: bool,
488}
489
490#[cfg(target_os = "macos")]
491impl Drop for Pipeline {
492 fn drop(&mut self) {
493 let _ = crate::gpu_metal::wait_replay();
495 crate::gpu::kv_mirror_drop(self.graph_kv_id);
496 }
497}
498
499#[cfg(not(target_os = "macos"))]
500impl Drop for Pipeline {
501 fn drop(&mut self) {
502 crate::gpu::graph_kv_reset(self.graph_kv_id);
506 }
507}
508
509pub struct PipelineWeights {
514 pub embed_tokens: QTensor,
516 pub layers: Vec<LayerWeights>,
518 pub lm_head: QTensor,
520 pub final_norm: Vec<f32>,
522}
523
524pub struct LayerWeights {
526 pub input_norm: Vec<f32>,
527 pub post_norm: Vec<f32>,
530 pub attn_out_norm: Option<Vec<f32>>,
533 pub layer_scale: Option<f32>,
535 pub ffn_out_norm: Option<Vec<f32>>,
538 pub ffn: FfnKind,
539 pub attn: AttnKind,
540}
541
542#[derive(Clone, Copy, PartialEq, Debug, Default)]
546pub enum Act {
547 #[default]
548 Silu,
549 GeluTanh,
550 Gelu,
552 Situ {
555 beta: f32,
556 linear_beta: f32,
557 },
558 SwiGluOai {
563 alpha: f32,
564 limit: f32,
565 },
566}
567
568impl Act {
569 pub fn from_arch(name: &str) -> Self {
570 if name == "gelu_tanh" {
571 Self::GeluTanh
572 } else if name == "gelu" {
573 Self::Gelu
574 } else {
575 Self::Silu
576 }
577 }
578
579 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
581 match arch.hidden_act.as_str() {
582 "situ" => Self::Situ {
583 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
584 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
585 },
586 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
591 alpha: 1.702,
592 limit: 7.0,
593 },
594 other => Self::from_arch(other),
595 }
596 }
597
598 #[inline]
603 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
604 Self::SwiGluOai { alpha, limit }
605 }
606
607 #[inline]
608 pub fn apply(self, x: f32) -> f32 {
609 match self {
610 Self::Silu => inference::silu(x),
611 Self::GeluTanh => inference::gelu_tanh(x),
612 Self::Gelu => inference::gelu_erf(x),
613 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
614 Self::SwiGluOai { alpha, limit } => {
615 let gate = x.min(limit);
616 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
617 }
618 }
619 }
620
621 #[inline]
624 pub fn combine(self, g: f32, u: f32) -> f32 {
625 match self {
626 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
627 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
628 }
629 Self::SwiGluOai { alpha, limit } => {
630 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
631 }
632 _ => self.apply(g) * u,
633 }
634 }
635
636 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
640 match self {
641 Self::Silu => Some(crate::gpu::GraphAct::Silu),
642 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
643 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
644 Self::Situ { .. } | Self::SwiGluOai { .. } => None,
645 }
646 }
647
648 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
654 self.moe_job_graph_contract().map(|(act, _)| act)
655 }
656
657 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
663 match self {
664 Self::SwiGluOai { alpha, limit }
665 if alpha.to_bits() == 1.702f32.to_bits()
666 && limit.to_bits() == 7.0f32.to_bits() =>
667 {
668 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
669 }
670 _ => self.graph_act().map(|act| (act, 0.0)),
671 }
672 }
673}
674
675pub struct DenseFfn {
677 pub gate_proj: QTensor,
678 pub up_proj: QTensor,
679 pub down_proj: QTensor,
680 pub act: Act,
682 pub down_t: Option<QTensor>,
688 pub segs: Vec<FfnSeg>,
695}
696
697pub struct FfnSeg {
702 pub gate: QTensor,
703 pub up: QTensor,
704 pub down: QTensor,
705 pub start: usize,
706 pub width: usize,
707}
708
709pub enum FfnKind {
712 Dense(DenseFfn),
713 Moe(MoeFfn),
717 DenseMoe(Box<DenseMoeFfn>),
724}
725
726pub struct DenseMoeFfn {
728 pub dense: DenseFfn,
729 pub moe: MoeFfn,
730 pub post_norm_1: Vec<f32>,
732 pub pre_norm_2: Vec<f32>,
735 pub post_norm_2: Vec<f32>,
737}
738
739pub struct MoeFfn {
740 pub router: QTensor,
742 pub experts: Vec<DenseFfn>,
743 pub top_k: usize,
744 pub norm_topk_prob: bool,
745 pub router_sigmoid: bool,
748 pub sigmoid_denom_eps: f32,
754 pub expert_bias: Option<Vec<f32>>,
758 pub routed_scaling: f32,
761 pub route_tau: Option<f32>,
767 pub shared: Option<(DenseFfn, Option<QTensor>)>,
770 pub stats: std::cell::RefCell<Vec<u64>>,
774 pub act_sq: std::cell::RefCell<Vec<f64>>,
781 pub act_rows: std::cell::RefCell<Vec<f32>>,
787 pub mask: Option<Vec<bool>>,
792 pub per_expert_scale: Option<Vec<f32>>,
795 pub router_input_norm: bool,
799 pub resonance: Option<Resonance>,
803 pub grown: Vec<GrownExpert>,
809}
810
811#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct GrownExpert {
814 pub record: String,
816 pub record_index: usize,
818 pub layer: usize,
819 pub expert: usize,
823}
824
825pub struct Resonance {
827 pub mu: Vec<f32>,
829 pub u: Vec<f32>,
831 pub k: usize,
832 pub bias: Vec<f32>,
834 pub shell: Vec<f32>,
840}
841
842static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
845
846pub fn growth_shell_enabled() -> bool {
851 use std::sync::atomic::Ordering;
852 match GROWTH_SHELL.load(Ordering::Relaxed) {
853 1 => true,
854 2 => false,
855 _ => {
856 let off = std::env::var("CMF_GROWTH_SHELL")
857 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
858 .unwrap_or(false);
859 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
860 !off
861 }
862 }
863}
864
865pub fn set_growth_shell(on: Option<bool>) {
870 GROWTH_SHELL.store(
871 match on {
872 Some(true) => 1,
873 Some(false) => 2,
874 None => 0,
875 },
876 std::sync::atomic::Ordering::Relaxed,
877 );
878}
879
880impl Resonance {
881 pub fn has_shell(&self) -> bool {
883 self.shell.iter().any(|s| s.is_finite())
884 }
885
886 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
889 let mut out = vec![f32::INFINITY; ne];
890 if growth_shell_enabled() {
891 for (o, s) in out.iter_mut().zip(&self.shell) {
892 *o = *s;
893 }
894 }
895 out
896 }
897
898 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
903 let h = x.len();
904 let ne = out.len();
905 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
906 for e in 0..ne {
907 let mu = &self.mu[e * h..(e + 1) * h];
908 let mut d2 = 0.0f32;
909 for j in 0..h {
910 let d = x[j] - mu[j];
911 d2 += d * d;
912 }
913 let mut proj = 0.0f32;
914 for i in 0..self.k {
915 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
916 let mut p = 0.0f32;
917 for j in 0..h {
918 p += (x[j] - mu[j]) * u[j];
919 }
920 proj += p * p;
921 }
922 let err = d2 - proj;
923 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
924 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
925 out[e] = f32::NEG_INFINITY;
926 }
927 }
928 }
929}
930
931pub enum AttnKind {
934 Full {
936 wq: QTensor,
937 wk: QTensor,
938 wv: QTensor,
939 wo: QTensor,
940 q_norm: Option<Vec<f32>>,
941 k_norm: Option<Vec<f32>>,
942 output_gate: bool,
943 softplus_gate: Option<(QTensor, bool)>,
947 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
949 },
950 Linear(VmfPhaseWeights),
952 LinearGdn(GdnWeights),
954 ShortConv(ShortConvWeights),
957 Mla(Box<MlaWeights>),
965 Kda(Box<crate::linear_core::KdaWeights>),
969 Bounded(Box<crate::bounded::BoundedWeights>),
974}
975
976pub struct MlaWeights {
978 pub q_proj: QTensor,
982 pub q_a: Option<QTensor>,
985 pub q_a_norm: Option<Vec<f32>>,
986 pub kv_a: QTensor,
988 pub kv_a_norm: Vec<f32>,
990 pub kv_b: QTensor,
992 pub o_proj: QTensor,
994 pub nh: usize,
995 pub qk_rope: usize,
996 pub qk_nope: usize,
997 pub v_dim: usize,
998 pub lora: usize,
999 pub scale: f32,
1001 pub nope: bool,
1003}
1004
1005pub struct MtpModule {
1010 pub enorm: Vec<f32>,
1011 pub hnorm: Vec<f32>,
1012 pub eh_proj: QTensor,
1014 pub layer: LayerWeights,
1015 pub final_norm: Vec<f32>,
1016 pub kv: crate::kv_cache::LayerKvCache,
1017}
1018
1019#[derive(Clone, Debug, PartialEq)]
1031pub struct MtpFfnProbe {
1032 pub mass: Vec<f64>,
1033 pub squared_mass: Vec<f64>,
1034 pub observed_positions: usize,
1035}
1036
1037#[cfg(target_os = "macos")]
1044enum MetalRowsItem<'a> {
1045 Gdn {
1046 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1047 first: usize,
1048 },
1049 Attn {
1050 l: crate::gpu_metal::AttnGpuLayer<'a>,
1051 li: usize,
1052 q_norm: Option<&'a [f32]>,
1053 k_norm: Option<&'a [f32]>,
1054 output_gate: bool,
1055 },
1056}
1057
1058#[cfg(target_os = "macos")]
1059struct MetalVerifyPending {
1060 graph: crate::gpu_metal::VerifyGraph,
1061 gdn_layers: Vec<usize>,
1062 attn_layers: Vec<(usize, usize)>,
1063}
1064
1065#[cfg(target_os = "macos")]
1069struct MetalWarmPending {
1070 graph: crate::gpu_metal::VerifyGraph,
1071 cpu_stored: usize,
1072 b: usize,
1073}
1074
1075#[cfg(target_os = "macos")]
1076enum MetalRowsRun {
1077 Declined,
1079 Failed,
1082 Completed(MetalVerifyPending),
1083}
1084
1085#[cfg(target_os = "macos")]
1086enum MetalPrefillOutcome {
1087 Declined,
1088 Failed,
1089 Completed(Vec<f32>),
1090}
1091
1092#[cfg(target_os = "macos")]
1093enum MetalBatchNllOutcome {
1094 Declined,
1095 Failed(String),
1096 Completed(f64, usize),
1097}
1098
1099#[derive(Clone, Copy)]
1105enum SpecTrial {
1106 Spec {
1107 t0: std::time::Instant,
1108 gen0: usize,
1109 rounds: usize,
1110 },
1111 Plain {
1112 t0: std::time::Instant,
1113 gen0: usize,
1114 },
1115 Decided {
1116 spec: bool,
1117 recheck_at: usize,
1118 },
1119}
1120
1121pub(crate) fn spec_time_level() -> u8 {
1125 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1126 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1127 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1128 Err(_) => 0,
1129 })
1130}
1131
1132fn graph_spec_wanted_by_policy(
1140 spec_env: Option<&str>,
1141 spec_default_ok: bool,
1142 penalized: bool,
1143 metal_wgpu: bool,
1144 native_metal: bool,
1145) -> bool {
1146 match spec_env {
1147 Some("0") => false,
1148 Some("1") => true,
1153 Some(_) => !native_metal,
1154 None => spec_default_ok && !penalized && !metal_wgpu && !native_metal,
1157 }
1158}
1159
1160struct SpecStampLog {
1166 t_last: std::time::Instant,
1167 items: Vec<(&'static str, f32)>,
1168}
1169
1170static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1171
1172pub(crate) fn spec_stamp(name: &'static str) {
1173 if spec_time_level() == 0 {
1174 return;
1175 }
1176 if let Ok(mut g) = SPEC_STAMPS.lock() {
1177 if let Some(log) = g.as_mut() {
1178 let now = std::time::Instant::now();
1179 log.items
1180 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1181 log.t_last = now;
1182 }
1183 }
1184}
1185
1186fn spec_stamps_begin() {
1187 if spec_time_level() == 0 {
1188 return;
1189 }
1190 if let Ok(mut g) = SPEC_STAMPS.lock() {
1191 *g = Some(SpecStampLog {
1192 t_last: std::time::Instant::now(),
1193 items: Vec::with_capacity(64),
1194 });
1195 }
1196}
1197
1198fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1199 SPEC_STAMPS
1200 .lock()
1201 .ok()
1202 .and_then(|mut g| g.take())
1203 .map(|l| l.items)
1204 .unwrap_or_default()
1205}
1206
1207fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1210 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1211 for &(n, ms) in items {
1212 match agg.iter_mut().find(|e| e.0 == n) {
1213 Some(e) => {
1214 e.1 += ms;
1215 e.2 += 1;
1216 }
1217 None => agg.push((n, ms, 1)),
1218 }
1219 }
1220 let mut s = String::with_capacity(agg.len() * 16);
1221 for (n, ms, k) in agg {
1222 if k > 1 {
1223 s.push_str(&format!("{n} {ms:.1}/{k} "));
1224 } else {
1225 s.push_str(&format!("{n} {ms:.1} "));
1226 }
1227 }
1228 s
1229}
1230
1231#[derive(Default, Clone, Copy)]
1253struct SpecMon {
1254 round_ms: f64,
1255 tokens: f64,
1256 plain_ms: f64,
1257 n: u32,
1258 fails: u32,
1259 zero_accept_rounds: u8,
1264 metal: bool,
1265}
1266
1267const SPEC_PROXY_TOKENS: f64 = 3.5;
1270const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1273
1274impl SpecMon {
1275 fn round(&mut self, dt_ms: f64, produced: usize) {
1276 if produced <= 1 {
1277 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1278 } else {
1279 self.zero_accept_rounds = 0;
1280 }
1281 self.n += 1;
1282 if self.n == 1 {
1283 return; }
1285 let a = if self.n == 2 { 1.0 } else { 0.3 };
1286 self.round_ms += a * (dt_ms - self.round_ms);
1287 self.tokens += a * (produced as f64 - self.tokens);
1288 }
1289 fn pays(&self) -> bool {
1290 if self.plain_ms > 0.0 {
1291 self.tokens * self.plain_ms > self.round_ms * 1.03
1292 } else {
1293 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1294 }
1295 }
1296 fn repeated_zero_acceptance(&self) -> bool {
1297 self.zero_accept_rounds >= 2
1298 }
1299 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1301 let n = generated.saturating_sub(gen0);
1302 if n >= 8 {
1303 return true;
1304 }
1305 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1306 }
1307}
1308
1309pub const KV_PREFIX_TAIL: usize = 128;
1312
1313#[derive(Debug, Clone, Default)]
1319pub struct KvPrefix {
1320 len: usize,
1321 hash: u64,
1322 tail: Vec<u32>,
1323 device: bool,
1328}
1329
1330impl KvPrefix {
1331 #[inline]
1332 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1333 for &id in ids {
1334 h ^= id as u64;
1335 h = h.wrapping_mul(0x100000001b3);
1336 h ^= h >> 29;
1337 }
1338 h
1339 }
1340
1341 pub fn clear(&mut self) {
1342 self.len = 0;
1343 self.hash = 0xcbf29ce484222325;
1344 self.tail.clear();
1345 self.device = false;
1346 }
1347
1348 pub fn on_device(&self) -> bool {
1350 self.device
1351 }
1352
1353 pub fn set_on_device(&mut self, device: bool) {
1355 self.device = device;
1356 }
1357
1358 pub fn len(&self) -> usize {
1360 self.len
1361 }
1362
1363 pub fn is_empty(&self) -> bool {
1364 self.len == 0
1365 }
1366
1367 pub fn tail_len(&self) -> usize {
1369 self.tail.len()
1370 }
1371
1372 pub fn set(&mut self, ids: &[u32]) {
1374 self.clear();
1375 self.extend(ids);
1376 }
1377
1378 pub fn extend(&mut self, more: &[u32]) {
1380 if self.len == 0 && self.hash == 0 {
1381 self.hash = 0xcbf29ce484222325;
1382 }
1383 self.hash = Self::fold(self.hash, more);
1384 self.len += more.len();
1385 if more.len() >= KV_PREFIX_TAIL {
1386 self.tail.clear();
1387 self.tail
1388 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1389 } else {
1390 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1391 self.tail.drain(..drop);
1392 self.tail.extend_from_slice(more);
1393 }
1394 }
1395
1396 pub fn extension(&self, ids: &[u32]) -> usize {
1400 if self.len == 0 || ids.len() <= self.len {
1401 return 0;
1402 }
1403 let t = self.tail.len();
1404 if ids[self.len - t..self.len] != self.tail[..] {
1405 return 0;
1406 }
1407 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1408 return 0;
1409 }
1410 self.len
1411 }
1412}
1413
1414pub struct GenerateResult {
1416 pub text: String,
1417 pub token_ids: Vec<u32>,
1418 pub prompt_tokens: usize,
1419 pub tokens_generated: usize,
1420 pub finish_reason: String,
1421 pub mtp_drafted: usize,
1423 pub mtp_accepted: usize,
1424 pub token_confidence: Vec<f32>,
1429 pub traces: Vec<TokenTrace>,
1432}
1433
1434#[derive(Clone, Debug)]
1439pub struct TokenTrace {
1440 pub t: usize,
1442 pub token_id: u32,
1444 pub confidence: f32,
1446 pub active_skill: Option<String>,
1448 pub recon: Option<f32>,
1452 pub switched: bool,
1455}
1456
1457#[cfg_attr(not(test), allow(dead_code))]
1462fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1463 let t = if temp > 1e-3 { temp } else { 1.0 };
1464 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1465 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1466 if sum > 0.0 {
1467 (((logits[id as usize] - max) / t).exp()) / sum
1468 } else {
1469 0.0
1470 }
1471}
1472
1473fn prefill_batched() -> bool {
1476 std::env::var("CMF_PREFILL")
1477 .map(|v| v != "seq")
1478 .unwrap_or(true)
1479}
1480
1481#[inline]
1485fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
1486 let graph_quality = unmasked && prefer_graph;
1487 let fused_head_quality = graph_quality && native_metal;
1488 (graph_quality, fused_head_quality)
1489}
1490
1491#[derive(Clone, Copy)]
1495enum PrefillIn<'a> {
1496 Ids(&'a [u32]),
1497 Hidden(&'a [f32]),
1498}
1499
1500impl Pipeline {
1507 pub(crate) fn can_prefill_batched(&self) -> bool {
1508 #[cfg(test)]
1509 let force_serial = self.nll_test_force_serial;
1510 #[cfg(not(test))]
1511 let force_serial = false;
1512 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1513 }
1514
1515 fn automatic_gpu_prefix(&self) -> Option<usize> {
1518 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1519 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1520 }
1521
1522 pub fn prefill_chunk(&self) -> usize {
1526 let env = env_prefill_chunk();
1527 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1528 return prefill_chunk_rule(env, ChunkHost::here(), false);
1529 }
1530 prefill_chunk_rule(
1531 None,
1532 ChunkHost::Other,
1533 self.chunk_stack_facts().dense_on_discrete(),
1534 )
1535 }
1536
1537 fn per_token_stack(&self) -> bool {
1541 self.g3n.is_some() || self.g4e.is_some()
1542 }
1543
1544 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1545 let plain_dense = !self.weights.layers.is_empty()
1546 && !self.per_token_stack()
1547 && self.dsv4.is_none()
1548 && self.dsv41.is_none()
1549 && self.qwen4_exp.is_none()
1550 && self.weights.layers.iter().all(|lw| {
1551 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1552 });
1553 let gpu_on = crate::gpu::enabled();
1554 ChunkStackFacts {
1555 plain_dense,
1556 discrete: gpu_on && crate::gpu::discrete(),
1557 gpu_on,
1558 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1561 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1562 multi_gpu: self.gpu_plan.is_some(),
1563 o1: self.o1_active(),
1564 }
1565 }
1566}
1567
1568pub fn prefill_chunk() -> usize {
1577 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1578}
1579
1580fn env_prefill_chunk() -> Option<usize> {
1581 std::env::var("CMF_PREFILL_CHUNK")
1582 .ok()
1583 .and_then(|v| v.parse::<usize>().ok())
1584}
1585
1586#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1588enum ChunkHost {
1589 Macos,
1590 Aarch64,
1592 Other,
1594}
1595
1596impl ChunkHost {
1597 fn here() -> Self {
1598 if cfg!(target_os = "macos") {
1599 ChunkHost::Macos
1600 } else if cfg!(target_arch = "aarch64") {
1601 ChunkHost::Aarch64
1602 } else {
1603 ChunkHost::Other
1604 }
1605 }
1606}
1607
1608const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1615
1616fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1622 if let Some(n) = env {
1623 return n.max(1);
1624 }
1625 match host {
1626 ChunkHost::Macos => 512,
1627 ChunkHost::Aarch64 => 256,
1630 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1631 ChunkHost::Other => 48,
1632 }
1633}
1634
1635#[derive(Clone, Copy, Debug, Default)]
1637struct ChunkStackFacts {
1638 plain_dense: bool,
1641 discrete: bool,
1643 gpu_on: bool,
1645 capacity_split: bool,
1647 multi_gpu: bool,
1649 o1: bool,
1651}
1652
1653impl ChunkStackFacts {
1654 fn dense_on_discrete(self) -> bool {
1655 self.plain_dense
1656 && self.discrete
1657 && self.gpu_on
1658 && !self.capacity_split
1659 && !self.multi_gpu
1660 && !self.o1
1661 }
1662}
1663
1664#[inline]
1670fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1671 if end <= start || start >= input_len {
1672 return 0;
1673 }
1674 let rows = (end.min(input_len) - start).min(input_len - start);
1675 if end < input_len {
1676 rows
1677 } else {
1678 rows.saturating_sub(1)
1679 }
1680}
1681
1682pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1684
1685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1687pub(crate) struct ReuseLayer {
1688 pub full: bool,
1691 pub host_rows: usize,
1694 pub device_rows: Option<usize>,
1696 pub device_state: bool,
1698}
1699
1700#[derive(Debug, Clone, PartialEq, Eq)]
1702pub(crate) enum ReusePlan {
1703 Ready,
1705 Pull(Vec<(usize, usize, usize)>),
1708 Fresh,
1710}
1711
1712pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1722 let mut pulls = Vec::new();
1723 for (li, l) in layers.iter().enumerate() {
1724 if !l.full {
1725 if l.device_state {
1726 return ReusePlan::Fresh;
1727 }
1728 continue;
1729 }
1730 if l.host_rows == reuse_from {
1731 continue;
1732 }
1733 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1734 pulls.push((li, l.host_rows, reuse_from));
1735 continue;
1736 }
1737 return ReusePlan::Fresh;
1738 }
1739 if pulls.is_empty() {
1740 ReusePlan::Ready
1741 } else {
1742 ReusePlan::Pull(pulls)
1743 }
1744}
1745
1746impl Pipeline {
1747 fn reset_fresh_mtp_draft_state(&mut self) {
1750 self.draft_full_streak = 0;
1751 }
1752
1753 fn clear_sequence_state(&mut self) {
1761 #[cfg(target_os = "macos")]
1764 let _ = crate::gpu_metal::wait_replay();
1765 self.kv_cache.clear();
1766 self.clear_history();
1769 self.graph_logits = None;
1770 if let Some(b) = &mut self.dsv41 {
1771 b.3.clear();
1772 }
1773 crate::gpu::graph_kv_reset(self.graph_kv_id);
1774 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1779 }
1780
1781 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1788 if self.graph_prefill_preferred() {
1789 return true;
1790 }
1791 let kv_id = self.graph_kv_id;
1792 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1793 .map(|li| {
1794 let full = matches!(
1795 self.weights.layers[self.phys_layer(li)].attn,
1796 AttnKind::Full { .. }
1797 );
1798 ReuseLayer {
1799 full,
1800 host_rows: self.kv_cache.layers[li].pos_len(),
1803 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1804 device_state: crate::gpu::graph_state_resident(kv_id, li),
1805 }
1806 })
1807 .collect();
1808 if layers
1812 .iter()
1813 .all(|l| l.device_rows.is_none() && !l.device_state)
1814 {
1815 return true;
1816 }
1817 let plan = kv_reuse_plan(reuse_from, &layers);
1818 let (what, rows, n) = match &plan {
1819 ReusePlan::Ready => ("host ready", 0, 0),
1820 ReusePlan::Fresh => ("fresh", 0, 0),
1821 ReusePlan::Pull(p) => (
1822 "pull",
1823 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1824 p.len(),
1825 ),
1826 };
1827 let t0 = std::time::Instant::now();
1828 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1829 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1830 eprintln!(
1831 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1832 if ok { "" } else { " (failed → fresh)" },
1833 t0.elapsed().as_secs_f64() * 1e3
1834 );
1835 }
1836 ok
1837 }
1838
1839 fn apply_kv_reuse_plan(
1840 &mut self,
1841 reuse_from: usize,
1842 plan: ReusePlan,
1843 layers: &[ReuseLayer],
1844 ) -> bool {
1845 let kv_id = self.graph_kv_id;
1846 match plan {
1847 ReusePlan::Fresh => return false,
1848 ReusePlan::Ready => {}
1849 ReusePlan::Pull(pulls) => {
1850 let (nkv, hd) = {
1855 let c = &self.kv_cache.layers[pulls[0].0];
1856 (c.num_kv_heads, c.head_dim)
1857 };
1858 let uniform = pulls.iter().all(|&(li, _, _)| {
1859 let c = &self.kv_cache.layers[li];
1860 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1861 });
1862 let batched = if uniform {
1863 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1864 } else {
1865 None
1866 };
1867 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1868 Some(rows) => rows,
1869 None => {
1870 let mut rows = Vec::with_capacity(pulls.len());
1871 for &(li, from, to) in &pulls {
1872 let (lnkv, lhd) = {
1873 let c = &self.kv_cache.layers[li];
1874 (c.num_kv_heads, c.head_dim)
1875 };
1876 let Some((k, v, first_valid)) =
1877 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1878 else {
1879 return false;
1880 };
1881 let need_from = match self.layer_window(li) {
1885 Some(w) => from.max((to + 1).saturating_sub(w)),
1886 None => from,
1887 };
1888 if first_valid > need_from {
1889 return false;
1890 }
1891 rows.push((k, v));
1892 }
1893 rows
1894 }
1895 };
1896 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1897 let cache = &mut self.kv_cache.layers[li];
1898 let row = cache.num_kv_heads * cache.head_dim;
1899 for p in 0..to - from {
1900 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1901 }
1902 if cache.pos_len() != to {
1903 return false;
1904 }
1905 }
1906 }
1907 }
1908 for (li, l) in layers.iter().enumerate() {
1912 if l.full
1913 && l.device_rows.is_some_and(|d| d > reuse_from)
1914 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1915 {
1916 return false;
1917 }
1918 }
1919 true
1920 }
1921
1922 fn finish_generation(
1928 &mut self,
1929 mtp: &mut Option<MtpModule>,
1930 router: &mut Option<crate::swarm::DynRouter>,
1931 clear_sequence: bool,
1932 ) {
1933 if router.is_some() {
1937 let _ = self.set_active_skill(None);
1938 }
1939 #[cfg(target_os = "macos")]
1946 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1947 if clear_sequence {
1948 self.clear_sequence_state();
1949 if let Some(m) = mtp.as_mut() {
1950 m.kv.clear();
1956 }
1957 if let Some(m) = self.mtp.as_mut() {
1958 m.kv.clear();
1962 }
1963 }
1964 self.graph_want_logits = false;
1965 self.graph_head_required = false;
1966 self.graph_logits = None;
1967 self.graph_failed
1968 .store(false, std::sync::atomic::Ordering::Relaxed);
1969 self.cancel
1970 .store(false, std::sync::atomic::Ordering::Relaxed);
1971 self.dyn_router = router.take().or(self.dyn_router.take());
1972 self.mtp = mtp.take().or(self.mtp.take());
1973 self.mtp_graph_mode = None;
1974 self.spec_forced = None;
1975 }
1976
1977 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1981 if self
1982 .graph_failed
1983 .swap(false, std::sync::atomic::Ordering::Relaxed)
1984 {
1985 self.cancel
1986 .store(false, std::sync::atomic::Ordering::Relaxed);
1987 self.clear_sequence_state();
1988 self.graph_logits = None;
1989 self.graph_want_logits = false;
1990 self.graph_head_required = false;
1991 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1992 }
1993 Ok(())
1994 }
1995
1996 #[cfg(target_os = "macos")]
1997 fn fail_metal_graph(&mut self, reason: &str) {
1998 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1999 self.clear_sequence_state();
2000 self.graph_logits = None;
2001 self.graph_failed
2002 .store(true, std::sync::atomic::Ordering::Relaxed);
2003 self.cancel
2004 .store(true, std::sync::atomic::Ordering::Relaxed);
2005 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2006 }
2007
2008 fn nll_begin(&mut self) -> Result<(), String> {
2013 if self
2014 .graph_failed
2015 .swap(false, std::sync::atomic::Ordering::Relaxed)
2016 {
2017 self.cancel
2018 .store(false, std::sync::atomic::Ordering::Relaxed);
2019 self.clear_sequence_state();
2020 self.graph_logits = None;
2021 self.graph_want_logits = false;
2022 self.graph_head_required = false;
2023 return Err("GPU graph failed before NLL scoring".to_string());
2024 }
2025 self.clear_sequence_state();
2026 self.graph_logits = None;
2027 self.graph_want_logits = false;
2028 self.graph_head_required = false;
2029 Ok(())
2030 }
2031
2032 fn nll_end(&mut self) {
2036 self.clear_sequence_state();
2037 self.graph_logits = None;
2038 self.graph_want_logits = false;
2039 self.graph_head_required = false;
2040 self.graph_failed
2041 .store(false, std::sync::atomic::Ordering::Relaxed);
2042 }
2043
2044 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2047 #[cfg(test)]
2048 if self.nll_test_fail_at == Some(pos) {
2049 self.nll_test_fail_at = None;
2050 self.graph_failed
2051 .store(true, std::sync::atomic::Ordering::Relaxed);
2052 self.cancel
2053 .store(true, std::sync::atomic::Ordering::Relaxed);
2054 }
2055 if self
2056 .graph_failed
2057 .swap(false, std::sync::atomic::Ordering::Relaxed)
2058 {
2059 self.cancel
2060 .store(false, std::sync::atomic::Ordering::Relaxed);
2061 self.clear_sequence_state();
2062 self.graph_logits = None;
2063 self.graph_want_logits = false;
2064 return Err(format!(
2065 "GPU graph failed during NLL {phase} at position {pos}"
2066 ));
2067 }
2068 Ok(())
2069 }
2070
2071 #[inline]
2075 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2076 virtual_idx % self.physical_layers
2077 }
2078
2079 #[inline]
2082 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2083 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2084 }
2085
2086 #[allow(clippy::too_many_arguments)]
2088
2089 #[cfg(target_os = "macos")]
2108 fn graph_prefill_preferred(&self) -> bool {
2109 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2110 if !crate::gpu::enabled_here()
2111 || !graph_force
2112 || std::env::var("CMF_GPU_BLOCK")
2113 .map(|v| v == "0")
2114 .unwrap_or(false)
2115 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
2118 {
2119 return false;
2120 }
2121 self.weights
2122 .layers
2123 .iter()
2124 .any(|lw| {
2125 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
2126 })
2127 }
2128
2129 #[cfg(not(target_os = "macos"))]
2136 fn batch_prefix_prefill(&self) -> bool {
2137 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
2138 Ok("0") => return false,
2139 Ok("1") => true,
2140 _ => false,
2141 };
2142 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2143 && crate::gpu::enabled_here()
2144 && !self.graph_refused()
2145 && (forced || self.graph_attn_decline_reason().is_some())
2146 && self.wgpu_graph_attn_decline().is_none()
2147 && self.attn_softcap == 0.0
2148 && self
2149 .weights
2150 .layers
2151 .iter()
2152 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2153 && self.automatic_gpu_prefix().is_some()
2154 }
2155
2156 fn g4_batch_prefill(&self) -> bool {
2162 #[cfg(target_os = "macos")]
2163 {
2164 false
2165 }
2166 #[cfg(not(target_os = "macos"))]
2167 {
2168 !self.weights.layers.is_empty()
2172 && !self.per_token_stack()
2173 && (self.global_attn.is_some() || self.attn_v_norm)
2174 && crate::gpu::g4_batch_on()
2175 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
2176 && crate::gpu::enabled_here()
2177 && crate::gpu::discrete()
2178 && !self.graph_refused()
2179 && !self.o1_active()
2180 && self.attn_softcap == 0.0
2181 && self.wgpu_graph_attn_decline().is_none()
2182 && self.weights.layers.iter().all(|lw| {
2183 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
2184 })
2185 && self.automatic_gpu_prefix().is_none()
2186 }
2187 }
2188
2189 #[cfg(not(target_os = "macos"))]
2196 fn g4_batch_ingest(
2197 &mut self,
2198 ids: &[u32],
2199 pos0: usize,
2200 keep_all: bool,
2201 ) -> Result<(usize, Vec<f32>), String> {
2202 let hs = self.hidden_size;
2203 let chunk = self.generation_batch_k().max(1);
2204 let end_all = pos0 + ids.len();
2205 let mut pos = pos0;
2206 let mut out = Vec::new();
2207 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
2208 let end = (pos + chunk).min(end_all);
2209 let bk = end - pos;
2210 let mut hiddens = vec![0f32; bk * hs];
2211 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
2212 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
2213 }
2214 let positions: Vec<usize> = (pos..end).collect();
2215 let t_chunk = std::time::Instant::now();
2216 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2217 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2218 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2219 eprintln!(
2220 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2221 bk as f64 / (ms / 1000.0)
2222 );
2223 }
2224 match outcome {
2225 crate::gpu::BatchGraphOutcome::Completed => {
2226 if keep_all {
2227 out.extend_from_slice(&hiddens);
2228 } else {
2229 out = hiddens[(bk - 1) * hs..].to_vec();
2230 }
2231 pos = end;
2232 }
2233 crate::gpu::BatchGraphOutcome::Failed => {
2234 return Err("Gemma-4 batched prefill failed after admission".into());
2235 }
2236 crate::gpu::BatchGraphOutcome::Declined => {
2237 #[cfg(feature = "gpu")]
2238 if pos > pos0 {
2239 self.pull_lagging_host_kv(0, self.num_layers, pos);
2240 }
2241 break;
2242 }
2243 }
2244 }
2245 Ok((pos - pos0, out))
2246 }
2247
2248 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2253 #[cfg(not(target_os = "macos"))]
2254 {
2255 if ids.is_empty() || !self.g4_batch_prefill() {
2256 return None;
2257 }
2258 self.clear_sequence_state();
2259 match self.g4_batch_ingest(ids, 0, true) {
2260 Ok((n, rows)) if n == ids.len() => Some(rows),
2261 _ => {
2262 self.clear_sequence_state();
2263 None
2264 }
2265 }
2266 }
2267 #[cfg(target_os = "macos")]
2268 {
2269 let _ = ids;
2270 None
2271 }
2272 }
2273
2274 #[cfg(not(target_os = "macos"))]
2275 fn graph_prefill_preferred(&self) -> bool {
2276 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2284 if !graph_on || !crate::gpu::enabled_here() {
2285 return false;
2286 }
2287 if self.embryo_resident_eligible() {
2291 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2296 }
2297 if self.o1_active() {
2311 return false;
2312 }
2313 if self.wgpu_graph_attn_decline().is_some() {
2317 return false;
2318 }
2319 if self
2320 .weights
2321 .layers
2322 .iter()
2323 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2324 {
2325 return true;
2326 }
2327 self.weights
2338 .layers
2339 .iter()
2340 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2341 && self.automatic_gpu_prefix().is_none()
2342 }
2343
2344 #[cfg(target_os = "macos")]
2345 fn q1_graph_gpu(
2346 &mut self,
2347 start: usize,
2348 upto: Option<usize>,
2349 position: usize,
2350 h: &mut [f32],
2351 ) -> usize {
2352 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2354 const MOE_WORKING_SET_REASON: &str =
2355 "model weights exceed Metal working set for native MoE graph";
2356 if self
2360 .graph_declines
2361 .borrow()
2362 .iter()
2363 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2364 {
2365 return start;
2366 }
2367 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2368 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2370 || !crate::gpu::enabled_here()
2371 || !graph_force
2372 || std::env::var("CMF_GPU_BLOCK")
2373 .map(|v| v == "0")
2374 .unwrap_or(false)
2375 {
2376 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2377 eprintln!(
2378 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2379 self.attn_softcap > 0.0,
2380 crate::gpu::enabled_here(),
2381 graph_force,
2382 );
2383 }
2384 if self.graph_head_required {
2385 self.fail_metal_graph("native graph front gate refused");
2386 }
2387 return start;
2388 }
2389 let gemma4 = self.metal_gemma4();
2399 let swa_graph = self.metal_graph_swa() || gemma4;
2400 let graph_attn_reason = self.graph_attn_decline_reason();
2407 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
2408 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
2409 if (self.swa.is_some() && !swa_graph)
2410 || (self.global_attn.is_some() && !gemma4)
2411 || self.attention_heads_per_layer.is_some()
2412 || (self.attn_v_norm && !gemma4)
2413 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
2416 || self.weights.layers.iter().any(|lw| {
2417 ((lw.attn_out_norm.is_some()
2418 || lw.ffn_out_norm.is_some()
2419 || lw.layer_scale.is_some())
2420 && !gemma4)
2421 || matches!(&lw.ffn, FfnKind::Dense(d)
2422 if !(matches!(d.act, Act::Silu | Act::Gelu)
2423 || (gemma4 && d.act == Act::GeluTanh)))
2424 })
2425 {
2426 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
2429 self.note_graph_decline("metal block graph", reason);
2430 }
2431 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2432 eprintln!(
2433 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2434 self.swa.is_some(),
2435 self.global_attn.is_some(),
2436 self.attention_heads_per_layer.is_some(),
2437 self.attn_v_norm,
2438 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2439 );
2440 }
2441 if self.graph_head_required {
2442 self.fail_metal_graph("native graph architecture gate refused");
2443 }
2444 return start;
2445 }
2446 let limit = upto
2449 .map(|u| u + 1)
2450 .unwrap_or(self.num_layers)
2451 .min(self.num_layers);
2452
2453 enum Item<'a> {
2454 Gdn {
2455 run: Vec<GdnGpuLayer<'a>>,
2456 first: usize,
2457 },
2458 Attn {
2459 l: AttnGpuLayer<'a>,
2460 li: usize,
2461 q_norm: Option<&'a [f32]>,
2462 k_norm: Option<&'a [f32]>,
2463 output_gate: bool,
2464 proj_gate: Option<(&'a QTensor, bool)>,
2468 head_gate_w: Option<&'a [f32]>,
2471 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2472 full_gpu: bool,
2475 },
2476 }
2477
2478 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2485 let attend_contract = attend_mode != "0"
2486 && attend_mode != "off"
2487 && self.head_dim % 4 == 0
2488 && self.head_dim <= 256
2489 && (self.rotary_dim == 0
2493 || (self.rotary_dim >= 2
2494 && self.rotary_dim <= self.head_dim
2495 && (self.rotary_dim / 2) % 32 == 0))
2496 && self.num_kv_heads > 0
2497 && self.num_heads % self.num_kv_heads == 0;
2498
2499 let mut plan: Vec<Item> = Vec::new();
2500 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2501 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2503 let mut scan = start;
2504 while scan < limit {
2505 let lw = &self.weights.layers[self.phys_layer(scan)];
2506 let ffn = match &lw.ffn {
2507 FfnKind::Dense(d) if d.segs.is_empty() => {
2508 let (Some(g), Some(u), Some(dn)) = (
2509 d.gate_proj.metal_graph_parts(),
2510 d.up_proj.metal_graph_parts(),
2511 d.down_proj.metal_graph_parts(),
2512 ) else {
2513 if block_diag {
2514 eprintln!(
2515 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2516 );
2517 }
2518 break;
2519 };
2520 MetalFfn::Dense {
2521 gate: g,
2522 up: u,
2523 down: dn,
2524 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2527 }
2528 }
2529 FfnKind::Moe(m) => {
2530 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
2536 {
2537 MetalFfn::FlintMoe(flint)
2538 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
2539 MetalFfn::Moe(moe)
2540 } else {
2541 if block_diag {
2542 eprintln!(
2543 "block-graph: L{scan} MoE outside the graph contract — run ends"
2544 );
2545 }
2546 break;
2547 };
2548 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2549 model_ref.get_or_insert_with(|| model.clone());
2550 }
2551 ffn
2552 }
2553 _ => {
2554 if block_diag {
2555 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2556 }
2557 break;
2558 }
2559 };
2560 match &lw.attn {
2561 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2562 let parts = (
2563 w.in_proj_qkv.metal_graph_parts(),
2564 w.in_proj_z.metal_graph_parts(),
2565 w.in_proj_a.f32_parts(),
2566 w.in_proj_b.f32_parts(),
2567 w.out_proj.metal_graph_parts(),
2568 );
2569 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2570 if block_diag {
2571 eprintln!(
2572 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2573 w.in_proj_qkv.metal_graph_parts().is_some(),
2574 w.in_proj_z.metal_graph_parts().is_some(),
2575 w.in_proj_a.f32_parts().is_some(),
2576 w.in_proj_b.f32_parts().is_some(),
2577 w.out_proj.metal_graph_parts().is_some(),
2578 );
2579 }
2580 break;
2581 };
2582 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2583 model_ref.get_or_insert_with(|| model.clone());
2584 }
2585 let gl = GdnGpuLayer {
2586 attn_norm: &lw.input_norm,
2587 post_norm: &lw.post_norm,
2588 qkv,
2589 z,
2590 a,
2591 b,
2592 out,
2593 ffn,
2594 conv1d: &w.conv1d,
2595 a_log: &w.a_log,
2596 dt_bias: &w.dt_bias,
2597 gnorm: &w.norm,
2598 };
2599 match plan.last_mut() {
2600 Some(Item::Gdn { run, .. }) => run.push(gl),
2601 _ => plan.push(Item::Gdn {
2602 run: vec![gl],
2603 first: scan,
2604 }),
2605 }
2606 }
2607 AttnKind::Full {
2608 wq,
2609 wk,
2610 wv,
2611 wo,
2612 q_norm,
2613 k_norm,
2614 output_gate,
2615 softplus_gate,
2616 bias,
2617 } if (!self.kv_cache.layers[scan].o1_sealed()
2618 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2623 && softplus_gate
2627 .as_ref()
2628 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2629 {
2630 let parts = (
2631 wq.metal_graph_parts(),
2632 wk.metal_graph_parts(),
2633 wv.metal_graph_parts(),
2634 wo.metal_graph_parts(),
2635 );
2636 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2637 break;
2638 };
2639 if let QTensor::Mapped { model, .. } = wq {
2640 model_ref.get_or_insert_with(|| model.clone());
2641 }
2642 let cache = &self.kv_cache.layers[scan];
2643 let o1_metal = cache.o1.is_some()
2647 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2648 && cache.o1_views().is_some();
2649 let head_gate_w = softplus_gate
2653 .as_ref()
2654 .and_then(|(g, _)| g.f32_parts())
2655 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2656 .map(|(d, _, _)| d);
2657 let (nkv_g, hd_g) = if gemma4 {
2660 let (a, b, _) = self.layer_geom(scan);
2661 (a, b)
2662 } else {
2663 (self.num_kv_heads, self.head_dim)
2664 };
2665 let full_gpu = attend_contract
2666 && softplus_gate.is_none() == head_gate_w.is_none()
2667 && cache.mode == crate::kv_cache::KvMode::F32
2668 && (cache.o1.is_none() || o1_metal)
2669 && bias.is_none()
2670 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2671 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2672 && pk.1 == nkv_g * hd_g
2673 && pv.1 == nkv_g * hd_g
2674 && po.2 == self.num_heads * hd_g;
2675 plan.push(Item::Attn {
2676 l: AttnGpuLayer {
2677 attn_norm: &lw.input_norm,
2678 post_norm: &lw.post_norm,
2679 wq: pq,
2680 wk: pk,
2681 wv: pv,
2682 wo: po,
2683 ffn,
2684 attn_out_norm: lw.attn_out_norm.as_deref(),
2685 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2686 layer_scale: lw.layer_scale,
2687 },
2688 li: scan,
2689 q_norm: q_norm.as_deref(),
2690 k_norm: k_norm.as_deref(),
2691 output_gate: *output_gate,
2692 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2693 head_gate_w,
2694 bias: bias
2695 .as_ref()
2696 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2697 full_gpu,
2698 });
2699 }
2700 _ => break,
2701 }
2702 scan += 1;
2703 }
2704 let Some(model) = model_ref else {
2705 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2706 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2707 }
2708 if self.graph_head_required {
2709 self.fail_metal_graph("native graph has no mapped model reference");
2710 }
2711 return start;
2712 };
2713 if plan.is_empty() {
2714 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2715 eprintln!("q1-graph: empty plan at layer {start}");
2716 }
2717 if self.graph_head_required {
2718 self.fail_metal_graph("native graph plan is empty");
2719 }
2720 return start;
2721 }
2722 let has_moe = plan.iter().any(|it| match it {
2723 Item::Gdn { run, .. } => run
2724 .iter()
2725 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
2726 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
2727 });
2728 let has_flint_moe = plan.iter().any(|it| match it {
2729 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
2730 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
2731 });
2732 let metal_working_set = crate::gpu_metal::working_set_bytes();
2733 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
2734 let model_bytes = model.primary_bytes().len() as u64;
2735 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
2741 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
2742 let pager_cap = if !has_moe {
2751 None
2752 } else if has_flint_moe {
2753 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
2757 } else {
2758 crate::gpu_metal::moe_pager_cap_bytes()
2759 };
2760 let pager_eligible =
2761 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
2762 let mut pager_span: Option<(usize, usize)> = None;
2763 if pager_eligible {
2764 let cap = pager_cap.expect("checked above");
2765 let tail_span = (self.graph_want_logits
2769 && upto.is_none()
2770 && scan == self.num_layers
2771 && std::env::var("CMF_GPU_LMHEAD")
2772 .map(|v| v != "0")
2773 .unwrap_or(true))
2774 .then(|| self.weights.lm_head.metal_graph_parts())
2775 .flatten()
2776 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
2777 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
2778 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
2779 Some(match span {
2780 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
2781 None => (lo, hi),
2782 })
2783 };
2784 let page_span = |span: (usize, usize), include_tail: bool| {
2785 let (mut lo, mut hi) = span;
2786 if include_tail {
2787 if let Some((tail_lo, tail_hi)) = tail_span {
2788 lo = lo.min(tail_lo);
2789 hi = hi.max(tail_hi);
2790 }
2791 }
2792 crate::gpu_metal::page_round_span(lo, hi)
2793 };
2794 let mut selected = 0usize;
2795 let mut selected_span: Option<(usize, usize)> = None;
2796 let mut gdn_prefix: Option<(usize, usize)> = None;
2804 'items: for (i, item) in plan.iter().enumerate() {
2805 match item {
2806 Item::Attn { li, .. } => {
2807 let Some(span) = add_layer_span(selected_span, *li) else {
2808 break;
2809 };
2810 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
2811 break;
2812 };
2813 if page_hi - page_lo > cap {
2814 break;
2815 }
2816 selected = i + 1;
2817 selected_span = Some((page_lo, page_hi));
2818 }
2819 Item::Gdn { first, run } => {
2820 let mut whole = None;
2827 for take in 1..=run.len() {
2828 whole = add_layer_span(whole, *first + take - 1);
2829 if whole.is_none() {
2830 break;
2831 }
2832 }
2833 let whole_fits_empty_page = whole
2834 .and_then(|span| page_span(span, false))
2835 .is_some_and(|(lo, hi)| hi - lo <= cap);
2836 if whole_fits_empty_page {
2837 let mut span = selected_span;
2838 for take in 1..=run.len() {
2839 let Some(next) = add_layer_span(span, *first + take - 1) else {
2840 break 'items;
2841 };
2842 span = Some(next);
2843 }
2844 let Some(span) = span else {
2845 break;
2846 };
2847 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
2848 else {
2849 break;
2850 };
2851 if page_hi - page_lo > cap {
2852 break;
2853 }
2854 selected = i + 1;
2855 selected_span = Some((page_lo, page_hi));
2856 continue;
2857 }
2858 if selected != 0 {
2859 break;
2860 }
2861 let mut span = None;
2862 let mut best: Option<(usize, (usize, usize))> = None;
2863 for take in 1..=run.len() {
2864 let Some(next) = add_layer_span(span, *first + take - 1) else {
2865 break;
2866 };
2867 let Some((page_lo, page_hi)) = page_span(next, false) else {
2870 break;
2871 };
2872 if page_hi - page_lo > cap {
2873 break;
2874 }
2875 span = Some((page_lo, page_hi));
2876 best = Some((take, (page_lo, page_hi)));
2877 }
2878 let Some((take, page)) = best else {
2879 break;
2880 };
2881 selected = i + 1;
2882 selected_span = Some(page);
2883 if take != run.len() {
2884 gdn_prefix = Some((i, take));
2885 break 'items;
2886 }
2887 }
2888 }
2889 }
2890 if selected != 0 {
2891 if let Some((item, take)) = gdn_prefix {
2892 match &mut plan[item] {
2893 Item::Gdn { run, .. } => run.truncate(take),
2894 Item::Attn { .. } => unreachable!("only a GDN run is splittable"),
2895 }
2896 }
2897 let (page_lo, page_hi) = selected_span.expect("selected page span");
2898 tracing::info!(
2899 page_start = page_lo,
2900 page_end = page_hi,
2901 page_mb = (page_hi - page_lo) / (1024 * 1024),
2902 cap_mb = cap / (1024 * 1024),
2903 graph_items = selected,
2904 "native Metal MoE graph uses transient bounded weight page"
2905 );
2906 plan.truncate(selected);
2907 pager_span = Some((page_lo, page_hi));
2908 } else {
2909 tracing::warn!(
2910 cap_mb = cap / (1024 * 1024),
2911 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
2912 );
2913 }
2914 }
2915 if has_moe && needs_bounded_pager && pager_span.is_none() {
2924 tracing::info!(
2925 model_bytes,
2926 metal_working_set,
2927 "native Metal MoE graph declined before allocation"
2928 );
2929 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2930 if self.graph_head_required {
2931 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2932 }
2933 return start;
2934 }
2935 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2936 let dev_attend = attend_contract
2937 && (self.head_dim <= 128
2938 || has_moe
2939 || (self.head_dim <= 256 && has_gdn)
2945 || (self.head_dim <= 256 && swa_graph)
2949 || attend_mode == "force"
2950 || attend_mode == "256");
2951 if !dev_attend {
2952 for it in &mut plan {
2953 if let Item::Attn { li, full_gpu, .. } = it {
2954 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2957 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2958 if !keep_o1 {
2959 *full_gpu = false;
2960 }
2961 }
2962 }
2963 }
2964 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2965 use std::sync::atomic::{AtomicBool, Ordering};
2966 static SAID: AtomicBool = AtomicBool::new(false);
2967 if !SAID.swap(true, Ordering::Relaxed) {
2968 let fg = plan
2969 .iter()
2970 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2971 .count();
2972 let att = plan
2973 .iter()
2974 .filter(|it| matches!(it, Item::Attn { .. }))
2975 .count();
2976 eprintln!(
2977 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2978 plan.len(),
2979 self.head_dim,
2980 self.rotary_dim,
2981 self.num_kv_heads,
2982 self.num_heads,
2983 );
2984 }
2985 }
2986 let dims = GraphDims {
2987 hidden: self.hidden_size,
2988 eps: self.rms_eps as f32,
2989 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2990 };
2991 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
2995 crate::gpu_metal::MoePagerPageScope::new(
2996 _mt0,
2997 position,
2998 start,
2999 page_lo,
3000 page_hi,
3001 pager_cap.unwrap_or(0),
3005 )
3006 });
3007 let graph_new = match pager_span {
3008 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
3009 None => TokenGraph::new(&model, dims, h),
3010 };
3011 let Some(mut graph) = graph_new else {
3012 if self.graph_head_required {
3013 self.fail_metal_graph("native TokenGraph allocation refused");
3014 }
3015 return start;
3016 };
3017 if swa_graph && self.head_dim > 128 {
3018 graph.set_attend_blk(1, 32);
3028 }
3029 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
3030 nv: cfg.num_v_heads,
3031 nk: cfg.num_k_heads,
3032 dk: cfg.key_head_dim,
3033 dv: cfg.value_head_dim,
3034 kk: cfg.conv_kernel,
3035 hidden: self.hidden_size,
3036 inter: self.intermediate_size,
3037 c_dim: cfg.conv_dim(),
3038 eps: cfg.rms_eps as f32,
3039 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
3040 });
3041 let mut valid = 0usize;
3045 let mut end = start;
3046 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
3048 static ONCE: std::sync::Once = std::sync::Once::new();
3049 ONCE.call_once(|| {
3050 for it in &plan {
3051 match it {
3052 Item::Gdn { first, run } => {
3053 eprintln!("plan: Gdn first={first} len={}", run.len())
3054 }
3055 Item::Attn { li, full_gpu, .. } => {
3056 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
3057 }
3058 }
3059 }
3060 });
3061 }
3062 for item in &plan {
3063 let ok = match item {
3064 Item::Gdn { run, .. } => gcfg
3065 .as_ref()
3066 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
3067 .unwrap_or(false),
3068 Item::Attn { l, .. } => graph.attn_ok(l),
3069 };
3070 if !ok {
3071 if block_diag {
3072 eprintln!(
3073 "block-graph: plan item {} ({}) failed graph preflight",
3074 valid,
3075 match item {
3076 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
3077 Item::Attn { li, .. } => format!("Attn L{li}"),
3078 }
3079 );
3080 }
3081 break;
3082 }
3083 valid += 1;
3084 end += match item {
3085 Item::Gdn { run, .. } => run.len(),
3086 Item::Attn { .. } => 1,
3087 };
3088 }
3089 plan.truncate(valid);
3090 if plan.is_empty() {
3091 if self.graph_head_required {
3092 self.fail_metal_graph("native graph preflight produced no valid items");
3093 }
3094 return start;
3095 }
3096
3097 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
3098 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
3099 return start;
3100 }
3101
3102 let one_pass = |t: (usize, usize, usize)| {
3111 use cortiq_core::TensorDtype as D;
3112 matches!(
3113 model.tensors[t.0].dtype,
3114 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
3115 )
3116 };
3117 let dense_fast = plan.iter().all(|it| match it {
3118 Item::Attn {
3119 l, li, full_gpu, ..
3120 } => {
3121 *full_gpu
3122 && self.kv_cache.layers[*li].o1.is_none()
3123 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
3124 && match l.ffn {
3125 MetalFfn::Dense { gate, up, down, .. } => {
3126 one_pass(gate) && one_pass(up) && one_pass(down)
3127 }
3128 _ => false,
3129 }
3130 }
3131 Item::Gdn { .. } => false,
3132 });
3133 let moe_plan = !dense_fast
3137 && plan.iter().all(|it| match it {
3138 Item::Attn {
3139 l, li, full_gpu, ..
3140 } => {
3141 *full_gpu
3142 && self.kv_cache.layers[*li].o1.is_none()
3143 && matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
3144 }
3145 Item::Gdn { .. } => false,
3146 });
3147 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
3148 if block_diag {
3149 use std::sync::atomic::{AtomicBool, Ordering};
3150 static SAID_AB: AtomicBool = AtomicBool::new(false);
3151 if !SAID_AB.swap(true, Ordering::Relaxed) {
3152 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
3153 }
3154 }
3155 let _mv_fast = match ab {
3156 Some((bits, _)) => {
3157 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
3158 crate::gpu_metal::MvFastGuard::set_raw(bits)
3159 }
3160 None if moe_plan => {
3161 graph.set_dense_concurrent(
3162 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
3163 );
3164 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
3165 }
3166 None => {
3167 graph.set_dense_concurrent(dense_fast);
3168 let q8r4 = if swa_graph {
3171 crate::gpu_metal::DENSE_Q8R4
3172 } else {
3173 0
3174 };
3175 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
3176 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
3177 } else {
3178 0
3179 })
3180 }
3181 };
3182
3183 let pool = self.pool.clone();
3186 let (nh, nkv, hd, hs, eps) = (
3187 self.num_heads,
3188 self.num_kv_heads,
3189 self.head_dim,
3190 self.hidden_size,
3191 self.rms_eps,
3192 );
3193 let norm_style = self.norm_style;
3194 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
3195 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
3196 let kv_id = self.graph_kv_id;
3197 let mut pending: Vec<(usize, usize)> = Vec::new();
3200 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
3203 for item in &plan {
3204 let _xt0 = std::time::Instant::now();
3205 let _xkind: u32 = match item {
3206 Item::Gdn { .. } => 2,
3207 Item::Attn { .. } => 3,
3208 };
3209 if self.loop_final_norm {
3211 let item_start = match item {
3212 Item::Gdn { first, .. } => *first,
3213 Item::Attn { li, .. } => *li,
3214 };
3215 if item_start > start && self.is_loop_end(item_start - 1) {
3216 graph.encode_loop_norm(&self.weights.final_norm);
3217 }
3218 }
3219 match item {
3220 Item::Gdn { run, first } => {
3221 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
3222 if l.linear_state.len() != want {
3223 l.linear_state = vec![0f32; want];
3224 }
3225 }
3226 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
3227 .iter()
3228 .map(|l| l.linear_state.as_slice())
3229 .collect();
3230 let _ig = std::time::Instant::now();
3231 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
3232 tracing::error!("q1 graph: GDN run refused after validation");
3234 return start;
3235 }
3236 graph.commit_kind = 2;
3239 graph.commit();
3240 crate::gpu::stageprof(0, _ig.elapsed());
3241 pending.push((*first, run.len()));
3242 }
3243 Item::Attn {
3244 l,
3245 li,
3246 q_norm,
3247 k_norm,
3248 output_gate,
3249 proj_gate,
3250 head_gate_w,
3251 bias,
3252 full_gpu,
3253 } => {
3254 let _ia = std::time::Instant::now();
3255 let inv_freq_l = self.layer_inv_freq(*li);
3261 let rd_l = self.layer_geom(*li).2;
3262 let window_l = self.layer_window(*li);
3263 let head_gate_w = *head_gate_w;
3264 let (nkv, hd) = if gemma4 {
3267 let (a, b, _) = self.layer_geom(*li);
3268 (a, b)
3269 } else {
3270 (nkv, hd)
3271 };
3272 if *full_gpu {
3274 let cache = &self.kv_cache.layers[*li];
3275 let o1p = if cache.o1.is_some() {
3276 match cache.o1_views() {
3277 Some(views) => Some(crate::gpu::O1AttnParams {
3278 views,
3279 epoch: self.o1_epoch,
3280 }),
3281 None => None,
3283 }
3284 } else {
3285 None
3286 };
3287 let o1_layer = cache.o1.is_some();
3288 if o1_layer && o1p.is_none() {
3289 }
3291 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
3292 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
3293 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
3294 debug_assert!(
3297 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
3298 );
3299 let p = crate::gpu::AttnDeviceParams {
3300 kv_id,
3301 layer: *li,
3302 nh,
3303 nkv,
3304 hd,
3305 rd: rd_l,
3306 position,
3307 scale: self.attn_scale,
3308 eps: eps as f32,
3309 gemma,
3310 late_qk_norm: self.qk_norm_after_rope,
3311 output_gate: *output_gate,
3312 q_norm: *q_norm,
3313 k_norm: *k_norm,
3314 inv_freq: &inv_freq_l,
3315 rope_scale: self.layer_rope_scale(*li),
3316 cpu_k,
3317 cpu_v,
3318 cpu_stored,
3319 cpu_gen: cache.generation(),
3320 o1: o1p,
3321 window: window_l,
3322 softmax_sink: cache.sinks.as_deref(),
3323 head_gate: head_gate_w,
3324 v_norm: self.attn_v_norm,
3325 };
3326 let o1_bad = o1_layer && p.o1.is_none();
3327 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
3328 {
3329 if p.o1.is_none() {
3331 dev_attn.push((*li, nkv, hd));
3332 }
3333 graph.commit_kind = 3;
3334 graph.commit();
3335 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3339 continue;
3340 }
3341 }
3343 graph.encode_attn_prefix(l);
3344 if let Err(err) = graph.sync_checked() {
3345 self.fail_metal_graph(&err);
3346 return start;
3347 }
3348 if !pending.is_empty() {
3349 let idxs: Vec<usize> =
3350 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3351 let mut outs: Vec<&mut [f32]> = self
3352 .kv_cache
3353 .layers
3354 .iter_mut()
3355 .enumerate()
3356 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3357 .map(|(_, s)| s.linear_state.as_mut_slice())
3358 .collect();
3359 graph.read_states(&mut outs);
3360 }
3361 let mut q_raw = attention::take_buf(l.wq.1);
3362 let mut k = attention::take_buf(l.wk.1);
3363 let mut v = attention::take_buf(l.wv.1);
3364 graph.read_qkv(&mut q_raw, &mut k, &mut v);
3365 let mut gate_raw = proj_gate.map(|(gp, _)| {
3368 let mut normed = attention::take_buf(hs);
3369 graph.read_normed(&mut normed);
3370 let mut raw = attention::take_buf(gp.rows());
3371 gp.matvec(&normed, &mut raw, pool.as_deref());
3372 attention::recycle_buf(&mut normed);
3373 raw
3374 });
3375 let cfg = QwenAttnCfg {
3376 num_heads: nh,
3377 num_kv_heads: nkv,
3378 head_dim: hd,
3379 hidden_size: hs,
3380 position,
3381 inv_freq: &inv_freq_l,
3382 rotary_dim: rd_l,
3383 scale: self.attn_scale,
3384 softcap: self.attn_softcap,
3385 window: window_l,
3386 v_norm: self.attn_v_norm,
3389 qk_norm_after_rope: self.qk_norm_after_rope,
3390 gate_sigmoid: self.proj_gate_sigmoid,
3391 q_norm: *q_norm,
3392 k_norm: *k_norm,
3393 output_gate: *output_gate,
3394 softplus_gate: None,
3395 rope_scale: self.layer_rope_scale(*li),
3396 bias: *bias,
3397 rms_eps: eps,
3398 norm_style,
3399 pool: pool.as_deref(),
3400 v_head_dim: hd,
3401 };
3402 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
3405 || std::env::var("CMF_ATTN_DUMP").is_ok();
3406 let _ = full_gpu;
3407 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
3408 let mut ao = attention::qwen_attention_core(
3409 q_raw,
3410 k,
3411 v,
3412 &mut self.kv_cache.layers[*li],
3413 &cfg,
3414 );
3415 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
3419 if let Some((qr0, k0, v0)) = oracle_in.clone() {
3420 let (cq, _cg, _ck, _cv) =
3421 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3422 let cache = &self.kv_cache.layers[*li];
3423 let n = cache.head_keys(0).len() / hd;
3424 let mut bytes: Vec<u8> = Vec::new();
3425 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
3426 bytes.extend_from_slice(&v.to_le_bytes());
3427 }
3428 for v in &cq {
3429 bytes.extend_from_slice(&v.to_le_bytes());
3430 }
3431 for g in 0..nkv {
3432 for v in cache.head_keys(g) {
3433 bytes.extend_from_slice(&v.to_le_bytes());
3434 }
3435 }
3436 for g in 0..nkv {
3437 for v in cache.head_values(g) {
3438 bytes.extend_from_slice(&v.to_le_bytes());
3439 }
3440 }
3441 let _ =
3442 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
3443 }
3444 }
3445 if let Some((qr0, k0, v0)) =
3446 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
3447 {
3448 let (cq, _cg, ck, cv) =
3449 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
3450 let mut h_now = vec![0f32; hs];
3451 graph.read_h(&mut h_now);
3452 let cache = &self.kv_cache.layers[*li];
3453 let n_after = cache.head_keys(0).len() / hd;
3454 let stored = n_after.saturating_sub(1);
3458 let cpu_k: Vec<&[f32]> = (0..nkv)
3459 .map(|g| &cache.head_keys(g)[..stored * hd])
3460 .collect();
3461 let cpu_v: Vec<&[f32]> = (0..nkv)
3462 .map(|g| &cache.head_values(g)[..stored * hd])
3463 .collect();
3464 let p = crate::gpu::AttnDeviceParams {
3465 kv_id,
3466 layer: *li,
3467 nh,
3468 nkv,
3469 hd,
3470 rd: rd_l,
3474 position,
3475 scale: self.attn_scale,
3476 eps: eps as f32,
3477 gemma,
3478 late_qk_norm: self.qk_norm_after_rope,
3479 output_gate: *output_gate,
3480 q_norm: *q_norm,
3481 k_norm: *k_norm,
3482 inv_freq: &inv_freq_l,
3483 rope_scale: self.layer_rope_scale(*li),
3484 cpu_k,
3485 cpu_v,
3486 cpu_stored: stored,
3487 cpu_gen: cache.generation(),
3488 o1: None,
3489 window: window_l,
3490 softmax_sink: cache.sinks.as_deref(),
3491 head_gate: None,
3492 v_norm: self.attn_v_norm,
3493 };
3494 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3495 let md = |a: &[f32], b: &[f32]| {
3496 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3497 };
3498 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3499 eprintln!(
3500 "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}",
3501 nn(&cq),
3502 md(&cq, &dq),
3503 nn(&ck),
3504 md(&ck, &dk),
3505 nn(&cv),
3506 md(&cv, &dv),
3507 nn(&ao),
3508 md(&ao, &dao)
3509 );
3510 } else {
3511 eprintln!("attn-oracle L{li}: device probe declined");
3512 }
3513 }
3514 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3515 attention::apply_projected_gate(
3518 &mut ao,
3519 raw,
3520 per_head,
3521 hd,
3522 self.proj_gate_sigmoid,
3523 );
3524 }
3525 if let Some(mut raw) = gate_raw.take() {
3526 attention::recycle_buf(&mut raw);
3527 }
3528 graph.encode_attn_suffix(l, &ao);
3529 graph.commit();
3532 attention::recycle_buf(&mut ao);
3533 }
3534 }
3535
3536 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3537 }
3538 let mut lm_rows = None;
3543 if self.graph_want_logits
3544 && upto.is_none()
3545 && end == self.num_layers
3546 && std::env::var("CMF_GPU_LMHEAD")
3547 .map(|v| v != "0")
3548 .unwrap_or(true)
3549 {
3550 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3551 if graph.lm_head_ok(lm) {
3552 graph.encode_lm_head(&self.weights.final_norm, lm);
3553 lm_rows = Some(lm.1);
3554 }
3555 }
3556 }
3557 if self.graph_head_required && lm_rows.is_none() {
3558 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3559 self.fail_metal_graph("fused graph head was requested but not encodable");
3560 return start;
3561 }
3562 let _sy0 = std::time::Instant::now();
3563 if let Err(err) = graph.sync_checked() {
3564 if let Some(profile) = pager_profile_page.as_mut() {
3565 profile.finish(start, graph.pager_page_stats(), "sync-error");
3566 }
3567 self.fail_metal_graph(&err);
3568 return start;
3569 }
3570 let _rs0 = std::time::Instant::now();
3571 if !pending.is_empty() {
3572 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3573 let mut outs: Vec<&mut [f32]> = self
3574 .kv_cache
3575 .layers
3576 .iter_mut()
3577 .enumerate()
3578 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3579 .map(|(_, s)| s.linear_state.as_mut_slice())
3580 .collect();
3581 graph.read_states(&mut outs);
3582 }
3583 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3584 use std::sync::atomic::{AtomicU64, Ordering};
3585 static SY: AtomicU64 = AtomicU64::new(0);
3586 static RS: AtomicU64 = AtomicU64::new(0);
3587 static N: AtomicU64 = AtomicU64::new(0);
3588 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3589 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3590 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3591 if n % 100 == 0 {
3592 eprintln!(
3593 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
3594 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3595 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3596 );
3597 }
3598 }
3599 if let Some(rows) = lm_rows {
3600 crate::gpu::hostprof_encode_done(_mt0);
3601 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3602 graph.read_logits(&mut lg);
3603 crate::gpu::hostprof_total(_mt0);
3604 lg.resize(self.vocab_size, 0.0);
3605 if let Some(c) = self.final_softcap {
3606 softcap_in_place(&mut lg, c, self.pool.as_deref());
3607 }
3608 self.graph_logits = Some(lg);
3609 }
3610 graph.read_h(h);
3611 if self.graph_head_required && self.graph_logits.is_none() {
3612 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3613 self.fail_metal_graph("fused graph head completed without logits readback");
3614 return start;
3615 }
3616 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3617 METAL_GRAPH_LAYERS.fetch_add(
3618 end.saturating_sub(start) as u64,
3619 std::sync::atomic::Ordering::Relaxed,
3620 );
3621 if self.graph_head_required {
3622 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3623 }
3624 for (li, nkv, hd) in dev_attn {
3628 let mut krow = attention::take_buf(nkv * hd);
3629 let mut vrow = attention::take_buf(nkv * hd);
3630 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3631 let cache = &mut self.kv_cache.layers[li];
3632 cache.append(&krow, &vrow, &[]);
3633 let n = cache.seq_len;
3634 let mut imp = attention::take_buf(n);
3635 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3636 cache.accumulate_imp(&imp);
3637 attention::recycle_buf(&mut imp);
3638 }
3639 attention::recycle_buf(&mut krow);
3640 attention::recycle_buf(&mut vrow);
3641 }
3642 if let Some(profile) = pager_profile_page.as_mut() {
3643 profile.finish(end, graph.pager_page_stats(), "page-complete");
3644 }
3645 if let Some((_, arm)) = ab {
3646 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3647 }
3648 end
3649 }
3650
3651 pub fn new(
3652 tokenizer: Tokenizer,
3653 weights: PipelineWeights,
3654 hidden_size: usize,
3655 intermediate_size: usize,
3656 num_heads: usize,
3657 num_kv_heads: usize,
3658 head_dim: usize,
3659 num_layers: usize,
3660 physical_layers: usize,
3661 loop_final_norm: bool,
3662 vocab_size: usize,
3663 rms_eps: f64,
3664 rope_base: f32,
3665 norm_style: NormStyle,
3666 max_seq_len: usize,
3667 sampler_config: SamplerConfig,
3668 ) -> Self {
3669 let rng = match sampler_config.seed {
3670 Some(s) => SplitMix64::new(s),
3671 None => SplitMix64::from_entropy(),
3672 };
3673 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3674 let pool = Pool::from_env();
3675 if let Some(p) = &pool {
3676 tracing::info!("worker pool: {} threads", p.n_workers());
3677 if let Some(model) = weights
3679 .lm_head
3680 .model_arc()
3681 .or_else(|| weights.embed_tokens.model_arc())
3682 {
3683 let regions: Vec<&[u8]> =
3684 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3685 p.bind_numa(®ions);
3686 }
3687 }
3688 Self {
3689 gpu_plan: None,
3690 tokenizer: std::sync::Arc::new(tokenizer),
3691 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3692 sampler_config,
3693 weights,
3694 hidden_size,
3695 intermediate_size,
3696 num_heads,
3697 num_kv_heads,
3698 head_dim,
3699 num_layers,
3700 physical_layers,
3701 loop_final_norm,
3702 vocab_size,
3703 rms_eps,
3704 rope_base,
3705 norm_style,
3706 rotary_dim: head_dim,
3707 attention_heads_per_layer: None,
3708 kv_heads_per_layer: None,
3709 v_head_dim: None,
3710 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3711 .filter(|v| !v.is_empty())
3712 .map(std::path::PathBuf::from),
3713 graph_declines: std::cell::RefCell::new(Vec::new()),
3714 mimo_moe: Default::default(),
3715 minicpm_tail: Default::default(),
3716 vmf_cfg: None,
3717 gdn_cfg: None,
3718 kda_cfg: None,
3719 g3n: None,
3720 g4e: None,
3721 dsv4: None,
3722 dsv41: None,
3723 dsv41_vision: None,
3724 dsv41_prefill: None,
3725 qwen4_exp: None,
3726 clover: None,
3727 clover_stats: clover::CloverStats::default(),
3728 dsv4_mtp: Vec::new(),
3729 dspark: None,
3730 dspark_pending: Vec::new(),
3731 dspark_hist: Vec::new(),
3732 dspark_real: Vec::new(),
3733 dspark_trunk_picks: Vec::new(),
3734 dspark_exp: Vec::new(),
3735 dspark_draft_ns: 0,
3736 m3_winners: Vec::new(),
3737 m3_counts: Vec::new(),
3738 m3_n_exp: 0,
3739 m3_tokens: 0,
3740 m3_prewarmed: false,
3741 logit_multiplier: None,
3742 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3743 graph_failed: std::sync::atomic::AtomicBool::new(false),
3744 kv_history: Vec::new(),
3745 kv_history_device: false,
3746 short_conv_cfg: None,
3747 mtp: None,
3748 mimo_mtp: None,
3749 verify_exact_moe: false,
3750 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3751 ignore_eos: false,
3752 draft_full_streak: 0,
3753 spec_k_adapt: None,
3754 spec_acc_ewma: 0.7,
3755 rng,
3756 sampler_scratch: SamplerScratch::default(),
3757 spec_forced: None,
3758 spec_q: Vec::new(),
3759 spec_p: Vec::new(),
3760 spec_res: Vec::new(),
3761 spec_qs: Vec::new(),
3762 spec_ps: Vec::new(),
3763 spec_ress: Vec::new(),
3764 mtp_graph_mode: None,
3765 #[cfg(target_os = "macos")]
3766 metal_verify: None,
3767 inv_freq,
3768 ws: ForwardScratch::new(hidden_size),
3769 pool,
3770 model: None,
3771 dyn_force_f32: false,
3772 dyn_skill_layers: Vec::new(),
3773 dyn_active: None,
3774 dyn_blend_loaded: false,
3775 dyn_phi_layer: None,
3776 dyn_phi_ema: Vec::new(),
3777 dyn_phi_seen: 0,
3778 dyn_router: None,
3779 o1_cfg: None,
3780 o1_epoch: 0,
3781 o1_flags: Vec::new(),
3782 trace: false,
3783 calib_temp: 1.0,
3784 confidence_on: true,
3785 embed_multiplier: 1.0,
3786 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3787 swa: None,
3788 swa_trim: None,
3789 swa_trim_anchor: usize::MAX,
3790 chunk_min_rows: 32,
3791 sliding_layers: None,
3792 no_rope_layers: None,
3793 anchor_core: None,
3794 bounded_rope: None,
3795 kv_prefix: KvPrefix::default(),
3796 last_prefill_tokens: 0,
3797 inv_freq_local: None,
3798 rotary_dim_local: None,
3799 rope_scale: 1.0,
3800 rope_scale_local: 1.0,
3801 global_attn: None,
3802 inv_freq_global: None,
3803 attn_v_norm: false,
3804 qk_norm_after_rope: false,
3805 proj_gate_sigmoid: false,
3806 final_softcap: None,
3807 head_clusters: None,
3808 attn_softcap: 0.0,
3809 graph_want_logits: false,
3810 graph_head_required: false,
3811 graph_logits: None,
3812 embryo_graph: None,
3813 graph_refused: std::sync::atomic::AtomicBool::new(false),
3814 graph_kv_id: {
3815 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3816 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3817 },
3818 #[cfg(test)]
3819 nll_test_fail_at: None,
3820 #[cfg(test)]
3821 nll_test_force_serial: false,
3822 }
3823 }
3824
3825 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3833 if let Err(e) = self.try_set_o1(cfg) {
3834 tracing::error!("{e}");
3835 }
3836 }
3837
3838 pub fn bounded_native(&self) -> bool {
3842 self.anchor_core.is_some()
3843 }
3844
3845 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3848 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3849 }
3850
3851 pub fn o1_refusal(&self) -> Option<String> {
3853 self.anchor_core.as_ref().map(|ac| {
3854 format!(
3855 "--o1 / CMF_O1 refused: the anchor is native bounded \
3856 (anchor_core kind={} window={} sink={}); the file's operator \
3857 is executed as-is and no post-hoc Nyström overlay applies",
3858 ac.kind, ac.window, ac.sink
3859 )
3860 })
3861 }
3862
3863 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3865 if let Some(c) = &cfg {
3866 if let Some(why) = self.o1_refusal() {
3867 self.o1_flags = Vec::new();
3868 self.o1_cfg = None;
3869 return Err(why);
3870 }
3871 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3872 self.o1_flags.clear();
3873 self.o1_cfg = None;
3874 return Err(format!(
3875 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3876 c.w, c.sink
3877 ));
3878 }
3879 }
3880 self.o1_flags = match &cfg {
3881 Some(c) => {
3882 let mut flags = c.layer_flags(self.num_layers);
3883 for (li, f) in flags.iter_mut().enumerate() {
3884 if *f
3890 && (!matches!(
3891 self.weights.layers[self.phys_layer(li)].attn,
3892 AttnKind::Full { .. }
3893 ) || self.layer_window(li).is_some()
3894 || self.kv_cache.layers[li].sinks.is_some()
3895 || self.layer_v_dim(li) != self.layer_geom(li).1)
3896 {
3897 *f = false;
3898 }
3899 }
3900 flags
3901 }
3902 None => Vec::new(),
3903 };
3904 if let Some(c) = &cfg {
3905 let n = self.o1_flags.iter().filter(|&&f| f).count();
3906 tracing::info!(
3907 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3908 self.num_layers,
3909 c.m,
3910 c.w,
3911 c.sink,
3912 c.rect
3913 );
3914 }
3915 self.o1_cfg = cfg;
3916 Ok(())
3917 }
3918
3919 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
3925 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3926 return Err(format!(
3927 "anchor_core kind '{}' is not executable by this runtime",
3928 cfg.kind
3929 ));
3930 }
3931 if cfg.window == 0 {
3932 return Err("anchor_core.window must be >= 1".into());
3933 }
3934 let mut n = 0usize;
3935 for li in 0..self.num_layers {
3936 let pl = self.phys_layer(li);
3937 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3938 if w.window != cfg.window || w.sink != cfg.sink {
3939 return Err(format!(
3940 "layer {li}: bounded weights (window {} sink {}) disagree with \
3941 anchor_core (window {} sink {})",
3942 w.window, w.sink, cfg.window, cfg.sink
3943 ));
3944 }
3945 self.kv_cache.layers[li].install_bounded(cfg.window);
3946 n += 1;
3947 }
3948 }
3949 if n == 0 {
3950 return Err("anchor_core is present but no layer executes it".into());
3951 }
3952 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3953 self.bounded_rope = Some(std::sync::Arc::new(rope));
3954 self.anchor_core = Some(cfg.clone());
3955 self.embryo_graph = None;
3956 tracing::info!(
3957 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3958 cfg.kind,
3959 cfg.window,
3960 cfg.sink,
3961 self.kv_cache
3962 .layers
3963 .iter()
3964 .map(|l| l.bounded_state_bytes())
3965 .max()
3966 .unwrap_or(0)
3967 );
3968 Ok(())
3969 }
3970
3971 pub fn install_wire_identity(&mut self, identity: u64) {
3975 for li in 0..self.kv_cache.layers.len() {
3976 let pl = self.phys_layer(li);
3977 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3978 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3979 Some(AttnKind::Linear(_))
3980 | Some(AttnKind::LinearGdn(_))
3981 | Some(AttnKind::ShortConv(_))
3982 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3983 _ => crate::kv_cache::WireKind::Full,
3984 };
3985 let l = &mut self.kv_cache.layers[li];
3986 l.wire_kind = kind;
3987 l.wire_identity = identity;
3988 l.wire_layer = li as u32;
3991 }
3992 }
3993
3994 pub fn clear_history(&mut self) {
3996 self.kv_history.clear();
3997 self.kv_history_device = false;
3998 self.kv_prefix.clear();
3999 }
4000
4001 pub fn graph_refused(&self) -> bool {
4005 self.graph_refused
4006 .load(std::sync::atomic::Ordering::Relaxed)
4007 }
4008
4009 pub fn mark_graph_refused(&self) {
4011 if !self
4012 .graph_refused
4013 .swap(true, std::sync::atomic::Ordering::Relaxed)
4014 {
4015 tracing::info!(
4016 "token graph: unsupported for this pipeline (seq {}) — not retrying",
4017 self.graph_kv_id
4018 );
4019 }
4020 }
4021
4022 pub fn device_sequence_position(&self) -> Option<usize> {
4026 crate::gpu::embryo_device_next_position(self.graph_kv_id)
4027 }
4028
4029 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
4034 let dev = self.device_sequence_position();
4035 if recorded_on_device {
4036 dev == Some(n) && self.embryo_resident_wanted()
4037 } else {
4038 dev.is_none()
4039 }
4040 }
4041
4042 pub(crate) fn invalidate_for_weight_change(&mut self) {
4050 self.clear_sequence_state();
4051 self.embryo_graph = None;
4052 }
4053
4054 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
4060 let (n, on_device) = if self.bounded_native() {
4061 (
4062 self.kv_prefix.extension(input_ids),
4063 self.kv_prefix.on_device(),
4064 )
4065 } else {
4066 let h = &self.kv_history;
4067 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
4068 (h.len(), self.kv_history_device)
4069 } else if !h.is_empty() && !self.kv_history_device {
4070 (self.rollback_prefix_len(input_ids), false)
4071 } else {
4072 (0, false)
4073 }
4074 };
4075 if n > 0 && !self.prefix_owner_matches(n, on_device) {
4078 tracing::warn!(
4079 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
4080 the device now holds {:?} — re-prefilling from zero",
4081 if on_device { "resident device" } else { "host" },
4082 self.device_sequence_position()
4083 );
4084 return 0;
4085 }
4086 n
4087 }
4088
4089 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
4100 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
4101 || self.device_sequence_position().is_some()
4102 {
4103 return 0;
4104 }
4105 let h = &self.kv_history;
4106 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
4107 let n = lcp.min(input_ids.len().saturating_sub(1));
4108 if n == 0 || self.kv_cache.layers.len() < self.num_layers {
4109 return 0;
4110 }
4111 let kv_id = self.graph_kv_id;
4112 for li in 0..self.num_layers {
4113 let l = &self.kv_cache.layers[li];
4114 let tail_ok = l.base() == 0
4115 || self
4116 .layer_window(li)
4117 .is_some_and(|w| l.base() + w <= n + 1);
4118 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
4119 || l.o1.is_some()
4120 || l.bounded.is_some()
4121 || !l.linear_state.is_empty()
4122 || l.mode != crate::kv_cache::KvMode::F32
4123 || l.pos_len() != h.len()
4124 || !tail_ok
4125 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
4126 || crate::gpu::graph_state_resident(kv_id, li)
4127 {
4128 return 0;
4129 }
4130 }
4131 n
4132 }
4133
4134 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
4137 self.cached_prefix_len(input_ids)
4138 }
4139
4140 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
4146 let on_device = self.device_sequence_position().is_some();
4147 if self.bounded_native() {
4148 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
4149 let prev_device = self.kv_prefix.on_device();
4150 self.kv_history.clear();
4151 self.kv_history_device = false;
4152 if keep && prev_device == on_device {
4153 self.kv_prefix.extend(&consumed[reused..]);
4154 } else {
4155 self.kv_prefix.set(consumed);
4156 }
4157 self.kv_prefix.set_on_device(on_device);
4158 } else {
4159 self.kv_history = consumed.to_vec();
4160 self.kv_history_device = on_device;
4161 }
4162 }
4163
4164 pub fn o1_active(&self) -> bool {
4166 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
4167 }
4168
4169 pub fn generation_batch_k(&self) -> usize {
4187 if let Some(k) = std::env::var("CMF_BATCH_K")
4188 .ok()
4189 .and_then(|v| v.parse::<usize>().ok())
4190 {
4191 return k;
4192 }
4193 if self.g4_batch_prefill() {
4196 return 128;
4197 }
4198 #[cfg(not(target_os = "macos"))]
4199 if self.graph_prefill_preferred() && !self.o1_active() {
4200 let moe = self
4203 .weights
4204 .layers
4205 .iter()
4206 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
4207 let q82 = crate::gpu::moe_q82_graph_on()
4213 && self.weights.layers.iter().any(|lw| {
4214 matches!(&lw.ffn, FfnKind::Moe(m)
4215 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
4216 });
4217 return if q82 {
4218 128
4219 } else if moe {
4220 32
4221 } else {
4222 128
4223 };
4224 }
4225 0
4226 }
4227
4228 pub fn generation_graph_prefill(&self) -> bool {
4229 let graph = self.graph_prefill_preferred();
4230 #[cfg(not(target_os = "macos"))]
4241 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
4242 return false;
4243 }
4244 graph
4245 }
4246
4247 pub fn o1_device_stats(&self) -> (usize, u64) {
4252 crate::gpu::o1_device_stats(self.graph_kv_id)
4253 }
4254
4255 pub fn o1_begin(&mut self) {
4260 self.o1_begin_with_prefix(None);
4261 }
4262
4263 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
4267 if let Some(c) = &self.o1_cfg {
4268 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
4269 let boundary = requested_prefix.map(|p| {
4270 p.max(
4271 crate::nystrom::o1_deferred_boundary(w, sink)
4272 .expect("o1 config boundary validated in set_o1"),
4273 )
4274 });
4275 for (li, &f) in self.o1_flags.iter().enumerate() {
4276 if f {
4277 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
4278 }
4279 }
4280 }
4281 }
4282
4283 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
4285 self.o1_cfg.as_ref().and_then(|c| {
4286 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
4287 .map(|floor| requested_prefix.max(floor))
4288 })
4289 }
4290
4291 fn o1_note_transition(&mut self) {
4292 let mut transitioned = false;
4296 for (li, &flagged) in self.o1_flags.iter().enumerate() {
4297 if flagged {
4298 transitioned |= self.kv_cache.layers[li].take_o1_transition();
4299 }
4300 }
4301 if transitioned {
4302 self.o1_epoch = self.o1_epoch.wrapping_add(1);
4303 }
4304 }
4305
4306 fn o1_pending(&self) -> bool {
4307 self.o1_flags.iter().enumerate().any(|(li, &f)| {
4308 f && self.kv_cache.layers[li].seq_len > 0
4309 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
4310 })
4311 }
4312
4313 fn o1_fail(&mut self, err: String) {
4314 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
4315 self.clear_sequence_state();
4316 self.graph_failed
4317 .store(true, std::sync::atomic::Ordering::Relaxed);
4318 self.cancel
4319 .store(true, std::sync::atomic::Ordering::Relaxed);
4320 }
4321
4322 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
4327 if self.o1_cfg.is_none() {
4328 return Ok(false);
4329 }
4330 let mut participating = false;
4331 for li in 0..self.num_layers {
4332 if !self.o1_flags.get(li).copied().unwrap_or(false) {
4333 continue;
4334 }
4335 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4336 return Err(err);
4337 }
4338 if self.kv_cache.layers[li].seq_len == 0 {
4339 continue;
4340 }
4341 participating = true;
4342 let num_heads = self.layer_num_heads(li);
4343 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
4344 }
4345 self.o1_note_transition();
4346 for li in 0..self.num_layers {
4347 if self.o1_flags.get(li).copied().unwrap_or(false) {
4348 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4349 return Err(err);
4350 }
4351 }
4352 }
4353 Ok(participating
4354 && (0..self.num_layers).all(|li| {
4355 !self.o1_flags.get(li).copied().unwrap_or(false)
4356 || self.kv_cache.layers[li].seq_len == 0
4357 || self.kv_cache.layers[li].o1_sealed()
4358 }))
4359 }
4360
4361 fn o1_progress(&mut self) {
4364 if !self.o1_active() {
4365 return;
4366 }
4367 for li in 0..self.num_layers {
4368 if self.o1_flags.get(li).copied().unwrap_or(false) {
4369 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
4370 self.o1_fail(err);
4371 return;
4372 }
4373 }
4374 }
4375 self.o1_note_transition();
4379 if !self.o1_pending() {
4380 return;
4381 }
4382 if let Err(err) = self.o1_seal_checked() {
4383 self.o1_fail(err);
4384 }
4385 }
4386
4387 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
4392 if self
4393 .graph_failed
4394 .swap(false, std::sync::atomic::Ordering::Relaxed)
4395 {
4396 self.cancel
4397 .store(false, std::sync::atomic::Ordering::Relaxed);
4398 self.clear_sequence_state();
4399 return Err(format!("{phase}: deferred O(1) transition failed"));
4400 }
4401 Ok(())
4402 }
4403
4404 pub fn o1_seal(&mut self) {
4408 if let Err(err) = self.o1_seal_checked() {
4409 self.o1_fail(err);
4410 }
4411 }
4412
4413 pub fn set_trace(&mut self, on: bool) {
4415 self.trace = on;
4416 }
4417
4418 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
4421 self.rng = match config.seed {
4422 Some(seed) => SplitMix64::new(seed),
4423 None => SplitMix64::from_entropy(),
4424 };
4425 self.sampler_config = config;
4426 }
4427
4428 pub fn set_confidence(&mut self, on: bool) {
4433 self.confidence_on = on;
4434 }
4435
4436 pub fn set_calib_temp(&mut self, t: f32) {
4439 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
4440 }
4441
4442 pub fn calib_temp(&self) -> f32 {
4444 self.calib_temp
4445 }
4446
4447 pub fn rope_inv_freq(&self) -> &[f32] {
4451 self.inv_freq.as_slice()
4452 }
4453
4454 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
4457 self.rotary_dim = rotary_dim.min(self.head_dim);
4458 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
4459 self.embryo_graph = None;
4463 }
4464
4465 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
4466 QwenAttnCfg {
4467 num_heads: self.num_heads,
4468 num_kv_heads: self.num_kv_heads,
4469 head_dim: self.head_dim,
4470 hidden_size: self.hidden_size,
4471 position,
4472 inv_freq: &self.inv_freq,
4473 rotary_dim: self.rotary_dim,
4474 scale: self.attn_scale,
4475 softcap: self.attn_softcap,
4476 window: None,
4477 v_norm: false,
4478 qk_norm_after_rope: self.qk_norm_after_rope,
4479 gate_sigmoid: self.proj_gate_sigmoid,
4480 q_norm: None,
4481 k_norm: None,
4482 output_gate: false,
4483 softplus_gate: None,
4484 rope_scale: self.rope_scale,
4485 bias: None,
4486 rms_eps: self.rms_eps,
4487 norm_style: self.norm_style,
4488 pool: self.pool.as_deref(),
4489 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
4490 }
4491 }
4492
4493 pub fn generate(
4495 &mut self,
4496 prompt: &str,
4497 max_tokens: usize,
4498 task_mask: Option<&TaskMask>,
4499 on_token: Option<TokenCallback>,
4500 ) -> Result<GenerateResult, String> {
4501 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
4502 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
4503 }
4504
4505 pub fn generate_from_vl(
4508 &mut self,
4509 input: &crate::dsv41_vision::PreparedVlInputs,
4510 max_tokens: usize,
4511 task_mask: Option<&TaskMask>,
4512 on_token: Option<TokenCallback>,
4513 ) -> Result<GenerateResult, String> {
4514 let Some(dsv41) = &self.dsv41 else {
4515 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
4516 };
4517 if input.token_ids.is_empty() {
4518 return Err("empty V4.1 multimodal prompt".into());
4519 }
4520 if input.token_types.len() != input.token_ids.len() {
4521 return Err(format!(
4522 "V4.1 token type count {} != token count {}",
4523 input.token_types.len(),
4524 input.token_ids.len()
4525 ));
4526 }
4527 let dim = dsv41.2.dim;
4528 let mut embeddings = vec![None; input.token_ids.len()];
4529 let mut participates = vec![true; input.token_ids.len()];
4530 if !input.images.is_empty() {
4531 let vision = self
4532 .dsv41_vision
4533 .as_ref()
4534 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4535 for image in &input.images {
4536 let end = image.start.saturating_add(image.types.len());
4537 if end > input.token_ids.len() {
4538 return Err(format!(
4539 "V4.1 image span {}..{} exceeds prompt length {}",
4540 image.start,
4541 end,
4542 input.token_ids.len()
4543 ));
4544 }
4545 let mut span = vec![0.0f32; image.types.len() * dim];
4546 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4547 for (offset, &kind) in image.types.iter().enumerate() {
4548 let pos = image.start + offset;
4549 if input.token_types[pos] != kind {
4550 return Err(format!(
4551 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4552 input.token_types[pos]
4553 ));
4554 }
4555 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4556 participates[pos] = false;
4557 }
4558 }
4559 }
4560 for (pos, &kind) in input.token_types.iter().enumerate() {
4561 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4562 return Err(format!("V4.1 text position {pos} has an image embedding"));
4563 }
4564 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4565 return Err(format!("V4.1 image position {pos} has no image embedding"));
4566 }
4567 }
4568 self.dsv41_prefill = Some((embeddings, participates));
4569 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4570 self.dsv41_prefill = None;
4571 result
4572 }
4573
4574 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4576 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4577 }
4578
4579 pub fn generate_from_ids(
4587 &mut self,
4588 input_ids: &[u32],
4589 max_tokens: usize,
4590 task_mask: Option<&TaskMask>,
4591 on_token: Option<TokenCallback>,
4592 ) -> Result<GenerateResult, String> {
4593 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4594 }
4595
4596 pub fn generate_from_embeds(
4602 &mut self,
4603 input_ids: &[u32],
4604 prompt_rows: &[f32],
4605 max_tokens: usize,
4606 task_mask: Option<&TaskMask>,
4607 on_token: Option<TokenCallback>,
4608 ) -> Result<GenerateResult, String> {
4609 if input_ids.is_empty()
4610 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4611 {
4612 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4613 }
4614 if prompt_rows.iter().any(|x| !x.is_finite()) {
4615 return Err("embedded prompt contains non-finite values".into());
4616 }
4617 if !self.can_prefill_batched()
4618 || self.dyn_router.is_some()
4619 || self.o1_active()
4620 || self.mtp.is_some()
4621 || self.gpu_plan.is_some()
4622 {
4623 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4624 }
4625 self.generate_with_prompt_rows(
4626 input_ids,
4627 Some(prompt_rows),
4628 max_tokens,
4629 task_mask,
4630 on_token,
4631 )
4632 }
4633
4634 fn generate_with_prompt_rows(
4635 &mut self,
4636 input_ids: &[u32],
4637 prompt_rows: Option<&[f32]>,
4638 max_tokens: usize,
4639 task_mask: Option<&TaskMask>,
4640 mut on_token: Option<TokenCallback>,
4641 ) -> Result<GenerateResult, String> {
4642 #[cfg(target_os = "macos")]
4643 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4644 if std::env::var("CMF_TRACE_H").is_ok() {
4645 eprintln!("input_ids: {input_ids:?}");
4646 }
4647 if input_ids.is_empty() {
4648 return Err("empty prompt: nothing to generate from".to_string());
4649 }
4650 self.graph_failed
4654 .store(false, std::sync::atomic::Ordering::Relaxed);
4655 let task_mask = self.drop_open_mask(task_mask);
4660 if self.clover.is_some() {
4663 if prompt_rows.is_some() || task_mask.is_some() {
4664 return Err("block-diffusion generation takes token prompts without task masks".into());
4665 }
4666 return self.clover_generate(input_ids, max_tokens, on_token);
4667 }
4668
4669 let mut reuse_from = {
4677 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4678 if on
4679 && prompt_rows.is_none()
4680 && task_mask.is_none()
4681 && self.mtp.is_none()
4682 && !(self.mimo_mtp.is_some() && self.speculative)
4683 && self.o1_cfg.is_none()
4684 && self.dsv41.is_none()
4685 {
4686 self.cached_prefix_len(input_ids)
4687 } else {
4688 0
4689 }
4690 };
4691 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4694 reuse_from = 0;
4695 }
4696 if reuse_from > 0 && !self.bounded_native() && reuse_from < self.kv_history.len() {
4699 for l in &mut self.kv_cache.layers {
4700 let d = l.pos_len() - reuse_from;
4701 l.truncate_last(d);
4702 }
4703 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4704 eprintln!(
4705 "kv-reuse: rolled back {} answer position(s) to the common prefix",
4706 self.kv_history.len() - reuse_from
4707 );
4708 }
4709 self.kv_history.truncate(reuse_from);
4710 self.graph_logits = None;
4711 }
4712 self.swa_trim_anchor = input_ids.len();
4713 self.last_prefill_tokens = input_ids.len() - reuse_from;
4714 let bounded_native = self.bounded_native();
4715 if reuse_from == 0 {
4716 self.reset_fresh_mtp_draft_state();
4721 self.clear_sequence_state();
4722 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4723 eprintln!(
4724 "kv-reuse: {} of {} prompt positions already cached",
4725 reuse_from,
4726 input_ids.len()
4727 );
4728 }
4729 crate::gpu::graph_race_begin_generation();
4730 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4734 std::env::var("CMF_O1_PREFILL")
4735 .ok()
4736 .and_then(|v| v.parse::<usize>().ok())
4737 .filter(|&p| p > 0)
4738 } else {
4739 None
4740 };
4741 if task_mask.is_none() {
4742 self.o1_begin_with_prefix(o1_prefill);
4743 }
4744
4745 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4751 #[cfg(target_os = "macos")]
4789 let metal_graph = crate::gpu::q1_force()
4790 && crate::gpu::enabled_here()
4791 && std::env::var("CMF_GPU_BLOCK")
4792 .map(|v| v != "0")
4793 .unwrap_or(true);
4794 #[cfg(target_os = "macos")]
4800 let native_metal_backend = crate::gpu::q1_force();
4801 #[cfg(not(target_os = "macos"))]
4802 let metal_graph = false;
4803 #[cfg(not(target_os = "macos"))]
4804 let native_metal_backend = false;
4805 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4806 let spec_cheap_round =
4810 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
4811 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4812 || match spec_sample_env.as_deref() {
4813 Some("1") => true,
4814 Some(_) => false,
4815 None => metal_graph && spec_cheap_round,
4816 };
4817 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4833 for lw in &self.weights.layers {
4834 if let FfnKind::Dense(d) = &lw.ffn {
4835 dense_n += 1;
4836 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4837 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4838 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4839 {
4840 dense_q4tp += 1;
4841 }
4842 }
4843 }
4844 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4845 let penalized = !metal_graph
4857 && (self.sampler_config.repetition_penalty != 1.0
4858 || self.sampler_config.presence_penalty != 0.0
4859 || !self.sampler_config.suppress_tokens.is_empty());
4860 #[cfg(feature = "gpu")]
4865 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4866 #[cfg(not(feature = "gpu"))]
4867 let metal_wgpu = false;
4868 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4869 let spec_wanted = graph_spec_wanted_by_policy(
4870 spec_env.as_deref(),
4871 spec_default_ok,
4872 penalized,
4873 metal_wgpu,
4874 native_metal_backend,
4875 );
4876 if spec_wanted && metal_wgpu {
4877 tracing::warn!(
4878 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4879 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4880 );
4881 }
4882 let graph_spec = self.speculative
4886 && (graph_on || metal_graph)
4887 && self.mtp.is_some()
4888 && task_mask.is_none()
4889 && !self.o1_active()
4890 && spec_sampling_ok
4891 && spec_wanted;
4892 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
4905 let pair_pays = host_pair_opt_in
4906 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
4907 #[cfg(target_os = "macos")]
4911 if metal_graph {
4912 static SAID: std::sync::Once = std::sync::Once::new();
4913 SAID.call_once(|| {
4914 let spec = if graph_spec {
4915 let k = std::env::var("CMF_GRAPH_SPEC_K")
4916 .ok()
4917 .and_then(|v| v.parse::<usize>().ok())
4918 .filter(|&v| (1..=8).contains(&v))
4919 .unwrap_or(7);
4920 let arm = if self.sampler_config.temperature < 1e-6 {
4921 "greedy"
4922 } else {
4923 "sampling"
4924 };
4925 format!(
4926 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4927 Self::draft_vocab_rows(usize::MAX)
4928 )
4929 } else if !self.speculative {
4930 "spec off (CMF_MTP=0)".to_string()
4931 } else if self.mtp.is_none() {
4932 "spec off (no MTP head)".to_string()
4933 } else if !spec_sampling_ok {
4934 if spec_cheap_round {
4935 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4936 } else {
4937 "spec off (sampling without a top-k: the dense chain \
4938 costs more than it saves)"
4939 .to_string()
4940 }
4941 } else if !spec_wanted {
4942 if spec_env.is_none() {
4943 "spec off (native Metal batch verifier requires CMF_GRAPH_SPEC=1 after exact-ID validation)".to_string()
4944 } else {
4945 "spec off (native Metal accepts only exact CMF_GRAPH_SPEC=1)".to_string()
4946 }
4947 } else if task_mask.is_some() {
4948 "spec off (task mask)".to_string()
4949 } else if !graph_spec && !pair_pays {
4950 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
4951 .to_string()
4952 } else {
4953 "spec off (O(1) attention)".to_string()
4954 };
4955 let on = |var: &str| {
4956 if std::env::var(var).as_deref() == Ok("0") {
4957 "off"
4958 } else {
4959 "on"
4960 }
4961 };
4962 tracing::info!(
4963 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4964 MTP graph {}, attend {}, probe {}",
4965 if crate::gpu_metal::state4_on() {
4966 "on"
4967 } else {
4968 "off"
4969 },
4970 if crate::gpu_metal::async_replay_on() {
4971 "on"
4972 } else {
4973 "off"
4974 },
4975 on("CMF_METAL_PREFILL"),
4976 on("CMF_MTP_GRAPH"),
4977 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4978 if crate::gpu::probe_enabled() {
4979 "bypassed (q1 force)"
4980 } else {
4981 "off"
4982 },
4983 );
4984 });
4985 }
4986 let spec_active = self.speculative
4987 && self.mtp.is_some()
4988 && task_mask.is_none()
4989 && !self.o1_active()
4990 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4991 let mut mtp = if spec_active { self.mtp.take() } else { None };
4994 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4995 eprintln!(
4996 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4997 mtp.is_some(),
4998 self.speculative,
4999 self.sampler_config.temperature < 1e-6,
5000 );
5001 }
5002 if let Some(m) = &mut mtp {
5003 m.kv.clear();
5004 crate::gpu::graph_kv_reset(self.mtp_kv_id());
5006 self.mtp_graph_mode = None;
5007 }
5008 let mimo_spec = self.speculative
5012 && self.mimo_mtp.is_some()
5013 && task_mask.is_none()
5014 && !self.o1_active()
5015 && self.dyn_router.is_none()
5016 && self.sampler_config.temperature < 1e-6
5017 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
5018 if let Some(st) = self.mimo_mtp.as_mut() {
5019 st.reset();
5020 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
5021 Self::mimo_mtp_hist_cap(st, input_ids.len());
5022 }
5023 }
5024 let mut router = if mtp.is_none() {
5028 self.dyn_router.take()
5029 } else {
5030 None
5031 };
5032 let mut reuse_from = reuse_from;
5033 if let Some(r) = &mut router {
5034 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
5037 let _ = self.set_active_skill(None);
5040 reuse_from = 0;
5041 self.last_prefill_tokens = input_ids.len();
5042 }
5043 }
5044
5045 let mut all_ids = input_ids.to_vec();
5046 let mut generated = 0usize;
5047 let mut finish_reason = "max_tokens".to_string();
5048 let mut drafted = 0usize;
5049 let mut accepted = 0usize;
5050 let mut dsv4_spec_bad = 0usize;
5057 let mut dsv4_spec_retry_at = 0usize;
5058 let mut confidence: Vec<f32> = Vec::new();
5059 let trace_on = self.trace;
5060 let calib_temp = self.calib_temp;
5061 let mut traces: Vec<TokenTrace> = Vec::new();
5062
5063 let mut hidden = vec![0.0f32; self.hidden_size];
5069 let mut pos = reuse_from;
5070 let fuse_lm = mtp.is_none()
5079 && router.is_none()
5080 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
5081 self.graph_logits = None;
5082 self.graph_want_logits = false;
5083 let _tpf = std::time::Instant::now();
5084 let batch_k = self.generation_batch_k();
5085 if let Some(rows) = prompt_rows {
5086 let hs = self.hidden_size;
5087 let chunk = self.prefill_chunk().max(1);
5088 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5089 let end = (pos + chunk).min(input_ids.len());
5090 let hb = match self.prefill_input_rows(
5091 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
5092 pos,
5093 task_mask,
5094 ) {
5095 Ok(hb) => hb,
5096 Err(err) => {
5097 self.finish_generation(&mut mtp, &mut router, true);
5098 return Err(err);
5099 }
5100 };
5101 if mimo_spec {
5102 self.mimo_note_rows(&hb, pos);
5103 }
5104 hidden.copy_from_slice(&hb[hb.len() - hs..]);
5105 pos = end;
5106 }
5107 }
5108 while self.qwen4_exp.is_some()
5119 && mtp.is_none()
5120 && pos < input_ids.len()
5121 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5122 {
5123 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
5126 let want_logits = end == input_ids.len();
5127 let mut lg = Vec::new();
5128 if let Some(b) = &mut self.qwen4_exp {
5129 crate::qwen4_exp::forward_tokens(
5130 &b.0,
5131 &b.1,
5132 &b.2,
5133 &mut b.3,
5134 &input_ids[pos..end],
5135 pos,
5136 &self.inv_freq,
5137 self.pool.as_deref(),
5138 &mut lg,
5139 want_logits,
5140 );
5141 }
5142 if want_logits {
5143 self.graph_logits = Some(lg);
5144 }
5145 pos = end;
5146 hidden.fill(0.0);
5147 }
5148 while self.dsv4.is_some()
5149 && mtp.is_none()
5150 && pos < input_ids.len()
5151 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5152 {
5153 let end = (pos + prefill_chunk()).min(input_ids.len());
5154 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5155 let mut lg = Vec::new();
5156 if let Some(b) = &mut self.dsv4 {
5157 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
5158 crate::dsv4::forward_chunk(
5159 g,
5160 layers,
5161 &cfg,
5162 st,
5163 &ids,
5164 pos,
5165 &self.inv_freq,
5166 self.pool.as_deref(),
5167 &mut lg,
5168 end == input_ids.len(),
5169 );
5170 }
5171 if end == input_ids.len() {
5172 self.graph_logits = Some(lg);
5173 }
5174 pos = end;
5175 hidden = vec![0.0; self.hidden_size];
5176 }
5177 let dsv41_prefill = self.dsv41_prefill.take();
5178 while self.dsv41.is_some()
5179 && mtp.is_none()
5180 && pos < input_ids.len()
5181 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5182 {
5183 let end = (pos + prefill_chunk()).min(input_ids.len());
5184 let ids: Vec<u32> = input_ids[pos..end].to_vec();
5185 let mut lg = Vec::new();
5186 if let Some(b) = &mut self.dsv41 {
5187 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
5188 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
5189 crate::dsv41::forward_chunk_masked_with_embeddings(
5190 g,
5191 layers,
5192 cfg,
5193 st,
5194 &ids,
5195 pos,
5196 &embeddings[pos..end],
5197 &participates[pos..end],
5198 self.pool.as_deref(),
5199 &mut lg,
5200 );
5201 } else {
5202 crate::dsv41::forward_chunk(
5203 g,
5204 layers,
5205 cfg,
5206 st,
5207 &ids,
5208 pos,
5209 self.pool.as_deref(),
5210 &mut lg,
5211 );
5212 }
5213 }
5214 if end == input_ids.len() {
5215 self.graph_logits = Some(lg);
5216 }
5217 pos = end;
5218 hidden = vec![0.0; self.hidden_size];
5219 }
5220 let dyn_prefill = router.is_some();
5225 let o1_prefill_limit = o1_prefill
5233 .and_then(|requested| self.o1_effective_boundary(requested))
5234 .map(|boundary| boundary.min(input_ids.len()));
5235 let mut o1_sealed = false;
5236 if let Some(limit) = o1_prefill_limit {
5237 if self.can_prefill_batched() && limit > 2 {
5240 let chunk = self.prefill_chunk();
5241 let hs = self.hidden_size;
5242 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5243 let end = (pos + chunk).min(limit);
5244 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5245 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5246 pos = end;
5247 }
5248 } else {
5249 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5250 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
5251 pos += 1;
5252 }
5253 }
5254 if pos >= limit {
5255 o1_sealed = match self.o1_seal_checked() {
5256 Ok(sealed) => sealed,
5257 Err(err) => {
5258 self.finish_generation(&mut mtp, &mut router, true);
5259 return Err(err);
5260 }
5261 };
5262 tracing::info!(
5263 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
5264 o1_prefill.unwrap_or(0),
5265 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
5266 .unwrap_or(limit),
5267 limit,
5268 input_ids.len()
5269 );
5270 }
5271 }
5272 let graph_prefill = self.graph_prefill_preferred();
5278 #[cfg(target_os = "macos")]
5286 if task_mask.is_none()
5287 && !dyn_prefill
5288 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
5289 && crate::gpu::enabled_here()
5290 && self.gdn_cfg.is_some()
5291 && !self.per_token_stack()
5292 && input_ids.len() > 8
5293 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
5294 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
5295 {
5296 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
5297 .ok()
5298 .and_then(|v| v.parse().ok())
5299 .filter(|&v| (16..=512).contains(&v))
5300 .unwrap_or(256);
5301 let hs = self.hidden_size;
5302 let _tp = std::time::Instant::now();
5303 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5304 let end = (pos + chunk).min(input_ids.len());
5305 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
5306 MetalPrefillOutcome::Completed(hb) => hb,
5307 MetalPrefillOutcome::Declined => break,
5308 MetalPrefillOutcome::Failed => {
5309 self.finish_generation(&mut mtp, &mut router, true);
5310 return Err("ordinary Metal prefill failed after admission".into());
5311 }
5312 };
5313 if let Some(m) = &mut mtp {
5314 let n_pairs = if end < input_ids.len() {
5315 end - pos
5316 } else {
5317 end - pos - 1
5318 };
5319 if n_pairs > 0 {
5320 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
5321 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
5322 .collect();
5323 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
5324 for (j, (h, t)) in pairs.iter().enumerate() {
5325 let h = h.to_vec();
5326 let _ = self.mtp_step(m, &h, *t, pos + j);
5327 }
5328 }
5329 }
5330 }
5331 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5332 pos = end;
5333 }
5334 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5335 eprintln!(
5336 "metal-prefill: {} of {} tokens in {:.1} ms",
5337 pos,
5338 input_ids.len(),
5339 _tp.elapsed().as_secs_f64() * 1e3
5340 );
5341 }
5342 }
5343 #[cfg(not(target_os = "macos"))]
5348 if task_mask.is_none()
5349 && !dyn_prefill
5350 && mtp.is_none()
5351 && o1_prefill.is_none()
5352 && batch_k > 0
5353 && pos + 1 < input_ids.len()
5354 && self.g4_batch_prefill()
5355 {
5356 let t_g4 = std::time::Instant::now();
5357 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
5358 Ok((n, last)) => {
5359 if n > 0 {
5360 hidden.copy_from_slice(&last);
5361 }
5362 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5363 eprintln!(
5364 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
5365 input_ids.len() - pos,
5366 t_g4.elapsed().as_secs_f64() * 1e3
5367 );
5368 }
5369 pos += n;
5370 }
5371 Err(e) => {
5372 self.finish_generation(&mut mtp, &mut router, true);
5373 return Err(e);
5374 }
5375 }
5376 }
5377 self.mimo_moe_prepare();
5378 #[cfg(not(target_os = "macos"))]
5384 if task_mask.is_none()
5385 && !dyn_prefill
5386 && !graph_prefill
5387 && mtp.is_none()
5388 && o1_prefill.is_none()
5389 && !self.o1_active()
5390 && input_ids.len() > 2
5391 && self.batch_prefix_prefill()
5392 {
5393 let chunk = self.prefill_chunk().max(1);
5394 let hs = self.hidden_size;
5395 let t_bp = std::time::Instant::now();
5396 let pos0 = pos;
5397 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5398 let end = (pos + chunk).min(input_ids.len());
5399 let bk = end - pos;
5400 let mut hiddens = vec![0f32; bk * hs];
5401 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5402 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5403 }
5404 let positions: Vec<usize> = (pos..end).collect();
5405 let mut run = 0usize;
5406 let outcome = self.try_batch_graph_wgpu_prefix(
5407 &mut hiddens,
5408 &positions,
5409 bk,
5410 None,
5411 Some(&mut run),
5412 );
5413 match outcome {
5414 crate::gpu::BatchGraphOutcome::Completed => {
5415 let hb = if run < self.num_layers {
5416 self.prefill_batch_span(
5417 PrefillIn::Hidden(&hiddens),
5418 pos,
5419 None,
5420 run,
5421 self.num_layers,
5422 )
5423 } else {
5424 hiddens
5425 };
5426 if mimo_spec {
5427 self.mimo_note_rows(&hb, pos);
5428 }
5429 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
5430 pos = end;
5431 }
5432 crate::gpu::BatchGraphOutcome::Failed => {
5433 self.finish_generation(&mut mtp, &mut router, true);
5434 return Err("batched prefix prefill failed after admission".into());
5435 }
5436 crate::gpu::BatchGraphOutcome::Declined => {
5437 #[cfg(feature = "gpu")]
5440 if pos > pos0 {
5441 self.pull_lagging_host_kv(0, self.num_layers, pos);
5442 }
5443 break;
5444 }
5445 }
5446 }
5447 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5448 eprintln!(
5449 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
5450 pos - pos0,
5451 input_ids.len(),
5452 t_bp.elapsed().as_secs_f64() * 1e3
5453 );
5454 }
5455 }
5456 if task_mask.is_none()
5457 && !dyn_prefill
5458 && !graph_prefill
5459 && self.can_prefill_batched()
5460 && !self.per_token_stack()
5461 && o1_prefill.is_none()
5462 && input_ids.len() > 2
5463 {
5464 let chunk = self.prefill_chunk();
5470 let hs = self.hidden_size;
5471 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5472 let end = (pos + chunk).min(input_ids.len());
5473 let hb = self.prefill_batch(&input_ids[pos..end], pos);
5474 if mimo_spec {
5475 self.mimo_note_rows(&hb, pos);
5476 }
5477 if let Some(m) = &mut mtp {
5478 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5479 .ok()
5480 .and_then(|v| v.parse().ok())
5481 .unwrap_or(0);
5482 for p in pos..end {
5483 if p + 1 < input_ids.len() {
5484 if probe >= 1 && p + 2 < input_ids.len() {
5485 let (d1, mut hx) = self.mtp_step_h(
5489 m,
5490 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5491 input_ids[p + 1],
5492 p,
5493 );
5494 let mut ok = d1 == input_ids[p + 2];
5495 Self::chain_probe_note(0, ok);
5496 let mut d_prev = d1;
5497 let mut extra = 0usize;
5498 for j in 1..probe {
5499 if p + 2 + j >= input_ids.len() {
5500 break;
5501 }
5502 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
5503 extra += 1;
5504 ok = ok && dj == input_ids[p + 2 + j];
5505 Self::chain_probe_note(j, ok);
5506 d_prev = dj;
5507 hx = hj;
5508 }
5509 m.kv.truncate_last(extra);
5510 } else {
5511 let _ = self.mtp_step(
5512 m,
5513 &hb[(p - pos) * hs..(p - pos + 1) * hs],
5514 input_ids[p + 1],
5515 p,
5516 );
5517 }
5518 }
5519 }
5520 }
5521 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
5522 pos = end;
5523 }
5524 }
5525 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
5526 if task_mask.is_none()
5527 && !dyn_prefill
5528 && !graph_prefill
5529 && !pair_off
5530 && self.pair_supported()
5531 && o1_prefill.is_none()
5532 {
5533 while pos + 1 < input_ids.len()
5534 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5535 {
5536 let e1 = self.embed_single(input_ids[pos]);
5537 let e2 = self.embed_single(input_ids[pos + 1]);
5538 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
5539 if mimo_spec {
5540 self.mimo_note_rows(&h1, pos);
5541 self.mimo_note_rows(&h2, pos + 1);
5542 }
5543 self.commit_linear_scratch();
5545 if let Some(m) = &mut mtp {
5546 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
5547 if pos + 2 < input_ids.len() {
5548 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5549 .ok()
5550 .and_then(|v| v.parse().ok())
5551 .unwrap_or(0);
5552 if probe >= 1 && pos + 3 < input_ids.len() {
5553 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
5557 let mut ok = d1 == input_ids[pos + 3];
5558 Self::chain_probe_note(0, ok);
5559 let mut d_prev = d1;
5560 let mut extra = 0usize;
5561 for j in 1..probe {
5562 if pos + 3 + j >= input_ids.len() {
5563 break;
5564 }
5565 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
5566 extra += 1;
5567 ok = ok && dj == input_ids[pos + 3 + j];
5568 Self::chain_probe_note(j, ok);
5569 d_prev = dj;
5570 hx = hj;
5571 }
5572 m.kv.truncate_last(extra);
5573 } else {
5574 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
5575 }
5576 }
5577 }
5578 hidden = h2;
5579 pos += 2;
5580 }
5581 }
5582 let o1_batch_ready = o1_sealed
5595 && o1_prefill.is_some()
5596 && mtp.is_none()
5597 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5598 && (0..self.num_layers).all(|li| {
5599 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5600 cache.o1.is_none() || cache.o1_views().is_some()
5601 });
5602 let mtp_batch_prefill = mtp.is_some()
5607 && graph_prefill
5608 && task_mask.is_none()
5609 && !dyn_prefill
5610 && !self.o1_active()
5611 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5612 if batch_k > 0
5613 && (graph_prefill || o1_batch_ready)
5614 && task_mask.is_none()
5615 && (!self.o1_active() || o1_batch_ready)
5616 && (mtp.is_none() || mtp_batch_prefill)
5617 && !dyn_prefill
5618 && pos + 1 < input_ids.len()
5619 {
5620 let hs = self.hidden_size;
5621 let chunk = batch_k;
5622 while pos < input_ids.len() {
5623 let end = (pos + chunk).min(input_ids.len());
5624 let bk = end - pos;
5625 let mut hiddens = vec![0f32; bk * hs];
5626 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5627 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5628 }
5629 let positions: Vec<usize> = (pos..end).collect();
5630 let t_chunk = std::time::Instant::now();
5631 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5632 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5633 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5634 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5635 eprintln!(
5636 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5637 if o1_batch_ready {
5638 "o1"
5639 } else if mtp_batch_prefill {
5640 "ordinary_mtp"
5641 } else {
5642 "ordinary"
5643 },
5644 bk as f64 / (ms / 1000.0)
5645 );
5646 }
5647 {
5648 use std::sync::atomic::{AtomicBool, Ordering};
5649 static SAID: AtomicBool = AtomicBool::new(false);
5650 if !SAID.swap(true, Ordering::Relaxed) {
5651 if ok_b {
5652 tracing::info!(
5653 "batched prefill: ACTIVE mode={} (k={bk})",
5654 if o1_batch_ready {
5655 "o1"
5656 } else if mtp_batch_prefill {
5657 "ordinary_mtp"
5658 } else {
5659 "ordinary"
5660 }
5661 );
5662 } else {
5663 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5664 }
5665 }
5666 }
5667 if ok_b {
5668 if mimo_spec {
5669 self.mimo_note_rows(&hiddens, pos);
5670 }
5671 if mtp_batch_prefill {
5672 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5673 if n_pairs > 0 {
5674 let rows: Vec<Vec<f32>> = (0..n_pairs)
5680 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5681 .collect();
5682 let pairs: Vec<(&[f32], u32)> = rows
5683 .iter()
5684 .enumerate()
5685 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5686 .collect();
5687 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5688 eprintln!(
5689 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5690 pos,
5691 n_pairs,
5692 pos + n_pairs - 1,
5693 );
5694 }
5695 let warm_error = if let Some(m) = mtp.as_mut() {
5696 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5697 } else {
5698 None
5699 };
5700 if let Some(err) = warm_error {
5701 self.finish_generation(&mut mtp, &mut router, true);
5706 return Err(err.to_string());
5707 }
5708 }
5709 }
5710 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5711 pos = end;
5712 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5713 self.finish_generation(&mut mtp, &mut router, true);
5718 return Err(if o1_batch_ready {
5719 "sealed O(1) batch graph failed after admission".to_string()
5720 } else {
5721 "ordinary recurrent batch graph failed after admission".to_string()
5722 });
5723 } else {
5724 break; }
5726 }
5727 }
5728 if graph_prefill
5732 && task_mask.is_none()
5733 && mtp.is_none()
5734 && !dyn_prefill
5735 && pos == 0
5736 && input_ids.len() > 1
5737 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5738 {
5739 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5740 self.graph_logits = Some(lg);
5741 hidden = vec![0.0; self.hidden_size];
5742 pos = input_ids.len();
5743 }
5744 }
5745 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5746 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5747 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5748 if mimo_spec {
5749 self.mimo_note_rows(&hidden, pos);
5750 }
5751 if let Some(m) = &mut mtp {
5752 if pos + 1 < input_ids.len() {
5753 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5759 .ok()
5760 .and_then(|v| v.parse().ok())
5761 .unwrap_or(0);
5762 if probe >= 1 && pos + 2 < input_ids.len() {
5763 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5764 let mut ok = d1 == input_ids[pos + 2];
5765 Self::chain_probe_note(0, ok);
5766 let mut d_prev = d1;
5767 let mut extra = 0usize;
5768 for j in 1..probe {
5769 if pos + 2 + j >= input_ids.len() {
5770 break;
5771 }
5772 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5773 extra += 1;
5774 ok = ok && dj == input_ids[pos + 2 + j];
5775 Self::chain_probe_note(j, ok);
5776 d_prev = dj;
5777 hx = hj;
5778 }
5779 m.kv.truncate_last(extra);
5782 } else {
5783 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5784 }
5785 }
5786 }
5787 pos += 1;
5788 }
5789 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5790 eprintln!(
5791 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5792 input_ids.len(),
5793 _tpf.elapsed().as_secs_f64() * 1000.0
5794 );
5795 }
5796 if self
5797 .graph_failed
5798 .swap(false, std::sync::atomic::Ordering::Relaxed)
5799 {
5800 self.finish_generation(&mut mtp, &mut router, true);
5805 return Err("GPU token graph failed during prefill".to_string());
5806 }
5807 if self
5810 .cancel
5811 .swap(false, std::sync::atomic::Ordering::Relaxed)
5812 {
5813 self.finish_generation(&mut mtp, &mut router, true);
5817 return Ok(GenerateResult {
5818 text: String::new(),
5819 token_ids: Vec::new(),
5820 prompt_tokens: input_ids.len(),
5821 tokens_generated: 0,
5822 finish_reason: "cancelled".to_string(),
5823 mtp_drafted: 0,
5824 mtp_accepted: 0,
5825 token_confidence: Vec::new(),
5826 traces: Vec::new(),
5827 });
5828 }
5829
5830 if !o1_sealed {
5833 match self.o1_seal_checked() {
5834 Ok(_) => {}
5835 Err(err) => {
5836 self.finish_generation(&mut mtp, &mut router, true);
5837 return Err(err);
5838 }
5839 }
5840 }
5841
5842 macro_rules! commit {
5844 ($id:expr) => {{
5845 all_ids.push($id);
5846 generated += 1;
5847 self.note_draft_id($id);
5848 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5849 finish_reason = "stop".to_string();
5850 false
5851 } else {
5852 let token_text = self.tokenizer.decode_token($id);
5853 let mut go = true;
5854 if let Some(ref mut cb) = on_token {
5855 if !cb(&token_text) {
5856 finish_reason = "cancelled".to_string();
5857 go = false;
5858 }
5859 }
5860 go
5861 }
5862 }};
5863 }
5864
5865 let mut spec_trial = SpecTrial::Spec {
5876 t0: std::time::Instant::now(),
5877 gen0: generated,
5878 rounds: 0,
5879 };
5880 let mut spec_mon = SpecMon {
5886 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5887 ..SpecMon::default()
5888 };
5889 let mut spec_watchdog_off = false;
5890 let mut spec_walls: Vec<f32> = Vec::new();
5893 let mut spec_round_end: Option<std::time::Instant> = None;
5896 if mimo_spec {
5897 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5898 if let Some(mut st) = self.mimo_mtp.take() {
5899 self.mimo_mtp_probe(&mut st, input_ids, &path);
5900 self.mimo_mtp = Some(st);
5901 }
5902 }
5903 }
5904 let mut next_pos = input_ids.len();
5906 'decode: while generated < max_tokens {
5907 if self
5908 .graph_failed
5909 .swap(false, std::sync::atomic::Ordering::Relaxed)
5910 {
5911 self.finish_generation(&mut mtp, &mut router, true);
5916 return Err("GPU token graph failed during decode".to_string());
5917 }
5918 if self
5919 .cancel
5920 .swap(false, std::sync::atomic::Ordering::Relaxed)
5921 {
5922 finish_reason = "cancelled".to_string();
5923 break 'decode;
5924 }
5925 if mimo_spec && next_pos > 0 {
5930 self.mimo_note_rows(&hidden, next_pos - 1);
5933 }
5934 let forced = self.spec_forced.take();
5935 let mut logits = match (forced, self.graph_logits.take()) {
5936 (Some(_), _) => Vec::new(),
5937 (None, Some(lg)) => lg,
5938 (None, None) => {
5939 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5940 #[cfg(feature = "gpu")]
5945 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
5946 lg
5947 } else {
5948 inference::rms_norm_into(
5949 &hidden,
5950 &self.weights.final_norm,
5951 self.rms_eps,
5952 self.norm_style,
5953 &mut self.ws.n1,
5954 );
5955 self.lm_head_forward(&self.ws.n1)
5956 };
5957 #[cfg(not(feature = "gpu"))]
5958 let logits_out = {
5959 inference::rms_norm_into(
5960 &hidden,
5961 &self.weights.final_norm,
5962 self.rms_eps,
5963 self.norm_style,
5964 &mut self.ws.n1,
5965 );
5966 self.lm_head_forward(&self.ws.n1)
5967 };
5968 logits_out
5969 }
5970 };
5971 if generated
5974 == std::env::var("CMF_LOGIT_DUMP_STEP")
5975 .ok()
5976 .and_then(|v| v.parse().ok())
5977 .unwrap_or(0)
5978 {
5979 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5980 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5981 for v in hidden.iter().chain(logits.iter()) {
5982 bytes.extend_from_slice(&v.to_le_bytes());
5983 }
5984 if let Err(e) = std::fs::write(&path, &bytes) {
5985 eprintln!("logit dump: failed to write {path}: {e}");
5986 self.finish_generation(&mut mtp, &mut router, true);
5987 return Err(format!("logit dump write failed: {e}"));
5988 }
5989 }
5990 }
5991 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5995 if !logits.is_empty() {
5996 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5997 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5998 if let Err(e) =
5999 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
6000 {
6001 eprintln!("logit dump: failed to write {}: {e}", path.display());
6002 }
6003 }
6004 }
6005 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
6006 Some(c) => c,
6007 None => {
6008 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
6009 sampler::sample_with_scratch_pool(
6010 &logits,
6011 &self.sampler_config,
6012 self.sampler_config.penalty_past(&all_ids, bounded_native),
6013 &mut self.rng,
6014 &mut self.sampler_scratch,
6015 self.pool.as_deref(),
6016 )
6017 }
6018 };
6019 if self.confidence_on {
6020 confidence.push(if logits.is_empty() {
6021 0.0
6022 } else {
6023 sampler::top1_prob_pool(
6024 self.pool.as_deref(),
6025 &mut self.sampler_scratch,
6026 &logits,
6027 t_next,
6028 calib_temp,
6029 )
6030 });
6031 }
6032 if !logits.is_empty() {
6033 attention::recycle_buf(&mut logits);
6034 }
6035 if trace_on {
6036 let skill = router.as_ref().and_then(|r| r.active_id());
6040 traces.push(TokenTrace {
6041 t: generated,
6042 token_id: t_next,
6043 confidence: confidence.last().copied().unwrap_or(0.0),
6044 active_skill: skill,
6045 recon: None,
6046 switched: false,
6047 });
6048 }
6049 if !commit!(t_next) {
6050 break 'decode;
6051 }
6052 if generated >= max_tokens {
6053 break 'decode;
6054 }
6055
6056 self.swa_trim_tails();
6059 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
6060 static SAID: std::sync::Once = std::sync::Once::new();
6066 SAID.call_once(|| {
6067 tracing::warn!(
6068 "KV cache full at {} positions — evicting half; quality \
6069 will degrade. Raise CMF_MAX_SEQ.",
6070 self.kv_cache.max_seq_len,
6071 );
6072 });
6073 let keep = (self.kv_cache.max_seq_len / 2).max(1);
6074 self.kv_cache.evict(keep);
6075 }
6076
6077 if graph_spec {
6080 match spec_trial {
6081 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
6082 spec_mon.plain_ms =
6083 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
6084 let keep = spec_mon.pays();
6085 tracing::info!(
6086 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6087 spec_mon.tokens,
6088 spec_mon.round_ms,
6089 spec_mon.plain_ms,
6090 if keep { "speculating" } else { "plain" }
6091 );
6092 spec_mon.fails = 0;
6093 spec_trial = SpecTrial::Decided {
6094 spec: keep,
6095 recheck_at: if keep { usize::MAX } else { generated + 128 },
6096 };
6097 }
6098 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
6099 spec_mon.n = 0;
6100 spec_mon.zero_accept_rounds = 0;
6101 spec_trial = SpecTrial::Spec {
6102 t0: std::time::Instant::now(),
6103 gen0: generated,
6104 rounds: 0,
6105 };
6106 }
6107 _ => {}
6108 }
6109 spec_watchdog_off = matches!(
6110 spec_trial,
6111 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
6112 );
6113 }
6114 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
6116 let budget = max_tokens - generated - 1;
6117 if let Some(mut st) = self.mimo_mtp.take() {
6118 let k = st.depth.min(budget);
6119 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
6120 self.mimo_mtp = Some(st);
6121 let r = match r {
6122 Ok(r) => r,
6123 Err(err) => {
6124 self.finish_generation(&mut mtp, &mut router, true);
6125 return Err(err);
6126 }
6127 };
6128 if let Some(r) = r {
6129 drafted += r.drafted;
6130 accepted += r.accepted.len();
6131 let mut stopped = false;
6132 for &id in &r.accepted {
6133 if self.confidence_on {
6134 confidence.push(0.0);
6135 }
6136 if !commit!(id) {
6137 stopped = true;
6138 break;
6139 }
6140 }
6141 if stopped {
6142 break 'decode;
6143 }
6144 next_pos += r.accepted.len() + 1;
6145 hidden = r.hidden;
6146 self.graph_logits = Some(r.logits);
6149 continue 'decode;
6150 }
6151 }
6152 }
6153 #[cfg(feature = "gpu")]
6156 if self.speculative
6157 && self.qwen4_exp.is_some()
6158 && task_mask.is_none()
6159 && self.sampler_config.temperature < 1e-6
6160 && generated + 1 < max_tokens
6161 && next_pos > 0
6162 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
6163 {
6164 let r = match &mut self.qwen4_exp {
6165 Some(b) => crate::qwen4_exp::spec_round(
6166 &b.0,
6167 &b.1,
6168 &b.2,
6169 &mut b.3,
6170 next_pos,
6171 &all_ids,
6172 &self.inv_freq,
6173 self.pool.as_deref(),
6174 ),
6175 None => None,
6176 };
6177 if let Some(r) = r {
6178 drafted += r.drafted;
6179 accepted += r.accepted.len();
6180 let mut stopped = false;
6181 for &id in &r.accepted {
6182 if self.confidence_on {
6183 confidence.push(0.0);
6184 }
6185 if !commit!(id) {
6186 stopped = true;
6187 break;
6188 }
6189 }
6190 if stopped {
6191 break 'decode;
6192 }
6193 next_pos += r.accepted.len() + 1;
6194 hidden.fill(0.0);
6195 self.graph_logits = Some(r.logits);
6196 continue 'decode;
6197 }
6198 }
6199 match &mut mtp {
6200 #[cfg(feature = "gpu")]
6202 Some(m)
6203 if graph_spec
6204 && !spec_watchdog_off
6205 && generated + 1 < max_tokens
6206 && next_pos > 0 =>
6207 {
6208 let t_round = std::time::Instant::now();
6209 if spec_time_level() >= 2 {
6210 if let Some(t) = spec_round_end.take() {
6211 eprintln!(
6212 "spec-gap {:.2} ms (host between rounds)",
6213 t.elapsed().as_secs_f64() * 1e3
6214 );
6215 }
6216 }
6217 spec_stamps_begin();
6218 #[cfg(target_os = "macos")]
6223 let allocs0 =
6224 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
6225 #[cfg(not(target_os = "macos"))]
6226 let allocs0 = 0u64;
6227 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
6228 m,
6229 &hidden,
6230 t_next,
6231 next_pos,
6232 &mut drafted,
6233 &mut accepted,
6234 &mut all_ids,
6235 max_tokens - generated,
6236 ) {
6237 next_pos = n_pos;
6238 hidden = new_h;
6239 let level = spec_time_level();
6240 if level > 0 {
6241 let wall = t_round.elapsed().as_secs_f32() * 1e3;
6242 let stamps = spec_stamps_take();
6243 let median = if spec_walls.len() >= 3 {
6246 let mut s = spec_walls.clone();
6247 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6248 Some(s[s.len() / 2])
6249 } else {
6250 None
6251 };
6252 let outlier = median.is_some_and(|m| wall > 1.4 * m);
6253 #[cfg(target_os = "macos")]
6254 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
6255 .load(std::sync::atomic::Ordering::Relaxed)
6256 - allocs0;
6257 #[cfg(not(target_os = "macos"))]
6258 let allocs = allocs0;
6259 eprintln!(
6260 "spec-round wall {wall:.1} ms → {} tokens{}{}",
6261 extra.len() + 1,
6262 if allocs > 0 {
6263 format!(" [{allocs} new device buffers]")
6264 } else {
6265 String::new()
6266 },
6267 match (outlier, median) {
6268 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
6269 _ => String::new(),
6270 }
6271 );
6272 if level >= 2 || outlier {
6273 let sum: f32 = stamps.iter().map(|s| s.1).sum();
6274 eprintln!(
6275 "spec-stamps: {}| untracked {:.1}",
6276 spec_stamps_format(&stamps),
6277 wall - sum
6278 );
6279 }
6280 if spec_mon.n >= 1 {
6281 spec_walls.push(wall);
6282 }
6283 }
6284 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
6288 spec_trial = Self::spec_trial_round(
6291 spec_trial,
6292 &mut spec_mon,
6293 generated + extra.len() + 1,
6294 );
6295 let mut stopped = false;
6296 for &id in &extra {
6297 if self.confidence_on {
6298 confidence.push(0.0);
6299 }
6300 if trace_on {
6313 traces.push(TokenTrace {
6314 t: generated,
6315 token_id: id,
6316 confidence: confidence.last().copied().unwrap_or(0.0),
6317 active_skill: None,
6318 recon: None,
6319 switched: false,
6320 });
6321 }
6322 if !commit!(id) {
6323 stopped = true;
6324 break;
6325 }
6326 }
6327 if stopped {
6328 break 'decode;
6329 }
6330 if spec_time_level() >= 2 {
6331 spec_round_end = Some(std::time::Instant::now());
6332 }
6333 continue 'decode;
6334 }
6335 if self
6336 .graph_failed
6337 .swap(false, std::sync::atomic::Ordering::Relaxed)
6338 {
6339 self.finish_generation(&mut mtp, &mut router, true);
6345 return Err("GPU MTP graph failed during speculative decode".to_string());
6346 }
6347 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
6358 spec_mon.tokens = 0.0;
6359 spec_mon.fails = 3;
6360 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
6361 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
6362 next_pos += 1;
6363 continue 'decode;
6364 }
6365 Some(m) if !graph_spec && generated + 1 < max_tokens => {
6367 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
6368 drafted += 1;
6369 let emb1 = self.embed_single(t_next);
6370 let emb2 = self.embed_single(draft);
6371 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
6372
6373 inference::rms_norm_into(
6374 &h1,
6375 &self.weights.final_norm,
6376 self.rms_eps,
6377 self.norm_style,
6378 &mut self.ws.n1,
6379 );
6380 let mut logits1 = self.lm_head_forward(&self.ws.n1);
6381 let t_after = sampler::sample_with_scratch_pool(
6382 &logits1,
6383 &self.sampler_config,
6384 self.sampler_config.penalty_past(&all_ids, bounded_native),
6385 &mut self.rng,
6386 &mut self.sampler_scratch,
6387 self.pool.as_deref(),
6388 );
6389 if self.confidence_on {
6390 confidence.push(sampler::top1_prob_pool(
6391 self.pool.as_deref(),
6392 &mut self.sampler_scratch,
6393 &logits1,
6394 t_after,
6395 calib_temp,
6396 ));
6397 }
6398 attention::recycle_buf(&mut logits1);
6399 if trace_on {
6400 traces.push(TokenTrace {
6403 t: generated,
6404 token_id: t_after,
6405 confidence: confidence.last().copied().unwrap_or(0.0),
6406 active_skill: None,
6407 recon: None,
6408 switched: false,
6409 });
6410 }
6411 let stop = !commit!(t_after);
6412
6413 if t_after == draft {
6414 accepted += 1;
6415 self.commit_linear_scratch();
6416 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6417 hidden = h2;
6418 next_pos += 2;
6419 } else {
6420 for layer in &mut self.kv_cache.layers {
6422 layer.truncate_last(1);
6423 }
6424 if !stop {
6425 let _ = self.mtp_step(m, &h1, t_after, next_pos);
6426 hidden = self.forward_layers(
6427 &self.embed_single(t_after),
6428 next_pos + 1,
6429 None,
6430 );
6431 }
6432 next_pos += 2;
6433 }
6434 if stop {
6435 break 'decode;
6436 }
6437 }
6438 _ => {
6440 #[cfg(feature = "gpu")]
6445 if Self::dsv4_spec_on() && self.dsv4.is_some() {
6446 static SAID: std::sync::Once = std::sync::Once::new();
6447 SAID.call_once(|| {
6448 eprintln!(
6449 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
6450 !self.dsv4_mtp.is_empty(),
6451 task_mask.is_none(),
6452 router.is_none(),
6453 !trace_on,
6454 self.sampler_config.temperature < 1e-6,
6455 self.sampler_config.repetition_penalty == 1.0,
6456 );
6457 });
6458 }
6459 #[cfg(feature = "gpu")]
6460 if Self::dsv4_spec_on()
6461 && self.dsv4.is_some()
6462 && !self.dsv4_mtp.is_empty()
6463 && task_mask.is_none()
6464 && router.is_none()
6465 && !trace_on
6466 && self.sampler_config.temperature < 1e-6
6467 && self.sampler_config.repetition_penalty == 1.0
6468 && generated + 1 < max_tokens
6469 && all_ids.len() >= 2
6470 && generated >= dsv4_spec_retry_at
6471 {
6472 let tip_token = all_ids[all_ids.len() - 2];
6473 let drafted0 = drafted;
6474 let round = self.dsv4_spec_step(
6475 tip_token,
6476 t_next,
6477 next_pos,
6478 max_tokens.saturating_sub(generated),
6479 &mut drafted,
6480 &mut accepted,
6481 );
6482 if drafted > drafted0 {
6483 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
6484 if useful {
6485 dsv4_spec_bad = 0;
6486 } else {
6487 dsv4_spec_bad += 1;
6488 if dsv4_spec_bad >= 2 {
6489 dsv4_spec_bad = 0;
6490 dsv4_spec_retry_at = generated.saturating_add(32);
6491 tracing::info!(
6492 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
6493 );
6494 }
6495 }
6496 }
6497 if let Some((extra, n_pos)) = round {
6498 next_pos = n_pos;
6499 let mut stopped = false;
6500 for &id in &extra {
6501 if self.confidence_on {
6502 confidence.push(0.0);
6503 }
6504 if !commit!(id) {
6505 stopped = true;
6506 break;
6507 }
6508 }
6509 if stopped {
6510 break 'decode;
6511 }
6512 continue 'decode;
6513 }
6514 }
6515 self.graph_want_logits = fuse_lm;
6516 let mut t_fwd = t_next;
6522 let pure_greedy = self.sampler_config.temperature < 1e-6
6523 && self.sampler_config.repetition_penalty == 1.0
6524 && self.sampler_config.presence_penalty == 0.0
6525 && self.sampler_config.suppress_tokens.is_empty();
6526 let burst_k = std::env::var("CMF_MULTISTEP")
6531 .ok()
6532 .and_then(|v| v.parse::<usize>().ok())
6533 .unwrap_or(0);
6534 if pure_greedy
6535 && burst_k >= 1
6536 && fuse_lm
6537 && task_mask.is_none()
6538 && router.is_none()
6539 && !trace_on
6540 && !self.confidence_on
6541 && std::env::var_os("CMF_FORCE_IDS").is_none()
6546 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6547 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
6548 {
6549 let mut stopped = false;
6550 loop {
6551 if self
6556 .cancel
6557 .swap(false, std::sync::atomic::Ordering::Relaxed)
6558 {
6559 finish_reason = "cancelled".to_string();
6560 stopped = true;
6561 break;
6562 }
6563 let room = max_tokens.saturating_sub(generated);
6564 if room <= 2 {
6565 break;
6566 }
6567 let k = burst_k.min(room - 1);
6568 if k < 1 {
6569 break;
6570 }
6571 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
6572 if self
6573 .graph_failed
6574 .swap(false, std::sync::atomic::Ordering::Relaxed)
6575 {
6576 self.finish_generation(&mut mtp, &mut router, true);
6577 return Err(
6578 "GPU token graph failed during greedy burst".to_string()
6579 );
6580 }
6581 break;
6582 };
6583 next_pos += k;
6584 for &id in &ids {
6585 if !commit!(id) {
6586 stopped = true;
6587 break;
6588 }
6589 }
6590 if stopped {
6591 break;
6592 }
6593 t_fwd = *ids.last().unwrap();
6594 }
6595 if stopped {
6596 break 'decode;
6597 }
6598 }
6599 #[cfg(target_os = "macos")]
6609 if graph_spec
6610 && spec_watchdog_off
6611 && next_pos > 0
6612 && self.mtp_graph_mode == Some(true)
6613 && crate::gpu::q1_force()
6614 {
6615 if let Some(m) = mtp.as_mut() {
6616 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
6617 }
6618 }
6619 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
6620 next_pos += 1;
6621 if let Some(r) = &mut router {
6624 let phi = self.dyn_phi_ema.clone();
6625 let decision = r.step(&phi, generated);
6626 if let Some(new_active) = decision {
6627 let _ = self.set_active_skill(new_active);
6628 }
6629 if trace_on {
6632 if let Some(last) = traces.last_mut() {
6633 let e = r.last_best_e();
6634 last.recon = e.is_finite().then_some(e);
6635 last.switched = decision.is_some();
6636 }
6637 }
6638 }
6639 }
6640 }
6641 }
6642
6643 let cancelled = finish_reason == "cancelled";
6644 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
6648 if mimo_spec {
6649 if let Some(st) = self.mimo_mtp.as_ref() {
6650 let line = st.stats.line();
6651 tracing::info!("{line}");
6652 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
6653 eprintln!("{line}");
6654 }
6655 }
6656 }
6657 self.finish_generation(&mut mtp, &mut router, cancelled);
6658
6659 let output_ids = &all_ids[input_ids.len()..];
6660 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6664 let consumed = std::mem::take(&mut all_ids);
6669 if dyn_switched {
6670 self.clear_sequence_state();
6671 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6672 self.clear_history();
6673 } else {
6674 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6675 }
6676 all_ids = consumed;
6677 let output_ids = &all_ids[input_ids.len()..];
6678 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6680 Ok(GenerateResult {
6681 text: self.tokenizer.decode(output_ids),
6682 token_ids: output_ids.to_vec(),
6683 prompt_tokens: input_ids.len(),
6684 tokens_generated: generated,
6685 finish_reason,
6686 mtp_drafted: drafted,
6687 mtp_accepted: accepted,
6688 token_confidence: confidence,
6689 traces,
6690 })
6691 }
6692
6693 fn mtp_step(
6697 &mut self,
6698 m: &mut MtpModule,
6699 hidden: &[f32],
6700 next_token: u32,
6701 position: usize,
6702 ) -> u32 {
6703 self.mtp_step_h(m, hidden, next_token, position).0
6704 }
6705
6706 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6710 use std::sync::Mutex;
6711 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6712 let mut t = T.lock().unwrap();
6713 if t.len() <= depth {
6714 t.resize(depth + 1, (0, 0));
6715 }
6716 t[depth].0 += 1;
6717 t[depth].1 += prefix_ok as u64;
6718 if depth == 0 && t[0].0 % 128 == 0 {
6719 let line: Vec<String> = t
6720 .iter()
6721 .enumerate()
6722 .map(|(d, (n, k))| {
6723 format!(
6724 "d{}={:.0}%({n})",
6725 d + 1,
6726 100.0 * *k as f64 / (*n).max(1) as f64
6727 )
6728 })
6729 .collect();
6730 eprintln!("mtp-chain: {}", line.join(" "));
6731 }
6732 }
6733
6734 fn mtp_step_hl(
6742 &mut self,
6743 m: &mut MtpModule,
6744 hidden: &[f32],
6745 next_token: u32,
6746 position: usize,
6747 ) -> (Vec<f32>, Vec<f32>) {
6748 #[cfg(target_os = "macos")]
6753 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6754 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6755 self.mtp_graph_mode = Some(true);
6756 return r;
6757 }
6758 if self.mtp_graph_mode == Some(true) {
6759 tracing::error!("mtp Metal graph failed after admission");
6760 self.clear_sequence_state();
6761 self.graph_failed
6762 .store(true, std::sync::atomic::Ordering::Relaxed);
6763 self.cancel
6764 .store(true, std::sync::atomic::Ordering::Relaxed);
6765 return (Vec::new(), Vec::new());
6766 }
6767 self.mtp_graph_mode = Some(false);
6768 }
6769 #[cfg(feature = "gpu")]
6770 if self.mtp_graph_mode != Some(false) {
6771 if !self.mtp_graph_ok(m) {
6772 if self.mtp_graph_mode == Some(true) {
6773 tracing::error!("mtp graph became unavailable after admission");
6778 self.clear_sequence_state();
6779 self.graph_failed
6780 .store(true, std::sync::atomic::Ordering::Relaxed);
6781 self.cancel
6782 .store(true, std::sync::atomic::Ordering::Relaxed);
6783 return (Vec::new(), Vec::new());
6784 }
6785 self.mtp_graph_mode = Some(false);
6786 } else {
6787 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6788 self.mtp_graph_mode = Some(true);
6789 return r;
6790 }
6791 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6792 return (Vec::new(), Vec::new());
6799 }
6800 tracing::error!("mtp graph failed or declined after admission");
6804 self.clear_sequence_state();
6805 self.graph_failed
6806 .store(true, std::sync::atomic::Ordering::Relaxed);
6807 self.cancel
6808 .store(true, std::sync::atomic::Ordering::Relaxed);
6809 return (Vec::new(), Vec::new());
6810 }
6811 }
6812 let e = self.embed_single(next_token);
6816 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6817 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6818 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6819 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6820 let mut x = vec![0.0f32; self.hidden_size];
6821 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6822
6823 let lw = &m.layer;
6825 inference::rms_norm_into(
6826 &x,
6827 &lw.input_norm,
6828 self.rms_eps,
6829 self.norm_style,
6830 &mut self.ws.n1,
6831 );
6832 let attn = match &lw.attn {
6833 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6835 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6836 AttnKind::Full {
6837 wq,
6838 wk,
6839 wv,
6840 wo,
6841 q_norm,
6842 k_norm,
6843 output_gate,
6844 softplus_gate,
6845 bias,
6846 } => {
6847 let mut cfg = self.attn_cfg(position);
6848 cfg.q_norm = q_norm.as_deref();
6849 cfg.k_norm = k_norm.as_deref();
6850 cfg.output_gate = *output_gate;
6851 cfg.softplus_gate = softplus_gate
6852 .as_ref()
6853 .map(|(gate, per_head)| (gate, *per_head));
6854 cfg.bias = bias
6855 .as_ref()
6856 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6857 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6858 }
6859 AttnKind::Linear(_)
6860 | AttnKind::LinearGdn(_)
6861 | AttnKind::ShortConv(_)
6862 | AttnKind::Bounded(_) => {
6863 unreachable!("MTP block is full attention")
6864 }
6865 };
6866 for (i, &a) in attn.iter().enumerate() {
6867 x[i] += a;
6868 }
6869 inference::rms_norm_into(
6870 &x,
6871 &lw.post_norm,
6872 self.rms_eps,
6873 self.norm_style,
6874 &mut self.ws.p1,
6875 );
6876 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6877 for (i, &f) in ffn.iter().enumerate() {
6878 x[i] += f;
6879 }
6880
6881 inference::rms_norm_into(
6882 &x,
6883 &m.final_norm,
6884 self.rms_eps,
6885 self.norm_style,
6886 &mut self.ws.n1,
6887 );
6888 let lg = self.lm_head_forward(&self.ws.n1);
6889 (lg, x)
6890 }
6891
6892 fn mtp_step_h(
6894 &mut self,
6895 m: &mut MtpModule,
6896 hidden: &[f32],
6897 next_token: u32,
6898 position: usize,
6899 ) -> (u32, Vec<f32>) {
6900 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6901 let draft = sampler::argmax(&lg);
6902 attention::recycle_buf(&mut lg);
6903 (draft, x)
6904 }
6905
6906 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6912 match trial {
6913 SpecTrial::Spec { t0, gen0, rounds } => {
6914 let rounds = rounds + 1;
6915 if mon.repeated_zero_acceptance() {
6923 tracing::info!(
6924 "speculation trial: no accepted drafts in two rounds — timing plain"
6925 );
6926 return SpecTrial::Plain {
6927 t0: std::time::Instant::now(),
6928 gen0: generated,
6929 };
6930 }
6931 if rounds >= 5 {
6932 if mon.plain_ms > 0.0 {
6933 let keep = mon.pays();
6934 mon.fails = 0;
6935 tracing::info!(
6936 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6937 mon.tokens,
6938 mon.round_ms,
6939 mon.plain_ms,
6940 if keep { "speculating" } else { "plain" }
6941 );
6942 SpecTrial::Decided {
6943 spec: keep,
6944 recheck_at: if keep { usize::MAX } else { generated + 128 },
6945 }
6946 } else if mon.pays() {
6947 mon.fails = 0;
6952 tracing::info!(
6953 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6954 mon.tokens,
6955 mon.round_ms,
6956 );
6957 SpecTrial::Decided {
6958 spec: true,
6959 recheck_at: usize::MAX,
6960 }
6961 } else {
6962 SpecTrial::Plain {
6963 t0: std::time::Instant::now(),
6964 gen0: generated,
6965 }
6966 }
6967 } else {
6968 SpecTrial::Spec { t0, gen0, rounds }
6969 }
6970 }
6971 SpecTrial::Decided { spec: true, .. } => {
6972 if mon.pays() {
6973 mon.fails = 0;
6974 trial
6975 } else {
6976 mon.fails += 1;
6977 if mon.fails >= 4 {
6978 if mon.plain_ms <= 0.0 {
6979 tracing::info!(
6983 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6984 mon.tokens,
6985 mon.round_ms,
6986 );
6987 return SpecTrial::Plain {
6988 t0: std::time::Instant::now(),
6989 gen0: generated,
6990 };
6991 }
6992 tracing::info!(
6993 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6994 mon.tokens,
6995 mon.round_ms,
6996 mon.plain_ms
6997 );
6998 SpecTrial::Decided {
6999 spec: false,
7000 recheck_at: generated + 128,
7001 }
7002 } else {
7003 trial
7004 }
7005 }
7006 }
7007 other => other,
7008 }
7009 }
7010
7011 fn mtp_kv_id(&self) -> u64 {
7014 self.graph_kv_id | (1u64 << 40)
7015 }
7016
7017 const MTP_LAYER_BASE: usize = 0;
7022
7023 #[cfg(feature = "gpu")]
7030 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
7031 self.mtp_graph_mode != Some(true)
7032 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
7033 }
7034
7035 #[cfg(feature = "gpu")]
7041 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
7042 let mut ok = true;
7043 let mut expected = false;
7044 for li in 0..self.num_layers {
7045 if matches!(
7046 self.weights.layers[self.phys_layer(li)].attn,
7047 AttnKind::Full { .. }
7048 ) {
7049 expected = true;
7050 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
7051 }
7052 }
7053 !expected || ok
7054 }
7055
7056 fn graph_gdn_layer_count(&self) -> usize {
7060 (0..self.num_layers)
7061 .filter(|&li| {
7062 matches!(
7063 &self.weights.layers[self.phys_layer(li)].attn,
7064 AttnKind::LinearGdn(_)
7065 )
7066 })
7067 .count()
7068 }
7069
7070 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
7073 let e = self.embed_single(next_token);
7074 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7075 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7076 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7077 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7078 let mut x = vec![0.0f32; self.hidden_size];
7079 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7080 x
7081 }
7082
7083 #[cfg(feature = "gpu")]
7086 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
7087 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
7088 return false;
7089 }
7090 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
7091 || !crate::gpu::enabled_here()
7092 || self.attn_softcap > 0.0
7093 || self.attention_heads_per_layer.is_some()
7094 || self.v_head_dim.is_some()
7097 {
7098 return false;
7099 }
7100 matches!(
7101 &m.layer.attn,
7102 AttnKind::Full {
7103 softplus_gate: None,
7104 ..
7105 }
7106 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
7107 }
7108
7109 #[cfg(feature = "gpu")]
7113 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
7114 if !self.mtp_block_graph_ok(m) {
7115 return false;
7116 }
7117 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
7118 return false;
7119 };
7120 let FfnKind::Dense(d) = &m.layer.ffn else {
7121 return false;
7122 };
7123 d.segs.is_empty()
7124 && wq.graph_weight().is_some()
7125 && wk.graph_weight().is_some()
7126 && wv.graph_weight().is_some()
7127 && wo.graph_weight().is_some()
7128 && d.gate_proj.graph_weight().is_some()
7129 && d.up_proj.graph_weight().is_some()
7130 && d.down_proj.graph_weight().is_some()
7131 && self.weights.lm_head.graph_weight().is_some()
7132 }
7133
7134 #[cfg(feature = "gpu")]
7140 fn mtp_step_graph(
7141 &mut self,
7142 m: &mut MtpModule,
7143 hidden: &[f32],
7144 next_token: u32,
7145 position: usize,
7146 ) -> Option<(Vec<f32>, Vec<f32>)> {
7147 if !self.mtp_graph_ok(m) {
7148 return None;
7149 }
7150 let lw = &m.layer;
7151 let AttnKind::Full {
7152 wq,
7153 wk,
7154 wv,
7155 wo,
7156 q_norm,
7157 k_norm,
7158 output_gate,
7159 softplus_gate,
7160 bias,
7161 } = &lw.attn
7162 else {
7163 return None;
7164 };
7165 if softplus_gate.is_some() {
7166 return None;
7167 }
7168 let FfnKind::Dense(d) = &lw.ffn else {
7169 return None;
7170 };
7171 if !d.segs.is_empty() {
7172 return None; }
7174 let mut x = self.mtp_block_input(m, hidden, next_token);
7177 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7178 let (_, i, kind, rs) = t.graph_weight()?;
7179 Some(crate::gpu::GraphW {
7180 idx: i,
7181 kind,
7182 row_scale: rs,
7183 data: &[],
7184 prism: crate::gpu::GraphPrismOp::None,
7185 affine: false,
7186 })
7187 }
7188 let (model, _, _, _) = wq.graph_weight()?;
7189 let model = model.clone();
7190 let (lm_gw, lm_rows) = {
7191 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7192 let rows = if kind == 6 {
7196 self.draft_head_rows(self.weights.lm_head.rows())
7197 } else {
7198 self.weights.lm_head.rows()
7199 };
7200 (
7201 crate::gpu::GraphW {
7202 idx: i,
7203 kind,
7204 row_scale: rs,
7205 data: &[],
7206 prism: crate::gpu::GraphPrismOp::None,
7207 affine: false,
7208 },
7209 rows,
7210 )
7211 };
7212 let layer = crate::gpu::GraphLayer {
7213 input_norm: &lw.input_norm,
7214 attn: crate::gpu::GraphAttn::Full {
7215 wq: gw(wq)?,
7216 wk: gw(wk)?,
7217 wv: gw(wv)?,
7218 wo: gw(wo)?,
7219 q_norm: q_norm.as_deref(),
7220 k_norm: k_norm.as_deref(),
7221 late_qk_norm: self.qk_norm_after_rope,
7222 bias: bias
7223 .as_ref()
7224 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7225 output_gate: *output_gate,
7226 cpu_k: m.kv.k_heads(),
7227 cpu_v: m.kv.v_heads(),
7228 cpu_base: m.kv.base(),
7229 geom: None,
7230 head_gate: None,
7231 },
7232 post_norm: &lw.post_norm,
7233 attn_out_norm: None,
7234 ffn_out_norm: None,
7235 layer_scale: None,
7236 ffn: crate::gpu::GraphFfn::Dense {
7237 gate: gw(&d.gate_proj)?,
7238 up: gw(&d.up_proj)?,
7239 down: gw(&d.down_proj)?,
7240 act: d.act.graph_act()?,
7241 },
7242 };
7243 let nh = self.num_heads;
7244 let (nkv, hd, rd) = self.layer_geom(0);
7245 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7246 let mut logits = Vec::new();
7247 let ok = crate::gpu::forward_token_graph(
7248 &model,
7249 self.mtp_kv_id(),
7250 std::slice::from_ref(&layer),
7251 &[None],
7252 self.o1_epoch,
7253 &self.inv_freq,
7254 &mut x,
7255 nh,
7256 nkv,
7257 hd,
7258 self.attn_scale,
7259 rd,
7260 self.hidden_size,
7261 d.gate_proj.rows(),
7265 position,
7266 self.kv_cache.max_seq_len,
7267 gemma,
7268 self.rms_eps as f32,
7269 Some((&lm_gw, lm_rows)),
7270 &m.final_norm,
7271 &mut logits,
7272 &[],
7273 1,
7274 None,
7275 None,
7276 None,
7277 Self::MTP_LAYER_BASE,
7278 true,
7279 );
7280 match ok {
7281 crate::gpu::TokenGraphOutcome::Completed => {}
7282 crate::gpu::TokenGraphOutcome::Declined => return None,
7283 crate::gpu::TokenGraphOutcome::Failed => {
7284 self.clear_sequence_state();
7288 self.graph_failed
7289 .store(true, std::sync::atomic::Ordering::Relaxed);
7290 self.cancel
7291 .store(true, std::sync::atomic::Ordering::Relaxed);
7292 return None;
7293 }
7294 }
7295 pad_mtp_draft_logits(&mut logits, self.vocab_size);
7299 Some((logits, x))
7300 }
7301
7302 #[cfg(feature = "gpu")]
7310 fn mtp_warm_graph(
7311 &mut self,
7312 m: &mut MtpModule,
7313 pairs: &[(&[f32], u32)],
7314 first_pos: usize,
7315 ) -> crate::gpu::BatchGraphOutcome {
7316 if pairs.is_empty() {
7317 return crate::gpu::BatchGraphOutcome::Completed;
7318 }
7319 if !self.mtp_block_graph_ok(m) {
7320 return crate::gpu::BatchGraphOutcome::Declined;
7321 }
7322 let hs = self.hidden_size;
7323 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
7326 for (h, t) in pairs {
7327 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
7328 }
7329 let lw = &m.layer;
7330 let AttnKind::Full {
7331 wq,
7332 wk,
7333 wv,
7334 wo,
7335 q_norm,
7336 k_norm,
7337 output_gate,
7338 bias,
7339 ..
7340 } = &lw.attn
7341 else {
7342 return crate::gpu::BatchGraphOutcome::Declined;
7343 };
7344 let FfnKind::Dense(d) = &lw.ffn else {
7345 return crate::gpu::BatchGraphOutcome::Declined;
7346 };
7347 if !d.segs.is_empty() {
7348 return crate::gpu::BatchGraphOutcome::Declined; }
7350 let (
7353 AttnKind::Full {
7354 softplus_gate: None,
7355 ..
7356 },
7357 Some(gact),
7358 ) = (&lw.attn, d.act.graph_act())
7359 else {
7360 return crate::gpu::BatchGraphOutcome::Declined;
7361 };
7362 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
7363 let (_, i, kind, rs) = t.graph_weight()?;
7364 Some(crate::gpu::GraphW {
7365 idx: i,
7366 kind,
7367 row_scale: rs,
7368 data: &[],
7369 prism: crate::gpu::GraphPrismOp::None,
7370 affine: false,
7371 })
7372 }
7373 let Some((model, _, _, _)) = wq.graph_weight() else {
7374 return crate::gpu::BatchGraphOutcome::Declined;
7375 };
7376 let model = model.clone();
7377 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
7378 gw(wq),
7379 gw(wk),
7380 gw(wv),
7381 gw(wo),
7382 gw(&d.gate_proj),
7383 gw(&d.up_proj),
7384 gw(&d.down_proj),
7385 ) else {
7386 return crate::gpu::BatchGraphOutcome::Declined;
7387 };
7388 let layer = crate::gpu::GraphLayer {
7389 input_norm: &lw.input_norm,
7390 attn: crate::gpu::GraphAttn::Full {
7391 wq: gwq,
7392 wk: gwk,
7393 wv: gwv,
7394 wo: gwo,
7395 q_norm: q_norm.as_deref(),
7396 k_norm: k_norm.as_deref(),
7397 late_qk_norm: self.qk_norm_after_rope,
7398 bias: bias
7399 .as_ref()
7400 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7401 output_gate: *output_gate,
7402 cpu_k: m.kv.k_heads(),
7403 cpu_v: m.kv.v_heads(),
7404 cpu_base: m.kv.base(),
7405 geom: None,
7406 head_gate: None,
7407 },
7408 post_norm: &lw.post_norm,
7409 attn_out_norm: None,
7410 ffn_out_norm: None,
7411 layer_scale: None,
7412 ffn: crate::gpu::GraphFfn::Dense {
7413 gate: gg,
7414 up: gu,
7415 down: gd,
7416 act: gact,
7417 },
7418 };
7419 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
7420 let nh = self.num_heads;
7421 let (nkv, hd, rd) = self.layer_geom(0);
7422 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
7423 crate::gpu::forward_batch_graph(
7424 &model,
7425 self.mtp_kv_id(),
7426 std::slice::from_ref(&layer),
7427 &self.inv_freq,
7428 &mut hiddens,
7429 nh,
7430 nkv,
7431 hd,
7432 rd,
7433 hs,
7434 d.gate_proj.rows(),
7437 &positions,
7438 self.kv_cache.max_seq_len,
7439 gemma,
7440 self.rms_eps as f32,
7441 self.attn_scale,
7442 pairs.len(),
7443 &[],
7444 0,
7445 None,
7446 None,
7447 )
7448 }
7449
7450 #[cfg(feature = "gpu")]
7457 fn mtp_warm_graph_fallback(
7458 &mut self,
7459 m: &mut MtpModule,
7460 pairs: &[(&[f32], u32)],
7461 first_pos: usize,
7462 ) -> bool {
7463 if pairs.is_empty() {
7464 return true;
7465 }
7466 let graphable = self.mtp_block_graph_ok(m);
7467 if !graphable {
7468 if self.mtp_graph_mode == Some(true) {
7472 return false;
7473 }
7474 self.mtp_graph_mode = Some(false);
7475 for (j, (h, t)) in pairs.iter().enumerate() {
7476 self.mtp_warm(m, h, *t, first_pos + j);
7477 }
7478 return true;
7479 }
7480
7481 for (j, (h, t)) in pairs.iter().enumerate() {
7486 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
7487 return false;
7488 }
7489 }
7490 self.mtp_graph_mode = Some(true);
7491 true
7492 }
7493
7494 #[cfg(feature = "gpu")]
7499 fn mtp_warm_prefill_pairs(
7500 &mut self,
7501 m: &mut MtpModule,
7502 pairs: &[(&[f32], u32)],
7503 first_pos: usize,
7504 ) -> Result<(), &'static str> {
7505 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
7510 if self.mtp_graph_mode == Some(true) {
7511 return Err("MTP token graph became unavailable after admission");
7512 }
7513 self.mtp_graph_mode = Some(false);
7514 for (j, (h, t)) in pairs.iter().enumerate() {
7515 self.mtp_warm(m, h, *t, first_pos + j);
7516 }
7517 return Ok(());
7518 }
7519 match self.mtp_warm_graph(m, pairs, first_pos) {
7520 crate::gpu::BatchGraphOutcome::Completed => {
7521 if !pairs.is_empty() {
7522 self.mtp_graph_mode = Some(true);
7523 }
7524 Ok(())
7525 }
7526 crate::gpu::BatchGraphOutcome::Declined => {
7527 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
7528 Ok(())
7529 } else {
7530 Err("MTP warm-up fallback failed after device admission")
7531 }
7532 }
7533 crate::gpu::BatchGraphOutcome::Failed => {
7534 Err("MTP warm batch graph failed after admission")
7535 }
7536 }
7537 }
7538
7539 #[cfg(not(feature = "gpu"))]
7540 fn mtp_warm_prefill_pairs(
7541 &mut self,
7542 m: &mut MtpModule,
7543 pairs: &[(&[f32], u32)],
7544 first_pos: usize,
7545 ) -> Result<(), &'static str> {
7546 for (j, (h, t)) in pairs.iter().enumerate() {
7547 self.mtp_warm(m, h, *t, first_pos + j);
7548 }
7549 Ok(())
7550 }
7551
7552 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
7556 let e = self.embed_single(next_token);
7557 let mut cat = vec![0.0f32; 2 * self.hidden_size];
7558 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
7559 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
7560 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
7561 let mut x = vec![0.0f32; self.hidden_size];
7562 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
7563 inference::rms_norm_into(
7564 &x,
7565 &m.layer.input_norm,
7566 self.rms_eps,
7567 self.norm_style,
7568 &mut self.ws.n1,
7569 );
7570 let attn = match &m.layer.attn {
7571 AttnKind::Full {
7572 wq,
7573 wk,
7574 wv,
7575 wo,
7576 q_norm,
7577 k_norm,
7578 output_gate,
7579 softplus_gate,
7580 bias,
7581 } => {
7582 let mut cfg = self.attn_cfg(position);
7583 cfg.q_norm = q_norm.as_deref();
7584 cfg.k_norm = k_norm.as_deref();
7585 cfg.output_gate = *output_gate;
7586 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
7587 cfg.bias = bias
7588 .as_ref()
7589 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
7590 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
7591 }
7592 _ => return,
7593 };
7594 let _ = attn;
7595 }
7596
7597 #[cfg(feature = "gpu")]
7604 #[allow(clippy::too_many_arguments)]
7605 fn graph_spec_step(
7606 &mut self,
7607 m: &mut MtpModule,
7608 hidden: &[f32],
7609 t_next: u32,
7610 next_pos: usize,
7611 drafted: &mut usize,
7612 accepted: &mut usize,
7613 all_ids: &mut Vec<u32>,
7617 room: usize,
7622 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
7623 #[cfg(target_os = "macos")]
7634 let metal_native = crate::gpu::q1_force();
7635 #[cfg(not(target_os = "macos"))]
7636 let metal_native = false;
7637 #[cfg(feature = "gpu")]
7638 let k_default = if metal_native {
7639 7
7642 } else if crate::gpu_wgpu::verify_i8_on() {
7643 5
7644 } else {
7645 4
7646 };
7647 #[cfg(not(feature = "gpu"))]
7648 let k_default = 4;
7649 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
7650 .ok()
7651 .and_then(|v| v.parse().ok())
7652 .filter(|&v| (1..=8).contains(&v));
7653 let (k_start, k_max) = if metal_native {
7658 (7, 7)
7659 } else {
7660 (3, k_default.max(5))
7661 };
7662 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
7663 let k_spec = k_full.min(room).max(1);
7664 let k_capped = k_spec < k_full;
7667 if next_pos == 0 {
7668 return None;
7669 }
7670 let t_round = std::time::Instant::now();
7671 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
7687 let sub0 = subs();
7688 let cfg = self.sampler_config.clone();
7693 let penalized = !(cfg.repetition_penalty == 1.0
7694 && cfg.presence_penalty == 0.0
7695 && cfg.suppress_tokens.is_empty());
7696 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7701 let sampling = cfg.temperature >= 1e-6;
7702 let sparse = sampling && sampler::sparse_ok(&cfg);
7708 let base_len = all_ids.len();
7709 if sampling && !sparse && self.spec_q.len() < k_spec {
7710 self.spec_q.resize_with(k_spec, Vec::new);
7711 }
7712 if sparse && self.spec_qs.len() < k_spec {
7713 self.spec_qs.resize_with(k_spec, Vec::new);
7714 }
7715 let mut drafts = Vec::with_capacity(k_spec);
7720 let mut hx = hidden.to_vec();
7721 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7724 spec_stamp("pro");
7725 #[cfg(target_os = "macos")]
7731 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7732 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7733 Ok(ids) => {
7734 self.mtp_graph_mode = Some(true);
7735 drafts = ids;
7736 }
7737 Err(true) => {
7738 tracing::error!("mtp Metal draft chain failed after commit");
7739 self.clear_sequence_state();
7740 self.graph_failed
7741 .store(true, std::sync::atomic::Ordering::Relaxed);
7742 self.cancel
7743 .store(true, std::sync::atomic::Ordering::Relaxed);
7744 return None;
7745 }
7746 Err(false) => {}
7747 }
7748 }
7749 for j in drafts.len()..k_spec {
7750 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7751 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7752 if spec_dbg {
7753 let saved = self.mtp_graph_mode;
7754 self.mtp_graph_mode = Some(false);
7755 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7756 self.mtp_graph_mode = saved;
7757 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7758 return None;
7759 }
7760 m.kv.truncate_last(1);
7761 dbg_ref = Some(r);
7762 }
7763 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7764 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7765 return None;
7766 }
7767 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7768 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7769 let dl = lg
7770 .iter()
7771 .zip(&lg_cpu)
7772 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7773 let dh = hj
7774 .iter()
7775 .zip(&h_cpu)
7776 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7777 eprintln!(
7778 "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 {}",
7779 next_pos - 1 + j,
7780 sampler::argmax(&lg_cpu),
7781 sampler::argmax(&lg),
7782 n(&h_cpu),
7783 n(&hj),
7784 m.kv.seq_len
7785 );
7786 }
7787 let dj = if sparse {
7788 let mut q = std::mem::take(&mut self.spec_qs[j]);
7789 let ok = sampler::sparse_distribution_into(
7790 &lg,
7791 &cfg,
7792 all_ids,
7793 &mut self.sampler_scratch,
7794 self.pool.as_deref(),
7795 &mut q,
7796 );
7797 let d = if ok {
7798 sampler::draw_sparse(&q, &mut self.rng)
7799 } else {
7800 let t = sampler::argmax(&lg);
7802 q.clear();
7803 q.push((t, 1.0));
7804 t
7805 };
7806 self.spec_qs[j] = q;
7807 all_ids.push(d);
7808 d
7809 } else if sampling {
7810 let mut q = std::mem::take(&mut self.spec_q[j]);
7811 sampler::distribution_into(
7812 &lg,
7813 &cfg,
7814 all_ids,
7815 &mut self.sampler_scratch,
7816 self.pool.as_deref(),
7817 &mut q,
7818 );
7819 let d = sampler::draw(&q, &mut self.rng);
7820 self.spec_q[j] = q;
7821 all_ids.push(d); d
7823 } else if greedy_pen {
7824 let d = sampler::argmax_penalized(
7825 &lg,
7826 &cfg,
7827 all_ids,
7828 &mut self.sampler_scratch,
7829 self.pool.as_deref(),
7830 );
7831 all_ids.push(d);
7832 d
7833 } else {
7834 sampler::argmax(&lg)
7835 };
7836 attention::recycle_buf(&mut lg);
7837 drafts.push(dj);
7838 hx = hj;
7839 spec_stamp("d.pick");
7840 }
7841 all_ids.truncate(base_len);
7842 *drafted += k_spec;
7843 let t_draft = t_round.elapsed();
7844 let sub_draft = subs();
7845 let b = k_spec + 1;
7848 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7849 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7850 let e = self.embed_single(t);
7851 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7852 }
7853 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7854 spec_stamp("v.emb");
7855 let (lm_gw, lm_rows) = {
7856 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7857 (
7858 crate::gpu::GraphW {
7859 idx: i,
7860 kind,
7861 row_scale: rs,
7862 data: &[],
7863 prism: crate::gpu::GraphPrismOp::None,
7864 affine: false,
7865 },
7866 self.weights.lm_head.rows(),
7867 )
7868 };
7869 let mut logits = Vec::new();
7870 let final_norm = self.weights.final_norm.clone();
7871 #[cfg(target_os = "macos")]
7880 let greedy_dev = metal_native
7881 && !sampling
7882 && !greedy_pen
7883 && !self.confidence_on
7884 && self.final_softcap.is_none()
7885 && self.vocab_size == lm_rows
7891 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7892 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7893 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7894 #[cfg(not(target_os = "macos"))]
7895 let greedy_dev = false;
7896 let mut dev_ids: Vec<u32> = Vec::new();
7897 #[cfg(target_os = "macos")]
7898 let verify_outcome = if metal_native {
7899 let lm = self.weights.lm_head.q1_parts()?;
7900 let n_score = self.vocab_size.min(lm_rows);
7901 self.try_batch_graph_metal(
7902 &mut hiddens,
7903 &positions,
7904 b,
7905 Some((lm, &final_norm, &mut logits)),
7906 if greedy_dev {
7907 Some((n_score, &mut dev_ids))
7908 } else {
7909 None
7910 },
7911 )
7912 } else {
7913 self.try_batch_graph_wgpu(
7914 &mut hiddens,
7915 &positions,
7916 b,
7917 Some(crate::gpu::SpecTail {
7918 lm: lm_gw,
7919 lm_rows,
7920 final_norm: &final_norm,
7921 logits_out: &mut logits,
7922 pick: None,
7923 }),
7924 )
7925 };
7926 #[cfg(not(target_os = "macos"))]
7927 let verify_outcome = self.try_batch_graph_wgpu(
7928 &mut hiddens,
7929 &positions,
7930 b,
7931 Some(crate::gpu::SpecTail {
7932 lm: lm_gw,
7933 lm_rows,
7934 final_norm: &final_norm,
7935 logits_out: &mut logits,
7936 pick: None,
7937 }),
7938 );
7939 match verify_outcome {
7940 crate::gpu::BatchGraphOutcome::Completed => {}
7941 crate::gpu::BatchGraphOutcome::Declined => {
7942 m.kv.truncate_last(k_spec);
7946 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7947 self.clear_sequence_state();
7948 self.graph_failed
7949 .store(true, std::sync::atomic::Ordering::Relaxed);
7950 self.cancel
7951 .store(true, std::sync::atomic::Ordering::Relaxed);
7952 tracing::error!("MTP graph mirror rewind failed after verify decline");
7953 }
7954 return None;
7955 }
7956 crate::gpu::BatchGraphOutcome::Failed => {
7957 self.clear_sequence_state();
7961 self.graph_failed
7962 .store(true, std::sync::atomic::Ordering::Relaxed);
7963 self.cancel
7964 .store(true, std::sync::atomic::Ordering::Relaxed);
7965 tracing::error!("MTP verify batch graph failed after admission");
7966 return None;
7967 }
7968 }
7969 #[cfg(target_os = "macos")]
7975 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7976 let snap: Vec<Vec<f32>> = self
7977 .kv_cache
7978 .layers
7979 .iter()
7980 .map(|l| l.linear_state.clone())
7981 .collect();
7982 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7983 let toks: Vec<u32> = std::iter::once(t_next)
7984 .chain(drafts.iter().copied())
7985 .collect();
7986 let want_save = self.graph_want_logits;
7987 self.graph_want_logits = false;
7988 for (i, &t) in toks.iter().enumerate() {
7989 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7990 let _ = self.graph_logits.take();
7991 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7995 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7996 }
7997 let ref_lg = self.logits_from_hidden(&hi);
7998 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7999 let ra = sampler::argmax(&ref_lg);
8000 let va = sampler::argmax(row);
8001 let mut md = 0f32;
8002 let mut rms = 0f64;
8003 for j in 0..lm_rows.min(ref_lg.len()) {
8004 let d = (ref_lg[j] - row[j]).abs();
8005 md = md.max(d);
8006 rms += (d as f64) * (d as f64);
8007 }
8008 let mut hd = 0f32;
8009 for j in 0..self.hidden_size {
8010 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
8011 }
8012 eprintln!(
8013 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
8014 next_pos + i,
8015 if ra == va { "OK" } else { "MISMATCH" },
8016 (rms / lm_rows as f64).sqrt()
8017 );
8018 }
8019 self.graph_want_logits = want_save;
8020 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8023 if l.linear_state.len() == st.len() {
8024 l.linear_state.copy_from_slice(&st);
8025 } else {
8026 l.linear_state = st;
8027 }
8028 }
8029 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
8030 let extra = l.seq_len.saturating_sub(n0);
8031 if extra > 0 {
8032 l.truncate_last(extra);
8033 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
8034 }
8035 }
8036 }
8037 let t_verify = t_round.elapsed();
8038 let sub_verify = subs();
8039 let mut a = 0usize;
8044 let mut forced: Option<u32> = None;
8045 let ids: Vec<u32> = if sparse {
8046 let mut p = std::mem::take(&mut self.spec_ps);
8047 let mut res = std::mem::take(&mut self.spec_ress);
8048 while a < k_spec {
8049 let ok = sampler::sparse_distribution_into(
8050 &logits[a * lm_rows..(a + 1) * lm_rows],
8051 &cfg,
8052 all_ids,
8053 &mut self.sampler_scratch,
8054 self.pool.as_deref(),
8055 &mut p,
8056 );
8057 if !ok {
8058 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
8059 p.clear();
8060 p.push((t, 1.0));
8061 }
8062 match sampler::spec_accept_or_correct_sparse(
8063 &p,
8064 &self.spec_qs[a],
8065 drafts[a],
8066 &mut self.rng,
8067 &mut res,
8068 ) {
8069 None => {
8070 all_ids.push(drafts[a]);
8071 a += 1;
8072 }
8073 Some(c) => {
8074 forced = Some(c);
8075 break;
8076 }
8077 }
8078 }
8079 all_ids.truncate(base_len);
8080 self.spec_ps = p;
8081 self.spec_ress = res;
8082 drafts.clone()
8083 } else if sampling {
8084 let mut p = std::mem::take(&mut self.spec_p);
8085 let mut res = std::mem::take(&mut self.spec_res);
8086 while a < k_spec {
8087 sampler::distribution_into(
8088 &logits[a * lm_rows..(a + 1) * lm_rows],
8089 &cfg,
8090 all_ids,
8091 &mut self.sampler_scratch,
8092 self.pool.as_deref(),
8093 &mut p,
8094 );
8095 match sampler::spec_accept_or_correct(
8096 &p,
8097 &self.spec_q[a],
8098 drafts[a],
8099 &mut self.rng,
8100 &mut res,
8101 self.pool.as_deref(),
8102 ) {
8103 None => {
8104 all_ids.push(drafts[a]);
8105 a += 1;
8106 }
8107 Some(c) => {
8108 forced = Some(c);
8109 break;
8110 }
8111 }
8112 }
8113 all_ids.truncate(base_len);
8114 self.spec_p = p;
8115 self.spec_res = res;
8116 drafts.clone()
8118 } else if greedy_pen {
8119 let mut ids: Vec<u32> = Vec::with_capacity(b);
8123 for i in 0..b {
8124 let t = sampler::argmax_penalized(
8125 &logits[i * lm_rows..(i + 1) * lm_rows],
8126 &cfg,
8127 all_ids,
8128 &mut self.sampler_scratch,
8129 self.pool.as_deref(),
8130 );
8131 ids.push(t);
8132 if i < k_spec && t == drafts[i] {
8133 all_ids.push(t);
8134 } else {
8135 break;
8136 }
8137 }
8138 all_ids.truncate(base_len);
8139 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
8140 a += 1;
8141 }
8142 ids
8145 } else if greedy_dev && dev_ids.len() == b {
8146 let ids = std::mem::take(&mut dev_ids);
8147 while a < k_spec && ids[a] == drafts[a] {
8148 a += 1;
8149 }
8150 ids
8151 } else {
8152 if logits.len() < b * lm_rows {
8153 self.clear_sequence_state();
8156 self.graph_failed
8157 .store(true, std::sync::atomic::Ordering::Relaxed);
8158 self.cancel
8159 .store(true, std::sync::atomic::Ordering::Relaxed);
8160 tracing::error!("Metal verify returned neither logits nor argmax ids");
8161 return None;
8162 }
8163 let ids: Vec<u32> = (0..b)
8164 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
8165 .collect();
8166 while a < k_spec && ids[a] == drafts[a] {
8167 a += 1;
8168 }
8169 ids
8170 };
8171 spec_stamp("acc");
8172 if spec_dbg {
8173 eprintln!(
8174 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
8175 drafts, ids
8176 );
8177 }
8178 #[cfg(target_os = "macos")]
8182 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
8183 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
8184 {
8185 let snap: Vec<Vec<f32>> = self
8186 .kv_cache
8187 .layers
8188 .iter()
8189 .map(|l| l.linear_state.clone())
8190 .collect();
8191 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
8192 let toks: Vec<u32> = std::iter::once(t_next)
8193 .chain(drafts.iter().copied())
8194 .collect();
8195 let want_save = self.graph_want_logits;
8196 self.graph_want_logits = false;
8197 for (i, &t) in toks.iter().take(a + 1).enumerate() {
8198 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
8199 let _ = self.graph_logits.take();
8200 }
8201 self.graph_want_logits = want_save;
8202 let plain_states: Vec<Vec<f32>> = self
8203 .kv_cache
8204 .layers
8205 .iter()
8206 .map(|l| l.linear_state.clone())
8207 .collect();
8208 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8209 let mut rows = Vec::new();
8210 for (li, (l, n0)) in self
8211 .kv_cache
8212 .layers
8213 .iter_mut()
8214 .zip(attn_lens.iter())
8215 .enumerate()
8216 {
8217 let extra = l.seq_len.saturating_sub(*n0);
8218 if extra > 0 {
8219 let mut kk = Vec::new();
8220 let mut vv = Vec::new();
8221 for g in 0..nkv {
8222 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8223 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8224 }
8225 rows.push((li, kk, vv));
8226 l.truncate_last(extra);
8227 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
8228 }
8229 }
8230 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
8231 if l.linear_state.len() == st.len() {
8232 l.linear_state.copy_from_slice(&st);
8233 } else {
8234 l.linear_state = st;
8235 }
8236 }
8237 Some((plain_states, rows))
8238 } else {
8239 None
8240 };
8241 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
8242 #[cfg(target_os = "macos")]
8251 let mut warm_pending: Option<MetalWarmPending> = None;
8252 #[cfg(target_os = "macos")]
8253 if metal_native {
8254 m.kv.truncate_last(k_spec.saturating_sub(1));
8255 if self.mtp_graph_mode == Some(true) {
8256 crate::gpu_metal::kv_mirror_set_stored(
8259 self.mtp_kv_id(),
8260 Self::MTP_LAYER_BASE,
8261 m.kv.seq_len,
8262 );
8263 if !warm_off && a > 0 {
8264 let pairs: Vec<(&[f32], u32)> = (0..a)
8265 .map(|j| {
8266 (
8267 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
8268 ids[j],
8269 )
8270 })
8271 .collect();
8272 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
8273 }
8274 }
8275 spec_stamp("c.wsub");
8276 }
8277 #[cfg(target_os = "macos")]
8279 if metal_native {
8280 if !self.metal_verify_commit(a) {
8283 self.clear_sequence_state();
8284 self.graph_failed
8285 .store(true, std::sync::atomic::Ordering::Relaxed);
8286 self.cancel
8287 .store(true, std::sync::atomic::Ordering::Relaxed);
8288 tracing::error!("Metal verify state/KV handoff failed after admission");
8289 return None;
8290 }
8291 if let Some((plain_states, rows)) = commit_ref {
8292 crate::gpu_metal::queue_fence();
8293 let _ = crate::gpu_metal::wait_replay();
8296 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
8297 let mut worst_s = 0f32;
8298 let mut worst_li = 0usize;
8299 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
8300 if l.linear_state.len() != ps.len() || ps.is_empty() {
8301 continue;
8302 }
8303 let d = l
8304 .linear_state
8305 .iter()
8306 .zip(ps)
8307 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8308 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
8309 let rel = d / n.max(1e-6);
8310 if rel > worst_s {
8311 worst_s = rel;
8312 worst_li = li;
8313 }
8314 }
8315 let mut worst_k = 0f32;
8316 for (li, kk, vv) in &rows {
8317 let l = &self.kv_cache.layers[*li];
8318 let n0 = l.seq_len - (kk.len() / (nkv * hd));
8319 let mut ck = Vec::new();
8320 let mut cv = Vec::new();
8321 for g in 0..nkv {
8322 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
8323 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
8324 }
8325 if ck.len() == kk.len() {
8326 let dk = ck
8327 .iter()
8328 .zip(kk)
8329 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8330 let dv = cv
8331 .iter()
8332 .zip(vv)
8333 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
8334 worst_k = worst_k.max(dk).max(dv);
8335 } else {
8336 eprintln!(
8337 "commit-check L{li}: kv row count mismatch {} vs {}",
8338 ck.len(),
8339 kk.len()
8340 );
8341 }
8342 }
8343 eprintln!(
8344 "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}"
8345 );
8346 }
8347 }
8348 if !metal_native && a + 1 < b {
8349 let expected_gdn_layers = self.graph_gdn_layer_count();
8350 if expected_gdn_layers > 0
8351 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
8352 {
8353 self.clear_sequence_state();
8354 self.graph_failed
8355 .store(true, std::sync::atomic::Ordering::Relaxed);
8356 self.cancel
8357 .store(true, std::sync::atomic::Ordering::Relaxed);
8358 tracing::error!("GDN speculative restore failed after verify");
8359 return None;
8360 }
8361 }
8362 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
8363 self.clear_sequence_state();
8368 self.graph_failed
8369 .store(true, std::sync::atomic::Ordering::Relaxed);
8370 self.cancel
8371 .store(true, std::sync::atomic::Ordering::Relaxed);
8372 tracing::error!("trunk graph KV rewind failed after speculative verify");
8373 return None;
8374 }
8375 *accepted += a;
8376 if !metal_native {
8387 m.kv.truncate_last(k_spec.saturating_sub(1));
8389 }
8390 spec_stamp("c.trunc");
8391 if !metal_native
8392 && self.mtp_graph_mode == Some(true)
8393 && !self.rewind_mtp_graph_mirror(next_pos)
8394 {
8395 self.clear_sequence_state();
8399 self.graph_failed
8400 .store(true, std::sync::atomic::Ordering::Relaxed);
8401 self.cancel
8402 .store(true, std::sync::atomic::Ordering::Relaxed);
8403 tracing::error!("MTP graph mirror rewind failed after verify commit");
8404 return None;
8405 }
8406 if !warm_off && a > 0 {
8407 let mut warmed = false;
8410 #[cfg(target_os = "macos")]
8411 if metal_native && self.mtp_graph_mode == Some(true) {
8412 warmed = match warm_pending.take() {
8416 Some(p) => self.mtp_warm_batch_finish(m, p),
8417 None => false,
8418 };
8419 if !warmed {
8420 warmed = true;
8421 for j in 0..a {
8422 let row =
8423 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
8424 if self
8425 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
8426 .is_none()
8427 {
8428 warmed = false;
8429 break;
8430 }
8431 }
8432 }
8433 }
8434 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
8435 let rows: Vec<Vec<f32>> = (0..a)
8436 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
8437 .collect();
8438 let pairs: Vec<(&[f32], u32)> = rows
8439 .iter()
8440 .zip(ids.iter())
8441 .map(|(r, &t)| (r.as_slice(), t))
8442 .collect();
8443 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
8444 Ok(()) => warmed = true,
8445 Err(err) => {
8446 tracing::error!("{err}");
8452 self.clear_sequence_state();
8453 self.graph_failed
8454 .store(true, std::sync::atomic::Ordering::Relaxed);
8455 self.cancel
8456 .store(true, std::sync::atomic::Ordering::Relaxed);
8457 return None;
8458 }
8459 }
8460 }
8461 if !warmed {
8462 for j in 0..a {
8463 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
8464 let row = row.to_vec();
8465 self.mtp_warm(m, &row, ids[j], next_pos + j);
8466 }
8467 }
8468 }
8469 spec_stamp("c.warm");
8473 if let Some(c) = forced {
8474 self.spec_forced = Some(c);
8475 self.graph_logits = None;
8476 } else if greedy_dev && logits.is_empty() {
8477 self.spec_forced = Some(ids[a]);
8480 self.graph_logits = None;
8481 } else {
8482 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
8483 row.resize(self.vocab_size, 0.0);
8484 if let Some(c) = self.final_softcap {
8485 for l in row.iter_mut() {
8486 *l = c * (*l / c).tanh();
8487 }
8488 }
8489 self.graph_logits = Some(row);
8490 }
8491 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
8492 spec_stamp("c.row");
8493 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8499 let end = subs();
8500 eprintln!(
8501 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
8502 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
8503 t_draft.as_secs_f64() * 1e3,
8504 sub_draft - sub0,
8505 (t_verify - t_draft).as_secs_f64() * 1e3,
8506 sub_verify - sub_draft,
8507 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
8508 end - sub_verify,
8509 self.draft_full_streak,
8510 );
8511 }
8512 if k_env.is_none() && !metal_native && !k_capped {
8517 let f = a as f32 / k_spec.max(1) as f32;
8521 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
8522 let mut k_next = k_spec;
8523 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
8524 k_next = k_spec + 1;
8525 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
8526 k_next = k_spec - 1;
8527 }
8528 if k_next != k_spec {
8529 self.spec_acc_ewma = 0.6;
8530 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
8531 eprintln!("spec-k: {k_spec} → {k_next}");
8532 }
8533 }
8534 self.spec_k_adapt = Some(k_next);
8535 }
8536 spec_stamp("end");
8537 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
8538 }
8539
8540 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
8549 if !self.pair_supported() {
8550 return (0.0, 0.0);
8551 }
8552 if self.gdn_cfg.is_some()
8560 && self.graph_prefill_preferred()
8561 && self.automatic_gpu_prefix().is_some()
8562 {
8563 tracing::info!(
8564 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
8565 );
8566 return (0.0, 0.0);
8567 }
8568 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
8575 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
8576 let emb1 = self.embed_single(1);
8577 let emb2 = self.embed_single(2);
8578 let pos = self.kv_cache.seq_len();
8579
8580 let t0 = std::time::Instant::now();
8581 for _ in 0..iters {
8582 let _ = self.forward_layers(&emb1, pos, None);
8583 let _ = self.forward_layers(&emb2, pos + 1, None);
8584 for l in &mut self.kv_cache.layers {
8585 l.truncate_last(2);
8586 }
8587 }
8588 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8589
8590 let t1 = std::time::Instant::now();
8591 for _ in 0..iters {
8592 let _ = self.forward_pair(&emb1, &emb2, pos);
8593 for l in &mut self.kv_cache.layers {
8594 l.truncate_last(2);
8595 }
8596 }
8597 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
8598 match graph_env {
8599 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
8600 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
8601 }
8602 (singles_ms, pair_ms)
8603 }
8604
8605 fn pair_supported(&self) -> bool {
8613 !self.weights.layers.is_empty()
8620 && !self.per_token_stack()
8621 && !self
8622 .weights
8623 .layers
8624 .iter()
8625 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
8626 }
8627
8628 fn forward_pair(
8629 &mut self,
8630 emb1: &[f32],
8631 emb2: &[f32],
8632 position: usize,
8633 ) -> (Vec<f32>, Vec<f32>) {
8634 self.mimo_moe_prepare();
8637 let mut h1 = emb1.to_vec();
8638 let mut h2 = emb2.to_vec();
8639 let (_nkv, _hd, hs, _rd, eps) = (
8640 self.num_kv_heads,
8641 self.head_dim,
8642 self.hidden_size,
8643 self.rotary_dim,
8644 self.rms_eps,
8645 );
8646 let pool = self.pool.clone();
8647 let automatic_gpu_prefix = self.automatic_gpu_prefix();
8654
8655 for li in 0..self.num_layers {
8656 let _capacity_tail = automatic_gpu_prefix
8657 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
8658 .map(|_| crate::gpu::enter_cpu_scope());
8659 crate::gpu::set_layer(li as i64);
8662 let lw = &self.weights.layers[self.phys_layer(li)];
8663 inference::rms_norm_into(
8666 &h1,
8667 &lw.input_norm,
8668 self.rms_eps,
8669 self.norm_style,
8670 &mut self.ws.n1,
8671 );
8672 inference::rms_norm_into(
8673 &h2,
8674 &lw.input_norm,
8675 self.rms_eps,
8676 self.norm_style,
8677 &mut self.ws.n2,
8678 );
8679
8680 let (a1, a2) = match &lw.attn {
8681 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
8682 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
8683 AttnKind::Bounded(w) => {
8684 let rope = self
8687 .bounded_rope
8688 .clone()
8689 .expect("bounded layer without an installed rotation table");
8690 let cfg = crate::bounded::BoundedAttnCfg {
8691 num_heads: self.num_heads,
8692 num_kv_heads: self.num_kv_heads,
8693 head_dim: self.head_dim,
8694 hidden_size: hs,
8695 scale: self.attn_scale,
8696 rope: &rope,
8697 pool: pool.as_deref(),
8698 };
8699 let a1 = crate::bounded::bounded_attention(
8700 &self.ws.n1,
8701 w,
8702 &mut self.kv_cache.layers[li],
8703 &cfg,
8704 );
8705 let a2 = crate::bounded::bounded_attention(
8706 &self.ws.n2,
8707 w,
8708 &mut self.kv_cache.layers[li],
8709 &cfg,
8710 );
8711 (a1, a2)
8712 }
8713 AttnKind::Linear(w) => {
8714 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
8715 let layer = &mut self.kv_cache.layers[li];
8716 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8717 vmf_phase_pair(
8718 &self.ws.n1,
8719 &self.ws.n2,
8720 w,
8721 &cfg,
8722 state,
8723 scratch,
8724 self.pool.as_deref(),
8725 )
8726 }
8727 AttnKind::LinearGdn(w) => {
8728 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8729 let layer = &mut self.kv_cache.layers[li];
8730 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8731 gdn_pair(
8732 &self.ws.n1,
8733 &self.ws.n2,
8734 w,
8735 &cfg,
8736 state,
8737 scratch,
8738 self.pool.as_deref(),
8739 )
8740 }
8741 AttnKind::ShortConv(w) => {
8742 let cfg = self
8743 .short_conv_cfg
8744 .expect("short-conv layer without short_conv_cfg");
8745 let layer = &mut self.kv_cache.layers[li];
8746 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8747 short_conv_pair(
8748 &self.ws.n1,
8749 &self.ws.n2,
8750 w,
8751 &cfg,
8752 state,
8753 scratch,
8754 self.pool.as_deref(),
8755 )
8756 }
8757 AttnKind::Full {
8758 wq,
8759 wk,
8760 wv,
8761 wo,
8762 q_norm,
8763 k_norm,
8764 output_gate,
8765 softplus_gate,
8766 bias,
8767 } => {
8768 let inv_freq_l = self.layer_inv_freq(li);
8769 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8770 let cfg = QwenAttnCfg {
8771 num_heads: self.layer_num_heads(li),
8772 num_kv_heads: nkv_l,
8773 head_dim: hd_l,
8774 hidden_size: hs,
8775 position,
8776 inv_freq: &inv_freq_l,
8777 rotary_dim: rd_l,
8778 scale: self.attn_scale,
8779 softcap: self.attn_softcap,
8780 window: self.layer_window(li),
8781 v_norm: self.attn_v_norm,
8782 qk_norm_after_rope: self.qk_norm_after_rope,
8783 gate_sigmoid: self.proj_gate_sigmoid,
8784 q_norm: q_norm.as_deref(),
8785 k_norm: k_norm.as_deref(),
8786 output_gate: *output_gate,
8787 softplus_gate: softplus_gate
8788 .as_ref()
8789 .map(|(gate, per_head)| (gate, *per_head)),
8790 rope_scale: self.layer_rope_scale(li),
8791 bias: bias
8792 .as_ref()
8793 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8794 rms_eps: eps,
8795 norm_style: self.norm_style,
8796 pool: pool.as_deref(),
8797 v_head_dim: self.layer_v_dim(li),
8798 };
8799 attention::qwen_attention_pair(
8800 &self.ws.n1,
8801 &self.ws.n2,
8802 wq,
8803 wk,
8804 wv,
8805 wo,
8806 &mut self.kv_cache.layers[li],
8807 &cfg,
8808 )
8809 }
8810 };
8811 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8812 Some(w) => (
8813 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8814 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8815 ),
8816 None => (a1, a2),
8817 };
8818 for i in 0..self.hidden_size {
8819 h1[i] += a1[i];
8820 h2[i] += a2[i];
8821 }
8822 let (mut a1, mut a2) = (a1, a2);
8823 attention::recycle_buf(&mut a1);
8824 attention::recycle_buf(&mut a2);
8825
8826 let lw = &self.weights.layers[self.phys_layer(li)];
8827 inference::rms_norm_into(
8828 &h1,
8829 &lw.post_norm,
8830 self.rms_eps,
8831 self.norm_style,
8832 &mut self.ws.p1,
8833 );
8834 inference::rms_norm_into(
8835 &h2,
8836 &lw.post_norm,
8837 self.rms_eps,
8838 self.norm_style,
8839 &mut self.ws.p2,
8840 );
8841 let (f1, f2) = match &lw.ffn {
8842 FfnKind::DenseMoe(dm) => (
8845 dense_moe_ffn(
8846 dm,
8847 &self.ws.p1,
8848 &h1,
8849 self.rms_eps,
8850 self.norm_style,
8851 self.pool.as_deref(),
8852 ),
8853 dense_moe_ffn(
8854 dm,
8855 &self.ws.p2,
8856 &h2,
8857 self.rms_eps,
8858 self.norm_style,
8859 self.pool.as_deref(),
8860 ),
8861 ),
8862 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8863 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8864 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8865 ),
8866 _ => ffn_forward_pair(
8867 &lw.ffn,
8868 &self.ws.p1,
8869 &self.ws.p2,
8870 self.pool.as_deref(),
8871 None,
8872 ),
8873 };
8874 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8875 Some(w) => (
8876 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8877 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8878 ),
8879 None => (f1, f2),
8880 };
8881 for i in 0..self.hidden_size {
8882 h1[i] += f1[i];
8883 h2[i] += f2[i];
8884 }
8885 let (mut f1, mut f2) = (f1, f2);
8886 attention::recycle_buf(&mut f1);
8887 attention::recycle_buf(&mut f2);
8888 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8889 for i in 0..self.hidden_size {
8890 h1[i] *= sc;
8891 h2[i] *= sc;
8892 }
8893 }
8894 if self.is_loop_end(li) && li + 1 < self.num_layers {
8896 h1 = inference::rms_norm(
8897 &h1,
8898 &self.weights.final_norm,
8899 self.rms_eps,
8900 self.norm_style,
8901 );
8902 h2 = inference::rms_norm(
8903 &h2,
8904 &self.weights.final_norm,
8905 self.rms_eps,
8906 self.norm_style,
8907 );
8908 }
8909 }
8910 crate::gpu::set_layer(-1); if self.o1_active() {
8917 self.commit_linear_scratch();
8918 }
8919 self.o1_progress();
8920 self.swa_trim_tails();
8921 (h1, h2)
8922 }
8923
8924 fn commit_linear_scratch(&mut self) {
8926 for layer in &mut self.kv_cache.layers {
8927 if !layer.linear_scratch.is_empty() {
8928 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8929 layer.linear_scratch.clear();
8930 }
8931 }
8932 }
8933
8934 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8942 #[cfg(target_os = "macos")]
8943 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8944 if ids.is_empty() {
8945 return Err("empty id sequence".to_string());
8946 }
8947
8948 self.nll_begin()?;
8949 let result = (|| {
8950 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8951 self.nll_check_graph("encoder prefill", 0)?;
8952 let expected = ids.len() * self.hidden_size;
8953 if hidden.len() != expected {
8954 return Err(format!(
8955 "encoder prefill returned {} values for {} rows of hidden size {}",
8956 hidden.len(),
8957 ids.len(),
8958 self.hidden_size
8959 ));
8960 }
8961
8962 let mut out = vec![0.0f32; expected];
8963 for (src, dst) in hidden
8964 .chunks_exact(self.hidden_size)
8965 .zip(out.chunks_exact_mut(self.hidden_size))
8966 {
8967 inference::rms_norm_into(
8968 src,
8969 &self.weights.final_norm,
8970 self.rms_eps,
8971 self.norm_style,
8972 dst,
8973 );
8974 }
8975 Ok(out)
8976 })();
8977 self.nll_end();
8978 result
8979 }
8980
8981 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
8986 #[cfg(target_os = "macos")]
8987 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8988 if ids.is_empty() {
8989 return Err("empty id sequence".to_string());
8990 }
8991 self.nll_begin()?;
8992 let result = (|| {
8993 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
8994 self.nll_check_graph("hidden prefill", 0)?;
8995 if hidden.len() != ids.len() * self.hidden_size {
8996 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
8997 }
8998 Ok(hidden)
8999 })();
9000 self.nll_end();
9001 result
9002 }
9003
9004 pub fn forward_ids(
9007 &mut self,
9008 ids: &[u32],
9009 task_mask: Option<&TaskMask>,
9010 ) -> Result<Vec<f32>, String> {
9011 #[cfg(target_os = "macos")]
9012 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9013 if ids.is_empty() {
9014 return Err("empty id sequence".to_string());
9015 }
9016 self.clear_sequence_state();
9017 self.check_forward_graph("forward_ids setup", 0)?;
9018 if task_mask.is_none() {
9019 self.o1_begin();
9020 }
9021 let mut hidden = vec![0.0f32; self.hidden_size];
9022 let mut pos = 0usize;
9023 if let Some(b) = &mut self.dsv41 {
9024 let pool = self.pool.clone();
9025 let mut logits = Vec::new();
9026 crate::dsv41::forward_chunk(
9027 &b.0,
9028 &b.1,
9029 &b.2,
9030 &mut b.3,
9031 ids,
9032 0,
9033 pool.as_deref(),
9034 &mut logits,
9035 );
9036 if let Err(err) = self.o1_seal_checked() {
9037 self.clear_sequence_state();
9038 return Err(err);
9039 }
9040 return Ok(logits);
9041 }
9042 #[cfg(not(target_os = "macos"))]
9053 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
9054 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
9055 Ok(r) => r,
9056 Err(e) => {
9057 self.clear_sequence_state();
9058 return Err(e);
9059 }
9060 };
9061 if n > 0 {
9062 hidden.copy_from_slice(&last);
9063 pos = n;
9064 }
9065 }
9066 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
9067 let chunk = self.prefill_chunk();
9071 let hs = self.hidden_size;
9072 while pos < ids.len() {
9073 let end = (pos + chunk).min(ids.len());
9074 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9075 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
9076 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
9077 pos = end;
9078 }
9079 }
9080 if task_mask.is_none()
9089 && !self.graph_prefill_preferred()
9090 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
9091 && self.pair_supported()
9092 {
9093 while pos + 1 < ids.len() {
9094 let e1 = self.embed_single(ids[pos]);
9095 let e2 = self.embed_single(ids[pos + 1]);
9096 let (_, h2) = self.forward_pair(&e1, &e2, pos);
9097 self.check_forward_graph("forward_ids pair", pos + 1)?;
9098 self.commit_linear_scratch();
9099 hidden = h2;
9100 pos += 2;
9101 }
9102 }
9103 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
9106 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
9107 self.graph_logits = Some(lg);
9108 hidden = vec![0.0; self.hidden_size];
9109 pos = ids.len();
9110 }
9111 }
9112 while pos < ids.len() {
9113 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9114 self.check_forward_graph("forward_ids", pos)?;
9115 pos += 1;
9116 }
9117 if let Some(logits) = self.graph_logits.take() {
9118 if let Err(err) = self.o1_seal_checked() {
9122 self.clear_sequence_state();
9123 return Err(err);
9124 }
9125 return Ok(logits);
9126 }
9127 if let Err(err) = self.o1_seal_checked() {
9131 self.clear_sequence_state();
9132 return Err(err);
9133 }
9134 let normed = inference::rms_norm(
9135 &hidden,
9136 &self.weights.final_norm,
9137 self.rms_eps,
9138 self.norm_style,
9139 );
9140 Ok(self.lm_head_forward(&normed))
9141 }
9142
9143 #[doc(hidden)]
9147 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
9148 #[cfg(target_os = "macos")]
9149 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9150 if ids.is_empty() {
9151 return Err("empty id sequence".to_string());
9152 }
9153 self.clear_sequence_state();
9154 self.dsv41
9155 .as_ref()
9156 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
9157 self.o1_begin();
9158 let rows = {
9159 let pool = self.pool.clone();
9160 let b = self
9161 .dsv41
9162 .as_mut()
9163 .expect("dsv41 checked above; state cannot change during forward");
9164 let mut rows = Vec::with_capacity(ids.len());
9165 for (position, &id) in ids.iter().enumerate() {
9166 let mut logits = Vec::new();
9167 crate::dsv41::forward_token(
9168 &b.0,
9169 &b.1,
9170 &b.2,
9171 &mut b.3,
9172 id,
9173 position,
9174 pool.as_deref(),
9175 &mut logits,
9176 );
9177 rows.push(logits);
9178 }
9179 rows
9180 };
9181 self.o1_seal();
9182 Ok(rows)
9183 }
9184
9185 pub fn dsv41_moe_capture(
9189 &mut self,
9190 ids: &[u32],
9191 lo: usize,
9192 hi: usize,
9193 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
9194 #[cfg(target_os = "macos")]
9195 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9196 if ids.is_empty() {
9197 return Err("empty id sequence".to_string());
9198 }
9199 self.dsv41
9200 .as_ref()
9201 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
9202 self.clear_sequence_state();
9203 let res = {
9204 let pool = self.pool.clone();
9205 let b = self.dsv41.as_mut().expect("dsv41 checked above");
9206 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
9207 };
9208 self.clear_sequence_state();
9209 res
9210 }
9211
9212 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
9219 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
9220 Ok((nll / cnt.max(1) as f64).exp())
9221 }
9222
9223 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
9230 if !self.can_prefill_batched() {
9231 return self.nll_ids_from(ids, 0).map(|_| ());
9232 }
9233 self.nll_begin()?;
9234 let result: Result<(), String> = (|| {
9235 let chunk = chunk.max(1);
9236 let mut pos = 0usize;
9237 while pos < ids.len() {
9238 let end = (pos + chunk).min(ids.len());
9239 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
9240 self.nll_check_graph("calibration prefill", pos)?;
9241 pos = end;
9242 }
9243 Ok(())
9244 })();
9245 self.nll_end();
9246 result
9247 }
9248
9249 pub fn calib_window_span(
9258 &mut self,
9259 ids: &[u32],
9260 input: Option<&[f32]>,
9261 from: usize,
9262 mid: usize,
9263 upto: usize,
9264 ) -> Result<Vec<f32>, String> {
9265 if !self.can_prefill_batched() {
9266 return Err("calib_window_span: this architecture has no batched prefill".into());
9267 }
9268 self.nll_begin()?;
9269 let result: Result<Vec<f32>, String> = (|| {
9270 let h = if mid > from {
9271 let inp = match input {
9272 Some(h) => PrefillIn::Hidden(h),
9273 None => PrefillIn::Ids(ids),
9274 };
9275 let h = self.prefill_batch_span(inp, 0, None, from, mid);
9276 self.nll_check_graph("calibration span", 0)?;
9277 h
9278 } else {
9279 input.map(<[f32]>::to_vec).unwrap_or_default()
9280 };
9281 if upto > mid {
9282 let inp = if mid > from || input.is_some() {
9283 PrefillIn::Hidden(&h)
9284 } else {
9285 PrefillIn::Ids(ids)
9286 };
9287 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
9288 self.nll_check_graph("calibration span", 0)?;
9289 }
9290 Ok(h)
9291 })();
9292 self.nll_end();
9293 result
9294 }
9295
9296 #[cfg(feature = "gpu")]
9300 pub fn qwen4_capture(
9301 &mut self,
9302 ids: &[u32],
9303 lo: usize,
9304 hi: usize,
9305 ) -> Result<Vec<Vec<f32>>, String> {
9306 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
9307 crate::qwen4_exp::capture_window(
9308 &b.0,
9309 &b.1,
9310 &b.2,
9311 &mut b.3,
9312 ids,
9313 lo,
9314 hi,
9315 &self.inv_freq,
9316 self.pool.as_deref(),
9317 )
9318 }
9319
9320 #[cfg(feature = "gpu")]
9326 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
9327 use crate::gpu_wgpu::clover as cap;
9328 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
9329 let ne = (lo..hi)
9330 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
9331 FfnKind::Moe(m) => Some(m.experts.len()),
9332 _ => None,
9333 })
9334 .ok_or("no MoE layer in the capture range")?;
9335 let chunk = self.prefill_chunk().max(1);
9336 self.clear_sequence_state();
9337 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
9338 return Err("capture unavailable (no wgpu device)".into());
9339 }
9340 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
9341 let res = (|| {
9342 for (ci, part) in ids.chunks(chunk).enumerate() {
9343 let mut hiddens = Vec::with_capacity(part.len() * hs);
9344 for &id in part {
9345 hiddens.extend_from_slice(&self.embed_single(id));
9346 }
9347 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
9348 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
9349 crate::gpu::BatchGraphOutcome::Completed => {}
9350 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
9351 }
9352 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
9353 if written < hi - lo {
9354 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
9355 }
9356 for (o, r) in out.iter_mut().zip(rows) {
9357 o.extend_from_slice(&r);
9358 }
9359 }
9360 Ok(())
9361 })();
9362 cap::moe_capture_end();
9363 self.clear_sequence_state();
9364 res.map(|_| out)
9365 }
9366
9367 pub fn moe_expert_dirs(
9370 &self,
9371 li: usize,
9372 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9373 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
9374 return None;
9375 };
9376 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
9377 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
9378 let shared = match &m.shared {
9379 Some((se, _)) => Some(dirs(se)?),
9380 None => None,
9381 };
9382 Some((routed, shared))
9383 }
9384
9385 pub fn qwen4_expert_dirs(
9387 &self,
9388 li: usize,
9389 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
9390 let b = self.qwen4_exp.as_ref()?;
9391 crate::qwen4_exp::expert_dirs(&b.1, li)
9392 }
9393
9394 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
9409 self.reset_mtp_probe_state();
9410 let _exact_calibration = ExactMtpCalibrationGuard::enter();
9414 let result = if ids.len() < 2 {
9415 Err("MTP FFN probe needs at least two token ids".into())
9416 } else if ids
9417 .iter()
9418 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
9419 {
9420 Err("MTP FFN probe received a token id outside the embedding table".into())
9421 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
9422 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
9425 } else if self.mimo_mtp.is_some() {
9426 let mut st = self
9427 .mimo_mtp
9428 .take()
9429 .expect("checked MiMo MTP before detaching it");
9430 let result = crate::gpu::cpu_scope(|| {
9431 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids)
9432 });
9433 st.reset();
9436 self.mimo_mtp = Some(st);
9437 result
9438 } else if self.mtp.is_some() {
9439 let mut mtp = self
9440 .mtp
9441 .take()
9442 .expect("checked canonical MTP before detaching it");
9443 let result = crate::gpu::cpu_scope(|| self.mtp_ffn_probe_teacher_forced(&mut mtp, ids));
9444 mtp.kv.clear();
9445 self.mtp = Some(mtp);
9446 result
9447 } else {
9448 Err("MTP FFN probe: this pipeline has no draft stack".into())
9449 };
9450 self.reset_mtp_probe_state();
9453 result
9454 }
9455
9456 fn reset_mtp_probe_state(&mut self) {
9458 self.clear_sequence_state();
9459 if let Some(mtp) = self.mtp.as_mut() {
9460 mtp.kv.clear();
9461 }
9462 if let Some(mimo) = self.mimo_mtp.as_mut() {
9463 mimo.reset();
9464 }
9465 self.mtp_graph_mode = None;
9466 }
9467
9468 fn mtp_ffn_probe_teacher_forced(
9474 &mut self,
9475 mtp: &mut MtpModule,
9476 ids: &[u32],
9477 ) -> Result<Vec<MtpFfnProbe>, String> {
9478 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
9479 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
9480 };
9481 let mut mass = vec![0.0f64; dense.gate_proj.rows()];
9482 let mut squared_mass = vec![0.0f64; dense.gate_proj.rows()];
9483 for position in 0..ids.len() - 1 {
9484 let hidden = self.forward_layers(&self.embed_single(ids[position]), position, None);
9485 if hidden.len() != self.hidden_size {
9486 return Err(format!(
9487 "MTP FFN probe: trunk hidden width {} != {}",
9488 hidden.len(),
9489 self.hidden_size
9490 ));
9491 }
9492 self.mtp_probe_step_ffn(
9493 mtp,
9494 &hidden,
9495 ids[position + 1],
9496 position,
9497 &mut mass,
9498 &mut squared_mass,
9499 )?;
9500 }
9501 Ok(vec![MtpFfnProbe {
9502 mass,
9503 squared_mass,
9504 observed_positions: ids.len() - 1,
9505 }])
9506 }
9507
9508 fn mtp_probe_step_ffn(
9513 &mut self,
9514 mtp: &mut MtpModule,
9515 hidden: &[f32],
9516 next_token: u32,
9517 position: usize,
9518 mass: &mut [f64],
9519 squared_mass: &mut [f64],
9520 ) -> Result<(), String> {
9521 let e = self.embed_single(next_token);
9522 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9523 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9524 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
9525 inference::rms_norm_into(
9526 hidden,
9527 &mtp.hnorm,
9528 self.rms_eps,
9529 self.norm_style,
9530 cat_h,
9531 );
9532 let mut x = vec![0.0f32; self.hidden_size];
9533 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9534
9535 let lw = &mtp.layer;
9536 inference::rms_norm_into(
9537 &x,
9538 &lw.input_norm,
9539 self.rms_eps,
9540 self.norm_style,
9541 &mut self.ws.n1,
9542 );
9543 let attn = match &lw.attn {
9544 AttnKind::Full {
9545 wq,
9546 wk,
9547 wv,
9548 wo,
9549 q_norm,
9550 k_norm,
9551 output_gate,
9552 softplus_gate,
9553 bias,
9554 } => {
9555 let mut cfg = self.attn_cfg(position);
9556 cfg.q_norm = q_norm.as_deref();
9557 cfg.k_norm = k_norm.as_deref();
9558 cfg.output_gate = *output_gate;
9559 cfg.softplus_gate = softplus_gate
9560 .as_ref()
9561 .map(|(gate, per_head)| (gate, *per_head));
9562 cfg.bias = bias
9563 .as_ref()
9564 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9565 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
9566 }
9567 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
9568 };
9569 for (dst, src) in x.iter_mut().zip(attn) {
9570 *dst += src;
9571 }
9572 inference::rms_norm_into(
9573 &x,
9574 &lw.post_norm,
9575 self.rms_eps,
9576 self.norm_style,
9577 &mut self.ws.p1,
9578 );
9579 let FfnKind::Dense(dense) = &lw.ffn else {
9580 return Err("MTP FFN probe: draft FFN is not dense".into());
9581 };
9582 accumulate_dense_ffn_activation_mass(
9583 dense,
9584 &self.ws.p1,
9585 1,
9586 self.pool.as_deref(),
9587 mass,
9588 squared_mass,
9589 )?;
9590 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9593 for (dst, src) in x.iter_mut().zip(ffn) {
9594 *dst += src;
9595 }
9596 Ok(())
9597 }
9598
9599 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
9604 self.clear_sequence_state();
9605 FFN_PROBE.with(|p| {
9606 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9607 });
9608 crate::gpu::cpu_scope(|| {
9609 for (pos, &id) in ids.iter().enumerate() {
9610 let emb = self.embed_single(id);
9611 let _ = self.forward_layers(&emb, pos, None);
9612 }
9613 });
9614 self.clear_sequence_state();
9615 FFN_PROBE
9616 .with(|p| p.borrow_mut().take())
9617 .unwrap_or_default()
9618 }
9619
9620 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
9624 if let Err(err) = self.nll_begin() {
9625 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
9629 self.nll_end();
9630 return Err(err);
9631 }
9632 FFN_PROBE.with(|p| {
9633 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
9634 });
9635 let result: Result<(), String> = (|| {
9636 for chunk in ids.chunks(256) {
9637 if chunk.len() < 2 {
9638 continue;
9639 }
9640 self.nll_ids_masked(chunk, 0, None)?;
9641 }
9642 Ok(())
9643 })();
9644 self.nll_end();
9645 let probe = FFN_PROBE
9646 .with(|p| p.borrow_mut().take())
9647 .unwrap_or_default();
9648 match result {
9649 Ok(()) => Ok(probe),
9650 Err(err) => {
9651 drop(probe);
9652 Err(err)
9653 }
9654 }
9655 }
9656
9657 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
9661 self.nll_begin()?;
9662 let result: Result<f64, String> = (|| {
9663 let mut nll = 0f64;
9664 let mut cnt = 0usize;
9665 let mut hidden = vec![0f32; self.hidden_size];
9666 for (pos, &id) in ids.iter().enumerate() {
9667 if pos > 0 {
9668 inference::rms_norm_into(
9669 &hidden,
9670 &self.weights.final_norm,
9671 self.rms_eps,
9672 self.norm_style,
9673 &mut self.ws.n1,
9674 );
9675 let mut logits = self.lm_head_forward(&self.ws.n1);
9676 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
9677 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
9678 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
9679 nll -= p.max(1e-300).ln();
9680 cnt += 1;
9681 attention::recycle_buf(&mut logits);
9682 }
9683 let emb = self.embed_single(id);
9684 hidden = self.forward_layers(&emb, pos, Some(mask));
9685 self.nll_check_graph("masked serial forward", pos)?;
9686 let _ = self.graph_logits.take();
9690 }
9691 Ok((nll / cnt.max(1) as f64).exp())
9692 })();
9693 self.nll_end();
9694 result
9695 }
9696
9697 pub fn nll_ids_masked(
9716 &mut self,
9717 ids: &[u32],
9718 start: usize,
9719 task_mask: Option<&TaskMask>,
9720 ) -> Result<(f64, usize), String> {
9721 let task_mask = self.drop_open_mask(task_mask);
9722 self.nll_ids_inner(ids, start, task_mask)
9723 }
9724
9725 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
9726 self.nll_ids_inner(ids, start, None)
9727 }
9728
9729 fn nll_ids_inner(
9730 &mut self,
9731 ids: &[u32],
9732 start: usize,
9733 task_mask: Option<&TaskMask>,
9734 ) -> Result<(f64, usize), String> {
9735 self.nll_begin()?;
9736 #[cfg(target_os = "macos")]
9738 if task_mask.is_none() && self.is_block_diffusion() {
9739 if let Some(r) = self.clover_metal_nll(ids, start) {
9740 self.nll_end();
9741 return r;
9742 }
9743 }
9744 let result: Result<(f64, usize), String> = (|| {
9745 let mut nll = 0f64;
9746 let mut cnt = 0usize;
9747 let (graph_quality, fused_head_quality) = nll_graph_policy(
9760 task_mask.is_none(),
9761 self.graph_prefill_preferred(),
9762 crate::gpu::q1_force(),
9763 );
9764 self.graph_head_required = fused_head_quality;
9765 self.graph_want_logits = fused_head_quality;
9766 #[cfg(target_os = "macos")]
9767 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
9768 match self.nll_batch_metal(ids, start) {
9769 MetalBatchNllOutcome::Completed(nll, count) => {
9770 return Ok((nll, count));
9771 }
9772 MetalBatchNllOutcome::Declined => {}
9773 MetalBatchNllOutcome::Failed(err) => return Err(err),
9774 }
9775 }
9776 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
9781 if self.can_prefill_batched() && !graph_quality && !force_serial {
9782 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
9791 .ok()
9792 .and_then(|v| v.parse().ok())
9793 .filter(|&c: &usize| c > 0)
9794 .unwrap_or(128);
9795 const LM_SUB: usize = 32;
9796 let n = ids.len().saturating_sub(1);
9797 let hs = self.hidden_size;
9798 let rows = self.weights.lm_head.rows();
9799 let mut pos = 0usize;
9800 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
9801 while pos < n {
9802 let end = (pos + chunk_len).min(n);
9803 let bsz = end - pos;
9804 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
9805 self.nll_check_graph("batched prefill", pos)?;
9806 if state_trace && end % 256 == 0 {
9807 self.trace_recurrent_state(end);
9808 }
9809 let mut k0 = 0usize;
9810 while k0 < bsz {
9811 let k1 = (k0 + LM_SUB).min(bsz);
9812 let sb = k1 - k0;
9813 if pos + k1 <= start {
9816 k0 = k1;
9817 continue;
9818 }
9819 let mut normed = vec![0.0f32; sb * hs];
9820 for k in 0..sb {
9821 let r = inference::rms_norm(
9822 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
9823 &self.weights.final_norm,
9824 self.rms_eps,
9825 self.norm_style,
9826 );
9827 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
9828 }
9829 let mut logits = vec![0.0f32; sb * rows];
9830 self.weights
9831 .lm_head
9832 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
9833 for k in 0..sb {
9834 if pos + k0 + k < start {
9835 continue;
9836 }
9837 self.nll_check_graph("batched score row", pos + k0 + k)?;
9838 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
9839 if let Some(mu) = self.logit_multiplier {
9840 for v in lg.iter_mut() {
9841 *v *= mu;
9842 }
9843 }
9844 if let Some(c) = self.final_softcap {
9848 softcap_in_place(lg, c, self.pool.as_deref());
9849 }
9850 if let Some(cm) = self.head_clusters.clone() {
9853 self.hierarchical_head_logprobs(
9854 &normed[k * hs..(k + 1) * hs],
9855 &cm,
9856 lg,
9857 );
9858 }
9859 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
9860 let target = ids[pos + k0 + k + 1] as usize;
9861 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9862 let lse: f64 = lg
9863 .iter()
9864 .map(|&v| ((v - max) as f64).exp())
9865 .sum::<f64>()
9866 .ln()
9867 + max as f64;
9868 nll += lse - lg[target] as f64;
9869 cnt += 1;
9870 if std::env::var("CMF_PPL_TRACE").is_ok() {
9871 let top = lg
9872 .iter()
9873 .enumerate()
9874 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9875 .map(|(i, _)| i)
9876 .unwrap_or(0);
9877 eprintln!(
9878 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
9879 pos + k0 + k,
9880 target,
9881 lse - lg[target] as f64,
9882 top,
9883 lg[target],
9884 lg[top]
9885 );
9886 }
9887 }
9888 k0 = k1;
9889 }
9890 pos = end;
9891 }
9892 return Ok((nll, cnt));
9893 }
9894 for pos in 0..ids.len().saturating_sub(1) {
9895 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
9896 self.nll_check_graph("serial forward", pos)?;
9897 let out_of_band = self.graph_logits.take();
9905 if self.graph_head_required && out_of_band.is_none() {
9906 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9907 return Err(format!(
9908 "fused Metal graph head did not complete at NLL position {pos}"
9909 ));
9910 }
9911 if pos < start {
9912 continue;
9913 }
9914 let logits = match out_of_band {
9915 Some(lg) => lg,
9916 None => {
9917 let normed = inference::rms_norm(
9918 &hidden,
9919 &self.weights.final_norm,
9920 self.rms_eps,
9921 self.norm_style,
9922 );
9923 self.lm_head_forward(&normed)
9927 }
9928 };
9929 let target = ids[pos + 1] as usize;
9930 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9931 let lse: f64 = logits
9932 .iter()
9933 .map(|&v| ((v - max) as f64).exp())
9934 .sum::<f64>()
9935 .ln()
9936 + max as f64;
9937 let tok_nll = lse - logits[target] as f64;
9938 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9939 let top = logits
9940 .iter()
9941 .enumerate()
9942 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9943 .map(|(i, _)| i)
9944 .unwrap_or(0);
9945 eprintln!(
9946 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9947 logits[target], logits[top]
9948 );
9949 }
9950 nll += tok_nll;
9951 cnt += 1;
9952 }
9953 Ok((nll, cnt))
9954 })();
9955 self.nll_end();
9956 result
9957 }
9958
9959 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
9964 let normed = inference::rms_norm(
9965 hidden,
9966 &self.weights.final_norm,
9967 self.rms_eps,
9968 self.norm_style,
9969 );
9970 let mut logits = self.lm_head_forward(&normed);
9973 let target = target as usize;
9974 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9975 let lse: f64 = logits
9976 .iter()
9977 .map(|&v| ((v - max) as f64).exp())
9978 .sum::<f64>()
9979 .ln()
9980 + max as f64;
9981 let tok_nll = lse - logits[target] as f64;
9982 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
9983 let top = logits
9984 .iter()
9985 .enumerate()
9986 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9987 .map(|(i, _)| i)
9988 .unwrap_or(0);
9989 eprintln!(
9990 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9991 logits[target], logits[top]
9992 );
9993 }
9994 attention::recycle_buf(&mut logits);
9995 tok_nll
9996 }
9997
9998 fn trace_recurrent_state(&self, pos: usize) {
10006 let stats = |v: &[f32]| -> (f64, f64) {
10007 if v.is_empty() {
10008 return (0.0, 0.0);
10009 }
10010 let (mut ss, mut mx) = (0f64, 0f64);
10011 for &x in v {
10012 ss += (x as f64) * (x as f64);
10013 mx = mx.max((x as f64).abs());
10014 }
10015 ((ss / v.len() as f64).sqrt(), mx)
10016 };
10017 for (li, l) in self.kv_cache.layers.iter().enumerate() {
10018 let lw = &self.weights.layers[self.phys_layer(li)];
10019 let (kind, s_len) = match &lw.attn {
10020 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
10021 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
10022 AttnKind::Bounded(_) => ("bounded", 0),
10023 AttnKind::Full { .. } => ("full", 0),
10024 _ => ("other", 0),
10025 };
10026 let (rms, max) = stats(&l.linear_state);
10027 let s_part = if kind == "vmf" {
10028 &l.linear_state[..s_len.min(l.linear_state.len())]
10029 } else if kind == "gdn" {
10030 let ring = l
10031 .linear_state
10032 .len()
10033 .saturating_sub(s_len)
10034 .min(l.linear_state.len());
10035 &l.linear_state[ring..]
10036 } else {
10037 &l.linear_state[..0]
10038 };
10039 let (s_rms, s_max) = stats(s_part);
10040 let (ring_rms, ring_len) = match &l.bounded {
10041 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
10042 None => (0.0, 0),
10043 };
10044 eprintln!(
10045 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
10046 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
10047 l.linear_state.len(),
10048 l.seq_len
10049 );
10050 }
10051 }
10052
10053 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
10071 self.nll_begin()?;
10076 let requested_prefix = (prefill > 0).then_some(prefill);
10077 self.o1_begin_with_prefix(requested_prefix);
10078 let n = ids.len().saturating_sub(1);
10079 let requested_start = prefill.min(n);
10080 let exact_end = if self.o1_active() {
10085 match requested_prefix {
10086 Some(requested) => self.o1_effective_boundary(requested),
10087 None => self
10088 .o1_cfg
10089 .as_ref()
10090 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
10091 }
10092 .unwrap_or(requested_start)
10093 .min(n)
10094 } else {
10095 requested_start
10096 };
10097 let mut nll = 0f64;
10098 let mut cnt = 0usize;
10099
10100 let mut pos = 0usize;
10104 if self.can_prefill_batched() {
10105 const CHUNK: usize = 128;
10106 while pos < exact_end {
10107 let end = (pos + CHUNK).min(exact_end);
10108 let hiddens = self.prefill_batch(&ids[pos..end], pos);
10109 if self
10110 .graph_failed
10111 .swap(false, std::sync::atomic::Ordering::Relaxed)
10112 {
10113 self.cancel
10114 .store(false, std::sync::atomic::Ordering::Relaxed);
10115 self.nll_end();
10116 return Err("GPU graph failed during O(1) NLL prefix".into());
10117 }
10118 for row in 0..end - pos {
10119 let score_pos = pos + row;
10120 if score_pos >= requested_start && score_pos < n {
10121 nll += self.nll_from_hidden(
10122 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
10123 ids[score_pos + 1],
10124 score_pos,
10125 );
10126 cnt += 1;
10127 }
10128 }
10129 pos = end;
10130 }
10131 } else {
10132 while pos < exact_end {
10133 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10134 if self
10135 .graph_failed
10136 .swap(false, std::sync::atomic::Ordering::Relaxed)
10137 {
10138 self.cancel
10139 .store(false, std::sync::atomic::Ordering::Relaxed);
10140 self.nll_end();
10141 return Err("GPU graph failed during O(1) NLL prefix".into());
10142 }
10143 if pos >= requested_start && pos < n {
10144 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10145 cnt += 1;
10146 }
10147 pos += 1;
10148 }
10149 }
10150 self.o1_seal_checked().map_err(|err| {
10151 self.nll_end();
10152 err
10153 })?;
10154
10155 let batch_k = std::env::var("CMF_BATCH_K")
10164 .ok()
10165 .and_then(|v| v.parse::<usize>().ok())
10166 .unwrap_or(0);
10167 let batch_admitted = batch_k > 0
10168 && self.can_prefill_batched()
10169 && self.o1_active()
10170 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
10171 && (0..self.num_layers).all(|li| {
10172 let cache = &self.kv_cache.layers[self.phys_layer(li)];
10173 cache.o1.is_none() || cache.o1_views().is_some()
10174 });
10175 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10176 eprintln!(
10177 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
10178 batch_admitted,
10179 batch_k,
10180 n.saturating_sub(exact_end),
10181 );
10182 }
10183 let mut batch_completed = false;
10184 if batch_admitted && exact_end < n {
10185 let hs = self.hidden_size;
10186 let mut batch_pos = exact_end;
10187 while batch_pos < n {
10188 let end = (batch_pos + batch_k).min(n);
10189 let bk = end - batch_pos;
10190 let mut hiddens = vec![0.0f32; bk * hs];
10191 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
10192 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
10193 }
10194 let positions: Vec<usize> = (batch_pos..end).collect();
10195 let t_batch = std::time::Instant::now();
10196 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
10197 if std::env::var("CMF_GRAPH_PROF").is_ok() {
10198 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
10199 eprintln!(
10200 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
10201 batch_pos,
10202 end.saturating_sub(1),
10203 bk as f64 / (ms / 1000.0),
10204 );
10205 }
10206 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
10207 self.nll_end();
10208 return Err(err);
10209 }
10210 match outcome {
10211 crate::gpu::BatchGraphOutcome::Completed => {
10212 batch_completed = true;
10213 for row in 0..bk {
10214 nll += self.nll_from_hidden(
10215 &hiddens[row * hs..(row + 1) * hs],
10216 ids[batch_pos + row + 1],
10217 batch_pos + row,
10218 );
10219 cnt += 1;
10220 }
10221 batch_pos = end;
10222 }
10223 crate::gpu::BatchGraphOutcome::Declined => {
10224 if batch_completed {
10225 self.nll_end();
10226 return Err(format!(
10227 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
10228 ));
10229 }
10230 break;
10231 }
10232 crate::gpu::BatchGraphOutcome::Failed => {
10233 self.nll_end();
10234 return Err(format!(
10235 "O(1) NLL batch graph failed after admission at position {batch_pos}"
10236 ));
10237 }
10238 }
10239 }
10240 if batch_completed && cnt == n.saturating_sub(requested_start) {
10241 self.nll_end();
10242 return Ok((nll, cnt));
10243 }
10244 }
10245
10246 for pos in exact_end..n {
10251 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10252 if self
10253 .graph_failed
10254 .swap(false, std::sync::atomic::Ordering::Relaxed)
10255 {
10256 self.cancel
10257 .store(false, std::sync::atomic::Ordering::Relaxed);
10258 self.nll_end();
10259 return Err(format!(
10260 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
10261 ));
10262 }
10263 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
10264 cnt += 1;
10265 }
10266 self.nll_end();
10267 Ok((nll, cnt))
10268 }
10269
10270 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
10278 self.clear_sequence_state();
10279 let n = ids.len().saturating_sub(1);
10280 let mut correct = Vec::with_capacity(n);
10281 let mut pmax = Vec::with_capacity(n);
10282 for pos in 0..n {
10283 let emb = self.embed_single(ids[pos]);
10284 let hidden = self.forward_layers(&emb, pos, None);
10285 let logits = if let Some(logits) = self.graph_logits.take() {
10286 logits
10287 } else {
10288 let normed = inference::rms_norm(
10289 &hidden,
10290 &self.weights.final_norm,
10291 self.rms_eps,
10292 self.norm_style,
10293 );
10294 self.lm_head_forward(&normed)
10298 };
10299 let target = ids[pos + 1] as usize;
10300 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
10301 for (i, &v) in logits.iter().enumerate() {
10302 if v > mval {
10303 mval = v;
10304 amax = i;
10305 }
10306 }
10307 correct.push(amax == target);
10308 let row: Vec<f32> = temps
10309 .iter()
10310 .map(|&t| {
10311 let tt = t.max(1e-3);
10312 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
10313 1.0 / s.max(1e-12) })
10315 .collect();
10316 pmax.push(row);
10317 }
10318 self.clear_sequence_state();
10319 (correct, pmax)
10320 }
10321
10322 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
10329 if self.dyn_router.is_none() {
10330 return Ok((self.ppl_ids(ids)?, 0));
10331 }
10332 self.nll_begin()?;
10333 let saved_active = self.dyn_active;
10334 let mut router = self
10335 .dyn_router
10336 .take()
10337 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
10338 router.reset();
10339 self.dyn_phi_seen = 0;
10340 let _ = self.set_active_skill(None);
10341
10342 let result: Result<(f64, usize), String> = (|| {
10343 let mut nll = 0f64;
10344 let mut cnt = 0usize;
10345 for pos in 0..ids.len().saturating_sub(1) {
10346 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
10347 self.nll_check_graph("dynamic serial forward", pos)?;
10348 let out_of_band = self.graph_logits.take();
10349 let mut logits = match out_of_band {
10350 Some(lg) => lg,
10351 None => {
10352 let normed = inference::rms_norm(
10353 &hidden,
10354 &self.weights.final_norm,
10355 self.rms_eps,
10356 self.norm_style,
10357 );
10358 self.lm_head_forward(&normed)
10362 }
10363 };
10364 let target = ids[pos + 1] as usize;
10365 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
10366 let lse: f64 = logits
10367 .iter()
10368 .map(|&v| ((v - max) as f64).exp())
10369 .sum::<f64>()
10370 .ln()
10371 + max as f64;
10372 let tok_nll = lse - logits[target] as f64;
10373 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
10374 let top = logits
10375 .iter()
10376 .enumerate()
10377 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
10378 .map(|(i, _)| i)
10379 .unwrap_or(0);
10380 eprintln!(
10381 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
10382 logits[target], logits[top]
10383 );
10384 }
10385 nll += tok_nll;
10386 cnt += 1;
10387 attention::recycle_buf(&mut logits);
10388 let phi = self.dyn_phi_ema.clone();
10390 if let Some(new_active) = router.step(&phi, pos) {
10391 let _ = self.set_active_skill(new_active);
10392 }
10393 }
10394 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
10395 })();
10396
10397 let _ = self.set_active_skill(saved_active);
10400 self.dyn_router = Some(router);
10401 self.nll_end();
10402 result
10403 }
10404
10405 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
10407 self.clear_sequence_state();
10408 let mut acc = vec![0f32; self.hidden_size];
10409 for (pos, &id) in ids.iter().enumerate() {
10410 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10411 for (a, v) in acc.iter_mut().zip(&h) {
10412 *a += v;
10413 }
10414 }
10415 let n = ids.len().max(1) as f32;
10416 for a in acc.iter_mut() {
10417 *a /= n;
10418 }
10419 self.clear_sequence_state();
10420 acc
10421 }
10422
10423 pub fn probe_phi_span(
10437 &mut self,
10438 ids: &[u32],
10439 layer: usize,
10440 span: std::ops::Range<usize>,
10441 ) -> Vec<f32> {
10442 #[cfg(target_os = "macos")]
10443 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10444 let end = span.end.min(ids.len());
10445 let start = span.start.min(end);
10446 let reset = |p: &mut Self| p.clear_sequence_state();
10447 reset(self);
10448 let mut acc = vec![0f32; self.hidden_size];
10449 for (pos, &id) in ids[..end].iter().enumerate() {
10450 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
10451 if pos >= start {
10452 for (a, v) in acc.iter_mut().zip(&h) {
10453 *a += v;
10454 }
10455 }
10456 }
10457 let n = end - start;
10458 if n > 0 {
10459 let n = n as f32;
10460 for a in acc.iter_mut() {
10461 *a /= n;
10462 }
10463 }
10464 reset(self);
10465 acc
10466 }
10467
10468 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
10475 #[cfg(target_os = "macos")]
10476 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
10477 self.graph_logits = None;
10478 let hidden = self.forward_layers(&self.embed_single(token), position, None);
10479 if let Some(logits) = self.graph_logits.take() {
10480 return logits;
10481 }
10482 inference::rms_norm_into(
10483 &hidden,
10484 &self.weights.final_norm,
10485 self.rms_eps,
10486 self.norm_style,
10487 &mut self.ws.n1,
10488 );
10489 self.lm_head_forward(&self.ws.n1)
10490 }
10491
10492 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
10498 self.prefill_batch_masked(ids, start_pos, None)
10499 }
10500
10501 fn prefill_batch_masked(
10507 &mut self,
10508 ids: &[u32],
10509 start_pos: usize,
10510 task_mask: Option<&TaskMask>,
10511 ) -> Vec<f32> {
10512 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
10513 }
10514
10515 fn prefill_rows(
10523 &mut self,
10524 ids: &[u32],
10525 pos: usize,
10526 task_mask: Option<&TaskMask>,
10527 ) -> Result<Vec<f32>, String> {
10528 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
10529 }
10530
10531 fn prefill_input_rows(
10532 &mut self,
10533 input: PrefillIn<'_>,
10534 pos: usize,
10535 task_mask: Option<&TaskMask>,
10536 ) -> Result<Vec<f32>, String> {
10537 self.mimo_moe_prepare();
10538 let hs = self.hidden_size;
10539 let bk = match input {
10540 PrefillIn::Ids(ids) => ids.len(),
10541 PrefillIn::Hidden(rows) => rows.len() / hs,
10542 };
10543 #[cfg(not(target_os = "macos"))]
10544 if task_mask.is_none()
10545 && !self.o1_active()
10546 && bk > 1
10547 && (self.batch_prefix_prefill()
10548 || (self.verify_exact_moe
10549 && crate::gpu::enabled_here()
10550 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
10551 {
10552 let mut hiddens = match input {
10553 PrefillIn::Hidden(rows) => rows.to_vec(),
10554 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
10555 };
10556 let positions: Vec<usize> = (pos..pos + bk).collect();
10557 let mut run = 0usize;
10558 match self.try_batch_graph_wgpu_prefix(
10559 &mut hiddens,
10560 &positions,
10561 bk,
10562 None,
10563 Some(&mut run),
10564 ) {
10565 crate::gpu::BatchGraphOutcome::Completed => {
10566 let out = if run < self.num_layers {
10567 self.prefill_batch_span(
10568 PrefillIn::Hidden(&hiddens),
10569 pos,
10570 None,
10571 run,
10572 self.num_layers,
10573 )
10574 } else {
10575 hiddens
10576 };
10577 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10578 Err("MiMo attention graph failed after admission".into())
10579 } else {
10580 Ok(out)
10581 };
10582 }
10583 crate::gpu::BatchGraphOutcome::Failed => {
10584 return Err("batched prefix prefill failed after admission".into());
10585 }
10586 crate::gpu::BatchGraphOutcome::Declined => {
10587 #[cfg(feature = "gpu")]
10589 self.pull_lagging_host_kv(0, self.num_layers, pos);
10590 }
10591 }
10592 }
10593 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
10594 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10595 Err("batch tail graph failed after admission".into())
10596 } else {
10597 Ok(out)
10598 }
10599 }
10600
10601 fn prefill_batch_span(
10607 &mut self,
10608 input: PrefillIn<'_>,
10609 start_pos: usize,
10610 task_mask: Option<&TaskMask>,
10611 from: usize,
10612 upto_excl: usize,
10613 ) -> Vec<f32> {
10614 let hs = self.hidden_size;
10615 let b = match input {
10616 PrefillIn::Ids(ids) => ids.len(),
10617 PrefillIn::Hidden(hb) => hb.len() / hs,
10618 };
10619 let upto_excl = upto_excl.min(self.num_layers);
10620 let mut h: Vec<f32>;
10624 let mut h_ready;
10625 match input {
10626 PrefillIn::Ids(_) => {
10627 h = vec![0.0; b * hs];
10628 h_ready = false;
10629 }
10630 PrefillIn::Hidden(hb) => {
10631 h = hb.to_vec();
10632 h_ready = true;
10633 }
10634 }
10635 let fill_h = |h: &mut Vec<f32>, me: &Self| {
10636 if let PrefillIn::Ids(ids) = input {
10637 for (bi, &id) in ids.iter().enumerate() {
10638 let e = me.embed_single(id);
10639 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
10640 }
10641 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
10642 if let Ok(t) = tp.parse::<usize>() {
10643 if t >= start_pos && t < start_pos + ids.len() {
10644 let bi = t - start_pos;
10645 let row = &h[bi * hs..(bi + 1) * hs];
10646 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
10647 eprintln!(
10648 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
10649 ids[bi],
10650 row[0],
10651 row[1],
10652 ids.len(),
10653 &ids[..ids.len().min(8)]
10654 );
10655 }
10656 }
10657 }
10658 }
10659 };
10660 let (_nkv, _hd, _rd, eps) = (
10661 self.num_kv_heads,
10662 self.head_dim,
10663 self.rotary_dim,
10664 self.rms_eps,
10665 );
10666 let pool = self.pool.clone();
10667 let norm_style = self.norm_style;
10668 self.mimo_moe_prepare();
10669 let automatic_gpu_prefix = self.automatic_gpu_prefix();
10670
10671 #[cfg(target_os = "macos")]
10672 let mut chunk_skip_until = 0usize;
10673 for li in from..upto_excl {
10674 let _capacity_tail = automatic_gpu_prefix
10675 .filter(|&prefix| {
10676 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
10677 })
10678 .map(|_| crate::gpu::enter_cpu_scope());
10679 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
10684 let _fast_gemm = self
10685 .proj_gate_sigmoid
10686 .then(crate::gpu::enter_prefill_fast_gemm);
10687 #[cfg(target_os = "macos")]
10693 if task_mask.is_none() {
10694 if li < chunk_skip_until {
10695 continue;
10696 }
10697 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
10703 fill_h(&mut h, self);
10704 h_ready = true;
10705 }
10706 let ids_for_embed = match input {
10707 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
10708 PrefillIn::Hidden(_) => None,
10709 };
10710 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
10711 if end > li {
10712 h_ready = true;
10713 chunk_skip_until = end;
10714 if self.is_loop_end(end - 1) && end < self.num_layers {
10717 for bi in 0..b {
10718 let normed = inference::rms_norm(
10719 &h[bi * hs..(bi + 1) * hs],
10720 &self.weights.final_norm,
10721 eps,
10722 norm_style,
10723 );
10724 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
10725 }
10726 }
10727 continue;
10728 }
10729 }
10730 if !h_ready {
10731 fill_h(&mut h, self);
10732 h_ready = true;
10733 }
10734 if task_mask.is_none() && self.verify_exact_moe {
10735 let positions: Vec<_> = (start_pos..start_pos + b).collect();
10736 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
10737 crate::gpu::BatchGraphOutcome::Completed => continue,
10738 crate::gpu::BatchGraphOutcome::Failed => return h,
10739 crate::gpu::BatchGraphOutcome::Declined => {}
10740 }
10741 }
10742 #[cfg(feature = "gpu")]
10743 self.pull_lagging_host_kv(li, li + 1, start_pos);
10744 let lw = &self.weights.layers[self.phys_layer(li)];
10745 let t_attn = std::time::Instant::now();
10746 match &lw.attn {
10748 AttnKind::Kda(w) => {
10749 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
10751 let mut normed = vec![0.0f32; b * hs];
10752 for bi in 0..b {
10753 inference::rms_norm_into(
10754 &h[bi * hs..(bi + 1) * hs],
10755 &lw.input_norm,
10756 eps,
10757 norm_style,
10758 &mut normed[bi * hs..(bi + 1) * hs],
10759 );
10760 }
10761 let attn = crate::linear_core::kda_forward_batch(
10762 &normed,
10763 b,
10764 w,
10765 &cfg,
10766 &mut self.kv_cache.layers[li].linear_state,
10767 pool.as_deref(),
10768 );
10769 for (dst, &a) in h.iter_mut().zip(&attn) {
10770 *dst += a;
10771 }
10772 }
10773 AttnKind::LinearGdn(w) => {
10774 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
10776 let mut normed = vec![0.0f32; b * hs];
10777 for bi in 0..b {
10778 let r = inference::rms_norm(
10779 &h[bi * hs..(bi + 1) * hs],
10780 &lw.input_norm,
10781 eps,
10782 norm_style,
10783 );
10784 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
10785 }
10786 let attn = crate::linear_core::gdn_forward_batch(
10787 &normed,
10788 b,
10789 w,
10790 &cfg,
10791 &mut self.kv_cache.layers[li].linear_state,
10792 pool.as_deref(),
10793 );
10794 for (dst, &a) in h.iter_mut().zip(&attn) {
10795 *dst += a;
10796 }
10797 }
10798 AttnKind::ShortConv(w) => {
10799 let cfg = self
10802 .short_conv_cfg
10803 .expect("short-conv layer without short_conv_cfg");
10804 let mut normed = vec![0.0f32; b * hs];
10805 for bi in 0..b {
10806 inference::rms_norm_into(
10807 &h[bi * hs..(bi + 1) * hs],
10808 &lw.input_norm,
10809 eps,
10810 norm_style,
10811 &mut normed[bi * hs..(bi + 1) * hs],
10812 );
10813 }
10814 let attn = short_conv_forward_batch(
10815 &normed,
10816 b,
10817 w,
10818 &cfg,
10819 &mut self.kv_cache.layers[li].linear_state,
10820 pool.as_deref(),
10821 );
10822 for (dst, &a) in h.iter_mut().zip(&attn) {
10823 *dst += a;
10824 }
10825 }
10826 AttnKind::Mla(w) => {
10827 let inv_freq_l = self.layer_inv_freq(li);
10830 let rs = self.layer_rope_scale(li);
10831 let mut normed = vec![0.0f32; hs];
10832 for bi in 0..b {
10833 inference::rms_norm_into(
10834 &h[bi * hs..(bi + 1) * hs],
10835 &lw.input_norm,
10836 eps,
10837 norm_style,
10838 &mut normed,
10839 );
10840 let ao = mla_attention(
10841 w,
10842 &normed,
10843 &mut self.kv_cache.layers[li],
10844 start_pos + bi,
10845 &inv_freq_l,
10846 rs,
10847 eps,
10848 pool.as_deref(),
10849 );
10850 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
10851 *dst += a;
10852 }
10853 }
10854 }
10855 AttnKind::Full {
10856 wq,
10857 wk,
10858 wv,
10859 wo,
10860 q_norm,
10861 k_norm,
10862 output_gate,
10863 softplus_gate,
10864 bias,
10865 } => {
10866 let mut normed = vec![0.0f32; b * hs];
10870 for bi in 0..b {
10871 inference::rms_norm_into(
10872 &h[bi * hs..(bi + 1) * hs],
10873 &lw.input_norm,
10874 eps,
10875 norm_style,
10876 &mut normed[bi * hs..(bi + 1) * hs],
10877 );
10878 }
10879 let inv_freq_l = self.layer_inv_freq(li);
10880 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
10881 let cfg = QwenAttnCfg {
10882 num_heads: self.layer_num_heads(li),
10883 num_kv_heads: nkv_l,
10884 head_dim: hd_l,
10885 hidden_size: hs,
10886 position: start_pos,
10887 inv_freq: &inv_freq_l,
10888 rotary_dim: rd_l,
10889 scale: self.attn_scale,
10890 softcap: self.attn_softcap,
10891 window: self.layer_window(li),
10892 v_norm: self.attn_v_norm,
10893 qk_norm_after_rope: self.qk_norm_after_rope,
10894 gate_sigmoid: self.proj_gate_sigmoid,
10895 q_norm: q_norm.as_deref(),
10896 k_norm: k_norm.as_deref(),
10897 output_gate: *output_gate,
10898 softplus_gate: softplus_gate
10899 .as_ref()
10900 .map(|(gate, per_head)| (gate, *per_head)),
10901 rope_scale: self.layer_rope_scale(li),
10902 bias: bias
10903 .as_ref()
10904 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10905 rms_eps: eps,
10906 norm_style,
10907 pool: pool.as_deref(),
10908 v_head_dim: self.layer_v_dim(li),
10909 };
10910 #[cfg(feature = "gpu")]
10911 let _mirror = self
10912 .prefill_mirror_target(li)
10913 .map(crate::gpu::enter_prefill_mirror);
10914 let mut attn = attention::qwen_attention_batch(
10915 &normed,
10916 b,
10917 wq,
10918 wk,
10919 wv,
10920 wo,
10921 &mut self.kv_cache.layers[li],
10922 &cfg,
10923 );
10924 if let Some(w) = &lw.attn_out_norm {
10925 for bi in 0..b {
10926 inference::rms_norm_into(
10927 &attn[bi * hs..(bi + 1) * hs],
10928 w,
10929 eps,
10930 norm_style,
10931 &mut normed[bi * hs..(bi + 1) * hs],
10932 );
10933 }
10934 attn.copy_from_slice(&normed);
10935 }
10936 for (dst, &a) in h.iter_mut().zip(&attn) {
10937 *dst += a;
10938 }
10939 }
10940 AttnKind::Bounded(w) => {
10941 let mut normed = vec![0.0f32; b * hs];
10944 for bi in 0..b {
10945 inference::rms_norm_into(
10946 &h[bi * hs..(bi + 1) * hs],
10947 &lw.input_norm,
10948 eps,
10949 norm_style,
10950 &mut normed[bi * hs..(bi + 1) * hs],
10951 );
10952 }
10953 let rope = self
10954 .bounded_rope
10955 .clone()
10956 .expect("bounded layer without an installed rotation table");
10957 let cfg = crate::bounded::BoundedAttnCfg {
10958 num_heads: self.num_heads,
10959 num_kv_heads: self.num_kv_heads,
10960 head_dim: self.head_dim,
10961 hidden_size: hs,
10962 scale: self.attn_scale,
10963 rope: &rope,
10964 pool: pool.as_deref(),
10965 };
10966 let mut attn = crate::bounded::bounded_attention_batch(
10967 &normed,
10968 b,
10969 w,
10970 &mut self.kv_cache.layers[li],
10971 &cfg,
10972 );
10973 if let Some(wn) = &lw.attn_out_norm {
10974 for bi in 0..b {
10975 inference::rms_norm_into(
10976 &attn[bi * hs..(bi + 1) * hs],
10977 wn,
10978 eps,
10979 norm_style,
10980 &mut normed[bi * hs..(bi + 1) * hs],
10981 );
10982 }
10983 attn.copy_from_slice(&normed);
10984 }
10985 for (dst, &a) in h.iter_mut().zip(&attn) {
10986 *dst += a;
10987 }
10988 attention::recycle_buf(&mut attn);
10989 }
10990 AttnKind::Linear(w) => {
10991 for bi in 0..b {
10992 let normed = inference::rms_norm(
10993 &h[bi * hs..(bi + 1) * hs],
10994 &lw.input_norm,
10995 eps,
10996 norm_style,
10997 );
10998 vmf_phase_forward(
10999 &normed,
11000 w,
11001 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
11002 &mut self.kv_cache.layers[li].linear_state,
11003 pool.as_deref(),
11004 )
11005 .iter()
11006 .enumerate()
11007 .for_each(|(i, &a)| h[bi * hs + i] += a);
11008 }
11009 }
11010 }
11011
11012 let lw = &self.weights.layers[self.phys_layer(li)];
11014 let mut post = vec![0.0f32; b * hs];
11015 for bi in 0..b {
11016 let r =
11017 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
11018 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11019 }
11020 let mask_row = task_mask
11023 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
11024 .and_then(|m| m.ffn_masks.get(li))
11025 .map(|v| v.as_slice());
11026 let attn_ns = t_attn.elapsed().as_nanos() as u64;
11027 let t_ffn = std::time::Instant::now();
11028 let mut ffn = match &lw.ffn {
11029 FfnKind::Dense(d) if !d.segs.is_empty() => {
11030 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
11031 }
11032 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
11033 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
11034 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
11035 }
11036 FfnKind::Moe(m) if self.verify_exact_moe => {
11037 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
11038 }
11039 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
11042 let before = m.stats.borrow().clone();
11043 let out = crate::gpu::cpu_scope(|| {
11044 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
11045 });
11046 self.mimo_moe.prime(li, m, &before);
11047 out
11048 }
11049 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
11050 FfnKind::DenseMoe(dm) => {
11053 let mut out = vec![0.0f32; b * hs];
11054 for bi in 0..b {
11055 let r = dense_moe_ffn(
11056 dm,
11057 &post[bi * hs..(bi + 1) * hs],
11058 &h[bi * hs..(bi + 1) * hs],
11059 eps,
11060 norm_style,
11061 pool.as_deref(),
11062 );
11063 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
11064 }
11065 out
11066 }
11067 };
11068 if prefill_prof_on() {
11069 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
11070 PREFILL_SPLIT[1].fetch_add(
11071 t_ffn.elapsed().as_nanos() as u64,
11072 std::sync::atomic::Ordering::Relaxed,
11073 );
11074 }
11075 if let Some(w) = &lw.ffn_out_norm {
11076 for bi in 0..b {
11077 inference::rms_norm_into(
11078 &ffn[bi * hs..(bi + 1) * hs],
11079 w,
11080 eps,
11081 norm_style,
11082 &mut post[bi * hs..(bi + 1) * hs],
11083 );
11084 }
11085 ffn.copy_from_slice(&post);
11086 }
11087 for (dst, &f) in h.iter_mut().zip(&ffn) {
11088 *dst += f;
11089 }
11090 if let Some(sc) = lw.layer_scale {
11091 for v in h.iter_mut() {
11092 *v *= sc;
11093 }
11094 }
11095 if self.layer_dump.is_some() {
11097 for bi in 0..b {
11098 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
11099 }
11100 }
11101 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
11102 if let Ok(t) = tp.parse::<usize>() {
11103 if t >= start_pos && t < start_pos + b {
11104 let bi = t - start_pos;
11105 let row = &h[bi * hs..(bi + 1) * hs];
11106 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
11107 eprintln!(
11108 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
11109 row[0], row[1]
11110 );
11111 }
11112 }
11113 }
11114 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
11118 let row = &h[(b - 1) * hs..b * hs];
11119 let rms =
11120 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
11121 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
11122 eprintln!(
11123 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
11124 match &self.weights.layers[self.phys_layer(li)].attn {
11125 AttnKind::LinearGdn(_) => "gdn",
11126 AttnKind::Linear(_) => "vmf",
11127 AttnKind::ShortConv(_) => "conv",
11128 _ => "attn",
11129 },
11130 match &lw.ffn {
11131 FfnKind::Moe(_) => "moe",
11132 FfnKind::Dense(_) => "dense",
11133 FfnKind::DenseMoe(_) => "dense+moe",
11134 },
11135 );
11136 }
11137 if self.is_loop_end(li) && li + 1 < self.num_layers {
11139 for bi in 0..b {
11140 let normed = inference::rms_norm(
11141 &h[bi * hs..(bi + 1) * hs],
11142 &self.weights.final_norm,
11143 eps,
11144 norm_style,
11145 );
11146 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
11147 }
11148 }
11149 if std::env::var("CMF_TRACE_H").is_ok() {
11150 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
11151 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
11152 eprintln!(
11153 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
11154 lw.layer_scale
11155 );
11156 }
11157 }
11158 crate::gpu::set_layer(-1); if prefill_prof_on() {
11160 let ms = |a: &std::sync::atomic::AtomicU64| {
11161 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
11162 };
11163 let sp = &attention::ATTN_SPLIT;
11164 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
11165 eprintln!(
11166 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
11167 host gate {:.1}], host loop {:.1}, attend {:.1} \
11168 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
11169 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
11170 ms(&PREFILL_SPLIT[0]),
11171 ms(&sp[0]),
11172 qkv_enq,
11173 qkv_wait,
11174 ms(&sp[6]),
11175 ms(&sp[1]),
11176 ms(&sp[2]),
11177 ms(&sp[4]),
11178 ms(&sp[5]),
11179 ca_wait,
11180 ms(&sp[3]),
11181 ms(&PREFILL_SPLIT[1]),
11182 );
11183 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
11184 eprintln!(
11185 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
11186 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
11187 fallbacks (cumulative)",
11188 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
11189 );
11190 }
11191 self.o1_progress();
11196 self.swa_trim_tails();
11198 h
11199 }
11200
11201 fn embed_single(&self, id: u32) -> Vec<f32> {
11203 let mut out = vec![0.0f32; self.hidden_size];
11204 if (id as usize) < self.weights.embed_tokens.rows() {
11205 self.weights.embed_tokens.row_f32(id as usize, &mut out);
11206 }
11207 if self.embed_multiplier != 1.0 {
11208 for v in out.iter_mut() {
11209 *v *= self.embed_multiplier;
11210 }
11211 }
11212 if self.dsv4.is_some() || self.dsv41.is_some() || self.qwen4_exp.is_some() {
11216 let mut v = vec![0.0f32; self.hidden_size.max(1)];
11217 v[0] = id as f32;
11218 return v;
11219 }
11220 if let Some(b) = &self.g4e {
11224 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11225 }
11226 if let Some(b) = &self.g3n {
11227 return b.0.extend_embedding(id, &out, self.pool.as_deref());
11228 }
11229 out
11230 }
11231
11232 #[cfg(target_os = "macos")]
11238 fn chunk_run_gpu(
11239 &mut self,
11240 li0: usize,
11241 h: &mut [f32],
11242 b: usize,
11243 pos0: usize,
11244 embed_ids: Option<&[u32]>,
11245 cap: usize,
11246 ) -> usize {
11247 let gemma4 =
11254 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
11255 if !crate::gpu::enabled_here()
11256 || std::env::var("CMF_GPU_CHUNK")
11257 .map(|v| v == "0")
11258 .unwrap_or(false)
11259 || b < self.chunk_min_rows
11260 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
11263 || (self.global_attn.is_some() && !gemma4)
11264 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11268 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
11271 || self.o1_active()
11274 || (self.attn_v_norm && !gemma4)
11275 || !self.metal_rope_scale_ok()
11276 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
11277 {
11278 return li0;
11279 }
11280 let Some(model) = self.model.clone() else {
11281 return li0;
11282 };
11283 let (nh, nkv, hd, hs) = (
11284 self.num_heads,
11285 self.num_kv_heads,
11286 self.head_dim,
11287 self.hidden_size,
11288 );
11289 let loop_end = if self.loop_final_norm {
11293 ((li0 / self.physical_layers) + 1) * self.physical_layers
11294 } else {
11295 self.num_layers
11296 };
11297 let run_end = self.num_layers.min(loop_end).min(cap);
11298 let tables: Vec<std::sync::Arc<Vec<f32>>> =
11303 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
11304 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
11305 let mut stored_at: Vec<usize> = Vec::new();
11306 for li in li0..run_end {
11307 let lw = &self.weights.layers[self.phys_layer(li)];
11308 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
11309 && !gemma4
11310 {
11311 break;
11312 }
11313 let AttnKind::Full {
11314 wq,
11315 wk,
11316 wv,
11317 wo,
11318 q_norm,
11319 k_norm,
11320 output_gate: false,
11321 softplus_gate,
11322 bias,
11323 } = &lw.attn
11324 else {
11325 break;
11326 };
11327 let head_gate = match softplus_gate {
11329 None => None,
11330 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
11331 Some((d, r, c)) if r == nh && c == hs => Some(d),
11332 _ => break,
11333 },
11334 Some(_) => break,
11335 };
11336 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
11340 FfnKind::Dense(d)
11341 if (matches!(d.act, Act::Silu | Act::Gelu)
11342 || (gemma4 && d.act == Act::GeluTanh))
11343 && d.segs.is_empty() =>
11344 {
11345 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
11346 }
11347 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
11348 Some((router, experts, q8)) => {
11349 let e0 = &m.experts[0];
11350 let route = Box::new(move |lg: &[f32]| {
11351 let (idx, p, wsum) = moe_route(lg, m, None);
11353 let mut st = m.stats.borrow_mut();
11354 if st.len() < m.experts.len() {
11355 st.resize(m.experts.len(), 0);
11356 }
11357 for &e in &idx {
11358 st[e] += 1;
11359 }
11360 let w = idx.iter().map(|&e| p[e] / wsum).collect();
11361 (idx, w)
11362 });
11363 (
11364 &e0.gate_proj,
11365 &e0.up_proj,
11366 &e0.down_proj,
11367 Act::Silu,
11368 Some(crate::gpu_metal::ChunkMoe {
11369 router,
11370 experts,
11371 q8,
11372 route,
11373 }),
11374 )
11375 }
11376 None => break,
11377 },
11378 _ => break,
11379 };
11380 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
11386 t.q8_row_parts()
11387 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
11388 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11389 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
11390 }
11391 fn cf(t: &QTensor) -> &[f32] {
11392 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
11393 }
11394 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
11395 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
11396 if moe.is_none()
11406 && !gemma4
11407 && ffn_act != Act::Gelu
11408 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
11409 {
11410 break;
11411 }
11412 let parts = (
11413 cw(wq),
11414 cw(wk),
11415 cw(wv),
11416 cw(wo),
11417 cw(gate_t),
11418 cw(up_t),
11419 cw(down_t),
11420 );
11421 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
11422 else {
11423 break;
11424 };
11425 let layer = &self.kv_cache.layers[li];
11426 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
11427 break;
11428 }
11429 debug_assert!(
11431 layer.base() == 0
11432 || self
11433 .layer_window(li)
11434 .is_some_and(|w| layer.head_len(0) + 1 >= w)
11435 );
11436 stored_at.push(layer.head_len(0));
11437 let (nkv, hd) = if gemma4 {
11440 let (a, bb, _) = self.layer_geom(li);
11441 (a, bb)
11442 } else {
11443 (nkv, hd)
11444 };
11445 layers.push(crate::gpu_metal::ChunkLayer {
11446 model: &model,
11447 kv_id: self.graph_kv_id,
11448 layer: li,
11449 wq: pq,
11450 wk: pk,
11451 wv: pv,
11452 wo: po,
11453 gate: pg,
11454 up: pu,
11455 down: pd,
11456 input_norm: &lw.input_norm,
11457 post_norm: &lw.post_norm,
11458 bias: bias
11459 .as_ref()
11460 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
11461 q_norm: q_norm.as_deref(),
11462 k_norm: k_norm.as_deref(),
11463 inv_freq: &tables[li - li0],
11464 rope_scale: self.layer_rope_scale(li),
11465 rd: self.layer_geom(li).2,
11466 nh,
11467 nkv,
11468 hd,
11469 hs,
11470 inter: gate_t.rows(),
11471 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
11472 late_qk_norm: self.qk_norm_after_rope,
11473 eps: self.rms_eps as f32,
11474 window: self.layer_window(li),
11475 head_gate,
11476 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
11477 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
11478 v_norm: self.attn_v_norm,
11479 scale: self.attn_scale,
11480 attn_out_norm: lw.attn_out_norm.as_deref(),
11481 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11482 layer_scale: lw.layer_scale,
11483 }),
11484 col,
11485 ffn_col: if moe.is_none() && !gemma4 {
11488 ffn_col
11489 } else {
11490 [&[][..]; 3]
11491 },
11492 moe,
11493 });
11494 }
11495 if layers.is_empty() {
11496 return li0;
11497 }
11498 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
11500 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
11501 .iter()
11502 .zip(&rows_kv)
11503 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
11504 .collect();
11505 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
11506 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
11507 let li = layers[i].layer;
11508 let layer = &self.kv_cache.layers[li];
11509 let nkv = rows_kv[i].0;
11510 io.push(crate::gpu_metal::ChunkIo {
11511 cpu_stored: stored_at[i],
11512 cpu_gen: layer.generation(),
11513 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
11514 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
11515 out_k: ok,
11516 out_v: ov,
11517 imp: oi,
11518 });
11519 }
11520 let n_run = layers.len();
11521 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
11522 let ep = embed_ids.and_then(|ids| {
11525 self.weights
11526 .embed_tokens
11527 .q8_row_parts()
11528 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
11529 idx,
11530 rows,
11531 row_scale: rs,
11532 ids,
11533 mult: self.embed_multiplier,
11534 })
11535 });
11536 if embed_ids.is_some() && ep.is_none() {
11537 return li0;
11538 }
11539 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
11540 return li0;
11541 }
11542 drop(io);
11543 drop(layers);
11544 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
11547 let li = li0 + i;
11548 let row = rows_kv[i].1;
11549 let layer = &mut self.kv_cache.layers[li];
11550 for bi in 0..b {
11551 layer.append(
11552 &ok[bi * row..(bi + 1) * row],
11553 &ov[bi * row..(bi + 1) * row],
11554 &[],
11555 );
11556 }
11557 layer.accumulate_imp(oi);
11558 }
11559 last
11560 }
11561
11562 fn layer_is_local(&self, li: usize) -> bool {
11565 if let Some(layers) = &self.sliding_layers {
11566 return layers.get(li).copied().unwrap_or(false);
11567 }
11568 match self.swa {
11569 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
11570 None => false,
11571 }
11572 }
11573
11574 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
11577 if self.layer_is_local(li) {
11578 if let Some(f) = &self.inv_freq_local {
11579 return f.clone();
11580 }
11581 } else if let Some(f) = &self.inv_freq_global {
11582 return f.clone();
11583 }
11584 self.inv_freq.clone()
11585 }
11586
11587 fn layer_window(&self, li: usize) -> Option<usize> {
11589 self.swa
11590 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
11591 }
11592
11593 fn swa_trim_tails(&mut self) {
11602 let Some((slack, align)) = self.swa_trim else {
11603 return;
11604 };
11605 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
11606 return;
11607 }
11608 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
11609 if let Some(w) = self.layer_window(li) {
11610 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
11611 }
11612 }
11613 }
11614
11615 fn layer_num_heads(&self, li: usize) -> usize {
11616 self.attention_heads_per_layer
11617 .as_ref()
11618 .and_then(|v| v.get(li).copied())
11619 .unwrap_or(self.num_heads)
11620 }
11621
11622 fn layer_rope_scale(&self, li: usize) -> f32 {
11623 if self.layer_is_local(li) {
11624 self.rope_scale_local
11625 } else {
11626 self.rope_scale
11627 }
11628 }
11629
11630 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
11633 if !self.layer_is_local(li) {
11634 if let Some((ghd, gkv)) = self.global_attn {
11635 return (gkv, ghd, ghd);
11636 }
11637 }
11638 let rd = if self.layer_is_local(li) {
11639 self.rotary_dim_local.unwrap_or(self.rotary_dim)
11640 } else {
11641 self.rotary_dim
11642 };
11643 let rd = if self
11645 .no_rope_layers
11646 .as_ref()
11647 .and_then(|v| v.get(li))
11648 .copied()
11649 == Some(true)
11650 {
11651 0
11652 } else {
11653 rd
11654 };
11655 (self.layer_num_kv_heads(li), self.head_dim, rd)
11656 }
11657
11658 fn layer_num_kv_heads(&self, li: usize) -> usize {
11661 self.kv_heads_per_layer
11662 .as_ref()
11663 .and_then(|v| v.get(self.phys_layer(li)).copied())
11664 .unwrap_or(self.num_kv_heads)
11665 }
11666
11667 fn layer_v_dim(&self, li: usize) -> usize {
11669 let (_, hd, _) = self.layer_geom(li);
11670 self.v_head_dim.unwrap_or(hd).min(hd)
11671 }
11672
11673 pub fn set_attn_geometry(
11682 &mut self,
11683 kv_heads_per_layer: Option<Vec<usize>>,
11684 v_head_dim: Option<usize>,
11685 ) -> Result<(), String> {
11686 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
11687 if self.global_attn.is_some() {
11688 return Err(
11689 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
11690 attention geometry"
11691 .into(),
11692 );
11693 }
11694 if self
11695 .weights
11696 .layers
11697 .iter()
11698 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
11699 {
11700 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
11701 }
11702 }
11703 if let Some(vd) = v_head_dim {
11704 if vd == 0 || vd > self.head_dim {
11705 return Err(format!(
11706 "v_head_dim {vd} must be in 1..={} (head_dim)",
11707 self.head_dim
11708 ));
11709 }
11710 }
11711 if let Some(v) = &kv_heads_per_layer {
11712 if v.len() != self.physical_layers {
11713 return Err(format!(
11714 "kv_heads_per_layer has {} entries, expected {} layers",
11715 v.len(),
11716 self.physical_layers
11717 ));
11718 }
11719 for (li, &nkv) in v.iter().enumerate() {
11720 let is_attn = matches!(
11721 self.weights.layers.get(li).map(|lw| &lw.attn),
11722 Some(AttnKind::Full { .. }) | None
11723 );
11724 if !is_attn {
11725 continue;
11726 }
11727 let nh = self
11728 .attention_heads_per_layer
11729 .as_ref()
11730 .and_then(|h| h.get(li).copied())
11731 .unwrap_or(self.num_heads);
11732 if nkv == 0 || nh % nkv != 0 {
11733 return Err(format!(
11734 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
11735 ));
11736 }
11737 }
11738 }
11739 self.kv_heads_per_layer = kv_heads_per_layer;
11740 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
11741 if self.kv_heads_per_layer.is_some() {
11742 for li in 0..self.kv_cache.layers.len() {
11743 let full = matches!(
11744 self.weights
11745 .layers
11746 .get(self.phys_layer(li))
11747 .map(|lw| &lw.attn),
11748 Some(AttnKind::Full { .. })
11749 );
11750 let nkv = self.layer_num_kv_heads(li);
11751 let cache = &self.kv_cache.layers[li];
11752 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
11753 let sinks = cache.sinks.clone();
11754 self.kv_cache.layers[li] =
11755 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
11756 self.kv_cache.layers[li].sinks = sinks;
11757 }
11758 }
11759 }
11760 Ok(())
11761 }
11762
11763 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
11767 let Some(lw) = self.weights.layers.get(phys) else {
11768 return Err(format!("sinks for layer {phys}: no such layer"));
11769 };
11770 if !matches!(lw.attn, AttnKind::Full { .. }) {
11771 return Err(format!(
11772 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
11773 ));
11774 }
11775 let nh = self
11776 .attention_heads_per_layer
11777 .as_ref()
11778 .and_then(|h| h.get(phys).copied())
11779 .unwrap_or(self.num_heads);
11780 if sinks.len() != nh {
11781 return Err(format!(
11782 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
11783 sinks.len()
11784 ));
11785 }
11786 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
11787 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
11788 }
11789 for li in 0..self.kv_cache.layers.len() {
11790 if self.phys_layer(li) == phys {
11791 self.kv_cache.layers[li].sinks = Some(sinks.clone());
11792 }
11793 }
11794 Ok(())
11795 }
11796
11797 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
11807 if self.kv_heads_per_layer.is_some() {
11808 return Some("per-layer KV head counts");
11809 }
11810 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
11811 return Some("V heads narrower than Q/K heads");
11812 }
11813 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
11814 return Some("learned attention sinks");
11815 }
11816 if self.swa.is_some() || self.sliding_layers.is_some() {
11817 return Some("sliding-window layers");
11818 }
11819 None
11820 }
11821
11822 #[cfg(target_os = "macos")]
11830 fn metal_rope_scale_ok(&self) -> bool {
11831 !(self.qk_norm_after_rope
11832 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
11833 }
11834
11835 #[cfg(target_os = "macos")]
11852 fn metal_graph_swa(&self) -> bool {
11853 (self.swa.is_some() || self.sliding_layers.is_some())
11854 && self.attention_heads_per_layer.is_none()
11855 && !self.attn_v_norm
11856 && self.attn_softcap == 0.0
11857 && self.kv_heads_per_layer.is_none()
11858 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
11859 && self.global_attn.is_none()
11860 && self.inv_freq_global.is_none()
11861 && self.metal_rope_scale_ok()
11862 && !(0..self.num_layers).any(|li| {
11863 self.layer_is_local(li)
11864 && self.inv_freq_local.is_none()
11865 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11866 })
11867 }
11868
11869 #[cfg(target_os = "macos")]
11877 fn metal_gemma4(&self) -> bool {
11878 crate::gpu_metal::gemma4::enabled()
11879 && (self.global_attn.is_some() || self.attn_v_norm)
11880 && self.attention_heads_per_layer.is_none()
11881 && self.kv_heads_per_layer.is_none()
11882 && self.v_head_dim.is_none()
11883 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
11884 && self.attn_softcap == 0.0
11885 && !self.qk_norm_after_rope
11886 && self.metal_rope_scale_ok()
11887 && self
11888 .weights
11889 .layers
11890 .iter()
11891 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
11892 && !(0..self.num_layers).any(|li| {
11893 self.layer_is_local(li)
11894 && self.inv_freq_local.is_none()
11895 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11896 })
11897 }
11898
11899 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
11906 let geometry_reason = self.graph_attn_decline_reason();
11910 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
11911 if has_scaled_rope && geometry_reason.is_none() {
11912 return Some("scaled RoPE without per-layer geometry");
11913 }
11914 if has_scaled_rope && self.qk_norm_after_rope {
11919 return Some("scaled RoPE with post-RoPE Q/K norm");
11920 }
11921 geometry_reason?;
11922 if self.attention_heads_per_layer.is_some() {
11928 return Some("per-layer Q head counts with per-layer geometry");
11929 }
11930 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
11934 if !gemma4
11935 && self.weights.layers.iter().any(|lw| {
11936 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11937 })
11938 {
11939 return Some("sandwich norms / layer scale with per-layer geometry");
11940 }
11941 if self.weights.layers.iter().any(|lw| {
11945 matches!(
11946 &lw.attn,
11947 AttnKind::Full {
11948 output_gate: true,
11949 ..
11950 }
11951 )
11952 }) && self.v_head_dim.is_some()
11953 {
11954 return Some("gated attention with V narrower than K");
11955 }
11956 if (0..self.num_layers).any(|li| {
11957 self.layer_is_local(li)
11958 && self.inv_freq_local.is_none()
11959 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
11960 }) {
11961 return Some("local rotary width without a local RoPE table");
11962 }
11963 None
11964 }
11965
11966 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
11970 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
11973 return None;
11974 }
11975 if self.global_attn.is_some() {
11976 return Some("per-layer head width (Gemma-4 global layers)");
11977 }
11978 if self.attn_v_norm {
11979 return Some("V norm");
11980 }
11981 if self.weights.layers.iter().any(|lw| {
11982 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
11983 }) {
11984 return Some("sandwich norms / layer scale");
11985 }
11986 None
11987 }
11988
11989 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
11994 self.graph_attn_decline_reason()?;
11995 let (nkv, hd, rd) = self.layer_geom(li);
11996 let invf: &[f32] = if self.layer_is_local(li) {
11997 match &self.inv_freq_local {
11998 Some(f) => f.as_slice(),
11999 None => self.inv_freq.as_slice(),
12000 }
12001 } else {
12002 match &self.inv_freq_global {
12003 Some(f) => f.as_slice(),
12004 None => self.inv_freq.as_slice(),
12005 }
12006 };
12007 Some(crate::gpu::GraphAttnGeom {
12008 nkv,
12009 hd,
12010 dv: self.layer_v_dim(li),
12011 rd,
12012 invf,
12013 rope_scale: self.layer_rope_scale(li),
12014 window: self.layer_window(li),
12015 sink: self.kv_cache.layers[li].sinks.as_deref(),
12016 v_norm: self.attn_v_norm,
12017 })
12018 }
12019
12020 #[cfg(feature = "gpu")]
12032 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
12033 if !self.proj_gate_sigmoid
12034 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
12035 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
12036 || self.graph_refused()
12037 || self.o1_active()
12038 || self.attn_softcap > 0.0
12039 || self.wgpu_graph_attn_decline().is_some()
12040 {
12041 return None;
12042 }
12043 let g = self.graph_attn_geom(li)?;
12044 let (nkv, hd, _) = self.layer_geom(li);
12045 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
12047 return None;
12048 }
12049 Some(crate::gpu::PrefillMirror {
12050 kv_id: self.graph_kv_id,
12051 layer: li,
12052 limit: self.kv_cache.max_seq_len,
12053 })
12054 }
12055
12056 #[cfg(feature = "gpu")]
12065 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
12066 let kv_id = self.graph_kv_id;
12067 for li in from..upto.min(self.num_layers) {
12068 if !matches!(
12069 self.weights.layers[self.phys_layer(li)].attn,
12070 AttnKind::Full { .. }
12071 ) {
12072 continue;
12073 }
12074 let host = self.kv_cache.layers[li].pos_len();
12076 if host >= position {
12077 continue;
12078 }
12079 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
12080 continue;
12081 };
12082 let to = dev.min(position);
12083 if to <= host {
12084 continue;
12085 }
12086 let (nkv, hd) = {
12087 let c = &self.kv_cache.layers[li];
12088 (c.num_kv_heads, c.head_dim)
12089 };
12090 let Some((k, v, first_valid)) =
12091 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
12092 else {
12093 continue;
12094 };
12095 let need_from = match self.layer_window(li) {
12098 Some(w) => host.max((position + 1).saturating_sub(w)),
12099 None => host,
12100 };
12101 if first_valid > need_from {
12102 tracing::warn!(
12103 "layer {li}: device KV rows {host}..{to} no longer resident \
12104 (from {first_valid}); host attention will miss them"
12105 );
12106 }
12107 let row = nkv * hd;
12108 let cache = &mut self.kv_cache.layers[li];
12109 for p in 0..to - host {
12110 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
12111 }
12112 }
12113 }
12114
12115 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
12118 let mut seen = self.graph_declines.borrow_mut();
12119 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
12120 tracing::warn!("{site} declined: {reason} (CPU attention path)");
12121 seen.push((site, reason));
12122 }
12123 }
12124
12125 pub fn graph_declines(&self) -> Vec<String> {
12128 self.graph_declines
12129 .borrow()
12130 .iter()
12131 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
12132 .collect()
12133 }
12134
12135 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
12144 let Some(dir) = &self.layer_dump else {
12145 return;
12146 };
12147 let mut bytes = Vec::with_capacity(row.len() * 4);
12148 for v in row {
12149 bytes.extend_from_slice(&v.to_le_bytes());
12150 }
12151 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
12152 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
12153 use std::sync::atomic::{AtomicBool, Ordering};
12154 static SAID: AtomicBool = AtomicBool::new(false);
12155 if !SAID.swap(true, Ordering::Relaxed) {
12156 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
12157 }
12158 }
12159 }
12160
12161 fn mimo_moe_prepare(&mut self) {
12164 if !self.mimo_moe.is_undecided() {
12165 return;
12166 }
12167 let slot = {
12168 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
12169 .filter_map(
12170 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
12171 FfnKind::Moe(m) => Some((li, m)),
12172 _ => None,
12173 },
12174 )
12175 .collect();
12176 if layers.is_empty()
12179 || self.physical_layers != self.num_layers
12180 || self.gpu_plan.is_some()
12181 {
12182 crate::mimo_moe::Slot::Off
12183 } else {
12184 let graph_prefix =
12188 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
12189 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
12190 }
12191 };
12192 self.mimo_moe = slot;
12193 }
12194
12195 #[cfg(test)]
12196 pub(crate) fn test_graph_kv_id(&self) -> u64 {
12197 self.graph_kv_id
12198 }
12199
12200 pub(crate) fn mimo_graph_layer_rows(
12204 &mut self,
12205 li: usize,
12206 h: &mut [f32],
12207 positions: &[usize],
12208 ) -> crate::gpu::BatchGraphOutcome {
12209 use crate::gpu::BatchGraphOutcome as Out;
12210 let b = positions.len();
12211 if !(1..=4).contains(&b)
12212 || h.len() != b * self.hidden_size
12213 || !self.mimo_moe.is_dynamic(li, true)
12214 || !crate::gpu::enabled_here()
12215 || !crate::gpu::wgpu_active()
12216 || self.o1_active()
12217 || self.physical_layers != self.num_layers
12218 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
12222 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
12223 || self.wgpu_graph_attn_decline().is_some()
12224 {
12225 return Out::Declined;
12226 }
12227 let attn_started = std::time::Instant::now();
12228 let outcome = {
12229 let lw = &self.weights.layers[li];
12230 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12231 return Out::Declined;
12232 }
12233 let FfnKind::Moe(m) = &lw.ffn else {
12234 return Out::Declined;
12235 };
12236 let AttnKind::Full {
12237 wq,
12238 wk,
12239 wv,
12240 wo,
12241 q_norm,
12242 k_norm,
12243 output_gate,
12244 softplus_gate,
12245 bias,
12246 } = &lw.attn
12247 else {
12248 return Out::Declined;
12249 };
12250 if *output_gate || softplus_gate.is_some() {
12251 return Out::Declined;
12252 }
12253 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
12254 m.experts
12255 .first()?
12256 .gate_proj
12257 .mapped_q4tp()
12258 .map(|(m, _)| m.clone())
12259 }) else {
12260 return Out::Declined;
12261 };
12262 fn gw<'a>(
12263 t: &'a QTensor,
12264 owner: &std::sync::Arc<cortiq_core::CmfModel>,
12265 ) -> Option<crate::gpu::GraphW<'a>> {
12266 if let Some((m, idx, kind, rs)) = t.graph_weight() {
12267 if m.uid() != owner.uid() || t.has_prism_contract() {
12268 return None;
12269 }
12270 return Some(crate::gpu::GraphW {
12271 idx,
12272 kind,
12273 row_scale: rs,
12274 data: &[],
12275 prism: crate::gpu::GraphPrismOp::None,
12276 affine: false,
12277 });
12278 }
12279 t.as_f32().map(|data| crate::gpu::GraphW {
12280 idx: 0,
12281 kind: 4,
12282 row_scale: &[],
12283 data,
12284 prism: crate::gpu::GraphPrismOp::None,
12285 affine: false,
12286 })
12287 }
12288 let (Some(q), Some(k), Some(v), Some(o)) = (
12289 gw(wq, &model),
12290 gw(wk, &model),
12291 gw(wv, &model),
12292 gw(wo, &model),
12293 ) else {
12294 return Out::Declined;
12295 };
12296 let layer = crate::gpu::GraphLayer {
12297 input_norm: &lw.input_norm,
12298 post_norm: &lw.post_norm,
12299 attn_out_norm: None,
12300 ffn_out_norm: None,
12301 layer_scale: None,
12302 ffn: crate::gpu::GraphFfn::AttentionOnly,
12303 attn: crate::gpu::GraphAttn::Full {
12304 wq: q,
12305 wk: k,
12306 wv: v,
12307 wo: o,
12308 q_norm: q_norm.as_deref(),
12309 k_norm: k_norm.as_deref(),
12310 late_qk_norm: self.qk_norm_after_rope,
12311 bias: bias
12312 .as_ref()
12313 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
12314 output_gate: false,
12315 cpu_k: self.kv_cache.layers[li].k_heads(),
12316 cpu_v: self.kv_cache.layers[li].v_heads(),
12317 cpu_base: self.kv_cache.layers[li].base(),
12318 geom: self.graph_attn_geom(li),
12319 head_gate: None,
12320 },
12321 };
12322 let (nkv, hd, rd) = self.layer_geom(li);
12323 crate::gpu::forward_batch_graph_at(
12324 &model,
12325 self.graph_kv_id,
12326 li,
12327 &[layer],
12328 &self.inv_freq,
12329 h,
12330 self.layer_num_heads(li),
12331 nkv,
12332 hd,
12333 rd,
12334 self.hidden_size,
12335 1,
12336 positions,
12337 self.kv_cache.max_seq_len,
12338 self.norm_style == cortiq_core::NormStyle::Gemma,
12339 self.rms_eps as f32,
12340 self.attn_scale,
12341 b,
12342 &[],
12343 self.o1_epoch,
12344 None,
12345 None,
12346 )
12347 };
12348 match outcome {
12349 Out::Completed => {}
12350 Out::Declined => return Out::Declined,
12351 Out::Failed => {
12352 self.graph_failed
12353 .store(true, std::sync::atomic::Ordering::Relaxed);
12354 return Out::Failed;
12355 }
12356 }
12357 let attn_ns = attn_started.elapsed().as_nanos() as u64;
12358 let hs = self.hidden_size;
12359 let lw = &self.weights.layers[li];
12360 let FfnKind::Moe(m) = &lw.ffn else {
12361 unreachable!()
12362 };
12363 let mut post = vec![0.0; h.len()];
12364 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
12365 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
12366 }
12367 let mut ffn = if b == 1 {
12368 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
12369 } else {
12370 moe_ffn_banked_rows(
12371 &mut self.mimo_moe,
12372 li,
12373 m,
12374 &post,
12375 b,
12376 hs,
12377 self.pool.as_deref(),
12378 )
12379 };
12380 for (x, &f) in h.iter_mut().zip(&ffn) {
12381 *x += f;
12382 }
12383 attention::recycle_buf(&mut ffn);
12384 if self.layer_dump.is_some() {
12385 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
12386 self.dump_layer_row(pos, li, row);
12387 }
12388 }
12389 crate::mimo_moe::note_attention_graph(b, attn_ns);
12390 Out::Completed
12391 }
12392
12393 fn layer_attn_plain(&self, li: usize) -> bool {
12394 self.kv_heads_per_layer.is_none()
12395 && self.v_head_dim.is_none()
12396 && self.global_attn.is_none()
12397 && self.layer_window(li).is_none()
12398 && self.kv_cache.layers[li].sinks.is_none()
12399 }
12400
12401 fn forward_layers(
12403 &mut self,
12404 hidden: &[f32],
12405 position: usize,
12406 task_mask: Option<&TaskMask>,
12407 ) -> Vec<f32> {
12408 let out = self.forward_layers_upto(hidden, position, task_mask, None);
12409 self.o1_progress();
12410 self.swa_trim_tails();
12411 out
12412 }
12413
12414 pub fn embed_id(&self, id: u32) -> Vec<f32> {
12422 self.embed_single(id)
12423 }
12424
12425 pub fn split_supported(&self) -> Result<(), String> {
12429 if self.dsv4.is_some() {
12430 return Err(
12431 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
12432 );
12433 }
12434 if self.dsv41.is_some() {
12435 return Err(
12436 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
12437 .into(),
12438 );
12439 }
12440 if self.qwen4_exp.is_some() {
12441 return Err(
12442 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
12443 );
12444 }
12445 if self.per_token_stack() {
12446 return Err(
12447 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
12448 );
12449 }
12450 Ok(())
12451 }
12452
12453 pub fn forward_span(
12458 &mut self,
12459 hidden: &[f32],
12460 position: usize,
12461 from: usize,
12462 upto: usize,
12463 task_mask: Option<&TaskMask>,
12464 ) -> Result<Vec<f32>, String> {
12465 self.split_supported()?;
12466 if from > upto || upto >= self.num_layers {
12467 return Err(format!(
12468 "forward_span: layer range {from}..={upto} outside 0..{}",
12469 self.num_layers
12470 ));
12471 }
12472 if hidden.len() != self.hidden_size {
12473 return Err(format!(
12474 "forward_span: hidden len {} ≠ hidden_size {}",
12475 hidden.len(),
12476 self.hidden_size
12477 ));
12478 }
12479 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
12480 self.o1_progress();
12481 self.swa_trim_tails();
12482 if self
12483 .graph_failed
12484 .swap(false, std::sync::atomic::Ordering::Relaxed)
12485 {
12486 self.cancel
12487 .store(false, std::sync::atomic::Ordering::Relaxed);
12488 self.clear_sequence_state();
12489 return Err("forward_span: deferred O(1) transition failed".into());
12490 }
12491 Ok(out)
12492 }
12493
12494 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
12497 let normed = inference::rms_norm(
12498 hidden,
12499 &self.weights.final_norm,
12500 self.rms_eps,
12501 self.norm_style,
12502 );
12503 self.lm_head_forward(&normed)
12504 }
12505
12506 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
12508 sampler::sample_with_scratch(
12509 logits,
12510 &self.sampler_config,
12511 past_tokens,
12512 &mut self.rng,
12513 &mut self.sampler_scratch,
12514 )
12515 }
12516
12517 pub fn reset_session(&mut self) {
12519 self.clear_sequence_state();
12520 }
12521
12522 pub fn prefill_span_ids(
12528 &mut self,
12529 ids: &[u32],
12530 start_pos: usize,
12531 upto: usize,
12532 task_mask: Option<&TaskMask>,
12533 ) -> Result<Vec<f32>, String> {
12534 self.split_supported()?;
12535 if upto >= self.num_layers {
12536 return Err(format!(
12537 "prefill_span_ids: upto {upto} outside 0..{}",
12538 self.num_layers
12539 ));
12540 }
12541 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12545 let out =
12546 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
12547 self.check_o1_progress_failure("prefill_span_ids")?;
12548 Ok(out)
12549 } else {
12550 let hs = self.hidden_size;
12551 let mut out = Vec::with_capacity(ids.len() * hs);
12552 for (i, &id) in ids.iter().enumerate() {
12553 let emb = self.embed_id(id);
12554 out.extend_from_slice(&self.forward_span(
12555 &emb,
12556 start_pos + i,
12557 0,
12558 upto,
12559 task_mask,
12560 )?);
12561 }
12562 Ok(out)
12563 }
12564 }
12565
12566 pub fn prefill_span_hidden(
12569 &mut self,
12570 hidden: &[f32],
12571 start_pos: usize,
12572 from: usize,
12573 upto: usize,
12574 task_mask: Option<&TaskMask>,
12575 ) -> Result<Vec<f32>, String> {
12576 self.split_supported()?;
12577 let hs = self.hidden_size;
12578 if hidden.is_empty() || hidden.len() % hs != 0 {
12579 return Err(format!(
12580 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
12581 hidden.len()
12582 ));
12583 }
12584 if from > upto || upto >= self.num_layers {
12585 return Err(format!(
12586 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
12587 self.num_layers
12588 ));
12589 }
12590 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
12591 let out = self.prefill_batch_span(
12592 PrefillIn::Hidden(hidden),
12593 start_pos,
12594 task_mask,
12595 from,
12596 upto + 1,
12597 );
12598 self.check_o1_progress_failure("prefill_span_hidden")?;
12599 Ok(out)
12600 } else {
12601 let b = hidden.len() / hs;
12602 let mut out = Vec::with_capacity(hidden.len());
12603 for i in 0..b {
12604 let h = self.forward_span(
12605 &hidden[i * hs..(i + 1) * hs],
12606 start_pos + i,
12607 from,
12608 upto,
12609 task_mask,
12610 )?;
12611 out.extend_from_slice(&h);
12612 }
12613 Ok(out)
12614 }
12615 }
12616
12617 fn try_token_graph_wgpu(
12621 &self,
12622 hidden: &[f32],
12623 position: usize,
12624 logits_out: &mut Vec<f32>,
12625 layers_run: &mut usize,
12626 ) -> Option<Result<Vec<f32>, ()>> {
12627 self.try_token_graph_wgpu_steps(
12628 hidden,
12629 position,
12630 logits_out,
12631 1,
12632 None,
12633 Some(layers_run),
12634 0,
12635 self.num_layers,
12636 )
12637 }
12638
12639 fn try_token_graph_wgpu_span(
12643 &self,
12644 hidden: &[f32],
12645 position: usize,
12646 logits_out: &mut Vec<f32>,
12647 from: usize,
12648 upto_excl: usize,
12649 layers_run: &mut usize,
12650 ) -> Option<Result<Vec<f32>, ()>> {
12651 self.try_token_graph_wgpu_steps(
12652 hidden,
12653 position,
12654 logits_out,
12655 1,
12656 None,
12657 Some(layers_run),
12658 from,
12659 upto_excl,
12660 )
12661 }
12662
12663 #[cfg(feature = "gpu")]
12667 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
12668 if !crate::minicpm_tail::minimax_pool_on()
12669 || !crate::gpu::enabled_here()
12670 || self.head_clusters.is_some()
12671 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
12672 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
12673 {
12674 return None;
12675 }
12676 let (model, i, kind, rs) = self
12677 .weights
12678 .lm_head
12679 .graph_weight()
12680 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
12681 let name = &model.tensors[i].name;
12682 let prism = if crate::prism::is_inverse_embedding(&model, name) {
12683 crate::gpu::GraphPrismOp::InverseEmbedding
12684 } else if crate::prism::is_forward_weight(&model, name) {
12685 crate::gpu::GraphPrismOp::Forward
12686 } else {
12687 crate::gpu::GraphPrismOp::None
12688 };
12689 let gw = crate::gpu::GraphW {
12690 idx: i,
12691 kind,
12692 row_scale: rs,
12693 data: &[],
12694 prism,
12695 affine: crate::prism::is_affine_target(&model, name),
12696 };
12697 let rows = self.weights.lm_head.rows();
12698 let mut h = hidden.to_vec();
12699 let mut lg = Vec::new();
12700 let mut layers_run = 0usize;
12701 let outcome = crate::gpu::forward_token_graph(
12702 &model,
12703 self.graph_kv_id,
12704 &[],
12705 &[],
12706 self.o1_epoch,
12707 &self.inv_freq,
12708 &mut h,
12709 self.num_heads,
12710 self.num_kv_heads,
12711 self.head_dim,
12712 self.attn_scale,
12713 self.head_dim,
12714 self.hidden_size,
12715 self.intermediate_size,
12716 0,
12717 self.kv_cache.max_seq_len,
12718 self.norm_style == cortiq_core::NormStyle::Gemma,
12719 self.rms_eps as f32,
12720 Some((&gw, rows)),
12721 &self.weights.final_norm,
12722 &mut lg,
12723 &[],
12724 1,
12725 None,
12726 None,
12727 Some(&mut layers_run),
12728 0,
12729 false,
12730 );
12731 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
12732 return None;
12733 }
12734 lg.resize(self.vocab_size, 0.0);
12737 if let Some(m) = self.logit_multiplier {
12738 for l in lg.iter_mut() {
12739 *l *= m;
12740 }
12741 }
12742 if let Some(c) = self.final_softcap {
12743 softcap_in_place(&mut lg, c, self.pool.as_deref());
12744 }
12745 Some(lg)
12746 }
12747
12748 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
12751 if winners.is_empty() {
12752 return;
12753 }
12754 if self.m3_n_exp == 0 {
12755 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12756 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
12757 }
12758 for &e in winners {
12759 let e = e as usize;
12760 if e < self.m3_n_exp && li < self.num_layers {
12761 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
12762 }
12763 }
12764 if li + 1 == self.num_layers {
12765 self.m3_tokens += 1;
12766 if self.m3_tokens % 16 == 0 {
12767 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
12768 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
12769 let _ = std::fs::write(&path, bytes);
12770 }
12771 }
12772 }
12773 }
12774
12775 fn m3_prewarm(&mut self) {
12780 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
12781 return;
12782 };
12783 let Ok(bytes) = std::fs::read(&path) else {
12784 tracing::warn!("m3 prewarm: cannot read profile {path}");
12785 return;
12786 };
12787 let counts: Vec<u32> = bytes
12788 .chunks_exact(4)
12789 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
12790 .collect();
12791 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
12792 if counts.len() != self.num_layers * n_exp {
12793 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
12794 return;
12795 }
12796 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
12797 .ok()
12798 .and_then(|v| v.parse().ok())
12799 .unwrap_or(300);
12800 let mut ranked: Vec<(u32, usize, usize)> = counts
12801 .iter()
12802 .enumerate()
12803 .filter(|(_, c)| **c > 0)
12804 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
12805 .collect();
12806 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
12807 ranked.truncate(limit);
12808 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
12809 for (_, li, e) in ranked {
12810 per_layer[li].push(e);
12811 }
12812 let mut admitted = 0usize;
12813 for (li, ids) in per_layer.iter().enumerate() {
12814 if ids.is_empty() {
12815 continue;
12816 }
12817 let lw = &self.weights.layers[self.phys_layer(li)];
12818 let FfnKind::Moe(m) = &lw.ffn else {
12819 continue;
12820 };
12821 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
12823 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
12824 admitted += ids.len();
12825 }
12826 }
12827 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
12828 }
12829
12830 #[cfg(feature = "gpu")]
12841 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
12842 if self.o1_active() || self.attn_softcap > 0.0 {
12843 return Ok(false);
12844 }
12845 if !self.m3_prewarmed {
12846 self.m3_prewarmed = true;
12847 self.m3_prewarm();
12848 }
12849 let frame = {
12850 let lw = &self.weights.layers[self.phys_layer(li)];
12851 let FfnKind::Moe(m) = &lw.ffn else {
12852 return Ok(false);
12853 };
12854 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
12855 return Ok(false);
12856 }
12857 let t_arm = std::time::Instant::now();
12858 if self.m3_winners.len() != self.num_layers {
12859 self.m3_winners = vec![Vec::new(); self.num_layers];
12860 }
12861 let pred = std::mem::take(&mut self.m3_winners[li]);
12862 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
12863 if armed.is_none() {
12864 self.m3_winners[li] = pred; }
12866 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12867 match armed {
12868 Some(frame) => frame,
12869 None => return Ok(false),
12872 }
12873 };
12874 crate::gpu_wgpu::pooled_frame_enter(frame);
12875 let want_logits = self.graph_want_logits;
12879 self.graph_want_logits = false;
12880 let mut lg = Vec::new();
12881 let mut gl = 0usize;
12882 let t_span = std::time::Instant::now();
12883 let span = crate::gpu::gpu_scope(|| {
12884 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
12885 });
12886 self.graph_want_logits = want_logits;
12887 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12888 let hh = match span {
12889 Some(Ok(hh)) => hh,
12890 Some(Err(())) => return Err(()),
12891 None => {
12892 return Ok(false);
12895 }
12896 };
12897 if gl != 1 {
12898 return if gl == 0 { Ok(false) } else { Err(()) };
12901 }
12902 *h = hh;
12903 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
12904 self.m3_note_route(li, &winners);
12905 if self.m3_winners.len() != self.num_layers {
12906 self.m3_winners = vec![Vec::new(); self.num_layers];
12907 }
12908 self.m3_winners[li] = winners;
12909 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
12910 cold.retain(|&(_, w)| {
12913 if w.is_finite() {
12914 true
12915 } else {
12916 tracing::error!("pooled tail: non-finite cold weight from expert route");
12917 false
12918 }
12919 });
12920 if cold.is_empty() {
12921 return Ok(true);
12922 }
12923 let x = crate::gpu_wgpu::pooled_frame_take_input();
12924 if x.len() != self.hidden_size {
12925 return Err(());
12926 }
12927 let lw = &self.weights.layers[self.phys_layer(li)];
12933 let FfnKind::Moe(m) = &lw.ffn else {
12934 return Err(());
12935 };
12936 let t_cold = std::time::Instant::now();
12937 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
12938 let mut out = crate::attention::take_buf(self.hidden_size);
12939 let on_device = !force_host
12940 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
12941 if !on_device {
12942 crate::attention::recycle_buf(&mut out);
12943 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
12944 .iter()
12945 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
12946 .collect::<Result<_, ()>>()?;
12947 out = crate::gpu::cpu_scope(|| {
12948 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
12949 });
12950 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
12951 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
12952 }
12953 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
12954 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12955 if out.len() != h.len() {
12956 crate::attention::recycle_buf(&mut out);
12957 return Err(());
12958 }
12959 for (dst, src) in h.iter_mut().zip(out.iter()) {
12960 *dst += src;
12961 }
12962 crate::attention::recycle_buf(&mut out);
12963 if on_device {
12964 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
12965 }
12966 Ok(true)
12967 }
12968
12969 #[cfg(not(feature = "gpu"))]
12970 fn m3_pooled_layer(
12971 &mut self,
12972 _li: usize,
12973 _position: usize,
12974 _h: &mut Vec<f32>,
12975 ) -> Result<bool, ()> {
12976 Ok(false)
12977 }
12978
12979 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
12983 if self.o1_active() || self.attn_softcap > 0.0 {
12984 return None;
12985 }
12986 if let Some(reason) = self.wgpu_graph_attn_decline() {
12989 self.note_graph_decline("wgpu multi-burst", reason);
12990 return None;
12991 }
12992 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
12993 if !graph_on || self.graph_refused() {
12994 return None;
13001 }
13002 let emb = self.embed_single(t_next);
13003 let mut lg = Vec::new();
13004 let mut ids = Vec::new();
13005 match self.try_token_graph_wgpu_steps(
13006 &emb,
13007 position,
13008 &mut lg,
13009 k,
13010 Some(&mut ids),
13011 None,
13012 0,
13013 self.num_layers,
13014 ) {
13015 Some(Ok(_)) => {}
13016 Some(Err(())) => {
13017 self.graph_failed
13022 .store(true, std::sync::atomic::Ordering::Relaxed);
13023 return None;
13024 }
13025 None => return None,
13026 }
13027 (ids.len() == k).then_some(ids)
13028 }
13029
13030 fn try_token_graph_wgpu_steps(
13034 &self,
13035 hidden: &[f32],
13036 position: usize,
13037 logits_out: &mut Vec<f32>,
13038 steps: usize,
13039 ids_out: Option<&mut Vec<u32>>,
13040 layers_run: Option<&mut usize>,
13041 from: usize,
13042 upto_excl: usize,
13043 ) -> Option<Result<Vec<f32>, ()>> {
13044 let upto_excl = match self.mimo_moe.graph_prefix_end() {
13047 Some(end) if end < upto_excl => {
13048 if steps != 1 || layers_run.is_none() || from >= end {
13049 return None;
13050 }
13051 end
13052 }
13053 _ => upto_excl,
13054 };
13055 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
13058 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
13059 return None;
13063 }
13064 if let Some(reason) = self.wgpu_graph_attn_decline() {
13071 self.note_graph_decline("wgpu token graph", reason);
13072 return None;
13073 }
13074 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
13079 .map(|li| {
13080 if !o1_gpu {
13081 return None;
13082 }
13083 self.kv_cache.layers[self.phys_layer(li)].o1_views()
13084 })
13085 .collect();
13086 if self.o1_active() && o1_gpu {
13087 let want: usize = (from..upto_excl)
13090 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
13091 .count();
13092 let have = o1_views.iter().filter(|v| v.is_some()).count();
13093 if want == 0 || have != want {
13094 use std::sync::atomic::{AtomicUsize, Ordering};
13104 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
13105 let code = have * 1000 + want;
13106 if LAST.swap(code, Ordering::Relaxed) != code {
13107 tracing::warn!(
13108 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
13109 );
13110 }
13111 return None;
13112 }
13113 }
13114 let nh = self.num_heads;
13115 let (nkv, hd, rd) = self.layer_geom(0);
13116 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13117 let mut layers = Vec::with_capacity(upto_excl - from);
13118 let mut model = None;
13119 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
13120 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13121 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
13123 let name = &m.tensors[i].name;
13124 let prism = if crate::prism::is_inverse_embedding(m, name) {
13125 crate::gpu::GraphPrismOp::InverseEmbedding
13126 } else if crate::prism::is_forward_weight(m, name) {
13127 crate::gpu::GraphPrismOp::Forward
13128 } else {
13129 crate::gpu::GraphPrismOp::None
13130 };
13131 return Some(crate::gpu::GraphW {
13132 idx: i,
13133 kind,
13134 row_scale: rs,
13135 data: &[],
13136 prism,
13137 affine: crate::prism::is_affine_target(m, name),
13138 });
13139 }
13140 match t.as_f32() {
13142 Some(d) => Some(crate::gpu::GraphW {
13143 idx: 0,
13144 kind: 4,
13145 row_scale: &[],
13146 data: d,
13147 prism: crate::gpu::GraphPrismOp::None,
13148 affine: false,
13149 }),
13150 None => {
13151 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13152 eprintln!("batch graph: weight has no graph/f32 representation");
13153 }
13154 None
13155 }
13156 }
13157 }
13158 for li in from..upto_excl {
13159 let lw = &self.weights.layers[self.phys_layer(li)];
13160 if dbg {
13161 let ak = match &lw.attn {
13162 AttnKind::Mla(_) => "Mla".into(),
13163 AttnKind::Full {
13164 output_gate, bias, ..
13165 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
13166 AttnKind::LinearGdn(_) => "LinearGdn".into(),
13167 AttnKind::Kda(_) => "Kda".into(),
13168 AttnKind::Linear(_) => "Linear".into(),
13169 AttnKind::ShortConv(_) => "ShortConv".into(),
13170 AttnKind::Bounded(_) => "Bounded".into(),
13171 };
13172 let fk = match &lw.ffn {
13173 FfnKind::Dense(_) => "Dense",
13174 FfnKind::Moe(_) => "Moe",
13175 FfnKind::DenseMoe(_) => "DenseMoe",
13176 };
13177 eprintln!("graph L{li}: attn={ak} ffn={fk}");
13178 }
13179 let gffn = match &lw.ffn {
13180 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
13184 FfnKind::Dense(d) => {
13185 let Some(act) = d.act.wgpu_graph_act() else {
13189 self.note_graph_decline(
13190 "wgpu token graph",
13191 "dense FFN activation without a graph kernel",
13192 );
13193 return None;
13194 };
13195 crate::gpu::GraphFfn::Dense {
13196 gate: gw(&d.gate_proj)?,
13197 up: gw(&d.up_proj)?,
13198 down: gw(&d.down_proj)?,
13199 act,
13200 }
13201 }
13202 FfnKind::Moe(m) => {
13203 if m.route_tau.is_some() || m.mask.is_some() {
13211 return None;
13212 }
13213 let shared = m.shared.as_ref();
13214 let inter = m.experts.first()?.gate_proj.rows();
13220 let detached_shared = match shared {
13228 Some((se, None))
13229 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13230 {
13231 if !matches!(se.act, Act::Silu)
13232 || se.gate_proj.mapped_q4tp().is_none()
13233 || se.up_proj.mapped_q4tp().is_none()
13234 || se.down_proj.mapped_q4tp().is_none()
13235 {
13236 return None;
13237 }
13238 Some(crate::gpu::GraphMoeDetachedShared {
13239 gate: gw(&se.gate_proj)?,
13240 up: gw(&se.up_proj)?,
13241 down: gw(&se.down_proj)?,
13242 width: se.gate_proj.rows(),
13243 })
13244 }
13245 Some((se, Some(_)))
13249 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
13250 {
13251 return None;
13252 }
13253 _ => None,
13254 };
13255 let has_shared = shared.is_some() && detached_shared.is_none();
13256 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
13257 let sgate = match shared {
13258 Some((_, Some(sg))) if has_shared => gw(sg)?,
13259 _ => gw(&m.router)?,
13263 };
13264 let router = gw(&m.router)?;
13265 if router.prism != crate::gpu::GraphPrismOp::None
13271 || sgate.prism != crate::gpu::GraphPrismOp::None
13272 || router.affine
13273 || sgate.affine
13274 {
13275 tracing::warn!(
13276 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
13277 );
13278 return None;
13279 }
13280 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13281 let mut q4tp: Option<bool> = None;
13284 let mut gu_q2: Option<bool> = None;
13287 let mut q82: Option<bool> = None;
13289 let mut moe_act = None;
13293 for e in m
13294 .experts
13295 .iter()
13296 .chain(shared.filter(|_| has_shared).map(|(se, _)| se).into_iter())
13297 {
13298 let Some(e_act) = e.act.wgpu_graph_act() else {
13299 return None;
13300 };
13301 match moe_act {
13302 Some(existing) if existing != e_act => return None,
13303 None => moe_act = Some(e_act),
13304 _ => {}
13305 }
13306 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
13307 return None;
13308 }
13309 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
13314 let Some((em, ei, _, _)) = expert_weight
13315 .graph_weight()
13316 .or_else(|| expert_weight.graph_weight_descriptor())
13317 else {
13318 return None;
13319 };
13320 let name = &em.tensors[ei].name;
13321 if crate::prism::is_forward_weight(em, name)
13322 || crate::prism::is_inverse_embedding(em, name)
13323 || crate::prism::is_affine_target(em, name)
13324 {
13325 tracing::warn!(
13326 "resident MoE declined: expert Prism/affine transform is not implemented"
13327 );
13328 return None;
13329 }
13330 }
13331 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
13332 Some((mm, gi)) => (
13333 mm,
13334 gi,
13335 e.up_proj.mapped_q4t()?.1,
13336 e.down_proj.mapped_q4t()?.1,
13337 false,
13338 false,
13339 false,
13340 ),
13341 None => match e.gate_proj.mapped_q2tp() {
13342 Some((mm, gi)) => (
13343 mm,
13344 gi,
13345 e.up_proj.mapped_q2tp()?.1,
13346 e.down_proj.mapped_q4tp()?.1,
13347 true,
13348 true,
13349 false,
13350 ),
13351 None => match e.gate_proj.mapped_q4tp() {
13352 Some((mm, gi)) => (
13353 mm,
13354 gi,
13355 e.up_proj.mapped_q4tp()?.1,
13356 e.down_proj.mapped_q4tp()?.1,
13357 true,
13358 false,
13359 false,
13360 ),
13361 None => {
13364 if !crate::gpu::moe_q82_graph_on() {
13365 return None;
13366 }
13367 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13368 (
13369 mm,
13370 gi,
13371 e.up_proj.mapped_q8_2f()?.1,
13372 e.down_proj.mapped_q8_2f()?.1,
13373 false,
13374 false,
13375 true,
13376 )
13377 }
13378 },
13379 },
13380 };
13381 if *q4tp.get_or_insert(is_p) != is_p
13382 || *gu_q2.get_or_insert(is_q2) != is_q2
13383 || *q82.get_or_insert(is_8) != is_8
13384 {
13385 tracing::warn!(
13391 "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."
13392 );
13393 return None;
13394 }
13395 model.get_or_insert_with(|| mm.clone());
13396 experts.push((gi, ui, di));
13397 }
13398 crate::gpu::GraphFfn::Moe {
13399 router,
13400 shared_gate: sgate,
13401 experts,
13402 n_exp: m.experts.len(),
13403 top_k: std::env::var("CMF_TOPK_PROBE")
13409 .ok()
13410 .and_then(|v| v.parse::<usize>().ok())
13411 .filter(|k| *k > 0 && *k <= m.top_k)
13412 .unwrap_or(m.top_k),
13413 inter,
13414 act: moe_act?,
13415 norm_topk: m.norm_topk_prob,
13416 q4tp: q4tp?,
13417 gu_q2: gu_q2.unwrap_or(false),
13418 q82: q82.unwrap_or(false),
13419 sigmoid: m.router_sigmoid,
13420 sigmoid_denom_eps: m.sigmoid_denom_eps,
13421 bias: m.expert_bias.as_deref(),
13422 has_shared,
13423 detached_shared,
13424 shared_gated,
13425 route_scale: m.routed_scaling,
13426 }
13427 }
13428 };
13429 let attn = match &lw.attn {
13430 AttnKind::Full {
13431 wq,
13432 wk,
13433 wv,
13434 wo,
13435 q_norm,
13436 k_norm,
13437 output_gate,
13438 softplus_gate,
13439 bias,
13440 } => {
13441 if self.attention_heads_per_layer.is_some() {
13442 return None;
13443 }
13444 let head_gate = match softplus_gate {
13448 None => None,
13449 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
13450 Some(_) => {
13451 self.note_graph_decline(
13452 "wgpu token graph",
13453 "projected softplus / per-element output gate",
13454 );
13455 return None;
13456 }
13457 };
13458 let (m, _, _, _) = wq.graph_weight_or_blr()?;
13459 model = Some(m.clone());
13460 crate::gpu::GraphAttn::Full {
13461 wq: gw(wq)?,
13462 wk: gw(wk)?,
13463 wv: gw(wv)?,
13464 wo: gw(wo)?,
13465 q_norm: q_norm.as_deref(),
13466 k_norm: k_norm.as_deref(),
13467 late_qk_norm: self.qk_norm_after_rope,
13468 bias: bias
13469 .as_ref()
13470 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13471 output_gate: *output_gate,
13472 cpu_k: self.kv_cache.layers[li].k_heads(),
13473 cpu_v: self.kv_cache.layers[li].v_heads(),
13474 cpu_base: self.kv_cache.layers[li].base(),
13475 geom: self.graph_attn_geom(li),
13476 head_gate,
13477 }
13478 }
13479 AttnKind::LinearGdn(w) => {
13480 let cfg = self.gdn_cfg?;
13481 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
13482 model = Some(m.clone());
13483 crate::gpu::GraphAttn::Gdn {
13484 qkv: gw(&w.in_proj_qkv)?,
13485 z: gw(&w.in_proj_z)?,
13486 a: gw(&w.in_proj_a)?,
13487 b: gw(&w.in_proj_b)?,
13488 out: gw(&w.out_proj)?,
13489 conv1d: &w.conv1d,
13490 a_log: &w.a_log,
13491 dt_bias: &w.dt_bias,
13492 norm: &w.norm,
13493 nv: cfg.num_v_heads,
13494 nk: cfg.num_k_heads,
13495 dk: cfg.key_head_dim,
13496 dv: cfg.value_head_dim,
13497 kk: cfg.conv_kernel,
13498 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13499 preview: None,
13500 }
13501 }
13502 AttnKind::ShortConv(w) => {
13503 let cfg = self.short_conv_cfg?;
13504 let (m, _, _, _) = w
13505 .in_proj
13506 .graph_weight()
13507 .or_else(|| w.in_proj.graph_weight_descriptor())?;
13508 model = Some(m.clone());
13509 crate::gpu::GraphAttn::ShortConv {
13510 inp: gw(&w.in_proj)?,
13511 out: gw(&w.out_proj)?,
13512 taps: &w.conv,
13513 kernel: cfg.kernel,
13514 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13515 }
13516 }
13517 _ => return None,
13518 };
13519 layers.push(crate::gpu::GraphLayer {
13520 input_norm: &lw.input_norm,
13521 attn,
13522 post_norm: &lw.post_norm,
13523 attn_out_norm: lw.attn_out_norm.as_deref(),
13524 ffn_out_norm: lw.ffn_out_norm.as_deref(),
13525 layer_scale: lw.layer_scale,
13526 ffn: gffn,
13527 });
13528 }
13529 let model = model?;
13530 let lm_gw = if upto_excl == self.num_layers
13536 && self.graph_want_logits
13537 && std::env::var("CMF_GPU_LMHEAD")
13538 .map(|v| v != "0")
13539 .unwrap_or(true)
13540 {
13541 self.weights
13542 .lm_head
13543 .graph_weight()
13544 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
13545 .map(|(m, i, kind, rs)| {
13546 let name = &m.tensors[i].name;
13547 let prism = if crate::prism::is_inverse_embedding(m, name) {
13548 crate::gpu::GraphPrismOp::InverseEmbedding
13549 } else if crate::prism::is_forward_weight(m, name) {
13550 crate::gpu::GraphPrismOp::Forward
13551 } else {
13552 crate::gpu::GraphPrismOp::None
13553 };
13554 (
13555 crate::gpu::GraphW {
13556 idx: i,
13557 kind,
13558 row_scale: rs,
13559 data: &[],
13560 prism,
13561 affine: crate::prism::is_affine_target(m, name),
13562 },
13563 self.weights.lm_head.rows(),
13564 )
13565 })
13566 } else {
13567 None
13568 };
13569 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
13570 let emb_gw = if steps > 1 {
13572 self.weights
13573 .embed_tokens
13574 .graph_weight()
13575 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
13576 .map(|(m, i, kind, rs)| {
13577 let name = &m.tensors[i].name;
13578 let prism = if crate::prism::is_inverse_embedding(m, name) {
13579 crate::gpu::GraphPrismOp::InverseEmbedding
13580 } else if crate::prism::is_forward_weight(m, name) {
13581 crate::gpu::GraphPrismOp::Forward
13582 } else {
13583 crate::gpu::GraphPrismOp::None
13584 };
13585 (
13586 crate::gpu::GraphW {
13587 idx: i,
13588 kind,
13589 row_scale: rs,
13590 data: &[],
13591 prism,
13592 affine: crate::prism::is_affine_target(m, name),
13593 },
13594 self.weights.embed_tokens.rows(),
13595 self.embed_multiplier,
13596 )
13597 })
13598 } else {
13599 None
13600 };
13601
13602 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
13608 (from..upto_excl.min(self.num_layers - 1))
13609 .filter(|&li| (li + 1) % self.physical_layers == 0)
13610 .map(|li| li - from)
13611 .collect()
13612 } else {
13613 Vec::new()
13614 };
13615 let mut h = hidden.to_vec();
13616 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
13622 let outcome = crate::gpu::forward_token_graph(
13623 &model,
13624 self.graph_kv_id,
13625 &layers,
13626 &o1_views,
13627 self.o1_epoch,
13628 &self.inv_freq,
13629 &mut h,
13630 nh,
13631 nkv,
13632 hd,
13633 self.attn_scale,
13634 rd,
13635 self.hidden_size,
13636 self.intermediate_size,
13637 position,
13638 self.kv_cache.max_seq_len,
13639 gemma,
13640 self.rms_eps as f32,
13641 lm,
13642 &self.weights.final_norm,
13643 logits_out,
13644 &loop_norm_at,
13645 steps,
13646 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
13647 ids_out,
13648 layers_run,
13649 from,
13650 dump_hidden,
13651 );
13652 match outcome {
13653 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
13654 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
13655 crate::gpu::TokenGraphOutcome::Declined => None,
13656 }
13657 }
13658
13659 #[cfg(target_os = "macos")]
13668 #[allow(clippy::type_complexity)]
13669 fn metal_rows_plan(
13670 &self,
13671 ) -> Option<(
13672 Vec<MetalRowsItem<'_>>,
13673 std::sync::Arc<cortiq_core::CmfModel>,
13674 Option<crate::gpu_metal::GdnGpuCfg>,
13675 )> {
13676 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
13677 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
13678 if !graph_force
13679 || !crate::gpu::enabled_here()
13680 || std::env::var("CMF_GPU_BLOCK")
13681 .map(|v| v == "0")
13682 .unwrap_or(false)
13683 || self.attn_softcap > 0.0
13684 || self.o1_active()
13685 || self.swa.is_some()
13686 || self.global_attn.is_some()
13687 || self.attention_heads_per_layer.is_some()
13688 || self.graph_attn_decline_reason().is_some()
13690 || self.attn_v_norm
13691 || self.loop_final_norm
13692 || !self.metal_rope_scale_ok()
13693 {
13694 return None;
13695 }
13696 let attend_contract = self.head_dim % 4 == 0
13697 && self.head_dim <= 256
13698 && self.rotary_dim >= 2
13699 && self.rotary_dim <= self.head_dim
13700 && (self.rotary_dim / 2) % 32 == 0
13701 && self.num_kv_heads > 0
13702 && self.num_heads % self.num_kv_heads == 0;
13703 if !attend_contract {
13704 return None;
13705 }
13706 let mut plan: Vec<MetalRowsItem> = Vec::new();
13707 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
13708 for li in 0..self.num_layers {
13709 let lw = &self.weights.layers[self.phys_layer(li)];
13710 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
13711 return None;
13712 }
13713 let ffn = match &lw.ffn {
13714 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
13715 let (Some(g), Some(u), Some(dn)) = (
13716 d.gate_proj.metal_graph_parts(),
13717 d.up_proj.metal_graph_parts(),
13718 d.down_proj.metal_graph_parts(),
13719 ) else {
13720 return None;
13721 };
13722 MetalFfn::Dense {
13723 gate: g,
13724 up: u,
13725 down: dn,
13726 act: 0, }
13728 }
13729 _ => return None,
13730 };
13731 match &lw.attn {
13732 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
13733 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
13734 w.in_proj_qkv.metal_graph_parts(),
13735 w.in_proj_z.metal_graph_parts(),
13736 w.in_proj_a.f32_parts(),
13737 w.in_proj_b.f32_parts(),
13738 w.out_proj.metal_graph_parts(),
13739 ) else {
13740 return None;
13741 };
13742 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
13743 model_ref.get_or_insert_with(|| model.clone());
13744 }
13745 let gl = GdnGpuLayer {
13746 attn_norm: &lw.input_norm,
13747 post_norm: &lw.post_norm,
13748 qkv,
13749 z,
13750 a,
13751 b: bb,
13752 out,
13753 ffn,
13754 conv1d: &w.conv1d,
13755 a_log: &w.a_log,
13756 dt_bias: &w.dt_bias,
13757 gnorm: &w.norm,
13758 };
13759 match plan.last_mut() {
13760 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
13761 _ => plan.push(MetalRowsItem::Gdn {
13762 run: vec![gl],
13763 first: li,
13764 }),
13765 }
13766 }
13767 AttnKind::Full {
13768 wq,
13769 wk,
13770 wv,
13771 wo,
13772 q_norm,
13773 k_norm,
13774 output_gate,
13775 softplus_gate: None,
13776 bias: None,
13777 } => {
13778 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
13779 wq.metal_graph_parts(),
13780 wk.metal_graph_parts(),
13781 wv.metal_graph_parts(),
13782 wo.metal_graph_parts(),
13783 ) else {
13784 return None;
13785 };
13786 if let QTensor::Mapped { model, .. } = wq {
13787 model_ref.get_or_insert_with(|| model.clone());
13788 }
13789 let cache = &self.kv_cache.layers[li];
13790 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
13791 return None;
13792 }
13793 plan.push(MetalRowsItem::Attn {
13794 l: AttnGpuLayer {
13795 attn_norm: &lw.input_norm,
13796 post_norm: &lw.post_norm,
13797 wq: pq,
13798 wk: pk,
13799 wv: pv,
13800 wo: po,
13801 ffn,
13802 attn_out_norm: None,
13803 ffn_out_norm: None,
13804 layer_scale: None,
13805 },
13806 li,
13807 q_norm: q_norm.as_deref(),
13808 k_norm: k_norm.as_deref(),
13809 output_gate: *output_gate,
13810 });
13811 }
13812 _ => return None,
13813 }
13814 }
13815 let model = model_ref?;
13816 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
13817 nv: cfg.num_v_heads,
13818 nk: cfg.num_k_heads,
13819 dk: cfg.key_head_dim,
13820 dv: cfg.value_head_dim,
13821 kk: cfg.conv_kernel,
13822 hidden: self.hidden_size,
13823 inter: self.intermediate_size,
13824 c_dim: cfg.conv_dim(),
13825 eps: cfg.rms_eps as f32,
13826 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13827 });
13828 Some((plan, model, gcfg))
13829 }
13830
13831 #[cfg(target_os = "macos")]
13833 #[allow(clippy::too_many_arguments)]
13834 fn metal_attn_params<'a>(
13835 li: usize,
13836 cache: &'a crate::kv_cache::LayerKvCache,
13837 q_norm: Option<&'a [f32]>,
13838 k_norm: Option<&'a [f32]>,
13839 output_gate: bool,
13840 inv_freq: &'a [f32],
13841 rope_scale: f32,
13842 geom: (usize, usize, usize, usize),
13843 pos0: usize,
13844 kv_id: u64,
13845 scale: f32,
13846 eps: f32,
13847 gemma: bool,
13848 late_qk_norm: bool,
13849 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
13850 let (nh, nkv, hd, rd) = geom;
13851 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13852 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13853 let cpu_stored = cpu_k[0].len() / hd;
13854 (
13855 crate::gpu_metal::AttnDeviceParams {
13856 kv_id,
13857 layer: li,
13858 nh,
13859 nkv,
13860 hd,
13861 rd,
13862 position: pos0,
13863 scale,
13864 eps,
13865 gemma,
13866 late_qk_norm,
13867 output_gate,
13868 q_norm,
13869 k_norm,
13870 inv_freq,
13871 rope_scale,
13872 cpu_k,
13873 cpu_v,
13874 cpu_stored,
13875 cpu_gen: cache.generation(),
13876 o1: None,
13877 window: None,
13878 softmax_sink: cache.sinks.as_deref(),
13879 head_gate: None,
13880 v_norm: false,
13881 },
13882 cpu_stored,
13883 )
13884 }
13885
13886 #[cfg(target_os = "macos")]
13891 #[allow(clippy::type_complexity)]
13892 fn metal_rows_run(
13893 &mut self,
13894 hiddens: &mut [f32],
13895 pos0: usize,
13896 b: usize,
13897 prefill: bool,
13898 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
13899 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
13903 ) -> MetalRowsRun {
13904 use crate::gpu_metal::{GraphDims, VerifyGraph};
13905 if !crate::gpu_metal::wait_replay() {
13911 tracing::error!("Metal rows graph: the pending async replay failed");
13912 return MetalRowsRun::Failed;
13913 }
13914 spec_stamp("v.wait");
13915 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
13921 if want > 0 {
13922 let phys = self.physical_layers.max(1);
13923 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
13924 let is_gdn = self
13925 .weights
13926 .layers
13927 .get(li % phys)
13928 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
13929 if is_gdn && l.linear_state.len() != want {
13930 l.linear_state = vec![0f32; want];
13931 }
13932 }
13933 }
13934 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
13935 return MetalRowsRun::Declined;
13936 };
13937 spec_stamp("v.plan");
13938 let dims = GraphDims {
13939 hidden: self.hidden_size,
13940 eps: self.rms_eps as f32,
13941 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13942 };
13943 let Some(mut graph) = (if prefill {
13944 VerifyGraph::new_prefill(&model, dims, hiddens, b)
13945 } else {
13946 VerifyGraph::new(&model, dims, hiddens, b)
13947 }) else {
13948 return MetalRowsRun::Declined;
13949 };
13950 let geom = (
13951 self.num_heads,
13952 self.num_kv_heads,
13953 self.head_dim,
13954 self.rotary_dim,
13955 );
13956 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13957 let eps = self.rms_eps as f32;
13958 let kv_id = self.graph_kv_id;
13959 let inv_freq = self.inv_freq.clone();
13960 for item in &plan {
13961 let ok = match item {
13962 MetalRowsItem::Gdn { run, .. } => gcfg
13963 .as_ref()
13964 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
13965 .unwrap_or(false),
13966 MetalRowsItem::Attn {
13967 l,
13968 li,
13969 q_norm,
13970 k_norm,
13971 output_gate,
13972 } => {
13973 let (p, _) = Self::metal_attn_params(
13974 *li,
13975 &self.kv_cache.layers[*li],
13976 *q_norm,
13977 *k_norm,
13978 *output_gate,
13979 &inv_freq,
13980 self.layer_rope_scale(*li),
13981 geom,
13982 pos0,
13983 kv_id,
13984 self.attn_scale,
13985 eps,
13986 gemma,
13987 self.qk_norm_after_rope,
13988 );
13989 graph.attn_ok(l, &p)
13990 }
13991 };
13992 if !ok {
13993 use std::sync::atomic::{AtomicBool, Ordering};
13994 static SAID: AtomicBool = AtomicBool::new(false);
13995 if !SAID.swap(true, Ordering::Relaxed) {
13996 tracing::warn!("metal rows graph: a layer failed preflight — declining");
13997 }
13998 return MetalRowsRun::Declined;
13999 }
14000 }
14001 let lm = match &spec {
14002 Some((lm, _, _)) => {
14003 if !graph.lm_head_ok(*lm) {
14004 return MetalRowsRun::Declined;
14005 }
14006 Some(*lm)
14007 }
14008 None => None,
14009 };
14010 let mut gdn_layers = Vec::new();
14011 let mut attn_layers = Vec::new();
14012 for item in &plan {
14013 match item {
14014 MetalRowsItem::Gdn { run, first } => {
14015 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
14016 .iter()
14017 .map(|l| l.linear_state.as_slice())
14018 .collect();
14019 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
14020 return MetalRowsRun::Declined;
14021 }
14022 gdn_layers.extend(*first..*first + run.len());
14023 }
14024 MetalRowsItem::Attn {
14025 l,
14026 li,
14027 q_norm,
14028 k_norm,
14029 output_gate,
14030 } => {
14031 let (p, cpu_stored) = Self::metal_attn_params(
14032 *li,
14033 &self.kv_cache.layers[*li],
14034 *q_norm,
14035 *k_norm,
14036 *output_gate,
14037 &inv_freq,
14038 self.layer_rope_scale(*li),
14039 geom,
14040 pos0,
14041 kv_id,
14042 self.attn_scale,
14043 eps,
14044 gemma,
14045 self.qk_norm_after_rope,
14046 );
14047 if !graph.encode_attn_b(l, &p) {
14048 return MetalRowsRun::Declined;
14049 }
14050 attn_layers.push((*li, cpu_stored));
14051 }
14052 }
14053 }
14054 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
14055 if !graph.encode_lm_head_b(final_norm, lm) {
14056 return MetalRowsRun::Declined;
14057 }
14058 if let Some((n, _)) = argmax_out.as_ref() {
14063 if !graph.encode_argmax_b(*n) {
14064 argmax_out = None;
14065 }
14066 }
14067 }
14068 spec_stamp("v.enc");
14069 if !graph.sync() {
14070 return MetalRowsRun::Failed;
14071 }
14072 spec_stamp("v.gpu");
14073 match (spec, argmax_out) {
14074 (Some(_), Some((_, ids))) => {
14075 ids.resize(b, 0);
14076 if !graph.read_argmax(ids) {
14077 return MetalRowsRun::Failed;
14078 }
14079 spec_stamp("v.am");
14080 }
14081 (Some((lm, _, logits)), None) => {
14082 logits.resize(b * lm.1, 0.0);
14083 if !graph.read_logits(logits) {
14084 return MetalRowsRun::Failed;
14085 }
14086 spec_stamp("v.lg");
14087 }
14088 (None, _) => {}
14089 }
14090 if !graph.read_hidden(hiddens) {
14091 return MetalRowsRun::Failed;
14092 }
14093 spec_stamp("v.hid");
14094 MetalRowsRun::Completed(MetalVerifyPending {
14095 graph,
14096 gdn_layers,
14097 attn_layers,
14098 })
14099 }
14100
14101 #[cfg(target_os = "macos")]
14107 fn try_batch_graph_metal(
14108 &mut self,
14109 hiddens: &mut [f32],
14110 positions: &[usize],
14111 b: usize,
14112 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14113 argmax_out: Option<(usize, &mut Vec<u32>)>,
14114 ) -> crate::gpu::BatchGraphOutcome {
14115 let _t0 = std::time::Instant::now();
14116 if positions.len() != b
14117 || positions.windows(2).any(|w| w[1] != w[0] + 1)
14118 || hiddens.len() != b * self.hidden_size
14119 {
14120 return crate::gpu::BatchGraphOutcome::Declined;
14121 }
14122 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
14123 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
14124 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
14125 MetalRowsRun::Completed(pending) => pending,
14126 };
14127 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
14128 eprintln!(
14129 "metal-verify: {:.1} ms | b={b}",
14130 _t0.elapsed().as_secs_f64() * 1e3
14131 );
14132 }
14133 self.metal_verify = Some(pending);
14134 crate::gpu::BatchGraphOutcome::Completed
14135 }
14136
14137 #[cfg(target_os = "macos")]
14142 fn prefill_rows_metal(
14143 &mut self,
14144 ids: &[u32],
14145 start_pos: usize,
14146 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
14147 ) -> MetalPrefillOutcome {
14148 let b = ids.len();
14149 if b == 0 || b > 512 {
14150 return MetalPrefillOutcome::Declined;
14151 }
14152 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14153 let with_head = spec.is_some();
14154 let hs = self.hidden_size;
14155 let mut hiddens = vec![0f32; b * hs];
14156 for (j, &id) in ids.iter().enumerate() {
14157 let e = self.embed_single(id);
14158 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
14159 }
14160 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
14161 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
14162 MetalRowsRun::Failed => {
14163 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14164 return MetalPrefillOutcome::Failed;
14165 }
14166 MetalRowsRun::Completed(pending) => pending,
14167 };
14168 let idxs = pending.gdn_layers.clone();
14170 let mut outs: Vec<&mut [f32]> = self
14171 .kv_cache
14172 .layers
14173 .iter_mut()
14174 .enumerate()
14175 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14176 .map(|(_, l)| l.linear_state.as_mut_slice())
14177 .collect();
14178 if !pending.graph.finish_states(&mut outs) {
14179 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14180 return MetalPrefillOutcome::Failed;
14181 }
14182 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14183 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14187 for (li, cpu_stored) in &pending.attn_layers {
14188 let mut kbuf = vec![0f32; b * nkv * hd];
14189 let mut vbuf = vec![0f32; b * nkv * hd];
14190 if !crate::gpu_metal::kv_mirror_read_rows(
14191 self.graph_kv_id,
14192 *li,
14193 nkv,
14194 hd,
14195 *cpu_stored,
14196 b,
14197 &mut kbuf,
14198 &mut vbuf,
14199 ) {
14200 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14201 return MetalPrefillOutcome::Failed;
14202 }
14203 rows.push((*li, *cpu_stored, kbuf, vbuf));
14204 }
14205 for (li, cpu_stored, kbuf, vbuf) in rows {
14206 let cache = &mut self.kv_cache.layers[li];
14207 for r in 0..b {
14208 cache.append(
14209 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14210 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14211 &[],
14212 );
14213 }
14214 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
14215 }
14216 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14217 if with_head {
14218 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
14219 }
14220 MetalPrefillOutcome::Completed(hiddens)
14221 }
14222
14223 #[cfg(target_os = "macos")]
14224 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
14225 self.prefill_rows_metal(ids, start_pos, None)
14226 }
14227
14228 #[cfg(target_os = "macos")]
14233 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
14234 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
14235 return MetalBatchNllOutcome::Declined;
14236 }
14237 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
14238 return MetalBatchNllOutcome::Declined;
14239 };
14240 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
14241 .ok()
14242 .and_then(|v| v.parse::<usize>().ok())
14243 .filter(|&v| (1..=512).contains(&v))
14244 .unwrap_or(32);
14245 let final_norm = self.weights.final_norm.clone();
14246 let mut nll = 0.0f64;
14247 let mut count = 0usize;
14248 let mut pos = 0usize;
14249 let mut completed = 0usize;
14250 while pos < ids.len() {
14251 let end = (pos + chunk).min(ids.len());
14252 let mut logits = Vec::new();
14253 let outcome =
14254 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
14255 match outcome {
14256 MetalPrefillOutcome::Declined => {
14257 return if completed == 0 {
14258 MetalBatchNllOutcome::Declined
14259 } else {
14260 MetalBatchNllOutcome::Failed(format!(
14261 "ordinary Metal NLL batch declined after {completed} chunks"
14262 ))
14263 };
14264 }
14265 MetalPrefillOutcome::Failed => {
14266 return MetalBatchNllOutcome::Failed(
14267 "ordinary Metal NLL batch failed after admission".to_string(),
14268 );
14269 }
14270 MetalPrefillOutcome::Completed(_) => {}
14271 }
14272 completed += 1;
14273 let vocab = self.vocab_size.min(lm.1);
14274 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
14275 return MetalBatchNllOutcome::Failed(
14276 "ordinary Metal NLL head returned an invalid shape".to_string(),
14277 );
14278 }
14279 for row in 0..(end - pos) {
14280 let absolute = pos + row;
14281 if absolute < start || absolute + 1 >= ids.len() {
14282 continue;
14283 }
14284 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
14285 if let Some(mu) = self.logit_multiplier {
14286 for v in lg.iter_mut() {
14287 *v *= mu;
14288 }
14289 }
14290 if let Some(c) = self.final_softcap {
14291 softcap_in_place(lg, c, self.pool.as_deref());
14292 }
14293 let target = ids[absolute + 1] as usize;
14294 if target >= vocab {
14295 return MetalBatchNllOutcome::Failed(format!(
14296 "target token {target} exceeds Metal head rows {vocab}"
14297 ));
14298 }
14299 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14300 let lse: f64 = lg
14301 .iter()
14302 .map(|&v| ((v - max) as f64).exp())
14303 .sum::<f64>()
14304 .ln()
14305 + max as f64;
14306 nll += lse - lg[target] as f64;
14307 count += 1;
14308 }
14309 pos = end;
14310 }
14311 MetalBatchNllOutcome::Completed(nll, count)
14312 }
14313
14314 #[cfg(target_os = "macos")]
14318 fn metal_verify_commit(&mut self, a: usize) -> bool {
14319 let Some(mut pending) = self.metal_verify.take() else {
14320 return false;
14321 };
14322 let n = a + 1;
14323 let idxs = pending.gdn_layers.clone();
14325 let mut outs: Vec<&mut [f32]> = self
14326 .kv_cache
14327 .layers
14328 .iter_mut()
14329 .enumerate()
14330 .filter(|(i, _)| idxs.binary_search(i).is_ok())
14331 .map(|(_, l)| l.linear_state.as_mut_slice())
14332 .collect();
14333 if !pending.graph.commit(n, &mut outs) {
14334 return false;
14335 }
14336 spec_stamp("c.replay");
14337 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14338 let mut rows = Vec::with_capacity(pending.attn_layers.len());
14342 for (li, cpu_stored) in &pending.attn_layers {
14343 let mut kbuf = vec![0f32; n * nkv * hd];
14344 let mut vbuf = vec![0f32; n * nkv * hd];
14345 if !crate::gpu_metal::kv_mirror_read_rows(
14346 self.graph_kv_id,
14347 *li,
14348 nkv,
14349 hd,
14350 *cpu_stored,
14351 n,
14352 &mut kbuf,
14353 &mut vbuf,
14354 ) {
14355 return false;
14356 }
14357 rows.push((*li, *cpu_stored, kbuf, vbuf));
14358 }
14359 for (li, cpu_stored, kbuf, vbuf) in rows {
14360 let cache = &mut self.kv_cache.layers[li];
14361 for r in 0..n {
14362 cache.append(
14363 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14364 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14365 &[],
14366 );
14367 }
14368 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
14369 }
14370 spec_stamp("c.kv");
14371 true
14372 }
14373
14374 #[cfg(target_os = "macos")]
14381 fn mtp_warm_batch_submit(
14382 &mut self,
14383 m: &mut MtpModule,
14384 pairs: &[(&[f32], u32)],
14385 first_pos: usize,
14386 ) -> Option<MetalWarmPending> {
14387 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
14388 let b = pairs.len();
14389 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
14390 return None;
14391 }
14392 let AttnKind::Full {
14393 wq,
14394 wk,
14395 wv,
14396 wo,
14397 q_norm,
14398 k_norm,
14399 output_gate,
14400 softplus_gate: None,
14401 bias: None,
14402 } = &m.layer.attn
14403 else {
14404 return None;
14405 };
14406 let FfnKind::Dense(d) = &m.layer.ffn else {
14407 return None;
14408 };
14409 if !d.segs.is_empty() {
14410 return None;
14411 }
14412 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14413 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14414 else {
14415 return None;
14416 };
14417 let (Some(g), Some(u), Some(dn)) = (
14418 d.gate_proj.q1_parts(),
14419 d.up_proj.q1_parts(),
14420 d.down_proj.q1_parts(),
14421 ) else {
14422 return None;
14423 };
14424 let Some(eh) = m.eh_proj.q1_parts() else {
14425 return None;
14426 };
14427 let QTensor::Mapped { model, .. } = wq else {
14428 return None;
14429 };
14430 let model = model.clone();
14431 let hs = self.hidden_size;
14432 let mut cat = vec![0f32; b * 2 * hs];
14434 for (j, (h, tok)) in pairs.iter().enumerate() {
14435 let e = self.embed_single(*tok);
14436 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
14437 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
14438 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
14439 }
14440 let dims = GraphDims {
14441 hidden: hs,
14442 eps: self.rms_eps as f32,
14443 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14444 };
14445 spec_stamp("w.cat");
14446 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
14447 return None;
14448 };
14449 spec_stamp("w.new");
14450 let l = AttnGpuLayer {
14451 attn_norm: &m.layer.input_norm,
14452 post_norm: &m.layer.post_norm,
14453 wq: pq,
14454 wk: pk,
14455 wv: pv,
14456 wo: po,
14457 ffn: MetalFfn::Dense {
14458 gate: g,
14459 up: u,
14460 down: dn,
14461 act: (d.act == Act::Gelu) as u8,
14462 },
14463 attn_out_norm: None,
14464 ffn_out_norm: None,
14465 layer_scale: None,
14466 };
14467 let (nh, nkv, hd, rd) = (
14468 self.num_heads,
14469 self.num_kv_heads,
14470 self.head_dim,
14471 self.rotary_dim,
14472 );
14473 let inv_freq = self.inv_freq.clone();
14474 let cpu_stored;
14475 {
14476 let cache = &m.kv;
14477 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14478 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14479 cpu_stored = cpu_k[0].len() / hd;
14480 if cpu_stored > first_pos {
14485 spec_stamp("w.decl");
14486 return None;
14487 }
14488 let p = AttnDeviceParams {
14489 kv_id: self.mtp_kv_id(),
14490 layer: Self::MTP_LAYER_BASE,
14491 nh,
14492 nkv,
14493 hd,
14494 rd,
14495 position: first_pos,
14496 scale: self.attn_scale,
14497 eps: self.rms_eps as f32,
14498 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14499 late_qk_norm: self.qk_norm_after_rope,
14500 output_gate: *output_gate,
14501 q_norm: q_norm.as_deref(),
14502 k_norm: k_norm.as_deref(),
14503 inv_freq: &inv_freq,
14504 rope_scale: self.rope_scale,
14505 cpu_k,
14506 cpu_v,
14507 cpu_stored,
14508 cpu_gen: cache.generation(),
14509 o1: None,
14510 window: None,
14511 softmax_sink: None,
14512 head_gate: None,
14513 v_norm: false,
14514 };
14515 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
14516 return None;
14517 }
14518 }
14519 spec_stamp("w.enc");
14520 if !graph.submit() {
14521 return None;
14522 }
14523 spec_stamp("w.sub");
14524 Some(MetalWarmPending {
14525 graph,
14526 cpu_stored,
14527 b,
14528 })
14529 }
14530
14531 #[cfg(target_os = "macos")]
14534 fn mtp_warm_batch_metal(
14535 &mut self,
14536 m: &mut MtpModule,
14537 pairs: &[(&[f32], u32)],
14538 first_pos: usize,
14539 ) -> bool {
14540 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
14541 Some(p) => self.mtp_warm_batch_finish(m, p),
14542 None => false,
14543 }
14544 }
14545
14546 #[cfg(target_os = "macos")]
14551 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
14552 let MetalWarmPending {
14553 mut graph,
14554 cpu_stored,
14555 b,
14556 } = pending;
14557 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
14558 if !graph.sync() {
14559 return false;
14560 }
14561 spec_stamp("w.gpu");
14562 let mut kbuf = vec![0f32; b * nkv * hd];
14563 let mut vbuf = vec![0f32; b * nkv * hd];
14564 if !crate::gpu_metal::kv_mirror_read_rows(
14565 self.mtp_kv_id(),
14566 Self::MTP_LAYER_BASE,
14567 nkv,
14568 hd,
14569 cpu_stored,
14570 b,
14571 &mut kbuf,
14572 &mut vbuf,
14573 ) {
14574 return false;
14575 }
14576 for r in 0..b {
14577 m.kv.append(
14578 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
14579 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
14580 &[],
14581 );
14582 }
14583 crate::gpu_metal::kv_mirror_set_stored(
14584 self.mtp_kv_id(),
14585 Self::MTP_LAYER_BASE,
14586 cpu_stored + b,
14587 );
14588 spec_stamp("w.kv");
14589 true
14590 }
14591
14592 pub(crate) fn note_draft_id(&mut self, id: u32) {
14599 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
14600 if (id as usize) >= cut {
14601 self.draft_full_streak = 16;
14602 } else {
14603 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
14604 }
14605 }
14606
14607 fn draft_head_rows(&self, head_rows: usize) -> usize {
14610 if self.draft_full_streak > 0 {
14611 head_rows
14612 } else {
14613 Self::draft_vocab_rows(head_rows)
14614 }
14615 }
14616
14617 fn draft_vocab_rows(head_rows: usize) -> usize {
14620 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
14621 let n = *N.get_or_init(|| {
14622 std::env::var("CMF_DRAFT_VOCAB")
14623 .ok()
14624 .and_then(|v| v.parse().ok())
14625 .unwrap_or(65536)
14626 });
14627 if n == 0 { head_rows } else { n.min(head_rows) }
14628 }
14629
14630 #[cfg(target_os = "macos")]
14635 fn mtp_step_metal(
14636 &mut self,
14637 m: &mut MtpModule,
14638 hidden: &[f32],
14639 next_token: u32,
14640 position: usize,
14641 want_logits: bool,
14642 ) -> Option<(Vec<f32>, Vec<f32>)> {
14643 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14644 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14645 || !crate::gpu::q1_force()
14646 || !crate::gpu::enabled_here()
14647 || self.attn_softcap > 0.0
14648 || self.attention_heads_per_layer.is_some()
14649 || m.kv.mode != crate::kv_cache::KvMode::F32
14650 || m.kv.o1.is_some()
14651 {
14652 return None;
14653 }
14654 let AttnKind::Full {
14655 wq,
14656 wk,
14657 wv,
14658 wo,
14659 q_norm,
14660 k_norm,
14661 output_gate,
14662 softplus_gate: None,
14663 bias: None,
14664 } = &m.layer.attn
14665 else {
14666 return None;
14667 };
14668 let FfnKind::Dense(d) = &m.layer.ffn else {
14669 return None;
14670 };
14671 if d.act != Act::Silu || !d.segs.is_empty() {
14672 return None;
14673 }
14674 let (pq, pk, pv, po) = (
14675 wq.q1_parts()?,
14676 wk.q1_parts()?,
14677 wv.q1_parts()?,
14678 wo.q1_parts()?,
14679 );
14680 let (g, u, dn) = (
14681 d.gate_proj.q1_parts()?,
14682 d.up_proj.q1_parts()?,
14683 d.down_proj.q1_parts()?,
14684 );
14685 let QTensor::Mapped { model, .. } = wq else {
14686 return None;
14687 };
14688 let model = model.clone();
14689 let lm = if want_logits {
14690 Some(self.weights.lm_head.q1_parts()?)
14691 } else {
14692 None
14693 };
14694 let dims = GraphDims {
14695 hidden: self.hidden_size,
14696 eps: self.rms_eps as f32,
14697 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14698 };
14699 let hs = self.hidden_size;
14702 let mut x = vec![0f32; hs];
14703 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14710 crate::gpu_metal::DENSE_MV,
14711 );
14712 let mut graph = TokenGraph::new(&model, dims, &x)?;
14713 let mut folded = false;
14714 if let Some(eh) = m.eh_proj.q1_parts() {
14715 let e = self.embed_single(next_token);
14716 let mut cat = vec![0.0f32; 2 * hs];
14717 let (cat_e, cat_h) = cat.split_at_mut(hs);
14718 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
14719 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
14720 folded = graph.encode_input_proj(eh, &cat);
14721 }
14722 if !folded {
14723 x = self.mtp_block_input(m, hidden, next_token);
14724 graph = TokenGraph::new(&model, dims, &x)?;
14725 }
14726 spec_stamp("d.in");
14727 let l = AttnGpuLayer {
14728 attn_norm: &m.layer.input_norm,
14729 post_norm: &m.layer.post_norm,
14730 wq: pq,
14731 wk: pk,
14732 wv: pv,
14733 wo: po,
14734 ffn: MetalFfn::Dense {
14735 gate: g,
14736 up: u,
14737 down: dn,
14738 act: (d.act == Act::Gelu) as u8,
14739 },
14740 attn_out_norm: None,
14741 ffn_out_norm: None,
14742 layer_scale: None,
14743 };
14744 let (nh, nkv, hd, rd) = (
14745 self.num_heads,
14746 self.num_kv_heads,
14747 self.head_dim,
14748 self.rotary_dim,
14749 );
14750 let inv_freq = self.inv_freq.clone();
14751 {
14752 let cache = &m.kv;
14753 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
14754 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
14755 let cpu_stored = cpu_k[0].len() / hd;
14756 let p = AttnDeviceParams {
14757 kv_id: self.mtp_kv_id(),
14758 layer: Self::MTP_LAYER_BASE,
14759 nh,
14760 nkv,
14761 hd,
14762 rd,
14763 position,
14764 scale: self.attn_scale,
14765 eps: self.rms_eps as f32,
14766 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14767 late_qk_norm: self.qk_norm_after_rope,
14768 output_gate: *output_gate,
14769 q_norm: q_norm.as_deref(),
14770 k_norm: k_norm.as_deref(),
14771 inv_freq: &inv_freq,
14772 rope_scale: self.rope_scale,
14773 cpu_k,
14774 cpu_v,
14775 cpu_stored,
14776 cpu_gen: cache.generation(),
14777 o1: None,
14778 window: None,
14779 softmax_sink: None,
14780 head_gate: None,
14781 v_norm: false,
14782 };
14783 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
14784 return None;
14785 }
14786 }
14787 let draft_rows = if let Some(lm) = lm {
14793 self.draft_head_rows(lm.1)
14794 } else {
14795 0
14796 };
14797 if let Some(lm) = lm {
14798 if !graph.lm_head_ok(lm) {
14799 return None;
14800 }
14801 if draft_rows < lm.1 {
14802 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
14803 return None;
14804 }
14805 } else {
14806 graph.encode_lm_head(&m.final_norm, lm);
14807 }
14808 }
14809 spec_stamp("d.enc");
14810 if graph.sync_checked().is_err() {
14811 return None;
14812 }
14813 spec_stamp("d.gpu");
14814 let mut logits = Vec::new();
14815 if let Some(lm) = lm {
14816 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
14817 logits = attention::take_buf(n_read);
14818 graph.read_logits(&mut logits);
14819 pad_mtp_draft_logits(&mut logits, self.vocab_size);
14821 }
14822 graph.finish(&mut x);
14823 let mut krow = attention::take_buf(nkv * hd);
14824 let mut vrow = attention::take_buf(nkv * hd);
14825 if crate::gpu_metal::kv_mirror_read_last(
14826 self.mtp_kv_id(),
14827 Self::MTP_LAYER_BASE,
14828 nkv,
14829 hd,
14830 &mut krow,
14831 &mut vrow,
14832 ) {
14833 m.kv.append(&krow, &vrow, &[]);
14834 }
14835 attention::recycle_buf(&mut krow);
14836 attention::recycle_buf(&mut vrow);
14837 spec_stamp("d.rd");
14838 Some((logits, x))
14839 }
14840
14841 fn mtp_chain_on() -> bool {
14854 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
14855 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
14856 }
14857
14858 #[cfg(target_os = "macos")]
14870 fn mtp_draft_chain_metal(
14871 &mut self,
14872 m: &mut MtpModule,
14873 hidden: &[f32],
14874 t_next: u32,
14875 position: usize,
14876 k: usize,
14877 ) -> Result<Vec<u32>, bool> {
14878 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
14879 if k == 0
14880 || k > 64
14881 || !Self::mtp_chain_on()
14882 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
14883 || !crate::gpu::q1_force()
14884 || !crate::gpu::enabled_here()
14885 || self.attn_softcap > 0.0
14886 || self.attention_heads_per_layer.is_some()
14887 || m.kv.mode != crate::kv_cache::KvMode::F32
14888 || m.kv.o1.is_some()
14889 || self.dsv4.is_some()
14891 || self.dsv41.is_some()
14892 || self.qwen4_exp.is_some()
14893 || self.per_token_stack()
14894 {
14895 return Err(false);
14896 }
14897 let AttnKind::Full {
14898 wq,
14899 wk,
14900 wv,
14901 wo,
14902 q_norm,
14903 k_norm,
14904 output_gate,
14905 softplus_gate: None,
14906 bias: None,
14907 } = &m.layer.attn
14908 else {
14909 return Err(false);
14910 };
14911 let FfnKind::Dense(d) = &m.layer.ffn else {
14912 return Err(false);
14913 };
14914 if d.act != Act::Silu || !d.segs.is_empty() {
14915 return Err(false);
14916 }
14917 let (Some(pq), Some(pk), Some(pv), Some(po)) =
14918 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
14919 else {
14920 return Err(false);
14921 };
14922 let (Some(g), Some(u), Some(dn)) = (
14923 d.gate_proj.q1_parts(),
14924 d.up_proj.q1_parts(),
14925 d.down_proj.q1_parts(),
14926 ) else {
14927 return Err(false);
14928 };
14929 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
14930 return Err(false);
14931 };
14932 let QTensor::Mapped { model, .. } = wq else {
14933 return Err(false);
14934 };
14935 let model = model.clone();
14936 let QTensor::Mapped {
14939 model: em,
14940 idx: eidx,
14941 dtype: cortiq_core::TensorDtype::Q4TiledP,
14942 ..
14943 } = &self.weights.embed_tokens
14944 else {
14945 return Err(false);
14946 };
14947 if !std::sync::Arc::ptr_eq(em, &model)
14948 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
14949 {
14950 return Err(false);
14951 }
14952 let embed = (
14953 *eidx,
14954 self.weights.embed_tokens.rows(),
14955 self.weights.embed_tokens.cols(),
14956 );
14957 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
14958 return Err(false);
14959 }
14960 let dims = GraphDims {
14961 hidden: self.hidden_size,
14962 eps: self.rms_eps as f32,
14963 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
14964 };
14965 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
14969 crate::gpu_metal::DENSE_MV,
14970 );
14971 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
14972 return Err(false);
14973 };
14974 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
14975 return Err(false);
14976 }
14977 let l = AttnGpuLayer {
14978 attn_norm: &m.layer.input_norm,
14979 post_norm: &m.layer.post_norm,
14980 wq: pq,
14981 wk: pk,
14982 wv: pv,
14983 wo: po,
14984 ffn: MetalFfn::Dense {
14985 gate: g,
14986 up: u,
14987 down: dn,
14988 act: (d.act == Act::Gelu) as u8,
14989 },
14990 attn_out_norm: None,
14991 ffn_out_norm: None,
14992 layer_scale: None,
14993 };
14994 let (nh, nkv, hd, rd) = (
14995 self.num_heads,
14996 self.num_kv_heads,
14997 self.head_dim,
14998 self.rotary_dim,
14999 );
15000 let inv_freq = self.inv_freq.clone();
15001 let draft_rows = self.draft_head_rows(lm.1);
15002 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
15003 if n_arg == 0 {
15004 return Err(false);
15005 }
15006 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
15013 let t_chain = std::time::Instant::now();
15014 graph.chain_ids_init(t_next, k);
15015 let cpu_stored;
15016 {
15017 let cache = &m.kv;
15018 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
15019 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
15020 cpu_stored = cpu_k[0].len() / hd;
15021 for j in 0..k {
15022 if !graph.encode_chain_input(
15023 embed,
15024 j as u32,
15025 &m.enorm,
15026 &m.hnorm,
15027 self.embed_multiplier,
15028 eh,
15029 ) {
15030 return Err(false);
15031 }
15032 let p = AttnDeviceParams {
15036 kv_id: self.mtp_kv_id(),
15037 layer: Self::MTP_LAYER_BASE,
15038 nh,
15039 nkv,
15040 hd,
15041 rd,
15042 position: position + j,
15043 scale: self.attn_scale,
15044 eps: self.rms_eps as f32,
15045 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
15046 late_qk_norm: self.qk_norm_after_rope,
15047 output_gate: *output_gate,
15048 q_norm: q_norm.as_deref(),
15049 k_norm: k_norm.as_deref(),
15050 inv_freq: &inv_freq,
15051 rope_scale: self.rope_scale,
15052 cpu_k: cpu_k.clone(),
15053 cpu_v: cpu_v.clone(),
15054 cpu_stored: cpu_stored + j,
15055 cpu_gen: cache.generation(),
15056 o1: None,
15057 window: None,
15058 softmax_sink: None,
15059 head_gate: None,
15060 v_norm: false,
15061 };
15062 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
15063 return Err(false);
15064 }
15065 if draft_rows < lm.1 {
15066 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
15067 return Err(false);
15068 }
15069 } else {
15070 graph.encode_lm_head(&m.final_norm, lm);
15071 }
15072 if !graph.encode_argmax(n_arg, j as u32 + 1) {
15073 return Err(false);
15074 }
15075 if split {
15076 graph.commit();
15079 }
15080 }
15081 }
15082 let t_enc = t_chain.elapsed();
15083 if graph.sync_checked().is_err() {
15084 return Err(true);
15085 }
15086 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
15087 eprintln!(
15088 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
15089 t_enc.as_secs_f64() * 1e3,
15090 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
15091 if split { ", split" } else { "" }
15092 );
15093 }
15094 let mut ids = vec![0u32; k];
15095 if !graph.chain_ids_read(&mut ids) {
15096 return Err(true);
15097 }
15098 let mut kbuf = vec![0f32; k * nkv * hd];
15099 let mut vbuf = vec![0f32; k * nkv * hd];
15100 if !crate::gpu_metal::kv_mirror_read_rows(
15101 self.mtp_kv_id(),
15102 Self::MTP_LAYER_BASE,
15103 nkv,
15104 hd,
15105 cpu_stored,
15106 k,
15107 &mut kbuf,
15108 &mut vbuf,
15109 ) {
15110 return Err(true);
15111 }
15112 for r in 0..k {
15113 m.kv.append(
15114 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
15115 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
15116 &[],
15117 );
15118 }
15119 Ok(ids)
15120 }
15121
15122 fn try_batch_graph_wgpu(
15123 &self,
15124 hiddens: &mut [f32],
15125 positions: &[usize],
15126 k: usize,
15127 spec: Option<crate::gpu::SpecTail<'_>>,
15128 ) -> crate::gpu::BatchGraphOutcome {
15129 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
15130 }
15131
15132 fn try_batch_graph_wgpu_prefix(
15137 &self,
15138 hiddens: &mut [f32],
15139 positions: &[usize],
15140 k: usize,
15141 spec: Option<crate::gpu::SpecTail<'_>>,
15142 layers_run: Option<&mut usize>,
15143 ) -> crate::gpu::BatchGraphOutcome {
15144 let graph_end = match self.mimo_moe.graph_prefix_end() {
15145 Some(end) if end < self.num_layers => {
15146 if layers_run.is_none() || spec.is_some() || end == 0 {
15147 return crate::gpu::BatchGraphOutcome::Declined;
15148 }
15149 end
15150 }
15151 _ => self.num_layers,
15152 };
15153 let _tb = std::time::Instant::now();
15154 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
15155 if self.attn_softcap > 0.0 {
15156 return crate::gpu::BatchGraphOutcome::Declined; }
15158 if let Some(reason) = self
15161 .wgpu_graph_attn_decline()
15162 .or_else(|| self.batch_graph_gemma_decline())
15163 {
15164 self.note_graph_decline("wgpu batch graph", reason);
15165 return crate::gpu::BatchGraphOutcome::Declined;
15166 }
15167 let nh = self.num_heads;
15168 let (nkv, hd, rd) = self.layer_geom(0);
15169 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15170 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
15171 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
15172 {
15173 let name = &m.tensors[i].name;
15174 let prism = if crate::prism::is_inverse_embedding(m, name) {
15175 crate::gpu::GraphPrismOp::InverseEmbedding
15176 } else if crate::prism::is_forward_weight(m, name) {
15177 crate::gpu::GraphPrismOp::Forward
15178 } else {
15179 crate::gpu::GraphPrismOp::None
15180 };
15181 return Some(crate::gpu::GraphW {
15182 idx: i,
15183 kind,
15184 row_scale: rs,
15185 data: &[],
15186 prism,
15187 affine: crate::prism::is_affine_target(m, name),
15188 });
15189 }
15190 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
15191 eprintln!(
15192 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
15193 t.rows(),
15194 t.cols()
15195 );
15196 }
15197 t.as_f32().map(|d| crate::gpu::GraphW {
15198 idx: 0,
15199 kind: 4,
15200 row_scale: &[],
15201 data: d,
15202 prism: crate::gpu::GraphPrismOp::None,
15203 affine: false,
15204 })
15205 }
15206 let built: Option<(
15207 Vec<crate::gpu::GraphLayer<'_>>,
15208 std::sync::Arc<cortiq_core::CmfModel>,
15209 )> = (|| {
15210 let mut layers = Vec::with_capacity(graph_end);
15211 let mut model = None;
15212 for li in 0..graph_end {
15213 let lw = &self.weights.layers[self.phys_layer(li)];
15214 let gffn = match &lw.ffn {
15221 FfnKind::Dense(d) if !d.segs.is_empty() => {
15222 if batch_debug {
15223 eprintln!("batch graph: dense segmented FFN at layer {li}");
15224 }
15225 return None;
15226 }
15227 FfnKind::Dense(d) => {
15228 let Some(act) = d.act.wgpu_graph_act() else {
15229 if batch_debug {
15230 eprintln!(
15231 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
15232 d.act
15233 );
15234 }
15235 return None;
15236 };
15237 crate::gpu::GraphFfn::Dense {
15238 gate: gw(&d.gate_proj)?,
15239 up: gw(&d.up_proj)?,
15240 down: gw(&d.down_proj)?,
15241 act,
15242 }
15243 }
15244 FfnKind::Moe(m) => {
15245 if m.route_tau.is_some() || m.mask.is_some() {
15252 return None;
15253 }
15254 let shared = m.shared.as_ref();
15258 let has_shared = shared.is_some();
15259 let shared_gated = matches!(shared, Some((_, Some(_))));
15260 let sgate = match shared {
15261 Some((_, Some(sg))) => gw(sg)?,
15262 _ => gw(&m.router)?,
15266 };
15267 let router = gw(&m.router)?;
15268 if router.prism != crate::gpu::GraphPrismOp::None
15274 || router.affine
15275 || sgate.prism != crate::gpu::GraphPrismOp::None
15276 || sgate.affine
15277 {
15278 return None;
15279 }
15280 let inter = m.experts.first()?.gate_proj.rows();
15281 let mut experts = Vec::with_capacity(m.experts.len() + 1);
15282 let mut q4tp: Option<bool> = None;
15283 let mut gu_q2: Option<bool> = None;
15284 let mut q82: Option<bool> = None;
15285 let mut moe_act = None;
15289 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
15290 let Some(e_act) = e.act.wgpu_graph_act() else {
15291 return None;
15292 };
15293 match moe_act {
15294 Some(existing) if existing != e_act => return None,
15295 None => moe_act = Some(e_act),
15296 _ => {}
15297 }
15298 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
15299 return None;
15300 }
15301 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
15305 {
15306 Some((mm, gi)) => (
15307 mm,
15308 gi,
15309 e.up_proj.mapped_q4t()?.1,
15310 e.down_proj.mapped_q4t()?.1,
15311 false,
15312 false,
15313 false,
15314 ),
15315 None => match e.gate_proj.mapped_q2tp() {
15316 Some((mm, gi)) => (
15317 mm,
15318 gi,
15319 e.up_proj.mapped_q2tp()?.1,
15320 e.down_proj.mapped_q4tp()?.1,
15321 true,
15322 true,
15323 false,
15324 ),
15325 None => match e.gate_proj.mapped_q4tp() {
15326 Some((mm, gi)) => (
15327 mm,
15328 gi,
15329 e.up_proj.mapped_q4tp()?.1,
15330 e.down_proj.mapped_q4tp()?.1,
15331 true,
15332 false,
15333 false,
15334 ),
15335 None => {
15336 if !crate::gpu::moe_q82_graph_on() {
15337 return None;
15338 }
15339 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
15340 (
15341 mm,
15342 gi,
15343 e.up_proj.mapped_q8_2f()?.1,
15344 e.down_proj.mapped_q8_2f()?.1,
15345 false,
15346 false,
15347 true,
15348 )
15349 }
15350 },
15351 },
15352 };
15353 if *q4tp.get_or_insert(is_p) != is_p
15354 || *gu_q2.get_or_insert(is_q2) != is_q2
15355 || *q82.get_or_insert(is_8) != is_8
15356 {
15357 return None;
15358 }
15359 if [gi, ui, di].into_iter().any(|idx| {
15360 mm.tensors.get(idx).is_some_and(|t| {
15361 crate::prism::is_forward_weight(mm, &t.name)
15362 || crate::prism::is_affine_target(mm, &t.name)
15363 })
15364 }) {
15365 return None;
15366 }
15367 model.get_or_insert_with(|| mm.clone());
15368 experts.push((gi, ui, di));
15369 }
15370 crate::gpu::GraphFfn::Moe {
15371 router,
15372 shared_gate: sgate,
15373 experts,
15374 n_exp: m.experts.len(),
15375 top_k: m.top_k,
15376 inter,
15377 act: moe_act?,
15378 norm_topk: m.norm_topk_prob,
15379 q4tp: q4tp?,
15380 gu_q2: gu_q2.unwrap_or(false),
15381 q82: q82.unwrap_or(false),
15382 sigmoid: m.router_sigmoid,
15383 sigmoid_denom_eps: m.sigmoid_denom_eps,
15384 bias: m.expert_bias.as_deref(),
15385 has_shared,
15386 detached_shared: None,
15387 shared_gated,
15388 route_scale: m.routed_scaling,
15389 }
15390 }
15391 _ => return None,
15392 };
15393 let attn = match &lw.attn {
15394 AttnKind::Full {
15395 wq,
15396 wk,
15397 wv,
15398 wo,
15399 q_norm,
15400 k_norm,
15401 output_gate,
15402 softplus_gate,
15403 bias,
15404 } => {
15405 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
15406 if batch_debug {
15407 eprintln!(
15408 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
15409 softplus_gate.is_some(),
15410 self.attention_heads_per_layer.is_some()
15411 );
15412 }
15413 return None;
15414 }
15415 let (m, _, _, _) =
15416 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
15417 model = Some(m.clone());
15418 crate::gpu::GraphAttn::Full {
15419 wq: gw(wq)?,
15420 wk: gw(wk)?,
15421 wv: gw(wv)?,
15422 wo: gw(wo)?,
15423 q_norm: q_norm.as_deref(),
15424 k_norm: k_norm.as_deref(),
15425 late_qk_norm: self.qk_norm_after_rope,
15426 bias: bias
15427 .as_ref()
15428 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15429 output_gate: *output_gate,
15430 cpu_k: self.kv_cache.layers[li].k_heads(),
15431 cpu_v: self.kv_cache.layers[li].v_heads(),
15432 cpu_base: self.kv_cache.layers[li].base(),
15433 geom: self.graph_attn_geom(li),
15434 head_gate: None,
15437 }
15438 }
15439 AttnKind::LinearGdn(w) => {
15440 let Some(cfg) = self.gdn_cfg else {
15441 if batch_debug {
15442 eprintln!("batch graph: no GDN config at layer {li}");
15443 }
15444 return None;
15445 };
15446 let (m, _, _, _) = w
15447 .in_proj_qkv
15448 .graph_weight()
15449 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
15450 model = Some(m.clone());
15451 crate::gpu::GraphAttn::Gdn {
15452 qkv: gw(&w.in_proj_qkv)?,
15453 z: gw(&w.in_proj_z)?,
15454 a: gw(&w.in_proj_a)?,
15455 b: gw(&w.in_proj_b)?,
15456 out: gw(&w.out_proj)?,
15457 conv1d: &w.conv1d,
15458 a_log: &w.a_log,
15459 dt_bias: &w.dt_bias,
15460 norm: &w.norm,
15461 nv: cfg.num_v_heads,
15462 nk: cfg.num_k_heads,
15463 dk: cfg.key_head_dim,
15464 dv: cfg.value_head_dim,
15465 kk: cfg.conv_kernel,
15466 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
15467 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
15468 }
15469 }
15470 _ => return None,
15471 };
15472 let g4 = self.global_attn.is_some() || self.attn_v_norm;
15475 layers.push(crate::gpu::GraphLayer {
15476 input_norm: &lw.input_norm,
15477 attn,
15478 post_norm: &lw.post_norm,
15479 attn_out_norm: if g4 {
15480 lw.attn_out_norm.as_deref()
15481 } else {
15482 None
15483 },
15484 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
15485 layer_scale: if g4 { lw.layer_scale } else { None },
15486 ffn: gffn,
15487 });
15488 }
15489 Some((layers, model?))
15490 })();
15491 let Some((layers, model)) = built else {
15492 {
15493 use std::sync::atomic::{AtomicBool, Ordering};
15494 static SAID: AtomicBool = AtomicBool::new(false);
15495 if !SAID.swap(true, Ordering::Relaxed) {
15496 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
15497 }
15498 }
15499 return crate::gpu::BatchGraphOutcome::Declined;
15500 };
15501 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
15502 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
15503 }
15504 crate::gpu::forward_batch_graph(
15505 &model,
15506 self.graph_kv_id,
15507 &layers,
15508 &self.inv_freq,
15509 hiddens,
15510 nh,
15511 nkv,
15512 hd,
15513 rd,
15514 self.hidden_size,
15515 self.intermediate_size,
15516 positions,
15517 self.kv_cache.max_seq_len,
15518 gemma,
15519 self.rms_eps as f32,
15520 self.attn_scale,
15521 k,
15522 &(0..graph_end)
15523 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
15524 .collect::<Vec<_>>(),
15525 self.o1_epoch,
15526 spec,
15527 layers_run,
15528 )
15529 }
15530
15531 fn draft_probe() -> bool {
15535 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15536 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
15537 }
15538
15539 #[cfg(feature = "gpu")]
15551 fn dsv4_spec_on() -> bool {
15552 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
15553 *ON.get_or_init(|| {
15554 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
15558 return v != "0";
15559 }
15560 std::env::var("CMF_DSV4_SPEC")
15567 .map(|v| v != "0")
15568 .unwrap_or_else(|_| {
15569 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
15570 })
15571 })
15572 }
15573
15574 #[cfg(feature = "gpu")]
15581 fn dsv4_spec_step(
15582 &mut self,
15583 tip_token: u32,
15584 t_next: u32,
15585 next_pos: usize,
15586 max_extra: usize,
15587 drafted: &mut usize,
15588 accepted_ctr: &mut usize,
15589 ) -> Option<(Vec<u32>, usize)> {
15590 let t_all = std::time::Instant::now();
15591 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15592 thread_local! {
15593 static LAST: std::cell::Cell<Option<std::time::Instant>> =
15594 const { std::cell::Cell::new(None) };
15595 }
15596 LAST.with(|l| {
15597 if let Some(prev) = l.get() {
15598 eprintln!(
15599 "between rounds {:.1} ms",
15600 prev.elapsed().as_secs_f64() * 1e3
15601 );
15602 }
15603 l.set(Some(std::time::Instant::now()));
15604 });
15605 }
15606 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15607 eprintln!("spec_step: enter pos={next_pos}");
15608 }
15609 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
15610 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
15611 if self.dspark.is_none() {
15613 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15614 if t.is_empty() {
15615 return None;
15616 }
15617 crate::dsv4::dspark_arm(&t, cfg.dim);
15618 self.dspark = Some(crate::dsv4::DsparkState::new(
15619 self.dsv4_mtp.len(),
15620 &cfg,
15621 t.len(),
15622 ));
15623 }
15624 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15625 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
15626 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
15627 eprintln!("spec_step: the pack was not built (targets {targets:?})");
15628 }
15629 let pack = pack?;
15630 let block = crate::dsv4::dspark_block();
15631 let b_box = self.dsv4.as_mut()?;
15632 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
15633 let ds = self.dspark.as_mut()?;
15634 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
15637 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
15638 if dbg {
15639 eprintln!("spec_step: no capture");
15640 }
15641 return None;
15642 }
15643 ds.have_hidden = true;
15644 let tip_pos = next_pos.checked_sub(1)?;
15645 let draft_started = std::time::Instant::now();
15646 let mut conf = Vec::new();
15647 let props = crate::dsv4::dspark_draft_gpu(
15648 g,
15649 &self.dsv4_mtp,
15650 &cfg,
15651 ds,
15652 pack,
15653 st.kv_id,
15654 tip_token,
15655 tip_pos,
15656 self.pool.as_deref(),
15657 &mut conf,
15658 );
15659 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15660 *drafted += block;
15661 if props.is_empty() || props[0] != t_next {
15662 if dbg {
15663 eprintln!(
15664 "spec_step: draft {} (props0={:?} t_next={t_next})",
15665 if props.is_empty() {
15666 "empty"
15667 } else {
15668 "miss"
15669 },
15670 props.first()
15671 );
15672 }
15673 return None;
15674 }
15675 let mut k_verify = crate::dsv4::dspark_verify_k()
15682 .min(props.len())
15683 .min(max_extra.saturating_add(1));
15684 let conf_min = {
15690 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15691 *M.get_or_init(|| {
15692 std::env::var("CMF_DSPARK_CONF_MIN")
15693 .ok()
15694 .and_then(|v| v.parse().ok())
15695 .unwrap_or(0.0)
15696 })
15697 };
15698 if conf_min > 0.0 && conf.len() >= props.len() {
15699 let mut keep = 1usize;
15700 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
15701 keep += 1;
15702 }
15703 k_verify = k_verify.min(keep.max(2));
15704 }
15705 if k_verify < 2 {
15706 return None;
15707 }
15708 let mut fed = Vec::with_capacity(k_verify);
15709 fed.push(t_next);
15710 fed.extend_from_slice(&props[1..k_verify]);
15711 let mut argmax = Vec::new();
15712 let mut logits_all = Vec::new();
15713 let mut walked = Vec::new();
15714 let txn = crate::dsv4::dsv4_verify_chunk(
15715 g,
15716 layers,
15717 &cfg,
15718 st,
15719 &fed,
15720 next_pos,
15721 &self.inv_freq,
15722 self.pool.as_deref(),
15723 &targets,
15724 &mut argmax,
15725 &mut logits_all,
15726 &mut walked,
15727 );
15728 if txn.is_none() && dbg {
15729 eprintln!("spec_step: verify declined");
15730 }
15731 let txn = txn?;
15732 let spec_gpu_end = txn.gpu_end;
15733 let b = fed.len();
15734 let mut accepted = 1usize;
15735 while accepted < b && fed[accepted] == argmax[accepted - 1] {
15736 accepted += 1;
15737 }
15738 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
15743 accepted = 1;
15744 }
15745 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
15746 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
15747 }
15748 let t_fin = std::time::Instant::now();
15749 if !crate::dsv4::dsv4_spec_finish(
15750 g,
15751 layers,
15752 &cfg,
15753 st,
15754 txn,
15755 accepted,
15756 &fed,
15757 &self.inv_freq,
15758 self.pool.as_deref(),
15759 ) {
15760 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
15761 return None;
15762 }
15763 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15764 eprintln!(
15765 "finish(k={accepted}): {:.1} ms",
15766 t_fin.elapsed().as_secs_f64() * 1e3
15767 );
15768 }
15769 *accepted_ctr += accepted - 1;
15770 let (hc, dim) = (cfg.hc_mult, cfg.dim);
15775 let dev_caps: Vec<usize> = targets
15780 .iter()
15781 .copied()
15782 .filter(|&t| t < spec_gpu_end)
15783 .collect();
15784 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
15785 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
15786 return None;
15787 }
15788 for t in 0..accepted {
15789 let tip = t + 1 == accepted;
15790 for (slot, &tl) in targets.iter().enumerate() {
15791 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
15792 let lo = (di * b + t) * hc * dim;
15793 crate::dsv4::dspark_capture(
15794 &caps_all[lo..lo + hc * dim],
15795 &cfg,
15796 slot,
15797 &mut ds.main_hidden,
15798 );
15799 } else if tip
15800 && crate::dsv4::dspark_peek_slot(slot, dim, {
15801 let lo = slot * dim;
15802 &mut ds.main_hidden[lo..lo + dim]
15803 })
15804 {
15805 } else {
15810 crate::dsv4::dspark_capture(
15814 &walked[t * hc * dim..(t + 1) * hc * dim],
15815 &cfg,
15816 slot,
15817 &mut ds.main_hidden,
15818 );
15819 }
15820 }
15821 crate::dsv4::dspark_ring_append(
15822 g,
15823 &self.dsv4_mtp,
15824 &cfg,
15825 ds,
15826 next_pos + t,
15827 self.pool.as_deref(),
15828 );
15829 }
15830 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
15831 self.graph_logits = Some(row);
15832 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
15837 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
15838 crate::dsv4::pick_tally_arm();
15839 }
15840 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
15841 eprintln!(
15842 "spec_step total {:.1} ms (k={accepted})",
15843 t_all.elapsed().as_secs_f64() * 1e3
15844 );
15845 }
15846 Some((fed[1..accepted].to_vec(), next_pos + accepted))
15847 }
15848
15849 fn dspark_probe(&mut self, position: usize, token_id: u32) {
15850 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
15851 return;
15852 }
15853 let trunk_now = crate::dsv4::pick_tally_take();
15855 crate::dsv4::trunk_freq_note(&trunk_now);
15856 if !trunk_now.is_empty() {
15857 self.dspark_trunk_picks.push(trunk_now);
15858 let keep = crate::dsv4::dspark_block();
15859 if self.dspark_trunk_picks.len() > keep {
15860 self.dspark_trunk_picks.remove(0);
15861 }
15862 }
15863 for p in std::mem::take(&mut self.dspark_pending) {
15866 let Some(i) = position.checked_sub(p.0 + 1) else {
15867 continue;
15868 };
15869 let mut p = p;
15870 if i < p.1.len() {
15871 if p.2 && p.1[i] == token_id {
15872 p.3 = i + 1;
15873 } else {
15874 p.2 = false;
15875 }
15876 if i + 1 < p.1.len() {
15877 self.dspark_pending.push(p);
15878 continue;
15879 }
15880 }
15881 self.dspark_hist.push(p.3);
15882 self.dspark_real.push(token_id);
15883 }
15884 let Some(b) = &mut self.dsv4 else { return };
15885 let (g, layers, cfg) = (&b.0, &b.1, b.2);
15886 let n_layers = layers.len();
15887 if self.dspark.is_none() {
15888 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
15889 if t.is_empty() {
15890 return;
15891 }
15892 eprintln!(
15893 "DSpark: capture from layers {t:?}, block {}",
15894 crate::dsv4::dspark_block()
15895 );
15896 crate::dsv4::dspark_arm(&t, cfg.dim);
15897 self.dspark = Some(crate::dsv4::DsparkState::new(
15898 self.dsv4_mtp.len(),
15899 &cfg,
15900 t.len(),
15901 ));
15902 }
15903 let ds = self.dspark.as_mut().unwrap();
15904 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
15905 return; }
15907 let mut conf = Vec::new();
15908 crate::dsv4::pick_tally_arm();
15909 let draft_started = std::time::Instant::now();
15914 #[cfg(feature = "gpu")]
15915 let gpu_draft = crate::dsv4::dspark_gpu_on();
15916 #[cfg(not(feature = "gpu"))]
15917 let gpu_draft = false;
15918 let props = if gpu_draft {
15919 #[cfg(feature = "gpu")]
15920 {
15921 let kv_id = b.3.kv_id;
15922 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
15923 Some(pk) => crate::dsv4::dspark_draft_gpu(
15924 g,
15925 &self.dsv4_mtp,
15926 &cfg,
15927 ds,
15928 pk,
15929 kv_id,
15930 token_id,
15931 position,
15932 self.pool.as_deref(),
15933 &mut conf,
15934 ),
15935 None => Vec::new(),
15936 }
15937 }
15938 #[cfg(not(feature = "gpu"))]
15939 Vec::new()
15940 } else {
15941 crate::gpu::cpu_scope(|| {
15942 crate::dsv4::dspark_draft(
15943 g,
15944 &self.dsv4_mtp,
15945 &cfg,
15946 ds,
15947 token_id,
15948 position,
15949 self.pool.as_deref(),
15950 &mut conf,
15951 )
15952 })
15953 };
15954 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
15955 let draft_picks = crate::dsv4::pick_tally_take();
15956 crate::dsv4::dspark_freq_note(&draft_picks);
15957 crate::dsv4::pick_tally_arm();
15960 if !props.is_empty() {
15961 let (tu, tt) = {
15965 let flat: Vec<(usize, Vec<usize>)> = self
15966 .dspark_trunk_picks
15967 .iter()
15968 .flat_map(|v| v.iter().cloned())
15969 .collect();
15970 let mut per: std::collections::HashMap<usize, Vec<usize>> =
15972 std::collections::HashMap::new();
15973 for (li, picks) in flat {
15974 per.entry(li).or_default().extend(picks);
15975 }
15976 let n = per.len().max(1);
15977 let mut u = 0usize;
15978 let mut t = 0usize;
15979 for (_, v) in per {
15980 t += v.len();
15981 u += v.iter().collect::<std::collections::HashSet<_>>().len();
15982 }
15983 (u / n, t / n)
15984 };
15985 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
15986 self.dspark_exp.push((tu, tt, du, dt));
15987 self.dspark_pending.push((position, props, true, 0));
15988 }
15989 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
15990 let n = self.dspark_hist.len() as f32;
15991 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
15992 let block = crate::dsv4::dspark_block();
15993 let mut at = vec![0usize; block + 1];
15994 for &k in &self.dspark_hist {
15995 at[k] += 1;
15996 }
15997 let mut surv = Vec::with_capacity(block);
15999 for i in 1..=block {
16000 let k = at[i..].iter().sum::<usize>() as f32 / n;
16001 surv.push(format!("{k:.2}"));
16002 }
16003 let distinct = self
16004 .dspark_real
16005 .iter()
16006 .collect::<std::collections::HashSet<_>>()
16007 .len();
16008 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
16009 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
16010 });
16011 let m = self.dspark_exp.len().max(1);
16012 eprintln!(
16013 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
16014 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
16015 self.dspark_hist.len(),
16016 mean + 1.0,
16017 surv.join(" ")
16018 );
16019 eprintln!(
16020 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
16021 trunk experts {}/{} per layer over {block} tokens, \
16022 draft {}/{} per block, draft {:.2} ms/block",
16023 self.dspark_real.len(),
16024 tu / m,
16025 tt / m,
16026 du / m,
16027 dt / m,
16028 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
16029 );
16030 }
16031 }
16032
16033 fn forward_layers_upto(
16034 &mut self,
16035 hidden: &[f32],
16036 position: usize,
16037 task_mask: Option<&TaskMask>,
16038 upto: Option<usize>,
16039 ) -> Vec<f32> {
16040 if let Some(plan) = self.gpu_plan.clone() {
16046 if upto.is_none() && plan.len() > 1 {
16047 let mut h = hidden.to_vec();
16048 for &(dev, from, upto_incl) in plan.iter() {
16049 h = crate::gpu::with_device(dev, || {
16050 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
16051 });
16052 }
16053 return h;
16054 }
16055 }
16056 self.forward_layers_span(hidden, position, task_mask, 0, upto)
16057 }
16058
16059 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
16064 self.set_gpu_plan_at(devices, None)
16065 }
16066
16067 pub fn set_gpu_plan_at(
16071 &mut self,
16072 devices: Option<&[usize]>,
16073 at: Option<usize>,
16074 ) -> Result<(), String> {
16075 let Some(devs) = devices.filter(|d| d.len() > 1) else {
16076 self.gpu_plan = None;
16077 return Ok(());
16078 };
16079 self.split_supported()?;
16080 let n = self.num_layers;
16081 if devs.len() > n {
16082 return Err(format!("{} devices for {n} layers", devs.len()));
16083 }
16084 if let Some(k) = at {
16085 if k == 0 || k >= n {
16086 return Err(format!("split at {k}: the model has {n} layers"));
16087 }
16088 if devs.len() == 2 {
16089 self.gpu_plan = Some(std::sync::Arc::new(vec![
16090 (devs[0], 0, k - 1),
16091 (devs[1], k, n - 1),
16092 ]));
16093 return Ok(());
16094 }
16095 return Err(format!(
16096 "an explicit split point takes exactly 2 devices, got {}",
16097 devs.len()
16098 ));
16099 }
16100 let per = n.div_ceil(devs.len());
16101 let mut plan = Vec::with_capacity(devs.len());
16102 let mut from = 0usize;
16103 for &d in devs {
16104 if from >= n {
16105 break;
16106 }
16107 let upto = (from + per - 1).min(n - 1);
16108 plan.push((d, from, upto));
16109 from = upto + 1;
16110 }
16111 self.gpu_plan = Some(std::sync::Arc::new(plan));
16112 Ok(())
16113 }
16114
16115 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
16117 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
16118 }
16119
16120 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
16126 if let Some(x) = t.as_f32() {
16127 return x.to_vec();
16128 }
16129 let mut out = vec![0.0; t.rows() * t.cols()];
16130 for r in 0..t.rows() {
16131 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
16132 }
16133 out
16134 }
16135
16136 fn embryo_resident_eligible(&self) -> bool {
16137 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
16140 || self.num_layers != self.physical_layers
16141 || self.loop_final_norm
16142 || self.weights.layers.len() != self.num_layers
16143 || self.head_clusters.is_none()
16144 || self.final_softcap.is_some()
16145 || self.logit_multiplier.is_some()
16146 || self.attn_softcap != 0.0
16147 || self.mtp.is_some()
16148 || self.per_token_stack()
16149 || self.dsv4.is_some()
16150 || self.dsv41.is_some()
16151 || self.qwen4_exp.is_some()
16152 || self.dyn_router.is_some()
16159 || self.dyn_phi_layer.is_some()
16160 || self.dyn_blend_loaded
16161 || self.o1_cfg.is_some()
16162 || self.swa.is_some()
16163 || self.sliding_layers.is_some()
16164 || self.global_attn.is_some()
16165 || self.attention_heads_per_layer.is_some()
16166 || self.attn_v_norm
16167 || self
16168 .kv_cache
16169 .layers
16170 .iter()
16171 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
16172 || self.rope_scale != 1.0
16173 || self.rope_scale_local != 1.0
16174 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
16175 || self.hidden_size == 0
16176 || self.hidden_size > 1024
16177 || self.intermediate_size > 1024
16178 || self.num_heads == 0
16179 || self.num_kv_heads == 0
16180 || self.num_heads % self.num_kv_heads != 0
16181 || self.num_heads.saturating_mul(self.head_dim) > 1024
16182 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
16183 || self.vocab_size == 0
16184 || self.kv_cache.max_seq_len == 0
16185 || self.rotary_dim == 0
16186 || self.rotary_dim > self.head_dim
16187 || self.rotary_dim % 2 != 0
16188 || self.inv_freq.len() < self.rotary_dim / 2
16189 {
16190 return false;
16191 }
16192 if self.weights.lm_head.as_f32().is_none()
16203 || self.weights.embed_tokens.as_f32().is_none()
16204 || self.weights.lm_head.rows() < self.vocab_size
16205 || self.weights.lm_head.cols() != self.hidden_size
16206 || self.weights.embed_tokens.rows() < self.vocab_size
16207 || self.weights.embed_tokens.cols() != self.hidden_size
16208 || self.weights.final_norm.len() != self.hidden_size
16209 {
16210 return false;
16211 }
16212 if let Some(cfg) = self.vmf_cfg {
16213 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
16214 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
16215 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
16216 || cfg.state_len() == 0
16217 {
16218 return false;
16219 }
16220 }
16221 if let Some(g) = self.gdn_cfg {
16222 if g.num_v_heads == 0
16227 || g.num_k_heads == 0
16228 || g.num_v_heads % g.num_k_heads != 0
16229 || g.key_head_dim == 0
16230 || g.key_head_dim > 128
16231 || g.value_head_dim == 0
16232 || g.value_head_dim > 256
16233 || g.value_head_dim % 4 != 0
16234 || g.conv_kernel == 0
16235 || g.num_v_heads > 512
16236 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
16237 || g.conv_dim() > 2048
16238 || g.conv_dim() % 4 != 0
16239 || g.hidden_size != self.hidden_size
16240 || g.output_gate_sigmoid
16241 || g.rms_eps != self.rms_eps
16242 || g.state_len() == 0
16243 {
16244 return false;
16245 }
16246 }
16247 let mut full_seen = false;
16248 for lw in &self.weights.layers {
16249 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16250 return false;
16251 }
16252 match &lw.attn {
16253 AttnKind::LinearGdn(w) => {
16254 let Some(g) = self.gdn_cfg else {
16255 return false;
16256 };
16257 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
16258 if w.in_proj_qkv.rows() != g.conv_dim()
16259 || w.in_proj_qkv.cols() != self.hidden_size
16260 || w.in_proj_qkv.as_f32().is_none()
16261 || w.in_proj_z.rows() != nv * dv
16262 || w.in_proj_z.cols() != self.hidden_size
16263 || w.in_proj_z.as_f32().is_none()
16264 || w.in_proj_a.rows() != nv
16265 || w.in_proj_a.cols() != self.hidden_size
16266 || w.in_proj_a.as_f32().is_none()
16267 || w.in_proj_b.rows() != nv
16268 || w.in_proj_b.cols() != self.hidden_size
16269 || w.in_proj_b.as_f32().is_none()
16270 || w.conv1d.len() != g.conv_dim() * kk
16271 || w.a_log.len() != nv
16272 || w.dt_bias.len() != nv
16273 || w.norm.len() != dv
16274 || w.out_proj.rows() != self.hidden_size
16275 || w.out_proj.cols() != nv * dv
16276 || w.out_proj.as_f32().is_none()
16277 {
16278 return false;
16279 }
16280 }
16281 AttnKind::Linear(w) => {
16282 let Some(cfg) = self.vmf_cfg else {
16283 return false;
16284 };
16285 if w.thq.rows() != cfg.num_heads * cfg.nphase
16286 || w.thq.cols() != self.hidden_size
16287 || w.thq.as_f32().is_none()
16288 || w.thk.rows() != cfg.num_heads * cfg.nphase
16289 || w.thk.cols() != self.hidden_size
16290 || w.thk.as_f32().is_none()
16291 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
16292 || w.v_proj.cols() != self.hidden_size
16293 || w.v_proj.as_f32().is_none()
16294 || w.out_proj.rows() != self.hidden_size
16295 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
16296 || w.out_proj.as_f32().is_none()
16297 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
16298 {
16299 return false;
16300 }
16301 if let Some((kg, kb)) = &w.k_gate {
16302 if kg.rows() != cfg.num_heads
16303 || kg.cols() != self.hidden_size
16304 || kg.as_f32().is_none()
16305 || kb.len() != cfg.num_heads
16306 {
16307 return false;
16308 }
16309 }
16310 if let Some(conv) = &w.conv {
16311 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
16312 return false;
16313 }
16314 }
16315 }
16316 AttnKind::Full {
16317 wq,
16318 wk,
16319 wv,
16320 wo,
16321 q_norm,
16322 k_norm,
16323 output_gate,
16324 softplus_gate,
16325 bias,
16326 } => {
16327 if full_seen
16328 || q_norm.is_some()
16329 || k_norm.is_some()
16330 || *output_gate
16331 || softplus_gate.is_some()
16332 || bias.is_some()
16333 || wq.as_f32().is_none()
16334 || wk.as_f32().is_none()
16335 || wv.as_f32().is_none()
16336 || wo.as_f32().is_none()
16337 || wq.rows() != self.num_heads * self.head_dim
16338 || wk.rows() != self.num_kv_heads * self.head_dim
16339 || wv.rows() != self.num_kv_heads * self.head_dim
16340 || wq.cols() != self.hidden_size
16341 || wk.cols() != self.hidden_size
16342 || wv.cols() != self.hidden_size
16343 || wo.rows() != self.hidden_size
16344 || wo.cols() != self.num_heads * self.head_dim
16345 {
16346 return false;
16347 }
16348 full_seen = true;
16349 }
16350 AttnKind::Bounded(w) => {
16351 let Some(ac) = self.anchor_core.as_ref() else {
16354 return false;
16355 };
16356 if self.bounded_rope.is_none()
16357 || w.window != ac.window
16358 || w.sink != ac.sink
16359 || w.window == 0
16360 || w.window + w.sink > 256
16361 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
16362 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
16363 || w.wq.as_f32().is_none()
16364 || w.wk.as_f32().is_none()
16365 || w.wv.as_f32().is_none()
16366 || w.wo.as_f32().is_none()
16367 || w.wq.rows() != self.num_heads * self.head_dim
16368 || w.wk.rows() != self.num_kv_heads * self.head_dim
16369 || w.wv.rows() != self.num_kv_heads * self.head_dim
16370 || w.wq.cols() != self.hidden_size
16371 || w.wk.cols() != self.hidden_size
16372 || w.wv.cols() != self.hidden_size
16373 || w.wo.rows() != self.hidden_size
16374 || w.wo.cols() != self.num_heads * self.head_dim
16375 {
16376 return false;
16377 }
16378 }
16379 _ => return false,
16380 }
16381 match &lw.ffn {
16382 FfnKind::Dense(d) => {
16383 if d.act != Act::Silu
16384 || !d.segs.is_empty()
16385 || d.gate_proj.as_f32().is_none()
16386 || d.up_proj.as_f32().is_none()
16387 || d.down_proj.as_f32().is_none()
16388 || d.gate_proj.rows() != self.intermediate_size
16389 || d.gate_proj.cols() != self.hidden_size
16390 || d.up_proj.rows() != self.intermediate_size
16391 || d.up_proj.cols() != self.hidden_size
16392 || d.down_proj.rows() != self.hidden_size
16393 || d.down_proj.cols() != self.intermediate_size
16394 {
16395 return false;
16396 }
16397 }
16398 FfnKind::Moe(m) => {
16399 if m.resonance.is_none()
16400 || m.top_k != 1
16401 || m.router_sigmoid
16402 || !m.norm_topk_prob
16403 || m.expert_bias.is_some()
16404 || m.routed_scaling != 1.0
16405 || m.route_tau.is_some()
16406 || m.shared.is_none()
16407 || m.mask.is_some()
16408 || m.per_expert_scale.is_some()
16409 || m.router_input_norm
16410 || m.experts.is_empty()
16411 || m.experts.len() > 8
16412 {
16413 return false;
16414 }
16415 let r = m.resonance.as_ref().unwrap();
16416 if r.mu.len() != m.experts.len() * self.hidden_size
16417 || r.bias.len() != m.experts.len()
16418 || r.u.len() != m.experts.len() * r.k * self.hidden_size
16419 || r.k > 128
16420 {
16421 return false;
16422 }
16423 let Some((shared, gate)) = &m.shared else {
16424 return false;
16425 };
16426 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
16427 return false;
16428 }
16429 if shared.gate_proj.as_f32().is_none()
16430 || shared.up_proj.as_f32().is_none()
16431 || shared.down_proj.as_f32().is_none()
16432 || shared.gate_proj.rows() != self.intermediate_size
16433 || shared.gate_proj.cols() != self.hidden_size
16434 || shared.up_proj.rows() != self.intermediate_size
16435 || shared.up_proj.cols() != self.hidden_size
16436 || shared.down_proj.rows() != self.hidden_size
16437 || shared.down_proj.cols() != self.intermediate_size
16438 {
16439 return false;
16440 }
16441 for e in &m.experts {
16442 if e.act != Act::Silu
16443 || !e.segs.is_empty()
16444 || e.gate_proj.as_f32().is_none()
16445 || e.up_proj.as_f32().is_none()
16446 || e.down_proj.as_f32().is_none()
16447 || e.gate_proj.rows() != self.intermediate_size
16448 || e.gate_proj.cols() != self.hidden_size
16449 || e.up_proj.rows() != self.intermediate_size
16450 || e.up_proj.cols() != self.hidden_size
16451 || e.down_proj.rows() != self.hidden_size
16452 || e.down_proj.cols() != self.intermediate_size
16453 {
16454 return false;
16455 }
16456 }
16457 }
16458 FfnKind::DenseMoe(_) => return false,
16459 }
16460 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
16461 return false;
16462 }
16463 }
16464 if full_seen && self.anchor_core.is_some() {
16465 return false;
16466 }
16467 full_seen || self.num_layers > 0
16468 }
16469
16470 fn embryo_resident_wanted(&self) -> bool {
16475 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16476 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
16477 && matches!(
16478 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
16479 Ok("1") | Ok("parallel")
16480 )
16481 && crate::gpu::enabled_here()
16482 && !self.graph_refused()
16483 && self.embryo_resident_eligible()
16484 }
16485
16486 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
16494 if ids.len() < 2
16495 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
16496 || !self.embryo_resident_wanted()
16497 {
16498 return None;
16499 }
16500 let model = self.ensure_embryo_graph()?;
16501 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
16502 .ok()
16503 .and_then(|v| v.parse::<usize>().ok())
16504 .filter(|&v| v >= 1)
16505 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
16506 .min(crate::gpu::EMBRYO_CHUNK_MAX);
16507 let hs = self.hidden_size;
16508 let n = ids.len();
16509 let mut pos = start;
16510 let mut last = None;
16511 let mut rows = Vec::with_capacity(cmax * hs);
16512 while pos < n {
16513 let end = (pos + cmax).min(n);
16514 rows.clear();
16515 for &id in &ids[pos..end] {
16516 rows.extend_from_slice(&self.embed_single(id));
16517 }
16518 let mut lg = Vec::new();
16519 if !crate::gpu::forward_embryo_graph_chunk(
16520 &model,
16521 self.graph_kv_id,
16522 &rows,
16523 pos,
16524 end - pos,
16525 &mut lg,
16526 ) {
16527 if pos == start {
16528 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16529 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
16530 }
16531 return None;
16532 }
16533 self.kv_cache.clear();
16537 self.clear_history();
16538 crate::gpu::graph_kv_reset(self.graph_kv_id);
16539 panic!(
16540 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
16541 );
16542 }
16543 last = Some(lg);
16544 pos = end;
16545 }
16546 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
16547 eprintln!(
16548 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
16549 n - start,
16550 (n - start).div_ceil(cmax)
16551 );
16552 }
16553 last
16554 }
16555
16556 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
16557 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
16558 const UMAX: u32 = u32::MAX;
16559 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
16560 const REC: usize = 64;
16561 struct Pack {
16562 data: Vec<f32>,
16563 }
16564 impl Pack {
16565 fn put(&mut self, x: &[f32]) -> u32 {
16566 if x.is_empty() {
16567 return u32::MAX;
16568 }
16569 let off = self.data.len();
16570 self.data.extend_from_slice(x);
16571 off as u32
16572 }
16573 }
16574 let vmf = self.vmf_cfg;
16578 let gdn = self.gdn_cfg;
16579 let mut pack = Pack { data: Vec::new() };
16580 let mut meta = vec![0u32; HEADER];
16581 meta[0] = self.hidden_size as u32;
16582 meta[1] = self.intermediate_size as u32;
16583 meta[2] = self.vocab_size as u32;
16584 meta[3] = self.num_layers as u32;
16585 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
16586 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
16587 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
16588 if let Some(g) = gdn {
16589 meta[24] = g.num_v_heads as u32;
16590 meta[25] = g.num_k_heads as u32;
16591 meta[26] = g.key_head_dim as u32;
16592 meta[27] = g.value_head_dim as u32;
16593 meta[28] = g.conv_kernel as u32;
16594 meta[29] = g.conv_dim() as u32;
16595 }
16596 meta[7] = self.num_heads as u32;
16597 meta[8] = self.num_kv_heads as u32;
16598 meta[9] = self.head_dim as u32;
16599 meta[10] = self.kv_cache.max_seq_len as u32;
16600 let clusters = self.head_clusters.as_ref().unwrap();
16601 let cluster_count = clusters.len() / self.hidden_size;
16602 if clusters.len() % self.hidden_size != 0
16603 || cluster_count == 0
16604 || cluster_count > 1024
16605 || self.vocab_size % cluster_count != 0
16606 || self.weights.lm_head.rows() < self.vocab_size
16607 || self.weights.final_norm.len() != self.hidden_size
16608 {
16609 return None;
16610 }
16611 meta[11] = cluster_count as u32;
16612 meta[12] = (self.vocab_size / cluster_count) as u32;
16613 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
16614 meta[16] = self.rotary_dim as u32;
16615 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
16616 meta[19] = (self.rms_eps as f32).to_bits();
16617 let max_conv = self
16618 .weights
16619 .layers
16620 .iter()
16621 .filter_map(|lw| match &lw.attn {
16622 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
16623 _ => None,
16624 })
16625 .max()
16626 .unwrap_or(1);
16627 let phase_stride = vmf
16633 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
16634 .unwrap_or(0);
16635 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
16636 let state_stride = phase_stride.max(gdn_stride);
16637 let bounded = self.anchor_core.clone();
16641 let (anchor_window, anchor_sink) = bounded
16642 .as_ref()
16643 .map(|ac| (ac.window, ac.sink))
16644 .unwrap_or((0, 0));
16645 let kv_stride = if bounded.is_some() {
16646 2usize
16647 .saturating_mul(self.num_kv_heads)
16648 .saturating_mul(anchor_window)
16649 .saturating_mul(self.head_dim)
16650 } else {
16651 2usize
16652 .saturating_mul(self.num_kv_heads)
16653 .saturating_mul(self.kv_cache.max_seq_len)
16654 .saturating_mul(self.head_dim)
16655 };
16656 meta[14] = state_stride as u32;
16657 meta[15] = kv_stride as u32;
16658 meta[18] = anchor_window as u32;
16659 meta[20] = anchor_sink as u32;
16660 meta[21] = match &self.bounded_rope {
16661 Some(rope) => {
16662 let off = pack.put(&rope.cos);
16664 let _ = pack.put(&rope.sin);
16665 off
16666 }
16667 None => UMAX,
16668 };
16669 let mut full_seen = false;
16670 let mut bounded_seen = 0usize;
16671 let mut phase_seen = 0usize;
16675 let mut gdn_seen = 0usize;
16676 for (li, lw) in self.weights.layers.iter().enumerate() {
16677 let base = meta.len();
16678 meta.resize(base + REC, UMAX);
16679 meta[base] = match &lw.attn {
16680 AttnKind::Linear(w) if w.phase_delta => 1,
16681 AttnKind::Linear(_) => 0,
16682 AttnKind::Full { .. } => 2,
16683 AttnKind::Bounded(_) => 3,
16684 AttnKind::LinearGdn(_) => 4,
16685 _ => UMAX,
16686 };
16687 meta[base + 1] = pack.put(&lw.input_norm);
16688 meta[base + 2] = pack.put(&lw.post_norm);
16689 meta[base + 25] = match &lw.attn {
16690 AttnKind::Linear(_) => {
16691 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16692 phase_seen += 1;
16693 off
16694 }
16695 AttnKind::LinearGdn(_) => {
16696 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
16697 gdn_seen += 1;
16698 off
16699 }
16700 _ => UMAX,
16701 };
16702 match &lw.attn {
16703 AttnKind::LinearGdn(w) => {
16704 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
16707 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
16708 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
16709 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
16710 meta[base + 60] = pack.put(&w.conv1d);
16711 meta[base + 61] = pack.put(&w.a_log);
16712 meta[base + 62] = pack.put(&w.dt_bias);
16713 meta[base + 63] = pack.put(&w.norm);
16714 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16715 meta[base + 24] = 0;
16716 }
16717 AttnKind::Linear(w) => {
16718 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
16719 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
16720 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
16721 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
16722 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
16723 meta[base + 7] = pack.put(&decay);
16724 if let Some((kg, kb)) = &w.k_gate {
16725 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
16726 meta[base + 9] = pack.put(kb);
16727 }
16728 if let Some(conv) = &w.conv {
16729 meta[base + 10] = pack.put(conv);
16730 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
16731 } else {
16732 meta[base + 24] = 0;
16733 }
16734 }
16735 AttnKind::Full { wq, wk, wv, wo, .. } => {
16736 full_seen = true;
16737 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
16738 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
16739 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
16740 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
16741 meta[base + 26] = (li * kv_stride) as u32;
16742 }
16743 AttnKind::Bounded(w) => {
16744 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
16745 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
16746 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
16747 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
16748 meta[base + 26] = (bounded_seen * kv_stride) as u32;
16750 meta[base + 27] = pack.put(&w.sink_k);
16751 meta[base + 28] = pack.put(&w.sink_v);
16752 bounded_seen += 1;
16753 }
16754 _ => return None,
16755 }
16756 match &lw.ffn {
16757 FfnKind::Dense(d) => {
16758 meta[base + 15] = 0;
16759 meta[base + 16] = 0;
16760 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
16761 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
16762 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
16763 }
16764 FfnKind::Moe(m) => {
16765 let r = m.resonance.as_ref().unwrap();
16766 let (shared, _) = m.shared.as_ref().unwrap();
16767 meta[base + 15] = 1;
16768 meta[base + 16] = m.experts.len() as u32;
16769 meta[base + 17] = pack.put(&r.mu);
16770 meta[base + 18] = pack.put(&r.u);
16771 meta[base + 19] = pack.put(&r.bias);
16772 meta[base + 20] = r.k as u32;
16773 let mut shell = r.effective_shell(m.experts.len());
16782 shell.push(f32::NEG_INFINITY);
16783 meta[base + 30] = pack.put(&shell);
16784 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
16785 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
16786 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
16787 for (e, ex) in m.experts.iter().enumerate() {
16788 meta[base + 32 + e * 3] =
16789 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
16790 meta[base + 33 + e * 3] =
16791 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
16792 meta[base + 34 + e * 3] =
16793 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
16794 }
16795 }
16796 FfnKind::DenseMoe(_) => return None,
16797 }
16798 }
16799 if !full_seen && self.num_layers == 0 {
16800 return None;
16801 }
16802 let id = {
16803 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
16804 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
16805 };
16806 let model = crate::gpu::EmbryoGraphModel {
16807 id,
16808 hidden: self.hidden_size,
16809 intermediate: self.intermediate_size,
16810 vocab: self.vocab_size,
16811 layers: self.num_layers,
16812 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
16813 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
16814 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
16815 anchor_q_heads: self.num_heads,
16816 anchor_kv_heads: self.num_kv_heads,
16817 anchor_head_dim: self.head_dim,
16818 rotary_dim: self.rotary_dim,
16819 max_seq: self.kv_cache.max_seq_len,
16820 cluster_count,
16821 cluster_size: self.vocab_size / cluster_count,
16822 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
16823 state_stride,
16824 kv_stride,
16825 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
16826 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
16827 weights: pack.data,
16828 meta,
16829 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
16830 clusters: clusters.as_ref().clone(),
16831 final_norm: self.weights.final_norm.clone(),
16832 inv_freq: self.inv_freq.as_ref().clone(),
16833 bounded: bounded.is_some(),
16834 kv_layers: if bounded.is_some() {
16835 bounded_seen
16836 } else {
16837 self.num_layers
16838 },
16839 state_layers: phase_seen + gdn_seen,
16840 anchor_window,
16841 anchor_sink,
16842 phase_layers: phase_seen,
16843 gdn_layers: gdn_seen,
16844 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
16845 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
16846 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
16847 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
16848 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
16849 };
16850 self.embryo_graph = Some(std::sync::Arc::new(model));
16851 }
16852 self.embryo_graph.clone()
16853 }
16854
16855 fn forward_layers_span(
16856 &mut self,
16857 hidden: &[f32],
16858 position: usize,
16859 task_mask: Option<&TaskMask>,
16860 from: usize,
16861 upto: Option<usize>,
16862 ) -> Vec<f32> {
16863 #[cfg(target_os = "macos")]
16864 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
16865 position,
16866 from,
16867 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
16868 );
16869 debug_assert!(
16870 from == 0
16871 || (self.dsv4.is_none()
16872 && self.dsv41.is_none()
16873 && self.qwen4_exp.is_none()
16874 && !self.per_token_stack())
16875 );
16876 #[cfg(target_os = "macos")]
16882 if !crate::gpu_metal::wait_replay() {
16883 self.fail_metal_graph("the pending async replay failed before a plain forward");
16884 return vec![0.0; self.hidden_size];
16885 }
16886 if let Some(b) = &mut self.qwen4_exp {
16887 let _ = (task_mask, upto);
16888 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16889 let mut logits = Vec::new();
16890 crate::qwen4_exp::forward_token(
16891 &b.0,
16892 &b.1,
16893 &b.2,
16894 &mut b.3,
16895 token_id,
16896 position,
16897 &self.inv_freq,
16898 self.pool.as_deref(),
16899 &mut logits,
16900 true,
16901 );
16902 self.graph_logits = Some(logits);
16903 return vec![0.0; self.hidden_size];
16904 }
16905 if let Some(b) = &mut self.dsv4 {
16911 let _ = (task_mask, upto);
16912 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16913 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
16914 st.pos = position;
16915 let mut logits = Vec::new();
16916 crate::dsv4::forward_token(
16917 g,
16918 layers,
16919 &cfg,
16920 st,
16921 token_id,
16922 &self.inv_freq,
16923 self.pool.as_deref(),
16924 &mut logits,
16925 );
16926 self.graph_logits = Some(logits);
16927 self.dspark_probe(position, token_id);
16928 return vec![0.0; self.hidden_size];
16931 }
16932 if let Some(b) = &mut self.dsv41 {
16934 let _ = (task_mask, upto);
16935 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
16936 let mut logits = Vec::new();
16937 crate::dsv41::forward_token(
16938 &b.0,
16939 &b.1,
16940 &b.2,
16941 &mut b.3,
16942 token_id,
16943 position,
16944 self.pool.as_deref(),
16945 &mut logits,
16946 );
16947 self.graph_logits = Some(logits);
16948 return vec![0.0; self.hidden_size];
16949 }
16950 if let Some(b) = &self.g4e {
16956 let _ = (task_mask, upto);
16957 return crate::g4e::g4e_forward(
16958 &b.0,
16959 &b.1,
16960 hidden,
16961 position,
16962 &mut self.kv_cache.layers,
16963 self.num_heads,
16964 self.num_kv_heads,
16965 self.pool.as_deref(),
16966 );
16967 }
16968 if let Some(b) = &self.g3n {
16969 let _ = (task_mask, upto);
16970 return crate::g3n::g3n_forward(
16971 &b.0,
16972 &b.1,
16973 hidden,
16974 position,
16975 &mut self.kv_cache.layers,
16976 self.num_heads,
16977 self.num_kv_heads,
16978 self.head_dim,
16979 self.pool.as_deref(),
16980 );
16981 }
16982 if from == 0
16989 && upto.is_none()
16990 && task_mask.is_none()
16991 && self.anchor_core.is_some()
16992 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
16993 {
16994 static ONCE: std::sync::Once = std::sync::Once::new();
16995 ONCE.call_once(|| {
16996 eprintln!(
16997 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
16998 unsupported={} eligible={}",
16999 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
17000 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
17001 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
17002 crate::gpu::enabled_here(),
17003 self.graph_refused(),
17004 self.embryo_resident_eligible(),
17005 );
17006 });
17007 }
17008 if from == 0
17009 && upto.is_none()
17010 && task_mask.is_none()
17011 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17015 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
17016 && matches!(
17020 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
17021 Ok("1") | Ok("parallel")
17022 )
17023 && crate::gpu::enabled_here()
17024 && !self.graph_refused()
17025 && (position == 0 || self.device_sequence_position().is_some())
17031 && self.embryo_resident_eligible()
17032 && let Some(model) = self.ensure_embryo_graph()
17033 {
17034 let mut lg = Vec::new();
17035 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
17036 {
17037 self.graph_logits = Some(lg);
17038 return vec![0.0; self.hidden_size];
17039 }
17040 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
17041 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
17042 }
17043 self.mark_graph_refused();
17049 if position != 0 {
17050 self.kv_cache.clear();
17054 self.clear_history();
17055 crate::gpu::graph_kv_reset(self.graph_kv_id);
17056 panic!(
17057 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
17058 );
17059 }
17060 }
17061 let mut h = hidden.to_vec();
17062 self.mimo_moe_prepare();
17065 let _mimo_q8 = self
17066 .mimo_moe
17067 .is_on()
17068 .then(crate::qtensor::enter_full_gpu_q8_scope);
17069 let (nh, _nkv, _hd, hs, _rd, eps) = (
17072 self.num_heads,
17073 self.num_kv_heads,
17074 self.head_dim,
17075 self.hidden_size,
17076 self.rotary_dim,
17077 self.rms_eps,
17078 );
17079 let pool = self.pool.clone();
17080 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
17092 let graph_on = match graph_env.as_deref() {
17093 Some("0") => false,
17094 Some("prefill") => false, Some(_) => true,
17096 None => crate::gpu::wgpu_graph_default(),
17102 };
17103 let graph_trusted =
17104 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
17105 let race_eligible =
17106 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
17107 let mut tail_start = 0usize;
17108 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
17109 let t_graph = std::time::Instant::now();
17110 let mut lg = Vec::new();
17111 let mut gl = 0usize;
17112 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
17113 let declined = built.is_none();
17114 let built = match built {
17115 Some(Ok(hh)) => Some(hh),
17116 Some(Err(())) => {
17117 self.clear_sequence_state();
17121 self.graph_failed
17122 .store(true, std::sync::atomic::Ordering::Relaxed);
17123 self.cancel
17124 .store(true, std::sync::atomic::Ordering::Relaxed);
17125 tracing::error!("token graph failed after admission; sequence state cleared");
17126 return vec![0.0; self.hidden_size];
17127 }
17128 None => None,
17129 };
17130 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
17135 self.mark_graph_refused();
17136 }
17137 graph_note(built.is_some(), gl, self.num_layers);
17138 if let Some(hh) = built {
17139 let dur = t_graph.elapsed();
17140 if std::env::var("CMF_GRAPH_PROF").is_ok() {
17141 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
17142 }
17143 if gl > 0 && gl < self.num_layers {
17144 h = hh;
17150 tail_start = gl;
17151 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
17152 if !graph_trusted {
17153 crate::gpu::graph_race_record(true, dur);
17154 }
17155 if !lg.is_empty() {
17156 lg.resize(self.vocab_size, 0.0);
17159 if let Some(c) = self.final_softcap {
17160 softcap_in_place(&mut lg, c, self.pool.as_deref());
17161 }
17162 self.graph_logits = Some(lg);
17163 }
17164 return hh;
17165 }
17166 }
17172 }
17173 let span = from > 0 || upto.is_some();
17197 if span && graph_on && task_mask.is_none() && graph_trusted {
17198 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
17199 let mut lg = Vec::new();
17200 let mut gl = 0usize;
17201 let span_res =
17202 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
17203 let span_res = match span_res {
17204 Some(Ok(hh)) => Some(hh),
17205 Some(Err(())) => {
17206 self.clear_sequence_state();
17207 self.graph_failed
17208 .store(true, std::sync::atomic::Ordering::Relaxed);
17209 self.cancel
17210 .store(true, std::sync::atomic::Ordering::Relaxed);
17211 tracing::error!(
17212 "span token graph failed after admission; sequence state cleared"
17213 );
17214 return vec![0.0; self.hidden_size];
17215 }
17216 None => None,
17217 };
17218 graph_note(span_res.is_some(), gl, upto_excl - from);
17219 if std::env::var("CMF_GPU_DEBUG").is_ok() {
17220 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
17224 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
17225 eprintln!(
17226 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
17227 upto_excl - from,
17228 span_res.is_some()
17229 );
17230 }
17231 }
17232 if let Some(hh) = span_res {
17233 if gl == upto_excl - from {
17234 if !lg.is_empty() {
17235 lg.resize(self.vocab_size, 0.0);
17236 if let Some(c) = self.final_softcap {
17237 softcap_in_place(&mut lg, c, self.pool.as_deref());
17238 }
17239 self.graph_logits = Some(lg);
17240 }
17241 crate::gpu::set_layer(-1);
17242 return hh;
17243 }
17244 h = hh;
17246 tail_start = from + gl;
17247 }
17248 }
17249 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
17254
17255 let host_tail = tail_start > from;
17265 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
17266 let automatic_gpu_prefix = self.automatic_gpu_prefix();
17267
17268 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
17269 #[cfg(target_os = "macos")]
17270 let mut gpu_skip_until = 0usize;
17271 for li in tail_start.max(from)..self.num_layers {
17272 let _capacity_tail = automatic_gpu_prefix
17273 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
17274 .map(|_| crate::gpu::enter_cpu_scope());
17275 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
17277 if li > u {
17278 break;
17279 }
17280 }
17281 if let Some(mask) = task_mask {
17282 if !mask.layer_alive(li) {
17283 continue; }
17285 }
17286 if host_tail && task_mask.is_none() {
17291 match self.m3_pooled_layer(li, position, &mut h) {
17292 Ok(true) => {
17293 crate::gpu::set_layer(-1);
17294 continue;
17295 }
17296 Ok(false) => {}
17297 Err(()) => {
17298 self.clear_sequence_state();
17299 self.graph_failed
17300 .store(true, std::sync::atomic::Ordering::Relaxed);
17301 self.cancel
17302 .store(true, std::sync::atomic::Ordering::Relaxed);
17303 tracing::error!(
17304 "pooled tail token graph failed after admission; sequence state cleared"
17305 );
17306 return vec![0.0; self.hidden_size];
17307 }
17308 }
17309 }
17310 #[cfg(target_os = "macos")]
17314 {
17315 if li < gpu_skip_until {
17316 continue;
17317 }
17318 if task_mask.is_none() {
17319 let end = self.q1_graph_gpu(li, upto, position, &mut h);
17320 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
17321 return vec![0.0; self.hidden_size];
17325 }
17326 if end > li {
17327 gpu_skip_until = end;
17328 if self.is_loop_end(end - 1) && end < self.num_layers {
17331 h = inference::rms_norm(
17332 &h,
17333 &self.weights.final_norm,
17334 self.rms_eps,
17335 self.norm_style,
17336 );
17337 }
17338 continue;
17339 }
17340 }
17341 }
17342
17343 if task_mask.is_none() {
17344 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
17345 crate::gpu::BatchGraphOutcome::Completed => continue,
17346 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
17347 crate::gpu::BatchGraphOutcome::Declined => {}
17348 }
17349 }
17350 #[cfg(feature = "gpu")]
17351 self.pull_lagging_host_kv(li, li + 1, position);
17352 let lw = &self.weights.layers[self.phys_layer(li)];
17353 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
17354 if tp.parse::<usize>().ok() == Some(position) {
17355 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
17356 eprintln!(
17357 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
17358 h[0], h[1]
17359 );
17360 }
17361 }
17362 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17365 inference::rms_norm_into(
17366 &h,
17367 &lw.input_norm,
17368 self.rms_eps,
17369 self.norm_style,
17370 &mut self.ws.n1,
17371 );
17372 drop(prof);
17373
17374 let attn_out = match &lw.attn {
17375 AttnKind::Mla(w) => {
17376 let inv_freq_l = self.layer_inv_freq(li);
17377 let rs = self.layer_rope_scale(li);
17378 let eps = self.rms_eps;
17379 let pool = self.pool.clone();
17380 mla_attention(
17381 w,
17382 &self.ws.n1,
17383 &mut self.kv_cache.layers[li],
17384 position,
17385 &inv_freq_l,
17386 rs,
17387 eps,
17388 pool.as_deref(),
17389 )
17390 }
17391 AttnKind::Linear(w) => {
17392 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
17393 vmf_phase_forward(
17394 &self.ws.n1,
17395 w,
17396 &cfg,
17397 &mut self.kv_cache.layers[li].linear_state,
17398 self.pool.as_deref(),
17399 )
17400 }
17401 AttnKind::Kda(w) => {
17402 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
17403 crate::linear_core::kda_forward(
17404 &self.ws.n1,
17405 w,
17406 &cfg,
17407 &mut self.kv_cache.layers[li].linear_state,
17408 self.pool.as_deref(),
17409 )
17410 }
17411 AttnKind::LinearGdn(w) => {
17412 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
17413 gdn_forward(
17414 &self.ws.n1,
17415 w,
17416 &cfg,
17417 &mut self.kv_cache.layers[li].linear_state,
17418 self.pool.as_deref(),
17419 )
17420 }
17421 AttnKind::ShortConv(w) => {
17422 let cfg = self
17423 .short_conv_cfg
17424 .expect("short-conv layer without short_conv_cfg");
17425 short_conv_forward(
17426 &self.ws.n1,
17427 w,
17428 &cfg,
17429 &mut self.kv_cache.layers[li].linear_state,
17430 self.pool.as_deref(),
17431 )
17432 }
17433 AttnKind::Bounded(w) => {
17434 let rope = self
17437 .bounded_rope
17438 .clone()
17439 .expect("bounded layer without an installed rotation table");
17440 let cfg = crate::bounded::BoundedAttnCfg {
17441 num_heads: self.num_heads,
17442 num_kv_heads: self.num_kv_heads,
17443 head_dim: self.head_dim,
17444 hidden_size: hs,
17445 scale: self.attn_scale,
17446 rope: &rope,
17447 pool: pool.as_deref(),
17448 };
17449 crate::bounded::bounded_attention(
17450 &self.ws.n1,
17451 w,
17452 &mut self.kv_cache.layers[li],
17453 &cfg,
17454 )
17455 }
17456 AttnKind::Full {
17457 wq,
17458 wk,
17459 wv,
17460 wo,
17461 q_norm,
17462 k_norm,
17463 output_gate,
17464 softplus_gate,
17465 bias,
17466 } if self.kv_cache.layers[li].o1_sealed() => {
17467 let inv_freq_l = self.layer_inv_freq(li);
17470 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17471 let cfg = QwenAttnCfg {
17472 num_heads: self.layer_num_heads(li),
17473 num_kv_heads: nkv_l,
17474 head_dim: hd_l,
17475 hidden_size: hs,
17476 position,
17477 inv_freq: &inv_freq_l,
17478 rotary_dim: rd_l,
17479 scale: self.attn_scale,
17480 softcap: self.attn_softcap,
17481 window: None,
17482 v_norm: self.attn_v_norm,
17483 qk_norm_after_rope: self.qk_norm_after_rope,
17484 gate_sigmoid: self.proj_gate_sigmoid,
17485 q_norm: q_norm.as_deref(),
17486 k_norm: k_norm.as_deref(),
17487 output_gate: *output_gate,
17488 softplus_gate: softplus_gate
17489 .as_ref()
17490 .map(|(gate, per_head)| (gate, *per_head)),
17491 rope_scale: self.layer_rope_scale(li),
17492 bias: bias
17493 .as_ref()
17494 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17495 rms_eps: eps,
17496 norm_style: self.norm_style,
17497 pool: pool.as_deref(),
17498 v_head_dim: self.layer_v_dim(li),
17499 };
17500 attention::qwen_attention_nystrom(
17501 &self.ws.n1,
17502 wq,
17503 wk,
17504 wv,
17505 wo,
17506 &mut self.kv_cache.layers[li],
17507 &cfg,
17508 )
17509 }
17510 AttnKind::Full {
17511 wq,
17512 wk,
17513 wv,
17514 wo,
17515 q_norm,
17516 k_norm,
17517 output_gate,
17518 softplus_gate,
17519 bias,
17520 } => 'attn: {
17521 let dropin_reason =
17526 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
17527 if let Some(reason) = dropin_reason {
17528 self.note_graph_decline("wgpu attn dropin", reason);
17529 }
17530 if graph_on
17531 && dropin_reason.is_none()
17532 && !*output_gate
17533 && softplus_gate.is_none()
17534 && self.attention_heads_per_layer.is_none()
17535 && bias.is_none()
17536 && task_mask.is_none()
17537 {
17538 let inv_freq_l = self.layer_inv_freq(li);
17539 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17540 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17541 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
17542 wq.mapped_q1(),
17543 wk.mapped_q1(),
17544 wv.mapped_q1(),
17545 wo.mapped_q1(),
17546 ) {
17547 let gm = gm.clone();
17548 let mut out = vec![0f32; hs];
17549 let cache = &self.kv_cache.layers[li];
17550 if crate::gpu::attn_dropin(
17551 &gm,
17552 self.graph_kv_id,
17553 li,
17554 &self.ws.n1,
17555 qi,
17556 ki,
17557 vi,
17558 oi,
17559 q_norm.as_deref(),
17560 k_norm.as_deref(),
17561 self.qk_norm_after_rope,
17562 &inv_freq_l,
17563 nh,
17564 nkv_l,
17565 hd_l,
17566 rd_l,
17567 hs,
17568 position,
17569 self.kv_cache.max_seq_len,
17570 gemma,
17571 eps as f32,
17572 cache.k_heads(),
17573 cache.v_heads(),
17574 &mut out,
17575 ) {
17576 break 'attn out;
17577 }
17578 }
17579 }
17580 let masked = task_mask
17581 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
17582 .unwrap_or(false);
17583 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
17584 let plain = self.layer_attn_plain(li);
17587 match (masked, f32_view) {
17588 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
17591 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
17592 attention::multi_head_attention(
17593 &self.ws.n1,
17594 q,
17595 k,
17596 v,
17597 o,
17598 &mut self.kv_cache.layers[li],
17599 self.num_heads,
17600 self.num_kv_heads,
17601 self.head_dim,
17602 self.hidden_size,
17603 position,
17604 &active_heads,
17605 &self.inv_freq,
17606 )
17607 }
17608 (masked, _) => {
17609 if masked {
17610 tracing::warn!(
17611 "layer {li}: head mask on quantized weights or on a \
17612 window/sink/per-layer-geometry layer not supported \
17613 yet — executing dense"
17614 );
17615 }
17616 let inv_freq_l = self.layer_inv_freq(li);
17617 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
17618 let cfg = QwenAttnCfg {
17619 num_heads: self.layer_num_heads(li),
17620 num_kv_heads: nkv_l,
17621 head_dim: hd_l,
17622 hidden_size: hs,
17623 position,
17624 inv_freq: &inv_freq_l,
17625 rotary_dim: rd_l,
17626 scale: self.attn_scale,
17627 softcap: self.attn_softcap,
17628 window: self.layer_window(li),
17629 v_norm: self.attn_v_norm,
17630 qk_norm_after_rope: self.qk_norm_after_rope,
17631 gate_sigmoid: self.proj_gate_sigmoid,
17632 q_norm: q_norm.as_deref(),
17633 k_norm: k_norm.as_deref(),
17634 output_gate: *output_gate,
17635 softplus_gate: softplus_gate
17636 .as_ref()
17637 .map(|(gate, per_head)| (gate, *per_head)),
17638 rope_scale: self.layer_rope_scale(li),
17639 bias: bias
17640 .as_ref()
17641 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
17642 rms_eps: eps,
17643 norm_style: self.norm_style,
17644 pool: pool.as_deref(),
17645 v_head_dim: self.layer_v_dim(li),
17646 };
17647 attention::qwen_attention(
17648 &self.ws.n1,
17649 wq,
17650 wk,
17651 wv,
17652 wo,
17653 &mut self.kv_cache.layers[li],
17654 &cfg,
17655 )
17656 }
17657 }
17658 }
17659 };
17660 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
17663 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
17664 None => attn_out,
17665 };
17666 let lw = &self.weights.layers[self.phys_layer(li)];
17667 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
17668 inference::add_rmsnorm_fused_into(
17669 &mut h,
17670 &attn_out,
17671 &lw.post_norm,
17672 self.rms_eps,
17673 self.norm_style,
17674 &mut self.ws.p1,
17675 );
17676 drop(prof);
17677 let mut attn_out = attn_out;
17678 attention::recycle_buf(&mut attn_out);
17679 let post_normed = &self.ws.p1;
17680
17681 let ffn_masked = task_mask
17682 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
17683 .unwrap_or(false);
17684 let ffn_out = match (ffn_masked, &lw.ffn) {
17696 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
17700 let row = task_mask
17701 .and_then(|tm| tm.ffn_masks.get(li))
17702 .map(|v| v.as_slice());
17703 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
17704 }
17705 (true, FfnKind::Dense(d)) => {
17706 let tm = task_mask.unwrap();
17707 let alive = tm.ffn_active_count(li);
17708 let deep = alive * 2 <= self.intermediate_size;
17709 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
17710 let active = tm.ffn_active_indices(li);
17711 sparse_ffn_quant(
17712 d,
17713 post_normed,
17714 &active,
17715 self.hidden_size,
17716 self.pool.as_deref(),
17717 )
17718 } else if deep
17719 && let (Some(g), Some(u), Some(dn)) = (
17720 d.gate_proj.as_f32(),
17721 d.up_proj.as_f32(),
17722 d.down_proj.as_f32(),
17723 )
17724 {
17725 let active = tm.ffn_active_indices(li);
17726 inference::sparse_ffn_forward(
17727 post_normed,
17728 g,
17729 u,
17730 dn,
17731 self.hidden_size,
17732 self.intermediate_size,
17733 &active,
17734 self.pool.as_deref(),
17735 )
17736 } else {
17737 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
17738 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
17739 }
17740 }
17741 (true, FfnKind::Moe(m)) => {
17742 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
17746 ffn_forward(
17747 &lw.ffn,
17748 post_normed,
17749 self.pool.as_deref(),
17750 allowed.as_deref(),
17751 )
17752 }
17753 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
17754 dm,
17755 post_normed,
17756 &h,
17757 self.rms_eps,
17758 self.norm_style,
17759 self.pool.as_deref(),
17760 ),
17761 (false, _) => match &lw.ffn {
17762 FfnKind::DenseMoe(dm) => dense_moe_ffn(
17763 dm,
17764 post_normed,
17765 &h,
17766 self.rms_eps,
17767 self.norm_style,
17768 self.pool.as_deref(),
17769 ),
17770 FfnKind::Moe(m)
17771 if task_mask.is_none()
17772 && host_tail
17773 && li >= tail_start
17774 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
17775 {
17776 moe_ffn_minicpm_tail(
17777 &mut self.minicpm_tail,
17778 li,
17779 self.num_layers,
17780 m,
17781 post_normed,
17782 self.pool.as_deref(),
17783 )
17784 }
17785 FfnKind::Moe(m)
17786 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
17787 {
17788 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
17789 }
17790 _ => {
17791 let allowed = match (&lw.ffn, task_mask) {
17792 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
17793 _ => None,
17794 };
17795 ffn_forward(
17796 &lw.ffn,
17797 post_normed,
17798 self.pool.as_deref(),
17799 allowed.as_deref(),
17800 )
17801 }
17802 },
17803 };
17804 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
17805 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
17806 None => ffn_out,
17807 };
17808 for (i, &f) in ffn_out.iter().enumerate() {
17809 h[i] += f;
17810 }
17811 let mut ffn_out = ffn_out;
17812 attention::recycle_buf(&mut ffn_out);
17813
17814 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
17816 for v in h.iter_mut() {
17817 *v *= sc;
17818 }
17819 }
17820 if self.layer_dump.is_some() {
17822 self.dump_layer_row(position, li, &h);
17823 }
17824
17825 if self.is_loop_end(li) && li + 1 < self.num_layers {
17828 h = inference::rms_norm(
17829 &h,
17830 &self.weights.final_norm,
17831 self.rms_eps,
17832 self.norm_style,
17833 );
17834 }
17835
17836 if self.dyn_phi_layer == Some(li) {
17840 self.update_dyn_phi(&h);
17841 }
17842 }
17843 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
17845 crate::gpu::graph_race_record(false, t.elapsed());
17846 }
17847
17848 h
17849 }
17850
17851 fn update_dyn_phi(&mut self, h: &[f32]) {
17854 const A: f32 = 0.2;
17855 if self.dyn_phi_ema.len() != h.len() {
17856 self.dyn_phi_ema = vec![0.0; h.len()];
17857 self.dyn_phi_seen = 0;
17858 }
17859 if self.dyn_phi_seen == 0 {
17860 self.dyn_phi_ema.copy_from_slice(h);
17861 } else {
17862 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
17863 *e = (1.0 - A) * *e + A * v;
17864 }
17865 }
17866 self.dyn_phi_seen += 1;
17867 }
17868
17869 pub fn dyn_phi(&self) -> &[f32] {
17871 &self.dyn_phi_ema
17872 }
17873
17874 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
17876 self.dyn_phi_layer = layer;
17877 self.dyn_phi_ema.clear();
17878 self.dyn_phi_seen = 0;
17879 }
17880
17881 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
17883 let Some(model) = &self.model else {
17884 return Vec::new();
17885 };
17886 model
17887 .header
17888 .skills
17889 .iter()
17890 .enumerate()
17891 .filter_map(|(i, sk)| {
17892 if sk.is_v2() {
17896 return None;
17897 }
17898 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
17899 let sel = sk.selection.as_ref()?;
17900 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
17901 })
17902 .collect()
17903 }
17904
17905 pub fn active_skill(&self) -> Option<usize> {
17907 self.dyn_active
17908 }
17909
17910 pub fn enable_dynamic_routing(&mut self) -> usize {
17915 use crate::swarm::{DynRouter, RoutableSkill};
17916 let Some(model) = self.model.clone() else {
17917 return 0;
17918 };
17919 if let Some(r) = &model.header.router {
17926 tracing::warn!(
17927 "dynamic routing disabled: this file declares router policy '{}' with \
17928 granularity \"{}\" — the request-level decision applies instead",
17929 r.policy,
17930 r.granularity
17931 );
17932 return 0;
17933 }
17934 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
17940 || model.header.skills.iter().any(|s| s.is_v2())
17941 {
17942 tracing::warn!(
17943 "dynamic routing disabled: this file carries format-v2 skill records \
17944 (SKILLS_V2) — they route per request through a router policy only"
17945 );
17946 return 0;
17947 }
17948 if self.dyn_blend_loaded {
17951 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
17952 return 0;
17953 }
17954 if let Some(a) = self.dyn_active {
17958 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
17959 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
17960 return 0;
17961 }
17962 }
17963 let hidden = self.hidden_size;
17964 let mut skills = Vec::new();
17965 for (idx, id, _phi) in self.dynamic_skills() {
17966 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
17967 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
17968 skills.push(rs);
17969 }
17970 }
17971 }
17972 if skills.is_empty() {
17973 return 0;
17974 }
17975 let phi = skills[0].phi_layer;
17977 if skills.iter().any(|s| s.phi_layer != phi) {
17978 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
17979 }
17980 let n = skills.len();
17981 self.set_dyn_phi_layer(Some(phi));
17982 self.dyn_router = Some(DynRouter::new(skills));
17983 n
17984 }
17985
17986 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
17988 self.dyn_router
17989 .as_ref()
17990 .map(|r| r.switches.clone())
17991 .unwrap_or_default()
17992 }
17993
17994 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
17997 let _mimo_q8 = self
17998 .mimo_moe
17999 .is_on()
18000 .then(crate::qtensor::enter_full_gpu_q8_scope);
18001 let rows = self.weights.lm_head.rows();
18002 let mut logits = attention::take_buf(rows.min(self.vocab_size));
18003 let served = self.mimo_moe.is_on()
18007 && crate::gpu::mimo_q8_short_enabled()
18008 && rows == self.vocab_size
18009 && !self.weights.lm_head.has_prism_contract()
18010 && self
18011 .weights
18012 .lm_head
18013 .graph_weight()
18014 .is_some_and(|(model, idx, kind, _)| {
18015 kind == 7
18016 && crate::gpu::q82_short_rows(
18017 model,
18018 idx,
18019 hidden,
18020 1,
18021 rows,
18022 self.hidden_size,
18023 &mut logits,
18024 )
18025 });
18026 if !served {
18027 self.weights
18028 .lm_head
18029 .matvec(hidden, &mut logits, self.pool.as_deref());
18030 }
18031 logits.resize(self.vocab_size, 0.0);
18032 if let Some(m) = self.logit_multiplier {
18033 for l in logits.iter_mut() {
18034 *l *= m;
18035 }
18036 }
18037 if let Some(c) = self.final_softcap {
18038 softcap_in_place(&mut logits, c, self.pool.as_deref());
18039 }
18040 if let Some(cm) = self.head_clusters.as_ref() {
18041 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
18042 }
18043 logits
18044 }
18045
18046 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
18049 let h = hidden.len();
18050 let ncl = cm.len() / h.max(1);
18051 if ncl == 0 || logits.len() % ncl != 0 {
18052 return;
18053 }
18054 let cs = logits.len() / ncl;
18055 let mut lc = vec![0.0f32; ncl];
18057 for c in 0..ncl {
18058 let row = &cm[c * h..(c + 1) * h];
18059 let mut s = 0.0f32;
18060 for j in 0..h {
18061 s += row[j] * hidden[j];
18062 }
18063 lc[c] = s;
18064 }
18065 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18066 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
18067 for c in 0..ncl {
18068 let blk = &mut logits[c * cs..(c + 1) * cs];
18069 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18070 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
18071 let add = lc[c] - lse - bl;
18072 for v in blk.iter_mut() {
18073 *v += add;
18074 }
18075 }
18076 }
18077
18078 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
18083 #[cfg(target_os = "macos")]
18084 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
18085 self.clear_sequence_state();
18086 crate::gpu::graph_race_begin_generation();
18090 if task_mask.is_none() {
18091 self.o1_begin();
18092 }
18093 let mut hidden = vec![0.0f32; self.hidden_size];
18094 for (pos, &id) in ids.iter().enumerate() {
18095 let emb = self.embed_single(id);
18096 hidden = self.forward_layers(&emb, pos, task_mask);
18097 }
18098 if let Err(err) = self.o1_seal_checked() {
18099 self.o1_fail(err);
18100 }
18101 if let Some(logits) = self.graph_logits.take() {
18104 return logits;
18105 }
18106 inference::rms_norm_into(
18107 &hidden,
18108 &self.weights.final_norm,
18109 self.rms_eps,
18110 self.norm_style,
18111 &mut self.ws.n1,
18112 );
18113 self.lm_head_forward(&self.ws.n1)
18114 }
18115}
18116
18117pub fn create_test_pipeline(
18119 hidden_size: usize,
18120 intermediate_size: usize,
18121 num_heads: usize,
18122 num_kv_heads: usize,
18123 head_dim: usize,
18124 num_layers: usize,
18125 vocab_size: usize,
18126) -> Pipeline {
18127 let synth = |n: usize, salt: usize| -> Vec<f32> {
18130 (0..n)
18131 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
18132 .collect()
18133 };
18134 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
18135 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
18136 };
18137 let layer_weights: Vec<LayerWeights> = (0..num_layers)
18138 .map(|li| LayerWeights {
18139 input_norm: vec![1.0; hidden_size],
18140 post_norm: vec![1.0; hidden_size],
18141 attn_out_norm: None,
18142 ffn_out_norm: None,
18143 layer_scale: None,
18144 ffn: FfnKind::Dense(DenseFfn {
18145 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
18146 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
18147 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
18148 act: Act::Silu,
18149 down_t: None,
18150 segs: Vec::new(),
18151 }),
18152 attn: AttnKind::Full {
18153 bias: None,
18154 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
18155 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
18156 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
18157 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
18158 q_norm: None,
18159 k_norm: None,
18160 output_gate: false,
18161 softplus_gate: None,
18162 },
18163 })
18164 .collect();
18165
18166 Pipeline::new(
18167 Tokenizer::byte_level(),
18168 PipelineWeights {
18169 embed_tokens: qt(vocab_size, hidden_size, 100),
18170 layers: layer_weights,
18171 lm_head: qt(vocab_size, hidden_size, 200),
18172 final_norm: vec![1.0; hidden_size],
18173 },
18174 hidden_size,
18175 intermediate_size,
18176 num_heads,
18177 num_kv_heads,
18178 head_dim,
18179 num_layers,
18180 num_layers, false, vocab_size,
18183 1e-6,
18184 10_000.0,
18185 NormStyle::Qwen,
18186 4096,
18187 SamplerConfig {
18188 seed: Some(42),
18189 ..Default::default()
18190 },
18191 )
18192}
18193
18194#[inline]
18199fn mask_bit(row: &[u8], j: usize) -> bool {
18200 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
18201}
18202
18203fn mask_gain() -> f32 {
18214 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
18215 *G.get_or_init(|| {
18216 std::env::var("CMF_FFN_MASK_GAIN")
18217 .ok()
18218 .and_then(|v| v.parse().ok())
18219 .unwrap_or(1.0)
18220 })
18221}
18222
18223fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
18224 let fill = meanfill().and_then(|(i, v)| {
18227 let li = crate::gpu::cur_layer();
18228 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
18229 });
18230 for r in 0..rows {
18231 let base = r * inter;
18232 for (bi, &byte) in row.iter().enumerate() {
18233 if byte == 0xFF {
18234 continue;
18235 }
18236 let j0 = bi * 8;
18237 for bit in 0..8 {
18238 let j = j0 + bit;
18239 if j < inter && byte & (1 << bit) == 0 {
18240 g[base + j] = fill.map_or(0.0, |f| f[j]);
18241 }
18242 }
18243 }
18244 }
18245 let gain = mask_gain();
18246 if gain != 1.0 {
18247 for v in g[..rows * inter].iter_mut() {
18248 *v *= gain;
18249 }
18250 }
18251}
18252
18253#[inline]
18255fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
18256 row.is_none_or(|r| mask_bit(r, i))
18257}
18258
18259fn all_bits_on(row: &[u8], n: usize) -> bool {
18262 (0..n).all(|i| mask_bit(row, i))
18263}
18264
18265fn tube_topk() -> usize {
18273 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18274 *K.get_or_init(|| {
18275 std::env::var("CMF_TUBE_TOPK")
18276 .ok()
18277 .and_then(|v| v.parse().ok())
18278 .unwrap_or(0)
18279 })
18280}
18281
18282fn tube_score_oracle() -> bool {
18283 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18284 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
18285}
18286
18287fn tube_ffn_routed(
18294 d: &DenseFfn,
18295 xs: &[f32],
18296 b: usize,
18297 pool: Option<&Pool>,
18298 mask_row: Option<&[u8]>,
18299 k: usize,
18300) -> Vec<f32> {
18301 let hidden = d.down_proj.rows();
18302 let core = d.gate_proj.rows();
18303 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18304 let mut out = match (b, core_full, mask_row) {
18305 (1, true, _) => dense_ffn(d, xs, pool),
18306 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18307 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18308 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18309 };
18310 let cand: Vec<usize> = (0..d.segs.len())
18311 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
18312 .collect();
18313 if cand.is_empty() {
18314 return out;
18315 }
18316 let oracle = tube_score_oracle();
18320 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
18321 let mut scores = vec![0f32; b * cand.len()];
18322 for (ci, &i) in cand.iter().enumerate() {
18323 let seg = &d.segs[i];
18324 let w = seg.width;
18325 let mut g = vec![0.0f32; b * w];
18326 if b == 1 {
18327 seg.gate.matvec(xs, &mut g, pool);
18328 } else {
18329 seg.gate.matmat(xs, b, &mut g, pool);
18330 }
18331 for v in g.iter_mut() {
18332 *v = Act::Silu.combine(*v, 1.0);
18333 }
18334 if !oracle {
18335 for t in 0..b {
18336 scores[t * cand.len() + ci] =
18337 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18338 }
18339 }
18340 if oracle || b > 1 {
18341 let mut u = vec![0.0f32; b * w];
18342 if b == 1 {
18343 seg.up.matvec(xs, &mut u, pool);
18344 } else {
18345 seg.up.matmat(xs, b, &mut u, pool);
18346 }
18347 for (a, &v) in g.iter_mut().zip(u.iter()) {
18348 *a *= v;
18349 }
18350 if oracle {
18351 for t in 0..b {
18352 scores[t * cand.len() + ci] =
18353 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
18354 }
18355 }
18356 }
18357 acts.push(g);
18358 }
18359 let keep = k.min(cand.len());
18361 let mut scratch: Vec<f32> = Vec::new();
18362 for t in 0..b {
18363 let mut sc: Vec<(f32, usize)> = (0..cand.len())
18364 .map(|ci| (scores[t * cand.len() + ci], ci))
18365 .collect();
18366 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
18367 let mut alive = vec![false; cand.len()];
18368 for &(_, ci) in sc.iter().take(keep) {
18369 alive[ci] = true;
18370 }
18371 if b > 1 {
18372 for (ci, a) in acts.iter_mut().enumerate() {
18373 if !alive[ci] {
18374 let w = d.segs[cand[ci]].width;
18375 a[t * w..(t + 1) * w].fill(0.0);
18376 }
18377 }
18378 } else {
18379 for (ci, &i) in cand.iter().enumerate() {
18383 if !alive[ci] {
18384 continue;
18385 }
18386 let seg = &d.segs[i];
18387 let w = seg.width;
18388 let g = &mut acts[ci];
18389 if !tube_score_oracle() {
18390 scratch.clear();
18391 scratch.resize(w, 0.0);
18392 seg.up.matvec(xs, &mut scratch, pool);
18393 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
18394 *a *= v;
18395 }
18396 }
18397 let mut acc = vec![0.0f32; hidden];
18398 seg.down.matvec(g, &mut acc, pool);
18399 for (o, a) in out.iter_mut().zip(&acc) {
18400 *o += *a;
18401 }
18402 }
18403 }
18404 }
18405 if b > 1 {
18406 for (ci, &i) in cand.iter().enumerate() {
18407 let seg = &d.segs[i];
18408 let mut acc = vec![0.0f32; b * hidden];
18409 seg.down.matmat(&acts[ci], b, &mut acc, pool);
18410 for (o, a) in out.iter_mut().zip(&acc) {
18411 *o += *a;
18412 }
18413 }
18414 }
18415 out
18416}
18417
18418fn tube_ffn(
18424 d: &DenseFfn,
18425 xs: &[f32],
18426 b: usize,
18427 pool: Option<&Pool>,
18428 mask_row: Option<&[u8]>,
18429) -> Vec<f32> {
18430 if tube_topk() > 0 {
18431 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
18432 }
18433 let hidden = d.down_proj.rows();
18434 let core = d.gate_proj.rows();
18435 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
18436 let mut out = match (b, core_full, mask_row) {
18437 (1, true, _) => dense_ffn(d, xs, pool),
18438 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
18439 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
18440 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
18441 };
18442 TUBE_SCRATCH.with(|sc| {
18443 let mut sc = sc.borrow_mut();
18444 let [g, u, acc] = &mut *sc;
18445 for seg in &d.segs {
18446 if !tube_bit(mask_row, seg.start) {
18447 continue;
18448 }
18449 let w = seg.width;
18450 g.resize(b * w, 0.0);
18451 if b == 1
18452 && d.act == Act::Silu
18453 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
18454 {
18455 } else {
18457 u.resize(b * w, 0.0);
18458 if b == 1 {
18459 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
18460 } else {
18461 seg.gate.matmat(xs, b, g, pool);
18462 seg.up.matmat(xs, b, u, pool);
18463 }
18464 for i in 0..b * w {
18465 g[i] = d.act.combine(g[i], u[i]);
18466 }
18467 }
18468 acc.resize(b * hidden, 0.0);
18469 acc.fill(0.0);
18470 if b == 1 {
18471 seg.down.matvec(g, acc, pool);
18472 } else {
18473 seg.down.matmat(g, b, acc, pool);
18474 }
18475 for (o, a) in out.iter_mut().zip(acc.iter()) {
18476 *o += *a;
18477 }
18478 }
18479 out
18480 })
18481}
18482
18483thread_local! {
18484 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
18488 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
18489}
18490
18491static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
18494 std::sync::atomic::AtomicU64::new(0),
18495 std::sync::atomic::AtomicU64::new(0),
18496];
18497
18498fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
18502 #[cfg(feature = "gpu")]
18503 {
18504 use std::sync::atomic::Ordering::Relaxed;
18505 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
18506 (
18507 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
18508 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
18509 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
18510 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
18511 )
18512 }
18513 #[cfg(not(feature = "gpu"))]
18514 (0.0, 0.0, 0.0, 0)
18515}
18516
18517fn wgpu_mirror_counters() -> [u64; 6] {
18520 #[cfg(feature = "gpu")]
18521 {
18522 crate::gpu_wgpu::MIRROR_EVENTS
18523 .each_ref()
18524 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
18525 }
18526 #[cfg(not(feature = "gpu"))]
18527 [0; 6]
18528}
18529
18530fn prefill_prof_on() -> bool {
18531 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18532 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
18533}
18534
18535pub(crate) fn accumulate_dense_ffn_activation_mass(
18544 d: &DenseFfn,
18545 xs: &[f32],
18546 b: usize,
18547 pool: Option<&Pool>,
18548 mass: &mut [f64],
18549 squared_mass: &mut [f64],
18550) -> Result<(), String> {
18551 let inter = d.gate_proj.rows();
18552 let hidden = d.gate_proj.cols();
18553 if mass.len() != inter || squared_mass.len() != inter {
18554 return Err(format!(
18555 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
18556 mass.len(),
18557 squared_mass.len()
18558 ));
18559 }
18560 if d.up_proj.rows() != inter
18561 || d.up_proj.cols() != hidden
18562 || d.down_proj.cols() != inter
18563 || d.down_proj.rows() != hidden
18564 {
18565 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
18566 }
18567 if !d.segs.is_empty() {
18568 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
18569 }
18570 if xs.len() != b.saturating_mul(hidden) {
18571 return Err(format!(
18572 "MTP FFN probe: {} input values for {b} × {hidden} rows",
18573 xs.len()
18574 ));
18575 }
18576 if b == 0 {
18577 return Ok(());
18578 }
18579 let mut gate = vec![0.0f32; b * inter];
18580 let mut up = vec![0.0f32; b * inter];
18581 d.gate_proj.matmat(xs, b, &mut gate, pool);
18582 d.up_proj.matmat(xs, b, &mut up, pool);
18583 for row in 0..b {
18584 for i in 0..inter {
18585 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
18586 let a = a as f64;
18587 mass[i] += a.abs();
18588 squared_mass[i] += a * a;
18589 }
18590 }
18591 Ok(())
18592}
18593
18594fn dense_ffn_batch(
18595 d: &DenseFfn,
18596 xs: &[f32],
18597 b: usize,
18598 pool: Option<&Pool>,
18599 mask_row: Option<&[u8]>,
18600) -> Vec<f32> {
18601 let inter = d.gate_proj.rows();
18602 let hidden = d.down_proj.rows();
18603 let fused_act = d.act.wgpu_graph_act();
18615 if mask_row.is_none()
18616 && fused_act.is_some()
18617 && b >= 32
18618 && crate::gpu::enabled_here()
18619 && !crate::gpu::mm_killed()
18620 && refit_dir().is_none()
18625 && !ffn_probe_active()
18630 && !crate::gptq_capture::capturing()
18634 {
18635 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18636 d.gate_proj.mapped_q4t(),
18637 d.up_proj.mapped_q4t(),
18638 d.down_proj.mapped_q4t(),
18639 ) {
18640 let mut out = vec![0.0f32; b * hidden];
18641 let act = fused_act.expect("checked above");
18642 if crate::gpu::q4_ffn_act(
18643 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
18644 ) {
18645 return out;
18646 }
18647 }
18648 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18653 d.gate_proj.mapped_q4tp(),
18654 d.up_proj.mapped_q4tp(),
18655 d.down_proj.mapped_q4tp(),
18656 ) {
18657 let mut out = vec![0.0f32; b * hidden];
18658 let act = fused_act.expect("checked above");
18659 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
18660 {
18661 return out;
18662 }
18663 }
18664 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
18670 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
18671 d.gate_proj.mapped_device_gemm(),
18672 d.up_proj.mapped_device_gemm(),
18673 d.down_proj.mapped_device_gemm(),
18674 ) {
18675 let mut out = vec![0.0f32; b * hidden];
18676 let act = fused_act.expect("checked above");
18677 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
18678 return out;
18679 }
18680 }
18681 }
18682 let mut g = vec![0.0f32; b * inter];
18683 d.gate_proj.matmat(xs, b, &mut g, pool);
18684 let mut u = vec![0.0f32; b * inter];
18685 d.up_proj.matmat(xs, b, &mut u, pool);
18686 if gate_topk() > 0 && d.act == Act::Silu {
18687 for t in 0..b {
18688 let row = &mut g[t * inter..(t + 1) * inter];
18689 for v in row.iter_mut() {
18690 *v = Act::Silu.combine(*v, 1.0);
18691 }
18692 keep_top_k(row, gate_topk());
18693 }
18694 for i in 0..b * inter {
18695 g[i] *= u[i];
18696 }
18697 } else {
18698 for i in 0..b * inter {
18699 g[i] = d.act.combine(g[i], u[i]);
18700 }
18701 }
18702 if let Some(row) = mask_row {
18703 zero_masked_cols(&mut g, b, inter, row);
18704 }
18705 if oracle_topk() > 0 {
18706 for t in 0..b {
18707 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
18708 }
18709 }
18710 let mut out = vec![0.0f32; b * hidden];
18711 d.down_proj.matmat(&g, b, &mut out, pool);
18712 if refit_dir().is_some() {
18713 let li = crate::gpu::cur_layer();
18714 if li >= 0 {
18715 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
18716 }
18717 }
18718 FFN_PROBE.with(|pr| {
18722 if let Some(acc) = pr.borrow_mut().as_mut() {
18723 let li = crate::gpu::cur_layer();
18724 if li < 0 {
18725 return;
18726 }
18727 let Some(row) = acc.get_mut(li as usize) else {
18728 return;
18729 };
18730 let sq = probe_sq();
18731 for t in 0..b {
18732 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
18733 *a += if sq {
18734 (v as f64) * (v as f64)
18735 } else {
18736 (v as f64).abs()
18737 };
18738 }
18739 }
18740 }
18741 });
18742 out
18743}
18744
18745fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
18750 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18751 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18752 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
18753 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
18754 if (!on && !dump) || b == 0 {
18755 return;
18756 }
18757 let hidden = xs.len() / b;
18758 if on {
18759 let mut acc = m.act_sq.borrow_mut();
18760 if acc.len() < hidden {
18761 acc.resize(hidden, 0.0);
18762 }
18763 for t in 0..b {
18764 let row = &xs[t * hidden..(t + 1) * hidden];
18765 for (a, &v) in acc.iter_mut().zip(row) {
18766 *a += (v as f64) * (v as f64);
18767 }
18768 }
18769 }
18770 if dump {
18771 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
18774 .ok()
18775 .and_then(|v| v.parse().ok())
18776 .unwrap_or(4096);
18777 let mut rows = m.act_rows.borrow_mut();
18778 if rows.len() < cap * hidden {
18779 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
18780 rows.extend_from_slice(&xs[..take * hidden]);
18781 }
18782 }
18783}
18784
18785#[derive(Clone, Copy)]
18788struct SendVecs(*mut Vec<f32>);
18789unsafe impl Send for SendVecs {}
18790unsafe impl Sync for SendVecs {}
18791impl SendVecs {
18792 #[inline]
18793 fn at(self, i: usize) -> *mut Vec<f32> {
18794 unsafe { self.0.add(i) }
18795 }
18796}
18797
18798fn moe_ffn_batch(
18799 m: &MoeFfn,
18800 xs: &[f32],
18801 b: usize,
18802 hidden: usize,
18803 pool: Option<&Pool>,
18804 allowed: Option<&[bool]>,
18805) -> Vec<f32> {
18806 accumulate_act(m, xs, b);
18807 let ne = m.experts.len();
18808 let mut logits = vec![0.0f32; b * ne];
18809 match &m.resonance {
18810 Some(r) => {
18811 let hdim = xs.len() / b.max(1);
18812 for bi in 0..b {
18813 r.scores(
18814 &xs[bi * hdim..(bi + 1) * hdim],
18815 &mut logits[bi * ne..(bi + 1) * ne],
18816 );
18817 }
18818 }
18819 None => m.router.matmat(xs, b, &mut logits, pool),
18820 }
18821
18822 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
18825 {
18826 let mut st = m.stats.borrow_mut();
18827 if st.len() < ne {
18828 st.resize(ne, 0);
18829 }
18830 for bi in 0..b {
18831 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
18832 for &e in &idx {
18833 st[e] += 1;
18834 assign[e].push((bi, p[e] / wsum));
18835 }
18836 }
18837 }
18838
18839 let mut out = vec![0.0f32; b * hidden];
18840 let cols = m.experts[0].gate_proj.cols();
18841 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
18842 let sb = list.len();
18843 let mut sub = vec![0.0f32; sb * cols];
18844 for (k, &(bi, _)) in list.iter().enumerate() {
18845 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18846 }
18847 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
18848 for (k, &(bi, w)) in list.iter().enumerate() {
18849 for i in 0..hidden {
18850 out[bi * hidden + i] += w * eo[k * hidden + i];
18851 }
18852 }
18853 };
18854 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
18860 if pool.is_some() && active.len() >= 8 {
18861 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
18862 {
18863 let panel_ptr = SendVecs(panels.as_mut_ptr());
18864 let experts = &m.experts;
18867 let (active_r, assign_r) = (&active, &assign);
18868 let inherit_cpu = crate::gpu::inherit_cpu_scope();
18869 let run = |start: usize, end: usize| {
18870 let _cpu_scope = inherit_cpu();
18871 #[cfg(target_os = "macos")]
18879 let _metal_cpu = crate::gpu::enter_cpu_scope();
18880 for ai in start..end {
18881 let e = active_r[ai];
18882 let list = &assign_r[e];
18883 let sb = list.len();
18884 let mut sub = vec![0.0f32; sb * cols];
18885 for (k, &(bi, _)) in list.iter().enumerate() {
18886 sub[k * cols..(k + 1) * cols]
18887 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
18888 }
18889 unsafe {
18891 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
18892 }
18893 }
18894 };
18895 match pool {
18896 Some(p) => p.run_rows(active.len(), &run),
18897 None => run(0, active.len()),
18898 }
18899 }
18900 for (ai, &e) in active.iter().enumerate() {
18901 for (k, &(bi, w)) in assign[e].iter().enumerate() {
18902 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
18903 for i in 0..hidden {
18904 out[bi * hidden + i] += w * eo[i];
18905 }
18906 }
18907 }
18908 } else {
18909 for &e in &active {
18910 run_expert(&m.experts[e], &assign[e], &mut out);
18911 }
18912 }
18913 if let Some((se, gate)) = &m.shared {
18914 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
18915 let mut gl = vec![0.0f32; b];
18916 gate.matmat(xs, b, &mut gl, pool);
18917 (0..b)
18918 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
18919 .collect()
18920 } else {
18921 (0..b).map(|bi| (bi, 1.0)).collect()
18922 };
18923 run_expert(se, &all, &mut out);
18924 }
18925 out
18926}
18927
18928fn moe_ffn_rows_exact(
18940 m: &MoeFfn,
18941 xs: &[f32],
18942 b: usize,
18943 hidden: usize,
18944 pool: Option<&Pool>,
18945) -> Vec<f32> {
18946 let mut out = vec![0.0f32; b * hidden];
18947 let per_row = |out: &mut [f32]| {
18948 for r in 0..b {
18949 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
18950 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
18951 }
18952 };
18953 let covered = !crate::gpu::enabled_here()
18954 && moe_batch_enabled()
18955 && m.shared.is_none()
18956 && m.resonance.is_none()
18957 && FFN_PROBE.with(|pr| pr.borrow().is_none())
18958 && m.experts.iter().all(|d| d.act == Act::Silu);
18959 if !covered {
18960 per_row(&mut out);
18961 return out;
18962 }
18963 let ne = m.experts.len();
18964 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
18966 for r in 0..b {
18967 let x = &xs[r * hidden..(r + 1) * hidden];
18968 accumulate_act(m, x, 1);
18969 let mut logits = vec![0.0f32; ne];
18970 m.router.matvec(x, &mut logits, pool);
18971 let (idx, p, wsum) = moe_route(&logits, m, None);
18972 {
18973 let mut st = m.stats.borrow_mut();
18974 if st.len() < ne {
18975 st.resize(ne, 0);
18976 }
18977 for &e in &idx {
18978 st[e] += 1;
18979 }
18980 }
18981 let w: Vec<f32> = idx
18982 .iter()
18983 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
18984 .collect();
18985 routes.push((idx, w));
18986 }
18987 if routes.iter().any(|(idx, _)| idx.is_empty()) {
18988 per_row(&mut out);
18989 return out;
18990 }
18991 let mut experts: Vec<usize> = Vec::new();
18993 let mut groups: Vec<Vec<usize>> = Vec::new();
18994 for (r, (idx, _)) in routes.iter().enumerate() {
18995 for &e in idx {
18996 match experts.iter().position(|&x| x == e) {
18997 Some(g) => groups[g].push(r),
18998 None => {
18999 experts.push(e);
19000 groups.push(vec![r]);
19001 }
19002 }
19003 }
19004 }
19005 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
19006 let inter = m.experts[experts[0]].gate_proj.rows();
19007 let pairs: Vec<(&QTensor, &QTensor)> = experts
19008 .iter()
19009 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
19010 .collect();
19011 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
19012 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
19013 per_row(&mut out);
19014 return out;
19015 }
19016 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
19017 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
19018 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
19019 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
19020 per_row(&mut out);
19021 return out;
19022 }
19023 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
19025 let mut p = 0usize;
19026 for (g, &e) in experts.iter().enumerate() {
19027 for &r in &groups[g] {
19028 slot.insert((r, e), p);
19029 p += 1;
19030 }
19031 }
19032 for (r, (idx, w)) in routes.iter().enumerate() {
19033 let terms: Vec<(&[f32], f32)> = idx
19034 .iter()
19035 .zip(w)
19036 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
19037 .collect();
19038 let row = &mut out[r * hidden..(r + 1) * hidden];
19039 for (i, dst) in row.iter_mut().enumerate() {
19040 let mut acc = 0f32;
19042 for (d, we) in &terms {
19043 acc += we * d[i];
19044 }
19045 *dst = acc;
19046 }
19047 }
19048 out
19049}
19050
19051pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
19055 match pool {
19056 Some(p) if v.len() >= 1 << 15 => {
19057 let base = v.as_mut_ptr() as usize;
19058 p.run_rows(v.len(), &|s, e| {
19059 let sl =
19062 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
19063 for x in sl {
19064 *x = c * (*x / c).tanh();
19065 }
19066 });
19067 }
19068 _ => {
19069 for x in v.iter_mut() {
19070 *x = c * (*x / c).tanh();
19071 }
19072 }
19073 }
19074}
19075
19076thread_local! {
19077 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
19081 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
19082}
19083
19084fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19086 if gate_topk() > 0
19089 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
19090 {
19091 return out;
19092 }
19093 let prism_body = d.gate_proj.has_prism_contract()
19109 || d.up_proj.has_prism_contract()
19110 || d.down_proj.has_prism_contract();
19111 if !prism_body
19112 && crate::gpu::enabled_here()
19113 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
19114 {
19115 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
19116 crate::gpu::ProbeArm::Gpu
19117 } else {
19118 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
19119 };
19120 match arm {
19121 crate::gpu::ProbeArm::Gpu => {
19122 let t0 = std::time::Instant::now();
19123 if let Some(out) = dense_ffn_gpu(d, x, pool) {
19124 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
19125 return out;
19126 }
19127 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
19131 }
19132 crate::gpu::ProbeArm::CpuTimed => {
19133 let t0 = std::time::Instant::now();
19134 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19135 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
19136 return out;
19137 }
19138 crate::gpu::ProbeArm::Cpu => {
19139 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
19140 }
19141 }
19142 }
19143 dense_ffn_cpu(d, x, pool)
19144}
19145
19146fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
19148 let inter = d.gate_proj.rows();
19149 FFN_SCRATCH.with(|s| {
19150 let mut s = s.borrow_mut();
19151 let [g, u, ..] = &mut *s;
19152 g.resize(inter, 0.0);
19153 if gate_topk() > 0 {
19156 u.resize(inter, 0.0);
19160 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19161 for i in 0..inter {
19162 g[i] = Act::Silu.combine(g[i], 1.0);
19163 }
19164 keep_top_k(g, gate_topk());
19165 for i in 0..inter {
19166 g[i] *= u[i];
19167 }
19168 } else if d.act == Act::Silu && {
19169 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19170 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
19171 } {
19172 } else {
19174 u.resize(inter, 0.0);
19175 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
19177 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19178 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
19179 for i in 0..inter {
19180 g[i] = d.act.combine(g[i], u[i]);
19181 }
19182 }
19183 FFN_PROBE.with(|pr| {
19191 if let Some(acc) = pr.borrow_mut().as_mut() {
19192 let li = crate::gpu::cur_layer();
19193 if li >= 0 {
19194 if let Some(row) = acc.get_mut(li as usize) {
19195 match probe_topk() {
19196 0 if probe_sq() => {
19197 for (a, &v) in row.iter_mut().zip(g.iter()) {
19198 *a += (v as f64) * (v as f64);
19199 }
19200 }
19201 0 if probe_signed() => {
19202 for (a, &v) in row.iter_mut().zip(g.iter()) {
19203 *a += v as f64;
19204 }
19205 }
19206 0 => {
19207 for (a, &v) in row.iter_mut().zip(g.iter()) {
19208 *a += (v as f64).abs();
19209 }
19210 }
19211 k => {
19212 let n = g.len();
19213 let k = k.min(n);
19214 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19215 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19216 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19217 });
19218 let thr = *kth;
19219 for (a, &v) in row.iter_mut().zip(g.iter()) {
19220 if v.abs() >= thr {
19221 *a += 1.0;
19222 }
19223 }
19224 }
19225 }
19226 }
19227 }
19228 }
19229 });
19230 if oracle_topk() > 0 {
19231 keep_top_k(g, oracle_topk());
19232 }
19233 {
19234 let li = crate::gpu::cur_layer();
19235 if li >= 0 {
19236 adump_row(li as usize, g);
19237 }
19238 }
19239 let mut out = attention::take_buf(d.down_proj.rows());
19240 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
19241 d.down_proj.matvec(g, &mut out, pool);
19242 out
19243 })
19244}
19245
19246pub struct RefitAcc {
19259 pub support: Vec<u32>,
19260 pub gss: Vec<f32>,
19261 pub ya: Vec<f32>,
19262 pub hidden: usize,
19263 pub tokens: u64,
19264 pub buf_g: Vec<f32>,
19270 pub buf_o: Vec<f32>,
19271 pub buf_t: usize,
19272}
19273
19274type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
19278
19279static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
19280 std::sync::OnceLock::new();
19281
19282fn ffn_probe_active() -> bool {
19285 FFN_PROBE.with(|p| p.borrow().is_some())
19286}
19287
19288fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
19289 REFIT
19290 .get_or_init(|| {
19291 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
19292 (
19293 d,
19294 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
19295 )
19296 })
19297 })
19298 .as_ref()
19299}
19300
19301fn refit_accumulate(
19303 li: usize,
19304 g: &[f32],
19305 b: usize,
19306 inter: usize,
19307 out: &[f32],
19308 hidden: usize,
19309 pool: Option<&Pool>,
19310) {
19311 let Some((dir, map)) = refit_dir() else {
19312 return;
19313 };
19314 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19315 let (from, to) = *SPAN.get_or_init(|| {
19316 let g = |k: &str, d: usize| {
19317 std::env::var(k)
19318 .ok()
19319 .and_then(|v| v.parse().ok())
19320 .unwrap_or(d)
19321 };
19322 (
19323 g("CMF_FFN_REFIT_FROM", 0),
19324 g("CMF_FFN_REFIT_TO", usize::MAX),
19325 )
19326 });
19327 if li < from || li > to {
19328 return;
19329 }
19330 let mut guard = map.lock().unwrap();
19331 let (map, shared) = &mut *guard;
19332 let acc = match map.entry(li) {
19333 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
19334 std::collections::hash_map::Entry::Vacant(e) => {
19335 let path = format!("{dir}/support.{li}.u32");
19336 let Ok(bytes) = std::fs::read(&path) else {
19337 eprintln!("refit: no {path} — layer {li} skipped");
19338 return;
19339 };
19340 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
19341 let support: Vec<u32> = bytes[4..4 + n * 4]
19342 .chunks_exact(4)
19343 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19344 .collect();
19345 eprintln!(
19346 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
19347 (n * n + hidden * n) as f64 * 4.0 / 1e6
19348 );
19349 e.insert(RefitAcc {
19350 gss: vec![0.0; n * n],
19351 ya: vec![0.0; hidden * n],
19352 buf_g: Vec::new(),
19353 buf_o: Vec::new(),
19354 buf_t: 0,
19355 support,
19356 hidden,
19357 tokens: 0,
19358 })
19359 }
19360 };
19361 let ns = acc.support.len();
19362 let cap = refit_batch();
19364 if acc.buf_g.is_empty() {
19365 acc.buf_g = vec![0.0; ns * cap];
19366 acc.buf_o = vec![0.0; hidden * cap];
19367 }
19368 let take = b.min(cap - acc.buf_t);
19369 for t in 0..take {
19370 let col = acc.buf_t + t;
19371 for (j, &n) in acc.support.iter().enumerate() {
19372 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
19373 }
19374 for h in 0..hidden {
19375 acc.buf_o[h * cap + col] = out[t * hidden + h];
19376 }
19377 }
19378 acc.buf_t += take;
19379 acc.tokens += take as u64;
19380 if acc.buf_t < cap {
19381 return;
19382 }
19383 let bt = acc.buf_t;
19384 acc.buf_t = 0;
19385 let RefitAcc {
19395 gss,
19396 ya,
19397 buf_g,
19398 buf_o,
19399 ..
19400 } = acc;
19401 let need = (ns * ns).max(hidden * ns);
19402 if shared.len() < need {
19403 shared.resize(need, 0.0);
19404 }
19405 let scratch = &mut shared[..];
19406 let _ = bt;
19407 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
19408 add_into(gss, &scratch[..ns * ns], pool);
19409 if crate::gpu::gemm_nt_f32_transient(
19410 buf_o,
19411 buf_g,
19412 &mut scratch[..hidden * ns],
19413 hidden,
19414 cap,
19415 ns,
19416 ) {
19417 add_into(ya, &scratch[..hidden * ns], pool);
19418 } else {
19419 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19420 }
19421 } else {
19422 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
19423 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
19424 }
19425 }
19429
19430fn refit_batch() -> usize {
19432 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19433 *B.get_or_init(|| {
19434 std::env::var("CMF_FFN_REFIT_BATCH")
19435 .ok()
19436 .and_then(|v| v.parse().ok())
19437 .unwrap_or(4096)
19438 })
19439}
19440
19441fn accum_outer_t(
19444 c: &mut [f32],
19445 m: usize,
19446 n: usize,
19447 b: usize,
19448 left: &[f32],
19449 right: &[f32],
19450 pool: Option<&Pool>,
19451) {
19452 let ptr = SendMut(c.as_mut_ptr());
19453 let body = |i: usize| {
19454 let ptr = &ptr;
19455 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
19456 for t in 0..b {
19457 let a = left[i * b + t];
19458 if a == 0.0 {
19459 continue;
19460 }
19461 for (j, o) in row.iter_mut().enumerate() {
19462 *o += a * right[j * b + t];
19463 }
19464 }
19465 };
19466 match pool {
19467 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
19468 for i in s..e {
19469 body(i);
19470 }
19471 }),
19472 _ => {
19473 for i in 0..m {
19474 body(i);
19475 }
19476 }
19477 }
19478}
19479
19480fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
19483 let n = dst.len().min(src.len());
19484 match pool {
19485 Some(p) if n >= 1 << 16 => {
19486 let ptr = SendMut(dst.as_mut_ptr());
19487 let f = |s: usize, e: usize| {
19488 let ptr = &ptr;
19489 for blk in s..e {
19490 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
19491 for i in a..b {
19492 unsafe { *ptr.0.add(i) += src[i] };
19493 }
19494 }
19495 };
19496 p.run_rows(n.div_ceil(4096), &f);
19497 }
19498 _ => {
19499 for (d, v) in dst.iter_mut().zip(&src[..n]) {
19500 *d += *v;
19501 }
19502 }
19503 }
19504}
19505
19506fn accum_outer(
19511 c: &mut [f32],
19512 m: usize,
19513 n: usize,
19514 b: usize,
19515 left: &[f32],
19516 right: &[f32],
19517 pool: Option<&Pool>,
19518) {
19519 const TILE: usize = 32;
19520 let tiles = m.div_ceil(TILE);
19521 let cp = SendMut(c.as_mut_ptr());
19522 let body = |ti: usize| {
19523 let cp = &cp;
19524 let i0 = ti * TILE;
19525 let i1 = (i0 + TILE).min(m);
19526 for t in 0..b {
19527 let r = &right[t * n..t * n + n];
19528 for i in i0..i1 {
19529 let a = left[i * b + t];
19530 if a == 0.0 {
19531 continue;
19532 }
19533 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
19535 for (o, v) in row.iter_mut().zip(r) {
19536 *o += a * *v;
19537 }
19538 }
19539 }
19540 };
19541 match pool {
19542 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
19543 for ti in s..e {
19544 body(ti);
19545 }
19546 }),
19547 _ => {
19548 for ti in 0..tiles {
19549 body(ti);
19550 }
19551 }
19552 }
19553}
19554
19555pub fn refit_flush() -> usize {
19557 let Some((dir, map)) = refit_dir() else {
19558 return 0;
19559 };
19560 let guard = map.lock().unwrap();
19561 let mut n = 0;
19562 for (li, acc) in guard.0.iter() {
19563 let w = |name: &str, v: &[f32]| {
19566 let path = format!("{dir}/{name}.{li}.f32");
19567 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
19568 match std::fs::write(&path, &bytes) {
19569 Ok(()) => {}
19570 Err(e) => eprintln!(
19571 "refit: FAILED to write {path} ({} MB): {e}",
19572 bytes.len() / 1_000_000
19573 ),
19574 }
19575 };
19576 w("gss", &acc.gss);
19577 w("ya", &acc.ya);
19578 println!(
19579 "refit L{li}: {} support, {} tokens, hidden {}",
19580 acc.support.len(),
19581 acc.tokens,
19582 acc.hidden
19583 );
19584 n += 1;
19585 }
19586 n
19587}
19588
19589fn adump_row(li: usize, g: &[f32]) {
19594 use std::io::Write as _;
19595 static FILES: std::sync::OnceLock<
19596 Option<(
19597 String,
19598 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
19599 )>,
19600 > = std::sync::OnceLock::new();
19601 let Some((prefix, map)) = FILES
19602 .get_or_init(|| {
19603 std::env::var("CMF_FFN_ADUMP")
19604 .ok()
19605 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
19606 })
19607 .as_ref()
19608 else {
19609 return;
19610 };
19611 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
19614 let (from, to) = *SPAN.get_or_init(|| {
19615 let g = |k: &str, d: usize| {
19616 std::env::var(k)
19617 .ok()
19618 .and_then(|v| v.parse().ok())
19619 .unwrap_or(d)
19620 };
19621 (
19622 g("CMF_FFN_ADUMP_FROM", 0),
19623 g("CMF_FFN_ADUMP_TO", usize::MAX),
19624 )
19625 });
19626 if li < from || li > to {
19627 return;
19628 }
19629 let mut map = map.lock().unwrap();
19630 let f = map.entry(li).or_insert_with(|| {
19631 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
19632 });
19633 let mut bytes = Vec::with_capacity(g.len() * 2);
19634 for v in g {
19635 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
19636 }
19637 let _ = f.write_all(&bytes);
19638}
19639
19640fn oracle_topk() -> usize {
19646 if exact_mtp_calibration_active() {
19647 return 0;
19648 }
19649 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19650 *K.get_or_init(|| {
19651 std::env::var("CMF_FFN_ORACLE_TOPK")
19652 .ok()
19653 .and_then(|v| v.parse().ok())
19654 .unwrap_or(0)
19655 })
19656}
19657
19658fn gate_topk() -> usize {
19664 if exact_mtp_calibration_active() {
19665 return 0;
19666 }
19667 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19668 *K.get_or_init(|| {
19669 std::env::var("CMF_FFN_GATE_TOPK")
19670 .ok()
19671 .and_then(|v| v.parse().ok())
19672 .unwrap_or(0)
19673 })
19674}
19675
19676fn gate_block() -> usize {
19683 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19684 *B.get_or_init(|| {
19685 std::env::var("CMF_FFN_GATE_BLOCK")
19686 .ok()
19687 .and_then(|v| v.parse().ok())
19688 .unwrap_or(1)
19689 })
19690}
19691
19692fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
19694 let n = g.len();
19695 let nb = n.div_ceil(block);
19696 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
19697 if kb >= nb {
19698 return;
19699 }
19700 let mut score: Vec<f32> = (0..nb)
19701 .map(|b| {
19702 g[b * block..((b + 1) * block).min(n)]
19703 .iter()
19704 .map(|v| v * v)
19705 .sum::<f32>()
19706 })
19707 .collect();
19708 let mut ord = score.clone();
19709 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
19710 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19711 });
19712 let thr = *kth;
19713 for b in 0..nb {
19714 if score[b] < thr {
19715 g[b * block..((b + 1) * block).min(n)].fill(0.0);
19716 }
19717 }
19718 score.clear();
19719}
19720
19721fn keep_top_k(g: &mut [f32], k: usize) {
19723 if gate_block() > 1 {
19724 return keep_top_blocks(g, k, gate_block());
19725 }
19726 let n = g.len();
19727 if k == 0 || k >= n {
19728 return;
19729 }
19730 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
19731 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19732 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19733 });
19734 let thr = *kth;
19735 for v in g.iter_mut() {
19736 if v.abs() < thr {
19737 *v = 0.0;
19738 }
19739 }
19740}
19741
19742fn probe_sq() -> bool {
19746 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19747 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
19748}
19749
19750fn probe_signed() -> bool {
19754 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19755 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
19756}
19757
19758fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
19766 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
19767 M.get_or_init(|| {
19768 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
19769 let b = std::fs::read(&p).ok()?;
19770 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
19771 let vals: Vec<f32> = b[8..]
19772 .chunks_exact(4)
19773 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
19774 .collect();
19775 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
19776 Some((inter, vals))
19777 })
19778 .as_ref()
19779}
19780
19781fn probe_topk() -> usize {
19784 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19785 *K.get_or_init(|| {
19786 std::env::var("CMF_FFN_PROBE_TOPK")
19787 .ok()
19788 .and_then(|v| v.parse().ok())
19789 .unwrap_or(0)
19790 })
19791}
19792
19793thread_local! {
19794 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
19797 const { std::cell::RefCell::new(None) };
19798}
19799
19800fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
19813 if d.gate_proj.has_prism_contract()
19818 || d.up_proj.has_prism_contract()
19819 || d.down_proj.has_prism_contract()
19820 {
19821 return None;
19822 }
19823 let dt = d.down_t.as_ref()?;
19824 let inter = d.gate_proj.rows();
19825 let hidden = dt.cols();
19826 if k == 0 || k >= inter || d.act != Act::Silu {
19827 return None;
19828 }
19829 DYN_SCRATCH.with(|sc| {
19830 let mut sc = sc.borrow_mut();
19831 let DynScratch {
19832 g,
19833 mag,
19834 live,
19835 parts,
19836 } = &mut *sc;
19837 g.resize(inter, 0.0);
19838 d.gate_proj.matvec(x, g, pool);
19839 for v in g.iter_mut() {
19840 *v = inference::silu(*v);
19841 }
19842 mag.clear();
19845 mag.extend(g.iter().map(|v| v.abs()));
19846 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
19847 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
19848 });
19849 let thr = *kth;
19850 live.clear();
19851 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
19852 let mut out = vec![0.0f32; hidden];
19853 match pool {
19854 Some(p) if live.len() >= 64 => {
19855 let nw = p.n_workers() + 1;
19856 parts.clear();
19857 parts.resize(nw * hidden, 0.0);
19858 let ptr = SendMut(parts.as_mut_ptr());
19859 let n = live.len();
19860 let live_ref: &[u32] = live;
19861 let g_ref: &[f32] = g;
19862 p.run(&|w, workers| {
19863 let chunk = n.div_ceil(workers);
19864 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
19865 if s >= e {
19866 return;
19867 }
19868 WORKER_SCRATCH.with(|ws| {
19869 let mut ws = ws.borrow_mut();
19870 let [scratch, acc] = &mut *ws;
19871 scratch.resize(hidden.max(x.len()), 0.0);
19872 acc.clear();
19873 acc.resize(hidden, 0.0);
19874 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
19875 if let Some(&nx) = live_ref[s..e].get(o + 1) {
19878 d.up_proj.prefetch_row(nx as usize);
19879 dt.prefetch_row(nx as usize);
19880 }
19881 let idx = nrm as usize;
19882 let up = d.up_proj.row_dot(idx, x, scratch);
19883 let a = g_ref[idx] * up;
19884 if a != 0.0 {
19885 dt.add_row_scaled(idx, a, acc, scratch);
19886 }
19887 }
19888 for (j, v) in acc.iter().enumerate() {
19889 unsafe { *ptr.at(w * hidden + j) = *v };
19890 }
19891 });
19892 });
19893 for w in 0..nw {
19894 for (j, o) in out.iter_mut().enumerate() {
19895 *o += parts[w * hidden + j];
19896 }
19897 }
19898 }
19899 _ => {
19900 WORKER_SCRATCH.with(|ws| {
19901 let mut ws = ws.borrow_mut();
19902 let [scratch, _acc] = &mut *ws;
19903 scratch.resize(hidden.max(x.len()), 0.0);
19904 for &nrm in live.iter() {
19905 let idx = nrm as usize;
19906 let up = d.up_proj.row_dot(idx, x, scratch);
19907 let a = g[idx] * up;
19908 if a != 0.0 {
19909 dt.add_row_scaled(idx, a, &mut out, scratch);
19910 }
19911 }
19912 });
19913 }
19914 }
19915 Some(out)
19916 })
19917}
19918
19919struct DynScratch {
19922 g: Vec<f32>,
19923 mag: Vec<f32>,
19924 live: Vec<u32>,
19925 parts: Vec<f32>,
19926}
19927
19928thread_local! {
19929 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
19930 std::cell::RefCell::new(DynScratch {
19931 g: Vec::new(),
19932 mag: Vec::new(),
19933 live: Vec::new(),
19934 parts: Vec::new(),
19935 })
19936 };
19937 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
19939 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
19940}
19941
19942fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
19947 let inter = d.gate_proj.rows();
19948 FFN_SCRATCH.with(|s| {
19949 let mut s = s.borrow_mut();
19950 let [g, u, ..] = &mut *s;
19951 g.resize(inter, 0.0);
19952 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
19953 } else {
19955 u.resize(inter, 0.0);
19956 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
19957 for i in 0..inter {
19958 g[i] = d.act.combine(g[i], u[i]);
19959 }
19960 }
19961 zero_masked_cols(g, 1, inter, mask_row);
19962 let mut out = attention::take_buf(d.down_proj.rows());
19963 d.down_proj.matvec(g, &mut out, pool);
19964 out
19965 })
19966}
19967
19968fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
19974 if d.gate_proj.has_prism_contract()
19975 || d.up_proj.has_prism_contract()
19976 || d.down_proj.has_prism_contract()
19977 {
19978 return None;
19979 }
19980 d.act.moe_job_graph_contract()?;
19983 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
19986 return None;
19987 }
19988 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
19989 let mut model_ref = None;
19990 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
19991 let model = model_ref?;
19992 let hidden = jobs[0].down.1;
19993 let mut out = attention::take_buf(hidden);
19994 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19995 Some(out)
19996 } else {
19997 let mut out = out;
19998 attention::recycle_buf(&mut out);
19999 None
20000 }
20001}
20002
20003#[allow(clippy::type_complexity)]
20008#[allow(clippy::type_complexity)]
20009pub(crate) fn moe_parts(
20010 t: &QTensor,
20011) -> Option<(
20012 &std::sync::Arc<cortiq_core::CmfModel>,
20013 usize,
20014 usize,
20015 usize,
20016 &[f32],
20017 &[f32],
20018 bool,
20019 bool,
20020 bool,
20021)> {
20022 match t {
20023 QTensor::Mapped {
20024 model,
20025 idx,
20026 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
20027 rows,
20028 cols,
20029 row_scale,
20030 col_field,
20031 ..
20032 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
20033 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
20034 )),
20035 QTensor::Mapped {
20037 model,
20038 idx,
20039 dtype: cortiq_core::TensorDtype::Q1,
20040 rows,
20041 cols,
20042 ..
20043 } => Some((
20044 model,
20045 *idx,
20046 *rows,
20047 *cols,
20048 &[][..],
20049 &[][..],
20050 true,
20051 false,
20052 false,
20053 )),
20054 QTensor::Mapped {
20056 model,
20057 idx,
20058 dtype: cortiq_core::TensorDtype::Q4Tiled,
20059 rows,
20060 cols,
20061 ..
20062 } => Some((
20063 model,
20064 *idx,
20065 *rows,
20066 *cols,
20067 &[][..],
20068 &[][..],
20069 false,
20070 true,
20071 false,
20072 )),
20073 QTensor::Mapped {
20075 model,
20076 idx,
20077 dtype: cortiq_core::TensorDtype::Q4TiledP,
20078 rows,
20079 cols,
20080 ..
20081 } => Some((
20082 model,
20083 *idx,
20084 *rows,
20085 *cols,
20086 &[][..],
20087 &[][..],
20088 false,
20089 true,
20090 false,
20091 )),
20092 QTensor::Mapped {
20096 model,
20097 idx,
20098 dtype: cortiq_core::TensorDtype::Q2TiledP,
20099 rows,
20100 cols,
20101 ..
20102 } => Some((
20103 model,
20104 *idx,
20105 *rows,
20106 *cols,
20107 &[][..],
20108 &[][..],
20109 false,
20110 true,
20111 true,
20112 )),
20113 _ => None,
20114 }
20115}
20116
20117#[cfg(target_os = "macos")]
20124#[allow(clippy::type_complexity)]
20125fn chunk_moe_parts(
20126 m: &MoeFfn,
20127 hidden: usize,
20128) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
20129 if m.shared.is_some()
20130 || m.per_expert_scale.is_some()
20131 || m.resonance.is_some()
20132 || m.route_tau.is_some()
20133 || m.router_input_norm
20134 || m.experts.is_empty()
20135 || m.top_k == 0
20136 || std::env::var("CMF_RMS_TRACE").is_ok()
20137 || std::env::var("CMF_ACT_DUMP").is_ok()
20138 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
20139 {
20140 return None;
20141 }
20142 let (rf, rr, rc) = m.router.f32_parts()?;
20143 if rr != m.experts.len() || rc != hidden {
20144 return None;
20145 }
20146 let inter = m.experts[0].gate_proj.rows();
20147 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
20150 let experts = m
20151 .experts
20152 .iter()
20153 .map(|e| {
20154 if e.act != Act::Silu
20155 || !e.segs.is_empty()
20156 || e.gate_proj.rows() != inter
20157 || e.up_proj.rows() != inter
20158 || e.down_proj.cols() != inter
20159 || e.gate_proj.cols() != hidden
20160 || e.down_proj.rows() != hidden
20161 {
20162 return None;
20163 }
20164 if q8 {
20165 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20166 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20167 }
20168 Some((
20169 e.gate_proj.mapped_q4tp()?.1,
20170 e.up_proj.mapped_q4tp()?.1,
20171 e.down_proj.mapped_q4tp()?.1,
20172 ))
20173 })
20174 .collect::<Option<Vec<_>>>()?;
20175 Some((rf, experts, q8))
20176}
20177
20178#[cfg(target_os = "macos")]
20191fn metal_flint_moe_graph_parts(
20192 m: &MoeFfn,
20193 hidden: usize,
20194 scratch_key: usize,
20195) -> Option<crate::gpu::GpuFlintMoe<'_>> {
20196 use std::sync::Arc;
20197
20198 const HIDDEN: usize = 2_048;
20199 const ROUTED_INTER: usize = 512;
20200 const SHARED_INTER: usize = 1_024;
20201 const EXPERTS: usize = 256;
20202 const TOP_K: usize = 6;
20203
20204 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
20205 return None;
20206 }
20207 let routed0 = m.experts.first()?;
20208 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
20209 let (router, router_rows, router_cols) = m.router.f32_parts()?;
20210 if router_rows != EXPERTS || router_cols != HIDDEN {
20211 return None;
20212 }
20213 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
20214 return None;
20215 }
20216 let same_q2 = |t: &QTensor| -> Option<usize> {
20217 let (owner, idx) = t.mapped_q2tp()?;
20218 Arc::ptr_eq(&owner, &model).then_some(idx)
20219 };
20220 let same_q4 = |t: &QTensor| -> Option<usize> {
20221 let (owner, idx) = t.mapped_q4tp()?;
20222 Arc::ptr_eq(&owner, &model).then_some(idx)
20223 };
20224 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20225 if e.act != Act::Silu
20226 || !e.segs.is_empty()
20227 || e.gate_proj.rows() != ROUTED_INTER
20228 || e.gate_proj.cols() != HIDDEN
20229 || e.up_proj.rows() != ROUTED_INTER
20230 || e.up_proj.cols() != HIDDEN
20231 || e.down_proj.rows() != HIDDEN
20232 || e.down_proj.cols() != ROUTED_INTER
20233 {
20234 return None;
20235 }
20236 Some((
20237 same_q2(&e.gate_proj)?,
20238 same_q2(&e.up_proj)?,
20239 same_q4(&e.down_proj)?,
20240 ))
20241 };
20242 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20243 if e.act != Act::Silu
20244 || !e.segs.is_empty()
20245 || e.gate_proj.rows() != SHARED_INTER
20246 || e.gate_proj.cols() != HIDDEN
20247 || e.up_proj.rows() != SHARED_INTER
20248 || e.up_proj.cols() != HIDDEN
20249 || e.down_proj.rows() != HIDDEN
20250 || e.down_proj.cols() != SHARED_INTER
20251 {
20252 return None;
20253 }
20254 Some((
20255 same_q4(&e.gate_proj)?,
20256 same_q4(&e.up_proj)?,
20257 same_q4(&e.down_proj)?,
20258 ))
20259 };
20260 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
20261 if experts.len() != EXPERTS {
20262 return None;
20263 }
20264 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
20265 if shared_gate.is_some() {
20266 return None;
20267 }
20268 Some(crate::gpu::GpuFlintMoe {
20269 router,
20270 bias: m.expert_bias.as_deref(),
20271 experts,
20272 scratch_key,
20273 shared: shared(shared_ffn)?,
20274 top_k: m.top_k,
20275 norm_topk: m.norm_topk_prob,
20276 route_scale: m.routed_scaling,
20277 sigmoid: m.router_sigmoid,
20278 sigmoid_denom_eps: m.sigmoid_denom_eps,
20279 })
20280}
20281
20282#[cfg(target_os = "macos")]
20283fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
20284 if m.router_input_norm
20285 || m.route_tau.is_some()
20286 || m.mask.is_some()
20287 || m.per_expert_scale.is_some()
20288 || m.experts.is_empty()
20289 || m.top_k == 0
20290 || m.resonance.is_some()
20291 {
20292 return None;
20293 }
20294 let (sh, sg) = match &m.shared {
20299 Some((sh, sg)) => (Some(sh), sg.as_ref()),
20300 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
20301 None => return None,
20302 };
20303 let (rf, rr, rc) = m.router.f32_parts()?;
20304 if rr != m.experts.len() || rc != hidden {
20305 return None;
20306 }
20307 let shared_gated = sg.is_some();
20308 let sf = match sg {
20309 Some(sg) => {
20310 let (sf, sr, sc) = sg.f32_parts()?;
20311 if sr * sc != hidden {
20312 return None;
20313 }
20314 sf
20315 }
20316 None => &rf[..hidden],
20319 };
20320 if let Some(b) = &m.expert_bias {
20321 if b.len() != m.experts.len() {
20322 return None;
20323 }
20324 }
20325 let inter = m.experts[0].gate_proj.rows();
20326 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
20329 let q8 = crate::gpu_metal::moe_q8::enabled()
20333 && m.shared.is_none()
20334 && m.experts[0].gate_proj.q8_2f_parts().is_some();
20335 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
20336 if e.act != Act::Silu
20337 || e.gate_proj.rows() != inter
20338 || e.gate_proj.cols() != hidden
20339 || e.up_proj.rows() != inter
20340 || e.up_proj.cols() != hidden
20341 || e.down_proj.rows() != hidden
20342 || e.down_proj.cols() != inter
20343 {
20344 return None;
20345 }
20346 if q8 {
20347 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
20348 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
20349 }
20350 let pick = |t: &QTensor| -> Option<usize> {
20351 if gu_q2 {
20352 t.mapped_q2tp().map(|(_, i)| i)
20353 } else {
20354 t.mapped_q4tp().map(|(_, i)| i)
20355 }
20356 };
20357 Some((
20358 pick(&e.gate_proj)?,
20359 pick(&e.up_proj)?,
20360 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
20361 ))
20362 };
20363 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
20364 let shared = match sh {
20365 Some(sh) => Some(trio(sh)?),
20366 None => None,
20367 };
20368 Some(crate::gpu::GpuMoe {
20369 router: rf,
20370 sgate: sf,
20371 experts,
20372 shared,
20373 n_exp: m.experts.len(),
20374 top_k: m.top_k,
20375 inter,
20376 norm_topk: m.norm_topk_prob,
20377 route_scale: m.routed_scaling,
20378 gu_q2,
20379 q8,
20380 sigmoid: m.router_sigmoid,
20381 sigmoid_denom_eps: m.sigmoid_denom_eps,
20382 bias: m.expert_bias.as_deref(),
20383 shared_gated,
20384 })
20385}
20386
20387fn forced_ids_at(step: usize) -> Option<u32> {
20393 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
20394 IDS.get_or_init(|| {
20395 std::env::var("CMF_FORCE_IDS")
20396 .ok()
20397 .and_then(|p| std::fs::read_to_string(p).ok())
20398 .map(|s| {
20399 s.split_whitespace()
20400 .filter_map(|t| t.parse().ok())
20401 .collect()
20402 })
20403 .unwrap_or_default()
20404 })
20405 .get(step)
20406 .copied()
20407}
20408
20409pub(crate) fn moe_push_job_parts<'a>(
20413 gate: &'a QTensor,
20414 up: &'a QTensor,
20415 down: &'a QTensor,
20416 x: &[f32],
20417 w: f32,
20418 swiglu_limit: f32,
20419 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20420 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20421) -> Option<()> {
20422 use crate::qtensor::prescale;
20423 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
20424 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
20425 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
20426 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20427 return None; }
20429 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
20432 return None;
20433 }
20434 if !gq2 && dq2 {
20435 return None;
20436 }
20437 model_ref.get_or_insert_with(|| gm.clone());
20438 let dt = |cf: &[f32]| {
20439 if cf.is_empty() {
20440 cortiq_core::TensorDtype::Q8Row
20441 } else {
20442 cortiq_core::TensorDtype::Q8_2f
20443 }
20444 };
20445 jobs.push(crate::gpu::MoeJob {
20446 gate: (gi, gr, gc, grs),
20447 up: (ui, ur, uc, urs),
20448 down: (di, dr, dc, drs),
20449 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
20450 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
20451 down_col: dcf,
20452 w,
20453 q1: gq1,
20454 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
20455 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
20456 gu_q2: gq2,
20457 swiglu_limit,
20458 act: 0,
20459 });
20460 Some(())
20461}
20462
20463fn moe_push_job<'a>(
20465 d: &'a DenseFfn,
20466 x: &[f32],
20467 w: f32,
20468 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
20469 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
20470) -> Option<()> {
20471 use crate::qtensor::prescale;
20472 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
20477 let act = graph_act.code();
20478 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
20479 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
20480 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
20481 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
20482 return None; }
20484 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
20485 return None;
20486 }
20487 if !gq2 && dq2 {
20488 return None;
20489 }
20490 model_ref.get_or_insert_with(|| gm.clone());
20491 let gdt = if gcf.is_empty() {
20492 cortiq_core::TensorDtype::Q8Row
20493 } else {
20494 cortiq_core::TensorDtype::Q8_2f
20495 };
20496 let udt = if ucf.is_empty() {
20497 cortiq_core::TensorDtype::Q8Row
20498 } else {
20499 cortiq_core::TensorDtype::Q8_2f
20500 };
20501 jobs.push(crate::gpu::MoeJob {
20502 gate: (gi, gr, gc, grs),
20503 up: (ui, ur, uc, urs),
20504 down: (di, dr, dc, drs),
20505 xs_gate: prescale(x, gcf, gdt).into_owned(),
20506 xs_up: prescale(x, ucf, udt).into_owned(),
20507 down_col: dcf,
20508 w,
20509 q1: gq1,
20510 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
20511 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
20512 gu_q2: gq2,
20513 swiglu_limit,
20514 act,
20515 });
20516 Some(())
20517}
20518
20519fn sparse_ffn_quant(
20526 d: &DenseFfn,
20527 x: &[f32],
20528 active: &[u16],
20529 hidden: usize,
20530 pool: Option<&Pool>,
20531) -> Vec<f32> {
20532 let n = active.len();
20533 let inter = d.gate_proj.rows();
20534 let mut act = vec![0.0f32; n];
20535 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
20538 let compute = |ai: usize| -> f32 {
20539 let idx = active[ai] as usize;
20540 if idx >= inter {
20541 return 0.0; }
20543 let mut s = if need_scratch {
20544 vec![0.0f32; hidden]
20545 } else {
20546 Vec::new()
20547 };
20548 let gate = d.gate_proj.row_dot(idx, x, &mut s);
20549 let up = d.up_proj.row_dot(idx, x, &mut s);
20550 d.act.combine(gate, up)
20551 };
20552 match pool {
20553 Some(p) if n >= 256 => {
20554 let ptr = SendMut(act.as_mut_ptr());
20555 p.run(&|widx, nw| {
20556 let chunk = n.div_ceil(nw);
20557 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
20558 for ai in s..e {
20559 unsafe { *ptr.at(ai) = compute(ai) };
20560 }
20561 });
20562 }
20563 _ => {
20564 for (ai, a) in act.iter_mut().enumerate() {
20565 *a = compute(ai);
20566 }
20567 }
20568 }
20569 let mut out = vec![0.0f32; hidden];
20571 for (ai, &idx) in active.iter().enumerate() {
20572 let w = act[ai];
20573 if w.abs() >= 1e-12 && (idx as usize) < inter {
20574 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
20575 }
20576 }
20577 out
20578}
20579
20580#[doc(hidden)]
20582pub fn sparse_ffn_quant_for_test(
20583 d: &DenseFfn,
20584 x: &[f32],
20585 active: &[u16],
20586 hidden: usize,
20587) -> Vec<f32> {
20588 sparse_ffn_quant(d, x, active, hidden, None)
20589}
20590
20591fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
20595 let deq = |t: &QTensor| -> Vec<f32> {
20596 let (rows, cols) = (t.rows(), t.cols());
20597 let mut out = vec![0.0f32; rows * cols];
20598 for r in 0..rows {
20599 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
20600 }
20601 out
20602 };
20603 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
20604}
20605
20606struct SendMut(*mut f32);
20608unsafe impl Send for SendMut {}
20609unsafe impl Sync for SendMut {}
20610impl SendMut {
20611 #[inline]
20612 #[allow(clippy::mut_from_ref)]
20615 unsafe fn at(&self, i: usize) -> &mut f32 {
20616 unsafe { &mut *self.0.add(i) }
20617 }
20618}
20619
20620pub(crate) fn moe_route(
20632 logits: &[f32],
20633 m: &MoeFfn,
20634 allowed: Option<&[bool]>,
20635) -> (Vec<usize>, Vec<f32>, f32) {
20636 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
20637}
20638
20639pub(crate) fn moe_route_with_eps(
20649 logits: &[f32],
20650 m: &MoeFfn,
20651 allowed: Option<&[bool]>,
20652 sigmoid_denom_eps: f32,
20653) -> (Vec<usize>, Vec<f32>, f32) {
20654 let ne = logits.len();
20655 let admit = |e: usize| {
20661 m.mask.as_ref().is_none_or(|mk| mk[e])
20662 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
20663 };
20664 if m.resonance.is_some() && m.top_k == 1 {
20677 let mut best: Option<usize> = None;
20678 for e in (0..ne).filter(|&e| admit(e)) {
20679 let l = logits[e];
20680 if l == f32::NEG_INFINITY || l.is_nan() {
20681 continue;
20682 }
20683 if best.is_none_or(|b| l > logits[b]) {
20684 best = Some(e);
20685 }
20686 }
20687 if let Some(b) = best {
20688 let mut p = vec![0.0f32; ne];
20689 p[b] = 1.0;
20690 return (vec![b], p, 1.0 / m.routed_scaling);
20691 }
20692 }
20693 let p: Vec<f32> = if m.router_sigmoid {
20699 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
20700 } else {
20701 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
20702 if mx == f32::NEG_INFINITY {
20703 vec![1.0 / ne.max(1) as f32; ne]
20704 } else {
20705 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
20706 let s: f32 = e.iter().sum();
20707 for v in &mut e {
20708 *v /= s;
20709 }
20710 e
20711 }
20712 };
20713 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
20714 match &m.expert_bias {
20716 Some(b) => idx.sort_unstable_by(|&x, &y| {
20717 (p[y] + b[y])
20718 .partial_cmp(&(p[x] + b[x]))
20719 .unwrap()
20720 .then(x.cmp(&y))
20721 }),
20722 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
20723 }
20724 idx.truncate(m.top_k);
20725 if let Some(tau) = m.route_tau {
20729 let total: f32 = idx.iter().map(|&e| p[e]).sum();
20730 if total > 0.0 {
20731 let mut acc = 0.0f32;
20732 let mut keep = idx.len();
20733 for (i, &e) in idx.iter().enumerate() {
20734 acc += p[e];
20735 if acc >= tau * total {
20736 keep = i + 1;
20737 break;
20738 }
20739 }
20740 idx.truncate(keep);
20741 }
20742 }
20743 let wsum: f32 = if m.norm_topk_prob {
20744 let s: f32 = idx.iter().map(|&e| p[e]).sum();
20745 (if m.router_sigmoid {
20749 s + sigmoid_denom_eps
20750 } else {
20751 s
20752 }) / m.routed_scaling
20753 } else {
20754 1.0 / m.routed_scaling
20755 };
20756 (idx, p, wsum)
20757}
20758
20759fn moe_trace(idx: &[usize]) {
20761 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
20762}
20763
20764pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
20767 use std::io::Write;
20768 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
20769 std::sync::OnceLock::new();
20770 let Some(f) = F.get_or_init(|| {
20771 let p = std::env::var("CMF_MOE_TRACE").ok()?;
20772 Some(std::sync::Mutex::new(
20773 std::fs::OpenOptions::new()
20774 .create(true)
20775 .append(true)
20776 .open(p)
20777 .ok()?,
20778 ))
20779 }) else {
20780 return;
20781 };
20782 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
20783 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
20784}
20785
20786pub(crate) fn moe_ffn(
20789 m: &MoeFfn,
20790 x: &[f32],
20791 pool: Option<&Pool>,
20792 allowed: Option<&[bool]>,
20793) -> Vec<f32> {
20794 let r = moe_ffn_route(m, x, pool, allowed);
20795 moe_ffn_experts(m, x, &r, pool)
20796}
20797
20798pub(crate) struct MoeRoute {
20802 pub idx: Vec<usize>,
20803 pub p: Vec<f32>,
20804 pub wsum: f32,
20805 pub logits: Vec<f32>,
20806}
20807
20808pub(crate) fn moe_ffn_route(
20814 m: &MoeFfn,
20815 x: &[f32],
20816 pool: Option<&Pool>,
20817 allowed: Option<&[bool]>,
20818) -> MoeRoute {
20819 accumulate_act(m, x, 1);
20820 let ne = m.experts.len();
20821 let mut logits = vec![0.0f32; ne];
20822 match &m.resonance {
20823 Some(r) => r.scores(x, &mut logits),
20824 None => m.router.matvec(x, &mut logits, pool),
20825 }
20826 let (idx, p, wsum) = moe_route(&logits, m, allowed);
20827 {
20828 let mut st = m.stats.borrow_mut();
20829 if st.len() < ne {
20830 st.resize(ne, 0);
20831 }
20832 for &e in &idx {
20833 st[e] += 1;
20834 }
20835 }
20836 moe_trace(&idx);
20842 MoeRoute {
20843 idx,
20844 p,
20845 wsum,
20846 logits,
20847 }
20848}
20849
20850#[cfg(target_os = "macos")]
20859fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
20860 const HIDDEN: usize = 2_048;
20861 const ROUTED_INTER: usize = 512;
20862 const SHARED_INTER: usize = 1_024;
20863 const EXPERTS: usize = 256;
20864 const TOP_K: usize = 6;
20865
20866 let Some(routed) = m.experts.first() else {
20867 return false;
20868 };
20869 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
20870 return false;
20871 };
20872 let Some((shared, shared_gate)) = &m.shared else {
20873 return false;
20874 };
20875 if shared_gate.is_some()
20876 || model.header.arch.arch_name != "flint"
20877 || model.header.arch.prism_hadamard.is_some()
20878 || model.header.arch.hidden_size != HIDDEN
20879 || m.experts.len() != EXPERTS
20880 || m.top_k != TOP_K
20881 || !m.router_sigmoid
20882 || !m.norm_topk_prob
20883 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
20884 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
20885 || m.route_tau.is_some()
20886 || m.mask.is_some()
20887 || m.per_expert_scale.is_some()
20888 || m.router_input_norm
20889 || m.resonance.is_some()
20890 || !m.grown.is_empty()
20891 {
20892 return false;
20893 }
20894
20895 let routed_q2 = routed.act == Act::Silu
20896 && routed.segs.is_empty()
20897 && routed.gate_proj.rows() == ROUTED_INTER
20898 && routed.gate_proj.cols() == HIDDEN
20899 && routed.up_proj.rows() == ROUTED_INTER
20900 && routed.up_proj.cols() == HIDDEN
20901 && routed.down_proj.rows() == HIDDEN
20902 && routed.down_proj.cols() == ROUTED_INTER
20903 && routed.up_proj.mapped_q2tp().is_some()
20904 && routed.down_proj.mapped_q4tp().is_some();
20905 let shared_q4 = shared.act == Act::Silu
20906 && shared.segs.is_empty()
20907 && shared.gate_proj.rows() == SHARED_INTER
20908 && shared.gate_proj.cols() == HIDDEN
20909 && shared.up_proj.rows() == SHARED_INTER
20910 && shared.up_proj.cols() == HIDDEN
20911 && shared.down_proj.rows() == HIDDEN
20912 && shared.down_proj.cols() == SHARED_INTER
20913 && shared.gate_proj.mapped_q4tp().is_some()
20914 && shared.up_proj.mapped_q4tp().is_some()
20915 && shared.down_proj.mapped_q4tp().is_some();
20916 routed_q2 && shared_q4
20917}
20918
20919pub(crate) fn moe_ffn_experts(
20922 m: &MoeFfn,
20923 x: &[f32],
20924 r: &MoeRoute,
20925 pool: Option<&Pool>,
20926) -> Vec<f32> {
20927 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
20928 if crate::gpu::enabled_here() {
20933 #[cfg(target_os = "macos")]
20934 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
20935 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
20941 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
20942 });
20943 }
20944 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
20945 crate::gpu::ProbeArm::Gpu => {
20946 let t0 = std::time::Instant::now();
20947 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
20948 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
20949 return out;
20950 }
20951 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
20952 }
20953 crate::gpu::ProbeArm::CpuTimed => {
20954 let t0 = std::time::Instant::now();
20955 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20956 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
20957 return out;
20958 }
20959 crate::gpu::ProbeArm::Cpu => {
20960 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
20961 }
20962 }
20963 }
20964 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
20965}
20966
20967fn moe_ffn_minicpm_tail(
20971 slot: &mut crate::minicpm_tail::MiniCpmTail,
20972 li: usize,
20973 total_layers: usize,
20974 m: &MoeFfn,
20975 x: &[f32],
20976 pool: Option<&Pool>,
20977) -> Vec<f32> {
20978 let route = moe_ffn_route(m, x, pool, None);
20979 match slot.forward(li, m, x, &route, pool, total_layers) {
20980 Some(out) => out,
20981 None => moe_ffn_experts(m, x, &route, pool),
20986 }
20987}
20988
20989fn moe_ffn_banked(
20993 slot: &mut crate::mimo_moe::Slot,
20994 li: usize,
20995 m: &MoeFfn,
20996 x: &[f32],
20997 pool: Option<&Pool>,
20998) -> Vec<f32> {
20999 let t0 = std::time::Instant::now();
21000 let r = moe_ffn_route(m, x, pool, None);
21001 slot.note_route(t0.elapsed().as_nanos() as u64);
21002 match slot.forward(li, m, x, &r, pool) {
21003 Some(out) => out,
21004 None => crate::qtensor::float_activations_scope(|| {
21005 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
21006 }),
21007 }
21008}
21009
21010fn moe_ffn_banked_rows(
21012 slot: &mut crate::mimo_moe::Slot,
21013 li: usize,
21014 m: &MoeFfn,
21015 xs: &[f32],
21016 b: usize,
21017 hidden: usize,
21018 pool: Option<&Pool>,
21019) -> Vec<f32> {
21020 let t0 = std::time::Instant::now();
21021 let routes: Vec<_> = xs
21022 .chunks_exact(hidden)
21023 .map(|x| moe_ffn_route(m, x, pool, None))
21024 .collect();
21025 slot.note_route(t0.elapsed().as_nanos() as u64);
21026 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
21027 return out;
21028 }
21029 let mut out = Vec::with_capacity(b * hidden);
21030 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
21031 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
21032 crate::qtensor::float_activations_scope(|| {
21034 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
21035 })
21036 });
21037 out.extend(row);
21038 }
21039 out
21040}
21041
21042fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
21047 use std::sync::atomic::{AtomicBool, Ordering};
21048 if built {
21049 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
21050 if total_layers > 0 && layers_run < total_layers {
21051 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
21052 } else {
21053 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
21054 }
21055 } else {
21056 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
21057 }
21058 static SAID: AtomicBool = AtomicBool::new(false);
21059 if !SAID.swap(true, Ordering::Relaxed) {
21060 if built {
21061 tracing::info!("wgpu whole-token graph: ACTIVE");
21062 } else {
21063 tracing::warn!("wgpu whole-token graph refused — per-op path");
21064 }
21065 }
21066}
21067
21068pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21072pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21073pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21077pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21079
21080pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21084pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21085pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
21086 std::sync::atomic::AtomicU64::new(0);
21087pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21088pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21089pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
21093 std::sync::atomic::AtomicU64::new(0);
21094pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21095pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
21096 std::sync::atomic::AtomicU64::new(0);
21097pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
21098 std::sync::atomic::AtomicU64::new(0);
21099
21100fn moe_batch_enabled() -> bool {
21103 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
21104 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
21105}
21106
21107fn moe_ffn_cpu_batched(
21113 m: &MoeFfn,
21114 x: &[f32],
21115 idx: &[usize],
21116 p: &[f32],
21117 wsum: f32,
21118 pool: Option<&Pool>,
21119) -> Option<Vec<f32>> {
21120 if idx.is_empty() || !moe_batch_enabled() {
21121 return None;
21122 }
21123 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
21127 return None;
21128 }
21129 let n = idx.len() + usize::from(m.shared.is_some());
21130 let mut pairs = Vec::with_capacity(n);
21131 let mut downs = Vec::with_capacity(n);
21132 let mut ws = Vec::with_capacity(n);
21133 for &e in idx {
21134 let d = &m.experts[e];
21135 if d.act != Act::Silu {
21136 return None;
21137 }
21138 pairs.push((&d.gate_proj, &d.up_proj));
21139 downs.push(&d.down_proj);
21140 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
21141 }
21142 if let Some((se, gate)) = &m.shared {
21145 if se.act != Act::Silu {
21146 return None;
21147 }
21148 let g = gate.as_ref().map_or(1.0, |gate| {
21149 let mut gl = [0.0f32; 1];
21150 gate.matvec(x, &mut gl, pool);
21151 1.0 / (1.0 + (-gl[0]).exp())
21152 });
21153 pairs.push((&se.gate_proj, &se.up_proj));
21154 downs.push(&se.down_proj);
21155 ws.push(g);
21156 }
21157 let inter = pairs[0].0.rows();
21158 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
21159 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
21160 return None;
21161 }
21162 let mut out = attention::take_buf(x.len());
21163 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
21164 attention::recycle_buf(&mut out);
21165 return None;
21166 }
21167 Some(out)
21168}
21169
21170pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21178pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21179pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21180pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21181pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21182
21183pub(crate) fn moe_cold_experts_cpu(
21184 experts: &[(&DenseFfn, f32)],
21185 x: &[f32],
21186 pool: Option<&Pool>,
21187) -> Vec<f32> {
21188 let mut out = attention::take_buf(x.len());
21189 if experts.is_empty() {
21190 return out;
21191 }
21192 let pairs: Vec<_> = experts
21193 .iter()
21194 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
21195 .collect();
21196 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
21197 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
21198 let inter = experts[0].0.gate_proj.rows();
21199 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
21200 let silu_only = experts
21208 .iter()
21209 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
21210 if silu_only
21211 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
21212 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
21213 {
21214 return out;
21215 }
21216 out.fill(0.0);
21217 for &(expert, weight) in experts {
21218 let mut one = dense_ffn(expert, x, pool);
21219 for (o, v) in out.iter_mut().zip(&one) {
21220 *o += weight * v;
21221 }
21222 attention::recycle_buf(&mut one);
21223 }
21224 out
21225}
21226
21227pub(crate) fn moe_cold_experts_rows_cpu(
21231 jobs: &[Vec<(&DenseFfn, f32)>],
21232 xs: &[f32],
21233 hidden: usize,
21234 pool: Option<&Pool>,
21235) -> Vec<f32> {
21236 let mut out = vec![0.0; xs.len()];
21237 let mut experts: Vec<&DenseFfn> = Vec::new();
21238 let mut groups: Vec<Vec<usize>> = Vec::new();
21239 let mut terms = vec![Vec::new(); jobs.len()];
21240 for (r, row) in jobs.iter().enumerate() {
21241 for &(e, w) in row {
21242 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
21243 Some(g) => g,
21244 None => {
21245 experts.push(e);
21246 groups.push(Vec::new());
21247 groups.len() - 1
21248 }
21249 };
21250 terms[r].push((g, groups[g].len(), w));
21251 groups[g].push(r);
21252 }
21253 }
21254 if experts.is_empty() {
21255 return out;
21256 }
21257 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
21258 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
21259 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
21260 let count: usize = lens.iter().sum();
21261 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
21262 let mut ds = vec![vec![0.0; hidden]; count];
21263 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
21264 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
21265 {
21266 let mut offset = 0;
21267 let offsets: Vec<_> = lens
21268 .iter()
21269 .map(|&n| {
21270 let start = offset;
21271 offset += n;
21272 start
21273 })
21274 .collect();
21275 for (r, terms) in terms.iter().enumerate() {
21276 for &(g, slot, w) in terms {
21277 for (o, &v) in out[r * hidden..(r + 1) * hidden]
21278 .iter_mut()
21279 .zip(&ds[offsets[g] + slot])
21280 {
21281 *o += w * v;
21282 }
21283 }
21284 }
21285 } else {
21286 for (r, jobs) in jobs.iter().enumerate() {
21287 if !jobs.is_empty() {
21288 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
21289 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
21290 attention::recycle_buf(&mut row);
21291 }
21292 }
21293 }
21294 out
21295}
21296
21297fn moe_ffn_cpu(
21299 m: &MoeFfn,
21300 x: &[f32],
21301 idx: &[usize],
21302 p: &[f32],
21303 wsum: f32,
21304 pool: Option<&Pool>,
21305) -> Vec<f32> {
21306 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
21307 return out;
21308 }
21309 let mut out = attention::take_buf(x.len());
21310 for &e in idx {
21311 let mut eo = dense_ffn(&m.experts[e], x, pool);
21312 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
21313 for i in 0..out.len() {
21314 out[i] += w * eo[i];
21315 }
21316 attention::recycle_buf(&mut eo);
21317 }
21318 if let Some((se, gate)) = &m.shared {
21319 let mut so = dense_ffn(se, x, pool);
21320 let g = gate.as_ref().map_or(1.0, |gate| {
21321 let mut gl = [0.0f32; 1];
21322 gate.matvec(x, &mut gl, pool);
21323 1.0 / (1.0 + (-gl[0]).exp())
21324 });
21325 for i in 0..out.len() {
21326 out[i] += g * so[i];
21327 }
21328 attention::recycle_buf(&mut so);
21329 }
21330 out
21331}
21332
21333#[allow(clippy::too_many_arguments)]
21341pub(crate) fn mla_attention(
21342 w: &MlaWeights,
21343 normed: &[f32],
21344 cache: &mut crate::kv_cache::LayerKvCache,
21345 position: usize,
21346 inv_freq: &[f32],
21347 rope_scale: f32,
21348 eps: f64,
21349 pool: Option<&Pool>,
21350) -> Vec<f32> {
21351 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
21352 let hd = dr + dn;
21353 let mut q = vec![0.0f32; nh * hd];
21354 match (&w.q_a, &w.q_a_norm) {
21355 (Some(qa), Some(qn)) => {
21356 let mut t = vec![0.0f32; qa.rows()];
21357 qa.matvec(normed, &mut t, pool);
21358 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
21359 w.q_proj.matvec(&tn, &mut q, pool);
21360 }
21361 _ => w.q_proj.matvec(normed, &mut q, pool),
21362 }
21363 let mut ca = vec![0.0f32; lora + dr];
21364 w.kv_a.matvec(normed, &mut ca, pool);
21365 let (c_lat, k_rope) = ca.split_at_mut(lora);
21366 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
21367 let mut kvb = vec![0.0f32; nh * (dn + dv)];
21368 w.kv_b.matvec(&latn, &mut kvb, pool);
21369 if !w.nope {
21370 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
21371 }
21372 for h in 0..nh {
21373 if !w.nope {
21374 attention::rope_rotate_scaled(
21375 &mut q[h * hd..h * hd + dr],
21376 position,
21377 inv_freq,
21378 rope_scale,
21379 );
21380 }
21381 }
21382 let mut k = vec![0.0f32; nh * hd];
21383 let mut v = vec![0.0f32; nh * hd];
21384 for h in 0..nh {
21385 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
21386 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
21387 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
21388 }
21389 cache.append(&k, &v, &vec![true; nh]);
21390 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
21391 attention::recycle_buf(&mut imp);
21392 let mut ov = vec![0.0f32; nh * dv];
21393 for h in 0..nh {
21394 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
21395 }
21396 let mut out = vec![0.0f32; w.o_proj.rows()];
21397 w.o_proj.matvec(&ov, &mut out, pool);
21398 out
21399}
21400
21401fn dense_moe_ffn(
21408 dm: &DenseMoeFfn,
21409 x_normed: &[f32],
21410 h_raw: &[f32],
21411 eps: f64,
21412 norm_style: NormStyle,
21413 pool: Option<&Pool>,
21414) -> Vec<f32> {
21415 let mut d = dense_ffn(&dm.dense, x_normed, pool);
21416 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
21417 let m = &dm.moe;
21418 let ne = m.experts.len();
21419 let mut logits = vec![0.0f32; ne];
21420 if m.router_input_norm {
21421 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
21422 let inv = 1.0 / (ss + eps as f32).sqrt();
21423 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
21424 m.router.matvec(&xr, &mut logits, pool);
21425 } else {
21426 m.router.matvec(h_raw, &mut logits, pool);
21427 }
21428 let (idx, p, wsum) = moe_route(&logits, m, None);
21429 {
21430 let mut st = m.stats.borrow_mut();
21431 if st.len() < ne {
21432 st.resize(ne, 0);
21433 }
21434 for &e in &idx {
21435 st[e] += 1;
21436 }
21437 }
21438 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
21439 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
21440 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
21441 for (di, mi) in d.iter_mut().zip(&mo) {
21442 *di += mi;
21443 }
21444 d
21445}
21446
21447fn moe_gpu_refused(why: &'static str) {
21454 use std::sync::atomic::{AtomicBool, Ordering};
21455 static SAID: AtomicBool = AtomicBool::new(false);
21456 if !SAID.swap(true, Ordering::Relaxed) {
21457 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
21458 }
21459}
21460
21461fn moe_ffn_gpu(
21462 m: &MoeFfn,
21463 x: &[f32],
21464 idx: &[usize],
21465 p: &[f32],
21466 wsum: f32,
21467 pool: Option<&Pool>,
21468) -> Option<Vec<f32>> {
21469 use crate::gpu::MoeJob;
21470
21471 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
21472 let mut model_ref = None;
21473 for &e in idx {
21474 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
21475 moe_gpu_refused("push_job(expert)");
21476 return None;
21477 }
21478 }
21479 if let Some((se, gate)) = &m.shared {
21480 let g = gate.as_ref().map_or(1.0, |gate| {
21481 let mut gl = [0.0f32; 1];
21482 gate.matvec(x, &mut gl, pool);
21483 1.0 / (1.0 + (-gl[0]).exp())
21484 });
21485 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
21486 moe_gpu_refused("push_job(shared)");
21487 return None;
21488 }
21489 }
21490 let Some(model) = model_ref else {
21491 moe_gpu_refused("no model_ref");
21492 return None;
21493 };
21494 let hidden = jobs[0].down.1;
21495 let mut out = vec![0.0f32; hidden];
21496 if crate::gpu::moe_block(&model, &jobs, &mut out) {
21497 Some(out)
21498 } else {
21499 moe_gpu_refused("gpu::moe_block");
21500 None
21501 }
21502}
21503
21504fn ffn_forward(
21506 ffn: &FfnKind,
21507 x: &[f32],
21508 pool: Option<&Pool>,
21509 experts_allowed: Option<&[bool]>,
21510) -> Vec<f32> {
21511 match ffn {
21512 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
21513 FfnKind::Dense(d) => dense_ffn(d, x, pool),
21514 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
21515 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21519 }
21520}
21521
21522fn ffn_forward_pair(
21526 ffn: &FfnKind,
21527 x1: &[f32],
21528 x2: &[f32],
21529 pool: Option<&Pool>,
21530 experts_allowed: Option<&[bool]>,
21531) -> (Vec<f32>, Vec<f32>) {
21532 let d = match ffn {
21533 FfnKind::Dense(d) if !d.segs.is_empty() => {
21536 return (
21537 tube_ffn(d, x1, 1, pool, None),
21538 tube_ffn(d, x2, 1, pool, None),
21539 );
21540 }
21541 FfnKind::Dense(d) => d,
21542 FfnKind::Moe(m) => {
21543 return (
21544 moe_ffn(m, x1, pool, experts_allowed),
21545 moe_ffn(m, x2, pool, experts_allowed),
21546 );
21547 }
21548 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
21549 };
21550 let inter = d.gate_proj.rows();
21551 FFN_SCRATCH.with(|s| {
21552 let mut s = s.borrow_mut();
21553 let [g1, g2, u1, u2] = &mut *s;
21554 g1.resize(inter, 0.0);
21555 g2.resize(inter, 0.0);
21556 u1.resize(inter, 0.0);
21557 u2.resize(inter, 0.0);
21558 QTensor::matvec2_many(
21561 [&d.gate_proj, &d.up_proj],
21562 x1,
21563 x2,
21564 [g1.as_mut_slice(), u1.as_mut_slice()],
21565 [g2.as_mut_slice(), u2.as_mut_slice()],
21566 pool,
21567 );
21568 for i in 0..inter {
21569 g1[i] = d.act.combine(g1[i], u1[i]);
21570 g2[i] = d.act.combine(g2[i], u2[i]);
21571 }
21572 let mut o1 = attention::take_buf(d.down_proj.rows());
21573 let mut o2 = attention::take_buf(d.down_proj.rows());
21574 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
21575 (o1, o2)
21576 })
21577}
21578
21579#[cfg(test)]
21580mod tests {
21581 use std::ffi::OsString;
21582 use std::sync::Mutex;
21583
21584 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
21587
21588 struct ScopedTestEnv {
21589 previous: Vec<(&'static str, Option<OsString>)>,
21590 }
21591
21592 impl ScopedTestEnv {
21593 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
21594 let previous = vars
21595 .iter()
21596 .map(|(name, _)| (*name, std::env::var_os(name)))
21597 .collect();
21598 for &(name, value) in vars {
21599 unsafe {
21600 match value {
21601 Some(value) => std::env::set_var(name, value),
21602 None => std::env::remove_var(name),
21603 }
21604 }
21605 }
21606 Self { previous }
21607 }
21608 }
21609
21610 impl Drop for ScopedTestEnv {
21611 fn drop(&mut self) {
21612 for (name, value) in self.previous.drain(..).rev() {
21613 unsafe {
21614 match value {
21615 Some(value) => std::env::set_var(name, value),
21616 None => std::env::remove_var(name),
21617 }
21618 }
21619 }
21620 }
21621 }
21622
21623 #[test]
21624 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
21625 use super::pad_mtp_draft_logits;
21626
21627 let mut logits = vec![-3.0, -1.0, -2.0];
21628 pad_mtp_draft_logits(&mut logits, 6);
21629 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
21630 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
21631 assert_eq!(
21632 logits
21633 .iter()
21634 .enumerate()
21635 .max_by(|(_, a), (_, b)| a.total_cmp(b))
21636 .map(|(i, _)| i),
21637 Some(1),
21638 "an unevaluated row cannot win an all-negative shortlist"
21639 );
21640 }
21641
21642 #[test]
21643 fn speculative_trial_fast_exits_after_two_zero_acceptance_rounds() {
21644 use super::{Pipeline, SpecMon, SpecTrial};
21645
21646 let mut mon = SpecMon::default();
21647 let start = std::time::Instant::now();
21648 mon.round(1.0, 1);
21649 let first = Pipeline::spec_trial_round(
21650 SpecTrial::Spec {
21651 t0: start,
21652 gen0: 0,
21653 rounds: 0,
21654 },
21655 &mut mon,
21656 1,
21657 );
21658 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
21659
21660 mon.round(1.0, 1);
21661 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
21662 assert!(
21663 matches!(second, SpecTrial::Plain { gen0: 2, .. }),
21664 "two verifier-tip-only rounds must stop the losing MTP trial"
21665 );
21666
21667 let mut viable = SpecMon::default();
21670 viable.round(1.0, 1);
21671 viable.round(1.0, 2);
21672 let continued = Pipeline::spec_trial_round(
21673 SpecTrial::Spec {
21674 t0: start,
21675 gen0: 0,
21676 rounds: 1,
21677 },
21678 &mut viable,
21679 3,
21680 );
21681 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
21682 }
21683
21684 #[test]
21685 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
21686 let mut p = spark_test_pipeline(None);
21687 let head_rows = 248_070;
21688 p.draft_full_streak = 7;
21689 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21690
21691 p.reset_fresh_mtp_draft_state();
21692 assert_eq!(
21693 p.draft_head_rows(head_rows),
21694 super::Pipeline::draft_vocab_rows(head_rows),
21695 "a fresh request returns to its configured shortlist"
21696 );
21697
21698 p.note_draft_id(u32::MAX);
21701 assert_eq!(p.draft_head_rows(head_rows), head_rows);
21702 }
21703
21704 #[test]
21705 fn native_metal_batch_spec_requires_exact_explicit_opt_in() {
21706 use super::graph_spec_wanted_by_policy;
21707
21708 assert!(!graph_spec_wanted_by_policy(None, true, false, false, true));
21712 assert!(!graph_spec_wanted_by_policy(
21713 Some("anything-but-1"),
21714 true,
21715 false,
21716 false,
21717 true,
21718 ));
21719 assert!(graph_spec_wanted_by_policy(
21720 Some("1"),
21721 false,
21722 true,
21723 false,
21724 true,
21725 ));
21726 assert!(!graph_spec_wanted_by_policy(
21727 Some("0"),
21728 true,
21729 false,
21730 false,
21731 true,
21732 ));
21733
21734 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
21737 assert!(graph_spec_wanted_by_policy(
21738 Some("legacy-force"),
21739 false,
21740 true,
21741 true,
21742 false,
21743 ));
21744 }
21745
21746 #[test]
21747 fn encoder_hidden_states_are_normalized_and_reusable() {
21748 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
21749 let ids = [1, 7, 11, 19];
21750
21751 let first = p.encode_hidden_states(&ids).expect("encoder forward");
21752 let second = p
21753 .encode_hidden_states(&ids)
21754 .expect("reusable encoder forward");
21755 assert_eq!(first.len(), ids.len() * 8);
21756 assert!(first.iter().all(|v| v.is_finite()));
21757 assert_eq!(
21758 first, second,
21759 "encoder forward must start from a fresh state"
21760 );
21761
21762 assert!(p.forward_ids(&ids, None).is_ok());
21765 }
21766
21767 #[test]
21772 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
21773 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
21774 let dense_card = ChunkStackFacts {
21775 plain_dense: true,
21776 discrete: true,
21777 gpu_on: true,
21778 ..Default::default()
21779 };
21780 assert!(dense_card.dense_on_discrete());
21781 assert_eq!(
21783 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
21784 DISCRETE_DENSE_PREFILL_CHUNK
21785 );
21786 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
21787 for (label, facts) in [
21788 (
21789 "GDN hybrid / MoE / DeepSeek stack",
21790 ChunkStackFacts {
21791 plain_dense: false,
21792 ..dense_card
21793 },
21794 ),
21795 (
21796 "integrated GPU",
21797 ChunkStackFacts {
21798 discrete: false,
21799 ..dense_card
21800 },
21801 ),
21802 (
21803 "CPU only",
21804 ChunkStackFacts {
21805 gpu_on: false,
21806 discrete: false,
21807 ..dense_card
21808 },
21809 ),
21810 (
21811 "capacity split",
21812 ChunkStackFacts {
21813 capacity_split: true,
21814 ..dense_card
21815 },
21816 ),
21817 (
21818 "multi-GPU plan",
21819 ChunkStackFacts {
21820 multi_gpu: true,
21821 ..dense_card
21822 },
21823 ),
21824 (
21825 "O(1) layers",
21826 ChunkStackFacts {
21827 o1: true,
21828 ..dense_card
21829 },
21830 ),
21831 ] {
21832 assert!(!facts.dense_on_discrete(), "{label}");
21833 assert_eq!(
21834 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
21835 48,
21836 "{label} keeps the historical x86 chunk"
21837 );
21838 }
21839 for dense in [false, true] {
21841 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
21842 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
21843 }
21844 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
21846 for dense in [false, true] {
21847 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
21848 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
21849 }
21850 }
21851 }
21852
21853 #[test]
21854 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
21855 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
21856 let full = |host_rows, device_rows| ReuseLayer {
21857 full: true,
21858 host_rows,
21859 device_rows,
21860 device_state: false,
21861 };
21862 assert_eq!(
21865 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
21866 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
21867 );
21868 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
21870 assert_eq!(
21872 kv_reuse_plan(339, &[full(339, Some(345))]),
21873 ReusePlan::Ready
21874 );
21875 assert_eq!(
21877 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
21878 ReusePlan::Pull(vec![(0, 300, 339)])
21879 );
21880 assert_eq!(
21882 kv_reuse_plan(339, &[full(300, Some(320))]),
21883 ReusePlan::Fresh
21884 );
21885 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
21886 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
21887 let conv = |device_state| ReuseLayer {
21890 full: false,
21891 host_rows: 0,
21892 device_rows: None,
21893 device_state,
21894 };
21895 assert_eq!(
21896 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
21897 ReusePlan::Fresh
21898 );
21899 assert_eq!(
21900 kv_reuse_plan(339, &[conv(false), full(339, None)]),
21901 ReusePlan::Ready
21902 );
21903 }
21904
21905 #[test]
21906 fn nll_graph_policy_scopes_only_the_fused_head() {
21907 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
21908 ("vulkan graph", true, true, false, true, false),
21910 ("native Metal graph", true, true, true, true, true),
21912 ("masked", false, true, false, false, false),
21914 ("graph disabled", true, false, true, false, false),
21915 ] {
21916 let (graph_quality, graph_head_required) =
21917 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
21918 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
21919 assert_eq!(graph_head_required, want_head, "{label}: fused head");
21920 }
21921 }
21922
21923 #[test]
21924 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
21925 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
21926 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
21927 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
21928 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
21929 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
21930 }
21931
21932 #[test]
21933 fn cancel_flag_stops_generation() {
21934 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
21935 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21938 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
21939 assert_eq!(r.finish_reason, "cancelled");
21940 assert!(
21941 r.token_ids.is_empty(),
21942 "no tokens after cancel: {:?}",
21943 r.token_ids
21944 );
21945 assert_eq!(p.kv_cache.seq_len(), 0);
21946 assert!(p.kv_history.is_empty());
21947 assert!(!p.graph_want_logits);
21948 assert!(p.graph_logits.is_none());
21949 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
21951 assert_ne!(r2.finish_reason, "cancelled");
21952 }
21953 use super::*;
21954
21955 #[test]
21956 fn minimax_m3_arch_selects_checked_oai_activation_only() {
21957 let arch = |arch_name: &str, hidden_act: &str| {
21961 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
21962 "arch_name": arch_name,
21963 "hidden_size": 8,
21964 "intermediate_size": 16,
21965 "num_layers": 1,
21966 "num_attention_heads": 1,
21967 "num_kv_heads": 1,
21968 "head_dim": 8,
21969 "vocab_size": 16,
21970 "layer_types": ["FullAttention"],
21971 "rms_norm_eps": 1e-6,
21972 "max_position_embeddings": 16,
21973 "hidden_act": hidden_act
21974 }))
21975 .expect("minimal architecture")
21976 };
21977 let minimax = arch("minimax_m3_vl", "swigluoai");
21978 assert_eq!(
21979 Act::from_arch_full(&minimax),
21980 Act::SwiGluOai {
21981 alpha: 1.702,
21982 limit: 7.0,
21983 }
21984 );
21985
21986 let unrelated = arch("qwen2", "swigluoai");
21989 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
21990
21991 let non_oai = arch("minimax_m3_vl", "gelu");
21995 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
21996 }
21997
21998 #[test]
21999 fn minimax_swiglu_oai_act_matches_scalar_contract() {
22000 let act = Act::swiglu_oai(1.702, 7.0);
22001 let got = act.combine(10.0, -10.0);
22002 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
22003 assert!((got - expected).abs() < 1e-6);
22004 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
22005 assert_eq!(
22006 act.wgpu_graph_act(),
22007 Some(crate::gpu::GraphAct::SwiGluOaiM3),
22008 "the WGPU-only arm accepts only the frozen M3 constants"
22009 );
22010 assert_eq!(
22011 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
22012 None,
22013 "nearby OAI constants must not borrow MiniMax's kernel"
22014 );
22015 assert_eq!(
22016 act.moe_job_graph_contract(),
22017 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
22018 "the direct MoE path carries the frozen clamp with the marker"
22019 );
22020 assert_eq!(
22021 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
22022 None,
22023 "a nearby clamp must not borrow MiniMax's device contract"
22024 );
22025
22026 let gate_only = act.apply(-10.0);
22029 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
22030 assert!((gate_only - expected_gate).abs() < 1e-10);
22031 }
22032
22033 #[test]
22034 fn explicit_sliding_schedule_selects_local_rope_profile() {
22035 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
22040 p.swa = Some((1024, usize::MAX));
22041 p.sliding_layers = Some(vec![true, true, true, false]);
22042 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
22043 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
22044 p.rope_scale = 1.277_258_9;
22045 p.rope_scale_local = 1.0;
22046
22047 for li in 0..3 {
22048 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
22049 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
22050 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
22051 }
22052 assert_eq!(p.layer_window(3), None);
22053 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
22054 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
22055 }
22056
22057 #[test]
22065 fn dynamic_ffn_equals_the_zeroing_arm() {
22066 let (hidden, inter) = (8usize, 32usize);
22067 let synth = |n: usize, salt: usize| -> Vec<f32> {
22068 (0..n)
22069 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
22070 .collect()
22071 };
22072 let down = synth(hidden * inter, 3);
22073 let mut down_t = vec![0.0f32; inter * hidden];
22074 for r in 0..hidden {
22075 for c in 0..inter {
22076 down_t[c * hidden + r] = down[r * inter + c];
22077 }
22078 }
22079 let d = DenseFfn {
22080 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22081 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22082 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
22083 act: Act::Silu,
22084 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
22085 segs: Vec::new(),
22086 };
22087 let x = synth(hidden, 11);
22088 let k = 12usize;
22089 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
22090 let mut g = vec![0.0f32; inter];
22092 d.gate_proj.matvec(&x, &mut g, None);
22093 let mut u = vec![0.0f32; inter];
22094 d.up_proj.matvec(&x, &mut u, None);
22095 for v in g.iter_mut() {
22096 *v = inference::silu(*v);
22097 }
22098 keep_top_k(&mut g, k);
22099 for i in 0..inter {
22100 g[i] *= u[i];
22101 }
22102 let mut want = vec![0.0f32; hidden];
22103 d.down_proj.matvec(&g, &mut want, None);
22104 for (a, b) in want.iter().zip(&got) {
22105 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
22106 }
22107 }
22108
22109 #[test]
22115 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
22116 let (hidden, core, tube) = (8usize, 12usize, 8usize);
22117 let inter = core + tube;
22118 let synth = |n: usize, salt: usize| -> Vec<f32> {
22119 (0..n)
22120 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
22121 .collect()
22122 };
22123 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
22124 let d_all = synth(hidden * inter, 3);
22125 let dense = DenseFfn {
22127 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
22128 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
22129 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
22130 act: Act::Silu,
22131 down_t: None,
22132 segs: Vec::new(),
22133 };
22134 let rows =
22135 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
22136 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
22137 let mut o = Vec::with_capacity(hidden * (b - a));
22138 for r in 0..hidden {
22139 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
22140 }
22141 o
22142 };
22143 let tubed = DenseFfn {
22144 down_t: None,
22145 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
22146 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
22147 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
22148 act: Act::Silu,
22149 segs: vec![FfnSeg {
22150 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
22151 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
22152 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
22153 start: core,
22154 width: tube,
22155 }],
22156 };
22157 let x = synth(hidden, 7);
22158 let want = dense_ffn(&dense, &x, None);
22159 let got = tube_ffn(&tubed, &x, 1, None, None);
22160 for (a, b) in want.iter().zip(&got) {
22161 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
22162 }
22163 let mut bits = vec![0u8; inter.div_ceil(8)];
22165 for n in 0..core {
22166 bits[n / 8] |= 1 << (n % 8);
22167 }
22168 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22169 let masked = dense_ffn_masked(&dense, &x, None, &bits);
22170 for (a, b) in masked.iter().zip(&closed) {
22171 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
22172 }
22173 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
22175 for (a, b) in closed.iter().zip(&batch) {
22176 assert_eq!(a, b, "batch arm disagrees with decode arm");
22177 }
22178 }
22179
22180 #[test]
22182 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
22183 let (hidden, inter) = (16usize, 40usize);
22184 let synth = |n: usize, salt: usize| -> Vec<f32> {
22185 (0..n)
22186 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
22187 .collect()
22188 };
22189 let d = DenseFfn {
22190 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
22191 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
22192 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
22193 act: Act::Silu,
22194 down_t: None,
22195 segs: Vec::new(),
22196 };
22197 let x = synth(hidden, 9);
22198 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
22200
22201 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
22202
22203 let mut g = vec![0.0f32; inter];
22205 d.gate_proj.matvec(&x, &mut g, None);
22206 let mut u = vec![0.0f32; inter];
22207 d.up_proj.matvec(&x, &mut u, None);
22208 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
22209 for i in 0..inter {
22210 g[i] = if act_set.contains(&(i as u16)) {
22211 inference::silu(g[i]) * u[i]
22212 } else {
22213 0.0
22214 };
22215 }
22216 let mut reference = vec![0.0f32; hidden];
22217 d.down_proj.matvec(&g, &mut reference, None);
22218
22219 let max_d = sparse
22220 .iter()
22221 .zip(&reference)
22222 .map(|(a, b)| (a - b).abs())
22223 .fold(0.0f32, f32::max);
22224 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
22225 }
22226
22227 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
22230 let (h, inter, heads, kv, hd) = (
22231 p.hidden_size,
22232 inter,
22233 p.num_heads,
22234 p.num_kv_heads,
22235 p.head_dim,
22236 );
22237 let synth = |n: usize, salt: usize| -> Vec<f32> {
22238 (0..n)
22239 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
22240 .collect()
22241 };
22242 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
22243 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22244 };
22245 p.mtp = Some(MtpModule {
22246 enorm: vec![1.0; h],
22247 hnorm: vec![1.0; h],
22248 eh_proj: qt(h, 2 * h, 301),
22249 layer: LayerWeights {
22250 input_norm: vec![1.0; h],
22251 post_norm: vec![1.0; h],
22252 attn_out_norm: None,
22253 ffn_out_norm: None,
22254 layer_scale: None,
22255 ffn: FfnKind::Dense(DenseFfn {
22256 gate_proj: qt(inter, h, 315),
22257 up_proj: qt(inter, h, 316),
22258 down_proj: qt(h, inter, 317),
22259 act: Act::Silu,
22260 down_t: None,
22261 segs: Vec::new(),
22262 }),
22263 attn: AttnKind::Full {
22264 bias: None,
22265 wq: qt(heads * hd, h, 311),
22266 wk: qt(kv * hd, h, 312),
22267 wv: qt(kv * hd, h, 313),
22268 wo: qt(h, heads * hd, 314),
22269 q_norm: None,
22270 k_norm: None,
22271 output_gate: false,
22272 softplus_gate: None,
22273 },
22274 },
22275 final_norm: vec![1.0; h],
22276 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
22277 });
22278 }
22279
22280 fn attach_test_mtp(p: &mut Pipeline) {
22282 attach_test_mtp_width(p, p.intermediate_size);
22283 }
22284
22285 #[test]
22286 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
22287 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
22288 attach_test_mtp_width(&mut p, 11);
22291 let ids = [3, 9, 4, 12, 7, 2];
22292 let trunk = p.probe_ffn_mass(&ids);
22293 assert_eq!(trunk[0].len(), p.intermediate_size);
22294
22295 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
22296 assert_eq!(got.len(), 1);
22297 assert_eq!(got[0].mass.len(), 11);
22298 assert_eq!(got[0].squared_mass.len(), 11);
22299 assert_eq!(got[0].observed_positions, ids.len() - 1);
22300 assert!(got[0].mass.iter().any(|&v| v > 0.0));
22301 assert!(got[0]
22302 .mass
22303 .iter()
22304 .zip(&got[0].squared_mass)
22305 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
22306 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
22307 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
22308
22309 let h = p.hidden_size;
22313 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
22314 panic!("synthetic MTP must be dense")
22315 };
22316 dense.segs.push(FfnSeg {
22317 gate: QTensor::from_f32(vec![0.0; h], 1, h),
22318 up: QTensor::from_f32(vec![0.0; h], 1, h),
22319 down: QTensor::from_f32(vec![0.0; h], h, 1),
22320 start: dense.gate_proj.rows(),
22321 width: 1,
22322 });
22323 let err = p.probe_mtp_ffn(&ids);
22324 assert!(err
22325 .unwrap_err()
22326 .contains("tube-segmented draft FFNs are not supported"));
22327 assert_eq!(p.kv_cache.seq_len(), 0);
22328 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
22329 }
22330
22331 #[test]
22332 fn speculative_equals_vanilla_greedy() {
22333 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22334 let _env = ScopedTestEnv::set(&[
22337 ("CMF_GPU", Some("0")),
22338 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22339 ("CMF_MTP_PAIR", Some("1")),
22340 ]);
22341 let run = |spec: bool| {
22342 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22343 p.sampler_config.temperature = 0.0;
22344 attach_test_mtp(&mut p);
22345 p.speculative = spec;
22346 let r = p.generate("abcdef", 12, None, None).unwrap();
22347 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22348 };
22349 let (vanilla, d0, _) = run(false);
22350 let (spec, d1, a1) = run(true);
22351 assert_eq!(d0, 0, "vanilla path must not draft");
22352 assert!(d1 > 0, "speculative path must draft");
22353 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
22354 assert_eq!(
22355 vanilla, spec,
22356 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
22357 );
22358 }
22359
22360 #[test]
22361 fn speculative_accepts_constant_oracle() {
22362 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22363 let _env = ScopedTestEnv::set(&[
22364 ("CMF_GPU", Some("0")),
22365 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22366 ("CMF_MTP_PAIR", Some("1")),
22367 ]);
22368 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22369 p.sampler_config.temperature = 0.0;
22370 p.sampler_config.repetition_penalty = 1.0;
22371 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
22374 attach_test_mtp(&mut p);
22375 p.speculative = true;
22376 let r = p.generate("abcd", 10, None, None).unwrap();
22377 assert!(r.mtp_drafted > 0);
22378 assert_eq!(
22379 r.mtp_accepted, r.mtp_drafted,
22380 "constant logits → every draft accepted"
22381 );
22382 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
22385 }
22386
22387 #[test]
22388 fn host_fused_mtp_pair_is_opt_in() {
22389 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
22390 let _env = ScopedTestEnv::set(&[
22391 ("CMF_GPU", Some("0")),
22392 ("CMF_GPU_WGPU_GRAPH", Some("0")),
22393 ("CMF_MTP_PAIR", None),
22394 ]);
22395 let run = |spec: bool| {
22396 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22397 p.sampler_config.temperature = 0.0;
22398 attach_test_mtp(&mut p);
22399 p.speculative = spec;
22400 let r = p.generate("abcdef", 12, None, None).unwrap();
22401 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
22402 };
22403 let (plain, d0, _) = run(false);
22404 let (guarded, d1, a1) = run(true);
22405 assert_eq!(d0, 0);
22406 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
22407 assert_eq!(a1, 0);
22408 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
22409 }
22410
22411 #[test]
22412 fn empty_prompt_is_an_error_not_a_panic() {
22413 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22414 let r = p.generate("", 4, None, None);
22415 assert!(r.is_err(), "empty prompt must be a clean error");
22416 }
22417
22418 #[test]
22419 fn every_token_enters_kv_exactly_once() {
22420 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22421 p.sampler_config.temperature = 0.0;
22423 let r = p.generate("abc", 2, None, None).unwrap();
22424 assert_eq!(r.prompt_tokens, 3);
22425 assert_eq!(
22429 p.kv_cache.seq_len(),
22430 3 + r.tokens_generated - 1,
22431 "each token must be cached exactly once (v1 cached the last prompt token twice)"
22432 );
22433 }
22434
22435 #[test]
22436 fn generation_is_reproducible_with_seed() {
22437 let run = || {
22438 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22439 p.generate("hello", 8, None, None).unwrap().token_ids
22440 };
22441 assert_eq!(run(), run());
22442 }
22443
22444 #[test]
22445 fn resetting_sampler_restarts_the_seeded_stream() {
22446 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
22447 let config = SamplerConfig {
22448 seed: Some(1234),
22449 ..SamplerConfig::default()
22450 };
22451 p.set_sampler_config(config.clone());
22452 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
22453 p.set_sampler_config(config);
22454 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
22455 assert_eq!(first, second);
22456 }
22457
22458 #[test]
22459 fn eviction_bounds_the_cache() {
22460 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
22461 p.kv_cache.max_seq_len = 6;
22462 p.sampler_config.temperature = 0.0;
22463 let _ = p.generate("abcd", 12, None, None).unwrap();
22464 assert!(
22465 p.kv_cache.seq_len() <= 6 + 1,
22466 "cache must stay bounded by max_seq_len (got {})",
22467 p.kv_cache.seq_len()
22468 );
22469 }
22470
22471 #[test]
22472 fn confidence_matches_tokens_and_is_a_probability() {
22473 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22474 p.sampler_config.temperature = 0.0;
22475 p.sampler_config.repetition_penalty = 1.0;
22476 let r = p.generate("abcd", 10, None, None).unwrap();
22477 assert_eq!(
22478 r.token_confidence.len(),
22479 r.token_ids.len(),
22480 "one confidence per emitted token"
22481 );
22482 for &c in &r.token_confidence {
22483 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
22484 }
22485 let logits = [1.0f32, 3.0, 0.5, 3.0];
22487 let p0 = top1_prob_t(&logits, 1, 1.0);
22488 let p1 = top1_prob_t(&logits, 3, 1.0);
22489 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
22490 assert!(p0 > 0.0 && p0 < 1.0);
22491 let sharp = top1_prob_t(&logits, 1, 1.0);
22493 let soft = top1_prob_t(&logits, 1, 2.0);
22494 assert!(soft < sharp, "higher temperature lowers peak confidence");
22495 }
22496
22497 #[test]
22498 fn trace_is_opt_in_and_parallels_the_output() {
22499 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22501 p.sampler_config.temperature = 0.0;
22502 p.sampler_config.repetition_penalty = 1.0;
22503 let r = p.generate("abcd", 10, None, None).unwrap();
22504 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
22505
22506 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22508 p.sampler_config.temperature = 0.0;
22509 p.sampler_config.repetition_penalty = 1.0;
22510 p.set_trace(true);
22511 let r = p.generate("abcd", 10, None, None).unwrap();
22512 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
22513 for (i, tr) in r.traces.iter().enumerate() {
22514 assert_eq!(tr.t, i, "trace index is sequential");
22515 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
22516 assert_eq!(
22517 tr.confidence, r.token_confidence[i],
22518 "trace confidence matches the confidence channel"
22519 );
22520 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
22522 }
22523 }
22524
22525 #[test]
22526 fn explain_prefill_logits_match_greedy_first_token() {
22527 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
22531 p.sampler_config.temperature = 0.0;
22532 p.sampler_config.repetition_penalty = 1.0;
22533 let ids = p.tokenizer.encode("abcd");
22534 let logits = p.prefill_next_logits(&ids, None);
22535 let argmax = logits
22536 .iter()
22537 .enumerate()
22538 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
22539 .unwrap()
22540 .0 as u32;
22541 let r = p.generate("abcd", 1, None, None).unwrap();
22542 assert_eq!(
22543 argmax, r.token_ids[0],
22544 "explain preview must match greedy emit"
22545 );
22546 }
22547
22548 #[test]
22549 fn laguna_shared_expert_is_unconditionally_added() {
22550 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
22551 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
22552 let zero_dense = || DenseFfn {
22553 gate_proj: matrix(vec![0.0; 4]),
22554 up_proj: matrix(vec![0.0; 4]),
22555 down_proj: matrix(vec![0.0; 4]),
22556 act: Act::Silu,
22557 down_t: None,
22558 segs: Vec::new(),
22559 };
22560 let shared = DenseFfn {
22561 gate_proj: identity(),
22562 up_proj: identity(),
22563 down_proj: identity(),
22564 act: Act::Silu,
22565 down_t: None,
22566 segs: Vec::new(),
22567 };
22568 let x = [1.0, 2.0];
22569 let expected = dense_ffn(&shared, &x, None);
22570 let moe = MoeFfn {
22571 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
22572 experts: vec![zero_dense()],
22573 top_k: 1,
22574 norm_topk_prob: true,
22575 router_sigmoid: true,
22576 sigmoid_denom_eps: 1e-6,
22577 expert_bias: None,
22578 routed_scaling: 1.0,
22579 route_tau: None,
22580 shared: Some((shared, None)),
22581 stats: std::cell::RefCell::new(Vec::new()),
22582 act_sq: std::cell::RefCell::new(Vec::new()),
22583 act_rows: std::cell::RefCell::new(Vec::new()),
22584 mask: None,
22585 per_expert_scale: None,
22586 router_input_norm: false,
22587 resonance: None,
22588 grown: Vec::new(),
22589 };
22590 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
22591 for (actual, expected) in actual.iter().zip(expected) {
22592 assert!((actual - expected).abs() < 1e-6);
22593 }
22594 }
22595
22596 fn mimo_test_pipeline() -> Pipeline {
22605 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
22606 let kvh = [1usize, 2, 2, 1];
22607 let synth = |n: usize, salt: usize| -> Vec<f32> {
22608 (0..n)
22609 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
22610 .collect()
22611 };
22612 let qt = |rows: usize, cols: usize, salt: usize| {
22613 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
22614 };
22615 let dense = |inter: usize, salt: usize| DenseFfn {
22616 gate_proj: qt(inter, hs, salt),
22617 up_proj: qt(inter, hs, salt + 1),
22618 down_proj: qt(hs, inter, salt + 2),
22619 act: Act::Silu,
22620 down_t: None,
22621 segs: Vec::new(),
22622 };
22623 let layers: Vec<LayerWeights> = (0..4)
22624 .map(|li| LayerWeights {
22625 input_norm: vec![1.0; hs],
22626 post_norm: vec![1.0; hs],
22627 attn_out_norm: None,
22628 ffn_out_norm: None,
22629 layer_scale: None,
22630 ffn: if li == 0 {
22631 FfnKind::Dense(dense(inter, 50))
22632 } else {
22633 FfnKind::Moe(MoeFfn {
22634 router: qt(4, hs, 60 + li),
22635 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
22636 top_k: 2,
22637 norm_topk_prob: true,
22638 router_sigmoid: true,
22639 sigmoid_denom_eps: 1e-6,
22640 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
22641 routed_scaling: 1.0,
22642 route_tau: None,
22643 shared: None,
22644 stats: std::cell::RefCell::new(Vec::new()),
22645 act_sq: std::cell::RefCell::new(Vec::new()),
22646 act_rows: std::cell::RefCell::new(Vec::new()),
22647 mask: None,
22648 per_expert_scale: None,
22649 router_input_norm: false,
22650 resonance: None,
22651 grown: Vec::new(),
22652 })
22653 },
22654 attn: AttnKind::Full {
22655 wq: qt(nh * hd, hs, li * 10 + 1),
22656 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
22657 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
22658 wo: qt(hs, nh * vd, li * 10 + 4),
22659 q_norm: None,
22660 k_norm: None,
22661 output_gate: false,
22662 softplus_gate: None,
22663 bias: None,
22664 },
22665 })
22666 .collect();
22667 let mut p = Pipeline::new(
22668 Tokenizer::byte_level(),
22669 PipelineWeights {
22670 embed_tokens: qt(vocab, hs, 100),
22671 layers,
22672 lm_head: qt(vocab, hs, 200),
22673 final_norm: vec![1.0; hs],
22674 },
22675 hs,
22676 inter,
22677 nh,
22678 1, hd,
22680 4,
22681 4,
22682 false,
22683 vocab,
22684 1e-6,
22685 1e7,
22686 NormStyle::Qwen,
22687 4096,
22688 SamplerConfig {
22689 seed: Some(7),
22690 ..Default::default()
22691 },
22692 );
22693 p.layer_dump = None;
22695 p.set_rotary(4, 1e7);
22696 p.sliding_layers = Some(vec![false, true, true, false]);
22697 p.swa = Some((3, usize::MAX));
22698 p.rotary_dim_local = Some(4);
22699 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
22700 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
22701 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
22702 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
22703 p
22704 }
22705
22706 #[test]
22707 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
22708 let mut p = mimo_test_pipeline();
22709 p.speculative = false;
22710 p.ignore_eos = true;
22711 p.sampler_config.temperature = 0.0;
22712 p.sampler_config.repetition_penalty = 1.0;
22713 let a = vec![3, 5, 7, 9, 11, 13];
22714 let b = vec![4, 8, 12, 16, 20, 24];
22715 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
22716 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
22717 let actual = p
22721 .generate_from_embeds(&a, &rows, 8, None, None)
22722 .unwrap()
22723 .token_ids;
22724 assert_eq!(actual, expected);
22725 assert!(p.kv_history.is_empty());
22726 let mut extended = a.clone();
22727 extended.push(17);
22728 let after_media = p
22729 .generate_from_ids(&extended, 8, None, None)
22730 .unwrap()
22731 .token_ids;
22732 p.reset_session();
22733 let fresh = p
22734 .generate_from_ids(&extended, 8, None, None)
22735 .unwrap()
22736 .token_ids;
22737 assert_eq!(after_media, fresh);
22738 assert!(
22739 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
22740 .is_err()
22741 );
22742 p.reset_session();
22746 p.generate_from_ids(&a, 1, None, None).unwrap();
22747 let mut media_ids = p.kv_history.clone();
22748 assert!(!media_ids.is_empty());
22749 media_ids.extend_from_slice(&[19, 21, 23]);
22750 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
22751 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
22752 let mut oracle = mimo_test_pipeline();
22753 oracle.speculative = false;
22754 oracle.ignore_eos = true;
22755 oracle.sampler_config.temperature = 0.0;
22756 oracle.sampler_config.repetition_penalty = 1.0;
22757 let expected = oracle
22758 .generate_from_ids(&source_ids, 8, None, None)
22759 .unwrap()
22760 .token_ids;
22761 assert_eq!(
22762 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
22763 .unwrap()
22764 .token_ids,
22765 expected
22766 );
22767 assert!(p.kv_history.is_empty());
22768 let mut bad = rows;
22769 bad[0] = f32::NAN;
22770 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
22771 }
22772
22773 fn f32_bits(v: &[f32]) -> Vec<u32> {
22774 v.iter().map(|x| x.to_bits()).collect()
22775 }
22776
22777 #[test]
22784 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
22785 let mut p = mimo_test_pipeline();
22786 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
22787 assert_eq!(kv, vec![1, 2, 2, 1]);
22788 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
22789 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
22790 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
22791 let hs = p.hidden_size;
22792 let mut decode = Vec::new();
22793 for (pos, &id) in ids.iter().enumerate() {
22794 let e = p.embed_single(id);
22795 let h = p.forward_layers(&e, pos, None);
22796 decode.push(p.logits_from_hidden(&h));
22797 }
22798 for l in &p.kv_cache.layers {
22799 assert_eq!(l.seq_len, 12);
22800 assert_eq!(l.head_values(0).len(), 12 * 8);
22802 }
22803 assert!(decode.iter().flatten().all(|v| v.is_finite()));
22804
22805 p.clear_sequence_state();
22806 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22807 for pos in 0..ids.len() {
22808 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
22809 assert_eq!(
22810 f32_bits(&decode[pos]),
22811 f32_bits(&lg),
22812 "whole prompt, pos {pos}"
22813 );
22814 }
22815
22816 p.clear_sequence_state();
22817 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
22818 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
22819 for pos in 0..ids.len() {
22820 let row = if pos < 5 {
22821 &a[pos * hs..(pos + 1) * hs]
22822 } else {
22823 &b[(pos - 5) * hs..(pos - 4) * hs]
22824 };
22825 let lg = p.logits_from_hidden(row);
22826 assert_eq!(
22827 f32_bits(&decode[pos]),
22828 f32_bits(&lg),
22829 "two chunks, pos {pos}"
22830 );
22831 }
22832
22833 let last = |p: &mut Pipeline| {
22836 p.clear_sequence_state();
22837 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
22838 p.logits_from_hidden(&hb[11 * hs..12 * hs])
22839 };
22840 let base = last(&mut p);
22841 let mut no_sinks = mimo_test_pipeline();
22842 for l in &mut no_sinks.kv_cache.layers {
22843 l.sinks = None;
22844 }
22845 assert_ne!(
22846 f32_bits(&last(&mut no_sinks)),
22847 f32_bits(&base),
22848 "sinks are live"
22849 );
22850 let mut wide = mimo_test_pipeline();
22851 wide.swa = Some((64, usize::MAX));
22852 assert_ne!(
22853 f32_bits(&last(&mut wide)),
22854 f32_bits(&base),
22855 "window is live"
22856 );
22857
22858 p.clear_sequence_state();
22860 p.ignore_eos = true;
22861 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
22862 assert_eq!(r.token_ids.len(), 4);
22863 }
22864
22865 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
22868 let (hs, nh) = (16usize, 4usize);
22869 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
22870 p.layer_dump = None;
22871 p.swa = Some((6, 4));
22872 p.proj_gate_sigmoid = true;
22873 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
22874 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
22875 let g: Vec<f32> = (0..nh * hs)
22876 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
22877 .collect();
22878 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
22879 }
22880 }
22881 p.swa_trim = trim;
22882 p
22883 }
22884
22885 #[test]
22891 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
22892 let mut a = spark_test_pipeline(None);
22893 let mut b = spark_test_pipeline(Some((2, 4)));
22894 assert_eq!(
22895 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
22896 vec![Some(6), Some(6), Some(6), None]
22897 );
22898 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
22899 let mut pos = 0usize;
22900 for &n in [5usize, 13, 7, 16].iter().cycle() {
22901 if pos >= ids.len() {
22902 break;
22903 }
22904 let end = (pos + n).min(ids.len());
22905 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22906 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
22907 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
22908 pos = end;
22909 }
22910 for _ in 0..23 {
22911 let e = a.embed_single(((pos * 7) % 64) as u32);
22912 let ha = a.forward_layers(&e, pos, None);
22913 let hb = b.forward_layers(&e, pos, None);
22914 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
22915 for li in 0..3 {
22916 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
22917 }
22918 pos += 1;
22919 }
22920 for round in 0..9 {
22921 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
22922 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
22923 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
22924 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
22925 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
22926 if round % 2 == 1 {
22927 for p in [&mut a, &mut b] {
22929 for l in &mut p.kv_cache.layers {
22930 l.truncate_last(2);
22931 }
22932 }
22933 } else {
22934 pos += 2;
22935 }
22936 }
22937 let e = a.embed_single(9);
22938 let (ha, hb) = (
22939 a.forward_layers(&e, pos, None),
22940 b.forward_layers(&e, pos, None),
22941 );
22942 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
22943 for li in 0..3 {
22944 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
22945 assert!(lb.base() > 0, "layer {li} trimmed");
22946 assert_eq!(la.base(), 0);
22947 assert_eq!(lb.pos_len(), la.seq_len);
22948 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
22949 }
22950 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
22951 assert_eq!(
22952 (gb.base(), gb.seq_len),
22953 (0, ga.seq_len),
22954 "the global layer keeps all"
22955 );
22956 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
22957 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
22958 }
22959
22960 #[test]
22965 fn swa_trim_wire_handoff_continues_bitwise() {
22966 let mut a = spark_test_pipeline(None);
22967 let mut b = spark_test_pipeline(Some((2, 4)));
22968 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
22969 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22970 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
22971 assert_eq!(f32_bits(&ha), f32_bits(&hb));
22972 let mut pos = ids.len();
22973 for _ in 0..5 {
22974 let e = a.embed_single(((pos * 5) % 64) as u32);
22975 assert_eq!(
22976 f32_bits(&a.forward_layers(&e, pos, None)),
22977 f32_bits(&b.forward_layers(&e, pos, None))
22978 );
22979 pos += 1;
22980 }
22981 let mut c = spark_test_pipeline(Some((2, 4)));
22982 for li in 0..4 {
22983 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
22984 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
22985 }
22986 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
22987 assert_eq!(c.kv_cache.seq_len(), pos);
22988 for step in 0..17 {
22989 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
22990 let ha = a.forward_layers(&e, pos, None);
22991 let hc = c.forward_layers(&e, pos, None);
22992 assert_eq!(
22993 f32_bits(&ha),
22994 f32_bits(&hc),
22995 "after the hand-off, step {step}"
22996 );
22997 pos += 1;
22998 }
22999 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
23000 }
23001
23002 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
23007 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
23008 let synth = |len: usize, salt: usize| -> Vec<f32> {
23009 (0..len)
23010 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
23011 .collect()
23012 };
23013 let qt = |rows: usize, cols: usize, salt: usize| {
23014 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23015 };
23016 let layers = widths
23017 .iter()
23018 .copied()
23019 .enumerate()
23020 .map(|(k, inter)| {
23021 let s = 500 + k * 40;
23022 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
23023 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
23024 MtpModule {
23025 enorm: vec![1.0; hs],
23026 hnorm: vec![1.0; hs],
23027 eh_proj: qt(hs, 2 * hs, s),
23028 layer: LayerWeights {
23029 input_norm: vec![1.0; hs],
23030 post_norm: vec![1.0; hs],
23031 attn_out_norm: None,
23032 ffn_out_norm: None,
23033 layer_scale: None,
23034 attn: AttnKind::Full {
23035 wq: qt(nh * hd, hs, s + 1),
23036 wk: qt(nkv * hd, hs, s + 2),
23037 wv: qt(nkv * vd, hs, s + 3),
23038 wo: qt(hs, nh * vd, s + 4),
23039 q_norm: None,
23040 k_norm: None,
23041 output_gate: false,
23042 softplus_gate: None,
23043 bias: None,
23044 },
23045 ffn: FfnKind::Dense(DenseFfn {
23046 gate_proj: qt(inter, hs, s + 5),
23047 up_proj: qt(inter, hs, s + 6),
23048 down_proj: qt(hs, inter, s + 7),
23049 act: Act::Silu,
23050 down_t: None,
23051 segs: Vec::new(),
23052 }),
23053 },
23054 final_norm: vec![1.0; hs],
23055 kv,
23056 }
23057 })
23058 .collect();
23059 mimo_mtp::MimoMtp::from_layers(layers)
23060 }
23061
23062 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
23063 mimo_test_mtp_widths(&vec![24; n], gain)
23064 }
23065
23066 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
23067 p.clear_sequence_state();
23068 p.speculative = spec;
23069 p.ignore_eos = true;
23070 p.sampler_config.temperature = 0.0;
23071 p.generate_from_ids(ids, n, None, None).unwrap()
23072 }
23073
23074 #[test]
23075 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
23076 let mut p = mimo_test_pipeline();
23077 let widths = [7usize, 13, 29];
23078 assert_ne!(widths[0], p.intermediate_size);
23079 assert_ne!(widths[1], p.intermediate_size);
23080 assert_ne!(widths[2], p.intermediate_size);
23081 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
23082 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
23083
23084 let trunk = p.probe_ffn_mass(&ids);
23085 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
23086 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
23087 assert_eq!(got.len(), widths.len());
23088 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
23089 assert_eq!(stat.mass.len(), width, "draft layer {k}");
23090 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
23091 assert_eq!(stat.observed_positions, ids.len() - k - 1);
23092 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
23093 assert!(stat
23094 .mass
23095 .iter()
23096 .zip(&stat.squared_mass)
23097 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
23098 }
23099 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
23100 let st = p.mimo_mtp.as_ref().unwrap();
23101 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
23102
23103 let h = p.hidden_size;
23106 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
23107 panic!("synthetic MiMo MTP must be dense")
23108 };
23109 dense.segs.push(FfnSeg {
23110 gate: QTensor::from_f32(vec![0.0; h], 1, h),
23111 up: QTensor::from_f32(vec![0.0; h], 1, h),
23112 down: QTensor::from_f32(vec![0.0; h], h, 1),
23113 start: dense.gate_proj.rows(),
23114 width: 1,
23115 });
23116 assert!(p
23117 .probe_mtp_ffn(&ids)
23118 .unwrap_err()
23119 .contains("tube-segmented draft FFNs are not supported"));
23120 assert_eq!(p.kv_cache.seq_len(), 0);
23121 assert!(p
23122 .mimo_mtp
23123 .as_ref()
23124 .unwrap()
23125 .layers
23126 .iter()
23127 .all(|m| m.kv.seq_len == 0));
23128 }
23129
23130 #[test]
23131 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
23132 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
23133 p.clear_sequence_state();
23134 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
23135 let _exact = ExactMtpCalibrationGuard::enter();
23136 let got = crate::gpu::cpu_scope(|| {
23137 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, rows)
23138 })
23139 .expect("teacher-forced MiMo probe");
23140 st.reset();
23141 p.mimo_mtp = Some(st);
23142 p.reset_mtp_probe_state();
23143 got
23144 }
23145
23146 for post_norm_hidden in [false, true] {
23147 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
23148 let make = || {
23149 let mut p = mimo_test_pipeline();
23150 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
23151 st.post_norm_hidden = post_norm_hidden;
23152 p.mimo_mtp = Some(st);
23153 p
23154 };
23155 let mut full = make();
23156 let expected = collect(&mut full, &ids, usize::MAX);
23157 for rows in [1usize, 7, 128] {
23158 let mut chunked = make();
23159 let got = collect(&mut chunked, &ids, rows);
23160 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
23161 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
23162 assert_eq!(
23163 got.observed_positions, expected.observed_positions,
23164 "post={post_norm_hidden}, rows={rows}, layer={layer}"
23165 );
23166 assert_eq!(
23167 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23168 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
23169 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
23170 );
23171 assert_eq!(
23172 got.squared_mass
23173 .iter()
23174 .map(|v| v.to_bits())
23175 .collect::<Vec<_>>(),
23176 expected
23177 .squared_mass
23178 .iter()
23179 .map(|v| v.to_bits())
23180 .collect::<Vec<_>>(),
23181 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
23182 );
23183 }
23184 }
23185 }
23186 }
23187
23188 #[test]
23194 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
23195 for post in [false, true] {
23198 let mut p = mimo_test_pipeline();
23199 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
23201 let mut st0 = mimo_test_mtp(3, 1.0);
23202 st0.post_norm_hidden = post;
23203 p.mimo_mtp = Some(st0);
23204 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
23205 let hs = p.hidden_size;
23206 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23207 p.mimo_note_rows(&hb, 0);
23208 let mut st = p.mimo_mtp.take().unwrap();
23209 let k = 3;
23212 let mut inc = Vec::new();
23213 for t in 0..ids.len() - k - 1 {
23214 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
23215 }
23216 let s = ids.len();
23219 let mut reference = vec![vec![0u32; k]; s - k - 1];
23220 let mut fresh = mimo_test_mtp(3, 1.0);
23221 for (layer, m) in fresh.layers.iter_mut().enumerate() {
23222 let n = s - layer - 1;
23223 let mut cats = vec![0.0f32; n * 2 * hs];
23224 for j in 0..n {
23225 let e = p.embed_single(ids[j + layer + 1]);
23226 let raw = &hb[j * hs..(j + 1) * hs];
23227 let g = if post {
23228 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
23229 } else {
23230 raw.to_vec()
23231 };
23232 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
23233 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
23234 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
23235 }
23236 let mut x = vec![0.0f32; n * hs];
23237 m.eh_proj.matmat(&cats, n, &mut x, None);
23238 p.mimo_mtp_block(m, &mut x, n, 0);
23239 for (t, row) in reference.iter_mut().enumerate() {
23240 let y = inference::rms_norm(
23241 &x[t * hs..(t + 1) * hs],
23242 &m.final_norm,
23243 p.rms_eps,
23244 p.norm_style,
23245 );
23246 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
23247 }
23248 }
23249 assert_eq!(inc, reference, "post_norm_hidden = {post}");
23250 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
23252 assert!(distinct.len() > 3, "{inc:?}");
23253 let last_t = ids.len() - k - 2;
23255 for m in &st.layers {
23256 assert_eq!(m.kv.seq_len, last_t + 1);
23257 }
23258 }
23259 }
23260
23261 #[test]
23268 fn mimo_speculative_greedy_equals_plain_greedy() {
23269 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
23270 let _env = ScopedTestEnv::set(&[
23271 ("CMF_GPU", Some("0")),
23272 ("CMF_GPU_WGPU_GRAPH", Some("0")),
23273 ]);
23274 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
23275 let n = 24;
23276 let mut p = mimo_test_pipeline();
23277 let plain = mimo_greedy(&mut p, &ids, n, false);
23278 assert_eq!(plain.mtp_drafted, 0);
23279 assert_eq!(plain.token_ids.len(), n);
23280 let plain_kv = p.kv_cache.layers[0].seq_len;
23281
23282 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
23284 let spec = mimo_greedy(&mut p, &ids, n, true);
23285 assert!(spec.mtp_drafted > 0, "the round must draft");
23286 assert_eq!(spec.token_ids, plain.token_ids);
23287 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23288
23289 let mut truth: Vec<u32> = ids.clone();
23292 truth.extend(&plain.token_ids);
23293 let mut noisy = truth.clone();
23294 for (i, t) in noisy.iter_mut().enumerate() {
23295 if i % 5 == 0 {
23296 *t = (*t + 1) % 64;
23297 }
23298 }
23299 let mut st = mimo_test_mtp(3, 1.0);
23300 st.draft_override = Some(noisy);
23301 p.mimo_mtp = Some(st);
23302 let spec = mimo_greedy(&mut p, &ids, n, true);
23303 assert_eq!(spec.token_ids, plain.token_ids);
23304 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23305 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
23306 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
23307 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
23308 assert!(
23309 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
23310 "{:?}",
23311 stats.accept_hist
23312 );
23313 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
23314
23315 let mut st = mimo_test_mtp(3, 1.0);
23318 st.draft_override = Some(truth);
23319 p.mimo_mtp = Some(st);
23320 let spec = mimo_greedy(&mut p, &ids, n, true);
23321 assert_eq!(spec.token_ids, plain.token_ids);
23322 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
23323 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
23324
23325 let off = mimo_greedy(&mut p, &ids, n, false);
23327 assert_eq!(off.token_ids, plain.token_ids);
23328 assert_eq!(off.mtp_drafted, 0);
23329 }
23330
23331 #[test]
23340 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
23341 let p = mimo_test_pipeline();
23342 assert_eq!(
23343 p.graph_attn_decline_reason(),
23344 Some("per-layer KV head counts")
23345 );
23346 assert_eq!(p.wgpu_graph_attn_decline(), None);
23347 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
23348 assert_eq!(
23349 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
23350 (1, 4, 4, None, false)
23351 );
23352 assert_eq!(g0.invf, p.inv_freq.as_slice());
23353 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
23354 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
23355 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
23356 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
23357 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
23358 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
23359 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
23360
23361 let emb = p.embed_single(3);
23364 let mut lg = Vec::new();
23365 assert!(
23366 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
23367 .is_none()
23368 );
23369 let mut hid = emb.clone();
23370 assert_eq!(
23371 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
23372 crate::gpu::BatchGraphOutcome::Declined
23373 );
23374 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
23375 assert!(p.try_multi_burst(3, 0, 4).is_none());
23376 assert!(
23377 p.graph_declines().is_empty(),
23378 "no attention decline logged: {:?}",
23379 p.graph_declines()
23380 );
23381 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
23386 assert_eq!(plain().graph_attn_decline_reason(), None);
23387 assert_eq!(plain().wgpu_graph_attn_decline(), None);
23388 assert!(
23389 plain().graph_attn_geom(0).is_none(),
23390 "uniform models keep the historical arms"
23391 );
23392 let mut q = plain();
23393 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
23394 assert_eq!(
23395 q.graph_attn_decline_reason(),
23396 Some("learned attention sinks")
23397 );
23398 assert_eq!(
23399 q.graph_attn_geom(1).unwrap().sink,
23400 Some(&[0.25f32, -0.25][..])
23401 );
23402 let mut q = plain();
23403 q.set_attn_geometry(None, Some(2)).unwrap();
23404 assert_eq!(
23405 q.graph_attn_decline_reason(),
23406 Some("V heads narrower than Q/K heads")
23407 );
23408 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
23409 let mut q = plain();
23410 q.sliding_layers = Some(vec![true, false]);
23411 q.swa = Some((4, usize::MAX));
23412 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
23413 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
23414 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
23415
23416 let mut q = mimo_test_pipeline();
23420 q.rope_scale = 2.0;
23421 q.rope_scale_local = 1.25;
23422 assert_eq!(q.wgpu_graph_attn_decline(), None);
23423 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
23424 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
23425
23426 let mut q = plain();
23429 q.rope_scale = 2.0;
23430 assert_eq!(
23431 q.wgpu_graph_attn_decline(),
23432 Some("scaled RoPE without per-layer geometry")
23433 );
23434 }
23435
23436 #[cfg(target_os = "macos")]
23442 #[test]
23443 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
23444 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
23448 p.swa = Some((16, usize::MAX));
23449 p.sliding_layers = Some(vec![true, true]);
23450 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
23451 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
23452
23453 assert_eq!(
23454 p.graph_attn_decline_reason(),
23455 Some("learned attention sinks")
23456 );
23457 let swa_graph = p.metal_graph_swa();
23458 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
23459 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
23460 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
23461 assert!(
23462 native_sink_attend,
23463 "the q1 sink admission must follow the SWA door"
23464 );
23465 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
23468 }
23469
23470 #[test]
23471 fn mimo_verify_rewind_preserves_lagging_host_caches() {
23472 let mut p = mimo_test_pipeline();
23473 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
23474 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
23475 for _ in 0..if li == 0 { 2 } else { 12 } {
23476 layer.append(&row, &row, &[]);
23477 }
23478 }
23479 p.mimo_verify_rewind(9).unwrap();
23480 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
23481 for layer in &p.kv_cache.layers[1..] {
23482 assert_eq!(layer.seq_len, 9);
23483 }
23484 }
23485
23486 #[test]
23490 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
23491 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
23492 let _ = std::fs::remove_dir_all(&dir);
23493 let mut p = mimo_test_pipeline();
23494 let hs = p.hidden_size;
23495 let ids = [5u32, 9, 11, 2, 40];
23496 p.layer_dump = Some(dir.join("decode"));
23497 for (pos, &id) in ids.iter().enumerate() {
23498 let e = p.embed_single(id);
23499 let _ = p.forward_layers(&e, pos, None);
23500 }
23501 p.clear_sequence_state();
23502 p.layer_dump = Some(dir.join("prefill"));
23503 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23504 for pos in 0..ids.len() {
23505 for li in 0..p.num_layers {
23506 let name = format!("p{pos:06}_l{li:02}.f32");
23507 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
23508 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
23509 assert_eq!(a.len(), hs * 4, "{name}");
23510 assert_eq!(a, b, "{name}");
23511 }
23512 }
23513 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
23514 let vals: Vec<f32> = last
23515 .chunks(4)
23516 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
23517 .collect();
23518 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
23519 let _ = std::fs::remove_dir_all(&dir);
23520 }
23521
23522 #[test]
23523 fn attn_geometry_and_sinks_are_validated() {
23524 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
23525 assert!(
23526 p.set_attn_geometry(Some(vec![2]), None).is_err(),
23527 "one entry per layer"
23528 );
23529 assert!(
23530 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
23531 "3 does not divide 4"
23532 );
23533 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
23534 assert!(p.set_attn_geometry(None, Some(0)).is_err());
23535 assert!(
23536 p.set_attn_geometry(None, Some(5)).is_err(),
23537 "V wider than the head"
23538 );
23539 p.set_attn_geometry(None, Some(4)).unwrap();
23540 assert_eq!(
23541 p.v_head_dim, None,
23542 "v_head_dim == head_dim is the uniform case"
23543 );
23544 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
23545 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
23546 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
23547 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
23548 assert!(
23549 p.kv_cache.layers[1].sinks.is_some(),
23550 "a reshape keeps the layer's sinks"
23551 );
23552 assert_eq!(p.layer_geom(1).0, 4);
23553 assert!(
23554 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
23555 "one sink per Q head"
23556 );
23557 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
23558 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
23559 }
23560
23561 #[test]
23564 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
23565 let cfg = || {
23566 Some(crate::nystrom::O1Cfg {
23567 layers: crate::nystrom::O1Layers::All,
23568 m: 4,
23569 w: 8,
23570 sink: 2,
23571 rect: crate::nystrom::O1Rect::Aggregate,
23572 })
23573 };
23574 let mut p = mimo_test_pipeline();
23575 p.set_o1(cfg());
23576 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
23577 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
23578 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
23579 q.sliding_layers = Some(vec![false, false, true]);
23580 q.swa = Some((4, usize::MAX));
23581 q.set_o1(cfg());
23582 assert_eq!(q.o1_flags, vec![true, false, false]);
23583 }
23584
23585 #[test]
23586 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
23587 const B: usize = 19;
23588 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23589 p.set_o1(Some(crate::nystrom::O1Cfg {
23590 layers: crate::nystrom::O1Layers::All,
23591 m: 4,
23592 w: 8,
23593 sink: 2,
23594 rect: crate::nystrom::O1Rect::Aggregate,
23595 }));
23596 p.o1_begin_with_prefix(Some(B));
23597 let ids: Vec<u32> = (0..B as u32).collect();
23598 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
23599
23600 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
23601 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
23602 let next = p.embed_single(B as u32);
23603 let _ = p.forward_layers(&next, B, None);
23604 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
23605 }
23606
23607 #[test]
23608 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
23609 const B: usize = 19;
23610 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
23611 let gdn_cfg = crate::linear_core::GdnCfg {
23615 num_v_heads: 2,
23616 num_k_heads: 1,
23617 key_head_dim: 2,
23618 value_head_dim: 4,
23619 conv_kernel: 3,
23620 hidden_size: 8,
23621 rms_eps: 1e-6,
23622 output_gate_sigmoid: false,
23623 };
23624 let synth = |n: usize, salt: usize| -> Vec<f32> {
23625 (0..n)
23626 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
23627 .collect()
23628 };
23629 let qt = |rows: usize, cols: usize, salt: usize| {
23630 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23631 };
23632 let c_dim = gdn_cfg.conv_dim();
23633 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
23634 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
23635 in_proj_qkv: qt(c_dim, 8, 1),
23636 in_proj_z: qt(vd, 8, 2),
23637 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
23638 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
23639 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
23640 a_log: vec![0.2, 0.5],
23641 dt_bias: synth(gdn_cfg.num_v_heads, 6),
23642 norm: vec![1.0; gdn_cfg.value_head_dim],
23643 out_proj: qt(8, vd, 7),
23644 });
23645 p.gdn_cfg = Some(gdn_cfg);
23646 p.set_o1(Some(crate::nystrom::O1Cfg {
23647 layers: crate::nystrom::O1Layers::All,
23648 m: 4,
23649 w: 8,
23650 sink: 2,
23651 rect: crate::nystrom::O1Rect::Aggregate,
23652 }));
23653 p.o1_begin_with_prefix(Some(B));
23654 for pos in 0..B - 2 {
23655 let emb = p.embed_single(pos as u32);
23656 let _ = p.forward_layers(&emb, pos, None);
23657 }
23658 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
23659
23660 let e1 = p.embed_single((B - 2) as u32);
23661 let e2 = p.embed_single((B - 1) as u32);
23662 let _ = p.forward_pair(&e1, &e2, B - 2);
23663
23664 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
23665 assert!(
23666 p.kv_cache
23667 .layers
23668 .iter()
23669 .enumerate()
23670 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
23671 );
23672 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
23673 assert_ne!(
23674 p.kv_cache.layers[0].linear_state, lane1_state,
23675 "real pair must commit GDN lane 2 before returning"
23676 );
23677 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
23678 let next = p.embed_single(B as u32);
23679 let _ = p.forward_layers(&next, B, None);
23680 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
23681 }
23682
23683 #[test]
23684 fn o1_error_observation_stays_terminal_until_reset() {
23685 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23686 p.set_o1(Some(crate::nystrom::O1Cfg {
23687 layers: crate::nystrom::O1Layers::All,
23688 m: 4,
23689 w: 8,
23690 sink: 2,
23691 rect: crate::nystrom::O1Rect::Aggregate,
23692 }));
23693 p.o1_begin();
23694 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
23695
23696 assert!(p.o1_seal_checked().is_err());
23697 assert!(
23698 p.o1_seal_checked().is_err(),
23699 "retry must see the sticky error"
23700 );
23701 let k = vec![0.2f32; 4];
23702 let v = vec![0.3f32; 4];
23703 p.kv_cache.layers[0].append(&k, &v, &[]);
23704 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
23705
23706 p.reset_session();
23707 p.o1_begin();
23708 p.kv_cache.layers[0].append(&k, &v, &[]);
23709 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
23710 }
23711
23712 #[test]
23713 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
23714 let ids = vec![1u32, 2, 3, 4, 5, 6];
23715 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23716 p.graph_logits = Some(vec![123.0]);
23717 p.graph_want_logits = true;
23718 p.graph_failed
23719 .store(true, std::sync::atomic::Ordering::Relaxed);
23720 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23721 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
23722 assert!(err.contains("before NLL"));
23723 assert!(p.graph_logits.is_none());
23724 assert!(!p.graph_want_logits);
23725 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23726 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23727
23728 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23729 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23730 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23731 assert_eq!(actual.1, expected.1);
23732 assert!((actual.0 - expected.0).abs() < 1e-9);
23733 }
23734
23735 #[test]
23736 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
23737 let ids = vec![1u32, 2, 3, 4, 5, 6];
23738 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23739 p.nll_test_fail_at = Some(1);
23740 let err = p
23741 .nll_ids_from(&ids, 0)
23742 .expect_err("one-shot forward failure");
23743 assert!(err.contains("forward") || err.contains("score row"));
23744 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23745 assert!(!p.graph_want_logits);
23746 assert!(p.graph_logits.is_none());
23747 assert!(p.kv_history.is_empty());
23748
23749 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23750 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
23751 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
23752 assert_eq!(actual.1, expected.1);
23753 assert!((actual.0 - expected.0).abs() < 1e-9);
23754 }
23755
23756 #[test]
23757 fn nll_serial_failure_before_first_row_is_reported() {
23758 let ids = vec![1u32, 2, 3, 4];
23759 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23760 p.nll_test_force_serial = true;
23761 p.nll_test_fail_at = Some(0);
23762 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
23763 assert!(err.contains("serial forward"));
23764 assert!(p.kv_history.is_empty());
23765 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23766 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23767 }
23768
23769 #[test]
23770 fn ffn_probe_failure_discards_recorder_and_state() {
23771 let ids = vec![1u32, 2, 3, 4];
23772 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23773 p.nll_test_fail_at = Some(0);
23774 let err = p
23775 .probe_ffn_mass_batch(&ids)
23776 .expect_err("probe forward failure");
23777 assert!(err.contains("NLL"));
23778 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
23779 assert!(p.kv_history.is_empty());
23780 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23781 }
23782
23783 #[test]
23784 fn nll_test_controls_are_pipeline_scoped() {
23785 let ids = vec![1u32, 2, 3, 4];
23786 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23787 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23788 failing.nll_test_force_serial = true;
23789 failing.nll_test_fail_at = Some(0);
23790
23791 assert!(!failing.can_prefill_batched());
23792 assert!(unaffected.can_prefill_batched());
23793 let expected = unaffected
23794 .nll_ids_from(&ids, 0)
23795 .expect("unaffected pipeline remains usable");
23796 let err = failing
23797 .nll_ids_from(&ids, 0)
23798 .expect_err("failure injection belongs to failing pipeline");
23799 assert!(err.contains("serial forward"));
23800 assert!(failing.nll_test_fail_at.is_none());
23801 assert!(unaffected.can_prefill_batched());
23802 let actual = unaffected
23803 .nll_ids_from(&ids, 0)
23804 .expect("unaffected pipeline remains reusable");
23805 assert_eq!(actual.1, expected.1);
23806 assert!((actual.0 - expected.0).abs() < 1e-9);
23807 }
23808
23809 #[test]
23810 fn forward_ids_failure_channel_is_terminal_and_reusable() {
23811 let ids = vec![1u32, 2, 3, 4, 5, 6];
23812 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
23813 p.graph_logits = Some(vec![123.0]);
23814 p.graph_want_logits = true;
23815 p.graph_failed
23816 .store(true, std::sync::atomic::Ordering::Relaxed);
23817 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
23818
23819 let err = p
23820 .forward_ids(&ids, None)
23821 .expect_err("a failed forward must not become a valid head result");
23822 assert!(err.contains("forward_ids setup"));
23823 assert!(p.graph_logits.is_none());
23824 assert!(!p.graph_want_logits);
23825 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
23826 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
23827 assert_eq!(p.kv_cache.seq_len(), 0);
23828
23829 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
23830 .forward_ids(&ids, None)
23831 .expect("fresh forward_ids");
23832 let actual = p
23833 .forward_ids(&ids, None)
23834 .expect("pipeline remains reusable after a failed forward");
23835 assert_eq!(actual.len(), expected.len());
23836 assert!(
23837 actual
23838 .iter()
23839 .zip(expected)
23840 .all(|(a, b)| (a - b).abs() < 1e-9)
23841 );
23842 assert_eq!(p.kv_cache.seq_len(), ids.len());
23843 }
23844
23845 #[test]
23846 fn sigmoid_router_floor_is_explicit_per_architecture() {
23847 let zero = || DenseFfn {
23853 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23854 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23855 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23856 act: Act::Silu,
23857 down_t: None,
23858 segs: Vec::new(),
23859 };
23860 let m = MoeFfn {
23861 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
23862 experts: vec![zero(), zero()],
23863 top_k: 1,
23864 norm_topk_prob: true,
23865 router_sigmoid: true,
23866 sigmoid_denom_eps: 1e-20,
23867 expert_bias: None,
23868 routed_scaling: 2.5,
23869 route_tau: None,
23870 shared: None,
23871 stats: std::cell::RefCell::new(Vec::new()),
23872 act_sq: std::cell::RefCell::new(Vec::new()),
23873 act_rows: std::cell::RefCell::new(Vec::new()),
23874 mask: None,
23875 per_expert_scale: None,
23876 router_input_norm: false,
23877 resonance: None,
23878 grown: Vec::new(),
23879 };
23880 let logits = [-20.0f32, -20.0];
23881 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
23882 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
23883 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
23884 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
23885 assert!((flint_wsum - reference_expected).abs() < 1e-15);
23886 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
23887 assert!(legacy_wsum > minimax_wsum * 100.0);
23888 }
23889
23890 #[test]
23891 fn resonance_scores_match_formula_and_stable_tie() {
23892 let r = Resonance {
23893 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
23895 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
23896 k: 1,
23897 bias: vec![1.5, 0.5, 0.0],
23898 shell: Vec::new(),
23899 };
23900 let x = [1.0f32, 1.0];
23901 let mut got = vec![0.0; 3];
23902 r.scores(&x, &mut got);
23903 assert!((got[0] - 0.5).abs() < 1e-6);
23907 assert!((got[1] - 0.5).abs() < 1e-6);
23908 assert!(got[2].abs() < 1e-6);
23909 let best = got
23910 .iter()
23911 .enumerate()
23912 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
23913 .map(|(i, _)| i);
23914 assert_eq!(best, Some(0));
23915 assert!(got.iter().all(|v| v.is_finite()));
23916 }
23917
23918 #[test]
23923 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
23924 let plain = Resonance {
23930 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
23931 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
23932 k: 1,
23933 bias: vec![1.5, 0.5, 0.0, 0.0],
23934 shell: Vec::new(),
23935 };
23936 let shelled = Resonance {
23937 mu: plain.mu.clone(),
23938 u: plain.u.clone(),
23939 k: 1,
23940 bias: plain.bias.clone(),
23941 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
23942 };
23943 assert!(!plain.has_shell());
23944 assert!(shelled.has_shell());
23945 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
23946 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
23947 set_growth_shell(Some(true));
23948 assert!(growth_shell_enabled());
23949 let x = [1.0f32, 1.0];
23952 plain.scores(&x, &mut a);
23953 shelled.scores(&x, &mut b);
23954 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
23955 assert!(a[2] == 0.0 && a[3] == 0.0);
23956 let xo = [3.0f32, 0.0];
23959 plain.scores(&xo, &mut a);
23960 shelled.scores(&xo, &mut b);
23961 assert_eq!(a[2], -6.0);
23962 assert_eq!(a[3], -6.0);
23963 assert_eq!(
23964 bits(&a[..3]),
23965 bits(&b[..3]),
23966 "trunk rows + the boundary row"
23967 );
23968 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
23969 assert_eq!(shelled.effective_shell(4), shelled.shell);
23970 set_growth_shell(Some(false));
23973 assert!(!growth_shell_enabled());
23974 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
23975 shelled.scores(&xo, &mut b);
23976 assert_eq!(bits(&a), bits(&b));
23977 set_growth_shell(None);
23978 let short = Resonance {
23981 shell: vec![f32::INFINITY, f32::INFINITY],
23982 ..shelled
23983 };
23984 set_growth_shell(Some(true));
23985 short.scores(&xo, &mut b);
23986 assert_eq!(bits(&a), bits(&b));
23987 set_growth_shell(None);
23988 }
23989
23990 #[test]
23994 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
23995 let zero = || DenseFfn {
23996 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23997 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23998 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
23999 act: Act::Silu,
24000 down_t: None,
24001 segs: Vec::new(),
24002 };
24003 let moe = |sigmoid: bool| MoeFfn {
24004 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
24005 experts: vec![zero(), zero(), zero(), zero()],
24006 top_k: 1,
24007 norm_topk_prob: true,
24008 router_sigmoid: sigmoid,
24009 sigmoid_denom_eps: 1e-6,
24010 expert_bias: None,
24011 routed_scaling: 1.0,
24012 route_tau: None,
24013 shared: None,
24014 stats: std::cell::RefCell::new(Vec::new()),
24015 act_sq: std::cell::RefCell::new(Vec::new()),
24016 act_rows: std::cell::RefCell::new(Vec::new()),
24017 mask: None,
24018 per_expert_scale: None,
24019 router_input_norm: false,
24020 resonance: None,
24021 grown: Vec::new(),
24022 };
24023 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
24024 for sigmoid in [false, true] {
24025 let m = moe(sigmoid);
24026 let (idx, p, wsum) = moe_route(&logits, &m, None);
24027 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
24028 assert_eq!(p[1], 0.0);
24029 assert_eq!(p[3], 0.0);
24030 assert!(p[2] > p[0] && p[0] > 0.0);
24031 assert!(p.iter().all(|v| v.is_finite()));
24032 let w = p[2] / wsum;
24033 if sigmoid {
24034 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
24036 } else {
24037 assert_eq!(
24038 w, 1.0,
24039 "softmax: the renormalized top-1 weight is exactly 1"
24040 );
24041 }
24042 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
24045 assert_eq!(idx, vec![2]);
24046 }
24047 let m = moe(false);
24049 let (idx, _, _) = moe_route(
24050 &[
24051 f32::NEG_INFINITY,
24052 f32::NEG_INFINITY,
24053 f32::NEG_INFINITY,
24054 -9.0,
24055 ],
24056 &m,
24057 None,
24058 );
24059 assert_eq!(idx, vec![3]);
24060 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
24063 assert_eq!(idx, vec![0]);
24064 assert!(p.iter().all(|&v| v == 0.25));
24065 assert!(wsum.is_finite() && wsum > 0.0);
24066 }
24067
24068 #[cfg(feature = "gpu")]
24074 #[test]
24075 fn minimax_device_route_matches_host_sigmoid_contract() {
24076 match crate::gpu_wgpu::selected_and_up() {
24077 None => {
24078 crate::gpu_wgpu::skip_or_fail(module_path!());
24079 return;
24080 }
24081 Some(false) => {
24089 eprintln!(
24090 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24091 module_path!()
24092 );
24093 return;
24094 }
24095 Some(true) => {}
24096 }
24097 let n = 128usize;
24098 let top_k = 4usize;
24099 let logits: Vec<f32> = (0..n)
24100 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24101 .collect();
24102 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24103 let m = MoeFfn {
24104 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24105 experts: Vec::new(),
24106 top_k,
24107 norm_topk_prob: true,
24108 router_sigmoid: true,
24109 sigmoid_denom_eps: 1e-20,
24110 expert_bias: Some(bias.clone()),
24111 routed_scaling: 2.0,
24112 route_tau: None,
24113 shared: None,
24114 stats: std::cell::RefCell::new(Vec::new()),
24115 act_sq: std::cell::RefCell::new(Vec::new()),
24116 act_rows: std::cell::RefCell::new(Vec::new()),
24117 mask: None,
24118 per_expert_scale: None,
24119 router_input_norm: false,
24120 resonance: None,
24121 grown: Vec::new(),
24122 };
24123 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24124 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
24125
24126 for shared in [Some(n as u32), None] {
24127 let mut gi = Vec::new();
24128 let mut gw = Vec::new();
24129 assert!(
24130 crate::gpu_wgpu::minimax_moe_route_for_test(
24131 &logits,
24132 Some(&bias),
24133 top_k,
24134 2.0,
24135 shared,
24136 &mut gi,
24137 &mut gw,
24138 ),
24139 "the device did not come up — the test checked nothing"
24140 );
24141 let routed = if shared.is_some() { top_k } else { gi.len() };
24142 assert_eq!(
24143 &gi[..routed],
24144 &ci[..],
24145 "different experts or order (shared={shared:?})"
24146 );
24147 let d = cw
24148 .iter()
24149 .zip(&gw[..routed])
24150 .map(|(a, b)| (a - b).abs())
24151 .fold(0.0f32, f32::max);
24152 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
24153 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
24154 if let Some(s) = shared {
24155 assert_eq!(gi[top_k], s as usize, "shared pin slot");
24156 assert_eq!(gw[top_k], 1.0, "shared weight");
24157 }
24158 }
24159 }
24160
24161 #[cfg(feature = "gpu")]
24166 #[test]
24167 fn minimax_device_cold_weights_match_host_contract() {
24168 match crate::gpu_wgpu::selected_and_up() {
24169 None => {
24170 crate::gpu_wgpu::skip_or_fail(module_path!());
24171 return;
24172 }
24173 Some(false) => {
24181 eprintln!(
24182 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
24183 module_path!()
24184 );
24185 return;
24186 }
24187 Some(true) => {}
24188 }
24189 let n = 128usize;
24190 let top_k = 4usize;
24191 let logits: Vec<f32> = (0..n)
24192 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
24193 .collect();
24194 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
24195 let m = MoeFfn {
24196 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
24197 experts: Vec::new(),
24198 top_k,
24199 norm_topk_prob: true,
24200 router_sigmoid: true,
24201 sigmoid_denom_eps: 1e-20,
24202 expert_bias: Some(bias.clone()),
24203 routed_scaling: 2.0,
24204 route_tau: None,
24205 shared: None,
24206 stats: std::cell::RefCell::new(Vec::new()),
24207 act_sq: std::cell::RefCell::new(Vec::new()),
24208 act_rows: std::cell::RefCell::new(Vec::new()),
24209 mask: None,
24210 per_expert_scale: None,
24211 router_input_norm: false,
24212 resonance: None,
24213 grown: Vec::new(),
24214 };
24215 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
24216 assert_eq!(ci.len(), top_k);
24217
24218 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
24220 let mut cold = Vec::new();
24221 let mut idx = Vec::new();
24222 let mut dw = Vec::new();
24223 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24224 &logits,
24225 Some(&bias),
24226 top_k,
24227 2.0,
24228 &resident,
24229 n as u32,
24230 &mut cold,
24231 &mut idx,
24232 &mut dw,
24233 ));
24234 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
24235 for (r, &e) in ci.iter().enumerate() {
24240 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
24241 let host = p[e] / wsum;
24242 let d = (dw[r] - host).abs();
24243 assert!(
24244 d <= 1e-6 * host.abs().max(1.0),
24245 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
24246 dw[r]
24247 );
24248 }
24249 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
24250 assert_eq!(dw[top_k], 1.0, "shared weight");
24251
24252 let mut remap = resident.clone();
24254 remap[ci[0]] = u32::MAX;
24255 remap[ci[2]] = u32::MAX;
24256 let mut cold = Vec::new();
24257 let mut idx2 = Vec::new();
24258 let mut dw2 = Vec::new();
24259 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
24260 &logits,
24261 Some(&bias),
24262 top_k,
24263 2.0,
24264 &remap,
24265 n as u32,
24266 &mut cold,
24267 &mut idx2,
24268 &mut dw2,
24269 ));
24270 let mut resident_weight_checked = 0usize;
24278 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
24279 if dw2[r] == 0.0 {
24280 continue; }
24282 let e = slot as usize;
24283 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
24284 let host = p[e] / wsum;
24285 let d = (dw2[r] - host).abs();
24286 assert!(
24287 d <= 1e-6 * host.abs().max(1.0),
24288 "partial-bank resident weight expert {e}: Δ={d:.3e}"
24289 );
24290 resident_weight_checked += 1;
24291 }
24292 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
24293 println!("partial bank: checked {resident_weight_checked} resident weights");
24294 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
24295 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
24296 ids.sort_unstable();
24297 let mut want = vec![ci[0], ci[2]];
24298 want.sort_unstable();
24299 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
24300 for &(e, w) in &cold {
24301 let host = p[e] / wsum;
24302 let d = (w - host).abs();
24303 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
24304 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
24305 }
24306 }
24307
24308 #[test]
24313 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
24314 let zero = || DenseFfn {
24315 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24316 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24317 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
24318 act: Act::Silu,
24319 down_t: None,
24320 segs: Vec::new(),
24321 };
24322 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
24323 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
24324 experts: vec![zero(), zero(), zero()],
24325 top_k: 1,
24326 norm_topk_prob: norm_topk,
24327 router_sigmoid: false,
24328 sigmoid_denom_eps: 1e-6,
24329 expert_bias: None,
24330 routed_scaling: 1.0,
24331 route_tau: None,
24332 shared: None,
24333 stats: std::cell::RefCell::new(Vec::new()),
24334 act_sq: std::cell::RefCell::new(Vec::new()),
24335 act_rows: std::cell::RefCell::new(Vec::new()),
24336 mask: None,
24337 per_expert_scale: None,
24338 router_input_norm: false,
24339 resonance: resonant.then(|| Resonance {
24340 mu: vec![0.0; 6],
24341 u: Vec::new(),
24342 k: 0,
24343 bias: vec![0.0; 3],
24344 shell: Vec::new(),
24345 }),
24346 grown: Vec::new(),
24347 };
24348 let lo = -0.1f32;
24351 let hi = f32::from_bits(lo.to_bits() - 1);
24352 assert!(hi > lo && hi - lo < 2f32.powi(-25));
24353 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
24354 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
24356 assert_eq!(
24357 idx,
24358 vec![0],
24359 "softmax tie → lower index (the gated contract)"
24360 );
24361 for norm in [true, false] {
24364 let m = moe(true, norm);
24365 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
24366 assert_eq!(idx, vec![1], "norm_topk {norm}");
24367 assert_eq!(p, vec![0.0, 1.0, 0.0]);
24368 assert_eq!(p[1] / wsum, 1.0);
24369 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
24372 assert_eq!(idx, vec![0]);
24373 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
24375 assert_eq!(idx, vec![2]);
24376 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
24377 assert_eq!(idx, vec![0]);
24378 assert_eq!(p[0] / wsum, 1.0);
24379 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
24382 assert_eq!(idx, vec![0]);
24383 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
24384 }
24385 }
24386}