1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16#[path = "clover.rs"]
17pub mod clover;
18#[path = "pipeline_masked.rs"]
20pub mod masked;
21#[path = "pipeline_speech.rs"]
22pub mod speech;
23use crate::kv_cache::KvCache;
24use crate::linear_core::{
25 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
26 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
27 vmf_phase_pair,
28};
29use crate::pool::Pool;
30use crate::qtensor::QTensor;
31use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
32use crate::tokenizer::Tokenizer;
33use cortiq_core::mask::TaskMask;
34use cortiq_core::types::NormStyle;
35
36pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
37 std::sync::atomic::AtomicBool::new(false);
38
39#[derive(Clone)]
46struct GraphMoeBank {
47 experts: Vec<(usize, usize, usize)>,
48 q4tp: Option<bool>,
49 gu_q2: Option<bool>,
50 q82: Option<bool>,
51 act: Option<crate::gpu::GraphAct>,
52 model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
53}
54
55type GraphMoeKey = (usize, usize, usize, usize, usize, usize, usize);
61
62thread_local! {
67 static EXACT_MTP_CALIBRATION: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
68}
69
70struct ExactMtpCalibrationGuard;
71
72impl ExactMtpCalibrationGuard {
73 fn enter() -> Self {
74 EXACT_MTP_CALIBRATION.with(|depth| depth.set(depth.get().saturating_add(1)));
75 Self
76 }
77}
78
79impl Drop for ExactMtpCalibrationGuard {
80 fn drop(&mut self) {
81 EXACT_MTP_CALIBRATION.with(|depth| {
82 debug_assert!(depth.get() > 0, "unbalanced MTP calibration guard");
83 depth.set(depth.get().saturating_sub(1));
84 });
85 }
86}
87
88fn exact_mtp_calibration_active() -> bool {
89 EXACT_MTP_CALIBRATION.with(|depth| depth.get() != 0)
90}
91
92fn pad_mtp_draft_logits(logits: &mut Vec<f32>, vocab_size: usize) {
96 logits.resize(vocab_size, f32::NEG_INFINITY);
97}
98
99struct ForwardScratch {
103 n1: Vec<f32>,
104 n2: Vec<f32>,
105 p1: Vec<f32>,
106 p2: Vec<f32>,
107}
108
109impl ForwardScratch {
110 fn new(hidden: usize) -> Self {
111 Self {
112 n1: vec![0.0; hidden],
113 n2: vec![0.0; hidden],
114 p1: vec![0.0; hidden],
115 p2: vec![0.0; hidden],
116 }
117 }
118}
119
120pub struct Pipeline {
122 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
127 pub tokenizer: std::sync::Arc<Tokenizer>,
130 pub kv_cache: KvCache,
131 pub sampler_config: SamplerConfig,
132 pub weights: PipelineWeights,
133 pub hidden_size: usize,
134 pub intermediate_size: usize,
135 pub num_heads: usize,
136 pub num_kv_heads: usize,
137 pub head_dim: usize,
138 pub num_layers: usize,
140 pub physical_layers: usize,
142 pub loop_final_norm: bool,
144 pub layer_map: Option<Vec<usize>>,
147 pub mhc: Option<Box<crate::sensenova::Mhc>>,
149 pub dash: Option<Box<crate::uocr::Dash>>,
151 dash_batch_on: bool,
153 mhc_r: Vec<f32>,
155 pub mrope_sections: Option<[usize; 3]>,
159 pub prompt_mrope: Option<Vec<[i64; 3]>>,
160 pub vocab_size: usize,
161 pub rms_eps: f64,
162 pub rope_base: f32,
163 pub norm_style: NormStyle,
164 pub rotary_dim: usize,
166 pub attention_heads_per_layer: Option<Vec<usize>>,
168 pub kv_heads_per_layer: Option<Vec<usize>>,
173 pub v_head_dim: Option<usize>,
178 pub layer_dump: Option<std::path::PathBuf>,
191 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
194 pub(crate) mimo_moe: crate::mimo_moe::Slot,
197 pub(crate) minicpm_tail: crate::minicpm_tail::MiniCpmTail,
202 pub vmf_cfg: Option<VmfPhaseCfg>,
204 pub gdn_cfg: Option<GdnCfg>,
206 pub logit_multiplier: Option<f32>,
208 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
213 graph_failed: std::sync::atomic::AtomicBool,
218 graph_moe_banks: std::sync::Mutex<std::collections::HashMap<GraphMoeKey, GraphMoeBank>>,
221 pub kv_history: Vec<u32>,
226 pub kv_history_device: bool,
229 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
231 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
234 pub g4e: Option<Box<(crate::g4e::G4eGlobals, Vec<crate::g4e::G4eLayer>)>>,
237 pub dsv4: Option<
241 Box<(
242 crate::dsv4::Dsv4Globals,
243 Vec<crate::dsv4::Dsv4Layer>,
244 crate::dsv4::Dsv4Cfg,
245 crate::dsv4::Dsv4State,
246 )>,
247 >,
248 pub dory: Option<Box<(crate::dory::Dory, crate::dory::DoryState)>>,
253 pub dsv41: Option<
254 Box<(
255 crate::dsv41::Dsv41Globals,
256 Vec<crate::dsv41::Dsv41Layer>,
257 crate::dsv41::Dsv41Cfg,
258 crate::dsv41::Dsv41State,
259 )>,
260 >,
261 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
263 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
265 pub(crate) clover: Option<std::sync::Arc<clover::Clover>>,
270 clover_stats: clover::CloverStats,
271 clover_pen: clover::Penalties,
273 pub qwen4_exp: Option<
274 Box<(
275 crate::qwen4_exp::Globals,
276 Vec<crate::qwen4_exp::Layer>,
277 crate::qwen4_exp::Cfg,
278 crate::qwen4_exp::State,
279 )>,
280 >,
281 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
285 pub dspark: Option<crate::dsv4::DsparkState>,
287 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
290 pub dspark_hist: Vec<usize>,
292 pub dspark_real: Vec<u32>,
296 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
300 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
302 pub dspark_draft_ns: u128,
306 m3_winners: Vec<Vec<u32>>,
308 m3_counts: Vec<u32>,
310 m3_n_exp: usize,
311 m3_tokens: u64,
312 m3_prewarmed: bool,
313 pub short_conv_cfg: Option<ShortConvCfg>,
316 pub mtp: Option<MtpModule>,
318 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
322 verify_exact_moe: bool,
325 pub speculative: bool,
327 pub ignore_eos: bool,
333 pub draft_full_streak: u32,
339 pub spec_k_adapt: Option<usize>,
347 pub spec_acc_ewma: f32,
349 pub spec_hold: [f32; 7],
352 spec_wall: [f32; 7],
357 spec_wall_n: [u8; 7],
358 spec_k_dwell: u8,
359 spec_plain_ms: (f64, usize),
363 pub dflash_hold: [f32; 7],
366 dflash_last: bool,
367 dflash_wait: u32,
370 dflash_back: u32,
371 dflash_turn: u32,
372 dflash_turn_sum: (f32, f32),
374 dflash_lean: f32,
377 dflash_k_adapt: Option<usize>,
380 dflash_acc_ewma: f32,
381 pub spec_lookup_gain: f32,
385 pub spec_lookup_wide: bool,
388 pub spec_lookup_rest: u32,
389 rng: SplitMix64,
390 sampler_scratch: SamplerScratch,
391 spec_forced: Option<u32>,
397 spec_q: Vec<Vec<f32>>,
398 spec_p: Vec<f32>,
399 spec_res: Vec<f32>,
400 spec_qs: Vec<sampler::Sparse>,
402 spec_ps: sampler::Sparse,
403 spec_ress: sampler::Sparse,
404 mtp_graph_mode: Option<bool>,
411 pub dflash: Option<Box<crate::dflash::Drafter>>,
414 dflash_rows: Option<(usize, Vec<f32>)>,
417 dflash_taps: std::sync::Mutex<Vec<(usize, Vec<f32>)>>,
422 pub(crate) dflash_solo: bool,
425 dflash_tap_from: usize,
429 #[cfg(target_os = "macos")]
432 metal_verify: Option<MetalVerifyPending>,
433 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
437 ws: ForwardScratch,
441 pool: Option<std::sync::Arc<Pool>>,
443 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
447 pub(crate) dyn_force_f32: bool,
449 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
454 pub(crate) dyn_active: Option<usize>,
460 pub(crate) dyn_blend_loaded: bool,
464 pub(crate) dyn_phi_layer: Option<usize>,
467 dyn_phi_ema: Vec<f32>,
469 dyn_phi_seen: usize,
470 pub dyn_router: Option<crate::swarm::DynRouter>,
473 o1_cfg: Option<crate::nystrom::O1Cfg>,
476 pub(crate) o1_turbo: Option<O1Turbo>,
478 mtp_pos_base: usize,
480 o1_prefix_tail: Vec<f32>,
483 gdn_snap: Option<Vec<u32>>,
488 mtp_warm_base: Option<usize>,
492 o1_epoch: u64,
495 o1_flags: Vec<bool>,
497 trace: bool,
500 calib_temp: f32,
503 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
505 graph_kv_id: u64,
506 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
509 graph_want_logits: bool,
510 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
514 graph_head_required: bool,
515 graph_logits: Option<Vec<f32>>,
518 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
521 embryo_wide_refused: bool,
523 graph_refused: std::sync::atomic::AtomicBool,
529 pub embed_multiplier: f32,
531 pub attn_scale: f32,
534 pub swa: Option<(usize, usize)>,
537 pub swa_trim: Option<(usize, usize)>,
544 swa_trim_anchor: usize,
548 pub chunk_min_rows: usize,
552 o1_chunk_prefix: bool,
555 dense_batch_declined: std::sync::atomic::AtomicBool,
558 pub sliding_layers: Option<Vec<bool>>,
561 pub no_rope_layers: Option<Vec<bool>>,
566 pub value_blend: Option<Vec<f32>>,
569 vres_e0: Vec<f32>,
571 vres_in: Vec<f32>,
573 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
579 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
582 pub kv_prefix: KvPrefix,
586 pub last_prefill_tokens: usize,
589 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
592 pub rotary_dim_local: Option<usize>,
593 pub rope_scale: f32,
594 pub rope_scale_local: f32,
595 pub global_attn: Option<(usize, usize)>,
598 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
601 pub attn_v_norm: bool,
603 pub qk_norm_after_rope: bool,
605 pub proj_gate_sigmoid: bool,
608 pub final_softcap: Option<f32>,
610 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
614 pub attn_softcap: f32,
616 confidence_on: bool,
620 #[cfg(test)]
623 nll_test_fail_at: Option<usize>,
624 #[cfg(test)]
627 nll_test_force_serial: bool,
628}
629
630#[cfg(target_os = "macos")]
631impl Drop for Pipeline {
632 fn drop(&mut self) {
633 let _ = crate::gpu_metal::wait_replay();
635 crate::gpu::kv_mirror_drop(self.graph_kv_id);
636 }
637}
638
639#[cfg(not(target_os = "macos"))]
640impl Drop for Pipeline {
641 fn drop(&mut self) {
642 crate::gpu::graph_kv_reset(self.graph_kv_id);
646 }
647}
648
649pub struct PipelineWeights {
654 pub embed_tokens: QTensor,
656 pub layers: Vec<LayerWeights>,
658 pub lm_head: QTensor,
660 pub final_norm: Vec<f32>,
662}
663
664pub struct LayerWeights {
666 pub input_norm: Vec<f32>,
667 pub post_norm: Vec<f32>,
670 pub attn_out_norm: Option<Vec<f32>>,
673 pub layer_scale: Option<f32>,
675 pub ffn_out_norm: Option<Vec<f32>>,
678 pub ffn: FfnKind,
679 pub attn: AttnKind,
680}
681
682#[derive(Clone, Copy, PartialEq, Debug, Default)]
686pub enum Act {
687 #[default]
688 Silu,
689 GeluTanh,
690 Gelu,
692 Situ {
695 beta: f32,
696 linear_beta: f32,
697 },
698 SwiGluOai {
703 alpha: f32,
704 limit: f32,
705 },
706 SiluClamp {
710 limit: f32,
711 },
712}
713
714impl Act {
715 pub fn from_arch(name: &str) -> Self {
716 if name == "gelu_tanh" {
717 Self::GeluTanh
718 } else if name == "gelu" {
719 Self::Gelu
720 } else {
721 Self::Silu
722 }
723 }
724
725 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
727 match arch.hidden_act.as_str() {
728 "situ" => Self::Situ {
729 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
730 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
731 },
732 "swigluoai" if arch.arch_name == "minimax_m3_vl" => Self::SwiGluOai {
737 alpha: 1.702,
738 limit: 7.0,
739 },
740 "silu" if arch.ffn_clamp.is_some_and(|l| l > 0.0) => Self::SiluClamp {
741 limit: arch.ffn_clamp.unwrap_or(0.0),
742 },
743 other => Self::from_arch(other),
744 }
745 }
746
747 #[inline]
749 pub fn clamp_limit(self) -> f32 {
750 match self {
751 Self::SiluClamp { limit } => limit,
752 _ => 0.0,
753 }
754 }
755
756 pub(crate) fn dense_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
760 match self {
761 Self::SiluClamp { limit } => Some((crate::gpu::GraphAct::Silu, limit)),
762 _ => self.wgpu_graph_act().map(|a| (a, 0.0)),
764 }
765 }
766
767 #[inline]
772 pub fn swiglu_oai(alpha: f32, limit: f32) -> Self {
773 Self::SwiGluOai { alpha, limit }
774 }
775
776 #[inline]
777 pub fn apply(self, x: f32) -> f32 {
778 match self {
779 Self::Silu => inference::silu(x),
780 Self::GeluTanh => inference::gelu_tanh(x),
781 Self::Gelu => inference::gelu_erf(x),
782 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
783 Self::SwiGluOai { alpha, limit } => {
784 let gate = x.min(limit);
785 gate * (1.0 / (1.0 + (-alpha * gate).exp()))
786 }
787 Self::SiluClamp { limit } => inference::silu(x.min(limit)),
788 }
789 }
790
791 #[inline]
794 pub fn combine(self, g: f32, u: f32) -> f32 {
795 match self {
796 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
797 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
798 }
799 Self::SwiGluOai { alpha, limit } => {
800 inference::swiglu_oai(g, u, alpha, 1.0, Some(limit))
801 }
802 Self::SiluClamp { limit } => self.apply(g) * u.clamp(-limit, limit),
803 _ => self.apply(g) * u,
804 }
805 }
806
807 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
811 match self {
812 Self::Silu => Some(crate::gpu::GraphAct::Silu),
813 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
814 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
815 Self::Situ { .. } | Self::SwiGluOai { .. } | Self::SiluClamp { .. } => None,
816 }
817 }
818
819 pub(crate) fn wgpu_graph_act(self) -> Option<crate::gpu::GraphAct> {
825 if matches!(self, Self::SiluClamp { .. }) {
828 return None;
829 }
830 self.moe_job_graph_contract().map(|(act, _)| act)
831 }
832
833 pub(crate) fn moe_job_graph_contract(self) -> Option<(crate::gpu::GraphAct, f32)> {
839 match self {
840 Self::SwiGluOai { alpha, limit }
841 if alpha.to_bits() == 1.702f32.to_bits()
842 && limit.to_bits() == 7.0f32.to_bits() =>
843 {
844 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0))
845 }
846 Self::SiluClamp { limit } => Some((crate::gpu::GraphAct::Silu, limit)),
848 _ => self.graph_act().map(|act| (act, 0.0)),
849 }
850 }
851}
852
853pub struct DenseFfn {
855 pub gate_proj: QTensor,
856 pub up_proj: QTensor,
857 pub down_proj: QTensor,
858 pub act: Act,
860 pub down_t: Option<QTensor>,
866 pub segs: Vec<FfnSeg>,
873}
874
875pub struct FfnSeg {
880 pub gate: QTensor,
881 pub up: QTensor,
882 pub down: QTensor,
883 pub start: usize,
884 pub width: usize,
885}
886
887pub enum FfnKind {
890 Dense(DenseFfn),
891 Moe(MoeFfn),
895 DenseMoe(Box<DenseMoeFfn>),
902}
903
904pub struct DenseMoeFfn {
906 pub dense: DenseFfn,
907 pub moe: MoeFfn,
908 pub post_norm_1: Vec<f32>,
910 pub pre_norm_2: Vec<f32>,
913 pub post_norm_2: Vec<f32>,
915}
916
917pub struct MoeFfn {
918 pub router: QTensor,
920 pub experts: Vec<DenseFfn>,
921 pub top_k: usize,
922 pub norm_topk_prob: bool,
923 pub router_sigmoid: bool,
926 pub sigmoid_denom_eps: f32,
932 pub expert_bias: Option<Vec<f32>>,
936 pub routed_scaling: f32,
939 pub route_tau: Option<f32>,
945 pub shared: Option<(DenseFfn, Option<QTensor>)>,
948 pub stats: std::cell::RefCell<Vec<u64>>,
952 pub act_sq: std::cell::RefCell<Vec<f64>>,
959 pub act_rows: std::cell::RefCell<Vec<f32>>,
965 pub mask: Option<Vec<bool>>,
970 pub per_expert_scale: Option<Vec<f32>>,
973 pub router_input_norm: bool,
977 pub resonance: Option<Resonance>,
981 pub grown: Vec<GrownExpert>,
987}
988
989#[derive(Debug, Clone, PartialEq, Eq)]
991pub struct GrownExpert {
992 pub record: String,
994 pub record_index: usize,
996 pub layer: usize,
997 pub expert: usize,
1001}
1002
1003pub struct Resonance {
1005 pub mu: Vec<f32>,
1007 pub u: Vec<f32>,
1009 pub k: usize,
1010 pub bias: Vec<f32>,
1012 pub shell: Vec<f32>,
1018}
1019
1020static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
1023
1024pub fn growth_shell_enabled() -> bool {
1029 use std::sync::atomic::Ordering;
1030 match GROWTH_SHELL.load(Ordering::Relaxed) {
1031 1 => true,
1032 2 => false,
1033 _ => {
1034 let off = std::env::var("CMF_GROWTH_SHELL")
1035 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
1036 .unwrap_or(false);
1037 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
1038 !off
1039 }
1040 }
1041}
1042
1043pub fn set_growth_shell(on: Option<bool>) {
1048 GROWTH_SHELL.store(
1049 match on {
1050 Some(true) => 1,
1051 Some(false) => 2,
1052 None => 0,
1053 },
1054 std::sync::atomic::Ordering::Relaxed,
1055 );
1056}
1057
1058impl Resonance {
1059 pub fn has_shell(&self) -> bool {
1061 self.shell.iter().any(|s| s.is_finite())
1062 }
1063
1064 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
1067 let mut out = vec![f32::INFINITY; ne];
1068 if growth_shell_enabled() {
1069 for (o, s) in out.iter_mut().zip(&self.shell) {
1070 *o = *s;
1071 }
1072 }
1073 out
1074 }
1075
1076 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
1081 let h = x.len();
1082 let ne = out.len();
1083 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
1084 for e in 0..ne {
1085 let mu = &self.mu[e * h..(e + 1) * h];
1086 let mut d2 = 0.0f32;
1087 for j in 0..h {
1088 let d = x[j] - mu[j];
1089 d2 += d * d;
1090 }
1091 let mut proj = 0.0f32;
1092 for i in 0..self.k {
1093 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
1094 let mut p = 0.0f32;
1095 for j in 0..h {
1096 p += (x[j] - mu[j]) * u[j];
1097 }
1098 proj += p * p;
1099 }
1100 let err = d2 - proj;
1101 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
1102 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
1103 out[e] = f32::NEG_INFINITY;
1104 }
1105 }
1106 }
1107}
1108
1109pub enum AttnKind {
1112 Full {
1114 wq: QTensor,
1115 wk: QTensor,
1116 wv: QTensor,
1117 wo: QTensor,
1118 q_norm: Option<Vec<f32>>,
1119 k_norm: Option<Vec<f32>>,
1120 output_gate: bool,
1121 softplus_gate: Option<(QTensor, bool)>,
1125 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
1127 },
1128 Linear(VmfPhaseWeights),
1130 LinearGdn(GdnWeights),
1132 ShortConv(ShortConvWeights),
1135 Mla(Box<MlaWeights>),
1143 Kda(Box<crate::linear_core::KdaWeights>),
1147 Bounded(Box<crate::bounded::BoundedWeights>),
1152}
1153
1154pub struct MlaWeights {
1156 pub q_proj: QTensor,
1160 pub q_a: Option<QTensor>,
1163 pub q_a_norm: Option<Vec<f32>>,
1164 pub kv_a: QTensor,
1166 pub kv_a_norm: Vec<f32>,
1168 pub kv_b: QTensor,
1170 pub o_proj: QTensor,
1172 pub nh: usize,
1173 pub qk_rope: usize,
1174 pub qk_nope: usize,
1175 pub v_dim: usize,
1176 pub lora: usize,
1177 pub scale: f32,
1179 pub nope: bool,
1181}
1182
1183pub struct MtpModule {
1188 pub enorm: Vec<f32>,
1189 pub hnorm: Vec<f32>,
1190 pub eh_proj: QTensor,
1192 pub layer: LayerWeights,
1193 pub final_norm: Vec<f32>,
1194 pub kv: crate::kv_cache::LayerKvCache,
1195}
1196
1197const MTP_PROBE_FFN_ROWS: usize = 128;
1206
1207#[derive(Clone, Debug, PartialEq)]
1212pub struct MtpFfnProbe {
1213 pub mass: Vec<f64>,
1214 pub squared_mass: Vec<f64>,
1215 pub observed_positions: usize,
1216}
1217
1218#[cfg(target_os = "macos")]
1225enum MetalRowsItem<'a> {
1226 Gdn {
1227 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
1228 first: usize,
1229 },
1230 Attn {
1231 l: crate::gpu_metal::AttnGpuLayer<'a>,
1232 li: usize,
1233 q_norm: Option<&'a [f32]>,
1234 k_norm: Option<&'a [f32]>,
1235 output_gate: bool,
1236 head_gate: Option<&'a [f32]>,
1238 },
1239}
1240
1241#[cfg(target_os = "macos")]
1242struct MetalVerifyPending {
1243 graph: crate::gpu_metal::VerifyGraph,
1244 gdn_layers: Vec<usize>,
1245 attn_layers: Vec<(usize, usize)>,
1246}
1247
1248#[cfg(target_os = "macos")]
1252struct MetalWarmPending {
1253 graph: crate::gpu_metal::VerifyGraph,
1254 cpu_stored: usize,
1255 b: usize,
1256}
1257
1258#[cfg(target_os = "macos")]
1259enum MetalRowsRun {
1260 Declined,
1262 Failed,
1265 Completed(MetalVerifyPending),
1266}
1267
1268#[cfg(target_os = "macos")]
1269enum MetalPrefillOutcome {
1270 Declined,
1271 Failed,
1272 Completed(Vec<f32>),
1273}
1274
1275#[cfg(target_os = "macos")]
1276enum MetalBatchNllOutcome {
1277 Declined,
1278 Failed(String),
1279 Completed(f64, usize),
1280}
1281
1282#[derive(Clone, Copy)]
1288enum SpecTrial {
1289 Spec {
1290 t0: std::time::Instant,
1291 gen0: usize,
1292 rounds: usize,
1293 },
1294 Plain {
1295 t0: std::time::Instant,
1296 gen0: usize,
1297 },
1298 Decided {
1299 spec: bool,
1300 recheck_at: usize,
1301 },
1302}
1303
1304pub(crate) fn spec_time_level() -> u8 {
1308 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
1309 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
1310 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
1311 Err(_) => 0,
1312 })
1313}
1314
1315fn graph_moe_bank_on() -> bool {
1318 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1319 *ON.get_or_init(|| std::env::var("CMF_GRAPH_MOE_BANK").as_deref() != Ok("0"))
1320}
1321
1322fn graph_spec_wanted_by_policy(
1330 spec_env: Option<&str>,
1331 spec_default_ok: bool,
1332 penalized: bool,
1333 metal_wgpu: bool,
1334 native_metal: bool,
1335) -> bool {
1336 match spec_env {
1337 Some("0") => false,
1338 Some("1") => true,
1339 Some(_) if !native_metal => true,
1342 _ => spec_default_ok && !penalized && !metal_wgpu,
1347 }
1348}
1349
1350struct SpecStampLog {
1356 t_last: std::time::Instant,
1357 items: Vec<(&'static str, f32)>,
1358}
1359
1360static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
1361
1362pub(crate) fn spec_stamp(name: &'static str) {
1363 if spec_time_level() == 0 {
1364 return;
1365 }
1366 if let Ok(mut g) = SPEC_STAMPS.lock() {
1367 if let Some(log) = g.as_mut() {
1368 let now = std::time::Instant::now();
1369 log.items
1370 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
1371 log.t_last = now;
1372 }
1373 }
1374}
1375
1376fn spec_stamps_begin() {
1377 if spec_time_level() == 0 {
1378 return;
1379 }
1380 if let Ok(mut g) = SPEC_STAMPS.lock() {
1381 *g = Some(SpecStampLog {
1382 t_last: std::time::Instant::now(),
1383 items: Vec::with_capacity(64),
1384 });
1385 }
1386}
1387
1388fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1389 SPEC_STAMPS
1390 .lock()
1391 .ok()
1392 .and_then(|mut g| g.take())
1393 .map(|l| l.items)
1394 .unwrap_or_default()
1395}
1396
1397fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1400 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1401 for &(n, ms) in items {
1402 match agg.iter_mut().find(|e| e.0 == n) {
1403 Some(e) => {
1404 e.1 += ms;
1405 e.2 += 1;
1406 }
1407 None => agg.push((n, ms, 1)),
1408 }
1409 }
1410 let mut s = String::with_capacity(agg.len() * 16);
1411 for (n, ms, k) in agg {
1412 if k > 1 {
1413 s.push_str(&format!("{n} {ms:.1}/{k} "));
1414 } else {
1415 s.push_str(&format!("{n} {ms:.1} "));
1416 }
1417 }
1418 s
1419}
1420
1421#[derive(Default, Clone, Copy)]
1443struct SpecMon {
1444 round_ms: f64,
1445 tokens: f64,
1446 plain_ms: f64,
1447 n: u32,
1448 fails: u32,
1449 zero_accept_rounds: u8,
1454 metal: bool,
1455 cuda: bool,
1459}
1460
1461const METAL_ROUND_COST: [f32; 7] = [1.10, 1.275, 1.54, 1.59, 1.65, 1.70, 1.76];
1467const CUDA_ROUND_COST: [f32; 7] = [1.12, 1.15, 1.17, 1.19, 1.21, 1.22, 1.24];
1471const CUDA_BLOCK_DRAFT: f32 = 0.19;
1474const CUDA_DRAFT_STEP: f32 = 0.03;
1475const SPEC_HOLD_PRIOR: [f32; 7] = [0.80, 0.75, 0.50, 0.67, 0.65, 0.69, 0.67];
1480
1481fn metal_spec_depth(hold: &[f32; 7]) -> usize {
1485 metal_spec_plan(hold, 0.0).0
1486}
1487
1488fn metal_spec_plan(hold: &[f32; 7], draft: f32) -> (usize, f32) {
1493 spec_plan_at(&METAL_ROUND_COST, hold, draft, 0.0)
1494}
1495
1496fn spec_plan_at(cost: &[f32; 7], hold: &[f32; 7], draft: f32, step: f32) -> (usize, f32) {
1499 let mut best = (1usize, 0f32);
1500 let (mut tokens, mut reach) = (1.0f32, 1.0f32);
1501 for k in 1..=7 {
1502 reach *= hold[k - 1];
1503 tokens += reach;
1504 let rate = tokens / (cost[k - 1] + draft + step * k as f32);
1505 if rate > best.1 * 1.04 {
1507 best = (k, rate);
1508 }
1509 }
1510 best
1511}
1512
1513fn wgpu_spec_depth(hold: &[f32; 7], wall: &[f32; 7], k_now: usize, k_min: usize, k_max: usize) -> Option<usize> {
1524 let mut timed = (1..=7usize).filter(|&k| wall[k - 1] > 0.0);
1525 let (first, second) = (timed.next()?, timed.next()?);
1526 let price = |k: usize| -> f32 {
1527 if wall[k - 1] > 0.0 {
1528 return wall[k - 1];
1529 }
1530 let (mut a, mut b) = (first, second);
1532 for j in (1..=7usize).filter(|&j| wall[j - 1] > 0.0 && j != first && j != second) {
1533 let far = if a.abs_diff(k) >= b.abs_diff(k) { &mut a } else { &mut b };
1534 if j.abs_diff(k) < far.abs_diff(k) {
1535 *far = j;
1536 }
1537 }
1538 let (a, b) = (a.min(b), a.max(b));
1539 let slope = ((wall[b - 1] - wall[a - 1]) / (b - a) as f32).max(0.0);
1540 (wall[a - 1] + slope * (k as f32 - a as f32)).max(wall[a - 1] * 0.5)
1541 };
1542 let (k_min, k_max) = (k_min.clamp(1, 7), k_max.clamp(1, 7));
1543 let (mut tokens, mut reach) = (1.0f32, 1.0f32);
1544 let mut rate = [0f32; 7];
1545 for k in 1..=7 {
1546 reach *= hold[k - 1];
1547 tokens += reach;
1548 rate[k - 1] = tokens / price(k);
1549 }
1550 let k_now = k_now.clamp(k_min, k_max);
1551 let best = (k_min..=k_max).max_by(|&a, &b| rate[a - 1].total_cmp(&rate[b - 1]))?;
1552 Some(if rate[best - 1] > rate[k_now - 1] * 1.04 { best } else { k_now })
1553}
1554
1555const DFLASH_ROUND_DRAFT: f32 = 0.44;
1560const DFLASH_HOLD_PRIOR: [f32; 7] = [0.85, 0.82, 0.80, 0.80, 0.80, 0.80, 0.80];
1564const DFLASH_WAIT: u32 = 48;
1571const DFLASH_WAIT_MOST: u32 = 768;
1572pub const DFLASH_LEAN_STEP: f32 = 0.5;
1577pub const DFLASH_LEAN_BAND: f32 = 0.0;
1578pub const DFLASH_COPY_MIN: usize = 8;
1584const DFLASH_LOOKUP_KEEPS: f32 = 5.0;
1588const DFLASH_WAIT_DIP: u32 = 8;
1589const DFLASH_TURN: u32 = 16;
1590
1591pub struct DflashStart<'a> {
1593 pub seq: &'a [u32],
1595 pub after: &'a [u32],
1597 pub block: &'a [u32],
1599 pub chain: &'a [u32],
1601}
1602
1603pub fn dflash_round_values(starts: &[DflashStart<'_>]) -> Vec<((f32, f32), (f32, f32))> {
1610 dflash_round_values_at(starts, &METAL_ROUND_COST, DFLASH_ROUND_DRAFT, 0.0)
1611}
1612
1613pub fn dflash_round_values_at(
1617 starts: &[DflashStart<'_>],
1618 cost: &[f32; 7],
1619 block_draft: f32,
1620 chain_step: f32,
1621) -> Vec<((f32, f32), (f32, f32))> {
1622 struct Side {
1624 hold: [f32; 7],
1625 gain: f32,
1626 rest: u32,
1627 draft: f32,
1628 step: f32,
1629 }
1630 let mut seq: Vec<u32> = Vec::new();
1631 let mut round = |side: &mut Side, s: &DflashStart<'_>, own: &[u32]| -> (f32, f32) {
1632 let k = spec_plan_at(cost, &side.hold, side.draft, side.step).0.min(own.len()).max(1);
1633 let mut drafts = own[..k.min(own.len())].to_vec();
1634 if k < 7 && drafts.len() == k {
1635 if side.rest > 0 {
1636 side.rest -= 1;
1637 } else {
1638 seq.clear();
1639 seq.extend_from_slice(s.seq);
1640 seq.extend_from_slice(&drafts);
1641 let more = lookup_continuation(&seq, k + LOOKUP_MIN_REAL, 7 - k).to_vec();
1642 drafts.extend(more);
1643 }
1644 }
1645 let kept = drafts.iter().zip(s.after).take_while(|(d, t)| d == t).count();
1646 if drafts.len() > k {
1647 side.gain += 0.2 * (kept.saturating_sub(k) as f32 - side.gain);
1648 if side.gain < LOOKUP_PAYS {
1649 (side.rest, side.gain) = (24, 1.6);
1650 }
1651 }
1652 spec_hold_update(&mut side.hold, k, kept.min(k));
1653 ((1 + kept) as f32, cost[drafts.len().clamp(1, 7) - 1] + side.draft + side.step * k as f32)
1654 };
1655 let mut block = Side { hold: DFLASH_HOLD_PRIOR, gain: 2.0, rest: 0, draft: block_draft, step: 0.0 };
1656 let mut chain = Side { hold: SPEC_HOLD_PRIOR, gain: 2.0, rest: 0, draft: 0.0, step: chain_step };
1657 starts.iter().map(|s| (round(&mut block, s, s.block), round(&mut chain, s, s.chain))).collect()
1658}
1659
1660fn dflash_rows_log() -> Option<&'static std::sync::Mutex<std::fs::File>> {
1669 static LOG: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> = std::sync::OnceLock::new();
1670 LOG.get_or_init(|| {
1671 let path = std::env::var("CMF_DFLASH_ROWS").ok()?;
1672 match std::fs::OpenOptions::new().create(true).append(true).open(&path) {
1673 Ok(f) => Some(std::sync::Mutex::new(f)),
1674 Err(e) => {
1675 tracing::warn!("CMF_DFLASH_ROWS: {path}: {e}");
1676 None
1677 }
1678 }
1679 })
1680 .as_ref()
1681}
1682
1683const DFLASH_TAPS_ROWS: usize = 16384;
1686
1687#[cfg(feature = "gpu")]
1693fn solo_script() -> Option<&'static [u32]> {
1694 static IDS: std::sync::OnceLock<Option<Vec<u32>>> = std::sync::OnceLock::new();
1695 IDS.get_or_init(|| {
1696 let path = std::env::var("CMF_SPEC_SCRIPT").ok()?;
1697 match std::fs::read_to_string(&path) {
1698 Ok(s) => Some(s.split_whitespace().filter_map(|t| t.parse().ok()).collect()),
1699 Err(e) => {
1700 tracing::warn!("CMF_SPEC_SCRIPT: {path}: {e}");
1701 None
1702 }
1703 }
1704 })
1705 .as_deref()
1706}
1707
1708fn f32_bytes(v: &[f32]) -> Vec<u8> {
1710 v.iter().flat_map(|x| x.to_le_bytes()).collect()
1711}
1712
1713#[cfg(feature = "gpu")]
1717struct SoloRound {
1718 accepted: Vec<u32>,
1719 drafted: usize,
1720 hidden: Vec<f32>,
1721 logits: Vec<f32>,
1722}
1723
1724#[cfg(feature = "gpu")]
1727struct SoloSpec {
1728 k: usize,
1729 rest: u32,
1732 back: u32,
1733 plain_ms: f32,
1736 plain_n: u32,
1737 round_ms: f32,
1738 rounds: u32,
1739 win_ms: f32,
1742 win_tok: u32,
1743 last: Option<(std::time::Instant, usize)>,
1745 first: bool,
1751}
1752
1753#[cfg(feature = "gpu")]
1759const SOLO_K_MOST: usize = 7;
1760#[cfg(feature = "gpu")]
1765const SOLO_K_AUTO: usize = 5;
1766#[cfg(feature = "gpu")]
1767const SOLO_TRIAL: u32 = 6;
1768#[cfg(feature = "gpu")]
1770const SOLO_JUDGE: u32 = 3;
1771#[cfg(feature = "gpu")]
1772const SOLO_REST: u32 = 32;
1773#[cfg(feature = "gpu")]
1774const SOLO_REST_MOST: u32 = 512;
1775
1776fn dflash_pick() -> Option<bool> {
1780 static PICK: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
1781 *PICK.get_or_init(|| match std::env::var("CMF_DFLASH_PICK").as_deref() {
1782 Ok("dflash") => Some(true),
1783 Ok("mtp") => Some(false),
1784 _ => None,
1785 })
1786}
1787
1788fn spec_hold_update(hold: &mut [f32; 7], k: usize, accepted: usize) {
1794 let k = k.clamp(1, 7);
1795 for j in 1..=k {
1796 if accepted + 1 >= j {
1797 let hit = if accepted >= j { 1.0 } else { 0.0 };
1798 hold[j - 1] += 0.1 * (hit - hold[j - 1]);
1799 }
1800 }
1801 if k < 7 && hold[k - 1] >= 0.9 {
1802 for j in k + 1..=7 {
1803 hold[j - 1] += 0.005 * (hold[k - 1] - hold[j - 1]);
1804 }
1805 }
1806}
1807
1808const LOOKUP_MIN_REAL: usize = 3;
1811const LOOKUP_PAYS: f32 = 1.0;
1815
1816fn lookup_continuation(seq: &[u32], min_len: usize, want: usize) -> &[u32] {
1821 const CAP: usize = 32;
1822 let n = seq.len();
1823 if want == 0 || min_len == 0 || n <= min_len {
1824 return &[];
1825 }
1826 let last = seq[n - 1];
1827 let mut best = (0usize, 0usize);
1828 for p in (0..n - 1).rev() {
1829 if seq[p] != last {
1830 continue;
1831 }
1832 let mut l = 1;
1833 while l < CAP && l <= p && seq[p - l] == seq[n - 1 - l] {
1834 l += 1;
1835 }
1836 if l > best.0 {
1837 best = (l, p);
1838 if l == CAP {
1839 break;
1840 }
1841 }
1842 }
1843 if best.0 < min_len {
1844 return &[];
1845 }
1846 let from = best.1 + 1;
1847 &seq[from..(from + want).min(n)]
1848}
1849
1850pub fn tail_repeat(seq: &[u32]) -> usize {
1855 const CAP: usize = 32;
1856 let n = seq.len();
1857 if n < 2 {
1858 return 0;
1859 }
1860 let last = seq[n - 1];
1861 let mut best = 0usize;
1862 for p in (0..n - 1).rev() {
1863 if seq[p] != last {
1864 continue;
1865 }
1866 let mut l = 1;
1867 while l < CAP && l <= p && seq[p - l] == seq[n - 1 - l] {
1868 l += 1;
1869 }
1870 if l > best {
1871 best = l;
1872 if l == CAP {
1873 break;
1874 }
1875 }
1876 }
1877 best
1878}
1879
1880const SPEC_ZERO_ROUNDS: u8 = 3;
1887const SPEC_RECHECK_THIN: usize = 32;
1891const SPEC_RECHECK_PLAIN: usize = 128;
1892
1893const SPEC_PROXY_TOKENS: f64 = 3.5;
1896const SPEC_PROXY_TOKENS_CUDA: f64 = 2.0;
1899const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1902
1903impl SpecMon {
1904 fn round(&mut self, dt_ms: f64, produced: usize) {
1905 if produced <= 1 {
1906 self.zero_accept_rounds = self.zero_accept_rounds.saturating_add(1);
1907 } else {
1908 self.zero_accept_rounds = 0;
1909 }
1910 self.n += 1;
1911 if self.n == 1 {
1912 return; }
1914 let a = if self.n == 2 { 1.0 } else { 0.3 };
1915 self.round_ms += a * (dt_ms - self.round_ms);
1916 self.tokens += a * (produced as f64 - self.tokens);
1917 }
1918 fn pays(&self) -> bool {
1919 if self.plain_ms > 0.0 {
1920 self.tokens * self.plain_ms > self.round_ms * 1.03
1921 } else {
1922 (self.metal && self.tokens >= SPEC_PROXY_TOKENS) || (self.cuda && self.tokens >= SPEC_PROXY_TOKENS_CUDA)
1923 }
1924 }
1925 fn repeated_zero_acceptance(&self) -> bool {
1926 self.zero_accept_rounds >= SPEC_ZERO_ROUNDS
1927 }
1928 fn recheck_plain_at(&self, generated: usize) -> usize {
1931 generated + if self.n < 6 { SPEC_RECHECK_THIN } else { SPEC_RECHECK_PLAIN }
1932 }
1933 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1935 let n = generated.saturating_sub(gen0);
1936 if n >= 8 {
1937 return true;
1938 }
1939 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1940 }
1941}
1942
1943pub const KV_PREFIX_TAIL: usize = 128;
1946
1947#[derive(Debug, Clone, Default)]
1953pub struct KvPrefix {
1954 len: usize,
1955 hash: u64,
1956 tail: Vec<u32>,
1957 device: bool,
1962}
1963
1964impl KvPrefix {
1965 #[inline]
1966 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1967 for &id in ids {
1968 h ^= id as u64;
1969 h = h.wrapping_mul(0x100000001b3);
1970 h ^= h >> 29;
1971 }
1972 h
1973 }
1974
1975 pub fn clear(&mut self) {
1976 self.len = 0;
1977 self.hash = 0xcbf29ce484222325;
1978 self.tail.clear();
1979 self.device = false;
1980 }
1981
1982 pub fn on_device(&self) -> bool {
1984 self.device
1985 }
1986
1987 pub fn set_on_device(&mut self, device: bool) {
1989 self.device = device;
1990 }
1991
1992 pub fn len(&self) -> usize {
1994 self.len
1995 }
1996
1997 pub fn is_empty(&self) -> bool {
1998 self.len == 0
1999 }
2000
2001 pub fn tail_len(&self) -> usize {
2003 self.tail.len()
2004 }
2005
2006 pub fn set(&mut self, ids: &[u32]) {
2008 self.clear();
2009 self.extend(ids);
2010 }
2011
2012 pub fn extend(&mut self, more: &[u32]) {
2014 if self.len == 0 && self.hash == 0 {
2015 self.hash = 0xcbf29ce484222325;
2016 }
2017 self.hash = Self::fold(self.hash, more);
2018 self.len += more.len();
2019 if more.len() >= KV_PREFIX_TAIL {
2020 self.tail.clear();
2021 self.tail
2022 .extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
2023 } else {
2024 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
2025 self.tail.drain(..drop);
2026 self.tail.extend_from_slice(more);
2027 }
2028 }
2029
2030 pub fn extension(&self, ids: &[u32]) -> usize {
2034 if self.len == 0 || ids.len() <= self.len {
2035 return 0;
2036 }
2037 let t = self.tail.len();
2038 if ids[self.len - t..self.len] != self.tail[..] {
2039 return 0;
2040 }
2041 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
2042 return 0;
2043 }
2044 self.len
2045 }
2046}
2047
2048pub struct GenerateResult {
2050 pub text: String,
2051 pub token_ids: Vec<u32>,
2052 pub prompt_tokens: usize,
2053 pub tokens_generated: usize,
2054 pub finish_reason: String,
2055 pub mtp_drafted: usize,
2057 pub mtp_accepted: usize,
2058 pub token_confidence: Vec<f32>,
2063 pub traces: Vec<TokenTrace>,
2066}
2067
2068#[derive(Clone, Debug)]
2073pub struct TokenTrace {
2074 pub t: usize,
2076 pub token_id: u32,
2078 pub confidence: f32,
2080 pub active_skill: Option<String>,
2082 pub recon: Option<f32>,
2086 pub switched: bool,
2089}
2090
2091#[cfg_attr(not(test), allow(dead_code))]
2096fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
2097 let t = if temp > 1e-3 { temp } else { 1.0 };
2098 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
2099 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
2100 if sum > 0.0 {
2101 (((logits[id as usize] - max) / t).exp()) / sum
2102 } else {
2103 0.0
2104 }
2105}
2106
2107fn prefill_batched() -> bool {
2110 std::env::var("CMF_PREFILL")
2111 .map(|v| v != "seq")
2112 .unwrap_or(true)
2113}
2114
2115#[inline]
2119fn rel_l2(a: &[f32], b: &[f32]) -> f64 {
2121 if a.len() != b.len() {
2122 return f64::INFINITY;
2123 }
2124 let (mut d, mut r) = (0f64, 0f64);
2125 for (&x, &y) in a.iter().zip(b) {
2126 let e = (x - y) as f64;
2127 d += e * e;
2128 r += y as f64 * y as f64;
2129 }
2130 (d / r.max(1e-30)).sqrt()
2131}
2132
2133fn nll_graph_policy(unmasked: bool, prefer_graph: bool, native_metal: bool) -> (bool, bool) {
2134 let graph_quality = unmasked && prefer_graph;
2135 let fused_head_quality = graph_quality && native_metal;
2136 (graph_quality, fused_head_quality)
2137}
2138
2139#[derive(Clone, Copy)]
2143enum PrefillIn<'a> {
2144 Ids(&'a [u32]),
2145 Hidden(&'a [f32]),
2146}
2147
2148#[derive(Clone)]
2156pub(crate) struct O1Turbo {
2157 pub(crate) cfg: crate::nystrom::O1Cfg,
2158 pub(crate) seal: usize,
2160 pub(crate) exact: crate::dory::ExactRows,
2161}
2162
2163const O1_PREFIX_MAX: usize = 8192;
2166
2167pub(crate) fn o1_seal_for_card(seal: usize, weight_bytes: u64) -> usize {
2172 o1_seal_for_card_rows(seal, weight_bytes, 190 << 10)
2173}
2174
2175pub(crate) fn o1_seal_for_card_rows(seal: usize, weight_bytes: u64, row_bytes: u64) -> usize {
2177 if let Some(s) = std::env::var("CMF_O1_PREFIX_MAX").ok().and_then(|v| v.parse().ok()) {
2178 return s;
2179 }
2180 let mut budget = crate::gpu::vram_budget();
2181 let mut row = row_bytes.max(1);
2184 #[cfg(target_os = "macos")]
2185 if budget == u64::MAX {
2186 budget = crate::gpu_metal::working_set_bytes();
2187 row *= 2;
2188 }
2189 if budget == 0 || budget == u64::MAX {
2190 return seal;
2191 }
2192 let room = budget.saturating_sub(weight_bytes + (1800 << 20));
2193 seal.min(((room / row) as usize).max(O1_PREFIX_MAX / 2))
2194}
2195
2196pub(crate) enum ArchStore {
2199 Ram(Vec<f32>),
2200 Disk { path: std::path::PathBuf, file: std::io::BufWriter<std::fs::File> },
2201}
2202
2203impl ArchStore {
2204 pub(crate) fn new(dir: Option<&std::path::Path>, name: String) -> Result<Self, String> {
2205 let Some(dir) = dir else { return Ok(Self::Ram(Vec::new())) };
2206 std::fs::create_dir_all(dir).map_err(|e| format!("O(1) spill {}: {e}", dir.display()))?;
2207 let path = dir.join(format!("cortiq-o1-{}-{name}.rows", std::process::id()));
2208 let file = std::fs::File::create(&path).map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2209 Ok(Self::Disk { path, file: std::io::BufWriter::with_capacity(1 << 22, file) })
2210 }
2211
2212 pub(crate) fn push(&mut self, rows: &[f32]) -> Result<(), String> {
2213 match self {
2214 Self::Ram(v) => {
2215 v.extend_from_slice(rows);
2216 Ok(())
2217 }
2218 Self::Disk { path, file } => {
2219 use std::io::Write;
2220 let bytes = unsafe { std::slice::from_raw_parts(rows.as_ptr().cast::<u8>(), rows.len() * 4) };
2222 file.write_all(bytes).map_err(|e| format!("O(1) spill {}: {e}", path.display()))
2223 }
2224 }
2225 }
2226
2227 pub(crate) fn freeze(mut self) -> Result<ArchRows, String> {
2228 match self {
2229 Self::Ram(ref mut v) => Ok(ArchRows::Ram(std::mem::take(v))),
2230 Self::Disk { ref mut path, ref mut file } => {
2231 use std::io::Write;
2232 file.flush().map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2233 let f = std::fs::File::open(&*path).map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2234 let map = unsafe { memmap2::Mmap::map(&f) }.map_err(|e| format!("O(1) spill {}: {e}", path.display()))?;
2236 Ok(ArchRows::Disk { map: Some(map), path: std::mem::take(path) })
2237 }
2238 }
2239 }
2240}
2241
2242impl Drop for ArchStore {
2243 fn drop(&mut self) {
2244 if let Self::Disk { path, .. } = self {
2245 if !path.as_os_str().is_empty() {
2247 let _ = std::fs::remove_file(&*path);
2248 }
2249 }
2250 }
2251}
2252
2253pub(crate) enum ArchRows {
2254 Ram(Vec<f32>),
2255 Disk { map: Option<memmap2::Mmap>, path: std::path::PathBuf },
2256}
2257
2258impl ArchRows {
2259 pub(crate) fn as_slice(&self) -> &[f32] {
2260 match self {
2261 Self::Ram(v) => v,
2262 Self::Disk { map, .. } => {
2263 let b = map.as_ref().map_or(&[][..], |m| &m[..]);
2264 unsafe { std::slice::from_raw_parts(b.as_ptr().cast::<f32>(), b.len() / 4) }
2266 }
2267 }
2268 }
2269}
2270
2271impl Drop for ArchRows {
2272 fn drop(&mut self) {
2273 if let Self::Disk { map, path } = self {
2274 *map = None;
2275 let _ = std::fs::remove_file(&*path);
2276 }
2277 }
2278}
2279
2280struct O1Archive {
2283 dir: Option<std::path::PathBuf>,
2284 layers: Vec<O1ArchLayer>,
2285 rows: usize,
2286 stride: usize,
2287 nh: usize,
2288 nk: usize,
2289 hd: usize,
2290}
2291
2292impl O1Archive {
2293 fn new() -> Self {
2294 Self { dir: std::env::var_os("CMF_O1_SPILL").map(Into::into), layers: Vec::new(), rows: 0, stride: 1, nh: 0, nk: 0, hd: 0 }
2295 }
2296}
2297
2298struct O1ArchLayer {
2299 li: usize,
2300 k: ArchStore,
2301 v: ArchStore,
2302 q: ArchStore,
2303}
2304
2305impl Pipeline {
2306 pub(crate) fn can_prefill_batched(&self) -> bool {
2307 #[cfg(test)]
2308 let force_serial = self.nll_test_force_serial;
2309 #[cfg(not(test))]
2310 let force_serial = false;
2311 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
2312 }
2313
2314 fn automatic_gpu_prefix(&self) -> Option<usize> {
2317 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
2318 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
2319 }
2320
2321 pub fn prefill_chunk(&self) -> usize {
2325 let env = env_prefill_chunk();
2326 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
2327 return prefill_chunk_rule(env, ChunkHost::here(), false);
2328 }
2329 let facts = self.chunk_stack_facts();
2330 if facts.dense_on_cpu() {
2331 return CPU_DENSE_PREFILL_CHUNK;
2332 }
2333 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete())
2334 }
2335
2336 fn spec_lookup_on() -> bool {
2339 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2340 *ON.get_or_init(|| std::env::var("CMF_SPEC_LOOKUP").as_deref() != Ok("0"))
2341 }
2342
2343 fn metal_small_batch_verify(&self) -> bool {
2347 #[cfg(target_os = "macos")]
2348 {
2349 crate::gpu::q1_force()
2350 && crate::gpu_metal::small_batch_mv()
2351 && self.weights.layers.first().is_some_and(|l| match &l.ffn {
2352 FfnKind::Dense(d) => d.gate_proj.mapped_q4tp().is_some(),
2353 _ => false,
2354 })
2355 }
2356 #[cfg(not(target_os = "macos"))]
2357 {
2358 false
2359 }
2360 }
2361
2362 fn per_token_stack(&self) -> bool {
2366 self.g3n.is_some() || self.g4e.is_some()
2367 }
2368
2369 fn chunk_stack_facts(&self) -> ChunkStackFacts {
2370 let plain_dense = !self.weights.layers.is_empty()
2371 && !self.per_token_stack()
2372 && self.dsv4.is_none()
2373 && self.dsv41.is_none()
2374 && self.dory.is_none()
2375 && self.qwen4_exp.is_none()
2376 && self.weights.layers.iter().all(|lw| {
2377 matches!(lw.attn, AttnKind::Full { .. } | AttnKind::ShortConv(_))
2382 && matches!(lw.ffn, FfnKind::Dense(_))
2383 });
2384 let gpu_on = crate::gpu::enabled();
2385 ChunkStackFacts {
2386 plain_dense,
2387 discrete: gpu_on && crate::gpu::discrete(),
2388 gpu_on,
2389 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
2392 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
2393 multi_gpu: self.gpu_plan.is_some(),
2394 o1: self.o1_active(),
2395 }
2396 }
2397}
2398
2399pub fn prefill_chunk() -> usize {
2408 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
2409}
2410
2411fn env_prefill_chunk() -> Option<usize> {
2412 std::env::var("CMF_PREFILL_CHUNK")
2413 .ok()
2414 .and_then(|v| v.parse::<usize>().ok())
2415}
2416
2417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2419enum ChunkHost {
2420 Macos,
2421 Aarch64,
2423 Other,
2425}
2426
2427impl ChunkHost {
2428 fn here() -> Self {
2429 if cfg!(target_os = "macos") {
2430 ChunkHost::Macos
2431 } else if cfg!(target_arch = "aarch64") {
2432 ChunkHost::Aarch64
2433 } else {
2434 ChunkHost::Other
2435 }
2436 }
2437}
2438
2439const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
2446
2447const CPU_DENSE_PREFILL_CHUNK: usize = 128;
2452
2453fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
2459 if let Some(n) = env {
2460 return n.max(1);
2461 }
2462 match host {
2463 ChunkHost::Macos => 512,
2464 ChunkHost::Aarch64 => 256,
2467 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
2468 ChunkHost::Other => 48,
2469 }
2470}
2471
2472#[derive(Clone, Copy, Debug, Default)]
2474struct ChunkStackFacts {
2475 plain_dense: bool,
2479 discrete: bool,
2481 gpu_on: bool,
2483 capacity_split: bool,
2485 multi_gpu: bool,
2487 o1: bool,
2489}
2490
2491impl ChunkStackFacts {
2492 fn dense_on_cpu(self) -> bool {
2495 cfg!(target_arch = "x86_64")
2496 && self.plain_dense
2497 && !self.gpu_on
2498 && !self.multi_gpu
2499 && !self.o1
2500 }
2501
2502 fn dense_on_discrete(self) -> bool {
2503 self.plain_dense
2504 && self.discrete
2505 && self.gpu_on
2506 && !self.capacity_split
2507 && !self.multi_gpu
2508 && !self.o1
2509 }
2510}
2511
2512#[inline]
2518fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
2519 if end <= start || start >= input_len {
2520 return 0;
2521 }
2522 let rows = (end.min(input_len) - start).min(input_len - start);
2523 if end < input_len {
2524 rows
2525 } else {
2526 rows.saturating_sub(1)
2527 }
2528}
2529
2530pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
2532
2533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2535pub(crate) struct ReuseLayer {
2536 pub full: bool,
2539 pub host_rows: usize,
2542 pub device_rows: Option<usize>,
2544 pub device_state: bool,
2546}
2547
2548#[derive(Debug, Clone, PartialEq, Eq)]
2550pub(crate) enum ReusePlan {
2551 Ready,
2553 Pull(Vec<(usize, usize, usize)>),
2556 Fresh,
2558}
2559
2560pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
2570 let mut pulls = Vec::new();
2571 for (li, l) in layers.iter().enumerate() {
2572 if !l.full {
2573 if l.device_state {
2574 return ReusePlan::Fresh;
2575 }
2576 continue;
2577 }
2578 if l.host_rows == reuse_from {
2579 continue;
2580 }
2581 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
2582 pulls.push((li, l.host_rows, reuse_from));
2583 continue;
2584 }
2585 return ReusePlan::Fresh;
2586 }
2587 if pulls.is_empty() {
2588 ReusePlan::Ready
2589 } else {
2590 ReusePlan::Pull(pulls)
2591 }
2592}
2593
2594impl Pipeline {
2595 fn reset_fresh_mtp_draft_state(&mut self) {
2598 self.draft_full_streak = 0;
2599 }
2600
2601 fn clear_sequence_state(&mut self) {
2609 #[cfg(target_os = "macos")]
2612 let _ = crate::gpu_metal::wait_replay();
2613 self.kv_cache.clear();
2614 self.clear_history();
2617 self.graph_logits = None;
2618 if let Some(d) = &mut self.dflash {
2619 d.reset();
2620 }
2621 self.dflash_rows = None;
2622 self.dflash_taps.lock().unwrap().clear();
2623 self.dflash_hold = DFLASH_HOLD_PRIOR;
2625 (self.dflash_last, self.dflash_wait, self.dflash_back, self.dflash_turn) = (false, 0, DFLASH_WAIT, 0);
2626 self.dflash_turn_sum = (0.0, 0.0);
2627 self.dflash_lean = 0.0;
2628 if let Some(b) = &mut self.dsv41 {
2629 b.3.clear();
2630 }
2631 if let Some(b) = &mut self.dory {
2632 b.1.clear();
2633 crate::gpu::dory_reset(self.graph_kv_id);
2634 }
2635 crate::gpu::graph_kv_reset(self.graph_kv_id);
2636 self.gdn_snap = None;
2638 self.mtp_warm_base = None;
2639 crate::gpu::graph_kv_reset(self.mtp_kv_id());
2644 }
2645
2646 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
2653 if self.dory.is_some() || self.graph_prefill_preferred() {
2654 return true;
2655 }
2656 let kv_id = self.graph_kv_id;
2657 let layers: Vec<ReuseLayer> = (0..self.num_layers)
2658 .map(|li| {
2659 let full = matches!(
2660 self.weights.layers[self.phys_layer(li)].attn,
2661 AttnKind::Full { .. }
2662 );
2663 ReuseLayer {
2664 full,
2665 host_rows: self.kv_cache.layers[li].pos_len(),
2668 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
2669 device_state: crate::gpu::graph_state_resident(kv_id, li),
2670 }
2671 })
2672 .collect();
2673 if layers
2677 .iter()
2678 .all(|l| l.device_rows.is_none() && !l.device_state)
2679 {
2680 return true;
2681 }
2682 let plan = kv_reuse_plan(reuse_from, &layers);
2683 let (what, rows, n) = match &plan {
2684 ReusePlan::Ready => ("host ready", 0, 0),
2685 ReusePlan::Fresh => ("fresh", 0, 0),
2686 ReusePlan::Pull(p) => (
2687 "pull",
2688 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
2689 p.len(),
2690 ),
2691 };
2692 let t0 = std::time::Instant::now();
2693 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
2694 if std::env::var("CMF_PREFILL_PROF").is_ok() {
2695 eprintln!(
2696 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
2697 if ok { "" } else { " (failed → fresh)" },
2698 t0.elapsed().as_secs_f64() * 1e3
2699 );
2700 }
2701 ok
2702 }
2703
2704 fn apply_kv_reuse_plan(
2705 &mut self,
2706 reuse_from: usize,
2707 plan: ReusePlan,
2708 layers: &[ReuseLayer],
2709 ) -> bool {
2710 let kv_id = self.graph_kv_id;
2711 match plan {
2712 ReusePlan::Fresh => return false,
2713 ReusePlan::Ready => {}
2714 ReusePlan::Pull(pulls) => {
2715 let (nkv, hd) = {
2720 let c = &self.kv_cache.layers[pulls[0].0];
2721 (c.num_kv_heads, c.head_dim)
2722 };
2723 let uniform = pulls.iter().all(|&(li, _, _)| {
2724 let c = &self.kv_cache.layers[li];
2725 (c.num_kv_heads, c.head_dim) == (nkv, hd)
2726 });
2727 let batched = if uniform {
2728 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
2729 } else {
2730 None
2731 };
2732 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
2733 Some(rows) => rows,
2734 None => {
2735 let mut rows = Vec::with_capacity(pulls.len());
2736 for &(li, from, to) in &pulls {
2737 let (lnkv, lhd) = {
2738 let c = &self.kv_cache.layers[li];
2739 (c.num_kv_heads, c.head_dim)
2740 };
2741 let Some((k, v, first_valid)) =
2742 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
2743 else {
2744 return false;
2745 };
2746 let need_from = match self.layer_window(li) {
2750 Some(w) => from.max((to + 1).saturating_sub(w)),
2751 None => from,
2752 };
2753 if first_valid > need_from {
2754 return false;
2755 }
2756 rows.push((k, v));
2757 }
2758 rows
2759 }
2760 };
2761 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
2762 let cache = &mut self.kv_cache.layers[li];
2763 let row = cache.num_kv_heads * cache.head_dim;
2764 for p in 0..to - from {
2765 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
2766 }
2767 if cache.pos_len() != to {
2768 return false;
2769 }
2770 }
2771 }
2772 }
2773 for (li, l) in layers.iter().enumerate() {
2777 if l.full
2778 && l.device_rows.is_some_and(|d| d > reuse_from)
2779 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
2780 {
2781 return false;
2782 }
2783 }
2784 true
2785 }
2786
2787 fn finish_generation(
2793 &mut self,
2794 mtp: &mut Option<MtpModule>,
2795 router: &mut Option<crate::swarm::DynRouter>,
2796 clear_sequence: bool,
2797 ) {
2798 if router.is_some() {
2802 let _ = self.set_active_skill(None);
2803 }
2804 #[cfg(target_os = "macos")]
2811 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
2812 if clear_sequence {
2813 self.clear_sequence_state();
2814 if let Some(m) = mtp.as_mut() {
2815 m.kv.clear();
2821 }
2822 if let Some(m) = self.mtp.as_mut() {
2823 m.kv.clear();
2827 }
2828 }
2829 self.graph_want_logits = false;
2830 self.graph_head_required = false;
2831 self.graph_logits = None;
2832 self.graph_failed
2833 .store(false, std::sync::atomic::Ordering::Relaxed);
2834 self.cancel
2835 .store(false, std::sync::atomic::Ordering::Relaxed);
2836 self.dyn_router = router.take().or(self.dyn_router.take());
2837 self.mtp = mtp.take().or(self.mtp.take());
2838 self.mtp_graph_mode = None;
2839 self.spec_forced = None;
2840 }
2841
2842 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2846 if self
2847 .graph_failed
2848 .swap(false, std::sync::atomic::Ordering::Relaxed)
2849 {
2850 self.cancel
2851 .store(false, std::sync::atomic::Ordering::Relaxed);
2852 self.clear_sequence_state();
2853 self.graph_logits = None;
2854 self.graph_want_logits = false;
2855 self.graph_head_required = false;
2856 return Err(format!("GPU graph failed during {phase} at position {pos}"));
2857 }
2858 Ok(())
2859 }
2860
2861 #[cfg(target_os = "macos")]
2862 fn fail_metal_graph(&mut self, reason: &str) {
2863 crate::pipeline::METAL_GRAPH_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2864 self.clear_sequence_state();
2865 self.graph_logits = None;
2866 self.graph_failed
2867 .store(true, std::sync::atomic::Ordering::Relaxed);
2868 self.cancel
2869 .store(true, std::sync::atomic::Ordering::Relaxed);
2870 tracing::error!("native Metal TokenGraph failed closed: {reason}");
2871 }
2872
2873 fn nll_begin(&mut self) -> Result<(), String> {
2878 if self
2879 .graph_failed
2880 .swap(false, std::sync::atomic::Ordering::Relaxed)
2881 {
2882 self.cancel
2883 .store(false, std::sync::atomic::Ordering::Relaxed);
2884 self.clear_sequence_state();
2885 self.graph_logits = None;
2886 self.graph_want_logits = false;
2887 self.graph_head_required = false;
2888 return Err("GPU graph failed before NLL scoring".to_string());
2889 }
2890 self.clear_sequence_state();
2891 self.graph_logits = None;
2892 self.graph_want_logits = false;
2893 self.graph_head_required = false;
2894 Ok(())
2895 }
2896
2897 fn nll_end(&mut self) {
2901 self.clear_sequence_state();
2902 self.graph_logits = None;
2903 self.graph_want_logits = false;
2904 self.graph_head_required = false;
2905 self.graph_failed
2906 .store(false, std::sync::atomic::Ordering::Relaxed);
2907 }
2908
2909 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
2912 #[cfg(test)]
2913 if self.nll_test_fail_at == Some(pos) {
2914 self.nll_test_fail_at = None;
2915 self.graph_failed
2916 .store(true, std::sync::atomic::Ordering::Relaxed);
2917 self.cancel
2918 .store(true, std::sync::atomic::Ordering::Relaxed);
2919 }
2920 if self
2921 .graph_failed
2922 .swap(false, std::sync::atomic::Ordering::Relaxed)
2923 {
2924 self.cancel
2925 .store(false, std::sync::atomic::Ordering::Relaxed);
2926 self.clear_sequence_state();
2927 self.graph_logits = None;
2928 self.graph_want_logits = false;
2929 return Err(format!(
2930 "GPU graph failed during NLL {phase} at position {pos}"
2931 ));
2932 }
2933 Ok(())
2934 }
2935
2936 #[inline]
2940 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
2941 match &self.layer_map {
2942 Some(m) => m[virtual_idx],
2943 None => virtual_idx % self.physical_layers,
2944 }
2945 }
2946
2947 fn onevision2_prefill(&self) -> bool {
2950 self.model.as_ref().is_some_and(|m| matches!(m.header.arch.arch_name.as_str(), "llava_onevision2" | "minicpm_ocr"))
2951 && std::env::var("CMF_GRAPH_PREFILL").as_deref() != Ok("0")
2952 }
2953
2954 #[inline]
2957 pub fn mhc_layer(&self, li: usize) -> bool {
2958 self.mhc.as_ref().is_some_and(|m| m.on(self.phys_layer(li)))
2959 }
2960
2961 fn mhc_pre(&mut self, li: usize, ffn: bool, h: &mut [f32]) -> crate::sensenova::MhcCoef {
2964 let p = self.phys_layer(li);
2965 let m = self.mhc.as_deref().expect("mhc layer");
2966 let n = m.s.streams * m.dim;
2967 if self.mhc_r.len() != n {
2968 self.mhc_r.resize(n, 0.0);
2969 }
2970 if !ffn && m.enters(p) {
2971 m.enter(&mut self.mhc_r, h);
2972 }
2973 m.route(p, ffn, &self.mhc_r, h)
2974 }
2975
2976 fn mhc_post(&mut self, li: usize, ffn: bool, y: &[f32], c: &crate::sensenova::MhcCoef, h: &mut [f32]) {
2979 let p = self.phys_layer(li);
2980 let m = self.mhc.as_deref().expect("mhc layer");
2981 m.merge(&mut self.mhc_r, y, c);
2982 if ffn && m.leaves(p) {
2983 m.leave(&self.mhc_r, h);
2984 }
2985 }
2986
2987 #[inline]
2990 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
2991 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
2992 }
2993
2994 #[allow(clippy::too_many_arguments)]
2996
2997 #[cfg(target_os = "macos")]
3016 fn graph_prefill_preferred(&self) -> bool {
3017 if crate::gpu::enabled_here()
3020 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
3021 && matches!(std::env::var("CMF_EMBRYO_RESIDENT").as_deref(), Ok("1") | Ok("parallel"))
3022 && self.embryo_resident_eligible()
3023 {
3024 return true;
3025 }
3026 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
3027 if !crate::gpu::enabled_here()
3028 || !graph_force
3029 || std::env::var("CMF_GPU_BLOCK")
3030 .map(|v| v == "0")
3031 .unwrap_or(false)
3032 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
3035 {
3036 return false;
3037 }
3038 self.weights
3039 .layers
3040 .iter()
3041 .any(|lw| {
3042 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
3043 })
3044 }
3045
3046 #[cfg(not(target_os = "macos"))]
3053 fn batch_prefix_prefill(&self) -> bool {
3054 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
3055 Ok("0") => return false,
3056 Ok("1") => true,
3057 _ => false,
3058 };
3059 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
3060 && crate::gpu::enabled_here()
3061 && !self.graph_refused()
3062 && (forced || self.graph_attn_decline_reason().is_some())
3063 && self.wgpu_graph_attn_decline().is_none()
3064 && self.attn_softcap == 0.0
3065 && self
3066 .weights
3067 .layers
3068 .iter()
3069 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
3070 && self.automatic_gpu_prefix().is_some()
3071 }
3072
3073 fn g4_batch_prefill(&self) -> bool {
3079 #[cfg(target_os = "macos")]
3080 {
3081 false
3082 }
3083 #[cfg(not(target_os = "macos"))]
3084 {
3085 !self.weights.layers.is_empty()
3089 && !self.per_token_stack()
3090 && (self.global_attn.is_some() || self.attn_v_norm || self.value_blend.is_some())
3093 && crate::gpu::g4_batch_on()
3094 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
3095 && crate::gpu::enabled_here()
3096 && crate::gpu::discrete()
3097 && !self.graph_refused()
3098 && !self.o1_active()
3099 && self.attn_softcap == 0.0
3100 && self.wgpu_graph_attn_decline().is_none()
3101 && self.weights.layers.iter().all(|lw| {
3102 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
3103 })
3104 && self.automatic_gpu_prefix().is_none()
3105 }
3106 }
3107
3108 #[cfg(not(target_os = "macos"))]
3115 fn g4_batch_ingest(
3116 &mut self,
3117 ids: &[u32],
3118 pos0: usize,
3119 keep_all: bool,
3120 ) -> Result<(usize, Vec<f32>), String> {
3121 let hs = self.hidden_size;
3122 let chunk = self.generation_batch_k().max(1);
3123 let end_all = pos0 + ids.len();
3124 let mut pos = pos0;
3125 let mut out = Vec::new();
3126 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
3127 let end = (pos + chunk).min(end_all);
3128 let bk = end - pos;
3129 let mut hiddens = vec![0f32; bk * hs];
3130 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
3131 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
3132 }
3133 let positions: Vec<usize> = (pos..end).collect();
3134 let t_chunk = std::time::Instant::now();
3135 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
3136 if std::env::var("CMF_GRAPH_PROF").is_ok() {
3137 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
3138 eprintln!(
3139 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
3140 bk as f64 / (ms / 1000.0)
3141 );
3142 }
3143 match outcome {
3144 crate::gpu::BatchGraphOutcome::Completed => {
3145 if keep_all {
3146 out.extend_from_slice(&hiddens);
3147 } else {
3148 out = hiddens[(bk - 1) * hs..].to_vec();
3149 }
3150 pos = end;
3151 }
3152 crate::gpu::BatchGraphOutcome::Failed => {
3153 return Err("Gemma-4 batched prefill failed after admission".into());
3154 }
3155 crate::gpu::BatchGraphOutcome::Declined => {
3156 #[cfg(feature = "gpu")]
3157 if pos > pos0 {
3158 self.pull_lagging_host_kv(0, self.num_layers, pos);
3159 }
3160 break;
3161 }
3162 }
3163 }
3164 Ok((pos - pos0, out))
3165 }
3166
3167 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
3172 #[cfg(not(target_os = "macos"))]
3173 {
3174 if ids.is_empty() || !self.g4_batch_prefill() {
3175 return None;
3176 }
3177 self.clear_sequence_state();
3178 match self.g4_batch_ingest(ids, 0, true) {
3179 Ok((n, rows)) if n == ids.len() => Some(rows),
3180 _ => {
3181 self.clear_sequence_state();
3182 None
3183 }
3184 }
3185 }
3186 #[cfg(target_os = "macos")]
3187 {
3188 let _ = ids;
3189 None
3190 }
3191 }
3192
3193 #[cfg(not(target_os = "macos"))]
3194 fn graph_prefill_preferred(&self) -> bool {
3195 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
3203 if !graph_on || !crate::gpu::enabled_here() {
3204 return false;
3205 }
3206 if self.embryo_resident_eligible() {
3210 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
3215 }
3216 if self.o1_active() {
3230 return false;
3231 }
3232 if self.wgpu_graph_attn_decline().is_some() {
3236 return false;
3237 }
3238 if self
3239 .weights
3240 .layers
3241 .iter()
3242 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
3243 || self.onevision2_prefill()
3247 {
3248 return true;
3249 }
3250 (self
3269 .weights
3270 .layers
3271 .iter()
3272 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
3273 || (std::env::var("CMF_DENSE_GRAPH_PREFILL").as_deref() != Ok("0")
3274 && !self.dense_batch_declined.load(std::sync::atomic::Ordering::Relaxed)
3275 && ((self.sliding_layers.is_none() && self.swa.is_none())
3281 || crate::gpu::batched_attend_tiled()
3282 || std::env::var("CMF_DENSE_GRAPH_PREFILL").as_deref() == Ok("1"))))
3283 && self.automatic_gpu_prefix().is_none()
3284 }
3285
3286 #[cfg(target_os = "macos")]
3287 fn q1_graph_gpu(
3288 &mut self,
3289 start: usize,
3290 upto: Option<usize>,
3291 position: usize,
3292 h: &mut [f32],
3293 ) -> usize {
3294 let _mt0 = std::time::Instant::now(); use crate::gpu::{
3296 AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, SconvGpuLayer, TokenGraph,
3297 };
3298 const MOE_WORKING_SET_REASON: &str =
3299 "model weights exceed Metal working set for native MoE graph";
3300 if self
3304 .graph_declines
3305 .borrow()
3306 .iter()
3307 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
3308 {
3309 return start;
3310 }
3311 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
3312 if self.attn_softcap > 0.0 || self.value_blend.is_some()
3315 || !self.metal_rope_scale_ok()
3316 || !crate::gpu::enabled_here()
3317 || !graph_force
3318 || std::env::var("CMF_GPU_BLOCK")
3319 .map(|v| v == "0")
3320 .unwrap_or(false)
3321 {
3322 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3323 eprintln!(
3324 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
3325 self.attn_softcap > 0.0,
3326 crate::gpu::enabled_here(),
3327 graph_force,
3328 );
3329 }
3330 if self.graph_head_required {
3331 self.fail_metal_graph("native graph front gate refused");
3332 }
3333 return start;
3334 }
3335 let gemma4 = self.metal_gemma4();
3345 let swa_graph = self.metal_graph_swa() || gemma4;
3346 let graph_attn_reason = self.graph_attn_decline_reason();
3353 let native_sink_attend = graph_attn_reason == Some("learned attention sinks")
3354 && (swa_graph || (self.swa.is_none() && self.sliding_layers.is_none()));
3355 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
3359 if (self.swa.is_some() && !swa_graph)
3360 || (self.global_attn.is_some() && !gemma4)
3361 || self.attention_heads_per_layer.is_some()
3362 || (self.attn_v_norm && !gemma4)
3363 || (graph_attn_reason.is_some() && !swa_graph && !native_sink_attend)
3366 || self.weights.layers.iter().any(|lw| {
3367 ((lw.layer_scale.is_some()
3368 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())))
3369 && !gemma4)
3370 || matches!(&lw.ffn, FfnKind::Dense(d)
3371 if !(matches!(d.act, Act::Silu | Act::Gelu)
3372 || (gemma4 && d.act == Act::GeluTanh)))
3373 })
3374 {
3375 if let Some(reason) = graph_attn_reason.filter(|_| !swa_graph && !native_sink_attend) {
3378 self.note_graph_decline("metal block graph", reason);
3379 }
3380 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3381 eprintln!(
3382 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
3383 self.swa.is_some(),
3384 self.global_attn.is_some(),
3385 self.attention_heads_per_layer.is_some(),
3386 self.attn_v_norm,
3387 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
3388 );
3389 }
3390 if self.graph_head_required {
3391 self.fail_metal_graph("native graph architecture gate refused");
3392 }
3393 return start;
3394 }
3395 let limit = upto
3398 .map(|u| u + 1)
3399 .unwrap_or(self.num_layers)
3400 .min(self.num_layers);
3401
3402 enum Item<'a> {
3403 Gdn {
3404 run: Vec<GdnGpuLayer<'a>>,
3405 first: usize,
3406 },
3407 Sconv {
3409 run: Vec<SconvGpuLayer<'a>>,
3410 first: usize,
3411 },
3412 Attn {
3413 l: AttnGpuLayer<'a>,
3414 li: usize,
3415 q_norm: Option<&'a [f32]>,
3416 k_norm: Option<&'a [f32]>,
3417 output_gate: bool,
3418 proj_gate: Option<(&'a QTensor, bool)>,
3422 head_gate_w: Option<&'a [f32]>,
3425 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
3426 full_gpu: bool,
3429 },
3430 }
3431
3432 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
3439 let attend_contract = attend_mode != "0"
3440 && attend_mode != "off"
3441 && self.head_dim % 4 == 0
3442 && self.head_dim <= 256
3443 && (self.rotary_dim == 0
3447 || (self.rotary_dim >= 2
3448 && self.rotary_dim <= self.head_dim
3449 && (self.rotary_dim / 2) % 32 == 0))
3450 && self.num_kv_heads > 0
3451 && self.num_heads % self.num_kv_heads == 0;
3452
3453 let mut plan: Vec<Item> = Vec::new();
3454 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
3455 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
3457 let mut scan = start;
3458 while scan < limit {
3459 if self.prompt_mrope.is_some()
3463 || (self.mhc_layer(scan)
3464 && (!crate::gpu_metal::mhc::available()
3465 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
3466 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
3467 || !matches!(self.weights.layers[self.phys_layer(scan)].ffn, FfnKind::Dense(_))))
3468 {
3469 break;
3470 }
3471 let lw = &self.weights.layers[self.phys_layer(scan)];
3472 let ffn = match &lw.ffn {
3473 FfnKind::Dense(d) if d.segs.is_empty() => {
3474 let (Some(g), Some(u), Some(dn)) = (
3475 d.gate_proj.metal_graph_parts(),
3476 d.up_proj.metal_graph_parts(),
3477 d.down_proj.metal_graph_parts(),
3478 ) else {
3479 if block_diag {
3480 eprintln!(
3481 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
3482 );
3483 }
3484 break;
3485 };
3486 MetalFfn::Dense {
3487 gate: g,
3488 up: u,
3489 down: dn,
3490 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
3493 }
3494 }
3495 FfnKind::Moe(m) => {
3496 let ffn = if let Some(flint) = metal_flint_moe_graph_parts(m, self.hidden_size, scan)
3502 {
3503 MetalFfn::FlintMoe(flint)
3504 } else if let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) {
3505 MetalFfn::Moe(moe)
3506 } else {
3507 if block_diag {
3508 eprintln!(
3509 "block-graph: L{scan} MoE outside the graph contract — run ends"
3510 );
3511 }
3512 break;
3513 };
3514 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
3515 model_ref.get_or_insert_with(|| model.clone());
3516 }
3517 ffn
3518 }
3519 _ => {
3520 if block_diag {
3521 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
3522 }
3523 break;
3524 }
3525 };
3526 match &lw.attn {
3527 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
3528 let parts = (
3529 w.in_proj_qkv.metal_graph_parts(),
3530 w.in_proj_z.metal_graph_parts(),
3531 w.in_proj_a.f32_parts(),
3532 w.in_proj_b.f32_parts(),
3533 w.out_proj.metal_graph_parts(),
3534 );
3535 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
3536 if block_diag {
3537 eprintln!(
3538 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
3539 w.in_proj_qkv.metal_graph_parts().is_some(),
3540 w.in_proj_z.metal_graph_parts().is_some(),
3541 w.in_proj_a.f32_parts().is_some(),
3542 w.in_proj_b.f32_parts().is_some(),
3543 w.out_proj.metal_graph_parts().is_some(),
3544 );
3545 }
3546 break;
3547 };
3548 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
3549 model_ref.get_or_insert_with(|| model.clone());
3550 }
3551 let gl = GdnGpuLayer {
3552 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
3553 attn_norm: &lw.input_norm,
3554 post_norm: &lw.post_norm,
3555 qkv,
3556 z,
3557 a,
3558 b,
3559 out,
3560 ffn,
3561 conv1d: &w.conv1d,
3562 a_log: &w.a_log,
3563 dt_bias: &w.dt_bias,
3564 gnorm: &w.norm,
3565 };
3566 let cut = scan > 0 && self.dflash.as_ref().is_some_and(|d| d.g.taps.contains(&(scan - 1)));
3568 match plan.last_mut() {
3569 Some(Item::Gdn { run, .. }) if !cut => run.push(gl),
3570 _ => plan.push(Item::Gdn {
3571 run: vec![gl],
3572 first: scan,
3573 }),
3574 }
3575 }
3576 AttnKind::ShortConv(w) if self.short_conv_cfg.is_some() => {
3577 let (Some(inp), Some(out)) =
3578 (w.in_proj.metal_graph_parts(), w.out_proj.metal_graph_parts())
3579 else {
3580 if block_diag {
3581 eprintln!("block-graph: L{scan} short-conv parts refused");
3582 }
3583 break;
3584 };
3585 if let QTensor::Mapped { model, .. } = &w.in_proj {
3586 model_ref.get_or_insert_with(|| model.clone());
3587 }
3588 let sl = SconvGpuLayer {
3589 attn_norm: &lw.input_norm,
3590 post_norm: &lw.post_norm,
3591 in_proj: inp,
3592 taps: &w.conv,
3593 out,
3594 attn_out_norm: lw.attn_out_norm.as_deref(),
3595 ffn_out_norm: lw.ffn_out_norm.as_deref(),
3596 ffn,
3597 };
3598 match plan.last_mut() {
3599 Some(Item::Sconv { run, .. }) => run.push(sl),
3600 _ => plan.push(Item::Sconv {
3601 run: vec![sl],
3602 first: scan,
3603 }),
3604 }
3605 }
3606 AttnKind::Full {
3607 wq,
3608 wk,
3609 wv,
3610 wo,
3611 q_norm,
3612 k_norm,
3613 output_gate,
3614 softplus_gate,
3615 bias,
3616 } if (!self.kv_cache.layers[scan].o1_sealed()
3617 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
3622 || self.o1_on_graph())
3623 && softplus_gate
3627 .as_ref()
3628 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
3629 {
3630 let parts = (
3631 wq.metal_graph_parts(),
3632 wk.metal_graph_parts(),
3633 wv.metal_graph_parts(),
3634 wo.metal_graph_parts(),
3635 );
3636 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
3637 break;
3638 };
3639 if let QTensor::Mapped { model, .. } = wq {
3640 model_ref.get_or_insert_with(|| model.clone());
3641 }
3642 let cache = &self.kv_cache.layers[scan];
3643 let o1_metal = cache.o1.is_some()
3647 && (std::env::var("CMF_O1_METAL").as_deref() == Ok("1") || self.o1_on_graph())
3648 && cache.o1_views().is_some();
3649 let head_gate_w = softplus_gate
3653 .as_ref()
3654 .and_then(|(g, _)| g.f32_parts())
3655 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
3656 .map(|(d, _, _)| d);
3657 let (nkv_g, hd_g) = if gemma4 {
3660 let (a, b, _) = self.layer_geom(scan);
3661 (a, b)
3662 } else {
3663 (self.num_kv_heads, self.head_dim)
3664 };
3665 let full_gpu = attend_contract
3666 && softplus_gate.is_none() == head_gate_w.is_none()
3667 && cache.mode == crate::kv_cache::KvMode::F32
3668 && (cache.o1.is_none() || o1_metal)
3669 && bias.is_none()
3670 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
3671 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
3672 && pk.1 == nkv_g * hd_g
3673 && pv.1 == nkv_g * hd_g
3674 && po.2 == self.num_heads * hd_g;
3675 if !full_gpu && (self.mhc_layer(scan) || self.dash.is_some()) {
3679 break;
3680 }
3681 plan.push(Item::Attn {
3682 l: AttnGpuLayer {
3683 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(scan)),
3684 attn_norm: &lw.input_norm,
3685 post_norm: &lw.post_norm,
3686 wq: pq,
3687 wk: pk,
3688 wv: pv,
3689 wo: po,
3690 ffn,
3691 attn_out_norm: lw.attn_out_norm.as_deref(),
3692 ffn_out_norm: lw.ffn_out_norm.as_deref(),
3693 layer_scale: lw.layer_scale,
3694 },
3695 li: scan,
3696 q_norm: q_norm.as_deref(),
3697 k_norm: k_norm.as_deref(),
3698 output_gate: *output_gate,
3699 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
3700 head_gate_w,
3701 bias: bias
3702 .as_ref()
3703 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
3704 full_gpu,
3705 });
3706 }
3707 _ => break,
3708 }
3709 scan += 1;
3710 }
3711 if let Some(m) = self.mhc.as_deref() {
3715 let inside = |li: usize| m.on(self.phys_layer(li));
3716 let starts_mid = inside(start) && !m.enters(self.phys_layer(start));
3717 let stops_mid = scan > start && inside(scan - 1) && !m.leaves(self.phys_layer(scan - 1));
3718 if starts_mid || stops_mid {
3719 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3720 eprintln!("q1-graph: [{start}, {scan}) would split an mHC range — host path");
3721 }
3722 return start;
3723 }
3724 }
3725 if self.dash.is_some()
3728 && (start != 0 || scan != self.num_layers || crate::gpu::backend_name() != "metal")
3729 {
3730 return start;
3731 }
3732 let Some(model) = model_ref else {
3733 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3734 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
3735 }
3736 if self.graph_head_required {
3737 self.fail_metal_graph("native graph has no mapped model reference");
3738 }
3739 return start;
3740 };
3741 if plan.is_empty() {
3742 if std::env::var("CMF_GRAPH_DBG").is_ok() {
3743 eprintln!("q1-graph: empty plan at layer {start}");
3744 }
3745 if self.graph_head_required {
3746 self.fail_metal_graph("native graph plan is empty");
3747 }
3748 return start;
3749 }
3750 let has_moe = plan.iter().any(|it| match it {
3751 Item::Gdn { run, .. } => run
3752 .iter()
3753 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
3754 Item::Sconv { run, .. } => run
3755 .iter()
3756 .any(|l| matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))),
3757 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_)),
3758 });
3759 let has_flint_moe = plan.iter().any(|it| match it {
3760 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
3761 Item::Sconv { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::FlintMoe(_))),
3762 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::FlintMoe(_)),
3763 });
3764 let metal_working_set = crate::gpu_metal::working_set_bytes();
3765 let metal_max_buffer = crate::gpu_metal::max_buffer_bytes();
3766 let model_bytes = model.primary_bytes().len() as u64;
3767 let needs_bounded_pager = (metal_working_set != 0 && model_bytes > metal_working_set)
3773 || (metal_max_buffer != 0 && model_bytes > metal_max_buffer);
3774 let pager_cap = if !has_moe {
3783 None
3784 } else if has_flint_moe {
3785 crate::gpu_metal::flint_moe_pager_cap_bytes(&model)
3789 } else {
3790 crate::gpu_metal::moe_pager_cap_bytes()
3791 };
3792 let pager_eligible =
3793 pager_cap.is_some() && !self.graph_head_required && needs_bounded_pager;
3794 let mut pager_span: Option<(usize, usize)> = None;
3795 if pager_eligible {
3796 let cap = pager_cap.expect("checked above");
3797 let tail_span = (self.graph_want_logits
3801 && upto.is_none()
3802 && scan == self.num_layers
3803 && std::env::var("CMF_GPU_LMHEAD")
3804 .map(|v| v != "0")
3805 .unwrap_or(true))
3806 .then(|| self.weights.lm_head.metal_graph_parts())
3807 .flatten()
3808 .and_then(|lm| crate::gpu_metal::model_tensor_span(&model, lm.0));
3809 let add_layer_span = |span: Option<(usize, usize)>, li: usize| {
3810 let (lo, hi) = crate::gpu_metal::model_layer_span(&model, self.phys_layer(li))?;
3811 Some(match span {
3812 Some((cur_lo, cur_hi)) => (cur_lo.min(lo), cur_hi.max(hi)),
3813 None => (lo, hi),
3814 })
3815 };
3816 let page_span = |span: (usize, usize), include_tail: bool| {
3817 let (mut lo, mut hi) = span;
3818 if include_tail {
3819 if let Some((tail_lo, tail_hi)) = tail_span {
3820 lo = lo.min(tail_lo);
3821 hi = hi.max(tail_hi);
3822 }
3823 }
3824 crate::gpu_metal::page_round_span(lo, hi)
3825 };
3826 let mut selected = 0usize;
3827 let mut selected_span: Option<(usize, usize)> = None;
3828 let mut gdn_prefix: Option<(usize, usize)> = None;
3836 'items: for (i, item) in plan.iter().enumerate() {
3837 match item {
3838 Item::Sconv { .. } => break,
3841 Item::Attn { li, .. } => {
3842 let Some(span) = add_layer_span(selected_span, *li) else {
3843 break;
3844 };
3845 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len()) else {
3846 break;
3847 };
3848 if page_hi - page_lo > cap {
3849 break;
3850 }
3851 selected = i + 1;
3852 selected_span = Some((page_lo, page_hi));
3853 }
3854 Item::Gdn { first, run } => {
3855 let mut whole = None;
3862 for take in 1..=run.len() {
3863 whole = add_layer_span(whole, *first + take - 1);
3864 if whole.is_none() {
3865 break;
3866 }
3867 }
3868 let whole_fits_empty_page = whole
3869 .and_then(|span| page_span(span, false))
3870 .is_some_and(|(lo, hi)| hi - lo <= cap);
3871 if whole_fits_empty_page {
3872 let mut span = selected_span;
3873 for take in 1..=run.len() {
3874 let Some(next) = add_layer_span(span, *first + take - 1) else {
3875 break 'items;
3876 };
3877 span = Some(next);
3878 }
3879 let Some(span) = span else {
3880 break;
3881 };
3882 let Some((page_lo, page_hi)) = page_span(span, i + 1 == plan.len())
3883 else {
3884 break;
3885 };
3886 if page_hi - page_lo > cap {
3887 break;
3888 }
3889 selected = i + 1;
3890 selected_span = Some((page_lo, page_hi));
3891 continue;
3892 }
3893 if selected != 0 {
3894 break;
3895 }
3896 let mut span = None;
3897 let mut best: Option<(usize, (usize, usize))> = None;
3898 for take in 1..=run.len() {
3899 let Some(next) = add_layer_span(span, *first + take - 1) else {
3900 break;
3901 };
3902 let Some((page_lo, page_hi)) = page_span(next, false) else {
3905 break;
3906 };
3907 if page_hi - page_lo > cap {
3908 break;
3909 }
3910 span = Some((page_lo, page_hi));
3911 best = Some((take, (page_lo, page_hi)));
3912 }
3913 let Some((take, page)) = best else {
3914 break;
3915 };
3916 selected = i + 1;
3917 selected_span = Some(page);
3918 if take != run.len() {
3919 gdn_prefix = Some((i, take));
3920 break 'items;
3921 }
3922 }
3923 }
3924 }
3925 if selected != 0 {
3926 if let Some((item, take)) = gdn_prefix {
3927 match &mut plan[item] {
3928 Item::Gdn { run, .. } => run.truncate(take),
3929 Item::Sconv { .. } | Item::Attn { .. } => {
3930 unreachable!("only a GDN run is splittable")
3931 }
3932 }
3933 }
3934 let (page_lo, page_hi) = selected_span.expect("selected page span");
3935 tracing::info!(
3936 page_start = page_lo,
3937 page_end = page_hi,
3938 page_mb = (page_hi - page_lo) / (1024 * 1024),
3939 cap_mb = cap / (1024 * 1024),
3940 graph_items = selected,
3941 "native Metal MoE graph uses transient bounded weight page"
3942 );
3943 plan.truncate(selected);
3944 pager_span = Some((page_lo, page_hi));
3945 } else {
3946 tracing::warn!(
3947 cap_mb = cap / (1024 * 1024),
3948 "native Metal MoE pager declined: one graph item does not fit its safe page cap"
3949 );
3950 }
3951 }
3952 if has_moe && needs_bounded_pager && pager_span.is_none() {
3961 tracing::info!(
3962 model_bytes,
3963 metal_working_set,
3964 "native Metal MoE graph declined before allocation"
3965 );
3966 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
3967 if self.graph_head_required {
3968 self.fail_metal_graph(MOE_WORKING_SET_REASON);
3969 }
3970 return start;
3971 }
3972 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
3973 let dev_attend = attend_contract
3974 && (self.head_dim <= 128
3975 || has_moe
3976 || (self.head_dim <= 256 && has_gdn)
3982 || (self.head_dim <= 256 && swa_graph)
3986 || attend_mode == "force"
3987 || attend_mode == "256");
3988 if !dev_attend {
3989 for it in &mut plan {
3990 if let Item::Attn { li, full_gpu, .. } = it {
3991 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
3994 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
3995 if !keep_o1 {
3996 *full_gpu = false;
3997 }
3998 }
3999 }
4000 }
4001 if std::env::var("CMF_GRAPH_DBG").is_ok() {
4002 use std::sync::atomic::{AtomicBool, Ordering};
4003 static SAID: AtomicBool = AtomicBool::new(false);
4004 if !SAID.swap(true, Ordering::Relaxed) {
4005 let fg = plan
4006 .iter()
4007 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
4008 .count();
4009 let att = plan
4010 .iter()
4011 .filter(|it| matches!(it, Item::Attn { .. }))
4012 .count();
4013 eprintln!(
4014 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
4015 plan.len(),
4016 self.head_dim,
4017 self.rotary_dim,
4018 self.num_kv_heads,
4019 self.num_heads,
4020 );
4021 }
4022 }
4023 let dims = GraphDims {
4024 hidden: self.hidden_size,
4025 eps: self.rms_eps as f32,
4026 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
4027 };
4028 let mut pager_profile_page = pager_span.map(|(page_lo, page_hi)| {
4032 crate::gpu_metal::MoePagerPageScope::new(
4033 _mt0,
4034 position,
4035 start,
4036 page_lo,
4037 page_hi,
4038 pager_cap.unwrap_or(0),
4042 )
4043 });
4044 let graph_new = match pager_span {
4045 Some((page_lo, page_hi)) => TokenGraph::new_paged(&model, dims, h, page_lo, page_hi),
4046 None => TokenGraph::new(&model, dims, h),
4047 };
4048 let Some(mut graph) = graph_new else {
4049 if self.graph_head_required {
4050 self.fail_metal_graph("native TokenGraph allocation refused");
4051 }
4052 return start;
4053 };
4054 if swa_graph && self.head_dim > 128 {
4055 graph.set_attend_blk(1, 32);
4065 }
4066 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
4067 nv: cfg.num_v_heads,
4068 nk: cfg.num_k_heads,
4069 dk: cfg.key_head_dim,
4070 dv: cfg.value_head_dim,
4071 kk: cfg.conv_kernel,
4072 hidden: self.hidden_size,
4073 inter: self.intermediate_size,
4074 c_dim: cfg.conv_dim(),
4075 eps: cfg.rms_eps as f32,
4076 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
4077 });
4078 let mut valid = 0usize;
4082 let mut end = start;
4083 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
4085 static ONCE: std::sync::Once = std::sync::Once::new();
4086 ONCE.call_once(|| {
4087 for it in &plan {
4088 match it {
4089 Item::Gdn { first, run } => {
4090 eprintln!("plan: Gdn first={first} len={}", run.len())
4091 }
4092 Item::Sconv { first, run } => {
4093 eprintln!("plan: Sconv first={first} len={}", run.len())
4094 }
4095 Item::Attn { li, full_gpu, .. } => {
4096 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
4097 }
4098 }
4099 }
4100 });
4101 }
4102 for item in &plan {
4103 let ok = match item {
4104 Item::Gdn { run, .. } => gcfg
4105 .as_ref()
4106 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
4107 .unwrap_or(false),
4108 Item::Sconv { run, .. } => self
4109 .short_conv_cfg
4110 .is_some_and(|sc| run.iter().all(|l| graph.sconv_ok(l, sc.kernel))),
4111 Item::Attn { l, .. } => graph.attn_ok(l),
4112 };
4113 if !ok {
4114 if block_diag {
4115 eprintln!(
4116 "block-graph: plan item {} ({}) failed graph preflight",
4117 valid,
4118 match item {
4119 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
4120 Item::Sconv { run, first } => {
4121 format!("short-conv run L{first}+{}", run.len())
4122 }
4123 Item::Attn { li, .. } => format!("Attn L{li}"),
4124 }
4125 );
4126 }
4127 break;
4128 }
4129 valid += 1;
4130 end += match item {
4131 Item::Gdn { run, .. } => run.len(),
4132 Item::Sconv { run, .. } => run.len(),
4133 Item::Attn { .. } => 1,
4134 };
4135 }
4136 plan.truncate(valid);
4137 if plan.is_empty() {
4138 if self.graph_head_required {
4139 self.fail_metal_graph("native graph preflight produced no valid items");
4140 }
4141 return start;
4142 }
4143
4144 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
4145 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
4146 return start;
4147 }
4148
4149 let one_pass = |t: (usize, usize, usize)| {
4158 use cortiq_core::TensorDtype as D;
4159 matches!(
4160 model.tensors[t.0].dtype,
4161 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1 | D::Q1G
4162 )
4163 };
4164 let dense_fast = plan.iter().all(|it| match it {
4165 Item::Attn {
4166 l, li, full_gpu, ..
4167 } => {
4168 *full_gpu
4169 && self.kv_cache.layers[*li].o1.is_none()
4170 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
4171 && match l.ffn {
4172 MetalFfn::Dense { gate, up, down, .. } => {
4173 one_pass(gate) && one_pass(up) && one_pass(down)
4174 }
4175 _ => false,
4176 }
4177 }
4178 Item::Sconv { run, .. } => run.iter().all(|l| {
4181 one_pass(l.in_proj)
4182 && one_pass(l.out)
4183 && match l.ffn {
4184 MetalFfn::Dense { gate, up, down, .. } => {
4185 one_pass(gate) && one_pass(up) && one_pass(down)
4186 }
4187 _ => false,
4188 }
4189 }),
4190 Item::Gdn { .. } => false,
4191 });
4192 let moe_plan = !dense_fast
4196 && plan.iter().all(|it| match it {
4197 Item::Attn {
4198 l, li, full_gpu, ..
4199 } => {
4200 *full_gpu
4201 && self.kv_cache.layers[*li].o1.is_none()
4202 && (matches!(l.ffn, MetalFfn::Moe(_) | MetalFfn::FlintMoe(_))
4203 || self.dash.is_some())
4204 }
4205 Item::Gdn { .. } | Item::Sconv { .. } => false,
4206 });
4207 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
4208 if block_diag {
4209 use std::sync::atomic::{AtomicBool, Ordering};
4210 static SAID_AB: AtomicBool = AtomicBool::new(false);
4211 if !SAID_AB.swap(true, Ordering::Relaxed) {
4212 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
4213 }
4214 }
4215 let _mv_fast = match ab {
4216 Some((bits, _)) => {
4217 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
4218 crate::gpu_metal::MvFastGuard::set_raw(bits)
4219 }
4220 None if moe_plan => {
4221 graph.set_dense_concurrent(
4222 crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0,
4223 );
4224 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
4225 }
4226 None => {
4227 graph.set_dense_concurrent(dense_fast);
4228 let _ = swa_graph;
4233 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
4234 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | crate::gpu_metal::DENSE_Q8R4
4235 } else {
4236 0
4237 })
4238 }
4239 };
4240
4241 let pool = self.pool.clone();
4244 let (nh, nkv, hd, hs, eps) = (
4245 self.num_heads,
4246 self.num_kv_heads,
4247 self.head_dim,
4248 self.hidden_size,
4249 self.rms_eps,
4250 );
4251 let norm_style = self.norm_style;
4252 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
4253 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
4254 let kv_id = self.graph_kv_id;
4255 let mut pending: Vec<(usize, usize)> = Vec::new();
4258 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
4261 let taps: Vec<usize> = match &self.dflash {
4264 Some(d) if start == 0 && end == self.num_layers => d.g.taps.clone(),
4265 _ => Vec::new(),
4266 };
4267 let mut tapped = 0usize;
4268 for item in &plan {
4269 let _xt0 = std::time::Instant::now();
4270 let _xkind: u32 = match item {
4271 Item::Gdn { .. } | Item::Sconv { .. } => 2,
4272 Item::Attn { .. } => 3,
4273 };
4274 if self.loop_final_norm {
4276 let item_start = match item {
4277 Item::Gdn { first, .. } | Item::Sconv { first, .. } => *first,
4278 Item::Attn { li, .. } => *li,
4279 };
4280 if item_start > start && self.is_loop_end(item_start - 1) {
4281 graph.encode_loop_norm(&self.weights.final_norm);
4282 }
4283 }
4284 match item {
4285 Item::Gdn { run, first } => {
4286 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
4287 if l.linear_state.len() != want {
4288 l.linear_state = vec![0f32; want];
4289 }
4290 }
4291 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
4292 .iter()
4293 .map(|l| l.linear_state.as_slice())
4294 .collect();
4295 let _ig = std::time::Instant::now();
4296 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
4297 tracing::error!("q1 graph: GDN run refused after validation");
4299 return start;
4300 }
4301 if let Some(slot) = taps.iter().position(|t| *t + 1 == *first + run.len()) {
4302 tapped += graph.tap(slot, taps.len()) as usize;
4303 }
4304 graph.commit_kind = 2;
4307 graph.commit();
4308 crate::gpu::stageprof(0, _ig.elapsed());
4309 pending.push((*first, run.len()));
4310 }
4311 Item::Sconv { run, first } => {
4312 let sc = self.short_conv_cfg.expect("validated short-conv run");
4313 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
4314 if l.linear_state.len() != sc.state_len() {
4315 l.linear_state = vec![0f32; sc.state_len()];
4316 }
4317 }
4318 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
4319 .iter()
4320 .map(|l| l.linear_state.as_slice())
4321 .collect();
4322 if !graph.encode_sconv_run(run, &ro, sc.kernel) {
4323 tracing::error!("q1 graph: short-conv run refused after validation");
4324 return start;
4325 }
4326 graph.commit_kind = 2;
4327 graph.commit();
4328 pending.push((*first, run.len()));
4329 }
4330 Item::Attn {
4331 l,
4332 li,
4333 q_norm,
4334 k_norm,
4335 output_gate,
4336 proj_gate,
4337 head_gate_w,
4338 bias,
4339 full_gpu,
4340 } => {
4341 let _ia = std::time::Instant::now();
4342 let inv_freq_l = self.layer_inv_freq(*li);
4348 let rd_l = self.layer_geom(*li).2;
4349 let window_l = self.layer_window(*li);
4350 let head_gate_w = *head_gate_w;
4351 let (nkv, hd) = if gemma4 {
4354 let (a, b, _) = self.layer_geom(*li);
4355 (a, b)
4356 } else {
4357 (nkv, hd)
4358 };
4359 if *full_gpu {
4361 let cache = &self.kv_cache.layers[*li];
4362 let o1p = if cache.o1.is_some() {
4363 match cache.o1_views() {
4364 Some(views) => Some(crate::gpu::O1AttnParams {
4365 views,
4366 epoch: self.o1_epoch,
4367 }),
4368 None => None,
4370 }
4371 } else {
4372 None
4373 };
4374 let o1_layer = cache.o1.is_some();
4375 if o1_layer && o1p.is_none() {
4376 }
4378 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
4379 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
4380 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
4381 debug_assert!(
4384 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
4385 );
4386 let dash_l = self.graph_dash(*li);
4387 let (rope_at, rope_invf, rope_sc): (usize, &[f32], f32) = match self.dash.as_deref() {
4390 Some(d) => (1, &d.angles, 1.0),
4391 None => (position, &inv_freq_l, self.layer_rope_scale(*li)),
4392 };
4393 let p = crate::gpu::AttnDeviceParams {
4394 kv_id,
4395 layer: *li,
4396 nh,
4397 nkv,
4398 hd,
4399 rd: rd_l,
4400 position: rope_at,
4401 scale: self.attn_scale,
4402 eps: eps as f32,
4403 gemma,
4404 late_qk_norm: self.qk_norm_after_rope,
4405 output_gate: *output_gate,
4406 q_norm: *q_norm,
4407 k_norm: *k_norm,
4408 inv_freq: rope_invf,
4409 rope_scale: rope_sc,
4410 cpu_k,
4411 cpu_v,
4412 cpu_stored,
4413 cpu_gen: cache.generation(),
4414 o1: o1p,
4415 window: window_l,
4416 softmax_sink: cache.sinks.as_deref(),
4417 head_gate: head_gate_w,
4418 v_norm: self.attn_v_norm,
4419 dash: dash_l.as_ref(),
4420 };
4421 let o1_bad = o1_layer && p.o1.is_none();
4422 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
4423 {
4424 if p.o1.is_none() {
4426 dev_attn.push((*li, nkv, hd));
4427 }
4428 if let Some(slot) = taps.iter().position(|t| t == li) {
4429 tapped += graph.tap(slot, taps.len()) as usize;
4430 }
4431 graph.commit_kind = 3;
4432 graph.commit();
4433 crate::gpu::stageprof(_xkind, _xt0.elapsed());
4437 continue;
4438 }
4439 }
4441 graph.encode_attn_prefix(l);
4442 if let Err(err) = graph.sync_checked() {
4443 self.fail_metal_graph(&err);
4444 return start;
4445 }
4446 if !pending.is_empty() {
4447 let idxs: Vec<usize> =
4448 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
4449 let mut outs: Vec<&mut [f32]> = self
4450 .kv_cache
4451 .layers
4452 .iter_mut()
4453 .enumerate()
4454 .filter(|(i, _)| idxs.binary_search(i).is_ok())
4455 .map(|(_, s)| s.linear_state.as_mut_slice())
4456 .collect();
4457 graph.read_states(&mut outs);
4458 }
4459 let mut q_raw = attention::take_buf(l.wq.1);
4460 let mut k = attention::take_buf(l.wk.1);
4461 let mut v = attention::take_buf(l.wv.1);
4462 graph.read_qkv(&mut q_raw, &mut k, &mut v);
4463 let mut gate_raw = proj_gate.map(|(gp, _)| {
4466 let mut normed = attention::take_buf(hs);
4467 graph.read_normed(&mut normed);
4468 let mut raw = attention::take_buf(gp.rows());
4469 gp.matvec(&normed, &mut raw, pool.as_deref());
4470 attention::recycle_buf(&mut normed);
4471 raw
4472 });
4473 let cfg = QwenAttnCfg {
4474 mrope: None,
4475 num_heads: nh,
4476 num_kv_heads: nkv,
4477 head_dim: hd,
4478 hidden_size: hs,
4479 position,
4480 inv_freq: &inv_freq_l,
4481 rotary_dim: rd_l,
4482 scale: self.attn_scale,
4483 softcap: self.attn_softcap,
4484 window: window_l,
4485 v_norm: self.attn_v_norm,
4488 qk_norm_after_rope: self.qk_norm_after_rope,
4489 gate_sigmoid: self.proj_gate_sigmoid,
4490 q_norm: *q_norm,
4491 k_norm: *k_norm,
4492 output_gate: *output_gate,
4493 softplus_gate: None,
4494 rope_scale: self.layer_rope_scale(*li),
4495 bias: *bias,
4496 rms_eps: eps,
4497 norm_style,
4498 pool: pool.as_deref(),
4499 v_head_dim: hd,
4500 value_in: None,
4501 };
4502 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
4505 || std::env::var("CMF_ATTN_DUMP").is_ok();
4506 let _ = full_gpu;
4507 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
4508 let mut ao = attention::qwen_attention_core(
4509 q_raw,
4510 k,
4511 v,
4512 &mut self.kv_cache.layers[*li],
4513 &cfg,
4514 );
4515 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
4519 if let Some((qr0, k0, v0)) = oracle_in.clone() {
4520 let (cq, _cg, _ck, _cv) =
4521 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
4522 let cache = &self.kv_cache.layers[*li];
4523 let n = cache.head_keys(0).len() / hd;
4524 let mut bytes: Vec<u8> = Vec::new();
4525 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
4526 bytes.extend_from_slice(&v.to_le_bytes());
4527 }
4528 for v in &cq {
4529 bytes.extend_from_slice(&v.to_le_bytes());
4530 }
4531 for g in 0..nkv {
4532 for v in cache.head_keys(g) {
4533 bytes.extend_from_slice(&v.to_le_bytes());
4534 }
4535 }
4536 for g in 0..nkv {
4537 for v in cache.head_values(g) {
4538 bytes.extend_from_slice(&v.to_le_bytes());
4539 }
4540 }
4541 let _ =
4542 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
4543 }
4544 }
4545 if let Some((qr0, k0, v0)) =
4546 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
4547 {
4548 let (cq, _cg, ck, cv) =
4549 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
4550 let mut h_now = vec![0f32; hs];
4551 graph.read_h(&mut h_now);
4552 let cache = &self.kv_cache.layers[*li];
4553 let n_after = cache.head_keys(0).len() / hd;
4554 let stored = n_after.saturating_sub(1);
4558 let cpu_k: Vec<&[f32]> = (0..nkv)
4559 .map(|g| &cache.head_keys(g)[..stored * hd])
4560 .collect();
4561 let cpu_v: Vec<&[f32]> = (0..nkv)
4562 .map(|g| &cache.head_values(g)[..stored * hd])
4563 .collect();
4564 let p = crate::gpu::AttnDeviceParams {
4565 kv_id,
4566 layer: *li,
4567 nh,
4568 nkv,
4569 hd,
4570 rd: rd_l,
4574 position,
4575 scale: self.attn_scale,
4576 eps: eps as f32,
4577 gemma,
4578 late_qk_norm: self.qk_norm_after_rope,
4579 output_gate: *output_gate,
4580 q_norm: *q_norm,
4581 k_norm: *k_norm,
4582 inv_freq: &inv_freq_l,
4583 rope_scale: self.layer_rope_scale(*li),
4584 cpu_k,
4585 cpu_v,
4586 cpu_stored: stored,
4587 cpu_gen: cache.generation(),
4588 o1: None,
4589 window: window_l,
4590 softmax_sink: cache.sinks.as_deref(),
4591 head_gate: None,
4592 v_norm: self.attn_v_norm,
4593 dash: None,
4594 };
4595 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
4596 let md = |a: &[f32], b: &[f32]| {
4597 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
4598 };
4599 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
4600 eprintln!(
4601 "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}",
4602 nn(&cq),
4603 md(&cq, &dq),
4604 nn(&ck),
4605 md(&ck, &dk),
4606 nn(&cv),
4607 md(&cv, &dv),
4608 nn(&ao),
4609 md(&ao, &dao)
4610 );
4611 } else {
4612 eprintln!("attn-oracle L{li}: device probe declined");
4613 }
4614 }
4615 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
4616 attention::apply_projected_gate(
4619 &mut ao,
4620 raw,
4621 per_head,
4622 hd,
4623 self.proj_gate_sigmoid,
4624 );
4625 }
4626 if let Some(mut raw) = gate_raw.take() {
4627 attention::recycle_buf(&mut raw);
4628 }
4629 graph.encode_attn_suffix(l, &ao);
4630 if let Some(slot) = taps.iter().position(|t| t == li) {
4631 tapped += graph.tap(slot, taps.len()) as usize;
4632 }
4633 graph.commit();
4636 attention::recycle_buf(&mut ao);
4637 }
4638 }
4639
4640 crate::gpu::stageprof(_xkind, _xt0.elapsed());
4641 }
4642 let mut lm_rows = None;
4647 if self.graph_want_logits
4648 && upto.is_none()
4649 && end == self.num_layers
4650 && std::env::var("CMF_GPU_LMHEAD")
4651 .map(|v| v != "0")
4652 .unwrap_or(true)
4653 {
4654 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
4655 if graph.lm_head_ok(lm) {
4656 graph.encode_lm_head(&self.weights.final_norm, lm);
4657 lm_rows = Some(lm.1);
4658 }
4659 }
4660 }
4661 if self.graph_head_required && lm_rows.is_none() {
4662 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4663 self.fail_metal_graph("fused graph head was requested but not encodable");
4664 return start;
4665 }
4666 let _sy0 = std::time::Instant::now();
4667 if let Err(err) = graph.sync_checked() {
4668 if let Some(profile) = pager_profile_page.as_mut() {
4669 profile.finish(start, graph.pager_page_stats(), "sync-error");
4670 }
4671 self.fail_metal_graph(&err);
4672 return start;
4673 }
4674 let _rs0 = std::time::Instant::now();
4675 if !pending.is_empty() {
4676 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
4677 let mut outs: Vec<&mut [f32]> = self
4678 .kv_cache
4679 .layers
4680 .iter_mut()
4681 .enumerate()
4682 .filter(|(i, _)| idxs.binary_search(i).is_ok())
4683 .map(|(_, s)| s.linear_state.as_mut_slice())
4684 .collect();
4685 graph.read_states(&mut outs);
4686 }
4687 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
4688 use std::sync::atomic::{AtomicU64, Ordering};
4689 static SY: AtomicU64 = AtomicU64::new(0);
4690 static RS: AtomicU64 = AtomicU64::new(0);
4691 static N: AtomicU64 = AtomicU64::new(0);
4692 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
4693 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
4694 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
4695 if n % 100 == 0 {
4696 eprintln!(
4697 "postprof: sync-wait {:.1} ms/tok | read_states {:.1} ms/tok ({n})",
4698 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
4699 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
4700 );
4701 }
4702 }
4703 if let Some(rows) = lm_rows {
4704 crate::gpu::hostprof_encode_done(_mt0);
4705 let mut lg = attention::take_buf(rows.min(self.vocab_size));
4706 graph.read_logits(&mut lg);
4707 crate::gpu::hostprof_total(_mt0);
4708 lg.resize(self.vocab_size, 0.0);
4709 if let Some(c) = self.final_softcap {
4710 softcap_in_place(&mut lg, c, self.pool.as_deref());
4711 }
4712 self.graph_logits = Some(lg);
4713 }
4714 graph.read_h(h);
4715 if !taps.is_empty() && tapped == taps.len() {
4717 let mut row = vec![0f32; taps.len() * self.hidden_size];
4718 if graph.read_taps(&mut row) {
4719 if let Some(d) = self.dflash.as_mut() {
4720 d.queue_context(position, &row, self.pool.as_deref());
4721 }
4722 }
4723 }
4724 if self.graph_head_required && self.graph_logits.is_none() {
4725 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4726 self.fail_metal_graph("fused graph head completed without logits readback");
4727 return start;
4728 }
4729 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4730 METAL_GRAPH_LAYERS.fetch_add(
4731 end.saturating_sub(start) as u64,
4732 std::sync::atomic::Ordering::Relaxed,
4733 );
4734 if self.graph_head_required {
4735 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4736 }
4737 for (li, nkv, hd) in dev_attn {
4741 let mut krow = attention::take_buf(nkv * hd);
4742 let mut vrow = attention::take_buf(nkv * hd);
4743 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
4744 let cache = &mut self.kv_cache.layers[li];
4745 cache.append(&krow, &vrow, &[]);
4746 let n = cache.seq_len;
4747 let mut imp = attention::take_buf(n);
4748 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
4749 cache.accumulate_imp(&imp);
4750 attention::recycle_buf(&mut imp);
4751 }
4752 attention::recycle_buf(&mut krow);
4753 attention::recycle_buf(&mut vrow);
4754 }
4755 if let Some(profile) = pager_profile_page.as_mut() {
4756 profile.finish(end, graph.pager_page_stats(), "page-complete");
4757 }
4758 if let Some((_, arm)) = ab {
4759 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
4760 }
4761 end
4762 }
4763
4764 pub fn new(
4765 tokenizer: Tokenizer,
4766 weights: PipelineWeights,
4767 hidden_size: usize,
4768 intermediate_size: usize,
4769 num_heads: usize,
4770 num_kv_heads: usize,
4771 head_dim: usize,
4772 num_layers: usize,
4773 physical_layers: usize,
4774 loop_final_norm: bool,
4775 vocab_size: usize,
4776 rms_eps: f64,
4777 rope_base: f32,
4778 norm_style: NormStyle,
4779 max_seq_len: usize,
4780 sampler_config: SamplerConfig,
4781 ) -> Self {
4782 let rng = match sampler_config.seed {
4783 Some(s) => SplitMix64::new(s),
4784 None => SplitMix64::from_entropy(),
4785 };
4786 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
4787 let pool = Pool::from_env();
4788 if let Some(p) = &pool {
4789 tracing::info!("worker pool: {} threads", p.n_workers());
4790 if let Some(model) = weights
4792 .lm_head
4793 .model_arc()
4794 .or_else(|| weights.embed_tokens.model_arc())
4795 {
4796 let regions: Vec<&[u8]> =
4797 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
4798 p.bind_numa(®ions);
4799 }
4800 }
4801 Self {
4802 gpu_plan: None,
4803 tokenizer: std::sync::Arc::new(tokenizer),
4804 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
4805 sampler_config,
4806 weights,
4807 hidden_size,
4808 intermediate_size,
4809 num_heads,
4810 num_kv_heads,
4811 head_dim,
4812 num_layers,
4813 physical_layers,
4814 loop_final_norm,
4815 layer_map: None,
4816 dash: None,
4817 dash_batch_on: true,
4818 mhc: None,
4819 mhc_r: Vec::new(),
4820 mrope_sections: None,
4821 prompt_mrope: None,
4822 vocab_size,
4823 rms_eps,
4824 rope_base,
4825 norm_style,
4826 rotary_dim: head_dim,
4827 attention_heads_per_layer: None,
4828 kv_heads_per_layer: None,
4829 v_head_dim: None,
4830 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
4831 .filter(|v| !v.is_empty())
4832 .map(std::path::PathBuf::from),
4833 graph_declines: std::cell::RefCell::new(Vec::new()),
4834 mimo_moe: Default::default(),
4835 minicpm_tail: Default::default(),
4836 vmf_cfg: None,
4837 gdn_cfg: None,
4838 kda_cfg: None,
4839 g3n: None,
4840 g4e: None,
4841 dsv4: None,
4842 dory: None,
4843 dsv41: None,
4844 dsv41_vision: None,
4845 dsv41_prefill: None,
4846 qwen4_exp: None,
4847 clover: None,
4848 clover_stats: clover::CloverStats::default(),
4849 clover_pen: Default::default(),
4850 dsv4_mtp: Vec::new(),
4851 dspark: None,
4852 dspark_pending: Vec::new(),
4853 dspark_hist: Vec::new(),
4854 dspark_real: Vec::new(),
4855 dspark_trunk_picks: Vec::new(),
4856 dspark_exp: Vec::new(),
4857 dspark_draft_ns: 0,
4858 m3_winners: Vec::new(),
4859 m3_counts: Vec::new(),
4860 m3_n_exp: 0,
4861 m3_tokens: 0,
4862 m3_prewarmed: false,
4863 logit_multiplier: None,
4864 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
4865 graph_failed: std::sync::atomic::AtomicBool::new(false),
4866 graph_moe_banks: std::sync::Mutex::new(std::collections::HashMap::new()),
4867 kv_history: Vec::new(),
4868 kv_history_device: false,
4869 short_conv_cfg: None,
4870 mtp: None,
4871 mimo_mtp: None,
4872 verify_exact_moe: false,
4873 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
4874 ignore_eos: false,
4875 draft_full_streak: 0,
4876 spec_k_adapt: None,
4877 spec_acc_ewma: 0.7,
4878 spec_hold: SPEC_HOLD_PRIOR,
4879 spec_wall: [0.0; 7],
4880 spec_wall_n: [0; 7],
4881 spec_k_dwell: 0,
4882 spec_plain_ms: (0.0, 0),
4883 dflash_hold: DFLASH_HOLD_PRIOR,
4884 dflash_last: false,
4885 dflash_wait: 0,
4886 dflash_back: DFLASH_WAIT,
4887 dflash_turn: 0,
4888 dflash_turn_sum: (0.0, 0.0),
4889 dflash_lean: 0.0,
4890 dflash_k_adapt: None,
4891 dflash_acc_ewma: 0.6,
4892 spec_lookup_gain: 2.0,
4893 spec_lookup_wide: false,
4894 spec_lookup_rest: 0,
4895 rng,
4896 sampler_scratch: SamplerScratch::default(),
4897 spec_forced: None,
4898 spec_q: Vec::new(),
4899 spec_p: Vec::new(),
4900 spec_res: Vec::new(),
4901 spec_qs: Vec::new(),
4902 spec_ps: Vec::new(),
4903 spec_ress: Vec::new(),
4904 mtp_graph_mode: None,
4905 dflash: None,
4906 dflash_rows: None,
4907 dflash_taps: std::sync::Mutex::new(Vec::new()),
4908 dflash_solo: false,
4909 dflash_tap_from: 0,
4910 #[cfg(target_os = "macos")]
4911 metal_verify: None,
4912 inv_freq,
4913 ws: ForwardScratch::new(hidden_size),
4914 pool,
4915 model: None,
4916 dyn_force_f32: false,
4917 dyn_skill_layers: Vec::new(),
4918 dyn_active: None,
4919 dyn_blend_loaded: false,
4920 dyn_phi_layer: None,
4921 dyn_phi_ema: Vec::new(),
4922 dyn_phi_seen: 0,
4923 dyn_router: None,
4924 o1_cfg: None,
4925 o1_turbo: None,
4926 mtp_pos_base: 0,
4927 o1_prefix_tail: Vec::new(),
4928 gdn_snap: None,
4929 mtp_warm_base: None,
4930 o1_epoch: 0,
4931 o1_flags: Vec::new(),
4932 trace: false,
4933 calib_temp: 1.0,
4934 confidence_on: true,
4935 embed_multiplier: 1.0,
4936 attn_scale: 1.0 / (head_dim as f32).sqrt(),
4937 swa: None,
4938 swa_trim: None,
4939 swa_trim_anchor: usize::MAX,
4940 chunk_min_rows: 32,
4941 o1_chunk_prefix: false,
4942 dense_batch_declined: std::sync::atomic::AtomicBool::new(false),
4943 sliding_layers: None,
4944 no_rope_layers: None,
4945 value_blend: None,
4946 vres_e0: Vec::new(),
4947 vres_in: Vec::new(),
4948 anchor_core: None,
4949 bounded_rope: None,
4950 kv_prefix: KvPrefix::default(),
4951 last_prefill_tokens: 0,
4952 inv_freq_local: None,
4953 rotary_dim_local: None,
4954 rope_scale: 1.0,
4955 rope_scale_local: 1.0,
4956 global_attn: None,
4957 inv_freq_global: None,
4958 attn_v_norm: false,
4959 qk_norm_after_rope: false,
4960 proj_gate_sigmoid: false,
4961 final_softcap: None,
4962 head_clusters: None,
4963 attn_softcap: 0.0,
4964 graph_want_logits: false,
4965 graph_head_required: false,
4966 graph_logits: None,
4967 embryo_graph: None,
4968 embryo_wide_refused: false,
4969 graph_refused: std::sync::atomic::AtomicBool::new(false),
4970 graph_kv_id: {
4971 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
4972 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
4973 },
4974 #[cfg(test)]
4975 nll_test_fail_at: None,
4976 #[cfg(test)]
4977 nll_test_force_serial: false,
4978 }
4979 }
4980
4981 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
4989 if let Err(e) = self.try_set_o1(cfg) {
4990 tracing::error!("{e}");
4991 }
4992 }
4993
4994 pub fn bounded_native(&self) -> bool {
4998 self.anchor_core.is_some()
4999 }
5000
5001 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
5004 if self.dory.is_some() {
5005 return crate::gpu::dory_kv_bytes(self.graph_kv_id).map(|b| (0, b));
5006 }
5007 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
5008 }
5009
5010 pub fn o1_refusal(&self) -> Option<String> {
5012 self.anchor_core.as_ref().map(|ac| {
5013 format!(
5014 "--o1 / CMF_O1 refused: the anchor is native bounded \
5015 (anchor_core kind={} window={} sink={}); the file's operator \
5016 is executed as-is and no post-hoc Nyström overlay applies",
5017 ac.kind, ac.window, ac.sink
5018 )
5019 })
5020 }
5021
5022 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
5024 if let Some(b) = &mut self.dory {
5026 b.0.o1 = cfg.map(|c| crate::dory::DoryO1 { m: c.m, w: c.w, sink: c.sink, seal: (c.w + c.sink + 16).max(8192) });
5027 return Ok(());
5028 }
5029 if let Some(c) = &cfg {
5030 if let Some(why) = self.o1_refusal() {
5031 self.o1_flags = Vec::new();
5032 self.o1_cfg = None;
5033 return Err(why);
5034 }
5035 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
5036 self.o1_flags.clear();
5037 self.o1_cfg = None;
5038 return Err(format!(
5039 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
5040 c.w, c.sink
5041 ));
5042 }
5043 }
5044 self.o1_flags = match &cfg {
5045 Some(c) => {
5046 let mut flags = c.layer_flags(self.num_layers);
5047 for (li, f) in flags.iter_mut().enumerate() {
5048 if *f
5054 && (!matches!(
5055 self.weights.layers[self.phys_layer(li)].attn,
5056 AttnKind::Full { .. }
5057 ) || self.layer_window(li).is_some()
5058 || self.kv_cache.layers[li].sinks.is_some()
5059 || self.layer_v_dim(li) != self.layer_geom(li).1)
5060 {
5061 *f = false;
5062 }
5063 }
5064 flags
5065 }
5066 None => Vec::new(),
5067 };
5068 if let Some(c) = &cfg {
5069 let n = self.o1_flags.iter().filter(|&&f| f).count();
5070 tracing::info!(
5071 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
5072 self.num_layers,
5073 c.m,
5074 c.w,
5075 c.sink,
5076 c.rect
5077 );
5078 }
5079 self.o1_cfg = cfg;
5080 Ok(())
5081 }
5082
5083 pub fn install_bounded(&mut self, cfg: &cortiq_core::AnchorCoreConfig) -> Result<(), String> {
5089 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
5090 return Err(format!(
5091 "anchor_core kind '{}' is not executable by this runtime",
5092 cfg.kind
5093 ));
5094 }
5095 if cfg.window == 0 {
5096 return Err("anchor_core.window must be >= 1".into());
5097 }
5098 let mut n = 0usize;
5099 for li in 0..self.num_layers {
5100 let pl = self.phys_layer(li);
5101 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
5102 if w.window != cfg.window
5103 || w.sink != cfg.sink
5104 || w.group_windows != cfg.group_windows
5105 {
5106 return Err(format!(
5107 "layer {li}: bounded weights (window {} sink {} groups {:?}) disagree \
5108 with anchor_core (window {} sink {} groups {:?})",
5109 w.window, w.sink, w.group_windows, cfg.window, cfg.sink, cfg.group_windows
5110 ));
5111 }
5112 self.kv_cache.layers[li].install_bounded(cfg.window);
5113 n += 1;
5114 }
5115 }
5116 if n == 0 {
5117 return Err("anchor_core is present but no layer executes it".into());
5118 }
5119 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
5120 self.bounded_rope = Some(std::sync::Arc::new(rope));
5121 self.anchor_core = Some(cfg.clone());
5122 self.embryo_graph = None;
5123 tracing::info!(
5124 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
5125 cfg.kind,
5126 cfg.window,
5127 cfg.sink,
5128 self.kv_cache
5129 .layers
5130 .iter()
5131 .map(|l| l.bounded_state_bytes())
5132 .max()
5133 .unwrap_or(0)
5134 );
5135 Ok(())
5136 }
5137
5138 pub fn install_wire_identity(&mut self, identity: u64) {
5142 for li in 0..self.kv_cache.layers.len() {
5143 let pl = self.phys_layer(li);
5144 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
5145 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
5146 Some(AttnKind::Linear(_))
5147 | Some(AttnKind::LinearGdn(_))
5148 | Some(AttnKind::ShortConv(_))
5149 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
5150 _ => crate::kv_cache::WireKind::Full,
5151 };
5152 let l = &mut self.kv_cache.layers[li];
5153 l.wire_kind = kind;
5154 l.wire_identity = identity;
5155 l.wire_layer = li as u32;
5158 }
5159 }
5160
5161 pub fn clear_history(&mut self) {
5163 self.kv_history.clear();
5164 self.kv_history_device = false;
5165 self.kv_prefix.clear();
5166 }
5167
5168 pub fn graph_refused(&self) -> bool {
5172 self.graph_refused
5173 .load(std::sync::atomic::Ordering::Relaxed)
5174 }
5175
5176 pub fn mark_graph_refused(&self) {
5178 if !self
5179 .graph_refused
5180 .swap(true, std::sync::atomic::Ordering::Relaxed)
5181 {
5182 tracing::info!(
5183 "token graph: unsupported for this pipeline (seq {}) — not retrying",
5184 self.graph_kv_id
5185 );
5186 }
5187 }
5188
5189 pub fn device_sequence_position(&self) -> Option<usize> {
5193 crate::gpu::embryo_device_next_position(self.graph_kv_id)
5194 }
5195
5196 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
5201 let dev = self.device_sequence_position();
5202 if recorded_on_device {
5203 dev == Some(n) && self.embryo_resident_wanted()
5204 } else {
5205 dev.is_none()
5206 }
5207 }
5208
5209 pub(crate) fn invalidate_for_weight_change(&mut self) {
5217 self.clear_sequence_state();
5218 self.embryo_graph = None;
5219 }
5220
5221 fn gdn_snap_ok(&self) -> bool {
5224 crate::gpu::cuda_active()
5225 && self.gdn_cfg.is_some()
5226 && self.o1_cfg.is_none()
5227 && self.dory.is_none()
5228 && self.dsv41.is_none()
5229 && self.qwen4_exp.is_none()
5230 && std::env::var("CMF_KV_REUSE").as_deref() != Ok("0")
5231 }
5232
5233 fn gdn_snap_target(&self, input_ids: &[u32], from: usize) -> Option<usize> {
5238 if !self.gdn_snap_ok() {
5239 return None;
5240 }
5241 let id = self.tokenizer.token_to_id("<|im_start|>")?;
5242 let s = input_ids.iter().rposition(|&t| t == id)?;
5243 (s > from && s + 1 < input_ids.len()).then_some(s)
5244 }
5245
5246 fn gdn_snap_reuse(&mut self, input_ids: &[u32]) -> usize {
5249 let n = match &self.gdn_snap {
5250 Some(s) if !s.is_empty() && s.len() < input_ids.len() && input_ids[..s.len()] == s[..] => s.len(),
5251 _ => return 0,
5252 };
5253 if !self.gdn_snap_ok() || !crate::gpu::gdn_snapshot_restore(self.graph_kv_id, n) {
5254 self.gdn_snap = None;
5255 return 0;
5256 }
5257 n
5258 }
5259
5260 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
5266 let (n, on_device) = if self.bounded_native() {
5267 (
5268 self.kv_prefix.extension(input_ids),
5269 self.kv_prefix.on_device(),
5270 )
5271 } else {
5272 let h = &self.kv_history;
5273 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
5274 (h.len(), self.kv_history_device)
5275 } else if !h.is_empty() && !self.kv_history_device {
5276 (self.rollback_prefix_len(input_ids), false)
5277 } else {
5278 (0, false)
5279 }
5280 };
5281 if n > 0 && !self.prefix_owner_matches(n, on_device) {
5284 tracing::warn!(
5285 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
5286 the device now holds {:?} — re-prefilling from zero",
5287 if on_device { "resident device" } else { "host" },
5288 self.device_sequence_position()
5289 );
5290 return 0;
5291 }
5292 n
5293 }
5294
5295 fn rollback_prefix_len(&self, input_ids: &[u32]) -> usize {
5306 if std::env::var("CMF_KV_ROLLBACK").as_deref() == Ok("0")
5307 || self.device_sequence_position().is_some()
5308 {
5309 return 0;
5310 }
5311 let h = &self.kv_history;
5312 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
5313 let n = lcp.min(input_ids.len().saturating_sub(1));
5314 if n == 0
5317 || self.kv_cache.layers.len() < self.num_layers
5318 || self.weights.layers.len() < self.num_layers
5319 {
5320 return 0;
5321 }
5322 let kv_id = self.graph_kv_id;
5323 for li in 0..self.num_layers {
5324 let l = &self.kv_cache.layers[li];
5325 let tail_ok = l.base() == 0
5326 || self
5327 .layer_window(li)
5328 .is_some_and(|w| l.base() + w <= n + 1);
5329 if !matches!(self.weights.layers[self.phys_layer(li)].attn, AttnKind::Full { .. })
5330 || l.o1.is_some()
5331 || l.bounded.is_some()
5332 || !l.linear_state.is_empty()
5333 || l.mode != crate::kv_cache::KvMode::F32
5334 || l.pos_len() != h.len()
5335 || !tail_ok
5336 || crate::gpu::graph_kv_stored(kv_id, li).is_some()
5337 || crate::gpu::graph_state_resident(kv_id, li)
5338 {
5339 return 0;
5340 }
5341 }
5342 n
5343 }
5344
5345 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
5348 self.cached_prefix_len(input_ids)
5349 }
5350
5351 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
5357 let on_device = self.device_sequence_position().is_some();
5358 if self.bounded_native() {
5359 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
5360 let prev_device = self.kv_prefix.on_device();
5361 self.kv_history.clear();
5362 self.kv_history_device = false;
5363 if keep && prev_device == on_device {
5364 self.kv_prefix.extend(&consumed[reused..]);
5365 } else {
5366 self.kv_prefix.set(consumed);
5367 }
5368 self.kv_prefix.set_on_device(on_device);
5369 } else {
5370 self.kv_history = consumed.to_vec();
5371 self.kv_history_device = on_device;
5372 }
5373 }
5374
5375 pub fn o1_active(&self) -> bool {
5377 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
5378 }
5379
5380 #[cfg(not(target_os = "macos"))]
5400 fn tiled_sliding_batch(&self) -> bool {
5401 (self.sliding_layers.is_some() || self.swa.is_some())
5402 && !self.weights.layers.iter().any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
5403 && crate::gpu::batched_attend_tiled()
5404 }
5405
5406 pub fn generation_batch_k(&self) -> usize {
5407 if let Some(k) = std::env::var("CMF_BATCH_K")
5408 .ok()
5409 .and_then(|v| v.parse::<usize>().ok())
5410 {
5411 return k;
5412 }
5413 if self.g4_batch_prefill() {
5416 return 128;
5417 }
5418 #[cfg(target_os = "macos")]
5419 if self.o1_active() && self.o1_on_graph() && crate::gpu::q1_force() && crate::gpu::enabled_here() {
5420 return if crate::gpu_metal::ane::wanted() { 512 } else { 128 };
5421 }
5422 #[cfg(not(target_os = "macos"))]
5427 if self.o1_active()
5428 && self.o1_on_graph()
5429 && crate::gpu::enabled_here()
5430 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
5431 {
5432 if let Some(k) = crate::gpu::cuda_batch_rows() {
5433 return k;
5434 }
5435 return if self.tiled_sliding_batch() { 512 } else { 128 };
5436 }
5437 #[cfg(not(target_os = "macos"))]
5438 if self.graph_prefill_preferred() && !self.o1_active() {
5439 if let Some(k) = crate::gpu::cuda_batch_rows() {
5442 return k;
5443 }
5444 if self.tiled_sliding_batch() {
5450 return 512;
5451 }
5452 let moe = self
5456 .weights
5457 .layers
5458 .iter()
5459 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
5460 if !moe && self.graph_gdn_layer_count() > 0 && crate::gpu::batched_attend_tiled() {
5466 return 512;
5467 }
5468 let q82 = crate::gpu::moe_q82_graph_on()
5474 && self.weights.layers.iter().any(|lw| {
5475 matches!(&lw.ffn, FfnKind::Moe(m)
5476 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
5477 });
5478 let q4tp_moe = self.weights.layers.iter().any(|lw| {
5485 matches!(&lw.ffn, FfnKind::Moe(m)
5486 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q4tp().is_some()))
5487 });
5488 return if q82 {
5489 128
5490 } else if q4tp_moe || self.onevision2_prefill() {
5491 256
5492 } else if moe {
5493 32
5494 } else {
5495 128
5496 };
5497 }
5498 0
5499 }
5500
5501 pub fn generation_graph_prefill(&self) -> bool {
5502 let graph = self.graph_prefill_preferred();
5503 #[cfg(not(target_os = "macos"))]
5514 if graph && self.generation_batch_k() > 0 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err() {
5515 return false;
5516 }
5517 graph
5518 }
5519
5520 pub fn o1_device_stats(&self) -> (usize, u64) {
5525 crate::gpu::o1_device_stats(self.graph_kv_id)
5526 }
5527
5528 pub fn o1_begin(&mut self) {
5533 self.o1_begin_with_prefix(None);
5534 }
5535
5536 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
5540 if let Some(c) = &self.o1_cfg {
5541 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
5542 let boundary = requested_prefix.map(|p| {
5543 p.max(
5544 crate::nystrom::o1_deferred_boundary(w, sink)
5545 .expect("o1 config boundary validated in set_o1"),
5546 )
5547 });
5548 let await_keep = boundary.is_some() && self.o1_exact_cfg().is_some();
5551 for (li, &f) in self.o1_flags.iter().enumerate() {
5552 if f {
5553 let cache = &mut self.kv_cache.layers[li];
5554 cache.o1_begin_with_boundary(m, w, sink, rect, boundary);
5555 cache.o1_await_keep = await_keep;
5556 }
5557 }
5558 }
5559 }
5560
5561 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
5563 self.o1_cfg.as_ref().and_then(|c| {
5564 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
5565 .map(|floor| requested_prefix.max(floor))
5566 })
5567 }
5568
5569 fn o1_note_transition(&mut self) {
5570 let mut transitioned = false;
5574 for (li, &flagged) in self.o1_flags.iter().enumerate() {
5575 if flagged {
5576 transitioned |= self.kv_cache.layers[li].take_o1_transition();
5577 }
5578 }
5579 if transitioned {
5580 self.o1_epoch = self.o1_epoch.wrapping_add(1);
5581 }
5582 }
5583
5584 fn o1_pending(&self) -> bool {
5585 self.o1_flags.iter().enumerate().any(|(li, &f)| {
5586 f && self.kv_cache.layers[li].seq_len > 0
5587 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
5588 })
5589 }
5590
5591 fn o1_fail(&mut self, err: String) {
5592 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
5593 self.clear_sequence_state();
5594 self.graph_failed
5595 .store(true, std::sync::atomic::Ordering::Relaxed);
5596 self.cancel
5597 .store(true, std::sync::atomic::Ordering::Relaxed);
5598 }
5599
5600 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
5605 if self.o1_cfg.is_none() {
5606 return Ok(false);
5607 }
5608 self.o1_choose_exact_rows();
5609 let (t_seal, mut sealed_now, epoch) = (std::time::Instant::now(), 0usize, self.o1_epoch);
5610 let mut todo: Vec<(usize, usize)> = Vec::new();
5611 for li in 0..self.num_layers {
5612 if !self.o1_flags.get(li).copied().unwrap_or(false) {
5613 continue;
5614 }
5615 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
5616 return Err(err);
5617 }
5618 if self.kv_cache.layers[li].seq_len == 0 {
5619 continue;
5620 }
5621 todo.push((li, self.layer_num_heads(li)));
5622 }
5623 let participating = !todo.is_empty();
5624 let par = std::env::var("CMF_O1_SEAL_PAR")
5631 .ok()
5632 .and_then(|v| v.parse::<usize>().ok())
5633 .unwrap_or_else(|| {
5634 if cfg!(target_os = "macos") {
5635 return 1;
5636 }
5637 let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
5638 let groups = todo.first().map_or(1, |&(li, _)| self.kv_cache.layers[li].num_kv_heads.max(1));
5639 cores.div_ceil(groups).clamp(1, 3)
5640 })
5641 .max(1);
5642 let mut slots: Vec<Option<&mut crate::kv_cache::LayerKvCache>> = self.kv_cache.layers.iter_mut().map(Some).collect();
5643 let mut jobs: Vec<(&mut crate::kv_cache::LayerKvCache, usize)> =
5644 todo.iter().filter_map(|&(li, nh)| slots[li].take().map(|c| (c, nh))).collect();
5645 for run in jobs.chunks_mut(par) {
5646 let done: Vec<Result<bool, String>> = if run.len() == 1 {
5647 let (c, nh) = &mut run[0];
5648 let was = c.o1_sealed();
5649 vec![c.o1_seal_checked(*nh).map(|ok| ok && !was)]
5650 } else {
5651 std::thread::scope(|s| {
5652 let hs: Vec<_> = run
5653 .iter_mut()
5654 .map(|(c, nh)| {
5655 let nh = *nh;
5656 s.spawn(move || {
5657 let was = c.o1_sealed();
5658 c.o1_seal_checked(nh).map(|ok| ok && !was)
5659 })
5660 })
5661 .collect();
5662 hs.into_iter()
5663 .map(|h| h.join().unwrap_or_else(|_| Err("o1 seal: a layer's thread panicked".into())))
5664 .collect()
5665 })
5666 };
5667 for r in done {
5668 sealed_now += r? as usize;
5669 }
5670 }
5671 drop(jobs);
5672 drop(slots);
5673 self.o1_note_transition();
5674 if self.o1_epoch != epoch && sealed_now > 0 {
5675 tracing::info!("o1: {sealed_now} layer(s) sealed in {:.2} s", t_seal.elapsed().as_secs_f64());
5676 }
5677 for li in 0..self.num_layers {
5678 if self.o1_flags.get(li).copied().unwrap_or(false) {
5679 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
5680 return Err(err);
5681 }
5682 }
5683 }
5684 Ok(participating
5685 && (0..self.num_layers).all(|li| {
5686 !self.o1_flags.get(li).copied().unwrap_or(false)
5687 || self.kv_cache.layers[li].seq_len == 0
5688 || self.kv_cache.layers[li].o1_sealed()
5689 }))
5690 }
5691
5692 fn o1_archive_chunk(&self, a: &mut O1Archive, pos: usize, bk: usize) -> Result<(), String> {
5694 let got = crate::gpu::o1_acap_take().ok_or("O(1) archive: readback failed")?;
5695 let qrow = a.nh * a.hd;
5696 let stride = a.stride;
5697 let kept = (pos..pos + bk).filter(|p| p % stride == 0).count();
5699 for (gl, q, k, v) in got {
5700 let Some(l) = a.layers.iter_mut().find(|l| l.li == gl) else { continue };
5701 l.k.push(&k)?;
5702 l.v.push(&v)?;
5703 if q.len() == kept * qrow && kept != bk {
5704 l.q.push(&q)?;
5705 continue;
5706 }
5707 if q.len() != bk * qrow {
5708 return Err(format!("O(1) archive: layer {gl} handed back {} query values", q.len()));
5709 }
5710 for p in pos..pos + bk {
5711 if p % stride == 0 {
5712 l.q.push(&q[(p - pos) * qrow..(p - pos + 1) * qrow])?;
5713 }
5714 }
5715 }
5716 a.rows = pos + bk;
5717 Ok(())
5718 }
5719
5720 fn o1_seal_archive(&mut self, a: O1Archive) -> Result<bool, String> {
5723 let t0 = std::time::Instant::now();
5724 let (rows, stride, nk, hd) = (a.rows, a.stride, a.nk, a.hd);
5725 let mut layers = Vec::with_capacity(a.layers.len());
5726 for l in a.layers {
5727 layers.push((l.li, l.k.freeze()?, l.v.freeze()?, l.q.freeze()?));
5728 }
5729 let keep = match (self.o1_exact_cfg().cloned(), self.o1_cfg.clone()) {
5730 (Some(x), Some(c)) => {
5731 let views: Vec<crate::dory::FlatRows> = layers.iter().map(|l| crate::dory::FlatRows { k: l.1.as_slice(), v: l.2.as_slice(), nk, hd }).collect();
5732 let refs: Vec<&dyn crate::dory::LayerRows> = views.iter().map(|v| v as &dyn crate::dory::LayerRows).collect();
5733 crate::dory::pick_exact_rows(&x, &refs, rows, nk, hd, c.m, c.sink, rows.saturating_sub(c.w))
5734 }
5735 _ => Vec::new(),
5736 };
5737 let t_pick = t0.elapsed().as_secs_f64();
5738 let mut all = !layers.is_empty();
5739 let prof = std::env::var("CMF_PREFILL_PROF").is_ok();
5740 let t_layers = std::time::Instant::now();
5741 let par = std::env::var("CMF_O1_SEAL_PAR")
5745 .ok()
5746 .and_then(|v| v.parse::<usize>().ok())
5747 .unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |p| p.get()).div_ceil(8).clamp(1, 4));
5748 let heads: Vec<usize> = (0..self.kv_cache.layers.len()).map(|li| self.layer_num_heads(li)).collect();
5749 let mut by_li: Vec<Option<_>> = (0..self.kv_cache.layers.len()).map(|_| None).collect();
5750 for (li, k, v, q) in layers {
5751 if li < by_li.len() {
5752 by_li[li] = Some((k, v, q));
5753 }
5754 }
5755 let mut jobs: Vec<(usize, &mut crate::kv_cache::LayerKvCache, _)> = Vec::new();
5756 for (li, cache) in self.kv_cache.layers.iter_mut().enumerate() {
5757 if let Some(d) = by_li[li].take() {
5758 jobs.push((li, cache, d));
5759 }
5760 }
5761 let keep_ref = &keep;
5762 let heads = &heads;
5763 let results: Vec<Result<bool, String>> = std::thread::scope(|sc| {
5764 let mut out = Vec::new();
5765 let mut jobs = jobs.into_iter();
5766 loop {
5767 let wave: Vec<_> = jobs.by_ref().take(par.max(1)).collect();
5768 if wave.is_empty() {
5769 break;
5770 }
5771 let hs: Vec<_> = wave
5772 .into_iter()
5773 .map(|(li, cache, (k, v, q))| {
5774 sc.spawn(move || -> Result<bool, String> {
5775 if !cache.o1_recollect(k.as_slice(), v.as_slice(), q.as_slice().to_vec(), stride, rows) {
5776 return Err(format!("O(1) archive: layer {li} is not sealed"));
5777 }
5778 drop((k, v, q));
5779 cache.o1_keep = keep_ref.clone();
5780 cache.o1_seal_checked(heads[li])
5781 })
5782 })
5783 .collect();
5784 for h in hs {
5785 out.push(h.join().unwrap_or_else(|_| Err("O(1) archive: a layer's seal panicked".into())));
5786 }
5787 }
5788 out
5789 });
5790 for r in results {
5791 all &= r?;
5792 }
5793 if prof {
5794 eprintln!("o1 archive seal: {} layers {par} at a time in {:.2} s", by_li.len(), t_layers.elapsed().as_secs_f64());
5795 }
5796 self.o1_note_transition();
5797 tracing::info!(
5798 "o1: far field rebuilt over {rows} rows, {} kept exact, chosen in {t_pick:.2} s, {:.2} s in all",
5799 keep.len(),
5800 t0.elapsed().as_secs_f64()
5801 );
5802 Ok(all)
5803 }
5804
5805 fn o1_prompt_chunk(&mut self, hiddens: &mut Vec<f32>, positions: &[usize], bk: usize) -> crate::gpu::BatchGraphOutcome {
5807 #[cfg(target_os = "macos")]
5808 if crate::gpu::q1_force() && self.o1_active() {
5809 return match self.prefill_hiddens_metal(hiddens.clone(), positions[0], None) {
5810 MetalPrefillOutcome::Completed(h) => {
5811 *hiddens = h;
5812 crate::gpu::BatchGraphOutcome::Completed
5813 }
5814 MetalPrefillOutcome::Declined => crate::gpu::BatchGraphOutcome::Declined,
5815 MetalPrefillOutcome::Failed => crate::gpu::BatchGraphOutcome::Failed,
5816 };
5817 }
5818 self.try_batch_graph_wgpu(hiddens, positions, bk, None)
5819 }
5820
5821 #[cfg(target_os = "macos")]
5824 fn o1_prefix_on_metal(
5825 &mut self,
5826 ids: &[u32],
5827 chunk: usize,
5828 mut arch: Option<&mut O1Archive>,
5829 ) -> Result<Option<Vec<f32>>, String> {
5830 const QT_STRIDE: usize = 8;
5831 let o1_layers: Vec<usize> = (0..self.num_layers)
5832 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
5833 .collect();
5834 let Some(&first) = o1_layers.first() else {
5835 return Ok(None);
5836 };
5837 let (nkv, hd) = {
5838 let c = &self.kv_cache.layers[first];
5839 (c.num_kv_heads, c.head_dim)
5840 };
5841 let nh = self.num_heads;
5842 if chunk == 0
5843 || o1_layers.iter().any(|&li| {
5844 let c = &self.kv_cache.layers[li];
5845 self.layer_num_heads(li) != nh
5846 || (c.num_kv_heads, c.head_dim) != (nkv, hd)
5847 || c.seq_len != 0
5848 || c.o1_pending_boundary().is_none()
5849 })
5850 {
5851 return Ok(None);
5852 }
5853 let hs = self.hidden_size;
5854 let n = ids.len();
5855 let mut last = vec![0f32; hs];
5856 let qrow = nh * hd;
5857 let mut qs: Vec<Vec<f32>> = vec![Vec::new(); o1_layers.len()];
5858 crate::gpu_metal::o1c::qcap_begin(&o1_layers);
5859 for &li in &o1_layers {
5860 self.kv_cache.layers[li].o1_set_q_stride(QT_STRIDE);
5861 }
5862 let mut pos = 0;
5863 let mut on_chunks = false;
5868 let mut step = chunk;
5869 while pos < n {
5870 let end = (pos + step).min(n);
5871 let bk = end - pos;
5872 let mut hiddens = vec![0f32; bk * hs];
5873 for (j, &id) in ids[pos..end].iter().enumerate() {
5874 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5875 }
5876 let rows = if on_chunks {
5877 MetalPrefillOutcome::Declined
5878 } else {
5879 self.prefill_hiddens_metal(hiddens.clone(), pos, None)
5880 };
5881 match rows {
5882 MetalPrefillOutcome::Completed(h) => last.copy_from_slice(&h[(bk - 1) * hs..]),
5883 MetalPrefillOutcome::Declined if pos == 0 || on_chunks => {
5884 if !on_chunks {
5885 on_chunks = true;
5886 let want = self.prefill_chunk().max(chunk);
5888 if want != step {
5889 step = want;
5890 continue;
5891 }
5892 }
5893 self.o1_chunk_prefix = true;
5894 let got = self.chunk_run_gpu(0, &mut hiddens, bk, pos, None, self.num_layers);
5895 self.o1_chunk_prefix = false;
5896 if got != self.num_layers {
5897 crate::gpu_metal::o1c::cap_end();
5898 if pos == 0 && got == 0 {
5899 for &li in &o1_layers {
5900 self.kv_cache.layers[li].o1_set_q_stride(1);
5901 }
5902 return Ok(None);
5903 }
5904 return Err("O(1) prefix: the Metal chunk graph stopped after admission".into());
5905 }
5906 self.swa_trim_tails();
5908 last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5909 }
5910 _ => {
5911 crate::gpu_metal::o1c::cap_end();
5912 return Err("O(1) prefix: the Metal rows graph stopped after admission".into());
5913 }
5914 }
5915 for (li, q, _, _) in crate::gpu_metal::o1c::cap_take() {
5916 let Some(i) = o1_layers.iter().position(|&l| l == li) else { continue };
5917 if q.len() != bk * qrow {
5918 crate::gpu_metal::o1c::cap_end();
5919 return Err(format!("O(1) prefix: {} query values, expected {}", q.len(), bk * qrow));
5920 }
5921 for p in pos..end {
5922 if p % QT_STRIDE == 0 {
5923 qs[i].extend_from_slice(&q[(p - pos) * qrow..(p - pos + 1) * qrow]);
5924 }
5925 }
5926 }
5927 pos = end;
5928 }
5929 crate::gpu_metal::o1c::cap_end();
5930 let row = nkv * hd;
5931 for (&li, q) in o1_layers.iter().zip(qs) {
5932 if q.len() != n.div_ceil(QT_STRIDE) * qrow {
5933 return Err(format!("O(1) prefix: layer {li} kept {} query values", q.len()));
5934 }
5935 if let Some(a) = arch.as_deref_mut() {
5936 let (mut k, mut v) = (vec![0f32; n * row], vec![0f32; n * row]);
5937 if !crate::gpu_metal::kv_mirror_read_rows(self.graph_kv_id, li, nkv, hd, 0, n, &mut k, &mut v) {
5938 return Err("O(1) prefix: K/V readback failed".into());
5939 }
5940 let dir = a.dir.clone();
5941 let mut layer = O1ArchLayer {
5942 li,
5943 k: ArchStore::new(dir.as_deref(), format!("{li}-k"))?,
5944 v: ArchStore::new(dir.as_deref(), format!("{li}-v"))?,
5945 q: ArchStore::new(dir.as_deref(), format!("{li}-q"))?,
5946 };
5947 layer.k.push(&k)?;
5948 layer.v.push(&v)?;
5949 layer.q.push(&q)?;
5950 a.layers.push(layer);
5951 a.rows = n;
5952 a.stride = QT_STRIDE;
5953 a.nh = nh;
5954 a.nk = nkv;
5955 a.hd = hd;
5956 }
5957 self.kv_cache.layers[li].o1_set_q_trace(q);
5958 }
5959 Ok(Some(last))
5960 }
5961
5962 fn o1_exact_cfg(&self) -> Option<&crate::dory::ExactRows> {
5963 static EXACT: std::sync::OnceLock<Option<crate::dory::ExactRows>> = std::sync::OnceLock::new();
5964 EXACT
5965 .get_or_init(|| crate::dory::exact_from_var("CMF_O1_EXACT"))
5966 .as_ref()
5967 .or(self.o1_turbo.as_ref().map(|t| &t.exact))
5968 }
5969
5970 pub fn is_far_turbo(&self) -> bool {
5971 self.o1_turbo.is_some() && self.o1_graph_backend()
5972 }
5973
5974 fn o1_graph_backend(&self) -> bool {
5976 crate::gpu::wgpu_active() || crate::gpu::cuda_active() || crate::gpu::backend_name() == "metal"
5977 }
5978
5979 #[cfg(not(target_os = "macos"))]
5987 fn o1_whole_prompt(&self) -> bool {
5988 if std::env::var("CMF_O1_WHOLE").as_deref() == Ok("1") {
5989 return true;
5990 }
5991 crate::gpu::cuda_active() && (self.graph_gdn_layer_count() == 0 || std::env::var("CMF_O1_CHUNKS").as_deref() == Ok("0"))
5992 }
5993
5994 pub fn far_turbo_seal(&self) -> usize {
5995 self.o1_turbo.as_ref().map_or(usize::MAX, |t| t.seal)
5996 }
5997
5998 fn o1_spec(&self) -> bool {
6001 self.o1_active()
6002 && self.o1_on_graph()
6003 && self.o1_graph_backend()
6004 && (self.o1_turbo.is_some() || std::env::var("CMF_O1_SPEC").as_deref() == Ok("1"))
6005 && std::env::var("CMF_O1_SPEC").as_deref() != Ok("0")
6006 }
6007
6008 fn o1_on_graph(&self) -> bool {
6010 self.o1_turbo.is_some() || std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
6011 }
6012
6013 fn o1_turbo_gate(&mut self, prompt_rows: usize) {
6016 if !self.is_far_turbo() {
6019 return;
6020 }
6021 let Some(t) = self.o1_turbo.clone() else { return };
6022 let want = prompt_rows >= t.seal;
6023 if want != self.o1_active() {
6024 self.clear_sequence_state();
6025 if let Err(e) = self.try_set_o1(want.then(|| t.cfg.clone())) {
6026 tracing::warn!("turbo file: the far field is off: {e}");
6027 self.o1_turbo = None;
6028 }
6029 }
6030 }
6031
6032 fn o1_choose_exact_rows(&mut self) {
6034 let Some(x) = self.o1_exact_cfg().cloned() else { return };
6035 let Some(c) = self.o1_cfg.clone() else { return };
6036 let layers: Vec<usize> = (0..self.num_layers)
6037 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false) && self.kv_cache.layers[li].seq_len > 0 && self.kv_cache.layers[li].o1_is_collecting())
6038 .collect();
6039 let Some(&first) = layers.first() else { return };
6040 let rows = self.kv_cache.layers[first].seq_len;
6041 let floor = crate::nystrom::o1_deferred_boundary(c.w, c.sink).unwrap_or(usize::MAX);
6042 let target = self.kv_cache.layers[first].o1_pending_boundary().unwrap_or(floor).max(floor);
6043 if rows < target || layers.iter().any(|&li| self.kv_cache.layers[li].seq_len != rows) {
6044 return;
6045 }
6046 let t0 = std::time::Instant::now();
6047 let (nk, hd) = (self.kv_cache.layers[first].num_kv_heads, self.kv_cache.layers[first].head_dim);
6048 let keep = {
6049 let views: Vec<_> = layers.iter().map(|&li| self.kv_cache.layers[li].group_rows()).collect();
6050 let refs: Vec<&dyn crate::dory::LayerRows> = views.iter().map(|v| v as &dyn crate::dory::LayerRows).collect();
6051 crate::dory::pick_exact_rows(&x, &refs, rows, nk, hd, c.m, c.sink, rows.saturating_sub(c.w))
6052 };
6053 tracing::info!("o1: {} exact rows of {rows} chosen in {:.2} s", keep.len(), t0.elapsed().as_secs_f64());
6054 for &li in &layers {
6055 self.kv_cache.layers[li].o1_keep = keep.clone();
6056 }
6057 }
6058
6059 fn o1_progress(&mut self) {
6062 if !self.o1_active() {
6063 return;
6064 }
6065 for li in 0..self.num_layers {
6066 if self.o1_flags.get(li).copied().unwrap_or(false) {
6067 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
6068 self.o1_fail(err);
6069 return;
6070 }
6071 }
6072 }
6073 self.o1_note_transition();
6077 if !self.o1_pending() {
6078 return;
6079 }
6080 if let Err(err) = self.o1_seal_checked() {
6081 self.o1_fail(err);
6082 }
6083 }
6084
6085 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
6090 if self
6091 .graph_failed
6092 .swap(false, std::sync::atomic::Ordering::Relaxed)
6093 {
6094 self.cancel
6095 .store(false, std::sync::atomic::Ordering::Relaxed);
6096 self.clear_sequence_state();
6097 return Err(format!("{phase}: deferred O(1) transition failed"));
6098 }
6099 Ok(())
6100 }
6101
6102 pub fn o1_seal(&mut self) {
6106 if let Err(err) = self.o1_seal_checked() {
6107 self.o1_fail(err);
6108 }
6109 }
6110
6111 fn o1_prefix_on_graph(
6120 &mut self,
6121 ids: &[u32],
6122 chunk: usize,
6123 mut arch: Option<&mut O1Archive>,
6124 ) -> Result<Option<Vec<f32>>, String> {
6125 let o1_layers: Vec<usize> = (0..self.num_layers)
6126 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
6127 .collect();
6128 let Some(&first) = o1_layers.first() else {
6129 return Ok(None);
6130 };
6131 let (nkv, hd) = {
6132 let c = &self.kv_cache.layers[first];
6133 (c.num_kv_heads, c.head_dim)
6134 };
6135 let nh = self.num_heads;
6136 if chunk == 0
6137 || o1_layers.iter().any(|&li| {
6138 let c = &self.kv_cache.layers[li];
6139 self.layer_num_heads(li) != nh
6140 || (c.num_kv_heads, c.head_dim) != (nkv, hd)
6141 || c.seq_len != 0
6142 || c.o1_pending_boundary().is_none()
6143 })
6144 || !crate::gpu::o1_qcap_begin(&o1_layers)
6145 {
6146 return Ok(None);
6147 }
6148 let hs = self.hidden_size;
6149 let n = ids.len();
6150 let mut last = vec![0f32; hs];
6151 let mut pos = 0;
6152 self.o1_prefix_tail.clear();
6153 let prof = std::env::var("CMF_PREFILL_PROF").is_ok();
6156 let (mut t_chunks, mut t_q) = (0f64, 0f64);
6157 const QT_STRIDE: usize = 8;
6160 let qrow = nh * hd;
6161 let strided = crate::gpu::o1_qcap_stride(QT_STRIDE);
6163 let mut qs: Vec<Vec<f32>> = vec![Vec::new(); o1_layers.len()];
6164 while pos < n {
6165 let end = (pos + chunk).min(n);
6166 let bk = end - pos;
6167 let mut hiddens = vec![0f32; bk * hs];
6168 for (j, &id) in ids[pos..end].iter().enumerate() {
6169 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
6170 }
6171 let positions: Vec<usize> = (pos..end).collect();
6172 let t_c = std::time::Instant::now();
6173 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
6174 t_chunks += t_c.elapsed().as_secs_f64();
6175 let t_d = std::time::Instant::now();
6176 match outcome {
6177 crate::gpu::BatchGraphOutcome::Completed => {
6178 last.copy_from_slice(&hiddens[(bk - 1) * hs..]);
6179 let tail_from = n.saturating_sub(crate::dory::EXACT_TAIL);
6181 if end > tail_from {
6182 let from = pos.max(tail_from);
6183 self.o1_prefix_tail.extend_from_slice(&hiddens[(from - pos) * hs..]);
6184 }
6185 }
6186 crate::gpu::BatchGraphOutcome::Declined if pos == 0 => {
6187 let _ = crate::gpu::o1_qcap_take();
6188 return Ok(None);
6189 }
6190 _ => {
6191 let _ = crate::gpu::o1_qcap_take();
6192 return Err("O(1) prefix batch graph stopped after admission".into());
6193 }
6194 }
6195 let Some(drained) = crate::gpu::o1_qcap_drain() else {
6196 let _ = crate::gpu::o1_qcap_take();
6197 return Err("O(1) prefix: query readback failed".into());
6198 };
6199 for (i, (_, rows)) in drained.iter().enumerate() {
6200 if strided {
6201 let kept = (pos..end).filter(|p| p % QT_STRIDE == 0).count();
6202 if rows.len() != kept * qrow {
6203 let _ = crate::gpu::o1_qcap_take();
6204 return Err(format!("O(1) prefix: {} query values, expected {}", rows.len(), kept * qrow));
6205 }
6206 qs[i].extend_from_slice(rows);
6207 continue;
6208 }
6209 if rows.len() != bk * qrow {
6210 let _ = crate::gpu::o1_qcap_take();
6211 return Err(format!("O(1) prefix: {} query values, expected {}", rows.len(), bk * qrow));
6212 }
6213 for p in pos..end {
6214 if p % QT_STRIDE == 0 {
6215 qs[i].extend_from_slice(&rows[(p - pos) * qrow..(p - pos + 1) * qrow]);
6216 }
6217 }
6218 }
6219 t_q += t_d.elapsed().as_secs_f64();
6220 pos = end;
6221 }
6222 let _ = crate::gpu::o1_qcap_take();
6223 let t_kv = std::time::Instant::now();
6224 let pulls: Vec<(usize, usize, usize)> = o1_layers.iter().map(|&li| (li, 0, n)).collect();
6225 let kv = crate::gpu::graph_kv_read_rows(self.graph_kv_id, &pulls, nkv, hd)
6226 .ok_or("O(1) prefix: K/V readback failed")?;
6227 let (t_kv, t_app) = (t_kv.elapsed().as_secs_f64(), std::time::Instant::now());
6228 let row = nkv * hd;
6229 if arch.is_none() {
6230 for ((&li, q), (k, v)) in o1_layers.iter().zip(&qs).zip(&kv) {
6233 if k.len() != n * row || v.len() != n * row || q.len() != n.div_ceil(QT_STRIDE) * qrow {
6234 return Err(format!(
6235 "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
6236 k.len(),
6237 v.len(),
6238 q.len(),
6239 n * row,
6240 n * row,
6241 n.div_ceil(QT_STRIDE) * qrow
6242 ));
6243 }
6244 }
6245 let mut work: Vec<(&mut crate::kv_cache::LayerKvCache, Vec<f32>, (Vec<f32>, Vec<f32>))> = Vec::new();
6246 let mut by_layer: Vec<Option<(Vec<f32>, (Vec<f32>, Vec<f32>))>> = (0..self.kv_cache.layers.len()).map(|_| None).collect();
6247 for ((&li, q), kvp) in o1_layers.iter().zip(qs).zip(kv) {
6248 by_layer[li] = Some((q, kvp));
6249 }
6250 for (cache, slot) in self.kv_cache.layers.iter_mut().zip(by_layer) {
6251 if let Some((q, kvp)) = slot {
6252 work.push((cache, q, kvp));
6253 }
6254 }
6255 let par = std::thread::available_parallelism().map_or(1, |p| p.get()).clamp(1, 8);
6256 let per = work.len().div_ceil(par).max(1);
6257 std::thread::scope(|sc| {
6258 for part in work.chunks_mut(per) {
6259 sc.spawn(move || {
6260 for (cache, q, (k, v)) in part.iter_mut() {
6261 cache.o1_set_q_stride(QT_STRIDE);
6262 cache.reserve_rows(n);
6263 for p in 0..n {
6264 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
6265 }
6266 cache.o1_set_q_trace(std::mem::take(q));
6267 }
6268 });
6269 }
6270 });
6271 if prof {
6272 eprintln!(
6273 "o1 prefix: {n} rows, chunks {t_chunks:.2} s, queries off the card {t_q:.2} s, K/V off the card {t_kv:.2} s, into the cache {:.2} s",
6274 t_app.elapsed().as_secs_f64()
6275 );
6276 }
6277 return Ok(Some(last));
6278 }
6279 for ((&li, q), (k, v)) in o1_layers.iter().zip(qs).zip(kv) {
6280 if k.len() != n * row || v.len() != n * row || q.len() != n.div_ceil(QT_STRIDE) * qrow {
6281 return Err(format!(
6282 "O(1) prefix: layer {li} read back {}/{}/{} values, expected {}/{}/{}",
6283 k.len(),
6284 v.len(),
6285 q.len(),
6286 n * row,
6287 n * row,
6288 n.div_ceil(QT_STRIDE) * qrow
6289 ));
6290 }
6291 let cache = &mut self.kv_cache.layers[li];
6292 cache.o1_set_q_stride(QT_STRIDE);
6293 cache.reserve_rows(n);
6294 for p in 0..n {
6295 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
6296 }
6297 if let Some(a) = arch.as_deref_mut() {
6298 let dir = a.dir.clone();
6299 let mut layer = O1ArchLayer {
6300 li,
6301 k: ArchStore::new(dir.as_deref(), format!("{li}-k"))?,
6302 v: ArchStore::new(dir.as_deref(), format!("{li}-v"))?,
6303 q: ArchStore::new(dir.as_deref(), format!("{li}-q"))?,
6304 };
6305 layer.k.push(&k)?;
6306 layer.v.push(&v)?;
6307 layer.q.push(&q)?;
6308 a.layers.push(layer);
6309 a.rows = n;
6310 a.stride = QT_STRIDE;
6311 a.nh = nh;
6312 a.nk = nkv;
6313 a.hd = hd;
6314 }
6315 cache.o1_set_q_trace(q);
6316 }
6317 if prof {
6318 eprintln!(
6319 "o1 prefix: {n} rows, chunks {t_chunks:.2} s, queries off the card {t_q:.2} s, K/V off the card {t_kv:.2} s, into the cache {:.2} s",
6320 t_app.elapsed().as_secs_f64()
6321 );
6322 }
6323 Ok(Some(last))
6324 }
6325
6326 pub fn set_trace(&mut self, on: bool) {
6328 self.trace = on;
6329 }
6330
6331 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
6334 self.rng = match config.seed {
6335 Some(seed) => SplitMix64::new(seed),
6336 None => SplitMix64::from_entropy(),
6337 };
6338 self.sampler_config = config;
6339 }
6340
6341 pub fn set_confidence(&mut self, on: bool) {
6346 self.confidence_on = on;
6347 }
6348
6349 pub fn set_calib_temp(&mut self, t: f32) {
6352 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
6353 }
6354
6355 pub fn calib_temp(&self) -> f32 {
6357 self.calib_temp
6358 }
6359
6360 pub fn rope_inv_freq(&self) -> &[f32] {
6364 self.inv_freq.as_slice()
6365 }
6366
6367 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
6370 self.rotary_dim = rotary_dim.min(self.head_dim);
6371 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
6372 self.embryo_graph = None;
6376 }
6377
6378 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
6379 QwenAttnCfg {
6380 mrope: None,
6381 num_heads: self.num_heads,
6382 num_kv_heads: self.num_kv_heads,
6383 head_dim: self.head_dim,
6384 hidden_size: self.hidden_size,
6385 position,
6386 inv_freq: &self.inv_freq,
6387 rotary_dim: self.rotary_dim,
6388 scale: self.attn_scale,
6389 softcap: self.attn_softcap,
6390 window: None,
6391 v_norm: false,
6392 qk_norm_after_rope: self.qk_norm_after_rope,
6393 gate_sigmoid: self.proj_gate_sigmoid,
6394 q_norm: None,
6395 k_norm: None,
6396 output_gate: false,
6397 softplus_gate: None,
6398 rope_scale: self.rope_scale,
6399 bias: None,
6400 rms_eps: self.rms_eps,
6401 norm_style: self.norm_style,
6402 pool: self.pool.as_deref(),
6403 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
6404 value_in: None,
6405 }
6406 }
6407
6408 pub fn generate(
6410 &mut self,
6411 prompt: &str,
6412 max_tokens: usize,
6413 task_mask: Option<&TaskMask>,
6414 on_token: Option<TokenCallback>,
6415 ) -> Result<GenerateResult, String> {
6416 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
6417 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
6418 }
6419
6420 pub fn generate_from_vl(
6423 &mut self,
6424 input: &crate::dsv41_vision::PreparedVlInputs,
6425 max_tokens: usize,
6426 task_mask: Option<&TaskMask>,
6427 on_token: Option<TokenCallback>,
6428 ) -> Result<GenerateResult, String> {
6429 let Some(dsv41) = &self.dsv41 else {
6430 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
6431 };
6432 if input.token_ids.is_empty() {
6433 return Err("empty V4.1 multimodal prompt".into());
6434 }
6435 if input.token_types.len() != input.token_ids.len() {
6436 return Err(format!(
6437 "V4.1 token type count {} != token count {}",
6438 input.token_types.len(),
6439 input.token_ids.len()
6440 ));
6441 }
6442 let dim = dsv41.2.dim;
6443 let mut embeddings = vec![None; input.token_ids.len()];
6444 let mut participates = vec![true; input.token_ids.len()];
6445 if !input.images.is_empty() {
6446 let vision = self
6447 .dsv41_vision
6448 .as_ref()
6449 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
6450 for image in &input.images {
6451 let end = image.start.saturating_add(image.types.len());
6452 if end > input.token_ids.len() {
6453 return Err(format!(
6454 "V4.1 image span {}..{} exceeds prompt length {}",
6455 image.start,
6456 end,
6457 input.token_ids.len()
6458 ));
6459 }
6460 let mut span = vec![0.0f32; image.types.len() * dim];
6461 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
6462 for (offset, &kind) in image.types.iter().enumerate() {
6463 let pos = image.start + offset;
6464 if input.token_types[pos] != kind {
6465 return Err(format!(
6466 "V4.1 image type mismatch at position {pos}: {} != {kind}",
6467 input.token_types[pos]
6468 ));
6469 }
6470 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
6471 participates[pos] = false;
6472 }
6473 }
6474 }
6475 for (pos, &kind) in input.token_types.iter().enumerate() {
6476 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
6477 return Err(format!("V4.1 text position {pos} has an image embedding"));
6478 }
6479 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
6480 return Err(format!("V4.1 image position {pos} has no image embedding"));
6481 }
6482 }
6483 self.dsv41_prefill = Some((embeddings, participates));
6484 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
6485 self.dsv41_prefill = None;
6486 result
6487 }
6488
6489 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
6491 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
6492 }
6493
6494 pub fn generate_from_ids(
6502 &mut self,
6503 input_ids: &[u32],
6504 max_tokens: usize,
6505 task_mask: Option<&TaskMask>,
6506 on_token: Option<TokenCallback>,
6507 ) -> Result<GenerateResult, String> {
6508 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
6509 }
6510
6511 pub fn generate_from_embeds(
6517 &mut self,
6518 input_ids: &[u32],
6519 prompt_rows: &[f32],
6520 max_tokens: usize,
6521 task_mask: Option<&TaskMask>,
6522 on_token: Option<TokenCallback>,
6523 ) -> Result<GenerateResult, String> {
6524 if input_ids.is_empty()
6525 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
6526 {
6527 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
6528 }
6529 if prompt_rows.iter().any(|x| !x.is_finite()) {
6530 return Err("embedded prompt contains non-finite values".into());
6531 }
6532 if !self.can_prefill_batched()
6533 || self.dyn_router.is_some()
6534 || self.o1_active()
6535 || self.mtp.is_some()
6536 || self.gpu_plan.is_some()
6537 {
6538 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
6539 }
6540 self.generate_with_prompt_rows(
6541 input_ids,
6542 Some(prompt_rows),
6543 max_tokens,
6544 task_mask,
6545 on_token,
6546 )
6547 }
6548
6549 fn generate_with_prompt_rows(
6550 &mut self,
6551 input_ids: &[u32],
6552 prompt_rows: Option<&[f32]>,
6553 max_tokens: usize,
6554 task_mask: Option<&TaskMask>,
6555 mut on_token: Option<TokenCallback>,
6556 ) -> Result<GenerateResult, String> {
6557 #[cfg(target_os = "macos")]
6558 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
6559 if std::env::var("CMF_TRACE_H").is_ok() {
6560 eprintln!("input_ids: {input_ids:?}");
6561 }
6562 if input_ids.is_empty() {
6563 return Err("empty prompt: nothing to generate from".to_string());
6564 }
6565 self.graph_failed
6569 .store(false, std::sync::atomic::Ordering::Relaxed);
6570 self.o1_turbo_gate(input_ids.len());
6575 self.mtp_pos_base = 0;
6576 let prev_mtp_warm = self.mtp_warm_base.take();
6577 let mut mtp_keep: Option<usize> = None;
6579 let task_mask = self.drop_open_mask(task_mask);
6580 if self.dash.is_some() {
6582 if prompt_rows.is_none() {
6583 return Err("Unlimited-OCR reads images: pass at least one".into());
6584 }
6585 let eos = self.tokenizer.eos_token_id.unwrap_or(1);
6586 if let Some(d) = self.dash.as_mut() {
6587 d.begin(input_ids, eos)?;
6588 }
6589 }
6590 if self.clover.is_some() && !self.clover_causal_request() {
6593 if prompt_rows.is_some() || task_mask.is_some() {
6594 return Err("block-diffusion generation takes token prompts without task masks".into());
6595 }
6596 return self.clover_generate(input_ids, max_tokens, on_token);
6597 }
6598
6599 let mut reuse_from = if self.dory.is_some() {
6607 self.dory_cached_prefix(input_ids)
6608 } else {
6609 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
6610 if on
6611 && prompt_rows.is_none()
6612 && task_mask.is_none()
6613 && self.mtp.is_none()
6614 && !(self.mimo_mtp.is_some() && self.speculative)
6615 && self.o1_cfg.is_none()
6616 && self.dsv41.is_none()
6617 {
6618 self.cached_prefix_len(input_ids)
6619 } else {
6620 0
6621 }
6622 };
6623 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
6626 reuse_from = 0;
6627 }
6628 if reuse_from == 0 && prompt_rows.is_none() && task_mask.is_none() {
6634 let n = self.gdn_snap_reuse(input_ids);
6635 if n > 0 {
6636 reuse_from = n;
6637 self.kv_history.truncate(n);
6638 for l in &mut self.kv_cache.layers {
6639 let d = l.pos_len().saturating_sub(n);
6640 if d > 0 {
6641 l.truncate_last(d);
6642 }
6643 }
6644 self.graph_logits = None;
6645 if self.mtp.is_some() {
6649 match prev_mtp_warm {
6650 Some(b) if b <= n => {
6651 self.mtp_pos_base = b;
6652 mtp_keep = Some(n - b);
6653 }
6654 _ => self.mtp_pos_base = n,
6655 }
6656 }
6657 if std::env::var("CMF_PREFILL_PROF").is_ok() {
6658 eprintln!("kv-reuse: GDN snapshot at {n} of {} prompt positions", input_ids.len());
6659 }
6660 }
6661 }
6662 if reuse_from > 0 && self.dory.is_some() && reuse_from < self.kv_history.len() {
6665 if let Some(b) = &mut self.dory {
6668 let kv_row = b.0.cfg.nk * b.0.cfg.hd;
6669 b.1.rollback(reuse_from, kv_row);
6670 }
6671 self.kv_history.truncate(reuse_from);
6672 self.graph_logits = None;
6673 }
6674 if reuse_from > 0 && self.dory.is_none() && !self.bounded_native() && reuse_from < self.kv_history.len() {
6675 for l in &mut self.kv_cache.layers {
6676 let d = l.pos_len() - reuse_from;
6677 l.truncate_last(d);
6678 }
6679 if std::env::var("CMF_PREFILL_PROF").is_ok() {
6680 eprintln!(
6681 "kv-reuse: rolled back {} answer position(s) to the common prefix",
6682 self.kv_history.len() - reuse_from
6683 );
6684 }
6685 self.kv_history.truncate(reuse_from);
6686 self.graph_logits = None;
6687 }
6688 self.swa_trim_anchor = input_ids.len();
6689 self.dflash_tap_from = match &self.dflash {
6690 Some(d) if (0..d.g.layers).all(|l| d.g.window_of(l) > 0) => {
6691 let w = (0..d.g.layers).map(|l| d.g.window_of(l)).max().unwrap_or(0);
6692 input_ids.len().saturating_sub(w + crate::dflash::BLOCK_SLACK)
6693 }
6694 _ => 0,
6695 };
6696 self.last_prefill_tokens = input_ids.len() - reuse_from;
6697 let bounded_native = self.bounded_native();
6698 if reuse_from == 0 {
6699 self.reset_fresh_mtp_draft_state();
6704 self.clear_sequence_state();
6705 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
6706 eprintln!(
6707 "kv-reuse: {} of {} prompt positions already cached",
6708 reuse_from,
6709 input_ids.len()
6710 );
6711 }
6712 crate::gpu::graph_race_begin_generation();
6713 let o1_prefill = if self.o1_active() && task_mask.is_none() {
6717 std::env::var("CMF_O1_PREFILL")
6718 .ok()
6719 .and_then(|v| v.parse::<usize>().ok())
6720 .filter(|&p| p > 0)
6721 .or_else(|| {
6723 self.o1_exact_cfg()
6724 .map(|_| {
6725 let max = std::env::var("CMF_O1_PREFIX_MAX")
6726 .ok()
6727 .and_then(|v| v.parse().ok())
6728 .or(self.o1_turbo.as_ref().map(|t| t.seal))
6729 .unwrap_or(O1_PREFIX_MAX);
6730 input_ids.len().saturating_sub(crate::dory::EXACT_TAIL).min(max)
6731 })
6732 .filter(|&p| p > 0)
6733 })
6734 } else {
6735 None
6736 };
6737 if task_mask.is_none() {
6738 self.o1_begin_with_prefix(o1_prefill);
6739 }
6740 if let Ok(f) = std::env::var("CMF_O1_TOKENS_DUMP") {
6741 let b: Vec<u8> = input_ids.iter().flat_map(|v| v.to_le_bytes()).collect();
6742 let _ = std::fs::write(f, b);
6743 }
6744
6745 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
6751 #[cfg(target_os = "macos")]
6789 let metal_graph = crate::gpu::q1_force()
6790 && crate::gpu::enabled_here()
6791 && std::env::var("CMF_GPU_BLOCK")
6792 .map(|v| v != "0")
6793 .unwrap_or(true);
6794 #[cfg(target_os = "macos")]
6800 let native_metal_backend = crate::gpu::q1_force();
6801 #[cfg(not(target_os = "macos"))]
6802 let metal_graph = false;
6803 #[cfg(not(target_os = "macos"))]
6804 let native_metal_backend = false;
6805 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
6806 let spec_cheap_round =
6810 self.sampler_config.temperature < 1e-6 || sampler::sparse_ok(&self.sampler_config);
6811 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
6812 || match spec_sample_env.as_deref() {
6813 Some("1") => true,
6814 Some(_) => false,
6815 None => metal_graph && spec_cheap_round,
6816 };
6817 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
6833 for lw in &self.weights.layers {
6834 if let FfnKind::Dense(d) = &lw.ffn {
6835 dense_n += 1;
6836 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
6837 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
6838 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
6839 {
6840 dense_q4tp += 1;
6841 }
6842 }
6843 }
6844 #[cfg(feature = "gpu")]
6855 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| {
6856 if native_metal_backend {
6857 self.mtp_block_shape_ok(m)
6858 } else {
6859 self.mtp_graph_ok(m)
6860 }
6861 });
6862 #[cfg(not(feature = "gpu"))]
6863 let mtp_on_graph = self.mtp.as_ref().is_none_or(|m| !native_metal_backend || self.mtp_block_shape_ok(m));
6864 let spec_default_ok = (dense_n == 0 || dense_q4tp * 10 >= dense_n * 9) && mtp_on_graph;
6865 let suppress_only_ok = crate::gpu::cuda_active()
6884 && self.sampler_config.repetition_penalty == 1.0
6885 && self.sampler_config.presence_penalty == 0.0;
6886 let penalized = !metal_graph
6887 && (self.sampler_config.repetition_penalty != 1.0
6888 || self.sampler_config.presence_penalty != 0.0
6889 || (!self.sampler_config.suppress_tokens.is_empty() && !suppress_only_ok));
6890 #[cfg(feature = "gpu")]
6895 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
6896 #[cfg(not(feature = "gpu"))]
6897 let metal_wgpu = false;
6898 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
6899 let spec_wanted = graph_spec_wanted_by_policy(
6900 spec_env.as_deref(),
6901 spec_default_ok,
6902 penalized,
6903 metal_wgpu,
6904 native_metal_backend,
6905 );
6906 if spec_wanted && metal_wgpu {
6907 tracing::warn!(
6908 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
6909 verified on this backend (garbage measured on Qwen3.5-0.8B)"
6910 );
6911 }
6912 let graph_spec = self.speculative
6916 && (graph_on || metal_graph)
6917 && self.mtp.is_some()
6918 && task_mask.is_none()
6919 && (!self.o1_active() || self.o1_spec())
6920 && spec_sampling_ok
6921 && spec_wanted;
6922 #[cfg(feature = "gpu")]
6927 let solo_spec = self.speculative
6928 && graph_on
6929 && !metal_wgpu
6930 && self.mtp.is_none()
6931 && task_mask.is_none()
6932 && (!self.o1_active() || self.o1_spec())
6933 && self.sampler_config.temperature < 1e-6
6934 && !penalized
6935 && spec_env.as_deref() != Some("0")
6936 && (self.dflash.is_some() || solo_script().is_some());
6937 #[cfg(feature = "gpu")]
6938 let solo_pin: Option<usize> =
6939 std::env::var("CMF_GRAPH_SPEC_K").ok().and_then(|v| v.parse().ok()).filter(|k| (1..=SOLO_K_MOST).contains(k));
6940 #[cfg(feature = "gpu")]
6941 let mut solo =
6942 SoloSpec { k: solo_pin.unwrap_or(4), rest: 3, back: SOLO_REST, plain_ms: 0.0, plain_n: 0, round_ms: 0.0, rounds: 0, win_ms: 0.0, win_tok: 0, last: None, first: true };
6943 let host_pair_opt_in = std::env::var("CMF_MTP_PAIR").as_deref() == Ok("1");
6956 let pair_pays = host_pair_opt_in
6957 && (self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1"));
6958 #[cfg(target_os = "macos")]
6961 if metal_graph {
6962 static SAID: std::sync::Once = std::sync::Once::new();
6963 SAID.call_once(|| {
6964 let spec = if graph_spec {
6965 let k = std::env::var("CMF_GRAPH_SPEC_K")
6966 .ok()
6967 .and_then(|v| v.parse::<usize>().ok())
6968 .filter(|&v| (1..=8).contains(&v))
6969 .map(|v| v.to_string())
6970 .unwrap_or_else(|| {
6971 if self.metal_small_batch_verify() {
6972 "auto".to_string()
6973 } else {
6974 "7".to_string()
6975 }
6976 });
6977 let arm = if self.sampler_config.temperature < 1e-6 {
6978 "greedy"
6979 } else {
6980 "sampling"
6981 };
6982 format!(
6983 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
6984 Self::draft_vocab_rows(usize::MAX)
6985 )
6986 } else if !self.speculative {
6987 "spec off (CMF_MTP=0)".to_string()
6988 } else if self.mtp.is_none() {
6989 "spec off (no MTP head)".to_string()
6990 } else if !spec_sampling_ok {
6991 if spec_cheap_round {
6992 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
6993 } else {
6994 "spec off (sampling without a top-k: the dense chain \
6995 costs more than it saves)"
6996 .to_string()
6997 }
6998 } else if !spec_wanted {
6999 if spec_env.as_deref() == Some("0") {
7000 "spec off (CMF_GRAPH_SPEC=0)".to_string()
7001 } else {
7002 "spec off (drafts by default only over a q4tp trunk; CMF_GRAPH_SPEC=1 asks for them)".to_string()
7003 }
7004 } else if task_mask.is_some() {
7005 "spec off (task mask)".to_string()
7006 } else if !graph_spec && !pair_pays {
7007 "spec off (host fused pair requires CMF_MTP_PAIR=1 after exact-ID validation)"
7008 .to_string()
7009 } else {
7010 "spec off (O(1) attention)".to_string()
7011 };
7012 let on = |var: &str| {
7013 if std::env::var(var).as_deref() == Ok("0") {
7014 "off"
7015 } else {
7016 "on"
7017 }
7018 };
7019 tracing::info!(
7020 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
7021 MTP graph {}, attend {}, probe {}",
7022 if crate::gpu_metal::state4_on() {
7023 "on"
7024 } else {
7025 "off"
7026 },
7027 if crate::gpu_metal::async_replay_on() {
7028 "on"
7029 } else {
7030 "off"
7031 },
7032 on("CMF_METAL_PREFILL"),
7033 on("CMF_MTP_GRAPH"),
7034 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
7035 if crate::gpu::probe_enabled() {
7036 "bypassed (q1 force)"
7037 } else {
7038 "off"
7039 },
7040 );
7041 });
7042 }
7043 let spec_active = self.speculative
7044 && self.mtp.is_some()
7045 && task_mask.is_none()
7046 && (!self.o1_active() || self.o1_spec())
7047 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
7048 let mut mtp = if spec_active { self.mtp.take() } else { None };
7051 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
7052 eprintln!(
7053 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
7054 mtp.is_some(),
7055 self.speculative,
7056 self.sampler_config.temperature < 1e-6,
7057 );
7058 }
7059 if let Some(m) = &mut mtp {
7060 let kept = mtp_keep.is_some_and(|rows| {
7061 crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, rows)
7062 });
7063 if kept {
7064 let rows = mtp_keep.unwrap_or(0);
7065 let d = m.kv.pos_len().saturating_sub(rows);
7066 if d > 0 {
7067 m.kv.truncate_last(d);
7068 }
7069 } else {
7070 m.kv.clear();
7071 crate::gpu::graph_kv_reset(self.mtp_kv_id());
7073 if mtp_keep.is_some() {
7074 self.mtp_pos_base = reuse_from;
7076 }
7077 }
7078 self.mtp_graph_mode = None;
7079 }
7080 let mimo_spec = self.speculative
7084 && self.mimo_mtp.is_some()
7085 && task_mask.is_none()
7086 && !self.o1_active()
7087 && self.dyn_router.is_none()
7088 && self.sampler_config.temperature < 1e-6
7089 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
7090 if let Some(st) = self.mimo_mtp.as_mut() {
7091 st.reset();
7092 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
7093 Self::mimo_mtp_hist_cap(st, input_ids.len());
7094 }
7095 }
7096 let mut router = if mtp.is_none() {
7100 self.dyn_router.take()
7101 } else {
7102 None
7103 };
7104 let mut reuse_from = reuse_from;
7105 if let Some(r) = &mut router {
7106 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
7109 let _ = self.set_active_skill(None);
7112 reuse_from = 0;
7113 self.last_prefill_tokens = input_ids.len();
7114 }
7115 }
7116
7117 let mut all_ids = input_ids.to_vec();
7118 let penalty_from = if self.clover.is_some() { input_ids.len() } else { 0 };
7121 let mut generated = 0usize;
7122 let mut finish_reason = "max_tokens".to_string();
7123 let mut drafted = 0usize;
7124 let mut accepted = 0usize;
7125 let mut dsv4_spec_bad = 0usize;
7132 let mut dsv4_spec_retry_at = 0usize;
7133 let mut confidence: Vec<f32> = Vec::new();
7134 let trace_on = self.trace;
7135 let calib_temp = self.calib_temp;
7136 let mut traces: Vec<TokenTrace> = Vec::new();
7137
7138 let mut hidden = vec![0.0f32; self.hidden_size];
7144 let mut pos = reuse_from;
7145 let fuse_lm = mtp.is_none()
7154 && router.is_none()
7155 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
7156 self.graph_logits = None;
7157 self.graph_want_logits = false;
7158 let _tpf = std::time::Instant::now();
7159 let batch_k = self.generation_batch_k();
7160 if let Some(rows) = prompt_rows {
7161 let hs = self.hidden_size;
7162 let chunk = if batch_k > 0 { batch_k } else { self.prefill_chunk().max(1) };
7164 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7165 let end = (pos + chunk).min(input_ids.len());
7166 let hb = match self.prefill_input_rows(
7167 PrefillIn::Hidden(&rows[pos * hs..end * hs]),
7168 pos,
7169 task_mask,
7170 ) {
7171 Ok(hb) => hb,
7172 Err(err) => {
7173 self.prompt_mrope = None;
7174 self.finish_generation(&mut mtp, &mut router, true);
7175 return Err(err);
7176 }
7177 };
7178 if mimo_spec {
7179 self.mimo_note_rows(&hb, pos);
7180 }
7181 hidden.copy_from_slice(&hb[hb.len() - hs..]);
7182 pos = end;
7183 }
7184 }
7185 self.prompt_mrope = None;
7188 while self.qwen4_exp.is_some()
7199 && mtp.is_none()
7200 && pos < input_ids.len()
7201 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7202 {
7203 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
7206 let want_logits = end == input_ids.len();
7207 let mut lg = Vec::new();
7208 if let Some(b) = &mut self.qwen4_exp {
7209 crate::qwen4_exp::forward_tokens(
7210 &b.0,
7211 &b.1,
7212 &b.2,
7213 &mut b.3,
7214 &input_ids[pos..end],
7215 pos,
7216 &self.inv_freq,
7217 self.pool.as_deref(),
7218 &mut lg,
7219 want_logits,
7220 );
7221 }
7222 if want_logits {
7223 self.graph_logits = Some(lg);
7224 }
7225 pos = end;
7226 hidden.fill(0.0);
7227 }
7228 while self.dsv4.is_some()
7229 && mtp.is_none()
7230 && pos < input_ids.len()
7231 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7232 {
7233 let end = (pos + prefill_chunk()).min(input_ids.len());
7234 let ids: Vec<u32> = input_ids[pos..end].to_vec();
7235 let mut lg = Vec::new();
7236 if let Some(b) = &mut self.dsv4 {
7237 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
7238 crate::dsv4::forward_chunk(
7239 g,
7240 layers,
7241 &cfg,
7242 st,
7243 &ids,
7244 pos,
7245 &self.inv_freq,
7246 self.pool.as_deref(),
7247 &mut lg,
7248 end == input_ids.len(),
7249 );
7250 }
7251 if end == input_ids.len() {
7252 self.graph_logits = Some(lg);
7253 }
7254 pos = end;
7255 hidden = vec![0.0; self.hidden_size];
7256 }
7257 if self.dory.is_some() && mtp.is_none() && pos < input_ids.len() {
7258 let lg = self.dory_forward(&input_ids[pos..], pos, crate::dory::Want::Last);
7259 self.graph_logits = Some(lg);
7260 pos = input_ids.len();
7261 hidden = vec![0.0; self.hidden_size];
7262 }
7263 let dsv41_prefill = self.dsv41_prefill.take();
7264 while self.dsv41.is_some()
7265 && mtp.is_none()
7266 && pos < input_ids.len()
7267 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7268 {
7269 let end = (pos + prefill_chunk()).min(input_ids.len());
7270 let ids: Vec<u32> = input_ids[pos..end].to_vec();
7271 let mut lg = Vec::new();
7272 if let Some(b) = &mut self.dsv41 {
7273 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
7274 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
7275 crate::dsv41::forward_chunk_masked_with_embeddings(
7276 g,
7277 layers,
7278 cfg,
7279 st,
7280 &ids,
7281 pos,
7282 &embeddings[pos..end],
7283 &participates[pos..end],
7284 self.pool.as_deref(),
7285 &mut lg,
7286 );
7287 } else {
7288 crate::dsv41::forward_chunk(
7289 g,
7290 layers,
7291 cfg,
7292 st,
7293 &ids,
7294 pos,
7295 self.pool.as_deref(),
7296 &mut lg,
7297 );
7298 }
7299 }
7300 if end == input_ids.len() {
7301 self.graph_logits = Some(lg);
7302 }
7303 pos = end;
7304 hidden = vec![0.0; self.hidden_size];
7305 }
7306 let dyn_prefill = router.is_some();
7311 let o1_prefill_limit = o1_prefill
7319 .and_then(|requested| self.o1_effective_boundary(requested))
7320 .map(|boundary| boundary.min(input_ids.len()));
7321 let mut o1_sealed = false;
7322 let mut o1_arch: Option<O1Archive> = None;
7323 if let Some(limit) = o1_prefill_limit {
7324 if pos == 0
7331 && limit < input_ids.len()
7332 && batch_k > 0
7333 && task_mask.is_none()
7334 && (mtp.is_none() || self.o1_spec())
7335 && !dyn_prefill
7336 && self.o1_on_graph()
7337 && std::env::var("CMF_O1_PREFIX_GPU").as_deref() != Ok("0")
7338 && (0..self.num_layers).all(|li| self.phys_layer(li) == li)
7339 {
7340 let t_px = std::time::Instant::now();
7341 let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7344 if limit < tail_from && self.o1_exact_cfg().is_some() {
7345 o1_arch = Some(O1Archive::new());
7346 }
7347 #[cfg(target_os = "macos")]
7353 let px_len = if crate::gpu::q1_force() && o1_arch.is_some() && self.metal_rows_plan().is_none() {
7354 o1_arch = None;
7355 let n = input_ids.len();
7356 for li in 0..self.num_layers {
7357 if self.o1_flags.get(li).copied().unwrap_or(false) {
7358 self.kv_cache.layers[li].o1_set_boundary(n);
7359 }
7360 }
7361 n
7362 } else {
7363 limit
7364 };
7365 #[cfg(not(target_os = "macos"))]
7366 let px_len = if self.o1_whole_prompt() {
7367 o1_arch = None;
7368 let n = input_ids.len();
7369 for li in 0..self.num_layers {
7370 if self.o1_flags.get(li).copied().unwrap_or(false) {
7371 self.kv_cache.layers[li].o1_set_boundary(n);
7372 }
7373 }
7374 n
7375 } else {
7376 limit
7377 };
7378 #[cfg(target_os = "macos")]
7379 let prefix = if crate::gpu::q1_force() {
7380 self.o1_prefix_on_metal(&input_ids[..px_len], batch_k, o1_arch.as_mut())
7381 } else {
7382 self.o1_prefix_on_graph(&input_ids[..px_len], batch_k, o1_arch.as_mut())
7383 };
7384 #[cfg(not(target_os = "macos"))]
7385 let prefix = {
7386 let hint = crate::gpu::cuda_active();
7388 if hint {
7389 crate::gpu::set_batch_kv_hint(px_len + 1);
7390 }
7391 let r = self.o1_prefix_on_graph(&input_ids[..px_len], batch_k, o1_arch.as_mut());
7392 if hint {
7393 crate::gpu::set_batch_kv_hint(0);
7394 }
7395 r
7396 };
7397 match prefix {
7398 Ok(Some(last)) => {
7399 hidden.copy_from_slice(&last);
7400 pos = px_len;
7401 let tail = std::mem::take(&mut self.o1_prefix_tail);
7404 if px_len == input_ids.len() && mtp.is_some() && !tail.is_empty() {
7405 let hs = self.hidden_size;
7406 let from = px_len - tail.len() / hs;
7407 self.mtp_pos_base = from;
7408 let n_pairs = mtp_prefill_pair_count(from, px_len, input_ids.len());
7409 let pairs: Vec<(&[f32], u32)> = (0..n_pairs).map(|j| (&tail[j * hs..(j + 1) * hs], input_ids[from + j + 1])).collect();
7410 let warm_error = if let Some(m) = mtp.as_mut() { self.mtp_warm_prefill_pairs(m, &pairs, from).err() } else { None };
7411 if let Some(err) = warm_error {
7412 self.finish_generation(&mut mtp, &mut router, true);
7413 return Err(err.to_string());
7414 }
7415 self.mtp_warm_base = Some(from);
7416 }
7417 if std::env::var("CMF_PREFILL_PROF").is_ok() {
7418 eprintln!(
7419 "o1 prefix on the batch graph: {px_len} rows in {:.1} ms",
7420 t_px.elapsed().as_secs_f64() * 1e3
7421 );
7422 }
7423 }
7424 Ok(None) => {}
7425 Err(err) => {
7426 self.finish_generation(&mut mtp, &mut router, true);
7427 return Err(err);
7428 }
7429 }
7430 }
7431 let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7436 let limit = if pos == 0 && limit < tail_from && self.o1_exact_cfg().is_some() {
7437 o1_arch = None;
7438 for li in 0..self.num_layers {
7439 if self.o1_flags.get(li).copied().unwrap_or(false) {
7440 self.kv_cache.layers[li].o1_set_boundary(tail_from);
7441 }
7442 }
7443 tracing::info!("o1: no device prefix; exact on the host up to row {tail_from}, then the seal");
7444 tail_from
7445 } else {
7446 limit
7447 };
7448 if pos < limit && self.can_prefill_batched() && limit > 2 {
7451 let chunk = self.prefill_chunk();
7452 let hs = self.hidden_size;
7453 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7454 let end = (pos + chunk).min(limit);
7455 let hb = self.prefill_batch(&input_ids[pos..end], pos);
7456 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7457 pos = end;
7458 }
7459 } else {
7460 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7461 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
7462 pos += 1;
7463 }
7464 }
7465 if pos >= limit {
7466 o1_sealed = match self.o1_seal_checked() {
7467 Ok(sealed) => sealed,
7468 Err(err) => {
7469 self.finish_generation(&mut mtp, &mut router, true);
7470 return Err(err);
7471 }
7472 };
7473 #[cfg(target_os = "macos")]
7476 if o1_sealed && crate::gpu::q1_force() {
7477 crate::gpu_metal::kv_mirror_drop(self.graph_kv_id);
7478 }
7479 tracing::info!(
7480 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
7481 o1_prefill.unwrap_or(0),
7482 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
7483 .unwrap_or(limit),
7484 limit,
7485 input_ids.len()
7486 );
7487 }
7488 }
7489 let graph_prefill = self.graph_prefill_preferred();
7495 #[cfg(target_os = "macos")]
7507 if task_mask.is_none()
7508 && !dyn_prefill
7509 && !o1_sealed
7510 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
7511 && crate::gpu::enabled_here()
7512 && !self.per_token_stack()
7513 && input_ids.len() > 8
7514 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
7515 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
7516 {
7517 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
7518 .ok()
7519 .and_then(|v| v.parse().ok())
7520 .filter(|&v| (16..=512).contains(&v))
7521 .unwrap_or(if crate::gpu_metal::ane::wanted() { 512 } else { 256 });
7523 let hs = self.hidden_size;
7524 let _tp = std::time::Instant::now();
7525 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7526 let end = (pos + chunk).min(input_ids.len());
7527 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
7528 MetalPrefillOutcome::Completed(hb) => hb,
7529 MetalPrefillOutcome::Declined => break,
7530 MetalPrefillOutcome::Failed => {
7531 self.finish_generation(&mut mtp, &mut router, true);
7532 return Err("ordinary Metal prefill failed after admission".into());
7533 }
7534 };
7535 if let Some(m) = &mut mtp {
7536 let n_pairs = if end < input_ids.len() {
7537 end - pos
7538 } else {
7539 end - pos - 1
7540 };
7541 if n_pairs > 0 {
7542 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
7543 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
7544 .collect();
7545 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
7546 for (j, (h, t)) in pairs.iter().enumerate() {
7547 let h = h.to_vec();
7548 let _ = self.mtp_step(m, &h, *t, pos + j);
7549 }
7550 }
7551 }
7552 }
7553 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7554 pos = end;
7555 }
7556 if std::env::var("CMF_PREFILL_PROF").is_ok() {
7557 eprintln!(
7558 "metal-prefill: {} of {} tokens in {:.1} ms",
7559 pos,
7560 input_ids.len(),
7561 _tp.elapsed().as_secs_f64() * 1e3
7562 );
7563 crate::gpu_metal::cut_report();
7565 }
7566 }
7567 #[cfg(not(target_os = "macos"))]
7572 if task_mask.is_none()
7573 && !dyn_prefill
7574 && mtp.is_none()
7575 && o1_prefill.is_none()
7576 && batch_k > 0
7577 && pos + 1 < input_ids.len()
7578 && self.g4_batch_prefill()
7579 {
7580 let t_g4 = std::time::Instant::now();
7581 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
7582 Ok((n, last)) => {
7583 if n > 0 {
7584 hidden.copy_from_slice(&last);
7585 }
7586 if std::env::var("CMF_PREFILL_PROF").is_ok() {
7587 eprintln!(
7588 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
7589 input_ids.len() - pos,
7590 t_g4.elapsed().as_secs_f64() * 1e3
7591 );
7592 }
7593 pos += n;
7594 }
7595 Err(e) => {
7596 self.finish_generation(&mut mtp, &mut router, true);
7597 return Err(e);
7598 }
7599 }
7600 }
7601 self.mimo_moe_prepare();
7602 #[cfg(not(target_os = "macos"))]
7608 if task_mask.is_none()
7609 && !dyn_prefill
7610 && !graph_prefill
7611 && mtp.is_none()
7612 && o1_prefill.is_none()
7613 && !self.o1_active()
7614 && input_ids.len() > 2
7615 && self.batch_prefix_prefill()
7616 {
7617 let chunk = self.prefill_chunk().max(1);
7618 let hs = self.hidden_size;
7619 let t_bp = std::time::Instant::now();
7620 let pos0 = pos;
7621 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7622 let end = (pos + chunk).min(input_ids.len());
7623 let bk = end - pos;
7624 let mut hiddens = vec![0f32; bk * hs];
7625 for (j, &id) in input_ids[pos..end].iter().enumerate() {
7626 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
7627 }
7628 let positions: Vec<usize> = (pos..end).collect();
7629 let mut run = 0usize;
7630 let outcome = self.try_batch_graph_wgpu_prefix(
7631 &mut hiddens,
7632 &positions,
7633 bk,
7634 None,
7635 Some(&mut run),
7636 );
7637 match outcome {
7638 crate::gpu::BatchGraphOutcome::Completed => {
7639 let hb = if run < self.num_layers {
7640 self.prefill_batch_span(
7641 PrefillIn::Hidden(&hiddens),
7642 pos,
7643 None,
7644 run,
7645 self.num_layers,
7646 )
7647 } else {
7648 hiddens
7649 };
7650 if mimo_spec {
7651 self.mimo_note_rows(&hb, pos);
7652 }
7653 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
7654 pos = end;
7655 }
7656 crate::gpu::BatchGraphOutcome::Failed => {
7657 self.finish_generation(&mut mtp, &mut router, true);
7658 return Err("batched prefix prefill failed after admission".into());
7659 }
7660 crate::gpu::BatchGraphOutcome::Declined => {
7661 #[cfg(feature = "gpu")]
7664 if pos > pos0 {
7665 self.pull_lagging_host_kv(0, self.num_layers, pos);
7666 }
7667 break;
7668 }
7669 }
7670 }
7671 if std::env::var("CMF_PREFILL_PROF").is_ok() {
7672 eprintln!(
7673 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
7674 pos - pos0,
7675 input_ids.len(),
7676 t_bp.elapsed().as_secs_f64() * 1e3
7677 );
7678 }
7679 }
7680 if task_mask.is_none()
7681 && !dyn_prefill
7682 && !graph_prefill
7683 && self.can_prefill_batched()
7684 && !self.per_token_stack()
7685 && o1_prefill.is_none()
7686 && input_ids.len() > 2
7687 {
7688 let chunk = self.prefill_chunk();
7694 let hs = self.hidden_size;
7695 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
7696 let end = (pos + chunk).min(input_ids.len());
7697 let hb = self.prefill_batch(&input_ids[pos..end], pos);
7698 if mimo_spec {
7699 self.mimo_note_rows(&hb, pos);
7700 }
7701 if let Some(m) = &mut mtp {
7702 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
7703 .ok()
7704 .and_then(|v| v.parse().ok())
7705 .unwrap_or(0);
7706 for p in pos..end {
7707 if p + 1 < input_ids.len() {
7708 if probe >= 1 && p + 2 < input_ids.len() {
7709 let (d1, mut hx) = self.mtp_step_h(
7713 m,
7714 &hb[(p - pos) * hs..(p - pos + 1) * hs],
7715 input_ids[p + 1],
7716 p,
7717 );
7718 let mut ok = d1 == input_ids[p + 2];
7719 Self::chain_probe_note(0, ok);
7720 let mut d_prev = d1;
7721 let mut extra = 0usize;
7722 for j in 1..probe {
7723 if p + 2 + j >= input_ids.len() {
7724 break;
7725 }
7726 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
7727 extra += 1;
7728 ok = ok && dj == input_ids[p + 2 + j];
7729 Self::chain_probe_note(j, ok);
7730 d_prev = dj;
7731 hx = hj;
7732 }
7733 m.kv.truncate_last(extra);
7734 } else {
7735 let _ = self.mtp_step(
7736 m,
7737 &hb[(p - pos) * hs..(p - pos + 1) * hs],
7738 input_ids[p + 1],
7739 p,
7740 );
7741 }
7742 }
7743 }
7744 }
7745 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7746 pos = end;
7747 }
7748 }
7749 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
7750 if task_mask.is_none()
7751 && !dyn_prefill
7752 && !graph_prefill
7753 && !pair_off
7754 && self.pair_supported()
7755 && o1_prefill.is_none()
7756 {
7757 while pos + 1 < input_ids.len()
7758 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
7759 {
7760 let e1 = self.embed_single(input_ids[pos]);
7761 let e2 = self.embed_single(input_ids[pos + 1]);
7762 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
7763 if mimo_spec {
7764 self.mimo_note_rows(&h1, pos);
7765 self.mimo_note_rows(&h2, pos + 1);
7766 }
7767 self.commit_linear_scratch();
7769 if let Some(m) = &mut mtp {
7770 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
7771 if pos + 2 < input_ids.len() {
7772 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
7773 .ok()
7774 .and_then(|v| v.parse().ok())
7775 .unwrap_or(0);
7776 if probe >= 1 && pos + 3 < input_ids.len() {
7777 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
7781 let mut ok = d1 == input_ids[pos + 3];
7782 Self::chain_probe_note(0, ok);
7783 let mut d_prev = d1;
7784 let mut extra = 0usize;
7785 for j in 1..probe {
7786 if pos + 3 + j >= input_ids.len() {
7787 break;
7788 }
7789 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
7790 extra += 1;
7791 ok = ok && dj == input_ids[pos + 3 + j];
7792 Self::chain_probe_note(j, ok);
7793 d_prev = dj;
7794 hx = hj;
7795 }
7796 m.kv.truncate_last(extra);
7797 } else {
7798 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
7799 }
7800 }
7801 }
7802 hidden = h2;
7803 pos += 2;
7804 }
7805 }
7806 let o1_batch_ready = o1_sealed
7819 && o1_prefill.is_some()
7820 && (mtp.is_none() || self.o1_spec())
7821 && self.o1_on_graph()
7822 && (0..self.num_layers).all(|li| {
7823 let cache = &self.kv_cache.layers[self.phys_layer(li)];
7824 cache.o1.is_none() || cache.o1_views().is_some()
7825 });
7826 let mtp_batch_prefill = mtp.is_some()
7831 && graph_prefill
7832 && task_mask.is_none()
7833 && !dyn_prefill
7834 && !self.o1_active()
7835 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
7836 #[cfg(not(target_os = "macos"))]
7838 crate::gpu::set_batch_kv_hint(0);
7839 if batch_k > 0
7840 && (graph_prefill || o1_batch_ready)
7841 && task_mask.is_none()
7842 && (!self.o1_active() || o1_batch_ready)
7843 && (mtp.is_none() || mtp_batch_prefill || o1_batch_ready)
7844 && !dyn_prefill
7845 && pos + 1 < input_ids.len()
7846 {
7847 let hs = self.hidden_size;
7848 let chunk = batch_k;
7849 let pos_start = pos;
7850 let tail_from = input_ids.len().saturating_sub(crate::dory::EXACT_TAIL);
7851 #[cfg(not(target_os = "macos"))]
7855 if !self.o1_active() && o1_arch.is_none() && !o1_batch_ready {
7856 crate::gpu::set_batch_kv_hint(input_ids.len() + 1);
7857 }
7858 if let Some(a) = o1_arch.as_ref() {
7859 crate::gpu::o1_acap_begin(a.stride);
7860 }
7861 if o1_batch_ready && mtp.is_some() {
7862 self.mtp_pos_base = tail_from.max(pos);
7863 }
7864 let snap_at = if o1_arch.is_none() && !o1_batch_ready { self.gdn_snap_target(input_ids, pos) } else { None };
7867 while pos < input_ids.len() {
7868 if pos >= tail_from {
7870 if let Some(a) = o1_arch.take() {
7871 crate::gpu::o1_acap_end();
7872 if let Err(err) = self.o1_seal_archive(a) {
7873 self.finish_generation(&mut mtp, &mut router, true);
7874 return Err(err);
7875 }
7876 }
7877 }
7878 let stop = if o1_arch.is_some() { tail_from } else { input_ids.len() };
7879 let end = match snap_at {
7880 Some(s) if pos < s => (pos + chunk).min(stop).min(s),
7881 _ => (pos + chunk).min(stop),
7882 };
7883 let bk = end - pos;
7884 let mut hiddens = vec![0f32; bk * hs];
7885 for (j, &id) in input_ids[pos..end].iter().enumerate() {
7886 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
7887 }
7888 let positions: Vec<usize> = (pos..end).collect();
7889 let t_chunk = std::time::Instant::now();
7890 #[cfg(not(target_os = "macos"))]
7894 crate::gpu::set_batch_defer(
7895 end < input_ids.len()
7896 && o1_arch.is_none()
7897 && !o1_batch_ready
7898 && !mimo_spec
7899 && !mtp_batch_prefill,
7900 );
7901 let outcome = self.o1_prompt_chunk(&mut hiddens, &positions, bk);
7902 #[cfg(not(target_os = "macos"))]
7903 crate::gpu::set_batch_defer(false);
7904 if let (Some(a), crate::gpu::BatchGraphOutcome::Completed) = (o1_arch.as_mut(), outcome) {
7905 if let Err(err) = self.o1_archive_chunk(a, pos, bk) {
7906 self.finish_generation(&mut mtp, &mut router, true);
7907 return Err(err);
7908 }
7909 }
7910 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
7911 if ok_b && o1_batch_ready && end > self.mtp_pos_base && mtp.is_some() {
7913 let first = pos.max(self.mtp_pos_base);
7914 let n_pairs = mtp_prefill_pair_count(first, end, input_ids.len());
7915 if n_pairs > 0 {
7916 let rows: Vec<Vec<f32>> = (0..n_pairs).map(|j| hiddens[(first - pos + j) * hs..(first - pos + j + 1) * hs].to_vec()).collect();
7917 let pairs: Vec<(&[f32], u32)> = rows.iter().enumerate().map(|(j, row)| (row.as_slice(), input_ids[first + j + 1])).collect();
7918 #[cfg(target_os = "macos")]
7919 let warmed = crate::gpu::q1_force() && {
7920 if let Some(m) = mtp.as_mut() {
7921 if !self.mtp_warm_batch_metal(m, &pairs, first) {
7922 for (j, (h, t)) in pairs.iter().enumerate() {
7923 let h = h.to_vec();
7924 let _ = self.mtp_step(m, &h, *t, first + j);
7925 }
7926 }
7927 }
7928 true
7929 };
7930 #[cfg(not(target_os = "macos"))]
7931 let warmed = false;
7932 let warm_error = if warmed {
7933 None
7934 } else if let Some(m) = mtp.as_mut() {
7935 self.mtp_warm_prefill_pairs(m, &pairs, first).err()
7936 } else {
7937 None
7938 };
7939 if let Some(err) = warm_error {
7940 self.finish_generation(&mut mtp, &mut router, true);
7941 return Err(err.to_string());
7942 }
7943 }
7944 }
7945 if std::env::var("CMF_GRAPH_PROF").is_ok() {
7946 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
7947 eprintln!(
7948 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
7949 if o1_batch_ready {
7950 "o1"
7951 } else if mtp_batch_prefill {
7952 "ordinary_mtp"
7953 } else {
7954 "ordinary"
7955 },
7956 bk as f64 / (ms / 1000.0)
7957 );
7958 }
7959 {
7960 use std::sync::atomic::{AtomicBool, Ordering};
7961 static SAID: AtomicBool = AtomicBool::new(false);
7962 if !SAID.swap(true, Ordering::Relaxed) {
7963 if ok_b {
7964 tracing::info!(
7965 "batched prefill: ACTIVE mode={} (k={bk})",
7966 if o1_batch_ready {
7967 "o1"
7968 } else if mtp_batch_prefill {
7969 "ordinary_mtp"
7970 } else {
7971 "ordinary"
7972 }
7973 );
7974 } else {
7975 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
7976 }
7977 }
7978 }
7979 if ok_b {
7980 if mimo_spec {
7981 self.mimo_note_rows(&hiddens, pos);
7982 }
7983 if mtp_batch_prefill {
7984 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
7985 if n_pairs > 0 {
7986 let rows: Vec<Vec<f32>> = (0..n_pairs)
7992 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
7993 .collect();
7994 let pairs: Vec<(&[f32], u32)> = rows
7995 .iter()
7996 .enumerate()
7997 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
7998 .collect();
7999 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8000 eprintln!(
8001 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
8002 pos,
8003 n_pairs,
8004 pos + n_pairs - 1,
8005 );
8006 }
8007 let warm_error = if let Some(m) = mtp.as_mut() {
8008 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
8009 } else {
8010 None
8011 };
8012 if let Some(err) = warm_error {
8013 self.finish_generation(&mut mtp, &mut router, true);
8018 return Err(err.to_string());
8019 }
8020 }
8021 }
8022 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
8023 pos = end;
8024 if snap_at == Some(end) {
8025 self.gdn_snap = crate::gpu::gdn_snapshot(self.graph_kv_id, end).then(|| input_ids[..end].to_vec());
8026 }
8027 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
8028 self.finish_generation(&mut mtp, &mut router, true);
8033 return Err(if o1_batch_ready {
8034 "sealed O(1) batch graph failed after admission".to_string()
8035 } else {
8036 "ordinary recurrent batch graph failed after admission".to_string()
8037 });
8038 } else {
8039 break; }
8041 }
8042 if mtp_batch_prefill && mtp.is_some() && pos == input_ids.len() {
8043 self.mtp_warm_base = Some(self.mtp_pos_base);
8044 }
8045 #[cfg(feature = "cuda")]
8048 if crate::gpu::cuda_active() {
8049 if let Some(d) = &self.dflash {
8050 let _ = d.dev_ready();
8051 }
8052 }
8053 #[cfg(target_os = "macos")]
8054 if std::env::var("CMF_PREFILL_PROF").is_ok() {
8055 crate::gpu_metal::cut_report();
8056 }
8057 if pos == pos_start
8061 && graph_prefill
8062 && !o1_batch_ready
8063 && mtp.is_none()
8064 && !mimo_spec
8065 && self.can_prefill_batched()
8066 && !self.per_token_stack()
8067 && self.weights.layers.iter().all(|lw| {
8068 !matches!(&lw.attn, AttnKind::LinearGdn(_)) && !matches!(&lw.ffn, FfnKind::Moe(_))
8069 })
8070 {
8071 self.dense_batch_declined.store(true, std::sync::atomic::Ordering::Relaxed);
8072 let chunk = self.prefill_chunk();
8073 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
8074 let end = (pos + chunk).min(input_ids.len());
8075 let hb = self.prefill_batch(&input_ids[pos..end], pos);
8076 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8077 pos = end;
8078 }
8079 }
8080 }
8081 if graph_prefill
8085 && task_mask.is_none()
8086 && mtp.is_none()
8087 && !dyn_prefill
8088 && pos == 0
8089 && input_ids.len() > 1
8090 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
8091 {
8092 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
8093 self.graph_logits = Some(lg);
8094 hidden = vec![0.0; self.hidden_size];
8095 pos = input_ids.len();
8096 }
8097 }
8098 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
8099 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
8100 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
8101 if mimo_spec {
8102 self.mimo_note_rows(&hidden, pos);
8103 }
8104 if let Some(m) = &mut mtp {
8105 if pos + 1 < input_ids.len() {
8106 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
8112 .ok()
8113 .and_then(|v| v.parse().ok())
8114 .unwrap_or(0);
8115 if probe >= 1 && pos + 2 < input_ids.len() {
8116 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
8117 let mut ok = d1 == input_ids[pos + 2];
8118 Self::chain_probe_note(0, ok);
8119 let mut d_prev = d1;
8120 let mut extra = 0usize;
8121 for j in 1..probe {
8122 if pos + 2 + j >= input_ids.len() {
8123 break;
8124 }
8125 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
8126 extra += 1;
8127 ok = ok && dj == input_ids[pos + 2 + j];
8128 Self::chain_probe_note(j, ok);
8129 d_prev = dj;
8130 hx = hj;
8131 }
8132 m.kv.truncate_last(extra);
8135 } else {
8136 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
8137 }
8138 }
8139 }
8140 pos += 1;
8141 }
8142 #[cfg(not(target_os = "macos"))]
8143 crate::gpu::set_batch_kv_hint(0);
8144 if std::env::var("CMF_PREFILL_PROF").is_ok() {
8145 eprintln!(
8146 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
8147 input_ids.len(),
8148 _tpf.elapsed().as_secs_f64() * 1000.0
8149 );
8150 }
8151 #[cfg(target_os = "macos")]
8154 crate::gpu_metal::ane::release_idle();
8155 if self
8156 .graph_failed
8157 .swap(false, std::sync::atomic::Ordering::Relaxed)
8158 {
8159 self.finish_generation(&mut mtp, &mut router, true);
8164 return Err("GPU token graph failed during prefill".to_string());
8165 }
8166 if self
8169 .cancel
8170 .swap(false, std::sync::atomic::Ordering::Relaxed)
8171 {
8172 self.finish_generation(&mut mtp, &mut router, true);
8176 return Ok(GenerateResult {
8177 text: String::new(),
8178 token_ids: Vec::new(),
8179 prompt_tokens: input_ids.len(),
8180 tokens_generated: 0,
8181 finish_reason: "cancelled".to_string(),
8182 mtp_drafted: 0,
8183 mtp_accepted: 0,
8184 token_confidence: Vec::new(),
8185 traces: Vec::new(),
8186 });
8187 }
8188
8189 if !o1_sealed {
8192 match self.o1_seal_checked() {
8193 Ok(_) => {}
8194 Err(err) => {
8195 self.finish_generation(&mut mtp, &mut router, true);
8196 return Err(err);
8197 }
8198 }
8199 }
8200
8201 let mut utf8_pending: Vec<u8> = Vec::new();
8203 macro_rules! commit {
8205 ($id:expr) => {{
8206 all_ids.push($id);
8207 generated += 1;
8208 self.note_draft_id($id);
8209 if self.tokenizer.is_eos($id) && !self.ignore_eos {
8210 finish_reason = "stop".to_string();
8211 false
8212 } else {
8213 let token_text = self.tokenizer.decode_token_stream($id, &mut utf8_pending);
8214 let mut go = true;
8215 if let Some(ref mut cb) = on_token {
8216 if !cb(&token_text) {
8217 finish_reason = "cancelled".to_string();
8218 go = false;
8219 }
8220 }
8221 if go
8222 && self.sampler_config.loop_stop
8223 && crate::sampler::tail_repeats(&all_ids[all_ids.len() - generated..])
8224 {
8225 finish_reason = "loop".to_string();
8226 go = false;
8227 }
8228 go
8229 }
8230 }};
8231 }
8232
8233 let mut spec_trial = SpecTrial::Spec {
8244 t0: std::time::Instant::now(),
8245 gen0: generated,
8246 rounds: 0,
8247 };
8248 let mut spec_mon = SpecMon {
8254 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
8255 cuda: graph_spec && crate::gpu::cuda_active() && self.sampler_config.temperature < 1e-6,
8256 ..SpecMon::default()
8257 };
8258 if !spec_mon.metal && self.spec_plain_ms.0 > 0.0 && input_ids.len() <= self.spec_plain_ms.1 * 2 + 1024 {
8262 spec_mon.plain_ms = self.spec_plain_ms.0;
8263 }
8264 let mut spec_watchdog_off = false;
8265 let mut spec_walls: Vec<f32> = Vec::new();
8268 let mut spec_round_end: Option<std::time::Instant> = None;
8271 if mimo_spec {
8272 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
8273 if let Some(mut st) = self.mimo_mtp.take() {
8274 self.mimo_mtp_probe(&mut st, input_ids, &path);
8275 self.mimo_mtp = Some(st);
8276 }
8277 }
8278 }
8279 let mut next_pos = input_ids.len();
8281 'decode: while generated < max_tokens {
8282 if self
8283 .graph_failed
8284 .swap(false, std::sync::atomic::Ordering::Relaxed)
8285 {
8286 self.finish_generation(&mut mtp, &mut router, true);
8291 return Err("GPU token graph failed during decode".to_string());
8292 }
8293 if self
8294 .cancel
8295 .swap(false, std::sync::atomic::Ordering::Relaxed)
8296 {
8297 finish_reason = "cancelled".to_string();
8298 break 'decode;
8299 }
8300 if mimo_spec && next_pos > 0 {
8305 self.mimo_note_rows(&hidden, next_pos - 1);
8308 }
8309 let forced = self.spec_forced.take();
8310 let mut logits = match (forced, self.graph_logits.take()) {
8311 (Some(_), _) => Vec::new(),
8312 (None, Some(lg)) => lg,
8313 (None, None) => {
8314 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
8315 #[cfg(feature = "gpu")]
8320 let logits_out = if let Some(lg) = self.m3_device_logits(&hidden) {
8321 lg
8322 } else {
8323 inference::rms_norm_into(
8324 &hidden,
8325 &self.weights.final_norm,
8326 self.rms_eps,
8327 self.norm_style,
8328 &mut self.ws.n1,
8329 );
8330 self.lm_head_forward(&self.ws.n1)
8331 };
8332 #[cfg(not(feature = "gpu"))]
8333 let logits_out = {
8334 inference::rms_norm_into(
8335 &hidden,
8336 &self.weights.final_norm,
8337 self.rms_eps,
8338 self.norm_style,
8339 &mut self.ws.n1,
8340 );
8341 self.lm_head_forward(&self.ws.n1)
8342 };
8343 logits_out
8344 }
8345 };
8346 if generated
8349 == std::env::var("CMF_LOGIT_DUMP_STEP")
8350 .ok()
8351 .and_then(|v| v.parse().ok())
8352 .unwrap_or(0)
8353 {
8354 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
8355 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
8356 for v in hidden.iter().chain(logits.iter()) {
8357 bytes.extend_from_slice(&v.to_le_bytes());
8358 }
8359 if let Err(e) = std::fs::write(&path, &bytes) {
8360 eprintln!("logit dump: failed to write {path}: {e}");
8361 self.finish_generation(&mut mtp, &mut router, true);
8362 return Err(format!("logit dump write failed: {e}"));
8363 }
8364 }
8365 }
8366 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
8370 if !logits.is_empty() {
8371 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
8372 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
8373 if let Err(e) =
8374 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
8375 {
8376 eprintln!("logit dump: failed to write {}: {e}", path.display());
8377 }
8378 }
8379 }
8380 self.clover_causal_logits(&mut logits, generated, &all_ids[penalty_from..]);
8381 if let Some(g) = self.dash.as_mut().and_then(|d| d.guard.as_mut()) {
8382 g.apply(&mut logits, &all_ids[input_ids.len()..]);
8383 }
8384 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
8385 Some(c) => c,
8386 None => {
8387 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
8388 sampler::sample_with_scratch_pool(
8389 &logits,
8390 &self.sampler_config,
8391 self.sampler_config.penalty_past(&all_ids[penalty_from..], bounded_native),
8392 &mut self.rng,
8393 &mut self.sampler_scratch,
8394 self.pool.as_deref(),
8395 )
8396 }
8397 };
8398 if self.confidence_on {
8399 confidence.push(if logits.is_empty() {
8400 0.0
8401 } else {
8402 sampler::top1_prob_pool(
8403 self.pool.as_deref(),
8404 &mut self.sampler_scratch,
8405 &logits,
8406 t_next,
8407 calib_temp,
8408 )
8409 });
8410 }
8411 if !logits.is_empty() {
8412 attention::recycle_buf(&mut logits);
8413 }
8414 if trace_on {
8415 let skill = router.as_ref().and_then(|r| r.active_id());
8419 traces.push(TokenTrace {
8420 t: generated,
8421 token_id: t_next,
8422 confidence: confidence.last().copied().unwrap_or(0.0),
8423 active_skill: skill,
8424 recon: None,
8425 switched: false,
8426 });
8427 }
8428 if !commit!(t_next) {
8429 break 'decode;
8430 }
8431 if generated >= max_tokens {
8432 break 'decode;
8433 }
8434
8435 self.swa_trim_tails();
8438 if self.dsv41.is_none() && self.dory.is_none() && self.kv_cache.needs_eviction() {
8439 static SAID: std::sync::Once = std::sync::Once::new();
8445 SAID.call_once(|| {
8446 tracing::warn!(
8447 "KV cache full at {} positions — evicting half; quality \
8448 will degrade. Raise CMF_MAX_SEQ.",
8449 self.kv_cache.max_seq_len,
8450 );
8451 });
8452 let keep = (self.kv_cache.max_seq_len / 2).max(1);
8453 self.kv_cache.evict(keep);
8454 }
8455
8456 if graph_spec {
8459 match spec_trial {
8460 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
8461 spec_mon.plain_ms =
8462 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
8463 if !spec_mon.metal {
8464 self.spec_plain_ms = (spec_mon.plain_ms, next_pos);
8465 }
8466 let keep = spec_mon.pays();
8467 tracing::info!(
8468 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
8469 spec_mon.tokens,
8470 spec_mon.round_ms,
8471 spec_mon.plain_ms,
8472 if keep { "speculating" } else { "plain" }
8473 );
8474 spec_mon.fails = 0;
8475 spec_trial = SpecTrial::Decided {
8476 spec: keep,
8477 recheck_at: if keep { usize::MAX } else { spec_mon.recheck_plain_at(generated) },
8478 };
8479 }
8480 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
8481 spec_mon.n = 0;
8482 spec_mon.zero_accept_rounds = 0;
8483 spec_trial = SpecTrial::Spec {
8484 t0: std::time::Instant::now(),
8485 gen0: generated,
8486 rounds: 0,
8487 };
8488 }
8489 _ => {}
8490 }
8491 spec_watchdog_off = matches!(
8492 spec_trial,
8493 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
8494 );
8495 }
8496 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
8498 let budget = max_tokens - generated - 1;
8499 if let Some(mut st) = self.mimo_mtp.take() {
8500 let k = st.depth.min(budget);
8501 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
8502 self.mimo_mtp = Some(st);
8503 let r = match r {
8504 Ok(r) => r,
8505 Err(err) => {
8506 self.finish_generation(&mut mtp, &mut router, true);
8507 return Err(err);
8508 }
8509 };
8510 if let Some(r) = r {
8511 drafted += r.drafted;
8512 accepted += r.accepted.len();
8513 let mut stopped = false;
8514 for &id in &r.accepted {
8515 if self.confidence_on {
8516 confidence.push(0.0);
8517 }
8518 if !commit!(id) {
8519 stopped = true;
8520 break;
8521 }
8522 }
8523 if stopped {
8524 break 'decode;
8525 }
8526 next_pos += r.accepted.len() + 1;
8527 hidden = r.hidden;
8528 self.graph_logits = Some(r.logits);
8531 continue 'decode;
8532 }
8533 }
8534 }
8535 #[cfg(feature = "gpu")]
8537 if solo_spec && next_pos > 0 {
8538 let now = std::time::Instant::now();
8539 if let Some((t, tokens)) = solo.last.take() {
8541 let ms = (now - t).as_secs_f32() * 1e3;
8542 if tokens == 0 {
8543 if solo.plain_n > 0 {
8544 solo.plain_ms = if solo.plain_ms == 0.0 { ms } else { 0.7 * solo.plain_ms + 0.3 * ms };
8545 }
8546 solo.plain_n += 1;
8547 } else if solo.first {
8548 solo.first = false;
8549 if spec_time_level() > 0 {
8550 eprintln!("solo-round wall {ms:.1} ms → {tokens} tokens (the first round: not a sample)");
8551 }
8552 } else {
8553 let per = ms / tokens as f32;
8554 solo.round_ms = if solo.rounds == 0 { per } else { 0.9 * solo.round_ms + 0.1 * per };
8559 solo.rounds += 1;
8560 if spec_time_level() > 0 {
8561 eprintln!(
8562 "solo-round wall {ms:.1} ms → {tokens} tokens ({:.1} ms a token, plain {:.1})",
8563 solo.round_ms, solo.plain_ms
8564 );
8565 }
8566 solo.win_ms += ms;
8573 solo.win_tok += tokens as u32;
8574 if solo_pin.is_none() && solo.plain_ms > 0.0 && solo.rounds >= SOLO_JUDGE {
8575 if solo.win_ms / solo.win_tok.max(1) as f32 > solo.plain_ms {
8576 solo.rest = solo.back;
8577 solo.back = (solo.back * 2).min(SOLO_REST_MOST);
8578 solo.rounds = 0;
8579 solo.win_ms = 0.0;
8580 solo.win_tok = 0;
8581 } else if solo.rounds >= SOLO_TRIAL {
8582 solo.rounds = 0;
8584 solo.win_ms = 0.0;
8585 solo.win_tok = 0;
8586 }
8587 }
8588 }
8589 }
8590 if solo.rest == 0 && generated + 1 < max_tokens {
8591 let k = solo.k.min(max_tokens - generated);
8592 match self.solo_spec_round(t_next, next_pos, &all_ids, k) {
8593 Some(r) => {
8594 drafted += r.drafted;
8595 accepted += r.accepted.len();
8596 if solo_pin.is_none() {
8597 solo.k = (r.accepted.len() + 2).clamp(2, SOLO_K_AUTO);
8599 }
8600 solo.last = Some((now, r.accepted.len() + 1));
8601 let mut stopped = false;
8602 for &id in &r.accepted {
8603 if self.confidence_on {
8604 confidence.push(0.0);
8605 }
8606 if trace_on {
8607 traces.push(TokenTrace {
8608 t: generated,
8609 token_id: id,
8610 confidence: confidence.last().copied().unwrap_or(0.0),
8611 active_skill: None,
8612 recon: None,
8613 switched: false,
8614 });
8615 }
8616 if !commit!(id) {
8617 stopped = true;
8618 break;
8619 }
8620 }
8621 if stopped {
8622 break 'decode;
8623 }
8624 next_pos += r.accepted.len() + 1;
8625 hidden = r.hidden;
8626 self.graph_logits = Some(r.logits);
8627 continue 'decode;
8628 }
8629 None => {
8630 if self.graph_failed.swap(false, std::sync::atomic::Ordering::Relaxed) {
8631 self.finish_generation(&mut mtp, &mut router, true);
8632 return Err("GPU batch graph failed during a speculative round".to_string());
8633 }
8634 solo.rest = 8;
8636 }
8637 }
8638 }
8639 solo.rest = solo.rest.saturating_sub(1);
8640 solo.last = Some((now, 0));
8641 }
8642 #[cfg(feature = "gpu")]
8645 if self.speculative
8646 && self.qwen4_exp.is_some()
8647 && task_mask.is_none()
8648 && self.sampler_config.temperature < 1e-6
8649 && generated + 1 < max_tokens
8650 && next_pos > 0
8651 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
8652 {
8653 let r = match &mut self.qwen4_exp {
8654 Some(b) => {
8655 b.3.set_draft_budget(max_tokens - generated - 1);
8657 crate::qwen4_exp::spec_round(
8658 &b.0,
8659 &b.1,
8660 &b.2,
8661 &mut b.3,
8662 next_pos,
8663 &all_ids,
8664 &self.inv_freq,
8665 self.pool.as_deref(),
8666 )
8667 }
8668 None => None,
8669 };
8670 if let Some(r) = r {
8671 drafted += r.drafted;
8672 accepted += r.accepted.len();
8673 let mut stopped = false;
8674 for &id in &r.accepted {
8675 if self.confidence_on {
8676 confidence.push(0.0);
8677 }
8678 if !commit!(id) {
8679 stopped = true;
8680 break;
8681 }
8682 }
8683 if stopped {
8684 break 'decode;
8685 }
8686 next_pos += r.accepted.len() + 1;
8687 hidden.fill(0.0);
8688 self.graph_logits = Some(r.logits);
8689 continue 'decode;
8690 }
8691 }
8692 match &mut mtp {
8693 #[cfg(feature = "gpu")]
8695 Some(m)
8696 if graph_spec
8697 && !spec_watchdog_off
8698 && generated + 1 < max_tokens
8699 && next_pos > 0 =>
8700 {
8701 let t_round = std::time::Instant::now();
8702 if spec_time_level() >= 2 {
8703 if let Some(t) = spec_round_end.take() {
8704 eprintln!(
8705 "spec-gap {:.2} ms (host between rounds)",
8706 t.elapsed().as_secs_f64() * 1e3
8707 );
8708 }
8709 }
8710 spec_stamps_begin();
8711 #[cfg(target_os = "macos")]
8716 let allocs0 =
8717 crate::gpu_metal::IO_BUF_ALLOCS.load(std::sync::atomic::Ordering::Relaxed);
8718 #[cfg(not(target_os = "macos"))]
8719 let allocs0 = 0u64;
8720 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
8721 m,
8722 &hidden,
8723 t_next,
8724 next_pos,
8725 &mut drafted,
8726 &mut accepted,
8727 &mut all_ids,
8728 max_tokens - generated,
8729 ) {
8730 next_pos = n_pos;
8731 hidden = new_h;
8732 let level = spec_time_level();
8733 if level > 0 {
8734 let wall = t_round.elapsed().as_secs_f32() * 1e3;
8735 let stamps = spec_stamps_take();
8736 let median = if spec_walls.len() >= 3 {
8739 let mut s = spec_walls.clone();
8740 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
8741 Some(s[s.len() / 2])
8742 } else {
8743 None
8744 };
8745 let outlier = median.is_some_and(|m| wall > 1.4 * m);
8746 #[cfg(target_os = "macos")]
8747 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
8748 .load(std::sync::atomic::Ordering::Relaxed)
8749 - allocs0;
8750 #[cfg(not(target_os = "macos"))]
8751 let allocs = allocs0;
8752 eprintln!(
8753 "spec-round wall {wall:.1} ms → {} tokens{}{}",
8754 extra.len() + 1,
8755 if allocs > 0 {
8756 format!(" [{allocs} new device buffers]")
8757 } else {
8758 String::new()
8759 },
8760 match (outlier, median) {
8761 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
8762 _ => String::new(),
8763 }
8764 );
8765 if level >= 2 || outlier {
8766 let sum: f32 = stamps.iter().map(|s| s.1).sum();
8767 eprintln!(
8768 "spec-stamps: {}| untracked {:.1}",
8769 spec_stamps_format(&stamps),
8770 wall - sum
8771 );
8772 }
8773 if spec_mon.n >= 1 {
8774 spec_walls.push(wall);
8775 }
8776 }
8777 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
8781 spec_trial = Self::spec_trial_round(
8784 spec_trial,
8785 &mut spec_mon,
8786 generated + extra.len() + 1,
8787 );
8788 let mut stopped = false;
8789 for &id in &extra {
8790 if self.confidence_on {
8791 confidence.push(0.0);
8792 }
8793 if trace_on {
8806 traces.push(TokenTrace {
8807 t: generated,
8808 token_id: id,
8809 confidence: confidence.last().copied().unwrap_or(0.0),
8810 active_skill: None,
8811 recon: None,
8812 switched: false,
8813 });
8814 }
8815 if !commit!(id) {
8816 stopped = true;
8817 break;
8818 }
8819 }
8820 if stopped {
8821 break 'decode;
8822 }
8823 if spec_time_level() >= 2 {
8824 spec_round_end = Some(std::time::Instant::now());
8825 }
8826 continue 'decode;
8827 }
8828 if self
8829 .graph_failed
8830 .swap(false, std::sync::atomic::Ordering::Relaxed)
8831 {
8832 self.finish_generation(&mut mtp, &mut router, true);
8838 return Err("GPU MTP graph failed during speculative decode".to_string());
8839 }
8840 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
8851 spec_mon.tokens = 0.0;
8852 spec_mon.fails = 3;
8853 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
8854 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
8855 next_pos += 1;
8856 continue 'decode;
8857 }
8858 Some(m) if !graph_spec && generated + 1 < max_tokens => {
8860 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
8861 drafted += 1;
8862 let emb1 = self.embed_single(t_next);
8863 let emb2 = self.embed_single(draft);
8864 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
8865
8866 inference::rms_norm_into(
8867 &h1,
8868 &self.weights.final_norm,
8869 self.rms_eps,
8870 self.norm_style,
8871 &mut self.ws.n1,
8872 );
8873 let mut logits1 = self.lm_head_forward(&self.ws.n1);
8874 let t_after = sampler::sample_with_scratch_pool(
8875 &logits1,
8876 &self.sampler_config,
8877 self.sampler_config.penalty_past(&all_ids, bounded_native),
8878 &mut self.rng,
8879 &mut self.sampler_scratch,
8880 self.pool.as_deref(),
8881 );
8882 if self.confidence_on {
8883 confidence.push(sampler::top1_prob_pool(
8884 self.pool.as_deref(),
8885 &mut self.sampler_scratch,
8886 &logits1,
8887 t_after,
8888 calib_temp,
8889 ));
8890 }
8891 attention::recycle_buf(&mut logits1);
8892 if trace_on {
8893 traces.push(TokenTrace {
8896 t: generated,
8897 token_id: t_after,
8898 confidence: confidence.last().copied().unwrap_or(0.0),
8899 active_skill: None,
8900 recon: None,
8901 switched: false,
8902 });
8903 }
8904 let stop = !commit!(t_after);
8905
8906 if t_after == draft {
8907 accepted += 1;
8908 self.commit_linear_scratch();
8909 let _ = self.mtp_step(m, &h1, t_after, next_pos);
8910 hidden = h2;
8911 next_pos += 2;
8912 } else {
8913 for layer in &mut self.kv_cache.layers {
8915 layer.truncate_last(1);
8916 }
8917 if !stop {
8918 let _ = self.mtp_step(m, &h1, t_after, next_pos);
8919 hidden = self.forward_layers(
8920 &self.embed_single(t_after),
8921 next_pos + 1,
8922 None,
8923 );
8924 }
8925 next_pos += 2;
8926 }
8927 if stop {
8928 break 'decode;
8929 }
8930 }
8931 _ => {
8933 #[cfg(feature = "gpu")]
8938 if Self::dsv4_spec_on() && self.dsv4.is_some() {
8939 static SAID: std::sync::Once = std::sync::Once::new();
8940 SAID.call_once(|| {
8941 eprintln!(
8942 "dsv4-spec gate: mtp={} mask={} router={} trace={} temp={} rep={} ",
8943 !self.dsv4_mtp.is_empty(),
8944 task_mask.is_none(),
8945 router.is_none(),
8946 !trace_on,
8947 self.sampler_config.temperature < 1e-6,
8948 self.sampler_config.repetition_penalty == 1.0,
8949 );
8950 });
8951 }
8952 #[cfg(feature = "gpu")]
8953 if Self::dsv4_spec_on()
8954 && self.dsv4.is_some()
8955 && !self.dsv4_mtp.is_empty()
8956 && task_mask.is_none()
8957 && router.is_none()
8958 && !trace_on
8959 && self.sampler_config.temperature < 1e-6
8960 && self.sampler_config.repetition_penalty == 1.0
8961 && generated + 1 < max_tokens
8962 && all_ids.len() >= 2
8963 && generated >= dsv4_spec_retry_at
8964 {
8965 let tip_token = all_ids[all_ids.len() - 2];
8966 let drafted0 = drafted;
8967 let round = self.dsv4_spec_step(
8968 tip_token,
8969 t_next,
8970 next_pos,
8971 max_tokens.saturating_sub(generated),
8972 &mut drafted,
8973 &mut accepted,
8974 );
8975 if drafted > drafted0 {
8976 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
8977 if useful {
8978 dsv4_spec_bad = 0;
8979 } else {
8980 dsv4_spec_bad += 1;
8981 if dsv4_spec_bad >= 2 {
8982 dsv4_spec_bad = 0;
8983 dsv4_spec_retry_at = generated.saturating_add(32);
8984 tracing::info!(
8985 "dsv4: the draft did not pay off twice — exact walk for 32 tokens"
8986 );
8987 }
8988 }
8989 }
8990 if let Some((extra, n_pos)) = round {
8991 next_pos = n_pos;
8992 let mut stopped = false;
8993 for &id in &extra {
8994 if self.confidence_on {
8995 confidence.push(0.0);
8996 }
8997 if !commit!(id) {
8998 stopped = true;
8999 break;
9000 }
9001 }
9002 if stopped {
9003 break 'decode;
9004 }
9005 continue 'decode;
9006 }
9007 }
9008 self.graph_want_logits = fuse_lm;
9009 let mut t_fwd = t_next;
9015 let pure_greedy = self.sampler_config.temperature < 1e-6
9016 && self.sampler_config.repetition_penalty == 1.0
9017 && self.sampler_config.presence_penalty == 0.0
9018 && self.sampler_config.suppress_tokens.is_empty();
9019 let burst_k = std::env::var("CMF_MULTISTEP")
9024 .ok()
9025 .and_then(|v| v.parse::<usize>().ok())
9026 .unwrap_or(0);
9027 if pure_greedy
9028 && burst_k >= 1
9029 && fuse_lm
9030 && task_mask.is_none()
9031 && router.is_none()
9032 && !trace_on
9033 && !self.confidence_on
9034 && std::env::var_os("CMF_FORCE_IDS").is_none()
9039 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
9040 && std::env::var_os("CMF_LOGIT_DUMP_ALL").is_none()
9041 {
9042 let mut stopped = false;
9043 loop {
9044 if self
9049 .cancel
9050 .swap(false, std::sync::atomic::Ordering::Relaxed)
9051 {
9052 finish_reason = "cancelled".to_string();
9053 stopped = true;
9054 break;
9055 }
9056 let room = max_tokens.saturating_sub(generated);
9057 if room <= 2 {
9058 break;
9059 }
9060 let k = burst_k.min(room - 1);
9061 if k < 1 {
9062 break;
9063 }
9064 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
9065 if self
9066 .graph_failed
9067 .swap(false, std::sync::atomic::Ordering::Relaxed)
9068 {
9069 self.finish_generation(&mut mtp, &mut router, true);
9070 return Err(
9071 "GPU token graph failed during greedy burst".to_string()
9072 );
9073 }
9074 break;
9075 };
9076 next_pos += k;
9077 for &id in &ids {
9078 if !commit!(id) {
9079 stopped = true;
9080 break;
9081 }
9082 }
9083 if stopped {
9084 break;
9085 }
9086 t_fwd = *ids.last().unwrap();
9087 }
9088 if stopped {
9089 break 'decode;
9090 }
9091 }
9092 #[cfg(target_os = "macos")]
9102 if graph_spec
9103 && spec_watchdog_off
9104 && next_pos > 0
9105 && self.mtp_graph_mode == Some(true)
9106 && crate::gpu::q1_force()
9107 {
9108 if let Some(m) = mtp.as_mut() {
9109 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
9110 }
9111 }
9112 if let Some(d) = self.dash.as_mut() {
9113 d.at_decode(t_fwd, next_pos);
9114 }
9115 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
9116 next_pos += 1;
9117 if let Some(r) = &mut router {
9120 let phi = self.dyn_phi_ema.clone();
9121 let decision = r.step(&phi, generated);
9122 if let Some(new_active) = decision {
9123 let _ = self.set_active_skill(new_active);
9124 }
9125 if trace_on {
9128 if let Some(last) = traces.last_mut() {
9129 let e = r.last_best_e();
9130 last.recon = e.is_finite().then_some(e);
9131 last.switched = decision.is_some();
9132 }
9133 }
9134 }
9135 }
9136 }
9137 }
9138
9139 let cancelled = finish_reason == "cancelled";
9140 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
9144 if mimo_spec {
9145 if let Some(st) = self.mimo_mtp.as_ref() {
9146 let line = st.stats.line();
9147 tracing::info!("{line}");
9148 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
9149 eprintln!("{line}");
9150 }
9151 }
9152 }
9153 self.finish_generation(&mut mtp, &mut router, cancelled);
9154
9155 if self.dflash_solo && spec_time_level() > 0 {
9156 if let Some(line) = crate::dflash::stage_report() {
9157 eprintln!("solo-draft: {line}");
9158 }
9159 }
9160 if let Ok(path) = std::env::var("CMF_GEN_IDS_OUT") {
9163 let text = all_ids.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" ");
9164 if let Err(e) = std::fs::write(&path, text) {
9165 tracing::warn!("CMF_GEN_IDS_OUT: {path}: {e}");
9166 }
9167 }
9168 let output_ids = &all_ids[input_ids.len()..];
9169 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
9173 let consumed = std::mem::take(&mut all_ids);
9178 if dyn_switched {
9179 self.clear_sequence_state();
9180 } else if cancelled || mimo_spec || prompt_rows.is_some() {
9181 self.clear_history();
9182 } else {
9183 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
9184 }
9185 all_ids = consumed;
9186 let output_ids = &all_ids[input_ids.len()..];
9187 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
9189 let output_ids = match finish_reason.as_str() {
9191 "loop" => {
9192 let excess = crate::sampler::tail_repeat_excess(output_ids).unwrap_or(0);
9193 &output_ids[..output_ids.len() - excess]
9194 }
9195 _ => output_ids,
9196 };
9197 Ok(GenerateResult {
9198 text: self.tokenizer.decode(output_ids),
9199 token_ids: output_ids.to_vec(),
9200 prompt_tokens: input_ids.len(),
9201 tokens_generated: generated,
9202 finish_reason,
9203 mtp_drafted: drafted,
9204 mtp_accepted: accepted,
9205 token_confidence: confidence,
9206 traces,
9207 })
9208 }
9209
9210 fn mtp_step(
9214 &mut self,
9215 m: &mut MtpModule,
9216 hidden: &[f32],
9217 next_token: u32,
9218 position: usize,
9219 ) -> u32 {
9220 self.mtp_step_h(m, hidden, next_token, position).0
9221 }
9222
9223 fn chain_probe_note(depth: usize, prefix_ok: bool) {
9227 use std::sync::Mutex;
9228 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
9229 let mut t = T.lock().unwrap();
9230 if t.len() <= depth {
9231 t.resize(depth + 1, (0, 0));
9232 }
9233 t[depth].0 += 1;
9234 t[depth].1 += prefix_ok as u64;
9235 if depth == 0 && t[0].0 % 128 == 0 {
9236 let line: Vec<String> = t
9237 .iter()
9238 .enumerate()
9239 .map(|(d, (n, k))| {
9240 format!(
9241 "d{}={:.0}%({n})",
9242 d + 1,
9243 100.0 * *k as f64 / (*n).max(1) as f64
9244 )
9245 })
9246 .collect();
9247 eprintln!("mtp-chain: {}", line.join(" "));
9248 }
9249 }
9250
9251 fn mtp_step_hl(
9259 &mut self,
9260 m: &mut MtpModule,
9261 hidden: &[f32],
9262 next_token: u32,
9263 position: usize,
9264 ) -> (Vec<f32>, Vec<f32>) {
9265 let position = position.saturating_sub(self.mtp_pos_base);
9266 #[cfg(target_os = "macos")]
9271 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
9272 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
9273 self.mtp_graph_mode = Some(true);
9274 return r;
9275 }
9276 if self.mtp_graph_mode == Some(true) {
9277 tracing::error!("mtp Metal graph failed after admission");
9278 self.clear_sequence_state();
9279 self.graph_failed
9280 .store(true, std::sync::atomic::Ordering::Relaxed);
9281 self.cancel
9282 .store(true, std::sync::atomic::Ordering::Relaxed);
9283 return (Vec::new(), Vec::new());
9284 }
9285 self.mtp_graph_mode = Some(false);
9286 }
9287 #[cfg(feature = "gpu")]
9288 if self.mtp_graph_mode != Some(false) {
9289 if !self.mtp_graph_ok(m) {
9290 if self.mtp_graph_mode == Some(true) {
9291 tracing::error!("mtp graph became unavailable after admission");
9296 self.clear_sequence_state();
9297 self.graph_failed
9298 .store(true, std::sync::atomic::Ordering::Relaxed);
9299 self.cancel
9300 .store(true, std::sync::atomic::Ordering::Relaxed);
9301 return (Vec::new(), Vec::new());
9302 }
9303 self.mtp_graph_mode = Some(false);
9304 } else {
9305 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
9306 self.mtp_graph_mode = Some(true);
9307 return r;
9308 }
9309 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9310 return (Vec::new(), Vec::new());
9317 }
9318 tracing::error!("mtp graph failed or declined after admission");
9322 self.clear_sequence_state();
9323 self.graph_failed
9324 .store(true, std::sync::atomic::Ordering::Relaxed);
9325 self.cancel
9326 .store(true, std::sync::atomic::Ordering::Relaxed);
9327 return (Vec::new(), Vec::new());
9328 }
9329 }
9330 let e = self.embed_single(next_token);
9334 let mut cat = vec![0.0f32; 2 * self.hidden_size];
9335 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
9336 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9337 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9338 let mut x = vec![0.0f32; self.hidden_size];
9339 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
9340
9341 let lw = &m.layer;
9343 inference::rms_norm_into(
9344 &x,
9345 &lw.input_norm,
9346 self.rms_eps,
9347 self.norm_style,
9348 &mut self.ws.n1,
9349 );
9350 let attn = match &lw.attn {
9351 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
9353 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
9354 AttnKind::Full {
9355 wq,
9356 wk,
9357 wv,
9358 wo,
9359 q_norm,
9360 k_norm,
9361 output_gate,
9362 softplus_gate,
9363 bias,
9364 } => {
9365 let mut cfg = self.attn_cfg(position);
9366 cfg.q_norm = q_norm.as_deref();
9367 cfg.k_norm = k_norm.as_deref();
9368 cfg.output_gate = *output_gate;
9369 cfg.softplus_gate = softplus_gate
9370 .as_ref()
9371 .map(|(gate, per_head)| (gate, *per_head));
9372 cfg.bias = bias
9373 .as_ref()
9374 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
9375 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
9376 }
9377 AttnKind::Linear(_)
9378 | AttnKind::LinearGdn(_)
9379 | AttnKind::ShortConv(_)
9380 | AttnKind::Bounded(_) => {
9381 unreachable!("MTP block is full attention")
9382 }
9383 };
9384 for (i, &a) in attn.iter().enumerate() {
9385 x[i] += a;
9386 }
9387 inference::rms_norm_into(
9388 &x,
9389 &lw.post_norm,
9390 self.rms_eps,
9391 self.norm_style,
9392 &mut self.ws.p1,
9393 );
9394 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
9395 for (i, &f) in ffn.iter().enumerate() {
9396 x[i] += f;
9397 }
9398
9399 inference::rms_norm_into(
9400 &x,
9401 &m.final_norm,
9402 self.rms_eps,
9403 self.norm_style,
9404 &mut self.ws.n1,
9405 );
9406 let lg = self.lm_head_forward(&self.ws.n1);
9407 (lg, x)
9408 }
9409
9410 fn mtp_step_h(
9412 &mut self,
9413 m: &mut MtpModule,
9414 hidden: &[f32],
9415 next_token: u32,
9416 position: usize,
9417 ) -> (u32, Vec<f32>) {
9418 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
9419 let draft = sampler::argmax(&lg);
9420 attention::recycle_buf(&mut lg);
9421 (draft, x)
9422 }
9423
9424 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
9430 static PIN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9434 if *PIN.get_or_init(|| std::env::var("CMF_GRAPH_SPEC_PIN").as_deref() == Ok("1")) {
9435 return SpecTrial::Decided {
9436 spec: true,
9437 recheck_at: usize::MAX,
9438 };
9439 }
9440 match trial {
9441 SpecTrial::Spec { t0, gen0, rounds } => {
9442 let rounds = rounds + 1;
9443 if mon.repeated_zero_acceptance() {
9451 tracing::info!(
9452 "speculation trial: no accepted drafts in two rounds — timing plain"
9453 );
9454 return SpecTrial::Plain {
9455 t0: std::time::Instant::now(),
9456 gen0: generated,
9457 };
9458 }
9459 if rounds >= 5 {
9460 if mon.plain_ms > 0.0 {
9461 let keep = mon.pays();
9462 mon.fails = 0;
9463 tracing::info!(
9464 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
9465 mon.tokens,
9466 mon.round_ms,
9467 mon.plain_ms,
9468 if keep { "speculating" } else { "plain" }
9469 );
9470 SpecTrial::Decided {
9471 spec: keep,
9472 recheck_at: if keep { usize::MAX } else { mon.recheck_plain_at(generated) },
9473 }
9474 } else if mon.pays() {
9475 mon.fails = 0;
9480 tracing::info!(
9481 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
9482 mon.tokens,
9483 mon.round_ms,
9484 );
9485 SpecTrial::Decided {
9486 spec: true,
9487 recheck_at: usize::MAX,
9488 }
9489 } else {
9490 SpecTrial::Plain {
9491 t0: std::time::Instant::now(),
9492 gen0: generated,
9493 }
9494 }
9495 } else {
9496 SpecTrial::Spec { t0, gen0, rounds }
9497 }
9498 }
9499 SpecTrial::Decided { spec: true, .. } => {
9500 if mon.pays() {
9501 mon.fails = 0;
9502 trial
9503 } else {
9504 mon.fails += 1;
9505 if mon.fails >= 4 {
9506 if mon.plain_ms <= 0.0 {
9507 tracing::info!(
9511 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
9512 mon.tokens,
9513 mon.round_ms,
9514 );
9515 return SpecTrial::Plain {
9516 t0: std::time::Instant::now(),
9517 gen0: generated,
9518 };
9519 }
9520 tracing::info!(
9521 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
9522 mon.tokens,
9523 mon.round_ms,
9524 mon.plain_ms
9525 );
9526 SpecTrial::Decided {
9527 spec: false,
9528 recheck_at: generated + SPEC_RECHECK_PLAIN,
9529 }
9530 } else {
9531 trial
9532 }
9533 }
9534 }
9535 other => other,
9536 }
9537 }
9538
9539 fn mtp_kv_id(&self) -> u64 {
9542 self.graph_kv_id | (1u64 << 40)
9543 }
9544
9545 const MTP_LAYER_BASE: usize = 0;
9550
9551 #[cfg(feature = "gpu")]
9558 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
9559 let stored = stored.saturating_sub(self.mtp_pos_base);
9560 self.mtp_graph_mode != Some(true)
9561 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
9562 }
9563
9564 #[cfg(feature = "gpu")]
9570 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
9571 let mut ok = true;
9572 let mut expected = false;
9573 for li in 0..self.num_layers {
9574 if matches!(
9575 self.weights.layers[self.phys_layer(li)].attn,
9576 AttnKind::Full { .. }
9577 ) {
9578 if self.o1_flags.get(li).copied().unwrap_or(false) && self.kv_cache.layers[li].o1_sealed() {
9580 continue;
9581 }
9582 expected = true;
9583 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
9584 }
9585 }
9586 !expected || ok
9587 }
9588
9589 fn graph_gdn_layer_count(&self) -> usize {
9593 (0..self.num_layers)
9596 .filter(|&li| {
9597 self.weights
9598 .layers
9599 .get(self.phys_layer(li))
9600 .is_some_and(|l| matches!(&l.attn, AttnKind::LinearGdn(_)))
9601 })
9602 .count()
9603 }
9604
9605 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
9608 self.mtp_block_inputs(m, &[(hidden, next_token)])
9609 }
9610
9611 fn mtp_block_inputs(&mut self, m: &MtpModule, pairs: &[(&[f32], u32)]) -> Vec<f32> {
9616 let hs = self.hidden_size;
9617 let mut cats = vec![0.0f32; pairs.len() * 2 * hs];
9618 for (i, (hidden, tok)) in pairs.iter().enumerate() {
9619 let e = self.embed_single(*tok);
9620 let cat = &mut cats[i * 2 * hs..(i + 1) * 2 * hs];
9621 let (cat_e, cat_h) = cat.split_at_mut(hs);
9622 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
9623 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
9624 }
9625 let mut xs = vec![0.0f32; pairs.len() * hs];
9626 #[cfg(feature = "gpu")]
9627 if crate::gpu::cuda_active() {
9628 if let Some((model, idx, kind, rs)) = m.eh_proj.graph_weight() {
9629 let gw = crate::gpu::GraphW {
9630 idx,
9631 kind,
9632 row_scale: rs,
9633 data: &[],
9634 prism: crate::gpu::GraphPrismOp::None,
9635 affine: false,
9636 };
9637 let model = model.clone();
9638 if crate::gpu::rows_matvec(&model, &gw, hs, 2 * hs, &cats, pairs.len(), &mut xs) {
9639 return xs;
9640 }
9641 }
9642 }
9643 for i in 0..pairs.len() {
9644 m.eh_proj.matvec(&cats[i * 2 * hs..(i + 1) * 2 * hs], &mut xs[i * hs..(i + 1) * hs], self.pool.as_deref());
9645 }
9646 xs
9647 }
9648
9649 #[cfg(feature = "gpu")]
9652 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
9653 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
9654 return false;
9655 }
9656 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode) || !crate::gpu::enabled_here() {
9657 return false;
9658 }
9659 self.mtp_block_shape_ok(m)
9660 }
9661
9662 fn mtp_block_shape_ok(&self, m: &MtpModule) -> bool {
9667 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9668 || self.attn_softcap > 0.0
9669 || self.attention_heads_per_layer.is_some()
9670 || self.v_head_dim.is_some()
9673 {
9674 return false;
9675 }
9676 matches!(
9677 &m.layer.attn,
9678 AttnKind::Full {
9679 softplus_gate: None,
9680 ..
9681 }
9682 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
9683 }
9684
9685 #[cfg(feature = "gpu")]
9689 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
9690 if !self.mtp_block_graph_ok(m) {
9691 return false;
9692 }
9693 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
9694 return false;
9695 };
9696 let FfnKind::Dense(d) = &m.layer.ffn else {
9697 return false;
9698 };
9699 d.segs.is_empty()
9700 && wq.graph_weight().is_some()
9701 && wk.graph_weight().is_some()
9702 && wv.graph_weight().is_some()
9703 && wo.graph_weight().is_some()
9704 && d.gate_proj.graph_weight().is_some()
9705 && d.up_proj.graph_weight().is_some()
9706 && d.down_proj.graph_weight().is_some()
9707 && self.weights.lm_head.graph_weight().is_some()
9708 }
9709
9710 #[cfg(feature = "gpu")]
9716 fn mtp_step_graph(
9717 &mut self,
9718 m: &mut MtpModule,
9719 hidden: &[f32],
9720 next_token: u32,
9721 position: usize,
9722 ) -> Option<(Vec<f32>, Vec<f32>)> {
9723 if !self.mtp_graph_ok(m) {
9724 return None;
9725 }
9726 let lw = &m.layer;
9727 let AttnKind::Full {
9728 wq,
9729 wk,
9730 wv,
9731 wo,
9732 q_norm,
9733 k_norm,
9734 output_gate,
9735 softplus_gate,
9736 bias,
9737 } = &lw.attn
9738 else {
9739 return None;
9740 };
9741 if softplus_gate.is_some() {
9742 return None;
9743 }
9744 let FfnKind::Dense(d) = &lw.ffn else {
9745 return None;
9746 };
9747 if !d.segs.is_empty() {
9748 return None; }
9750 let mut x = self.mtp_block_input(m, hidden, next_token);
9753 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9754 let (_, i, kind, rs) = t.graph_weight()?;
9755 Some(crate::gpu::GraphW {
9756 idx: i,
9757 kind,
9758 row_scale: rs,
9759 data: &[],
9760 prism: crate::gpu::GraphPrismOp::None,
9761 affine: false,
9762 })
9763 }
9764 let (model, _, _, _) = wq.graph_weight()?;
9765 let model = model.clone();
9766 let (lm_gw, lm_rows) = {
9767 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
9768 let rows = if kind == 6 {
9772 self.draft_head_rows(self.weights.lm_head.rows())
9773 } else {
9774 self.weights.lm_head.rows()
9775 };
9776 (
9777 crate::gpu::GraphW {
9778 idx: i,
9779 kind,
9780 row_scale: rs,
9781 data: &[],
9782 prism: crate::gpu::GraphPrismOp::None,
9783 affine: false,
9784 },
9785 rows,
9786 )
9787 };
9788 let layer = crate::gpu::GraphLayer {
9789 mhc: None,
9790 input_norm: &lw.input_norm,
9791 attn: crate::gpu::GraphAttn::Full {
9792 wq: gw(wq)?,
9793 wk: gw(wk)?,
9794 wv: gw(wv)?,
9795 wo: gw(wo)?,
9796 q_norm: q_norm.as_deref(),
9797 k_norm: k_norm.as_deref(),
9798 late_qk_norm: self.qk_norm_after_rope,
9799 bias: bias
9800 .as_ref()
9801 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9802 output_gate: *output_gate,
9803 cpu_k: m.kv.k_heads(),
9804 cpu_v: m.kv.v_heads(),
9805 cpu_base: m.kv.base(),
9806 geom: None,
9807 head_gate: None,
9808 },
9809 post_norm: &lw.post_norm,
9810 attn_out_norm: None,
9811 ffn_out_norm: None,
9812 layer_scale: None,
9813 ffn: crate::gpu::GraphFfn::Dense {
9814 gate: gw(&d.gate_proj)?,
9815 up: gw(&d.up_proj)?,
9816 down: gw(&d.down_proj)?,
9817 act: d.act.graph_act()?,
9818 limit: 0.0,
9819 hot: None,
9820 },
9821 };
9822 let nh = self.num_heads;
9823 let (nkv, hd, rd) = self.layer_geom(0);
9824 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
9825 let mut logits = Vec::new();
9826 let ok = crate::gpu::forward_token_graph(
9827 &model,
9828 self.mtp_kv_id(),
9829 std::slice::from_ref(&layer),
9830 &[None],
9831 self.o1_epoch,
9832 &self.inv_freq,
9833 &mut x,
9834 nh,
9835 nkv,
9836 hd,
9837 self.attn_scale,
9838 rd,
9839 self.hidden_size,
9840 d.gate_proj.rows(),
9844 position,
9845 self.kv_cache.max_seq_len,
9846 gemma,
9847 self.rms_eps as f32,
9848 Some((&lm_gw, lm_rows)),
9849 &m.final_norm,
9850 &mut logits,
9851 &[],
9852 1,
9853 None,
9854 None,
9855 None,
9856 Self::MTP_LAYER_BASE,
9857 true,
9858 );
9859 match ok {
9860 crate::gpu::TokenGraphOutcome::Completed => {}
9861 crate::gpu::TokenGraphOutcome::Declined => return None,
9862 crate::gpu::TokenGraphOutcome::Failed => {
9863 self.clear_sequence_state();
9867 self.graph_failed
9868 .store(true, std::sync::atomic::Ordering::Relaxed);
9869 self.cancel
9870 .store(true, std::sync::atomic::Ordering::Relaxed);
9871 return None;
9872 }
9873 }
9874 pad_mtp_draft_logits(&mut logits, self.vocab_size);
9878 Some((logits, x))
9879 }
9880
9881 #[cfg(feature = "cuda")]
9888 fn mtp_draft_chain_cuda(&self, m: &MtpModule, hidden: &[f32], t_next: u32, position: usize, k: usize) -> Result<Vec<u32>, bool> {
9889 let position = position.saturating_sub(self.mtp_pos_base);
9890 if !crate::gpu::cuda_active()
9891 || !Self::mtp_chain_on()
9892 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
9893 || self.draft_full_streak > 0
9894 || !self.mtp_graph_ok(m)
9895 || hidden.len() != self.hidden_size
9896 {
9897 return Err(false);
9898 }
9899 let lw = &m.layer;
9900 let AttnKind::Full { wq, wk, wv, wo, q_norm, k_norm, output_gate, softplus_gate: None, bias } = &lw.attn else {
9901 return Err(false);
9902 };
9903 let FfnKind::Dense(d) = &lw.ffn else {
9904 return Err(false);
9905 };
9906 if !d.segs.is_empty() {
9907 return Err(false);
9908 }
9909 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
9910 let (_, i, kind, rs) = t.graph_weight()?;
9911 Some(crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false })
9912 }
9913 let mk = || -> Option<_> {
9914 let (model, _, _, _) = wq.graph_weight()?;
9915 let (_, li, lkind, lrs) = self.weights.lm_head.graph_weight()?;
9916 if lkind != 6 {
9917 return None;
9918 }
9919 let rows = Self::draft_vocab_rows(self.weights.lm_head.rows());
9920 let lm = crate::gpu::GraphW { idx: li, kind: lkind, row_scale: lrs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false };
9921 let eh = gw(&m.eh_proj)?;
9922 let layer = crate::gpu::GraphLayer {
9923 mhc: None,
9924 input_norm: &lw.input_norm,
9925 attn: crate::gpu::GraphAttn::Full {
9926 wq: gw(wq)?,
9927 wk: gw(wk)?,
9928 wv: gw(wv)?,
9929 wo: gw(wo)?,
9930 q_norm: q_norm.as_deref(),
9931 k_norm: k_norm.as_deref(),
9932 late_qk_norm: self.qk_norm_after_rope,
9933 bias: bias.as_ref().map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9934 output_gate: *output_gate,
9935 cpu_k: m.kv.k_heads(),
9936 cpu_v: m.kv.v_heads(),
9937 cpu_base: m.kv.base(),
9938 geom: None,
9939 head_gate: None,
9940 },
9941 post_norm: &lw.post_norm,
9942 attn_out_norm: None,
9943 ffn_out_norm: None,
9944 layer_scale: None,
9945 ffn: crate::gpu::GraphFfn::Dense {
9946 gate: gw(&d.gate_proj)?,
9947 up: gw(&d.up_proj)?,
9948 down: gw(&d.down_proj)?,
9949 act: d.act.graph_act()?,
9950 limit: 0.0,
9951 hot: None,
9952 },
9953 };
9954 let (emodel, ei, ekind, ers) = self.weights.embed_tokens.graph_weight()?;
9956 if ekind != 6 || !std::sync::Arc::ptr_eq(emodel, model) || crate::prism::is_inverse_embedding(model, &model.tensors[ei].name) {
9957 return None;
9958 }
9959 let emb = crate::gpu::GraphW { idx: ei, kind: ekind, row_scale: ers, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false };
9960 Some((model.clone(), lm, rows, eh, layer, emb))
9961 };
9962 let Some((model, lm, rows, eh, layer, emb)) = mk() else {
9963 return Err(false);
9964 };
9965 let (nkv, hd, rd) = self.layer_geom(0);
9966 let e0 = self.embed_single(t_next);
9967 crate::gpu_cuda::mtp_chain(
9968 &model,
9969 self.mtp_kv_id(),
9970 &layer,
9971 &self.inv_freq,
9972 hidden,
9973 &e0,
9974 t_next,
9975 k,
9976 self.num_heads,
9977 nkv,
9978 hd,
9979 self.attn_scale,
9980 rd,
9981 self.hidden_size,
9982 position,
9983 self.kv_cache.max_seq_len,
9984 self.norm_style == cortiq_core::NormStyle::Gemma,
9985 self.rms_eps as f32,
9986 (&lm, rows),
9987 &m.final_norm,
9988 &eh,
9989 &m.enorm,
9990 &m.hnorm,
9991 &emb,
9992 Self::MTP_LAYER_BASE,
9993 )
9994 }
9995
9996 #[cfg(feature = "gpu")]
10004 fn mtp_warm_graph(
10005 &mut self,
10006 m: &mut MtpModule,
10007 pairs: &[(&[f32], u32)],
10008 first_pos: usize,
10009 ) -> crate::gpu::BatchGraphOutcome {
10010 let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
10011 if pairs.is_empty() {
10012 return crate::gpu::BatchGraphOutcome::Completed;
10013 }
10014 if !self.mtp_block_graph_ok(m) {
10015 return crate::gpu::BatchGraphOutcome::Declined;
10016 }
10017 let hs = self.hidden_size;
10018 let mut hiddens = self.mtp_block_inputs(m, pairs);
10021 debug_assert_eq!(hiddens.len(), pairs.len() * hs);
10022 spec_stamp("w.inp");
10023 let lw = &m.layer;
10024 let AttnKind::Full {
10025 wq,
10026 wk,
10027 wv,
10028 wo,
10029 q_norm,
10030 k_norm,
10031 output_gate,
10032 bias,
10033 ..
10034 } = &lw.attn
10035 else {
10036 return crate::gpu::BatchGraphOutcome::Declined;
10037 };
10038 let FfnKind::Dense(d) = &lw.ffn else {
10039 return crate::gpu::BatchGraphOutcome::Declined;
10040 };
10041 if !d.segs.is_empty() {
10042 return crate::gpu::BatchGraphOutcome::Declined; }
10044 let (
10047 AttnKind::Full {
10048 softplus_gate: None,
10049 ..
10050 },
10051 Some(gact),
10052 ) = (&lw.attn, d.act.graph_act())
10053 else {
10054 return crate::gpu::BatchGraphOutcome::Declined;
10055 };
10056 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
10057 let (_, i, kind, rs) = t.graph_weight()?;
10058 Some(crate::gpu::GraphW {
10059 idx: i,
10060 kind,
10061 row_scale: rs,
10062 data: &[],
10063 prism: crate::gpu::GraphPrismOp::None,
10064 affine: false,
10065 })
10066 }
10067 let Some((model, _, _, _)) = wq.graph_weight() else {
10068 return crate::gpu::BatchGraphOutcome::Declined;
10069 };
10070 let model = model.clone();
10071 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
10072 gw(wq),
10073 gw(wk),
10074 gw(wv),
10075 gw(wo),
10076 gw(&d.gate_proj),
10077 gw(&d.up_proj),
10078 gw(&d.down_proj),
10079 ) else {
10080 return crate::gpu::BatchGraphOutcome::Declined;
10081 };
10082 let layer = crate::gpu::GraphLayer {
10083 mhc: None,
10084 input_norm: &lw.input_norm,
10085 attn: crate::gpu::GraphAttn::Full {
10086 wq: gwq,
10087 wk: gwk,
10088 wv: gwv,
10089 wo: gwo,
10090 q_norm: q_norm.as_deref(),
10091 k_norm: k_norm.as_deref(),
10092 late_qk_norm: self.qk_norm_after_rope,
10093 bias: bias
10094 .as_ref()
10095 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
10096 output_gate: *output_gate,
10097 cpu_k: m.kv.k_heads(),
10098 cpu_v: m.kv.v_heads(),
10099 cpu_base: m.kv.base(),
10100 geom: None,
10101 head_gate: None,
10102 },
10103 post_norm: &lw.post_norm,
10104 attn_out_norm: None,
10105 ffn_out_norm: None,
10106 layer_scale: None,
10107 ffn: crate::gpu::GraphFfn::Dense {
10108 gate: gg,
10109 up: gu,
10110 down: gd,
10111 act: gact,
10112 limit: 0.0,
10113 hot: None,
10114 },
10115 };
10116 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
10117 let nh = self.num_heads;
10118 let (nkv, hd, rd) = self.layer_geom(0);
10119 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
10120 let defer = crate::gpu::cuda_active();
10123 if defer {
10124 crate::gpu::set_batch_defer(true);
10125 }
10126 let out = crate::gpu::forward_batch_graph(
10127 &model,
10128 self.mtp_kv_id(),
10129 std::slice::from_ref(&layer),
10130 &self.inv_freq,
10131 &mut hiddens,
10132 nh,
10133 nkv,
10134 hd,
10135 rd,
10136 hs,
10137 d.gate_proj.rows(),
10140 &positions,
10141 self.kv_cache.max_seq_len,
10142 gemma,
10143 self.rms_eps as f32,
10144 self.attn_scale,
10145 pairs.len(),
10146 &[],
10147 0,
10148 None,
10149 None,
10150 );
10151 if defer {
10152 crate::gpu::set_batch_defer(false);
10153 }
10154 spec_stamp("w.run");
10155 out
10156 }
10157
10158 #[cfg(feature = "gpu")]
10165 fn mtp_warm_graph_fallback(
10166 &mut self,
10167 m: &mut MtpModule,
10168 pairs: &[(&[f32], u32)],
10169 first_pos: usize,
10170 ) -> bool {
10171 let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
10172 if pairs.is_empty() {
10173 return true;
10174 }
10175 let graphable = self.mtp_block_graph_ok(m);
10176 if !graphable {
10177 if self.mtp_graph_mode == Some(true) {
10181 return false;
10182 }
10183 self.mtp_graph_mode = Some(false);
10184 for (j, (h, t)) in pairs.iter().enumerate() {
10185 self.mtp_warm(m, h, *t, first_pos + j);
10186 }
10187 return true;
10188 }
10189
10190 for (j, (h, t)) in pairs.iter().enumerate() {
10195 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
10196 return false;
10197 }
10198 }
10199 self.mtp_graph_mode = Some(true);
10200 true
10201 }
10202
10203 #[cfg(feature = "gpu")]
10208 fn mtp_warm_prefill_pairs(
10209 &mut self,
10210 m: &mut MtpModule,
10211 pairs: &[(&[f32], u32)],
10212 first_pos: usize,
10213 ) -> Result<(), &'static str> {
10214 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
10219 if self.mtp_graph_mode == Some(true) {
10220 return Err("MTP token graph became unavailable after admission");
10221 }
10222 self.mtp_graph_mode = Some(false);
10223 for (j, (h, t)) in pairs.iter().enumerate() {
10224 self.mtp_warm(m, h, *t, first_pos + j);
10225 }
10226 return Ok(());
10227 }
10228 match self.mtp_warm_graph(m, pairs, first_pos) {
10229 crate::gpu::BatchGraphOutcome::Completed => {
10230 if !pairs.is_empty() {
10231 self.mtp_graph_mode = Some(true);
10232 }
10233 Ok(())
10234 }
10235 crate::gpu::BatchGraphOutcome::Declined => {
10236 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
10237 Ok(())
10238 } else {
10239 Err("MTP warm-up fallback failed after device admission")
10240 }
10241 }
10242 crate::gpu::BatchGraphOutcome::Failed => {
10243 Err("MTP warm batch graph failed after admission")
10244 }
10245 }
10246 }
10247
10248 #[cfg(not(feature = "gpu"))]
10249 fn mtp_warm_prefill_pairs(
10250 &mut self,
10251 m: &mut MtpModule,
10252 pairs: &[(&[f32], u32)],
10253 first_pos: usize,
10254 ) -> Result<(), &'static str> {
10255 for (j, (h, t)) in pairs.iter().enumerate() {
10256 self.mtp_warm(m, h, *t, first_pos + j);
10257 }
10258 Ok(())
10259 }
10260
10261 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
10265 let position = position.saturating_sub(self.mtp_pos_base);
10266 let e = self.embed_single(next_token);
10267 let mut cat = vec![0.0f32; 2 * self.hidden_size];
10268 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
10269 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
10270 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
10271 let mut x = vec![0.0f32; self.hidden_size];
10272 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
10273 inference::rms_norm_into(
10274 &x,
10275 &m.layer.input_norm,
10276 self.rms_eps,
10277 self.norm_style,
10278 &mut self.ws.n1,
10279 );
10280 let attn = match &m.layer.attn {
10281 AttnKind::Full {
10282 wq,
10283 wk,
10284 wv,
10285 wo,
10286 q_norm,
10287 k_norm,
10288 output_gate,
10289 softplus_gate,
10290 bias,
10291 } => {
10292 let mut cfg = self.attn_cfg(position);
10293 cfg.q_norm = q_norm.as_deref();
10294 cfg.k_norm = k_norm.as_deref();
10295 cfg.output_gate = *output_gate;
10296 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
10297 cfg.bias = bias
10298 .as_ref()
10299 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
10300 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
10301 }
10302 _ => return,
10303 };
10304 let _ = attn;
10305 }
10306
10307 #[cfg(feature = "gpu")]
10314 #[allow(clippy::too_many_arguments)]
10315 fn graph_spec_step(
10316 &mut self,
10317 m: &mut MtpModule,
10318 hidden: &[f32],
10319 t_next: u32,
10320 next_pos: usize,
10321 drafted: &mut usize,
10322 accepted: &mut usize,
10323 all_ids: &mut Vec<u32>,
10327 room: usize,
10332 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
10333 #[cfg(target_os = "macos")]
10344 let metal_native = crate::gpu::q1_force();
10345 #[cfg(not(target_os = "macos"))]
10346 let metal_native = false;
10347 #[cfg(feature = "gpu")]
10348 let k_default = if metal_native {
10349 7
10352 } else if crate::gpu_wgpu::verify_i8_on() {
10353 5
10354 } else {
10355 4
10356 };
10357 #[cfg(not(feature = "gpu"))]
10358 let k_default = 4;
10359 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
10360 .ok()
10361 .and_then(|v| v.parse().ok())
10362 .filter(|&v| (1..=8).contains(&v));
10363 let metal_small = metal_native && self.metal_small_batch_verify();
10371 let cfg = self.sampler_config.clone();
10376 let penalized = !(cfg.repetition_penalty == 1.0
10377 && cfg.presence_penalty == 0.0
10378 && cfg.suppress_tokens.is_empty());
10379 let greedy_pen = cfg.temperature < 1e-6 && penalized;
10384 let sampling = cfg.temperature >= 1e-6;
10385 if !metal_native {
10405 self.dflash_taps_absorb();
10406 }
10407 let cuda_rate = !metal_native && crate::gpu::cuda_active();
10412 let dflash_ok = !sampling
10413 && !greedy_pen
10414 && match &self.dflash {
10415 Some(d) => metal_native || ((dflash_pick() == Some(true) || cuda_rate) && d.next_position() == next_pos),
10416 None => false,
10417 };
10418 if !metal_native && !cuda_rate && self.dflash.is_some() && dflash_pick() != Some(true) {
10419 static SAID: std::sync::Once = std::sync::Once::new();
10420 SAID.call_once(|| {
10421 tracing::info!(
10422 "DFlash drafter: on wgpu the MTP chain keeps the rounds (CMF_DFLASH_PICK=dflash pins the block)"
10423 )
10424 });
10425 }
10426 let (k_flash, rate_flash) = if cuda_rate {
10427 spec_plan_at(&CUDA_ROUND_COST, &self.dflash_hold, CUDA_BLOCK_DRAFT, 0.0)
10428 } else {
10429 metal_spec_plan(&self.dflash_hold, DFLASH_ROUND_DRAFT)
10430 };
10431 let (_, rate_mtp) = if cuda_rate {
10432 spec_plan_at(&CUDA_ROUND_COST, &self.spec_hold, 0.0, CUDA_DRAFT_STEP)
10433 } else {
10434 metal_spec_plan(&self.spec_hold, 0.0)
10435 };
10436 let routed = match (&self.dflash, dflash_ok) {
10443 (Some(d), true) => d.route.as_ref().map(|r| r.margin(hidden)),
10444 _ => None,
10445 };
10446 if let Some(m) = routed {
10447 self.dflash_lean += DFLASH_LEAN_STEP * (m - self.dflash_lean);
10448 }
10449 let copying = (routed.is_some() || cuda_rate)
10454 && dflash_pick().is_none()
10455 && (tail_repeat(all_ids) >= DFLASH_COPY_MIN || (cuda_rate && self.spec_lookup_gain > DFLASH_LOOKUP_KEEPS));
10456 let dflash_round = dflash_ok
10457 && match (dflash_pick(), routed) {
10458 (Some(pin), _) => pin,
10459 (None, Some(_)) if copying => false,
10460 (None, Some(_)) if cuda_rate && self.draft_full_streak > 0 => false,
10464 (None, Some(_)) if self.dflash_last => self.dflash_lean > -DFLASH_LEAN_BAND,
10465 (None, Some(_)) => self.dflash_lean > DFLASH_LEAN_BAND,
10466 (None, None) if copying => false,
10468 (None, None) if !metal_small && !cuda_rate => true,
10469 (None, None) if self.draft_full_streak > 0 => false,
10470 (None, None) if self.dflash_last => rate_flash * 1.05 >= rate_mtp,
10471 (None, None) if self.dflash_wait > 0 => {
10472 self.dflash_wait -= 1;
10473 false
10474 }
10475 (None, None) => rate_flash > rate_mtp * 1.05,
10476 };
10477 if dflash_ok && dflash_round != self.dflash_last {
10478 if dflash_round {
10479 (self.dflash_turn, self.dflash_turn_sum) = (0, (0.0, 0.0));
10480 } else {
10481 let (tokens, cost) = self.dflash_turn_sum;
10483 if self.dflash_turn >= DFLASH_TURN && tokens >= rate_mtp * cost {
10484 self.dflash_wait = DFLASH_WAIT_DIP;
10485 } else {
10486 self.dflash_back = if self.dflash_turn < DFLASH_TURN {
10487 (self.dflash_back * 2).min(DFLASH_WAIT_MOST)
10488 } else {
10489 DFLASH_WAIT
10490 };
10491 self.dflash_wait = self.dflash_back;
10492 }
10493 self.dflash_hold = DFLASH_HOLD_PRIOR;
10494 }
10495 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
10496 eprintln!(
10497 "spec-drafter: {} ({rate_flash:.2} against {rate_mtp:.2} tokens in a plain token's time)",
10498 if dflash_round { "the DFlash block" } else { "the MTP chain" }
10499 );
10500 }
10501 }
10502 self.dflash_turn += u32::from(dflash_round);
10503 self.dflash_last = dflash_round;
10504 let mtp_in_step = dflash_pick() != Some(true) || dflash_rows_log().is_some();
10506 let (k_start, k_max) = if dflash_round && metal_small {
10507 (k_flash, 7)
10508 } else if metal_small {
10509 (metal_spec_depth(&self.spec_hold), 7)
10510 } else if metal_native || dflash_round {
10511 (7, 7)
10513 } else {
10514 (3, k_default.max(5))
10515 };
10516 let k_full: usize = k_env.unwrap_or_else(|| {
10517 if metal_small {
10518 k_start
10519 } else if dflash_round && !metal_native {
10520 self.dflash_k_adapt.unwrap_or(k_start)
10521 } else {
10522 self.spec_k_adapt.unwrap_or(k_start)
10523 }
10524 });
10525 let k_spec = k_full.min(room).max(1);
10526 let k_capped = k_spec < k_full;
10529 if next_pos == 0 {
10530 return None;
10531 }
10532 let t_round = std::time::Instant::now();
10533 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
10549 let sub0 = subs();
10550 let k_spec = match &self.dflash {
10551 Some(d) if dflash_round => k_spec.min(d.g.block - 1),
10552 _ => k_spec,
10553 };
10554 let sparse = sampling && sampler::sparse_ok(&cfg);
10560 let base_len = all_ids.len();
10561 if sampling && !sparse && self.spec_q.len() < k_spec {
10562 self.spec_q.resize_with(k_spec, Vec::new);
10563 }
10564 if sparse && self.spec_qs.len() < k_spec {
10565 self.spec_qs.resize_with(k_spec, Vec::new);
10566 }
10567 let mut drafts = Vec::with_capacity(k_spec);
10572 let mut hx = hidden.to_vec();
10573 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
10576 spec_stamp("pro");
10577 if dflash_round {
10578 drafts = self.dflash_draft(t_next, next_pos, k_spec);
10579 spec_stamp("d.flash");
10580 }
10581 let mut beside: Vec<u32> = Vec::new();
10584 #[cfg(target_os = "macos")]
10585 if dflash_round && dflash_rows_log().is_some() && self.mtp_graph_mode != Some(false) {
10586 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, 7) {
10587 Ok(ids) => {
10588 self.mtp_graph_mode = Some(true);
10589 beside = ids;
10590 }
10591 Err(true) => {
10592 tracing::error!("mtp Metal draft chain failed after commit");
10593 self.clear_sequence_state();
10594 self.graph_failed
10595 .store(true, std::sync::atomic::Ordering::Relaxed);
10596 self.cancel
10597 .store(true, std::sync::atomic::Ordering::Relaxed);
10598 return None;
10599 }
10600 Err(false) => {}
10601 }
10602 }
10603 #[cfg(target_os = "macos")]
10609 if metal_native && !dflash_round && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
10610 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
10611 Ok(ids) => {
10612 self.mtp_graph_mode = Some(true);
10613 drafts = ids;
10614 }
10615 Err(true) => {
10616 tracing::error!("mtp Metal draft chain failed after commit");
10617 self.clear_sequence_state();
10618 self.graph_failed
10619 .store(true, std::sync::atomic::Ordering::Relaxed);
10620 self.cancel
10621 .store(true, std::sync::atomic::Ordering::Relaxed);
10622 return None;
10623 }
10624 Err(false) => {}
10625 }
10626 }
10627 #[cfg(feature = "cuda")]
10633 if crate::gpu::cuda_active()
10634 && !dflash_round
10635 && !sampling
10636 && !spec_dbg
10637 && drafts.is_empty()
10638 && self.mtp_graph_mode != Some(false)
10639 && (!greedy_pen
10640 || (cfg.repetition_penalty == 1.0
10641 && cfg.presence_penalty == 0.0
10642 && cfg.suppress_tokens.iter().all(|&t| (t as usize) >= Self::draft_vocab_rows(self.weights.lm_head.rows()))))
10643 {
10644 match self.mtp_draft_chain_cuda(m, &hx, t_next, next_pos - 1, k_spec) {
10645 Ok(ids) => {
10646 self.mtp_graph_mode = Some(true);
10647 drafts = ids;
10648 spec_stamp("d.chain");
10649 }
10650 Err(true) => {
10651 tracing::error!("cuda draft chain failed after admission");
10652 self.clear_sequence_state();
10653 self.graph_failed
10654 .store(true, std::sync::atomic::Ordering::Relaxed);
10655 self.cancel
10656 .store(true, std::sync::atomic::Ordering::Relaxed);
10657 return None;
10658 }
10659 Err(false) => {}
10660 }
10661 }
10662 for j in drafts.len()..k_spec {
10663 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
10664 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
10665 if spec_dbg {
10666 let saved = self.mtp_graph_mode;
10667 self.mtp_graph_mode = Some(false);
10668 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
10669 self.mtp_graph_mode = saved;
10670 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10671 return None;
10672 }
10673 m.kv.truncate_last(1);
10674 dbg_ref = Some(r);
10675 }
10676 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
10677 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
10678 return None;
10679 }
10680 if let Some((lg_cpu, h_cpu)) = dbg_ref {
10681 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
10682 let dl = lg
10683 .iter()
10684 .zip(&lg_cpu)
10685 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
10686 let dh = hj
10687 .iter()
10688 .zip(&h_cpu)
10689 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
10690 eprintln!(
10691 "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 {}",
10692 next_pos - 1 + j,
10693 sampler::argmax(&lg_cpu),
10694 sampler::argmax(&lg),
10695 n(&h_cpu),
10696 n(&hj),
10697 m.kv.seq_len
10698 );
10699 }
10700 let dj = if sparse {
10701 let mut q = std::mem::take(&mut self.spec_qs[j]);
10702 let ok = sampler::sparse_distribution_into(
10703 &lg,
10704 &cfg,
10705 all_ids,
10706 &mut self.sampler_scratch,
10707 self.pool.as_deref(),
10708 &mut q,
10709 );
10710 let d = if ok {
10711 sampler::draw_sparse(&q, &mut self.rng)
10712 } else {
10713 let t = sampler::argmax(&lg);
10715 q.clear();
10716 q.push((t, 1.0));
10717 t
10718 };
10719 self.spec_qs[j] = q;
10720 all_ids.push(d);
10721 d
10722 } else if sampling {
10723 let mut q = std::mem::take(&mut self.spec_q[j]);
10724 sampler::distribution_into(
10725 &lg,
10726 &cfg,
10727 all_ids,
10728 &mut self.sampler_scratch,
10729 self.pool.as_deref(),
10730 &mut q,
10731 );
10732 let d = sampler::draw(&q, &mut self.rng);
10733 self.spec_q[j] = q;
10734 all_ids.push(d); d
10736 } else if greedy_pen {
10737 let d = sampler::argmax_penalized(
10738 &lg,
10739 &cfg,
10740 all_ids,
10741 &mut self.sampler_scratch,
10742 self.pool.as_deref(),
10743 );
10744 all_ids.push(d);
10745 d
10746 } else {
10747 sampler::argmax(&lg)
10748 };
10749 attention::recycle_buf(&mut lg);
10750 drafts.push(dj);
10751 hx = hj;
10752 spec_stamp("d.pick");
10753 }
10754 all_ids.truncate(base_len);
10755 let k_mtp = k_spec;
10761 let mtp_steps = if dflash_round { beside.len() } else { k_mtp };
10764 let cuda_window = crate::gpu::cuda_active();
10768 let k_room = room.min(if cuda_window && self.spec_lookup_wide { 15 } else { 7 });
10769 let history_free = cfg.repetition_penalty == 1.0 && cfg.presence_penalty == 0.0;
10772 if (metal_native || cuda_window)
10773 && !sampling
10774 && (!greedy_pen || (cuda_window && history_free))
10775 && k_mtp < k_room
10776 && drafts.len() == k_mtp
10777 && Self::spec_lookup_on()
10778 {
10779 if self.spec_lookup_rest > 0 {
10780 self.spec_lookup_rest -= 1;
10781 } else {
10782 all_ids.extend_from_slice(&drafts);
10783 let more =
10784 lookup_continuation(all_ids, k_mtp + LOOKUP_MIN_REAL, k_room - k_mtp).to_vec();
10785 all_ids.truncate(base_len);
10786 drafts.extend(more);
10787 }
10788 }
10789 let k_spec = drafts.len();
10790 spec_stamp("d.look");
10791 *drafted += k_spec;
10792 let t_draft = t_round.elapsed();
10793 let sub_draft = subs();
10794 let b = k_spec + 1;
10797 let mut hiddens = vec![0.0f32; b * self.hidden_size];
10798 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
10799 let e = self.embed_single(t);
10800 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
10801 }
10802 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
10803 spec_stamp("v.emb");
10804 let (lm_gw, lm_rows) = {
10805 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
10806 (
10807 crate::gpu::GraphW {
10808 idx: i,
10809 kind,
10810 row_scale: rs,
10811 data: &[],
10812 prism: crate::gpu::GraphPrismOp::None,
10813 affine: false,
10814 },
10815 self.weights.lm_head.rows(),
10816 )
10817 };
10818 let mut logits = Vec::new();
10819 let final_norm = self.weights.final_norm.clone();
10820 #[cfg(target_os = "macos")]
10829 let greedy_dev = metal_native
10830 && !sampling
10831 && !greedy_pen
10832 && !self.confidence_on
10833 && self.final_softcap.is_none()
10834 && self.vocab_size == lm_rows
10840 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
10841 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
10842 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
10843 #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10847 let greedy_dev = crate::gpu::cuda_active()
10848 && !sampling
10849 && !greedy_pen
10850 && !self.confidence_on
10851 && self.final_softcap.is_none()
10852 && self.vocab_size == lm_rows
10853 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
10854 && std::env::var("CMF_CUDA_DEV_ARGMAX").as_deref() != Ok("0");
10855 #[cfg(all(not(target_os = "macos"), not(feature = "cuda")))]
10856 let greedy_dev = false;
10857 let mut dev_ids: Vec<u32> = Vec::new();
10858 #[cfg(target_os = "macos")]
10859 let verify_outcome = if metal_native {
10860 let lm = self.weights.lm_head.q1_parts()?;
10861 let n_score = self.vocab_size.min(lm_rows);
10862 self.try_batch_graph_metal(
10863 &mut hiddens,
10864 &positions,
10865 b,
10866 Some((lm, &final_norm, &mut logits)),
10867 if greedy_dev {
10868 Some((n_score, &mut dev_ids))
10869 } else {
10870 None
10871 },
10872 )
10873 } else {
10874 self.try_batch_graph_wgpu(
10875 &mut hiddens,
10876 &positions,
10877 b,
10878 Some(crate::gpu::SpecTail {
10879 lm: lm_gw,
10880 lm_rows,
10881 final_norm: &final_norm,
10882 logits_out: &mut logits,
10883 pick: None,
10884 }),
10885 )
10886 };
10887 #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10888 crate::gpu_cuda::verify_argmax_arm(greedy_dev);
10889 #[cfg(not(target_os = "macos"))]
10890 let verify_outcome = self.try_batch_graph_wgpu(
10891 &mut hiddens,
10892 &positions,
10893 b,
10894 Some(crate::gpu::SpecTail {
10895 lm: lm_gw,
10896 lm_rows,
10897 final_norm: &final_norm,
10898 logits_out: &mut logits,
10899 pick: None,
10900 }),
10901 );
10902 #[cfg(all(not(target_os = "macos"), feature = "cuda"))]
10903 if greedy_dev {
10904 if let Some(ids) = crate::gpu_cuda::verify_argmax_take() {
10905 dev_ids = ids;
10906 }
10907 }
10908 match verify_outcome {
10909 crate::gpu::BatchGraphOutcome::Completed => {}
10910 crate::gpu::BatchGraphOutcome::Declined => {
10911 self.dflash_rows = None;
10916 m.kv.truncate_last(mtp_steps);
10917 if !metal_native && mtp_steps > 0 && !self.rewind_mtp_graph_mirror(next_pos) {
10918 self.clear_sequence_state();
10919 self.graph_failed
10920 .store(true, std::sync::atomic::Ordering::Relaxed);
10921 self.cancel
10922 .store(true, std::sync::atomic::Ordering::Relaxed);
10923 tracing::error!("MTP graph mirror rewind failed after verify decline");
10924 }
10925 return None;
10926 }
10927 crate::gpu::BatchGraphOutcome::Failed => {
10928 self.clear_sequence_state();
10932 self.graph_failed
10933 .store(true, std::sync::atomic::Ordering::Relaxed);
10934 self.cancel
10935 .store(true, std::sync::atomic::Ordering::Relaxed);
10936 tracing::error!("MTP verify batch graph failed after admission");
10937 self.dflash_rows = None;
10938 return None;
10939 }
10940 }
10941 #[cfg(target_os = "macos")]
10947 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
10948 let snap: Vec<Vec<f32>> = self
10949 .kv_cache
10950 .layers
10951 .iter()
10952 .map(|l| l.linear_state.clone())
10953 .collect();
10954 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
10955 let toks: Vec<u32> = std::iter::once(t_next)
10956 .chain(drafts.iter().copied())
10957 .collect();
10958 let want_save = self.graph_want_logits;
10959 self.graph_want_logits = false;
10960 for (i, &t) in toks.iter().enumerate() {
10961 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
10962 let _ = self.graph_logits.take();
10963 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
10967 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
10968 }
10969 let ref_lg = self.logits_from_hidden(&hi);
10970 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
10971 let ra = sampler::argmax(&ref_lg);
10972 let va = sampler::argmax(row);
10973 let mut md = 0f32;
10974 let mut rms = 0f64;
10975 for j in 0..lm_rows.min(ref_lg.len()) {
10976 let d = (ref_lg[j] - row[j]).abs();
10977 md = md.max(d);
10978 rms += (d as f64) * (d as f64);
10979 }
10980 let mut hd = 0f32;
10981 for j in 0..self.hidden_size {
10982 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
10983 }
10984 eprintln!(
10985 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3e} rms {:.3e} | max|dhidden| {hd:.3e}",
10986 next_pos + i,
10987 if ra == va { "OK" } else { "MISMATCH" },
10988 (rms / lm_rows as f64).sqrt()
10989 );
10990 }
10991 self.graph_want_logits = want_save;
10992 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
10995 if l.linear_state.len() == st.len() {
10996 l.linear_state.copy_from_slice(&st);
10997 } else {
10998 l.linear_state = st;
10999 }
11000 }
11001 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
11002 let extra = l.seq_len.saturating_sub(n0);
11003 if extra > 0 {
11004 l.truncate_last(extra);
11005 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
11006 }
11007 }
11008 }
11009 let t_verify = t_round.elapsed();
11010 let sub_verify = subs();
11011 let mut a = 0usize;
11016 let mut forced: Option<u32> = None;
11017 let ids: Vec<u32> = if sparse {
11018 let mut p = std::mem::take(&mut self.spec_ps);
11019 let mut res = std::mem::take(&mut self.spec_ress);
11020 while a < k_spec {
11021 let ok = sampler::sparse_distribution_into(
11022 &logits[a * lm_rows..(a + 1) * lm_rows],
11023 &cfg,
11024 all_ids,
11025 &mut self.sampler_scratch,
11026 self.pool.as_deref(),
11027 &mut p,
11028 );
11029 if !ok {
11030 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
11031 p.clear();
11032 p.push((t, 1.0));
11033 }
11034 match sampler::spec_accept_or_correct_sparse(
11035 &p,
11036 &self.spec_qs[a],
11037 drafts[a],
11038 &mut self.rng,
11039 &mut res,
11040 ) {
11041 None => {
11042 all_ids.push(drafts[a]);
11043 a += 1;
11044 }
11045 Some(c) => {
11046 forced = Some(c);
11047 break;
11048 }
11049 }
11050 }
11051 all_ids.truncate(base_len);
11052 self.spec_ps = p;
11053 self.spec_ress = res;
11054 drafts.clone()
11055 } else if sampling {
11056 let mut p = std::mem::take(&mut self.spec_p);
11057 let mut res = std::mem::take(&mut self.spec_res);
11058 while a < k_spec {
11059 sampler::distribution_into(
11060 &logits[a * lm_rows..(a + 1) * lm_rows],
11061 &cfg,
11062 all_ids,
11063 &mut self.sampler_scratch,
11064 self.pool.as_deref(),
11065 &mut p,
11066 );
11067 match sampler::spec_accept_or_correct(
11068 &p,
11069 &self.spec_q[a],
11070 drafts[a],
11071 &mut self.rng,
11072 &mut res,
11073 self.pool.as_deref(),
11074 ) {
11075 None => {
11076 all_ids.push(drafts[a]);
11077 a += 1;
11078 }
11079 Some(c) => {
11080 forced = Some(c);
11081 break;
11082 }
11083 }
11084 }
11085 all_ids.truncate(base_len);
11086 self.spec_p = p;
11087 self.spec_res = res;
11088 drafts.clone()
11090 } else if greedy_pen {
11091 let mut ids: Vec<u32> = Vec::with_capacity(b);
11095 for i in 0..b {
11096 let t = sampler::argmax_penalized(
11097 &logits[i * lm_rows..(i + 1) * lm_rows],
11098 &cfg,
11099 all_ids,
11100 &mut self.sampler_scratch,
11101 self.pool.as_deref(),
11102 );
11103 ids.push(t);
11104 if i < k_spec && t == drafts[i] {
11105 all_ids.push(t);
11106 } else {
11107 break;
11108 }
11109 }
11110 all_ids.truncate(base_len);
11111 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
11112 a += 1;
11113 }
11114 ids
11117 } else if greedy_dev && dev_ids.len() == b {
11118 let ids = std::mem::take(&mut dev_ids);
11119 while a < k_spec && ids[a] == drafts[a] {
11120 a += 1;
11121 }
11122 ids
11123 } else {
11124 if logits.len() < b * lm_rows {
11125 self.clear_sequence_state();
11128 self.graph_failed
11129 .store(true, std::sync::atomic::Ordering::Relaxed);
11130 self.cancel
11131 .store(true, std::sync::atomic::Ordering::Relaxed);
11132 tracing::error!("Metal verify returned neither logits nor argmax ids");
11133 return None;
11134 }
11135 let ids: Vec<u32> = (0..b)
11136 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
11137 .collect();
11138 while a < k_spec && ids[a] == drafts[a] {
11139 a += 1;
11140 }
11141 ids
11142 };
11143 spec_stamp("acc");
11144 if dflash_round {
11145 if let Some(log) = dflash_rows_log() {
11146 let mut rec = [u32::MAX; 24];
11147 (rec[0], rec[1], rec[2], rec[3]) = (next_pos as u32, t_next, k_spec as u32, a as u32);
11148 for (o, &d) in rec[4..11].iter_mut().zip(&drafts) {
11149 *o = d;
11150 }
11151 for (o, &d) in rec[11..18].iter_mut().zip(&beside) {
11152 *o = d;
11153 }
11154 let mut more: Vec<u32> = Vec::new();
11156 if beside.len() >= 2 && Self::spec_lookup_on() {
11157 all_ids.extend_from_slice(&beside[..2]);
11158 more = lookup_continuation(all_ids, 2 + LOOKUP_MIN_REAL, 5).to_vec();
11159 all_ids.truncate(base_len);
11160 }
11161 rec[18] = more.len() as u32;
11162 for (o, &d) in rec[19..24].iter_mut().zip(&more) {
11163 *o = d;
11164 }
11165 let mut bytes: Vec<u8> = rec.iter().flat_map(|v| v.to_le_bytes()).collect();
11166 bytes.extend(hidden.iter().flat_map(|v| v.to_le_bytes()));
11167 use std::io::Write as _;
11168 static IDS: std::sync::Once = std::sync::Once::new();
11171 IDS.call_once(|| {
11172 if let Ok(path) = std::env::var("CMF_DFLASH_ROWS") {
11173 let ids: Vec<u8> = all_ids.iter().flat_map(|v| v.to_le_bytes()).collect();
11174 if let Err(e) = std::fs::write(format!("{path}.ids"), ids) {
11175 tracing::warn!("CMF_DFLASH_ROWS: {path}.ids: {e}");
11176 }
11177 }
11178 });
11179 if let Ok(mut f) = log.lock() {
11180 let _ = f.write_all(&bytes);
11181 }
11182 }
11183 }
11184 if spec_dbg {
11185 eprintln!(
11186 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
11187 drafts, ids
11188 );
11189 }
11190 #[cfg(target_os = "macos")]
11194 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
11195 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
11196 {
11197 let snap: Vec<Vec<f32>> = self
11198 .kv_cache
11199 .layers
11200 .iter()
11201 .map(|l| l.linear_state.clone())
11202 .collect();
11203 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
11204 let toks: Vec<u32> = std::iter::once(t_next)
11205 .chain(drafts.iter().copied())
11206 .collect();
11207 let want_save = self.graph_want_logits;
11208 self.graph_want_logits = false;
11209 for (i, &t) in toks.iter().take(a + 1).enumerate() {
11210 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
11211 let _ = self.graph_logits.take();
11212 }
11213 self.graph_want_logits = want_save;
11214 let plain_states: Vec<Vec<f32>> = self
11215 .kv_cache
11216 .layers
11217 .iter()
11218 .map(|l| l.linear_state.clone())
11219 .collect();
11220 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11221 let mut rows = Vec::new();
11222 for (li, (l, n0)) in self
11223 .kv_cache
11224 .layers
11225 .iter_mut()
11226 .zip(attn_lens.iter())
11227 .enumerate()
11228 {
11229 let extra = l.seq_len.saturating_sub(*n0);
11230 if extra > 0 {
11231 let mut kk = Vec::new();
11232 let mut vv = Vec::new();
11233 for g in 0..nkv {
11234 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
11235 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
11236 }
11237 rows.push((li, kk, vv));
11238 l.truncate_last(extra);
11239 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
11240 }
11241 }
11242 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
11243 if l.linear_state.len() == st.len() {
11244 l.linear_state.copy_from_slice(&st);
11245 } else {
11246 l.linear_state = st;
11247 }
11248 }
11249 Some((plain_states, rows))
11250 } else {
11251 None
11252 };
11253 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
11254 #[cfg(target_os = "macos")]
11263 let mut warm_pending: Option<MetalWarmPending> = None;
11264 let hs_w = self.hidden_size;
11269 let warm_first = usize::from(mtp_steps == 0);
11270 let warm_n = if dflash_round && !mtp_in_step { 0 } else { a + warm_first };
11271 let warm_pos = next_pos - warm_first;
11272 let warm_pair = |j: usize| -> (&[f32], u32) {
11273 if j < warm_first {
11274 (hidden, t_next)
11275 } else {
11276 let j = j - warm_first;
11277 (&hiddens[j * hs_w..(j + 1) * hs_w], ids[j])
11278 }
11279 };
11280 #[cfg(target_os = "macos")]
11281 if metal_native {
11282 if mtp_steps > 0 {
11283 m.kv.truncate_last(mtp_steps - 1);
11284 }
11285 if self.mtp_graph_mode == Some(true) {
11286 crate::gpu_metal::kv_mirror_set_stored(
11289 self.mtp_kv_id(),
11290 Self::MTP_LAYER_BASE,
11291 m.kv.seq_len,
11292 );
11293 if !warm_off && warm_n > 0 {
11294 let pairs: Vec<(&[f32], u32)> = (0..warm_n).map(&warm_pair).collect();
11295 warm_pending = self.mtp_warm_batch_submit(m, &pairs, warm_pos);
11296 }
11297 }
11298 spec_stamp("c.wsub");
11299 }
11300 #[cfg(target_os = "macos")]
11302 if metal_native {
11303 if !self.metal_verify_commit(a) {
11306 self.clear_sequence_state();
11307 self.graph_failed
11308 .store(true, std::sync::atomic::Ordering::Relaxed);
11309 self.cancel
11310 .store(true, std::sync::atomic::Ordering::Relaxed);
11311 tracing::error!("Metal verify state/KV handoff failed after admission");
11312 return None;
11313 }
11314 if let (Some((pos0, rows)), Some(d)) = (self.dflash_rows.take(), self.dflash.as_mut()) {
11316 let tw = d.g.tap_width();
11317 d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11318 spec_stamp("c.flash");
11319 }
11320 if let Some((plain_states, rows)) = commit_ref {
11321 crate::gpu_metal::queue_fence();
11322 let _ = crate::gpu_metal::wait_replay();
11325 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
11326 let mut worst_s = 0f32;
11327 let mut worst_li = 0usize;
11328 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
11329 if l.linear_state.len() != ps.len() || ps.is_empty() {
11330 continue;
11331 }
11332 let d = l
11333 .linear_state
11334 .iter()
11335 .zip(ps)
11336 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11337 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
11338 let rel = d / n.max(1e-6);
11339 if rel > worst_s {
11340 worst_s = rel;
11341 worst_li = li;
11342 }
11343 }
11344 let mut worst_k = 0f32;
11345 for (li, kk, vv) in &rows {
11346 let l = &self.kv_cache.layers[*li];
11347 let n0 = l.seq_len - (kk.len() / (nkv * hd));
11348 let mut ck = Vec::new();
11349 let mut cv = Vec::new();
11350 for g in 0..nkv {
11351 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
11352 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
11353 }
11354 if ck.len() == kk.len() {
11355 let dk = ck
11356 .iter()
11357 .zip(kk)
11358 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11359 let dv = cv
11360 .iter()
11361 .zip(vv)
11362 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
11363 worst_k = worst_k.max(dk).max(dv);
11364 } else {
11365 eprintln!(
11366 "commit-check L{li}: kv row count mismatch {} vs {}",
11367 ck.len(),
11368 kk.len()
11369 );
11370 }
11371 }
11372 eprintln!(
11373 "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}"
11374 );
11375 }
11376 }
11377 if !metal_native && a + 1 < b {
11378 let expected_gdn_layers = self.graph_gdn_layer_count();
11379 if expected_gdn_layers > 0
11380 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
11381 {
11382 self.clear_sequence_state();
11383 self.graph_failed
11384 .store(true, std::sync::atomic::Ordering::Relaxed);
11385 self.cancel
11386 .store(true, std::sync::atomic::Ordering::Relaxed);
11387 tracing::error!("GDN speculative restore failed after verify");
11388 return None;
11389 }
11390 }
11391 if self.o1_active() && !crate::gpu::o1_spec_commit(self.graph_kv_id, next_pos, a + 1) {
11392 self.clear_sequence_state();
11393 self.graph_failed
11394 .store(true, std::sync::atomic::Ordering::Relaxed);
11395 self.cancel
11396 .store(true, std::sync::atomic::Ordering::Relaxed);
11397 tracing::error!("far field commit failed after verify");
11398 return None;
11399 }
11400 if !metal_native {
11403 let kept = self.dflash_taps.lock().unwrap().pop();
11404 if let (Some((pos0, rows)), Some(d)) = (kept, self.dflash.as_mut()) {
11405 let tw = d.g.tap_width();
11406 if pos0 == next_pos && rows.len() >= (a + 1) * tw {
11407 d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11408 }
11409 }
11410 }
11411 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
11412 self.clear_sequence_state();
11417 self.graph_failed
11418 .store(true, std::sync::atomic::Ordering::Relaxed);
11419 self.cancel
11420 .store(true, std::sync::atomic::Ordering::Relaxed);
11421 tracing::error!("trunk graph KV rewind failed after speculative verify");
11422 return None;
11423 }
11424 *accepted += a;
11425 if !metal_native && mtp_steps > 0 {
11436 m.kv.truncate_last(mtp_steps - 1);
11438 }
11439 spec_stamp("c.trunc");
11440 if !metal_native
11443 && self.mtp_graph_mode == Some(true)
11444 && (mtp_steps > 0 || warm_n > 0)
11445 && !self.rewind_mtp_graph_mirror(warm_pos)
11446 {
11447 self.clear_sequence_state();
11451 self.graph_failed
11452 .store(true, std::sync::atomic::Ordering::Relaxed);
11453 self.cancel
11454 .store(true, std::sync::atomic::Ordering::Relaxed);
11455 tracing::error!("MTP graph mirror rewind failed after verify commit");
11456 return None;
11457 }
11458 if !warm_off && warm_n > 0 {
11459 let mut warmed = false;
11462 #[cfg(target_os = "macos")]
11463 if metal_native && self.mtp_graph_mode == Some(true) {
11464 warmed = match warm_pending.take() {
11468 Some(p) => self.mtp_warm_batch_finish(m, p),
11469 None => false,
11470 };
11471 if !warmed {
11472 warmed = true;
11473 for j in 0..warm_n {
11474 let (row, tok) = warm_pair(j);
11475 let row = row.to_vec();
11476 if self
11477 .mtp_step_metal(m, &row, tok, warm_pos + j, false)
11478 .is_none()
11479 {
11480 warmed = false;
11481 break;
11482 }
11483 }
11484 }
11485 }
11486 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
11487 let rows: Vec<(Vec<f32>, u32)> = (0..warm_n)
11488 .map(|j| {
11489 let (row, tok) = warm_pair(j);
11490 (row.to_vec(), tok)
11491 })
11492 .collect();
11493 let pairs: Vec<(&[f32], u32)> = rows.iter().map(|(r, t)| (r.as_slice(), *t)).collect();
11494 match self.mtp_warm_prefill_pairs(m, &pairs, warm_pos) {
11495 Ok(()) => warmed = true,
11496 Err(err) => {
11497 tracing::error!("{err}");
11503 self.clear_sequence_state();
11504 self.graph_failed
11505 .store(true, std::sync::atomic::Ordering::Relaxed);
11506 self.cancel
11507 .store(true, std::sync::atomic::Ordering::Relaxed);
11508 return None;
11509 }
11510 }
11511 }
11512 if !warmed {
11513 for j in 0..warm_n {
11514 let (row, tok) = warm_pair(j);
11515 let row = row.to_vec();
11516 self.mtp_warm(m, &row, tok, warm_pos + j);
11517 }
11518 }
11519 }
11520 spec_stamp("c.warm");
11524 if let Some(c) = forced {
11525 self.spec_forced = Some(c);
11526 self.graph_logits = None;
11527 } else if greedy_dev && logits.is_empty() {
11528 self.spec_forced = Some(ids[a]);
11531 self.graph_logits = None;
11532 } else {
11533 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
11534 row.resize(self.vocab_size, 0.0);
11535 if let Some(c) = self.final_softcap {
11536 for l in row.iter_mut() {
11537 *l = c * (*l / c).tanh();
11538 }
11539 }
11540 self.graph_logits = Some(row);
11541 }
11542 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
11543 spec_stamp("c.row");
11544 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11550 let end = subs();
11551 eprintln!(
11552 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
11553 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
11554 t_draft.as_secs_f64() * 1e3,
11555 sub_draft - sub0,
11556 (t_verify - t_draft).as_secs_f64() * 1e3,
11557 sub_verify - sub_draft,
11558 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
11559 end - sub_verify,
11560 self.draft_full_streak,
11561 );
11562 }
11563 if k_spec > k_mtp {
11568 let extra = a.saturating_sub(k_mtp) as f32;
11570 self.spec_lookup_gain += 0.2 * (extra - self.spec_lookup_gain);
11571 self.spec_lookup_wide = a == k_spec;
11573 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11574 eprintln!(
11575 "spec-lookup: {} drafts from the context, {extra} held (average {:.2})",
11576 k_spec - k_mtp,
11577 self.spec_lookup_gain
11578 );
11579 }
11580 if self.spec_lookup_gain < LOOKUP_PAYS {
11581 self.spec_lookup_rest = 24;
11582 self.spec_lookup_gain = 1.6;
11583 }
11584 }
11585 if metal_small && !k_capped {
11586 if dflash_round {
11590 self.dflash_turn_sum.0 += (a + 1) as f32;
11591 self.dflash_turn_sum.1 += METAL_ROUND_COST[k_spec.clamp(1, 7) - 1] + DFLASH_ROUND_DRAFT;
11592 }
11593 let hold = if dflash_round { &mut self.dflash_hold } else { &mut self.spec_hold };
11594 spec_hold_update(hold, k_mtp, a.min(k_mtp));
11595 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11596 let next = metal_spec_plan(hold, if dflash_round { DFLASH_ROUND_DRAFT } else { 0.0 }).0;
11597 if next != k_mtp && k_env.is_none() {
11598 eprintln!("spec-k: {k_mtp} → {next} (a draft holds after the last {hold:.2?})");
11599 }
11600 }
11601 }
11602
11603 if k_env.is_none() && !metal_native && !k_capped {
11604 let lookup_round = k_spec > k_mtp;
11608 let (k_spec, a) = (k_mtp, a.min(k_mtp));
11609 if cuda_rate && dflash_round && !k_capped {
11613 self.dflash_turn_sum.0 += (a + 1) as f32;
11614 self.dflash_turn_sum.1 += CUDA_ROUND_COST[k_spec.clamp(1, 7) - 1] + CUDA_BLOCK_DRAFT;
11615 spec_hold_update(&mut self.dflash_hold, k_spec, a.min(k_spec));
11616 }
11617 let (ewma, k_min) = if dflash_round { (&mut self.dflash_acc_ewma, 3) } else { (&mut self.spec_acc_ewma, 2) };
11618 let f = a as f32 / k_spec.max(1) as f32;
11619 *ewma += 0.2 * (f - *ewma);
11620 let mut k_next = k_spec;
11621 if *ewma >= 0.75 && k_spec < k_max {
11622 k_next = k_spec + 1;
11623 } else if *ewma < 0.4 && k_spec > k_min {
11624 k_next = k_spec - 1;
11625 }
11626 let mut planned = false;
11633 if !dflash_round && !lookup_round {
11634 spec_hold_update(&mut self.spec_hold, k_spec, a);
11635 let d = k_spec.clamp(1, 7) - 1;
11636 let w = t_round.elapsed().as_secs_f32() * 1e3;
11637 match self.spec_wall_n[d] {
11638 0 => self.spec_wall_n[d] = 1,
11639 1 => (self.spec_wall[d], self.spec_wall_n[d]) = (w, 2),
11640 _ => self.spec_wall[d] += 0.2 * (w - self.spec_wall[d]),
11641 }
11642 if let Some(k_plan) = wgpu_spec_depth(&self.spec_hold, &self.spec_wall, k_spec, k_min, k_max) {
11643 planned = true;
11644 k_next = if self.spec_k_dwell > 0 {
11645 self.spec_k_dwell -= 1;
11646 k_spec
11647 } else {
11648 k_plan
11649 };
11650 if k_next != k_spec {
11651 self.spec_k_dwell = 6;
11652 }
11653 }
11654 }
11655 if k_next != k_spec {
11656 *ewma = 0.6;
11657 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
11658 if planned {
11659 eprintln!(
11660 "spec-k: {k_spec} → {k_next} (a round costs {:.0?} ms, a draft holds after the last {:.2?})",
11661 self.spec_wall, self.spec_hold
11662 );
11663 } else {
11664 eprintln!("spec-k: {k_spec} → {k_next}");
11665 }
11666 }
11667 }
11668 if dflash_round {
11669 self.dflash_k_adapt = Some(k_next);
11670 } else {
11671 self.spec_k_adapt = Some(k_next);
11672 }
11673 }
11674 spec_stamp("end");
11675 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
11676 }
11677
11678 fn dflash_draft(&mut self, t_next: u32, next_pos: usize, k: usize) -> Vec<u32> {
11683 let Some(mut d) = self.dflash.take() else {
11684 return Vec::new();
11685 };
11686 let (hs, rows) = (self.hidden_size, d.g.block);
11687 let mut h = vec![0f32; rows * hs];
11688 h[..hs].copy_from_slice(&self.embed_single(t_next));
11689 let mask = self.embed_single(d.g.mask_token);
11690 for r in 1..rows {
11691 h[r * hs..(r + 1) * hs].copy_from_slice(&mask);
11692 }
11693 if self.embed_multiplier != 1.0 && std::env::var("CMF_DFLASH_EMBED").as_deref() != Ok("scaled") {
11697 let inv = 1.0 / self.embed_multiplier;
11698 h.iter_mut().for_each(|v| *v *= inv);
11699 }
11700 let pool = self.pool.as_deref();
11701 let t0 = std::time::Instant::now();
11702 d.settle(pool);
11703 spec_stamp("d.ctx");
11704 let t_ctx = t0.elapsed();
11705 #[cfg(feature = "gpu")]
11708 if let Some((logits, hp)) = d.block_dev(next_pos, &h, k.min(rows - 1), &self.weights.lm_head) {
11709 spec_stamp("d.block");
11710 let t_block = t0.elapsed();
11711 let n = k.min(rows - 1);
11712 let (mut ids, _) = d.select_with(&hp, &logits, t_next, n);
11713 ids.truncate(k);
11714 if std::env::var_os("CMF_DFLASH_DEV_CHECK").is_some() {
11717 let out = d.block(next_pos, h.clone(), pool);
11718 let head = &self.weights.lm_head;
11719 let mut hl = vec![0f32; n * head.rows()];
11720 head.matmat(&out[hs..(n + 1) * hs], n, &mut hl, pool);
11721 let width = logits.len() / n.max(1);
11723 let hl: Vec<f32> = (0..n).flat_map(|j| hl[j * head.rows()..j * head.rows() + width].to_vec()).collect();
11724 let (hids, _) = d.select(&out[hs..(n + 1) * hs], &hl, t_next);
11725 let (mut dmax, mut ref_max, mut dsum, mut cnt) = (0f32, 0f32, 0f64, 0usize);
11726 for (a, b) in logits.iter().zip(&hl) {
11727 dmax = dmax.max((a - b).abs());
11728 ref_max = ref_max.max(b.abs());
11729 dsum += ((a - b) * (a - b)) as f64;
11730 cnt += 1;
11731 }
11732 let arg = |v: &[f32]| (0..n).map(|j| sampler::argmax(&v[j * width..(j + 1) * width])).collect::<Vec<u32>>();
11733 eprintln!(
11734 "dflash-dev-check: logits max|Δ| {dmax:.4} of max {ref_max:.2}, rms Δ {:.5} | argmax device {:?} host {:?} | drafts device {:?} host {:?}",
11735 (dsum / cnt.max(1) as f64).sqrt(),
11736 arg(&logits),
11737 arg(&hl),
11738 ids,
11739 &hids[..k.min(hids.len())]
11740 );
11741 }
11742 if self.dflash_solo && spec_time_level() > 0 {
11743 eprintln!(
11744 "solo-draft: context {:.1} ms | device block + head {:.1} ms | selector {:.1} ms",
11745 t_ctx.as_secs_f64() * 1e3,
11746 (t_block - t_ctx).as_secs_f64() * 1e3,
11747 (t0.elapsed() - t_block).as_secs_f64() * 1e3
11748 );
11749 }
11750 spec_stamp("d.head");
11751 self.dflash = Some(d);
11752 return ids;
11753 }
11754 let out = d.block(next_pos, h, pool);
11755 spec_stamp("d.block");
11756 let t_block = t0.elapsed();
11757 let (n, head) = (rows - 1, &self.weights.lm_head);
11762 let width = if self.dflash_solo { head.rows() } else { self.draft_head_rows(head.rows()) };
11763 let mut logits = vec![0f32; n * width];
11764 let narrow = head.mapped_q4tp().is_some_and(|(m, idx)| {
11766 crate::gpu::q4tp_rows8_many(m, &[(idx, head.rows(), head.cols(), width)], &out[hs..], n, &mut [&mut logits[..]])
11767 });
11768 if !narrow {
11769 logits = vec![0f32; n * head.rows()];
11770 if !head.device_matmat(&out[hs..], n, &mut logits) {
11771 head.matmat(&out[hs..], n, &mut logits, pool);
11772 }
11773 }
11774 spec_stamp("d.head");
11775 let t_head = t0.elapsed();
11776 let (mut ids, _) = d.select(&out[hs..], &logits, t_next);
11777 ids.truncate(k);
11778 if self.dflash_solo && spec_time_level() > 0 {
11779 eprintln!(
11780 "solo-draft: context {:.1} ms | block {:.1} ms | head {:.1} ms | selector {:.1} ms",
11781 t_ctx.as_secs_f64() * 1e3,
11782 (t_block - t_ctx).as_secs_f64() * 1e3,
11783 (t_head - t_block).as_secs_f64() * 1e3,
11784 (t0.elapsed() - t_head).as_secs_f64() * 1e3
11785 );
11786 }
11787 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() && d.next_position() != next_pos {
11788 eprintln!("dflash: the context ends at {} for a block at {next_pos}", d.next_position());
11789 }
11790 self.dflash = Some(d);
11791 ids
11792 }
11793
11794 #[cfg(feature = "gpu")]
11801 fn solo_spec_round(&mut self, t_next: u32, next_pos: usize, all_ids: &[u32], k: usize) -> Option<SoloRound> {
11802 self.dflash_taps_absorb();
11803 let t0 = std::time::Instant::now();
11804 let drafts: Vec<u32> = match solo_script() {
11805 Some(script) => script.iter().skip(all_ids.len()).take(k).copied().collect(),
11806 None => {
11807 if self.dflash.as_ref().is_none_or(|d| d.next_position() != next_pos) {
11808 return None;
11809 }
11810 self.dflash_draft(t_next, next_pos, k)
11811 }
11812 };
11813 if drafts.is_empty() {
11814 return None;
11815 }
11816 let t_draft = t0.elapsed();
11817 let (b, hs) = (drafts.len() + 1, self.hidden_size);
11818 let mut hiddens = vec![0f32; b * hs];
11819 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
11820 hiddens[i * hs..(i + 1) * hs].copy_from_slice(&self.embed_single(t));
11821 }
11822 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
11823 let (lm_gw, lm_rows) = {
11824 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
11825 (
11826 crate::gpu::GraphW { idx: i, kind, row_scale: rs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false },
11827 self.weights.lm_head.rows(),
11828 )
11829 };
11830 let mut logits = Vec::new();
11831 let final_norm = self.weights.final_norm.clone();
11832 let outcome = self.try_batch_graph_wgpu(
11833 &mut hiddens,
11834 &positions,
11835 b,
11836 Some(crate::gpu::SpecTail { lm: lm_gw, lm_rows, final_norm: &final_norm, logits_out: &mut logits, pick: None }),
11837 );
11838 let fail = |p: &mut Self, what: &str| {
11839 p.clear_sequence_state();
11840 p.graph_failed.store(true, std::sync::atomic::Ordering::Relaxed);
11841 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
11842 tracing::error!("{what}");
11843 };
11844 match outcome {
11845 crate::gpu::BatchGraphOutcome::Completed if logits.len() >= b * lm_rows => {}
11846 crate::gpu::BatchGraphOutcome::Declined => return None,
11847 _ => {
11848 fail(self, "the verify of a speculative round failed after admission");
11849 return None;
11850 }
11851 }
11852 let t_verify = t0.elapsed();
11853 let ids: Vec<u32> = (0..b).map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows])).collect();
11855 let mut a = 0usize;
11856 while a < drafts.len() && ids[a] == drafts[a] {
11857 a += 1;
11858 }
11859 if std::env::var_os("CMF_SPEC_DBG").is_some() {
11860 eprintln!(
11861 "solo-dbg: tip {t_next} at {next_pos} | context {:?} | drafts {drafts:?} | the trunk's {ids:?} | kept {a}",
11862 self.dflash.as_ref().map(|d| d.span())
11863 );
11864 }
11865 if self.o1_active() && !crate::gpu::o1_spec_commit(self.graph_kv_id, next_pos, a + 1) {
11866 fail(self, "far field commit failed after a speculative round");
11867 return None;
11868 }
11869 if a + 1 < b {
11872 let gdn = self.graph_gdn_layer_count();
11873 if gdn > 0 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, gdn) {
11874 fail(self, "GDN restore failed after a speculative round");
11875 return None;
11876 }
11877 }
11878 let kept = self.dflash_taps.lock().unwrap().pop();
11880 if let (Some((pos0, rows)), Some(d)) = (kept, self.dflash.as_mut()) {
11881 let tw = d.g.tap_width();
11882 if pos0 == next_pos && rows.len() >= (a + 1) * tw {
11883 d.queue_context(pos0, &rows[..(a + 1) * tw], self.pool.as_deref());
11884 }
11885 }
11886 if !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
11887 fail(self, "trunk graph KV rewind failed after a speculative round");
11888 return None;
11889 }
11890 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
11891 row.resize(self.vocab_size, 0.0);
11892 if let Some(c) = self.final_softcap {
11893 for l in row.iter_mut() {
11894 *l = c * (*l / c).tanh();
11895 }
11896 }
11897 if spec_time_level() > 0 {
11898 eprintln!(
11899 "solo-round: draft {:.1} ms | verify {:.1} ms | commit {:.1} ms | kept {a} of {}",
11900 t_draft.as_secs_f64() * 1e3,
11901 (t_verify - t_draft).as_secs_f64() * 1e3,
11902 (t0.elapsed() - t_verify).as_secs_f64() * 1e3,
11903 drafts.len()
11904 );
11905 }
11906 Some(SoloRound { accepted: drafts[..a].to_vec(), drafted: drafts.len(), hidden: hiddens[a * hs..(a + 1) * hs].to_vec(), logits: row })
11907 }
11908
11909 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
11918 if !self.pair_supported() {
11919 return (0.0, 0.0);
11920 }
11921 if self.gdn_cfg.is_some()
11929 && self.graph_prefill_preferred()
11930 && self.automatic_gpu_prefix().is_some()
11931 {
11932 tracing::info!(
11933 "pair micro-benchmark skipped: GDN device-prefix path has no production pair route"
11934 );
11935 return (0.0, 0.0);
11936 }
11937 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
11944 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
11945 let emb1 = self.embed_single(1);
11946 let emb2 = self.embed_single(2);
11947 let pos = self.kv_cache.seq_len();
11948
11949 let t0 = std::time::Instant::now();
11950 for _ in 0..iters {
11951 let _ = self.forward_layers(&emb1, pos, None);
11952 let _ = self.forward_layers(&emb2, pos + 1, None);
11953 for l in &mut self.kv_cache.layers {
11954 l.truncate_last(2);
11955 }
11956 }
11957 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
11958
11959 let t1 = std::time::Instant::now();
11960 for _ in 0..iters {
11961 let _ = self.forward_pair(&emb1, &emb2, pos);
11962 for l in &mut self.kv_cache.layers {
11963 l.truncate_last(2);
11964 }
11965 }
11966 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
11967 match graph_env {
11968 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
11969 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
11970 }
11971 (singles_ms, pair_ms)
11972 }
11973
11974 fn pair_supported(&self) -> bool {
11982 !self.weights.layers.is_empty()
11989 && !self.per_token_stack()
11990 && !self
11991 .weights
11992 .layers
11993 .iter()
11994 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
11995 }
11996
11997 fn forward_pair(
11998 &mut self,
11999 emb1: &[f32],
12000 emb2: &[f32],
12001 position: usize,
12002 ) -> (Vec<f32>, Vec<f32>) {
12003 self.mimo_moe_prepare();
12006 let mut h1 = emb1.to_vec();
12007 let mut h2 = emb2.to_vec();
12008 if self.value_blend.is_some() {
12009 self.vres_e0.clear();
12010 self.vres_e0.extend_from_slice(emb1);
12011 self.vres_e0.extend_from_slice(emb2);
12012 }
12013 let (_nkv, _hd, hs, _rd, eps) = (
12014 self.num_kv_heads,
12015 self.head_dim,
12016 self.hidden_size,
12017 self.rotary_dim,
12018 self.rms_eps,
12019 );
12020 let pool = self.pool.clone();
12021 let automatic_gpu_prefix = self.automatic_gpu_prefix();
12028
12029 for li in 0..self.num_layers {
12030 let _capacity_tail = automatic_gpu_prefix
12031 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, false))
12032 .map(|_| crate::gpu::enter_cpu_scope());
12033 crate::gpu::set_layer(li as i64);
12036 let lw = &self.weights.layers[self.phys_layer(li)];
12037 inference::rms_norm_into(
12040 &h1,
12041 &lw.input_norm,
12042 self.rms_eps,
12043 self.norm_style,
12044 &mut self.ws.n1,
12045 );
12046 inference::rms_norm_into(
12047 &h2,
12048 &lw.input_norm,
12049 self.rms_eps,
12050 self.norm_style,
12051 &mut self.ws.n2,
12052 );
12053 let vres = match self.layer_value_blend(li) {
12055 Some(blend) => {
12056 let mut second = Vec::new();
12057 vres_lerp(&self.vres_e0[..hs], &self.ws.n1, blend, &mut self.vres_in);
12058 vres_lerp(&self.vres_e0[hs..], &self.ws.n2, blend, &mut second);
12059 self.vres_in.extend_from_slice(&second);
12060 true
12061 }
12062 None => false,
12063 };
12064
12065 let (a1, a2) = match &lw.attn {
12066 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
12067 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
12068 AttnKind::Bounded(w) => {
12069 let rope = self
12072 .bounded_rope
12073 .clone()
12074 .expect("bounded layer without an installed rotation table");
12075 let cfg = crate::bounded::BoundedAttnCfg {
12076 num_heads: self.num_heads,
12077 num_kv_heads: self.num_kv_heads,
12078 head_dim: self.head_dim,
12079 hidden_size: hs,
12080 scale: self.attn_scale,
12081 rope: &rope,
12082 pool: pool.as_deref(),
12083 };
12084 let a1 = crate::bounded::bounded_attention(
12085 &self.ws.n1,
12086 w,
12087 &mut self.kv_cache.layers[li],
12088 &cfg,
12089 );
12090 let a2 = crate::bounded::bounded_attention(
12091 &self.ws.n2,
12092 w,
12093 &mut self.kv_cache.layers[li],
12094 &cfg,
12095 );
12096 (a1, a2)
12097 }
12098 AttnKind::Linear(w) => {
12099 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
12100 let layer = &mut self.kv_cache.layers[li];
12101 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12102 vmf_phase_pair(
12103 &self.ws.n1,
12104 &self.ws.n2,
12105 w,
12106 &cfg,
12107 state,
12108 scratch,
12109 self.pool.as_deref(),
12110 )
12111 }
12112 AttnKind::LinearGdn(w) => {
12113 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
12114 let layer = &mut self.kv_cache.layers[li];
12115 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12116 gdn_pair(
12117 &self.ws.n1,
12118 &self.ws.n2,
12119 w,
12120 &cfg,
12121 state,
12122 scratch,
12123 self.pool.as_deref(),
12124 )
12125 }
12126 AttnKind::ShortConv(w) => {
12127 let cfg = self
12128 .short_conv_cfg
12129 .expect("short-conv layer without short_conv_cfg");
12130 let layer = &mut self.kv_cache.layers[li];
12131 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
12132 short_conv_pair(
12133 &self.ws.n1,
12134 &self.ws.n2,
12135 w,
12136 &cfg,
12137 state,
12138 scratch,
12139 self.pool.as_deref(),
12140 )
12141 }
12142 AttnKind::Full {
12143 wq,
12144 wk,
12145 wv,
12146 wo,
12147 q_norm,
12148 k_norm,
12149 output_gate,
12150 softplus_gate,
12151 bias,
12152 } => {
12153 let inv_freq_l = self.layer_inv_freq(li);
12154 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
12155 let cfg = QwenAttnCfg {
12156 mrope: None,
12157 num_heads: self.layer_num_heads(li),
12158 num_kv_heads: nkv_l,
12159 head_dim: hd_l,
12160 hidden_size: hs,
12161 position,
12162 inv_freq: &inv_freq_l,
12163 rotary_dim: rd_l,
12164 scale: self.attn_scale,
12165 softcap: self.attn_softcap,
12166 window: self.layer_window(li),
12167 v_norm: self.attn_v_norm,
12168 qk_norm_after_rope: self.qk_norm_after_rope,
12169 gate_sigmoid: self.proj_gate_sigmoid,
12170 q_norm: q_norm.as_deref(),
12171 k_norm: k_norm.as_deref(),
12172 output_gate: *output_gate,
12173 softplus_gate: softplus_gate
12174 .as_ref()
12175 .map(|(gate, per_head)| (gate, *per_head)),
12176 rope_scale: self.layer_rope_scale(li),
12177 bias: bias
12178 .as_ref()
12179 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
12180 rms_eps: eps,
12181 norm_style: self.norm_style,
12182 pool: pool.as_deref(),
12183 v_head_dim: self.layer_v_dim(li),
12184 value_in: vres.then(|| self.vres_in.as_slice()),
12185 };
12186 attention::qwen_attention_pair(
12187 &self.ws.n1,
12188 &self.ws.n2,
12189 wq,
12190 wk,
12191 wv,
12192 wo,
12193 &mut self.kv_cache.layers[li],
12194 &cfg,
12195 )
12196 }
12197 };
12198 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
12199 Some(w) => (
12200 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
12201 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
12202 ),
12203 None => (a1, a2),
12204 };
12205 for i in 0..self.hidden_size {
12206 h1[i] += a1[i];
12207 h2[i] += a2[i];
12208 }
12209 let (mut a1, mut a2) = (a1, a2);
12210 attention::recycle_buf(&mut a1);
12211 attention::recycle_buf(&mut a2);
12212
12213 let lw = &self.weights.layers[self.phys_layer(li)];
12214 inference::rms_norm_into(
12215 &h1,
12216 &lw.post_norm,
12217 self.rms_eps,
12218 self.norm_style,
12219 &mut self.ws.p1,
12220 );
12221 inference::rms_norm_into(
12222 &h2,
12223 &lw.post_norm,
12224 self.rms_eps,
12225 self.norm_style,
12226 &mut self.ws.p2,
12227 );
12228 let (f1, f2) = match &lw.ffn {
12229 FfnKind::DenseMoe(dm) => (
12232 dense_moe_ffn(
12233 dm,
12234 &self.ws.p1,
12235 &h1,
12236 self.rms_eps,
12237 self.norm_style,
12238 self.pool.as_deref(),
12239 ),
12240 dense_moe_ffn(
12241 dm,
12242 &self.ws.p2,
12243 &h2,
12244 self.rms_eps,
12245 self.norm_style,
12246 self.pool.as_deref(),
12247 ),
12248 ),
12249 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
12250 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
12251 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
12252 ),
12253 _ => ffn_forward_pair(
12254 &lw.ffn,
12255 &self.ws.p1,
12256 &self.ws.p2,
12257 self.pool.as_deref(),
12258 None,
12259 ),
12260 };
12261 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
12262 Some(w) => (
12263 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
12264 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
12265 ),
12266 None => (f1, f2),
12267 };
12268 for i in 0..self.hidden_size {
12269 h1[i] += f1[i];
12270 h2[i] += f2[i];
12271 }
12272 let (mut f1, mut f2) = (f1, f2);
12273 attention::recycle_buf(&mut f1);
12274 attention::recycle_buf(&mut f2);
12275 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
12276 for i in 0..self.hidden_size {
12277 h1[i] *= sc;
12278 h2[i] *= sc;
12279 }
12280 }
12281 if self.is_loop_end(li) && li + 1 < self.num_layers {
12283 h1 = inference::rms_norm(
12284 &h1,
12285 &self.weights.final_norm,
12286 self.rms_eps,
12287 self.norm_style,
12288 );
12289 h2 = inference::rms_norm(
12290 &h2,
12291 &self.weights.final_norm,
12292 self.rms_eps,
12293 self.norm_style,
12294 );
12295 }
12296 }
12297 crate::gpu::set_layer(-1); if self.o1_active() {
12304 self.commit_linear_scratch();
12305 }
12306 self.o1_progress();
12307 self.swa_trim_tails();
12308 (h1, h2)
12309 }
12310
12311 fn commit_linear_scratch(&mut self) {
12313 for layer in &mut self.kv_cache.layers {
12314 if !layer.linear_scratch.is_empty() {
12315 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
12316 layer.linear_scratch.clear();
12317 }
12318 }
12319 }
12320
12321 pub fn encode_hidden_states(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12329 #[cfg(target_os = "macos")]
12330 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12331 if ids.is_empty() {
12332 return Err("empty id sequence".to_string());
12333 }
12334
12335 self.nll_begin()?;
12336 let result = (|| {
12337 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
12338 self.nll_check_graph("encoder prefill", 0)?;
12339 let expected = ids.len() * self.hidden_size;
12340 if hidden.len() != expected {
12341 return Err(format!(
12342 "encoder prefill returned {} values for {} rows of hidden size {}",
12343 hidden.len(),
12344 ids.len(),
12345 self.hidden_size
12346 ));
12347 }
12348
12349 let mut out = vec![0.0f32; expected];
12350 for (src, dst) in hidden
12351 .chunks_exact(self.hidden_size)
12352 .zip(out.chunks_exact_mut(self.hidden_size))
12353 {
12354 inference::rms_norm_into(
12355 src,
12356 &self.weights.final_norm,
12357 self.rms_eps,
12358 self.norm_style,
12359 dst,
12360 );
12361 }
12362 Ok(out)
12363 })();
12364 self.nll_end();
12365 result
12366 }
12367
12368 pub fn final_hidden_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12373 #[cfg(target_os = "macos")]
12374 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12375 if ids.is_empty() {
12376 return Err("empty id sequence".to_string());
12377 }
12378 self.nll_begin()?;
12379 let result = (|| {
12380 let hidden = self.prefill_input_rows(PrefillIn::Ids(ids), 0, None)?;
12381 self.nll_check_graph("hidden prefill", 0)?;
12382 if hidden.len() != ids.len() * self.hidden_size {
12383 return Err(format!("hidden prefill returned {} values for {} rows", hidden.len(), ids.len()));
12384 }
12385 Ok(hidden)
12386 })();
12387 self.nll_end();
12388 result
12389 }
12390
12391 pub fn forward_ids(
12394 &mut self,
12395 ids: &[u32],
12396 task_mask: Option<&TaskMask>,
12397 ) -> Result<Vec<f32>, String> {
12398 #[cfg(target_os = "macos")]
12399 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12400 if ids.is_empty() {
12401 return Err("empty id sequence".to_string());
12402 }
12403 self.clear_sequence_state();
12404 self.check_forward_graph("forward_ids setup", 0)?;
12405 if task_mask.is_none() {
12406 self.o1_begin();
12407 }
12408 let mut hidden = vec![0.0f32; self.hidden_size];
12409 let mut pos = 0usize;
12410 if self.dory.is_some() {
12411 return Ok(self.dory_forward(ids, 0, crate::dory::Want::Last));
12412 }
12413 if let Some(b) = &mut self.dsv41 {
12414 let pool = self.pool.clone();
12415 let mut logits = Vec::new();
12416 crate::dsv41::forward_chunk(
12417 &b.0,
12418 &b.1,
12419 &b.2,
12420 &mut b.3,
12421 ids,
12422 0,
12423 pool.as_deref(),
12424 &mut logits,
12425 );
12426 if let Err(err) = self.o1_seal_checked() {
12427 self.clear_sequence_state();
12428 return Err(err);
12429 }
12430 return Ok(logits);
12431 }
12432 #[cfg(not(target_os = "macos"))]
12443 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
12444 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
12445 Ok(r) => r,
12446 Err(e) => {
12447 self.clear_sequence_state();
12448 return Err(e);
12449 }
12450 };
12451 if n > 0 {
12452 hidden.copy_from_slice(&last);
12453 pos = n;
12454 }
12455 }
12456 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
12457 let chunk = self.prefill_chunk();
12461 let hs = self.hidden_size;
12462 while pos < ids.len() {
12463 let end = (pos + chunk).min(ids.len());
12464 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
12465 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
12466 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
12467 pos = end;
12468 }
12469 }
12470 if task_mask.is_none()
12479 && !self.graph_prefill_preferred()
12480 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
12481 && self.pair_supported()
12482 {
12483 while pos + 1 < ids.len() {
12484 let e1 = self.embed_single(ids[pos]);
12485 let e2 = self.embed_single(ids[pos + 1]);
12486 let (_, h2) = self.forward_pair(&e1, &e2, pos);
12487 self.check_forward_graph("forward_ids pair", pos + 1)?;
12488 self.commit_linear_scratch();
12489 hidden = h2;
12490 pos += 2;
12491 }
12492 }
12493 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
12496 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
12497 self.graph_logits = Some(lg);
12498 hidden = vec![0.0; self.hidden_size];
12499 pos = ids.len();
12500 }
12501 }
12502 while pos < ids.len() {
12503 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
12504 self.check_forward_graph("forward_ids", pos)?;
12505 pos += 1;
12506 }
12507 if let Some(logits) = self.graph_logits.take() {
12508 if let Err(err) = self.o1_seal_checked() {
12512 self.clear_sequence_state();
12513 return Err(err);
12514 }
12515 return Ok(logits);
12516 }
12517 if let Err(err) = self.o1_seal_checked() {
12521 self.clear_sequence_state();
12522 return Err(err);
12523 }
12524 let normed = inference::rms_norm(
12525 &hidden,
12526 &self.weights.final_norm,
12527 self.rms_eps,
12528 self.norm_style,
12529 );
12530 Ok(self.lm_head_forward(&normed))
12531 }
12532
12533 #[doc(hidden)]
12537 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
12538 #[cfg(target_os = "macos")]
12539 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12540 if ids.is_empty() {
12541 return Err("empty id sequence".to_string());
12542 }
12543 self.clear_sequence_state();
12544 self.dsv41
12545 .as_ref()
12546 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
12547 self.o1_begin();
12548 let rows = {
12549 let pool = self.pool.clone();
12550 let b = self
12551 .dsv41
12552 .as_mut()
12553 .expect("dsv41 checked above; state cannot change during forward");
12554 let mut rows = Vec::with_capacity(ids.len());
12555 for (position, &id) in ids.iter().enumerate() {
12556 let mut logits = Vec::new();
12557 crate::dsv41::forward_token(
12558 &b.0,
12559 &b.1,
12560 &b.2,
12561 &mut b.3,
12562 id,
12563 position,
12564 pool.as_deref(),
12565 &mut logits,
12566 );
12567 rows.push(logits);
12568 }
12569 rows
12570 };
12571 self.o1_seal();
12572 Ok(rows)
12573 }
12574
12575 pub fn dsv41_moe_capture(
12579 &mut self,
12580 ids: &[u32],
12581 lo: usize,
12582 hi: usize,
12583 ) -> Result<crate::dsv41::Dsv41MoeCapture, String> {
12584 #[cfg(target_os = "macos")]
12585 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
12586 if ids.is_empty() {
12587 return Err("empty id sequence".to_string());
12588 }
12589 self.dsv41
12590 .as_ref()
12591 .ok_or_else(|| "not a DeepSeek-V4.1 model".to_string())?;
12592 self.clear_sequence_state();
12593 let res = {
12594 let pool = self.pool.clone();
12595 let b = self.dsv41.as_mut().expect("dsv41 checked above");
12596 crate::dsv41::capture_window(&b.0, &b.1, &b.2, &mut b.3, ids, lo, hi, pool.as_deref())
12597 };
12598 self.clear_sequence_state();
12599 res
12600 }
12601
12602 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
12609 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
12610 Ok((nll / cnt.max(1) as f64).exp())
12611 }
12612
12613 pub fn calib_forward(&mut self, ids: &[u32], upto: usize, chunk: usize) -> Result<(), String> {
12620 if !self.can_prefill_batched() {
12621 return self.nll_ids_from(ids, 0).map(|_| ());
12622 }
12623 self.nll_begin()?;
12624 let result: Result<(), String> = (|| {
12625 let chunk = chunk.max(1);
12626 let mut pos = 0usize;
12627 while pos < ids.len() {
12628 let end = (pos + chunk).min(ids.len());
12629 let _ = self.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, upto);
12630 self.nll_check_graph("calibration prefill", pos)?;
12631 pos = end;
12632 }
12633 Ok(())
12634 })();
12635 self.nll_end();
12636 result
12637 }
12638
12639 pub fn calib_window_span(
12648 &mut self,
12649 ids: &[u32],
12650 input: Option<&[f32]>,
12651 from: usize,
12652 mid: usize,
12653 upto: usize,
12654 ) -> Result<Vec<f32>, String> {
12655 if !self.can_prefill_batched() {
12656 return Err("calib_window_span: this architecture has no batched prefill".into());
12657 }
12658 self.nll_begin()?;
12659 let result: Result<Vec<f32>, String> = (|| {
12660 let h = if mid > from {
12661 let inp = match input {
12662 Some(h) => PrefillIn::Hidden(h),
12663 None => PrefillIn::Ids(ids),
12664 };
12665 let h = self.prefill_batch_span(inp, 0, None, from, mid);
12666 self.nll_check_graph("calibration span", 0)?;
12667 h
12668 } else {
12669 input.map(<[f32]>::to_vec).unwrap_or_default()
12670 };
12671 if upto > mid {
12672 let inp = if mid > from || input.is_some() {
12673 PrefillIn::Hidden(&h)
12674 } else {
12675 PrefillIn::Ids(ids)
12676 };
12677 let _ = self.prefill_batch_span(inp, 0, None, mid, upto);
12678 self.nll_check_graph("calibration span", 0)?;
12679 }
12680 Ok(h)
12681 })();
12682 self.nll_end();
12683 result
12684 }
12685
12686 #[cfg(feature = "gpu")]
12690 pub fn qwen4_capture(
12691 &mut self,
12692 ids: &[u32],
12693 lo: usize,
12694 hi: usize,
12695 ) -> Result<Vec<Vec<f32>>, String> {
12696 let b = self.qwen4_exp.as_mut().ok_or("not a qwen4_exp model")?;
12697 crate::qwen4_exp::capture_window(
12698 &b.0,
12699 &b.1,
12700 &b.2,
12701 &mut b.3,
12702 ids,
12703 lo,
12704 hi,
12705 &self.inv_freq,
12706 self.pool.as_deref(),
12707 )
12708 }
12709
12710 #[cfg(feature = "gpu")]
12716 pub fn moe_capture(&mut self, ids: &[u32], lo: usize, hi: usize) -> Result<Vec<Vec<f32>>, String> {
12717 use crate::gpu_wgpu::clover as cap;
12718 let (hs, hi) = (self.hidden_size, hi.min(self.num_layers));
12719 let ne = (lo..hi)
12720 .find_map(|li| match &self.weights.layers[self.phys_layer(li)].ffn {
12721 FfnKind::Moe(m) => Some(m.experts.len()),
12722 _ => None,
12723 })
12724 .ok_or("no MoE layer in the capture range")?;
12725 let chunk = self.prefill_chunk().max(1);
12726 self.clear_sequence_state();
12727 if !cap::moe_capture_begin(lo, hi, chunk, hs, ne) {
12728 return Err("capture unavailable (no wgpu device)".into());
12729 }
12730 let mut out: Vec<Vec<f32>> = (lo..hi).map(|_| Vec::with_capacity(ids.len() * (hs + ne))).collect();
12731 let res = (|| {
12732 for (ci, part) in ids.chunks(chunk).enumerate() {
12733 let mut hiddens = Vec::with_capacity(part.len() * hs);
12734 for &id in part {
12735 hiddens.extend_from_slice(&self.embed_single(id));
12736 }
12737 let positions: Vec<usize> = (ci * chunk..ci * chunk + part.len()).collect();
12738 match self.try_batch_graph_wgpu(&mut hiddens, &positions, part.len(), None) {
12739 crate::gpu::BatchGraphOutcome::Completed => {}
12740 _ => return Err("the batched graph declined the calibration forward (VRAM budget?)".to_string()),
12741 }
12742 let (rows, written) = cap::moe_capture_read(part.len()).ok_or("capture readback")?;
12743 if written < hi - lo {
12744 return Err(format!("{written} of {} captured layers ran through the device MoE arm", hi - lo));
12745 }
12746 for (o, r) in out.iter_mut().zip(rows) {
12747 o.extend_from_slice(&r);
12748 }
12749 }
12750 Ok(())
12751 })();
12752 cap::moe_capture_end();
12753 self.clear_sequence_state();
12754 res.map(|_| out)
12755 }
12756
12757 pub fn moe_expert_dirs(
12760 &self,
12761 li: usize,
12762 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
12763 let FfnKind::Moe(m) = &self.weights.layers.get(self.phys_layer(li))?.ffn else {
12764 return None;
12765 };
12766 let dirs = |d: &DenseFfn| Some((d.gate_proj.model_idx()?, d.up_proj.model_idx()?, d.down_proj.model_idx()?));
12767 let routed = m.experts.iter().map(dirs).collect::<Option<Vec<_>>>()?;
12768 let shared = match &m.shared {
12769 Some((se, _)) => Some(dirs(se)?),
12770 None => None,
12771 };
12772 Some((routed, shared))
12773 }
12774
12775 pub fn qwen4_expert_dirs(
12777 &self,
12778 li: usize,
12779 ) -> Option<(Vec<(usize, usize, usize)>, Option<(usize, usize, usize)>)> {
12780 let b = self.qwen4_exp.as_ref()?;
12781 crate::qwen4_exp::expert_dirs(&b.1, li)
12782 }
12783
12784 pub fn probe_mtp_ffn(&mut self, ids: &[u32]) -> Result<Vec<MtpFfnProbe>, String> {
12799 self.reset_mtp_probe_state();
12800 let _exact_calibration = ExactMtpCalibrationGuard::enter();
12806 let result = if ids.len() < 2 {
12807 Err("MTP FFN probe needs at least two token ids".into())
12808 } else if ids
12809 .iter()
12810 .any(|&id| id as usize >= self.weights.embed_tokens.rows())
12811 {
12812 Err("MTP FFN probe received a token id outside the embedding table".into())
12813 } else if self.mtp.is_some() && self.mimo_mtp.is_some() {
12814 Err("MTP FFN probe found both canonical and MiMo draft stacks".into())
12817 } else if self.mimo_mtp.is_some() {
12818 let mut st = self
12819 .mimo_mtp
12820 .take()
12821 .expect("checked MiMo MTP before detaching it");
12822 let result = match self.probe_trunk_rows(ids) {
12823 Ok(trunk) => crate::gpu::cpu_scope(|| {
12824 self.mimo_mtp_ffn_probe_teacher_forced(&mut st, ids, &trunk)
12825 }),
12826 Err(e) => Err(e),
12827 };
12828 st.reset();
12831 self.mimo_mtp = Some(st);
12832 result
12833 } else if self.mtp.is_some() {
12834 let mut mtp = self
12835 .mtp
12836 .take()
12837 .expect("checked canonical MTP before detaching it");
12838 let result = match self.probe_trunk_rows(ids) {
12839 Ok(trunk) => crate::gpu::cpu_scope(|| {
12840 self.mtp_ffn_probe_teacher_forced(&mut mtp, ids, &trunk)
12841 }),
12842 Err(e) => Err(e),
12843 };
12844 mtp.kv.clear();
12845 self.mtp = Some(mtp);
12846 result
12847 } else {
12848 Err("MTP FFN probe: this pipeline has no draft stack".into())
12849 };
12850 self.reset_mtp_probe_state();
12853 result
12854 }
12855
12856 fn reset_mtp_probe_state(&mut self) {
12858 self.clear_sequence_state();
12859 if let Some(mtp) = self.mtp.as_mut() {
12860 mtp.kv.clear();
12861 }
12862 if let Some(mimo) = self.mimo_mtp.as_mut() {
12863 mimo.reset();
12864 }
12865 self.mtp_graph_mode = None;
12866 }
12867
12868 pub(crate) fn probe_trunk_rows(&mut self, ids: &[u32]) -> Result<Vec<f32>, String> {
12874 if ids.len() < 2 {
12875 return Err("MTP FFN probe needs at least two token ids".into());
12876 }
12877 let rows = self.final_hidden_rows(&ids[..ids.len() - 1])?;
12878 self.clear_sequence_state();
12879 Ok(rows)
12880 }
12881
12882 fn mtp_ffn_probe_teacher_forced(
12888 &mut self,
12889 mtp: &mut MtpModule,
12890 ids: &[u32],
12891 trunk: &[f32],
12892 ) -> Result<Vec<MtpFfnProbe>, String> {
12893 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
12894 return Err("MTP FFN probe: canonical draft FFN is not dense".into());
12895 };
12896 let inter = dense.gate_proj.rows();
12897 let (hs, n) = (self.hidden_size, ids.len() - 1);
12898 if trunk.len() != n * hs {
12899 return Err(format!(
12900 "MTP FFN probe: {} trunk values for {n} rows of width {hs}",
12901 trunk.len()
12902 ));
12903 }
12904 let mut mass = vec![0.0f64; inter];
12905 let mut squared_mass = vec![0.0f64; inter];
12906 let mut inputs = Vec::with_capacity(MTP_PROBE_FFN_ROWS.min(n) * hs);
12907 for position in 0..n {
12908 self.mtp_probe_ffn_input(
12909 mtp,
12910 &trunk[position * hs..(position + 1) * hs],
12911 ids[position + 1],
12912 position,
12913 )?;
12914 inputs.extend_from_slice(&self.ws.p1[..hs]);
12915 if inputs.len() == MTP_PROBE_FFN_ROWS * hs || position + 1 == n {
12916 let FfnKind::Dense(dense) = &mtp.layer.ffn else {
12917 unreachable!("checked dense above");
12918 };
12919 accumulate_dense_ffn_activation_mass(
12920 dense,
12921 &inputs,
12922 inputs.len() / hs,
12923 self.pool.as_deref(),
12924 &mut mass,
12925 &mut squared_mass,
12926 )?;
12927 inputs.clear();
12928 }
12929 }
12930 Ok(vec![MtpFfnProbe {
12931 mass,
12932 squared_mass,
12933 observed_positions: n,
12934 }])
12935 }
12936
12937 fn mtp_probe_ffn_input(
12943 &mut self,
12944 mtp: &mut MtpModule,
12945 hidden: &[f32],
12946 next_token: u32,
12947 position: usize,
12948 ) -> Result<(), String> {
12949 let e = self.embed_single(next_token);
12950 let mut cat = vec![0.0f32; 2 * self.hidden_size];
12951 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
12952 inference::rms_norm_into(&e, &mtp.enorm, self.rms_eps, self.norm_style, cat_e);
12953 inference::rms_norm_into(
12954 hidden,
12955 &mtp.hnorm,
12956 self.rms_eps,
12957 self.norm_style,
12958 cat_h,
12959 );
12960 let mut x = vec![0.0f32; self.hidden_size];
12961 mtp.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
12962
12963 let lw = &mtp.layer;
12964 inference::rms_norm_into(
12965 &x,
12966 &lw.input_norm,
12967 self.rms_eps,
12968 self.norm_style,
12969 &mut self.ws.n1,
12970 );
12971 let attn = match &lw.attn {
12972 AttnKind::Full {
12973 wq,
12974 wk,
12975 wv,
12976 wo,
12977 q_norm,
12978 k_norm,
12979 output_gate,
12980 softplus_gate,
12981 bias,
12982 } => {
12983 let mut cfg = self.attn_cfg(position);
12984 cfg.q_norm = q_norm.as_deref();
12985 cfg.k_norm = k_norm.as_deref();
12986 cfg.output_gate = *output_gate;
12987 cfg.softplus_gate = softplus_gate
12988 .as_ref()
12989 .map(|(gate, per_head)| (gate, *per_head));
12990 cfg.bias = bias
12991 .as_ref()
12992 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
12993 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut mtp.kv, &cfg)
12994 }
12995 _ => return Err("MTP FFN probe: draft attention is not full attention".into()),
12996 };
12997 for (dst, src) in x.iter_mut().zip(attn) {
12998 *dst += src;
12999 }
13000 inference::rms_norm_into(
13001 &x,
13002 &lw.post_norm,
13003 self.rms_eps,
13004 self.norm_style,
13005 &mut self.ws.p1,
13006 );
13007 Ok(())
13008 }
13009
13010 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
13015 self.clear_sequence_state();
13016 FFN_PROBE.with(|p| {
13017 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13018 });
13019 crate::gpu::cpu_scope(|| {
13020 for (pos, &id) in ids.iter().enumerate() {
13021 let emb = self.embed_single(id);
13022 let _ = self.forward_layers(&emb, pos, None);
13023 }
13024 });
13025 self.clear_sequence_state();
13026 FFN_PROBE
13027 .with(|p| p.borrow_mut().take())
13028 .unwrap_or_default()
13029 }
13030
13031 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
13035 if let Err(err) = self.nll_begin() {
13036 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
13040 self.nll_end();
13041 return Err(err);
13042 }
13043 FFN_PROBE.with(|p| {
13044 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13045 });
13046 let result: Result<(), String> = (|| {
13047 for chunk in ids.chunks(256) {
13048 if chunk.len() < 2 {
13049 continue;
13050 }
13051 self.nll_ids_masked(chunk, 0, None)?;
13052 }
13053 Ok(())
13054 })();
13055 self.nll_end();
13056 let probe = FFN_PROBE
13057 .with(|p| p.borrow_mut().take())
13058 .unwrap_or_default();
13059 match result {
13060 Ok(()) => Ok(probe),
13061 Err(err) => {
13062 drop(probe);
13063 Err(err)
13064 }
13065 }
13066 }
13067
13068 pub fn probe_ffn_mass_span(&mut self, ids: &[u32], chunk: usize) -> Result<Vec<Vec<f64>>, String> {
13072 FFN_PROBE.with(|p| {
13073 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
13074 });
13075 let result = self.calib_forward(ids, self.num_layers, chunk);
13076 let probe = FFN_PROBE.with(|p| p.borrow_mut().take()).unwrap_or_default();
13077 result.map(|()| probe)
13078 }
13079
13080 pub fn probe_moe_mass(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
13087 let Some(b) = self.qwen4_exp.as_mut() else {
13088 return Err("hot-probe --experts: not a Qwen MoE model (the probe reads the qwen4 host path)".into());
13089 };
13090 let geom: Vec<(usize, usize)> = b.1.iter().map(crate::qwen4_exp::Layer::expert_geometry).collect();
13091 let inter = geom.iter().map(|g| g.1).max().unwrap_or(0);
13092 if inter == 0 || ids.is_empty() {
13093 return Err("hot-probe --experts: the model has no routed experts".into());
13094 }
13095 MOE_PROBE.with(|p| {
13096 *p.borrow_mut() = Some(MoeProbe {
13097 acc: geom.iter().map(|&(ne, _)| vec![0f64; ne * inter]).collect(),
13098 inter,
13099 cur: None,
13100 });
13101 });
13102 b.3.reset();
13103 let mut pos = 0usize;
13104 while pos < ids.len() {
13105 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(ids.len());
13106 let mut lg = Vec::new();
13107 crate::qwen4_exp::forward_tokens(&b.0, &b.1, &b.2, &mut b.3, &ids[pos..end], pos, &self.inv_freq, self.pool.as_deref(), &mut lg, false);
13108 pos = end;
13109 }
13110 b.3.reset();
13111 let probe = MOE_PROBE.with(|p| p.borrow_mut().take()).map_or_else(Vec::new, |p| p.acc);
13112 if !probe.iter().any(|r| r.iter().any(|&v| v > 0.0)) {
13113 return Err("hot-probe --experts: the statistic is all zero (a device path hid the experts from the probe; run with CMF_GPU=0)".into());
13114 }
13115 Ok(probe)
13116 }
13117
13118 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
13122 self.nll_begin()?;
13123 let result: Result<f64, String> = (|| {
13124 let mut nll = 0f64;
13125 let mut cnt = 0usize;
13126 let mut hidden = vec![0f32; self.hidden_size];
13127 for (pos, &id) in ids.iter().enumerate() {
13128 if pos > 0 {
13129 inference::rms_norm_into(
13130 &hidden,
13131 &self.weights.final_norm,
13132 self.rms_eps,
13133 self.norm_style,
13134 &mut self.ws.n1,
13135 );
13136 let mut logits = self.lm_head_forward(&self.ws.n1);
13137 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
13138 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
13139 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
13140 nll -= p.max(1e-300).ln();
13141 cnt += 1;
13142 attention::recycle_buf(&mut logits);
13143 }
13144 let emb = self.embed_single(id);
13145 hidden = self.forward_layers(&emb, pos, Some(mask));
13146 self.nll_check_graph("masked serial forward", pos)?;
13147 let _ = self.graph_logits.take();
13151 }
13152 Ok((nll / cnt.max(1) as f64).exp())
13153 })();
13154 self.nll_end();
13155 result
13156 }
13157
13158 pub fn nll_ids_masked(
13177 &mut self,
13178 ids: &[u32],
13179 start: usize,
13180 task_mask: Option<&TaskMask>,
13181 ) -> Result<(f64, usize), String> {
13182 let task_mask = self.drop_open_mask(task_mask);
13183 self.nll_ids_inner(ids, start, task_mask)
13184 }
13185
13186 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
13187 self.nll_ids_inner(ids, start, None)
13188 }
13189
13190 pub fn prefill_parity_probe(
13199 &mut self,
13200 ids: &[u32],
13201 split: usize,
13202 bk: usize,
13203 ) -> Result<String, String> {
13204 use std::fmt::Write as _;
13205 let (n, hs) = (ids.len(), self.hidden_size);
13206 if split == 0 || split >= n || bk == 0 || !self.can_prefill_batched() {
13207 return Err(format!(
13208 "prefill parity: needs 0 < split < {n} tokens, k > 0 and a batched host prefill"
13209 ));
13210 }
13211 let kinds: Vec<u8> = (0..self.num_layers)
13213 .map(|li| match &self.weights.layers[self.phys_layer(li)].attn {
13214 AttnKind::LinearGdn(_) => 1,
13215 AttnKind::Full { .. } => 2,
13216 _ => 0,
13217 })
13218 .collect();
13219 let kv_id = self.graph_kv_id;
13220 let host_chunk = self.prefill_chunk().max(1);
13221 let host_states = |p: &Self| -> Vec<Vec<f32>> {
13222 (0..p.num_layers)
13223 .map(|li| {
13224 if kinds[li] == 1 {
13225 p.kv_cache.layers[li].linear_state.clone()
13226 } else {
13227 Vec::new()
13228 }
13229 })
13230 .collect()
13231 };
13232 let graph_states = |_p: &Self| -> Vec<Vec<f32>> {
13233 (0..kinds.len())
13234 .map(|li| {
13235 if kinds[li] == 1 {
13236 crate::gpu::graph_gdn_state_read(kv_id, li).unwrap_or_default()
13237 } else {
13238 Vec::new()
13239 }
13240 })
13241 .collect()
13242 };
13243 let host_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
13245 (0..p.num_layers)
13246 .map(|li| {
13247 let c = &p.kv_cache.layers[li];
13248 if kinds[li] != 2
13249 || c.mode != crate::kv_cache::KvMode::F32
13250 || c.base() != 0
13251 || c.seq_len != n
13252 {
13253 return (Vec::new(), Vec::new());
13254 }
13255 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
13256 let (mut k, mut v) = (vec![0f32; n * nkv * hd], vec![0f32; n * nkv * hd]);
13257 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
13258 for pos in 0..n {
13259 let d = (pos * nkv + h) * hd;
13260 k[d..d + hd].copy_from_slice(&kh[pos * hd..(pos + 1) * hd]);
13261 v[d..d + hd].copy_from_slice(&vh[pos * hd..(pos + 1) * hd]);
13262 }
13263 }
13264 (k, v)
13265 })
13266 .collect()
13267 };
13268 let graph_kv = |p: &Self| -> Vec<(Vec<f32>, Vec<f32>)> {
13269 (0..p.num_layers)
13270 .map(|li| {
13271 let c = &p.kv_cache.layers[li];
13272 if kinds[li] != 2 {
13273 return (Vec::new(), Vec::new());
13274 }
13275 crate::gpu::graph_kv_read_rows(kv_id, &[(li, 0, n)], c.num_kv_heads, c.head_dim)
13276 .and_then(|mut v| v.pop())
13277 .unwrap_or_default()
13278 })
13279 .collect()
13280 };
13281 let host_walk = |p: &mut Self,
13282 from: usize,
13283 to: usize,
13284 rows: &mut [f32],
13285 at_split: &mut Option<Vec<Vec<f32>>>| {
13286 let mut pos = from;
13287 while pos < to {
13288 let mut end = (pos + host_chunk).min(to);
13289 if pos < split && end > split {
13290 end = split;
13291 }
13292 let hb = p.prefill_batch(&ids[pos..end], pos);
13293 rows[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13294 pos = end;
13295 if pos == split {
13296 *at_split = Some(host_states(p));
13297 }
13298 }
13299 };
13300 let graph_walk = |p: &mut Self,
13301 from: usize,
13302 to: usize,
13303 rows: &mut [f32],
13304 at_split: &mut Option<Vec<Vec<f32>>>|
13305 -> Result<(), String> {
13306 let mut pos = from;
13307 while pos < to {
13308 let mut end = (pos + bk).min(to);
13309 if pos < split && end > split {
13310 end = split;
13311 }
13312 let k = end - pos;
13313 let mut hid = vec![0f32; k * hs];
13314 for (j, &id) in ids[pos..end].iter().enumerate() {
13315 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
13316 }
13317 let positions: Vec<usize> = (pos..end).collect();
13318 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
13319 if outcome != crate::gpu::BatchGraphOutcome::Completed {
13320 return Err(format!(
13321 "prefill parity: batch graph {outcome:?} at position {pos}"
13322 ));
13323 }
13324 rows[pos * hs..end * hs].copy_from_slice(&hid);
13325 pos = end;
13326 if pos == split {
13327 *at_split = Some(graph_states(p));
13328 }
13329 }
13330 Ok(())
13331 };
13332 let score = |p: &Self, rows: &[f32], from: usize| p.parity_score(ids, rows, from);
13333
13334 let t = std::time::Instant::now();
13335 self.clear_sequence_state();
13336 let (mut hh, mut h_split) = (vec![0f32; n * hs], None);
13337 host_walk(self, 0, n, &mut hh, &mut h_split);
13338 let (h_end, h_kv) = (host_states(self), host_kv(self));
13339 let t_h = t.elapsed().as_secs_f64();
13340
13341 let t = std::time::Instant::now();
13342 self.clear_sequence_state();
13343 let (mut hg, mut g_split) = (vec![0f32; n * hs], None);
13344 let g_run = graph_walk(self, 0, n, &mut hg, &mut g_split);
13345 let (g_end, g_kv) = (graph_states(self), graph_kv(self));
13346 let t_g = t.elapsed().as_secs_f64();
13347
13348 let t = std::time::Instant::now();
13349 self.clear_sequence_state();
13350 let (mut hm, mut m_split) = (vec![0f32; n * hs], None);
13351 host_walk(self, 0, split, &mut hm, &mut m_split);
13352 let m_run = graph_walk(self, split, n, &mut hm, &mut None);
13353 let (m_end, m_kv) = (graph_states(self), graph_kv(self));
13354 let t_m = t.elapsed().as_secs_f64();
13355 self.clear_sequence_state();
13356 g_run?;
13357 m_run?;
13358
13359 let (sh, sg, sm) = (
13360 score(self, &hh, 0),
13361 score(self, &hg, 0),
13362 score(self, &hm, split),
13363 );
13364 let mut r = String::new();
13365 let _ = writeln!(
13366 r,
13367 "prefill parity: {n} tokens, split {split}, host chunk {host_chunk}, graph k {bk} \
13368 (host {t_h:.1} s, graph {t_g:.1} s, host+graph {t_m:.1} s)"
13369 );
13370 let _ = writeln!(
13371 r,
13372 " bin | NLL host graph h+g | top1 host=graph h+g=graph | hidden rel host:graph h+g:graph"
13373 );
13374 let bin = 256usize;
13375 let mut b0 = 0;
13376 while b0 + 1 < n {
13377 let b1 = (b0 + bin).min(n - 1);
13378 let mean = |s: &[(f64, usize)], off: usize| -> Option<f64> {
13379 let v: Vec<f64> = (b0.max(off)..b1).map(|p| s[p - off].0).collect();
13380 (!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
13381 };
13382 let agree = |a: &[(f64, usize)], ao: usize, b: &[(f64, usize)], bo: usize| {
13383 let ps: Vec<usize> = (b0.max(ao).max(bo)..b1).collect();
13384 if ps.is_empty() {
13385 return None;
13386 }
13387 let same = ps.iter().filter(|&&p| a[p - ao].1 == b[p - bo].1).count();
13388 Some(100.0 * same as f64 / ps.len() as f64)
13389 };
13390 let hid_rel = |a: &[f32], from: usize| -> Option<f64> {
13391 (b0.max(from)..b1)
13392 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &hg[p * hs..(p + 1) * hs]))
13393 .reduce(f64::max)
13394 };
13395 let f =
13396 |v: Option<f64>, prec: usize| v.map_or("-".to_string(), |x| format!("{x:.prec$}"));
13397 let e = |v: Option<f64>| v.map_or("-".to_string(), |x| format!("{x:.1e}"));
13398 let _ = writeln!(
13399 r,
13400 " {b0:5}..{b1:<5} | {:>8} {:>6} {:>6} | {:>14} {:>9} | {:>16} {:>9}",
13401 f(mean(&sh, 0), 3),
13402 f(mean(&sg, 0), 3),
13403 f(mean(&sm, split), 3),
13404 f(agree(&sh, 0, &sg, 0), 1),
13405 f(agree(&sm, split, &sg, 0), 1),
13406 e(hid_rel(&hh, 0)),
13407 e(hid_rel(&hm, split)),
13408 );
13409 b0 = b1;
13410 }
13411 let ring_len = self
13413 .gdn_cfg
13414 .map(|c| c.conv_dim() * c.conv_kernel.saturating_sub(1))
13415 .unwrap_or(0);
13416 let state_line = |r: &mut String, label: &str, a: &[Vec<f32>], b: &[Vec<f32>]| {
13417 let _ = write!(r, " GDN {label} (ring/S rel):");
13418 let mut first = None;
13419 for li in (0..kinds.len()).filter(|&li| kinds[li] == 1) {
13420 let (x, y) = (&a[li], &b[li]);
13421 if x.len() != y.len() || x.len() <= ring_len {
13422 let _ = write!(r, " L{li} len {}/{}", x.len(), y.len());
13423 continue;
13424 }
13425 let (rr, rs) = (
13426 rel_l2(&x[..ring_len], &y[..ring_len]),
13427 rel_l2(&x[ring_len..], &y[ring_len..]),
13428 );
13429 if first.is_none() && rr.max(rs) > 1e-2 {
13430 first = Some(li);
13431 }
13432 let _ = write!(r, " L{li} {rr:.0e}/{rs:.0e}");
13433 }
13434 let _ = writeln!(
13435 r,
13436 "\n first GDN layer past 1e-2: {}",
13437 first.map_or("none".to_string(), |l| l.to_string())
13438 );
13439 };
13440 match (&h_split, &g_split) {
13441 (Some(a), Some(b)) => state_line(&mut r, "host vs graph at split", a, b),
13442 _ => {
13443 let _ = writeln!(r, " GDN at split: not captured");
13444 }
13445 }
13446 if let (Some(a), Some(b)) = (&m_split, &h_split) {
13447 state_line(&mut r, "h+g host part vs host at split", a, b);
13448 }
13449 state_line(&mut r, "host vs graph at end", &h_end, &g_end);
13450 state_line(&mut r, "h+g vs graph at end", &m_end, &g_end);
13451 let kv_line = |r: &mut String, label: &str, a: &[(Vec<f32>, Vec<f32>)], from: usize| {
13454 let _ = writeln!(r, " K/V {label} (worst bin K/V rel, first bin past 1e-2):");
13455 for li in (0..kinds.len()).filter(|&li| kinds[li] == 2) {
13456 let ((ak, av), (gk, gv)) = (&a[li], &g_kv[li]);
13457 if ak.len() != gk.len() || ak.is_empty() {
13458 let _ = writeln!(r, " L{li}: rows {}/{}", ak.len(), gk.len());
13459 continue;
13460 }
13461 let row = ak.len() / n;
13462 let (mut wk, mut wv, mut first) = (0f64, 0f64, None);
13463 let mut b0 = from;
13464 while b0 < n {
13465 let b1 = (b0 + bin).min(n);
13466 let (s0, s1) = (b0 * row, b1 * row);
13467 let (rk, rv) = (
13468 rel_l2(&ak[s0..s1], &gk[s0..s1]),
13469 rel_l2(&av[s0..s1], &gv[s0..s1]),
13470 );
13471 (wk, wv) = (wk.max(rk), wv.max(rv));
13472 if first.is_none() && rk.max(rv) > 1e-2 {
13473 first = Some(b0);
13474 }
13475 b0 = b1;
13476 }
13477 let _ = writeln!(
13478 r,
13479 " L{li}: {wk:.1e}/{wv:.1e} first {}",
13480 first.map_or("none".to_string(), |p| p.to_string())
13481 );
13482 }
13483 };
13484 kv_line(&mut r, "host vs graph", &h_kv, 0);
13485 kv_line(&mut r, "h+g vs graph", &m_kv, 0);
13486 Ok(r)
13487 }
13488
13489 fn parity_score(&self, ids: &[u32], rows: &[f32], from: usize) -> Vec<(f64, usize)> {
13493 let (n, hs) = (ids.len(), self.hidden_size);
13494 let vrows = self.weights.lm_head.rows();
13495 let vs = self.vocab_size.min(vrows);
13496 let mut out = Vec::with_capacity(n.saturating_sub(from + 1));
13497 let mut pos = from;
13498 while pos + 1 < n {
13499 let end = (pos + 32).min(n - 1);
13500 let sb = end - pos;
13501 let mut normed = vec![0f32; sb * hs];
13502 for j in 0..sb {
13503 let r = inference::rms_norm(
13504 &rows[(pos + j) * hs..(pos + j + 1) * hs],
13505 &self.weights.final_norm,
13506 self.rms_eps,
13507 self.norm_style,
13508 );
13509 normed[j * hs..(j + 1) * hs].copy_from_slice(&r);
13510 }
13511 let mut logits = vec![0f32; sb * vrows];
13512 self.weights
13513 .lm_head
13514 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
13515 for j in 0..sb {
13516 let lg = &mut logits[j * vrows..j * vrows + vs];
13517 if let Some(mu) = self.logit_multiplier {
13518 lg.iter_mut().for_each(|v| *v *= mu);
13519 }
13520 if let Some(c) = self.final_softcap {
13521 softcap_in_place(lg, c, self.pool.as_deref());
13522 }
13523 let (mut max, mut top) = (f32::NEG_INFINITY, 0usize);
13524 for (i, &v) in lg.iter().enumerate() {
13525 if v > max {
13526 (max, top) = (v, i);
13527 }
13528 }
13529 let lse = lg
13530 .iter()
13531 .map(|&v| ((v - max) as f64).exp())
13532 .sum::<f64>()
13533 .ln()
13534 + max as f64;
13535 out.push((lse - lg[ids[pos + j + 1] as usize] as f64, top));
13536 }
13537 pos = end;
13538 }
13539 out
13540 }
13541
13542 pub fn o1_seal_parity_probe(
13556 &mut self,
13557 ids: &[u32],
13558 cfg: crate::nystrom::O1Cfg,
13559 requested: usize,
13560 bk: usize,
13561 ) -> Result<String, String> {
13562 use std::fmt::Write as _;
13563 let (n, hs) = (ids.len(), self.hidden_size);
13564 self.set_o1(Some(cfg));
13565 let split = self
13566 .o1_effective_boundary(requested)
13567 .ok_or("o1 parity: no O(1) configuration")?;
13568 if split + 1 >= n || bk == 0 || !self.can_prefill_batched() {
13569 return Err(format!(
13570 "o1 parity: needs the boundary {split} inside {n} tokens, k > 0 and a batched host prefill"
13571 ));
13572 }
13573 let o1_layers: Vec<usize> = (0..self.num_layers)
13574 .filter(|&li| self.o1_flags.get(li).copied().unwrap_or(false))
13575 .collect();
13576 let graph_rows = |p: &mut Self, from: usize, to: usize, rows: &mut [f32]| {
13577 let mut pos = from;
13578 while pos < to {
13579 let end = (pos + bk).min(to);
13580 let k = end - pos;
13581 let mut hid = vec![0f32; k * hs];
13582 for (j, &id) in ids[pos..end].iter().enumerate() {
13583 hid[j * hs..(j + 1) * hs].copy_from_slice(&p.embed_single(id));
13584 }
13585 let positions: Vec<usize> = (pos..end).collect();
13586 let outcome = p.try_batch_graph_wgpu(&mut hid, &positions, k, None);
13587 if outcome != crate::gpu::BatchGraphOutcome::Completed {
13588 return Err(format!(
13589 "o1 parity: batch graph {outcome:?} at position {pos}"
13590 ));
13591 }
13592 rows[pos * hs..end * hs].copy_from_slice(&hid);
13593 pos = end;
13594 }
13595 Ok(())
13596 };
13597 let seal_from = |p: &mut Self, trace: &[(Vec<f32>, Vec<f32>, Vec<f32>)]| {
13600 for (&li, (q, k, v)) in o1_layers.iter().zip(trace) {
13601 let c = &mut p.kv_cache.layers[li];
13602 let (row, qrow) = (k.len() / split, q.len() / split);
13603 for pos in 0..split {
13604 c.append(
13605 &k[pos * row..(pos + 1) * row],
13606 &v[pos * row..(pos + 1) * row],
13607 &[],
13608 );
13609 c.o1_push_q(&q[pos * qrow..(pos + 1) * qrow]);
13610 }
13611 }
13612 p.o1_seal_checked()
13613 };
13614 let steps: usize = std::env::var("CMF_PARITY_GREEDY")
13617 .ok()
13618 .and_then(|v| v.parse().ok())
13619 .unwrap_or(40);
13620 let token_decode = std::env::var("CMF_PARITY_DECODE").as_deref() == Ok("token");
13623 let greedy = |p: &mut Self, rows: &[f32]| -> Result<Vec<u32>, String> {
13624 let argmax = |lg: &[f32], vs: usize| {
13625 (0..vs.min(lg.len())).fold(0, |b, i| if lg[i] > lg[b] { i } else { b }) as u32
13626 };
13627 let normed = inference::rms_norm(
13628 &rows[(n - 1) * hs..n * hs],
13629 &p.weights.final_norm,
13630 p.rms_eps,
13631 p.norm_style,
13632 );
13633 let mut out = vec![argmax(&p.lm_head_forward(&normed), p.vocab_size)];
13634 for s in 1..steps {
13635 let (tok, at) = (out[s - 1], n + s - 1);
13636 let lg = if token_decode {
13637 p.decode_step_logits(tok, at)
13638 } else {
13639 let mut hid = p.embed_single(tok);
13640 let outcome = p.try_batch_graph_wgpu(&mut hid, &[at], 1, None);
13641 if outcome != crate::gpu::BatchGraphOutcome::Completed {
13642 return Err(format!("o1 parity: greedy step {s} {outcome:?}"));
13643 }
13644 let normed =
13645 inference::rms_norm(&hid, &p.weights.final_norm, p.rms_eps, p.norm_style);
13646 p.lm_head_forward(&normed)
13647 };
13648 out.push(argmax(&lg, p.vocab_size));
13649 }
13650 Ok(out)
13651 };
13652
13653 self.clear_sequence_state();
13659 self.o1_begin_with_prefix(Some(requested));
13660 let walk_chunk = self.prefill_chunk().max(1);
13661 let walk_from = (split - 1) / walk_chunk * walk_chunk;
13662 let mut ow = vec![0f32; n * hs];
13663 let mut pos = 0;
13664 while pos < split {
13665 let end = (pos + walk_chunk).min(split);
13666 let hb = self.prefill_batch(&ids[pos..end], pos);
13667 ow[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13668 pos = end;
13669 }
13670 self.o1_seal_checked()?;
13671 let ow_run = graph_rows(self, split, n, &mut ow);
13672 let ow_text = ow_run.clone().and_then(|_| greedy(self, &ow));
13673
13674 let t = std::time::Instant::now();
13678 self.clear_sequence_state();
13679 self.o1_begin_with_prefix(Some(split + 1));
13680 let mut oh = vec![0f32; n * hs];
13681 let mut pos = 0;
13682 while pos < split {
13683 let end = (pos + 128).min(split);
13684 let hb = self.prefill_batch(&ids[pos..end], pos);
13685 oh[pos * hs..end * hs].copy_from_slice(&hb[..(end - pos) * hs]);
13686 pos = end;
13687 }
13688 let mut host = Vec::with_capacity(o1_layers.len());
13689 for &li in &o1_layers {
13690 let c = &self.kv_cache.layers[li];
13691 let q = c
13692 .o1_q_trace()
13693 .ok_or("o1 parity: host walk sealed early")?
13694 .to_vec();
13695 let (nkv, hd) = (c.num_kv_heads, c.head_dim);
13696 let (mut k, mut v) = (vec![0f32; split * nkv * hd], vec![0f32; split * nkv * hd]);
13697 for (h, (kh, vh)) in c.k_heads().iter().zip(c.v_heads()).enumerate() {
13698 for p in 0..split {
13699 let d = (p * nkv + h) * hd;
13700 k[d..d + hd].copy_from_slice(&kh[p * hd..(p + 1) * hd]);
13701 v[d..d + hd].copy_from_slice(&vh[p * hd..(p + 1) * hd]);
13702 }
13703 }
13704 host.push((q, k, v));
13705 }
13706 for &li in &o1_layers {
13707 self.kv_cache.layers[li].o1_set_boundary(split);
13708 }
13709 self.o1_seal_checked()?;
13710 let oh_run = graph_rows(self, split, n, &mut oh);
13711 let t_oh = t.elapsed().as_secs_f64();
13712 let oh_text = oh_run.clone().and_then(|_| greedy(self, &oh));
13713
13714 self.clear_sequence_state();
13718 self.o1_begin_with_prefix(Some(requested));
13719 let mut og = vec![0f32; n * hs];
13720 graph_rows(self, 0, split, &mut og)?;
13721 let perturb: f32 = std::env::var("CMF_PARITY_PERTURB")
13722 .ok()
13723 .and_then(|v| v.parse().ok())
13724 .unwrap_or(0.0);
13725 if perturb > 0.0 {
13726 let mut seed = 0x9e37_79b9_7f4a_7c15u64;
13727 let noisy: Vec<_> = host
13728 .iter()
13729 .map(|(q, k, v)| {
13730 let mut jitter = |x: &f32| {
13731 seed ^= seed << 13;
13732 seed ^= seed >> 7;
13733 seed ^= seed << 17;
13734 x * if seed & 1 == 0 {
13735 1.0 + perturb
13736 } else {
13737 1.0 - perturb
13738 }
13739 };
13740 let k: Vec<f32> = k.iter().map(&mut jitter).collect();
13741 let v: Vec<f32> = v.iter().map(&mut jitter).collect();
13742 (q.clone(), k, v)
13743 })
13744 .collect();
13745 seal_from(self, &noisy)?;
13746 } else {
13747 seal_from(self, &host)?;
13748 }
13749 let og_run = graph_rows(self, split, n, &mut og);
13750 let og_text = og_run.clone().and_then(|_| greedy(self, &og));
13751
13752 self.clear_sequence_state();
13754 self.o1_begin_with_prefix(Some(requested));
13755 let mut od = vec![0f32; n * hs];
13756 graph_rows(self, 0, split, &mut od)?;
13757 let mut dev = Vec::with_capacity(o1_layers.len());
13758 let mut kv_rel = Vec::with_capacity(o1_layers.len());
13759 for (&li, (q, hk, hv)) in o1_layers.iter().zip(&host) {
13760 let c = &self.kv_cache.layers[li];
13761 let (k, v) = crate::gpu::graph_kv_read_rows(
13762 self.graph_kv_id,
13763 &[(li, 0, split)],
13764 c.num_kv_heads,
13765 c.head_dim,
13766 )
13767 .and_then(|mut rows| rows.pop())
13768 .ok_or_else(|| format!("o1 parity: no graph K/V rows for layer {li}"))?;
13769 kv_rel.push((li, rel_l2(hk, &k), rel_l2(hv, &v)));
13770 dev.push((q.clone(), k, v));
13771 }
13772 seal_from(self, &dev)?;
13773 let od_run = graph_rows(self, split, n, &mut od);
13774 let od_text = od_run.clone().and_then(|_| greedy(self, &od));
13775
13776 self.clear_sequence_state();
13778 self.set_o1(None);
13779 let mut xx = vec![0f32; n * hs];
13780 let x_run = graph_rows(self, 0, n, &mut xx);
13781 let x_text = x_run.clone().and_then(|_| greedy(self, &xx));
13782 self.clear_sequence_state();
13783 oh_run?;
13784 ow_run?;
13785 og_run?;
13786 od_run?;
13787 x_run?;
13788
13789 let sx = self.parity_score(ids, &xx, split);
13790 let arms = [
13791 ("OH", self.parity_score(ids, &oh, split)),
13792 ("OG", self.parity_score(ids, &og, split)),
13793 ("OD", self.parity_score(ids, &od, split)),
13794 ("OW", self.parity_score(ids, &ow, split)),
13795 ];
13796 let mut r = String::new();
13797 let _ = writeln!(
13798 r,
13799 "o1 seal parity: {n} tokens, boundary {split}, graph k {bk} (OH {t_oh:.1} s), \
13800 OW chunk {walk_chunk}, boundary chunk {walk_from}..{split}"
13801 );
13802 let _ = write!(r, " seal K/V rel host vs graph:");
13803 for (li, rk, rv) in &kv_rel {
13804 let _ = write!(r, " L{li} {rk:.0e}/{rv:.0e}");
13805 }
13806 let worst = |a: &[f32], p0: usize, p1: usize| {
13809 (p0..p1)
13810 .map(|p| rel_l2(&a[p * hs..(p + 1) * hs], &xx[p * hs..(p + 1) * hs]))
13811 .fold(0f64, f64::max)
13812 };
13813 let _ = writeln!(
13814 r,
13815 "\n prefix rows rel vs exact graph, [0,{walk_from}) / [{walk_from},{split}): \
13816 OH {:.1e} / {:.1e}, OW {:.1e} / {:.1e}",
13817 worst(&oh, 0, walk_from),
13818 worst(&oh, walk_from, split),
13819 worst(&ow, 0, walk_from),
13820 worst(&ow, walk_from, split),
13821 );
13822 let _ = writeln!(
13823 r,
13824 " bin | NLL exact OH OG OD OW | top1=exact OH OG OD OW"
13825 );
13826 let mean = |s: &[(f64, usize)], b0: usize, b1: usize| {
13827 (b0..b1).map(|p| s[p - split].0).sum::<f64>() / (b1 - b0).max(1) as f64
13828 };
13829 let agree = |s: &[(f64, usize)], b0: usize, b1: usize| {
13830 let same = (b0..b1)
13831 .filter(|&p| s[p - split].1 == sx[p - split].1)
13832 .count();
13833 100.0 * same as f64 / (b1 - b0).max(1) as f64
13834 };
13835 let mut b0 = split;
13836 let mut total = [0f64; 5];
13837 while b0 + 1 < n {
13838 let b1 = ((b0 / 256 + 1) * 256).min(n - 1);
13839 let _ = writeln!(
13840 r,
13841 " {b0:5}..{b1:<5} | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | {:>12.1} {:>5.1} {:>5.1} {:>5.1}",
13842 mean(&sx, b0, b1),
13843 mean(&arms[0].1, b0, b1),
13844 mean(&arms[1].1, b0, b1),
13845 mean(&arms[2].1, b0, b1),
13846 mean(&arms[3].1, b0, b1),
13847 agree(&arms[0].1, b0, b1),
13848 agree(&arms[1].1, b0, b1),
13849 agree(&arms[2].1, b0, b1),
13850 agree(&arms[3].1, b0, b1),
13851 );
13852 b0 = b1;
13853 }
13854 for (i, s) in std::iter::once(&sx)
13855 .chain(arms.iter().map(|a| &a.1))
13856 .enumerate()
13857 {
13858 total[i] = s.iter().map(|x| x.0).sum::<f64>() / s.len().max(1) as f64;
13859 }
13860 let _ = writeln!(
13861 r,
13862 " all | {:>9.3} {:>6.3} {:>6.3} {:>6.3} {:>6.3} | ppl ratio vs exact OH ×{:.3} OG ×{:.3} OD ×{:.3} OW ×{:.3}",
13863 total[0],
13864 total[1],
13865 total[2],
13866 total[3],
13867 total[4],
13868 (total[1] - total[0]).exp(),
13869 (total[2] - total[0]).exp(),
13870 (total[3] - total[0]).exp(),
13871 (total[4] - total[0]).exp(),
13872 );
13873 for (name, text) in [
13874 ("X ", x_text),
13875 ("OH", oh_text),
13876 ("OG", og_text),
13877 ("OD", od_text),
13878 ("OW", ow_text),
13879 ] {
13880 let s = text.map_or_else(|e| e, |t| self.tokenizer.decode(&t));
13881 let _ = writeln!(r, " greedy {name}: {:?}", s);
13882 }
13883 Ok(r)
13884 }
13885
13886 fn dory_cached_prefix(&self, input_ids: &[u32]) -> usize {
13889 if std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0") {
13890 return 0;
13891 }
13892 let h = &self.kv_history;
13893 let lcp = h.iter().zip(input_ids).take_while(|(a, b)| a == b).count();
13894 let n = lcp.min(input_ids.len().saturating_sub(1));
13895 if n == 0 || h.len() - n > crate::dory::REWIND {
13896 return 0;
13897 }
13898 let sealed = crate::gpu::dory_sealed(self.graph_kv_id)
13901 || self.dory.as_ref().is_some_and(|b| b.1.slots.iter().flatten().any(|s| !s.far.is_empty()));
13902 if n < h.len() && sealed {
13903 return 0;
13904 }
13905 n
13906 }
13907
13908 fn dory_nll(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
13910 self.clear_sequence_state();
13911 let chunk: usize = std::env::var("CMF_NLL_CHUNK")
13912 .ok()
13913 .and_then(|v| v.parse().ok())
13914 .filter(|&c: &usize| c > 0)
13915 .unwrap_or(128);
13916 let vocab = self.vocab_size;
13917 let n = ids.len().saturating_sub(1);
13918 let skip: usize = std::env::var("CMF_NLL_SKIP").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
13920 let start = start.max(skip.min(n));
13921 let (mut nll, mut cnt, mut pos) = (0f64, 0usize, 0usize);
13922 while pos < n {
13923 if pos < skip {
13924 let end = (pos + 64).min(skip).min(n);
13925 self.dory_forward(&ids[pos..end], pos, crate::dory::Want::None);
13926 pos = end;
13927 continue;
13928 }
13929 let end = (pos + chunk).min(n);
13930 let lg = self.dory_forward(&ids[pos..end], pos, crate::dory::Want::All);
13931 for (r, row) in lg.chunks_exact(vocab).enumerate() {
13932 if pos + r < start {
13933 continue;
13934 }
13935 let target = ids[pos + r + 1] as usize;
13936 let best = row.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b)) as f64;
13937 let den: f64 = row.iter().map(|&x| ((x as f64) - best).exp()).sum();
13938 nll += den.ln() - (row[target] as f64 - best);
13939 cnt += 1;
13940 }
13941 pos = end;
13942 }
13943 Ok((nll, cnt))
13944 }
13945
13946 fn dory_forward(&mut self, ids: &[u32], pos: usize, want: crate::dory::Want) -> Vec<f32> {
13948 let pool = self.pool.clone();
13949 let id = self.graph_kv_id;
13950 let b = self.dory.as_mut().expect("a Dory file");
13951 let (m, st) = (&b.0, &mut b.1);
13952 let mut logits = Vec::new();
13953 let loops = m.cfg.loops;
13954 let host = crate::gptq_capture::capturing();
13956 let file = m.file.clone();
13957 if host || !crate::gpu::dory_forward(id, file.as_ref(), m, ids, pos, loops, want, &mut logits) {
13958 logits.clear();
13959 crate::dory::forward_host(m, st, ids, pos, loops, want, pool.as_deref(), None, &mut logits);
13960 }
13961 if m.o1.is_some() && std::env::var_os("CMF_DORY_KV_REPORT").is_some() {
13962 let (e, f) = crate::dory::host_kv_bytes(st);
13963 eprintln!("dory kv at {}: exact {:.1} MB, far {:.1} MB", pos + ids.len(), e as f64 / 1e6, f as f64 / 1e6);
13964 }
13965 if let Ok(dir) = std::env::var("CMF_DORY_DUMP") {
13966 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
13967 let _ = std::fs::write(format!("{dir}/{pos}.bin"), bytes);
13968 }
13969 logits
13970 }
13971
13972 fn nll_ids_inner(
13973 &mut self,
13974 ids: &[u32],
13975 start: usize,
13976 task_mask: Option<&TaskMask>,
13977 ) -> Result<(f64, usize), String> {
13978 if self.dory.is_some() && task_mask.is_none() {
13979 return self.dory_nll(ids, start);
13980 }
13981 self.nll_begin()?;
13982 #[cfg(target_os = "macos")]
13984 if task_mask.is_none() && self.is_block_diffusion() {
13985 if let Some(r) = self.clover_metal_nll(ids, start) {
13986 self.nll_end();
13987 return r;
13988 }
13989 }
13990 let result: Result<(f64, usize), String> = (|| {
13991 let mut nll = 0f64;
13992 let mut cnt = 0usize;
13993 let (graph_quality, fused_head_quality) = nll_graph_policy(
14006 task_mask.is_none(),
14007 self.graph_prefill_preferred(),
14008 crate::gpu::q1_force(),
14009 );
14010 self.graph_head_required = fused_head_quality;
14011 self.graph_want_logits = fused_head_quality;
14012 #[cfg(target_os = "macos")]
14013 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
14014 match self.nll_batch_metal(ids, start) {
14015 MetalBatchNllOutcome::Completed(nll, count) => {
14016 return Ok((nll, count));
14017 }
14018 MetalBatchNllOutcome::Declined => {}
14019 MetalBatchNllOutcome::Failed(err) => return Err(err),
14020 }
14021 }
14022 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
14027 if self.can_prefill_batched() && !graph_quality && !force_serial {
14028 let chunk_len: usize = std::env::var("CMF_NLL_CHUNK")
14037 .ok()
14038 .and_then(|v| v.parse().ok())
14039 .filter(|&c: &usize| c > 0)
14040 .unwrap_or(128);
14041 const LM_SUB: usize = 32;
14042 let n = ids.len().saturating_sub(1);
14043 let hs = self.hidden_size;
14044 let rows = self.weights.lm_head.rows();
14045 let mut pos = 0usize;
14046 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
14047 while pos < n {
14048 let end = (pos + chunk_len).min(n);
14049 let bsz = end - pos;
14050 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
14051 self.nll_check_graph("batched prefill", pos)?;
14052 if state_trace && end % 256 == 0 {
14053 self.trace_recurrent_state(end);
14054 }
14055 let mut k0 = 0usize;
14056 while k0 < bsz {
14057 let k1 = (k0 + LM_SUB).min(bsz);
14058 let sb = k1 - k0;
14059 if pos + k1 <= start {
14062 k0 = k1;
14063 continue;
14064 }
14065 let mut normed = vec![0.0f32; sb * hs];
14066 for k in 0..sb {
14067 let r = inference::rms_norm(
14068 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
14069 &self.weights.final_norm,
14070 self.rms_eps,
14071 self.norm_style,
14072 );
14073 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
14074 }
14075 let mut logits = vec![0.0f32; sb * rows];
14076 self.weights
14077 .lm_head
14078 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
14079 for k in 0..sb {
14080 if pos + k0 + k < start {
14081 continue;
14082 }
14083 self.nll_check_graph("batched score row", pos + k0 + k)?;
14084 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
14085 if let Some(mu) = self.logit_multiplier {
14086 for v in lg.iter_mut() {
14087 *v *= mu;
14088 }
14089 }
14090 if let Some(c) = self.final_softcap {
14094 softcap_in_place(lg, c, self.pool.as_deref());
14095 }
14096 if let Some(cm) = self.head_clusters.clone() {
14099 self.hierarchical_head_logprobs(
14100 &normed[k * hs..(k + 1) * hs],
14101 &cm,
14102 lg,
14103 );
14104 }
14105 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
14106 let target = ids[pos + k0 + k + 1] as usize;
14107 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14108 let lse: f64 = lg
14109 .iter()
14110 .map(|&v| ((v - max) as f64).exp())
14111 .sum::<f64>()
14112 .ln()
14113 + max as f64;
14114 nll += lse - lg[target] as f64;
14115 cnt += 1;
14116 if std::env::var("CMF_PPL_TRACE").is_ok() {
14117 let top = lg
14118 .iter()
14119 .enumerate()
14120 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14121 .map(|(i, _)| i)
14122 .unwrap_or(0);
14123 eprintln!(
14124 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
14125 pos + k0 + k,
14126 target,
14127 lse - lg[target] as f64,
14128 top,
14129 lg[target],
14130 lg[top]
14131 );
14132 }
14133 }
14134 k0 = k1;
14135 }
14136 pos = end;
14137 }
14138 return Ok((nll, cnt));
14139 }
14140 for pos in 0..ids.len().saturating_sub(1) {
14141 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
14142 self.nll_check_graph("serial forward", pos)?;
14143 let out_of_band = self.graph_logits.take();
14151 if self.graph_head_required && out_of_band.is_none() {
14152 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
14153 return Err(format!(
14154 "fused Metal graph head did not complete at NLL position {pos}"
14155 ));
14156 }
14157 if pos < start {
14158 continue;
14159 }
14160 let logits = match out_of_band {
14161 Some(lg) => lg,
14162 None => {
14163 let normed = inference::rms_norm(
14164 &hidden,
14165 &self.weights.final_norm,
14166 self.rms_eps,
14167 self.norm_style,
14168 );
14169 self.lm_head_forward(&normed)
14173 }
14174 };
14175 let target = ids[pos + 1] as usize;
14176 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14177 let lse: f64 = logits
14178 .iter()
14179 .map(|&v| ((v - max) as f64).exp())
14180 .sum::<f64>()
14181 .ln()
14182 + max as f64;
14183 let tok_nll = lse - logits[target] as f64;
14184 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14185 let top = logits
14186 .iter()
14187 .enumerate()
14188 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14189 .map(|(i, _)| i)
14190 .unwrap_or(0);
14191 eprintln!(
14192 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14193 logits[target], logits[top]
14194 );
14195 }
14196 nll += tok_nll;
14197 cnt += 1;
14198 }
14199 Ok((nll, cnt))
14200 })();
14201 self.nll_end();
14202 result
14203 }
14204
14205 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
14210 let normed = inference::rms_norm(
14211 hidden,
14212 &self.weights.final_norm,
14213 self.rms_eps,
14214 self.norm_style,
14215 );
14216 let mut logits = self.lm_head_forward(&normed);
14219 let target = target as usize;
14220 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14221 let lse: f64 = logits
14222 .iter()
14223 .map(|&v| ((v - max) as f64).exp())
14224 .sum::<f64>()
14225 .ln()
14226 + max as f64;
14227 let tok_nll = lse - logits[target] as f64;
14228 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14229 let top = logits
14230 .iter()
14231 .enumerate()
14232 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14233 .map(|(i, _)| i)
14234 .unwrap_or(0);
14235 eprintln!(
14236 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14237 logits[target], logits[top]
14238 );
14239 }
14240 attention::recycle_buf(&mut logits);
14241 tok_nll
14242 }
14243
14244 fn trace_recurrent_state(&self, pos: usize) {
14252 let stats = |v: &[f32]| -> (f64, f64) {
14253 if v.is_empty() {
14254 return (0.0, 0.0);
14255 }
14256 let (mut ss, mut mx) = (0f64, 0f64);
14257 for &x in v {
14258 ss += (x as f64) * (x as f64);
14259 mx = mx.max((x as f64).abs());
14260 }
14261 ((ss / v.len() as f64).sqrt(), mx)
14262 };
14263 for (li, l) in self.kv_cache.layers.iter().enumerate() {
14264 let lw = &self.weights.layers[self.phys_layer(li)];
14265 let (kind, s_len) = match &lw.attn {
14266 AttnKind::Linear(_) => ("vmf", self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0)),
14267 AttnKind::LinearGdn(_) => ("gdn", self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0)),
14268 AttnKind::Bounded(_) => ("bounded", 0),
14269 AttnKind::Full { .. } => ("full", 0),
14270 _ => ("other", 0),
14271 };
14272 let (rms, max) = stats(&l.linear_state);
14273 let s_part = if kind == "vmf" {
14274 &l.linear_state[..s_len.min(l.linear_state.len())]
14275 } else if kind == "gdn" {
14276 let ring = l
14277 .linear_state
14278 .len()
14279 .saturating_sub(s_len)
14280 .min(l.linear_state.len());
14281 &l.linear_state[ring..]
14282 } else {
14283 &l.linear_state[..0]
14284 };
14285 let (s_rms, s_max) = stats(s_part);
14286 let (ring_rms, ring_len) = match &l.bounded {
14287 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
14288 None => (0.0, 0),
14289 };
14290 eprintln!(
14291 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
14292 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
14293 l.linear_state.len(),
14294 l.seq_len
14295 );
14296 }
14297 }
14298
14299 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
14317 self.nll_begin()?;
14322 let requested_prefix = (prefill > 0).then_some(prefill);
14323 self.o1_begin_with_prefix(requested_prefix);
14324 let n = ids.len().saturating_sub(1);
14325 let requested_start = prefill.min(n);
14326 let exact_end = if self.o1_active() {
14331 match requested_prefix {
14332 Some(requested) => self.o1_effective_boundary(requested),
14333 None => self
14334 .o1_cfg
14335 .as_ref()
14336 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
14337 }
14338 .unwrap_or(requested_start)
14339 .min(n)
14340 } else {
14341 requested_start
14342 };
14343 let mut nll = 0f64;
14344 let mut cnt = 0usize;
14345
14346 let mut pos = 0usize;
14350 if self.can_prefill_batched() {
14351 const CHUNK: usize = 128;
14352 while pos < exact_end {
14353 let end = (pos + CHUNK).min(exact_end);
14354 let hiddens = self.prefill_batch(&ids[pos..end], pos);
14355 if self
14356 .graph_failed
14357 .swap(false, std::sync::atomic::Ordering::Relaxed)
14358 {
14359 self.cancel
14360 .store(false, std::sync::atomic::Ordering::Relaxed);
14361 self.nll_end();
14362 return Err("GPU graph failed during O(1) NLL prefix".into());
14363 }
14364 for row in 0..end - pos {
14365 let score_pos = pos + row;
14366 if score_pos >= requested_start && score_pos < n {
14367 nll += self.nll_from_hidden(
14368 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
14369 ids[score_pos + 1],
14370 score_pos,
14371 );
14372 cnt += 1;
14373 }
14374 }
14375 pos = end;
14376 }
14377 } else {
14378 while pos < exact_end {
14379 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14380 if self
14381 .graph_failed
14382 .swap(false, std::sync::atomic::Ordering::Relaxed)
14383 {
14384 self.cancel
14385 .store(false, std::sync::atomic::Ordering::Relaxed);
14386 self.nll_end();
14387 return Err("GPU graph failed during O(1) NLL prefix".into());
14388 }
14389 if pos >= requested_start && pos < n {
14390 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
14391 cnt += 1;
14392 }
14393 pos += 1;
14394 }
14395 }
14396 self.o1_seal_checked().map_err(|err| {
14397 self.nll_end();
14398 err
14399 })?;
14400
14401 let batch_k = std::env::var("CMF_BATCH_K")
14410 .ok()
14411 .and_then(|v| v.parse::<usize>().ok())
14412 .unwrap_or(0);
14413 let batch_admitted = batch_k > 0
14414 && self.can_prefill_batched()
14415 && self.o1_active()
14416 && self.o1_on_graph()
14417 && (0..self.num_layers).all(|li| {
14418 let cache = &self.kv_cache.layers[self.phys_layer(li)];
14419 cache.o1.is_none() || cache.o1_views().is_some()
14420 });
14421 if std::env::var("CMF_GRAPH_PROF").is_ok() {
14422 eprintln!(
14423 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
14424 batch_admitted,
14425 batch_k,
14426 n.saturating_sub(exact_end),
14427 );
14428 }
14429 let mut batch_completed = false;
14430 if batch_admitted && exact_end < n {
14431 let hs = self.hidden_size;
14432 let mut batch_pos = exact_end;
14433 while batch_pos < n {
14434 let end = (batch_pos + batch_k).min(n);
14435 let bk = end - batch_pos;
14436 let mut hiddens = vec![0.0f32; bk * hs];
14437 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
14438 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
14439 }
14440 let positions: Vec<usize> = (batch_pos..end).collect();
14441 let t_batch = std::time::Instant::now();
14442 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
14443 if std::env::var("CMF_GRAPH_PROF").is_ok() {
14444 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
14445 eprintln!(
14446 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
14447 batch_pos,
14448 end.saturating_sub(1),
14449 bk as f64 / (ms / 1000.0),
14450 );
14451 }
14452 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
14453 self.nll_end();
14454 return Err(err);
14455 }
14456 match outcome {
14457 crate::gpu::BatchGraphOutcome::Completed => {
14458 batch_completed = true;
14459 for row in 0..bk {
14460 nll += self.nll_from_hidden(
14461 &hiddens[row * hs..(row + 1) * hs],
14462 ids[batch_pos + row + 1],
14463 batch_pos + row,
14464 );
14465 cnt += 1;
14466 }
14467 batch_pos = end;
14468 }
14469 crate::gpu::BatchGraphOutcome::Declined => {
14470 if batch_completed {
14471 self.nll_end();
14472 return Err(format!(
14473 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
14474 ));
14475 }
14476 break;
14477 }
14478 crate::gpu::BatchGraphOutcome::Failed => {
14479 self.nll_end();
14480 return Err(format!(
14481 "O(1) NLL batch graph failed after admission at position {batch_pos}"
14482 ));
14483 }
14484 }
14485 }
14486 if batch_completed && cnt == n.saturating_sub(requested_start) {
14487 self.nll_end();
14488 return Ok((nll, cnt));
14489 }
14490 }
14491
14492 for pos in exact_end..n {
14497 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14498 if self
14499 .graph_failed
14500 .swap(false, std::sync::atomic::Ordering::Relaxed)
14501 {
14502 self.cancel
14503 .store(false, std::sync::atomic::Ordering::Relaxed);
14504 self.nll_end();
14505 return Err(format!(
14506 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
14507 ));
14508 }
14509 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
14510 cnt += 1;
14511 }
14512 self.nll_end();
14513 Ok((nll, cnt))
14514 }
14515
14516 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
14524 self.clear_sequence_state();
14525 let n = ids.len().saturating_sub(1);
14526 let mut correct = Vec::with_capacity(n);
14527 let mut pmax = Vec::with_capacity(n);
14528 for pos in 0..n {
14529 let emb = self.embed_single(ids[pos]);
14530 let hidden = self.forward_layers(&emb, pos, None);
14531 let logits = if let Some(logits) = self.graph_logits.take() {
14532 logits
14533 } else {
14534 let normed = inference::rms_norm(
14535 &hidden,
14536 &self.weights.final_norm,
14537 self.rms_eps,
14538 self.norm_style,
14539 );
14540 self.lm_head_forward(&normed)
14544 };
14545 let target = ids[pos + 1] as usize;
14546 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
14547 for (i, &v) in logits.iter().enumerate() {
14548 if v > mval {
14549 mval = v;
14550 amax = i;
14551 }
14552 }
14553 correct.push(amax == target);
14554 let row: Vec<f32> = temps
14555 .iter()
14556 .map(|&t| {
14557 let tt = t.max(1e-3);
14558 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
14559 1.0 / s.max(1e-12) })
14561 .collect();
14562 pmax.push(row);
14563 }
14564 self.clear_sequence_state();
14565 (correct, pmax)
14566 }
14567
14568 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
14575 if self.dyn_router.is_none() {
14576 return Ok((self.ppl_ids(ids)?, 0));
14577 }
14578 self.nll_begin()?;
14579 let saved_active = self.dyn_active;
14580 let mut router = self
14581 .dyn_router
14582 .take()
14583 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
14584 router.reset();
14585 self.dyn_phi_seen = 0;
14586 let _ = self.set_active_skill(None);
14587
14588 let result: Result<(f64, usize), String> = (|| {
14589 let mut nll = 0f64;
14590 let mut cnt = 0usize;
14591 for pos in 0..ids.len().saturating_sub(1) {
14592 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
14593 self.nll_check_graph("dynamic serial forward", pos)?;
14594 let out_of_band = self.graph_logits.take();
14595 let mut logits = match out_of_band {
14596 Some(lg) => lg,
14597 None => {
14598 let normed = inference::rms_norm(
14599 &hidden,
14600 &self.weights.final_norm,
14601 self.rms_eps,
14602 self.norm_style,
14603 );
14604 self.lm_head_forward(&normed)
14608 }
14609 };
14610 let target = ids[pos + 1] as usize;
14611 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
14612 let lse: f64 = logits
14613 .iter()
14614 .map(|&v| ((v - max) as f64).exp())
14615 .sum::<f64>()
14616 .ln()
14617 + max as f64;
14618 let tok_nll = lse - logits[target] as f64;
14619 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 10_000 {
14620 let top = logits
14621 .iter()
14622 .enumerate()
14623 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
14624 .map(|(i, _)| i)
14625 .unwrap_or(0);
14626 eprintln!(
14627 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
14628 logits[target], logits[top]
14629 );
14630 }
14631 nll += tok_nll;
14632 cnt += 1;
14633 attention::recycle_buf(&mut logits);
14634 let phi = self.dyn_phi_ema.clone();
14636 if let Some(new_active) = router.step(&phi, pos) {
14637 let _ = self.set_active_skill(new_active);
14638 }
14639 }
14640 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
14641 })();
14642
14643 let _ = self.set_active_skill(saved_active);
14646 self.dyn_router = Some(router);
14647 self.nll_end();
14648 result
14649 }
14650
14651 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
14653 self.clear_sequence_state();
14654 let mut acc = vec![0f32; self.hidden_size];
14655 for (pos, &id) in ids.iter().enumerate() {
14656 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14657 for (a, v) in acc.iter_mut().zip(&h) {
14658 *a += v;
14659 }
14660 }
14661 let n = ids.len().max(1) as f32;
14662 for a in acc.iter_mut() {
14663 *a /= n;
14664 }
14665 self.clear_sequence_state();
14666 acc
14667 }
14668
14669 pub fn probe_hidden_rows(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
14672 #[cfg(target_os = "macos")]
14673 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14674 self.clear_sequence_state();
14675 let mut rows = Vec::with_capacity(ids.len() * self.hidden_size);
14676 for (pos, &id) in ids.iter().enumerate() {
14677 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14678 rows.extend_from_slice(&h);
14679 }
14680 self.clear_sequence_state();
14681 rows
14682 }
14683
14684 pub fn probe_phi_span(
14698 &mut self,
14699 ids: &[u32],
14700 layer: usize,
14701 span: std::ops::Range<usize>,
14702 ) -> Vec<f32> {
14703 #[cfg(target_os = "macos")]
14704 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14705 let end = span.end.min(ids.len());
14706 let start = span.start.min(end);
14707 let reset = |p: &mut Self| p.clear_sequence_state();
14708 reset(self);
14709 let mut acc = vec![0f32; self.hidden_size];
14710 for (pos, &id) in ids[..end].iter().enumerate() {
14711 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
14712 if pos >= start {
14713 for (a, v) in acc.iter_mut().zip(&h) {
14714 *a += v;
14715 }
14716 }
14717 }
14718 let n = end - start;
14719 if n > 0 {
14720 let n = n as f32;
14721 for a in acc.iter_mut() {
14722 *a /= n;
14723 }
14724 }
14725 reset(self);
14726 acc
14727 }
14728
14729 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
14736 #[cfg(target_os = "macos")]
14737 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
14738 self.graph_logits = None;
14739 let hidden = self.forward_layers(&self.embed_single(token), position, None);
14740 if let Some(logits) = self.graph_logits.take() {
14741 return logits;
14742 }
14743 inference::rms_norm_into(
14744 &hidden,
14745 &self.weights.final_norm,
14746 self.rms_eps,
14747 self.norm_style,
14748 &mut self.ws.n1,
14749 );
14750 self.lm_head_forward(&self.ws.n1)
14751 }
14752
14753 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
14759 self.prefill_batch_masked(ids, start_pos, None)
14760 }
14761
14762 fn prefill_batch_masked(
14768 &mut self,
14769 ids: &[u32],
14770 start_pos: usize,
14771 task_mask: Option<&TaskMask>,
14772 ) -> Vec<f32> {
14773 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
14774 }
14775
14776 fn prefill_rows(
14784 &mut self,
14785 ids: &[u32],
14786 pos: usize,
14787 task_mask: Option<&TaskMask>,
14788 ) -> Result<Vec<f32>, String> {
14789 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
14790 }
14791
14792 fn prefill_input_rows(
14793 &mut self,
14794 input: PrefillIn<'_>,
14795 pos: usize,
14796 task_mask: Option<&TaskMask>,
14797 ) -> Result<Vec<f32>, String> {
14798 if self.dash.is_some() {
14799 let PrefillIn::Hidden(rows) = input else {
14800 return Err("Unlimited-OCR prompts carry image rows".into());
14801 };
14802 return Ok(self.dash_prefill_rows(rows, pos));
14803 }
14804 self.mimo_moe_prepare();
14805 let hs = self.hidden_size;
14806 let bk = match input {
14807 PrefillIn::Ids(ids) => ids.len(),
14808 PrefillIn::Hidden(rows) => rows.len() / hs,
14809 };
14810 #[cfg(target_os = "macos")]
14813 if let PrefillIn::Hidden(rows) = input
14814 && task_mask.is_none()
14815 && self.onevision2_prefill()
14816 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
14817 && crate::gpu::enabled_here()
14818 && self.gdn_cfg.is_some()
14819 && !self.per_token_stack()
14820 && bk > 8
14821 && bk <= 512
14822 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
14823 {
14824 match self.prefill_hiddens_metal(rows.to_vec(), pos, None) {
14825 MetalPrefillOutcome::Completed(h) => return Ok(h),
14826 MetalPrefillOutcome::Failed => {
14827 return Err("Metal prefill of the prompt rows failed after admission".into());
14828 }
14829 MetalPrefillOutcome::Declined => {}
14830 }
14831 }
14832 #[cfg(not(target_os = "macos"))]
14833 if task_mask.is_none()
14834 && !self.o1_active()
14835 && bk > 1
14836 && (self.batch_prefix_prefill()
14837 || (self.value_blend.is_some() && self.g4_batch_prefill())
14840 || (matches!(input, PrefillIn::Hidden(_)) && self.generation_batch_k() > 0)
14843 || (self.verify_exact_moe
14844 && crate::gpu::enabled_here()
14845 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
14846 {
14847 let mut hiddens = match input {
14848 PrefillIn::Hidden(rows) => rows.to_vec(),
14849 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
14850 };
14851 let positions: Vec<usize> = (pos..pos + bk).collect();
14852 let mut run = 0usize;
14853 match self.try_batch_graph_wgpu_prefix(
14854 &mut hiddens,
14855 &positions,
14856 bk,
14857 None,
14858 Some(&mut run),
14859 ) {
14860 crate::gpu::BatchGraphOutcome::Completed => {
14861 let out = if run < self.num_layers {
14862 self.prefill_batch_span(
14863 PrefillIn::Hidden(&hiddens),
14864 pos,
14865 None,
14866 run,
14867 self.num_layers,
14868 )
14869 } else {
14870 hiddens
14871 };
14872 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
14873 Err("MiMo attention graph failed after admission".into())
14874 } else {
14875 Ok(out)
14876 };
14877 }
14878 crate::gpu::BatchGraphOutcome::Failed => {
14879 return Err("batched prefix prefill failed after admission".into());
14880 }
14881 crate::gpu::BatchGraphOutcome::Declined => {
14882 #[cfg(feature = "gpu")]
14884 self.pull_lagging_host_kv(0, self.num_layers, pos);
14885 }
14886 }
14887 }
14888 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
14889 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
14890 Err("batch tail graph failed after admission".into())
14891 } else {
14892 Ok(out)
14893 }
14894 }
14895
14896 fn dash_prefill_rows(&mut self, rows: &[f32], pos: usize) -> Vec<f32> {
14899 let hs = self.hidden_size;
14900 let n = rows.len() / hs;
14901 let mut last = vec![0f32; hs];
14902 let mut i = 0usize;
14903 while i < n && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
14904 #[cfg(not(target_os = "macos"))]
14905 if self.dash_batch_on {
14906 let k = (n - i).min(crate::gpu::DASH_CHUNK_MAX);
14907 if let Some(d) = self.dash.as_mut() {
14908 d.at_prompt_chunk(pos + i, k);
14909 }
14910 let mut h = rows[i * hs..(i + k) * hs].to_vec();
14911 let positions: Vec<usize> = (pos + i..pos + i + k).collect();
14912 match self.try_batch_graph_wgpu_prefix(&mut h, &positions, k, None, None) {
14913 crate::gpu::BatchGraphOutcome::Completed => {
14914 last.copy_from_slice(&h[(k - 1) * hs..]);
14915 i += k;
14916 continue;
14917 }
14918 crate::gpu::BatchGraphOutcome::Failed => {
14919 self.graph_failed.store(true, std::sync::atomic::Ordering::Relaxed);
14920 return last;
14921 }
14922 crate::gpu::BatchGraphOutcome::Declined => self.dash_batch_on = false,
14923 }
14924 }
14925 #[cfg(target_os = "macos")]
14926 if self.dash_batch_on && n - i >= 8 && crate::gpu::backend_name() == "metal" {
14927 let k = (n - i).min(crate::gpu::DASH_CHUNK_MAX);
14928 if let Some(d) = self.dash.as_mut() {
14929 d.at_prompt_chunk(pos + i, k);
14930 }
14931 let mut h = rows[i * hs..(i + k) * hs].to_vec();
14932 if self.chunk_run_gpu(0, &mut h, k, pos + i, None, self.num_layers) == self.num_layers {
14933 last.copy_from_slice(&h[(k - 1) * hs..]);
14934 i += k;
14935 continue;
14936 }
14937 self.dash_batch_on = false;
14938 }
14939 if let Some(d) = self.dash.as_mut() {
14940 d.at_prompt(pos + i);
14941 }
14942 last = self.forward_layers(&rows[i * hs..(i + 1) * hs], pos + i, None);
14943 i += 1;
14944 }
14945 last
14946 }
14947
14948 fn prefill_batch_span(
14954 &mut self,
14955 input: PrefillIn<'_>,
14956 start_pos: usize,
14957 task_mask: Option<&TaskMask>,
14958 from: usize,
14959 upto_excl: usize,
14960 ) -> Vec<f32> {
14961 let hs = self.hidden_size;
14962 let b = match input {
14963 PrefillIn::Ids(ids) => ids.len(),
14964 PrefillIn::Hidden(hb) => hb.len() / hs,
14965 };
14966 let upto_excl = upto_excl.min(self.num_layers);
14967 let mut h: Vec<f32>;
14971 let mut h_ready;
14972 match input {
14973 PrefillIn::Ids(_) => {
14974 h = vec![0.0; b * hs];
14975 h_ready = false;
14976 }
14977 PrefillIn::Hidden(hb) => {
14978 h = hb.to_vec();
14979 h_ready = true;
14980 }
14981 }
14982 if self.value_blend.is_some() {
14984 self.vres_e0.clear();
14985 match input {
14986 PrefillIn::Ids(ids) => {
14987 for &id in ids {
14988 let e = self.embed_single(id);
14989 self.vres_e0.extend_from_slice(&e);
14990 }
14991 }
14992 PrefillIn::Hidden(hb) => {
14993 assert!(from == 0, "value residual: a layer span past the first layer has no embedding rows");
14994 self.vres_e0.extend_from_slice(hb);
14995 }
14996 }
14997 }
14998 let fill_h = |h: &mut Vec<f32>, me: &Self| {
14999 if let PrefillIn::Ids(ids) = input {
15000 for (bi, &id) in ids.iter().enumerate() {
15001 let e = me.embed_single(id);
15002 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
15003 }
15004 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15005 if let Ok(t) = tp.parse::<usize>() {
15006 if t >= start_pos && t < start_pos + ids.len() {
15007 let bi = t - start_pos;
15008 let row = &h[bi * hs..(bi + 1) * hs];
15009 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
15010 eprintln!(
15011 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
15012 ids[bi],
15013 row[0],
15014 row[1],
15015 ids.len(),
15016 &ids[..ids.len().min(8)]
15017 );
15018 }
15019 }
15020 }
15021 }
15022 };
15023 let (_nkv, _hd, _rd, eps) = (
15024 self.num_kv_heads,
15025 self.head_dim,
15026 self.rotary_dim,
15027 self.rms_eps,
15028 );
15029 let pool = self.pool.clone();
15030 let norm_style = self.norm_style;
15031 self.mimo_moe_prepare();
15032 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15033
15034 #[cfg(target_os = "macos")]
15035 let mut chunk_skip_until = 0usize;
15036 let mut mhc_rs: Vec<f32> = Vec::new();
15038 for li in from..upto_excl {
15039 let _capacity_tail = automatic_gpu_prefix
15040 .filter(|&prefix| {
15041 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
15042 })
15043 .map(|_| crate::gpu::enter_cpu_scope());
15044 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
15049 let _fast_gemm = self
15050 .proj_gate_sigmoid
15051 .then(crate::gpu::enter_prefill_fast_gemm);
15052 #[cfg(target_os = "macos")]
15058 if task_mask.is_none() {
15059 if li < chunk_skip_until {
15060 continue;
15061 }
15062 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
15068 fill_h(&mut h, self);
15069 h_ready = true;
15070 }
15071 let ids_for_embed = match input {
15072 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
15073 PrefillIn::Hidden(_) => None,
15074 };
15075 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
15076 if end > li {
15077 h_ready = true;
15078 chunk_skip_until = end;
15079 if self.is_loop_end(end - 1) && end < self.num_layers {
15082 for bi in 0..b {
15083 let normed = inference::rms_norm(
15084 &h[bi * hs..(bi + 1) * hs],
15085 &self.weights.final_norm,
15086 eps,
15087 norm_style,
15088 );
15089 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
15090 }
15091 }
15092 continue;
15093 }
15094 }
15095 if !h_ready {
15096 fill_h(&mut h, self);
15097 h_ready = true;
15098 }
15099 if task_mask.is_none() && self.verify_exact_moe {
15100 let positions: Vec<_> = (start_pos..start_pos + b).collect();
15101 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
15102 crate::gpu::BatchGraphOutcome::Completed => continue,
15103 crate::gpu::BatchGraphOutcome::Failed => return h,
15104 crate::gpu::BatchGraphOutcome::Declined => {}
15105 }
15106 }
15107 #[cfg(feature = "gpu")]
15108 self.pull_lagging_host_kv(li, li + 1, start_pos);
15109 let mhc_attn = self.mhc_layer(li).then(|| {
15113 let m = self.mhc.as_deref().expect("mhc layer");
15114 let p = self.phys_layer(li);
15115 let n = m.s.streams * hs;
15116 mhc_rs.resize(b * n, 0.0);
15117 let coefs: Vec<_> = (0..b)
15118 .map(|bi| {
15119 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15120 if m.enters(p) {
15121 m.enter(r, &h[bi * hs..(bi + 1) * hs]);
15122 }
15123 m.route(p, false, r, &mut h[bi * hs..(bi + 1) * hs])
15124 })
15125 .collect();
15126 (h.clone(), coefs)
15127 });
15128 let lw = &self.weights.layers[self.phys_layer(li)];
15129 let t_attn = std::time::Instant::now();
15130 match &lw.attn {
15132 AttnKind::Kda(w) => {
15133 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15135 let mut normed = vec![0.0f32; b * hs];
15136 for bi in 0..b {
15137 inference::rms_norm_into(
15138 &h[bi * hs..(bi + 1) * hs],
15139 &lw.input_norm,
15140 eps,
15141 norm_style,
15142 &mut normed[bi * hs..(bi + 1) * hs],
15143 );
15144 }
15145 let attn = crate::linear_core::kda_forward_batch(
15146 &normed,
15147 b,
15148 w,
15149 &cfg,
15150 &mut self.kv_cache.layers[li].linear_state,
15151 pool.as_deref(),
15152 );
15153 for (dst, &a) in h.iter_mut().zip(&attn) {
15154 *dst += a;
15155 }
15156 }
15157 AttnKind::LinearGdn(w) => {
15158 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15160 let mut normed = vec![0.0f32; b * hs];
15161 for bi in 0..b {
15162 let r = inference::rms_norm(
15163 &h[bi * hs..(bi + 1) * hs],
15164 &lw.input_norm,
15165 eps,
15166 norm_style,
15167 );
15168 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15169 }
15170 let attn = crate::linear_core::gdn_forward_batch(
15171 &normed,
15172 b,
15173 w,
15174 &cfg,
15175 &mut self.kv_cache.layers[li].linear_state,
15176 pool.as_deref(),
15177 );
15178 for (dst, &a) in h.iter_mut().zip(&attn) {
15179 *dst += a;
15180 }
15181 }
15182 AttnKind::ShortConv(w) => {
15183 let cfg = self
15186 .short_conv_cfg
15187 .expect("short-conv layer without short_conv_cfg");
15188 let mut normed = vec![0.0f32; b * hs];
15189 for bi in 0..b {
15190 inference::rms_norm_into(
15191 &h[bi * hs..(bi + 1) * hs],
15192 &lw.input_norm,
15193 eps,
15194 norm_style,
15195 &mut normed[bi * hs..(bi + 1) * hs],
15196 );
15197 }
15198 let mut attn = short_conv_forward_batch(
15199 &normed,
15200 b,
15201 w,
15202 &cfg,
15203 &mut self.kv_cache.layers[li].linear_state,
15204 pool.as_deref(),
15205 );
15206 if let Some(wn) = &lw.attn_out_norm {
15209 for bi in 0..b {
15210 inference::rms_norm_into(
15211 &attn[bi * hs..(bi + 1) * hs],
15212 wn,
15213 eps,
15214 norm_style,
15215 &mut normed[bi * hs..(bi + 1) * hs],
15216 );
15217 }
15218 attn.copy_from_slice(&normed);
15219 }
15220 for (dst, &a) in h.iter_mut().zip(&attn) {
15221 *dst += a;
15222 }
15223 }
15224 AttnKind::Mla(w) => {
15225 let inv_freq_l = self.layer_inv_freq(li);
15228 let rs = self.layer_rope_scale(li);
15229 let mut normed = vec![0.0f32; hs];
15230 for bi in 0..b {
15231 inference::rms_norm_into(
15232 &h[bi * hs..(bi + 1) * hs],
15233 &lw.input_norm,
15234 eps,
15235 norm_style,
15236 &mut normed,
15237 );
15238 let ao = mla_attention(
15239 w,
15240 &normed,
15241 &mut self.kv_cache.layers[li],
15242 start_pos + bi,
15243 &inv_freq_l,
15244 rs,
15245 eps,
15246 pool.as_deref(),
15247 );
15248 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
15249 *dst += a;
15250 }
15251 }
15252 }
15253 AttnKind::Full {
15254 wq,
15255 wk,
15256 wv,
15257 wo,
15258 q_norm,
15259 k_norm,
15260 output_gate,
15261 softplus_gate,
15262 bias,
15263 } => {
15264 let mut normed = vec![0.0f32; b * hs];
15268 for bi in 0..b {
15269 inference::rms_norm_into(
15270 &h[bi * hs..(bi + 1) * hs],
15271 &lw.input_norm,
15272 eps,
15273 norm_style,
15274 &mut normed[bi * hs..(bi + 1) * hs],
15275 );
15276 }
15277 let vres = match self.layer_value_blend(li) {
15279 Some(blend) => {
15280 vres_lerp(&self.vres_e0, &normed, blend, &mut self.vres_in);
15281 true
15282 }
15283 None => false,
15284 };
15285 let inv_freq_l = self.layer_inv_freq(li);
15286 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15287 let cfg = QwenAttnCfg {
15288 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
15289 num_heads: self.layer_num_heads(li),
15290 num_kv_heads: nkv_l,
15291 head_dim: hd_l,
15292 hidden_size: hs,
15293 position: start_pos,
15294 inv_freq: &inv_freq_l,
15295 rotary_dim: rd_l,
15296 scale: self.attn_scale,
15297 softcap: self.attn_softcap,
15298 window: self.layer_window(li),
15299 v_norm: self.attn_v_norm,
15300 qk_norm_after_rope: self.qk_norm_after_rope,
15301 gate_sigmoid: self.proj_gate_sigmoid,
15302 q_norm: q_norm.as_deref(),
15303 k_norm: k_norm.as_deref(),
15304 output_gate: *output_gate,
15305 softplus_gate: softplus_gate
15306 .as_ref()
15307 .map(|(gate, per_head)| (gate, *per_head)),
15308 rope_scale: self.layer_rope_scale(li),
15309 bias: bias
15310 .as_ref()
15311 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15312 rms_eps: eps,
15313 norm_style,
15314 pool: pool.as_deref(),
15315 v_head_dim: self.layer_v_dim(li),
15316 value_in: vres.then(|| self.vres_in.as_slice()),
15317 };
15318 #[cfg(feature = "gpu")]
15319 let _mirror = self
15320 .prefill_mirror_target(li)
15321 .map(crate::gpu::enter_prefill_mirror);
15322 let mut attn = attention::qwen_attention_batch(
15323 &normed,
15324 b,
15325 wq,
15326 wk,
15327 wv,
15328 wo,
15329 &mut self.kv_cache.layers[li],
15330 &cfg,
15331 );
15332 if let Some(w) = &lw.attn_out_norm {
15333 for bi in 0..b {
15334 inference::rms_norm_into(
15335 &attn[bi * hs..(bi + 1) * hs],
15336 w,
15337 eps,
15338 norm_style,
15339 &mut normed[bi * hs..(bi + 1) * hs],
15340 );
15341 }
15342 attn.copy_from_slice(&normed);
15343 }
15344 for (dst, &a) in h.iter_mut().zip(&attn) {
15345 *dst += a;
15346 }
15347 }
15348 AttnKind::Bounded(w) => {
15349 let mut normed = vec![0.0f32; b * hs];
15352 for bi in 0..b {
15353 inference::rms_norm_into(
15354 &h[bi * hs..(bi + 1) * hs],
15355 &lw.input_norm,
15356 eps,
15357 norm_style,
15358 &mut normed[bi * hs..(bi + 1) * hs],
15359 );
15360 }
15361 let rope = self
15362 .bounded_rope
15363 .clone()
15364 .expect("bounded layer without an installed rotation table");
15365 let cfg = crate::bounded::BoundedAttnCfg {
15366 num_heads: self.num_heads,
15367 num_kv_heads: self.num_kv_heads,
15368 head_dim: self.head_dim,
15369 hidden_size: hs,
15370 scale: self.attn_scale,
15371 rope: &rope,
15372 pool: pool.as_deref(),
15373 };
15374 let mut attn = crate::bounded::bounded_attention_batch(
15375 &normed,
15376 b,
15377 w,
15378 &mut self.kv_cache.layers[li],
15379 &cfg,
15380 );
15381 if let Some(wn) = &lw.attn_out_norm {
15382 for bi in 0..b {
15383 inference::rms_norm_into(
15384 &attn[bi * hs..(bi + 1) * hs],
15385 wn,
15386 eps,
15387 norm_style,
15388 &mut normed[bi * hs..(bi + 1) * hs],
15389 );
15390 }
15391 attn.copy_from_slice(&normed);
15392 }
15393 for (dst, &a) in h.iter_mut().zip(&attn) {
15394 *dst += a;
15395 }
15396 attention::recycle_buf(&mut attn);
15397 }
15398 AttnKind::Linear(w) => {
15399 for bi in 0..b {
15400 let normed = inference::rms_norm(
15401 &h[bi * hs..(bi + 1) * hs],
15402 &lw.input_norm,
15403 eps,
15404 norm_style,
15405 );
15406 vmf_phase_forward(
15407 &normed,
15408 w,
15409 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
15410 &mut self.kv_cache.layers[li].linear_state,
15411 pool.as_deref(),
15412 )
15413 .iter()
15414 .enumerate()
15415 .for_each(|(i, &a)| h[bi * hs + i] += a);
15416 }
15417 }
15418 }
15419
15420 let mhc_ffn = mhc_attn.map(|(branch, coefs)| {
15423 let m = self.mhc.as_deref().expect("mhc layer");
15424 let p = self.phys_layer(li);
15425 let n = m.s.streams * hs;
15426 let mut y = vec![0f32; hs];
15427 (0..b)
15428 .map(|bi| {
15429 for (e, v) in y.iter_mut().enumerate() {
15430 *v = h[bi * hs + e] - branch[bi * hs + e];
15431 }
15432 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15433 m.merge(r, &y, &coefs[bi]);
15434 m.route(p, true, r, &mut h[bi * hs..(bi + 1) * hs])
15435 })
15436 .collect::<Vec<_>>()
15437 });
15438 let lw = &self.weights.layers[self.phys_layer(li)];
15440 let mut post = vec![0.0f32; b * hs];
15441 for bi in 0..b {
15442 let r =
15443 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
15444 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15445 }
15446 let mask_row = task_mask
15449 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
15450 .and_then(|m| m.ffn_masks.get(li))
15451 .map(|v| v.as_slice());
15452 let attn_ns = t_attn.elapsed().as_nanos() as u64;
15453 let t_ffn = std::time::Instant::now();
15454 let mut ffn = match &lw.ffn {
15455 FfnKind::Dense(d) if !d.segs.is_empty() => {
15456 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
15457 }
15458 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
15459 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
15460 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
15461 }
15462 FfnKind::Moe(m) if self.verify_exact_moe => {
15463 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
15464 }
15465 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
15468 let before = m.stats.borrow().clone();
15469 let out = crate::gpu::cpu_scope(|| {
15470 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
15471 });
15472 self.mimo_moe.prime(li, m, &before);
15473 out
15474 }
15475 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
15476 FfnKind::DenseMoe(dm) => {
15479 let mut out = vec![0.0f32; b * hs];
15480 for bi in 0..b {
15481 let r = dense_moe_ffn(
15482 dm,
15483 &post[bi * hs..(bi + 1) * hs],
15484 &h[bi * hs..(bi + 1) * hs],
15485 eps,
15486 norm_style,
15487 pool.as_deref(),
15488 );
15489 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
15490 }
15491 out
15492 }
15493 };
15494 if prefill_prof_on() {
15495 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
15496 PREFILL_SPLIT[1].fetch_add(
15497 t_ffn.elapsed().as_nanos() as u64,
15498 std::sync::atomic::Ordering::Relaxed,
15499 );
15500 }
15501 if let Some(w) = &lw.ffn_out_norm {
15502 for bi in 0..b {
15503 inference::rms_norm_into(
15504 &ffn[bi * hs..(bi + 1) * hs],
15505 w,
15506 eps,
15507 norm_style,
15508 &mut post[bi * hs..(bi + 1) * hs],
15509 );
15510 }
15511 ffn.copy_from_slice(&post);
15512 }
15513 if let Some(coefs) = mhc_ffn {
15514 let m = self.mhc.as_deref().expect("mhc layer");
15517 let p = self.phys_layer(li);
15518 let n = m.s.streams * hs;
15519 for bi in 0..b {
15520 let r = &mut mhc_rs[bi * n..(bi + 1) * n];
15521 m.merge(r, &ffn[bi * hs..(bi + 1) * hs], &coefs[bi]);
15522 if m.leaves(p) {
15523 m.leave(r, &mut h[bi * hs..(bi + 1) * hs]);
15524 }
15525 }
15526 } else {
15527 for (dst, &f) in h.iter_mut().zip(&ffn) {
15528 *dst += f;
15529 }
15530 }
15531 if let Some(sc) = lw.layer_scale {
15532 for v in h.iter_mut() {
15533 *v *= sc;
15534 }
15535 }
15536 if self.layer_dump.is_some() {
15538 for bi in 0..b {
15539 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
15540 }
15541 }
15542 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15543 if let Ok(t) = tp.parse::<usize>() {
15544 if t >= start_pos && t < start_pos + b {
15545 let bi = t - start_pos;
15546 let row = &h[bi * hs..(bi + 1) * hs];
15547 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
15548 eprintln!(
15549 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15550 row[0], row[1]
15551 );
15552 }
15553 }
15554 }
15555 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
15559 let row = &h[(b - 1) * hs..b * hs];
15560 let rms =
15561 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
15562 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
15563 eprintln!(
15564 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
15565 match &self.weights.layers[self.phys_layer(li)].attn {
15566 AttnKind::LinearGdn(_) => "gdn",
15567 AttnKind::Linear(_) => "vmf",
15568 AttnKind::ShortConv(_) => "conv",
15569 _ => "attn",
15570 },
15571 match &lw.ffn {
15572 FfnKind::Moe(_) => "moe",
15573 FfnKind::Dense(_) => "dense",
15574 FfnKind::DenseMoe(_) => "dense+moe",
15575 },
15576 );
15577 }
15578 if self.is_loop_end(li) && li + 1 < self.num_layers {
15580 for bi in 0..b {
15581 let normed = inference::rms_norm(
15582 &h[bi * hs..(bi + 1) * hs],
15583 &self.weights.final_norm,
15584 eps,
15585 norm_style,
15586 );
15587 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
15588 }
15589 }
15590 if std::env::var("CMF_TRACE_H").is_ok() {
15591 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
15592 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
15593 eprintln!(
15594 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
15595 lw.layer_scale
15596 );
15597 }
15598 }
15599 crate::gpu::set_layer(-1); if prefill_prof_on() {
15601 let ms = |a: &std::sync::atomic::AtomicU64| {
15602 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
15603 };
15604 let sp = &attention::ATTN_SPLIT;
15605 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
15606 eprintln!(
15607 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
15608 host gate {:.1}], host loop {:.1}, attend {:.1} \
15609 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
15610 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
15611 ms(&PREFILL_SPLIT[0]),
15612 ms(&sp[0]),
15613 qkv_enq,
15614 qkv_wait,
15615 ms(&sp[6]),
15616 ms(&sp[1]),
15617 ms(&sp[2]),
15618 ms(&sp[4]),
15619 ms(&sp[5]),
15620 ca_wait,
15621 ms(&sp[3]),
15622 ms(&PREFILL_SPLIT[1]),
15623 );
15624 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
15625 eprintln!(
15626 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
15627 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
15628 fallbacks (cumulative)",
15629 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
15630 );
15631 }
15632 self.o1_progress();
15637 self.swa_trim_tails();
15639 h
15640 }
15641
15642 fn embed_single(&self, id: u32) -> Vec<f32> {
15644 let mut out = vec![0.0f32; self.hidden_size];
15645 if (id as usize) < self.weights.embed_tokens.rows() {
15646 self.weights.embed_tokens.row_f32(id as usize, &mut out);
15647 }
15648 if self.embed_multiplier != 1.0 {
15649 for v in out.iter_mut() {
15650 *v *= self.embed_multiplier;
15651 }
15652 }
15653 if self.dsv4.is_some() || self.dsv41.is_some() || self.dory.is_some() || self.qwen4_exp.is_some() {
15657 let mut v = vec![0.0f32; self.hidden_size.max(1)];
15658 v[0] = id as f32;
15659 return v;
15660 }
15661 if let Some(b) = &self.g4e {
15665 return b.0.extend_embedding(id, &out, self.pool.as_deref());
15666 }
15667 if let Some(b) = &self.g3n {
15668 return b.0.extend_embedding(id, &out, self.pool.as_deref());
15669 }
15670 out
15671 }
15672
15673 #[cfg(target_os = "macos")]
15679 fn chunk_run_gpu(
15680 &mut self,
15681 li0: usize,
15682 h: &mut [f32],
15683 b: usize,
15684 pos0: usize,
15685 embed_ids: Option<&[u32]>,
15686 cap: usize,
15687 ) -> usize {
15688 let gemma4 =
15695 self.metal_gemma4() && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
15696 if !crate::gpu::enabled_here()
15697 || std::env::var("CMF_GPU_CHUNK")
15698 .map(|v| v == "0")
15699 .unwrap_or(false)
15700 || (b < self.chunk_min_rows && self.dash.is_none() && !self.o1_chunk_prefix)
15701 || self.prompt_mrope.is_some()
15703 || self.value_blend.is_some()
15705 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
15708 || (self.global_attn.is_some() && !gemma4)
15709 || self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
15713 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
15716 || (self.o1_active() && !self.o1_chunk_prefix)
15720 || (self.attn_v_norm && !gemma4)
15721 || !self.metal_rope_scale_ok()
15722 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
15723 {
15724 return li0;
15725 }
15726 let Some(model) = self.model.clone() else {
15727 return li0;
15728 };
15729 let (nh, nkv, hd, hs) = (
15730 self.num_heads,
15731 self.num_kv_heads,
15732 self.head_dim,
15733 self.hidden_size,
15734 );
15735 let loop_end = if self.loop_final_norm {
15739 ((li0 / self.physical_layers) + 1) * self.physical_layers
15740 } else {
15741 self.num_layers
15742 };
15743 let run_end = self.num_layers.min(loop_end).min(cap);
15744 let tables: Vec<std::sync::Arc<Vec<f32>>> =
15749 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
15750 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
15751 let mut stored_at: Vec<usize> = Vec::new();
15752 for li in li0..run_end {
15753 if self.mhc_layer(li) {
15754 break;
15755 }
15756 let lw = &self.weights.layers[self.phys_layer(li)];
15757 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
15758 && !gemma4
15759 {
15760 break;
15761 }
15762 let AttnKind::Full {
15763 wq,
15764 wk,
15765 wv,
15766 wo,
15767 q_norm,
15768 k_norm,
15769 output_gate: false,
15770 softplus_gate,
15771 bias,
15772 } = &lw.attn
15773 else {
15774 break;
15775 };
15776 let head_gate = match softplus_gate {
15778 None => None,
15779 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
15780 Some((d, r, c)) if r == nh && c == hs => Some(d),
15781 _ => break,
15782 },
15783 Some(_) => break,
15784 };
15785 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
15789 FfnKind::Dense(d)
15790 if (matches!(d.act, Act::Silu | Act::Gelu)
15791 || (gemma4 && d.act == Act::GeluTanh))
15792 && d.segs.is_empty() =>
15793 {
15794 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
15795 }
15796 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
15797 Some((router, experts, q8)) => {
15798 let e0 = &m.experts[0];
15799 let route = Box::new(move |lg: &[f32]| {
15800 let (idx, p, wsum) = moe_route(lg, m, None);
15802 let mut st = m.stats.borrow_mut();
15803 if st.len() < m.experts.len() {
15804 st.resize(m.experts.len(), 0);
15805 }
15806 for &e in &idx {
15807 st[e] += 1;
15808 }
15809 let w = idx.iter().map(|&e| p[e] / wsum).collect();
15810 (idx, w)
15811 });
15812 let detached = m.shared.as_ref().map(|(s, _)| {
15813 let t = |x: &QTensor| {
15814 x.q8_2f_parts().map(|p| p.0).or_else(|| x.q4tp_parts().map(|p| p.0)).unwrap_or(usize::MAX)
15815 };
15816 ([t(&s.gate_proj), t(&s.up_proj), t(&s.down_proj)], s.gate_proj.rows())
15817 });
15818 (
15819 &e0.gate_proj,
15820 &e0.up_proj,
15821 &e0.down_proj,
15822 Act::Silu,
15823 Some(crate::gpu_metal::ChunkMoe {
15824 router,
15825 experts,
15826 q8,
15827 route,
15828 detached,
15829 }),
15830 )
15831 }
15832 None => break,
15833 },
15834 _ => break,
15835 };
15836 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
15842 t.q8_row_parts()
15843 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
15844 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
15845 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
15846 }
15847 fn cf(t: &QTensor) -> &[f32] {
15848 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
15849 }
15850 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
15851 let ffn_col = [cf(gate_t), cf(up_t), cf(down_t)];
15852 if moe.is_none()
15862 && !gemma4
15863 && self.dash.is_none()
15864 && ffn_act != Act::Gelu
15865 && (col.iter().any(|c| !c.is_empty()) || ffn_col.iter().any(|c| !c.is_empty()))
15866 {
15867 break;
15868 }
15869 let parts = (
15870 cw(wq),
15871 cw(wk),
15872 cw(wv),
15873 cw(wo),
15874 cw(gate_t),
15875 cw(up_t),
15876 cw(down_t),
15877 );
15878 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
15879 else {
15880 break;
15881 };
15882 let layer = &self.kv_cache.layers[li];
15883 if layer.mode != crate::kv_cache::KvMode::F32
15884 || (layer.o1.is_some() && !(self.o1_chunk_prefix && layer.o1_is_collecting()))
15885 {
15886 break;
15887 }
15888 debug_assert!(
15890 layer.base() == 0
15891 || self
15892 .layer_window(li)
15893 .is_some_and(|w| layer.head_len(0) + 1 >= w)
15894 );
15895 stored_at.push(layer.head_len(0));
15896 let (nkv, hd) = if gemma4 {
15899 let (a, bb, _) = self.layer_geom(li);
15900 (a, bb)
15901 } else {
15902 (nkv, hd)
15903 };
15904 layers.push(crate::gpu_metal::ChunkLayer {
15905 model: &model,
15906 kv_id: self.graph_kv_id,
15907 layer: li,
15908 wq: pq,
15909 wk: pk,
15910 wv: pv,
15911 wo: po,
15912 gate: pg,
15913 up: pu,
15914 down: pd,
15915 input_norm: &lw.input_norm,
15916 post_norm: &lw.post_norm,
15917 bias: bias
15918 .as_ref()
15919 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
15920 q_norm: q_norm.as_deref(),
15921 k_norm: k_norm.as_deref(),
15922 inv_freq: &tables[li - li0],
15923 rope_scale: self.layer_rope_scale(li),
15924 rd: self.layer_geom(li).2,
15925 nh,
15926 nkv,
15927 hd,
15928 hs,
15929 inter: gate_t.rows(),
15930 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
15931 late_qk_norm: self.qk_norm_after_rope,
15932 eps: self.rms_eps as f32,
15933 window: self.layer_window(li),
15934 head_gate,
15935 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
15936 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
15937 v_norm: self.attn_v_norm,
15938 scale: self.attn_scale,
15939 attn_out_norm: lw.attn_out_norm.as_deref(),
15940 ffn_out_norm: lw.ffn_out_norm.as_deref(),
15941 layer_scale: lw.layer_scale,
15942 }),
15943 col,
15944 ffn_col: if moe.is_none() && !gemma4 {
15947 ffn_col
15948 } else {
15949 [&[][..]; 3]
15950 },
15951 moe,
15952 dash: self.graph_dash(li),
15953 });
15954 }
15955 if layers.is_empty() {
15956 return li0;
15957 }
15958 if self.dash.is_some() && (li0 != 0 || layers.len() != self.num_layers) {
15960 return li0;
15961 }
15962 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
15964 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
15965 .iter()
15966 .zip(&rows_kv)
15967 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
15968 .collect();
15969 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
15970 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
15971 let li = layers[i].layer;
15972 let layer = &self.kv_cache.layers[li];
15973 let nkv = rows_kv[i].0;
15974 io.push(crate::gpu_metal::ChunkIo {
15975 cpu_stored: stored_at[i],
15976 cpu_gen: layer.generation(),
15977 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
15978 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
15979 out_k: ok,
15980 out_v: ov,
15981 imp: oi,
15982 });
15983 }
15984 let n_run = layers.len();
15985 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
15986 let ep = embed_ids.and_then(|ids| {
15989 self.weights
15990 .embed_tokens
15991 .q8_row_parts()
15992 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
15993 idx,
15994 rows,
15995 row_scale: rs,
15996 ids,
15997 mult: self.embed_multiplier,
15998 })
15999 });
16000 if embed_ids.is_some() && ep.is_none() {
16001 return li0;
16002 }
16003 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
16004 return li0;
16005 }
16006 drop(io);
16007 drop(layers);
16008 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
16011 let li = li0 + i;
16012 let row = rows_kv[i].1;
16013 let layer = &mut self.kv_cache.layers[li];
16014 for bi in 0..b {
16015 layer.append(
16016 &ok[bi * row..(bi + 1) * row],
16017 &ov[bi * row..(bi + 1) * row],
16018 &[],
16019 );
16020 }
16021 layer.accumulate_imp(oi);
16022 }
16023 last
16024 }
16025
16026 fn layer_is_local(&self, li: usize) -> bool {
16029 if let Some(layers) = &self.sliding_layers {
16030 return layers.get(li).copied().unwrap_or(false);
16031 }
16032 match self.swa {
16033 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
16034 None => false,
16035 }
16036 }
16037
16038 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
16041 if self.layer_is_local(li) {
16042 if let Some(f) = &self.inv_freq_local {
16043 return f.clone();
16044 }
16045 } else if let Some(f) = &self.inv_freq_global {
16046 return f.clone();
16047 }
16048 self.inv_freq.clone()
16049 }
16050
16051 fn layer_value_blend(&self, li: usize) -> Option<f32> {
16053 self.value_blend
16054 .as_ref()
16055 .and_then(|v| v.get(self.phys_layer(li)).copied())
16056 }
16057
16058 fn layer_window(&self, li: usize) -> Option<usize> {
16060 self.swa
16061 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
16062 }
16063
16064 fn swa_trim_tails(&mut self) {
16073 let Some((slack, align)) = self.swa_trim else {
16074 return;
16075 };
16076 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
16077 return;
16078 }
16079 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
16080 if let Some(w) = self.layer_window(li) {
16081 self.kv_cache.layers[li].trim_window_anchored(w, slack, align, self.swa_trim_anchor);
16082 }
16083 }
16084 }
16085
16086 fn layer_num_heads(&self, li: usize) -> usize {
16087 self.attention_heads_per_layer
16088 .as_ref()
16089 .and_then(|v| v.get(li).copied())
16090 .unwrap_or(self.num_heads)
16091 }
16092
16093 fn layer_rope_scale(&self, li: usize) -> f32 {
16094 if self.layer_is_local(li) {
16095 self.rope_scale_local
16096 } else {
16097 self.rope_scale
16098 }
16099 }
16100
16101 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
16104 if !self.layer_is_local(li) {
16105 if let Some((ghd, gkv)) = self.global_attn {
16106 return (gkv, ghd, ghd);
16107 }
16108 }
16109 let rd = if self.layer_is_local(li) {
16110 self.rotary_dim_local.unwrap_or(self.rotary_dim)
16111 } else {
16112 self.rotary_dim
16113 };
16114 let rd = if self
16116 .no_rope_layers
16117 .as_ref()
16118 .and_then(|v| v.get(li))
16119 .copied()
16120 == Some(true)
16121 {
16122 0
16123 } else {
16124 rd
16125 };
16126 (self.layer_num_kv_heads(li), self.head_dim, rd)
16127 }
16128
16129 fn layer_num_kv_heads(&self, li: usize) -> usize {
16132 self.kv_heads_per_layer
16133 .as_ref()
16134 .and_then(|v| v.get(self.phys_layer(li)).copied())
16135 .unwrap_or(self.num_kv_heads)
16136 }
16137
16138 fn layer_v_dim(&self, li: usize) -> usize {
16140 let (_, hd, _) = self.layer_geom(li);
16141 self.v_head_dim.unwrap_or(hd).min(hd)
16142 }
16143
16144 pub fn set_attn_geometry(
16153 &mut self,
16154 kv_heads_per_layer: Option<Vec<usize>>,
16155 v_head_dim: Option<usize>,
16156 ) -> Result<(), String> {
16157 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
16158 if self.global_attn.is_some() {
16159 return Err(
16160 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
16161 attention geometry"
16162 .into(),
16163 );
16164 }
16165 if self
16166 .weights
16167 .layers
16168 .iter()
16169 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
16170 {
16171 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
16172 }
16173 }
16174 if let Some(vd) = v_head_dim {
16175 if vd == 0 || vd > self.head_dim {
16176 return Err(format!(
16177 "v_head_dim {vd} must be in 1..={} (head_dim)",
16178 self.head_dim
16179 ));
16180 }
16181 }
16182 if let Some(v) = &kv_heads_per_layer {
16183 if v.len() != self.physical_layers {
16184 return Err(format!(
16185 "kv_heads_per_layer has {} entries, expected {} layers",
16186 v.len(),
16187 self.physical_layers
16188 ));
16189 }
16190 for (li, &nkv) in v.iter().enumerate() {
16191 let is_attn = matches!(
16192 self.weights.layers.get(li).map(|lw| &lw.attn),
16193 Some(AttnKind::Full { .. }) | None
16194 );
16195 if !is_attn {
16196 continue;
16197 }
16198 let nh = self
16199 .attention_heads_per_layer
16200 .as_ref()
16201 .and_then(|h| h.get(li).copied())
16202 .unwrap_or(self.num_heads);
16203 if nkv == 0 || nh % nkv != 0 {
16204 return Err(format!(
16205 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
16206 ));
16207 }
16208 }
16209 }
16210 self.kv_heads_per_layer = kv_heads_per_layer;
16211 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
16212 if self.kv_heads_per_layer.is_some() {
16213 for li in 0..self.kv_cache.layers.len() {
16214 let full = matches!(
16215 self.weights
16216 .layers
16217 .get(self.phys_layer(li))
16218 .map(|lw| &lw.attn),
16219 Some(AttnKind::Full { .. })
16220 );
16221 let nkv = self.layer_num_kv_heads(li);
16222 let cache = &self.kv_cache.layers[li];
16223 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
16224 let sinks = cache.sinks.clone();
16225 self.kv_cache.layers[li] =
16226 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
16227 self.kv_cache.layers[li].sinks = sinks;
16228 }
16229 }
16230 }
16231 Ok(())
16232 }
16233
16234 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
16238 let Some(lw) = self.weights.layers.get(phys) else {
16239 return Err(format!("sinks for layer {phys}: no such layer"));
16240 };
16241 if !matches!(lw.attn, AttnKind::Full { .. }) {
16242 return Err(format!(
16243 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
16244 ));
16245 }
16246 let nh = self
16247 .attention_heads_per_layer
16248 .as_ref()
16249 .and_then(|h| h.get(phys).copied())
16250 .unwrap_or(self.num_heads);
16251 if sinks.len() != nh {
16252 return Err(format!(
16253 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
16254 sinks.len()
16255 ));
16256 }
16257 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
16258 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
16259 }
16260 for li in 0..self.kv_cache.layers.len() {
16261 if self.phys_layer(li) == phys {
16262 self.kv_cache.layers[li].sinks = Some(sinks.clone());
16263 }
16264 }
16265 Ok(())
16266 }
16267
16268 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
16278 if self.kv_heads_per_layer.is_some() {
16279 return Some("per-layer KV head counts");
16280 }
16281 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
16282 return Some("V heads narrower than Q/K heads");
16283 }
16284 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
16285 return Some("learned attention sinks");
16286 }
16287 if self.swa.is_some() || self.sliding_layers.is_some() {
16288 return Some("sliding-window layers");
16289 }
16290 None
16291 }
16292
16293 #[cfg(target_os = "macos")]
16301 fn metal_rope_scale_ok(&self) -> bool {
16302 !(self.qk_norm_after_rope
16303 && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
16304 }
16305
16306 #[cfg(target_os = "macos")]
16323 fn metal_graph_swa(&self) -> bool {
16324 (self.swa.is_some() || self.sliding_layers.is_some())
16325 && self.attention_heads_per_layer.is_none()
16326 && !self.attn_v_norm
16327 && self.attn_softcap == 0.0
16328 && self.kv_heads_per_layer.is_none()
16329 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
16330 && self.global_attn.is_none()
16331 && self.inv_freq_global.is_none()
16332 && self.metal_rope_scale_ok()
16333 && !(0..self.num_layers).any(|li| {
16334 self.layer_is_local(li)
16335 && self.inv_freq_local.is_none()
16336 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16337 })
16338 }
16339
16340 #[cfg(target_os = "macos")]
16348 fn metal_gemma4(&self) -> bool {
16349 crate::gpu_metal::gemma4::enabled()
16350 && (self.global_attn.is_some() || self.attn_v_norm)
16351 && self.attention_heads_per_layer.is_none()
16352 && self.kv_heads_per_layer.is_none()
16353 && self.v_head_dim.is_none()
16354 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
16355 && self.attn_softcap == 0.0
16356 && !self.qk_norm_after_rope
16357 && self.metal_rope_scale_ok()
16358 && self
16359 .weights
16360 .layers
16361 .iter()
16362 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
16363 && !(0..self.num_layers).any(|li| {
16364 self.layer_is_local(li)
16365 && self.inv_freq_local.is_none()
16366 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16367 })
16368 }
16369
16370 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
16377 if self.value_blend.is_some() && !crate::gpu::vres_graph_on() {
16378 return Some("value residual");
16379 }
16380 let geometry_reason = self.graph_attn_decline_reason();
16384 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
16385 if has_scaled_rope && geometry_reason.is_none() {
16386 return Some("scaled RoPE without per-layer geometry");
16387 }
16388 if has_scaled_rope && self.qk_norm_after_rope {
16393 return Some("scaled RoPE with post-RoPE Q/K norm");
16394 }
16395 geometry_reason?;
16396 if self.attention_heads_per_layer.is_some() {
16402 return Some("per-layer Q head counts with per-layer geometry");
16403 }
16404 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
16409 let plain_norms = self.norm_style == cortiq_core::NormStyle::Qwen;
16410 if !gemma4
16411 && self.weights.layers.iter().any(|lw| {
16412 lw.layer_scale.is_some()
16413 || (!plain_norms && (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some()))
16414 })
16415 {
16416 return Some("sandwich norms / layer scale with per-layer geometry");
16417 }
16418 if self.weights.layers.iter().any(|lw| {
16422 matches!(
16423 &lw.attn,
16424 AttnKind::Full {
16425 output_gate: true,
16426 ..
16427 }
16428 )
16429 }) && self.v_head_dim.is_some()
16430 {
16431 return Some("gated attention with V narrower than K");
16432 }
16433 if (0..self.num_layers).any(|li| {
16434 self.layer_is_local(li)
16435 && self.inv_freq_local.is_none()
16436 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
16437 }) {
16438 return Some("local rotary width without a local RoPE table");
16439 }
16440 None
16441 }
16442
16443 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
16447 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
16450 return None;
16451 }
16452 if self.global_attn.is_some() {
16453 return Some("per-layer head width (Gemma-4 global layers)");
16454 }
16455 if self.attn_v_norm {
16456 return Some("V norm");
16457 }
16458 if self.weights.layers.iter().any(|lw| lw.layer_scale.is_some()) {
16460 return Some("layer scale");
16461 }
16462 if self.value_blend.is_none()
16463 && self
16464 .weights
16465 .layers
16466 .iter()
16467 .any(|lw| lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some())
16468 {
16469 return Some("sandwich norms");
16470 }
16471 None
16472 }
16473
16474 fn graph_dash(&self, li: usize) -> Option<crate::gpu::GraphDash<'_>> {
16480 self.dash.as_deref().map(|d| {
16481 let l = &d.layers[li];
16482 crate::gpu::GraphDash {
16483 qk_proj: &l.qk_proj,
16484 q_norm: &l.q_norm,
16485 k_norm: &l.k_norm,
16486 head_cls: &l.head_cls,
16487 idx_scale: l.route.idx_scale,
16488 floor: l.route.floor,
16489 sigma: l.route.sigma,
16490 k: l.route.k,
16491 block: d.block,
16492 local_blocks: d.local_blocks,
16493 nblk: d.allow.len(),
16494 cands: &d.cands,
16495 coff: &d.coff,
16496 angles: &d.angles,
16497 }
16498 })
16499 }
16500
16501 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
16502 if self.dash.is_none() {
16503 self.graph_attn_decline_reason()?;
16504 }
16505 let (nkv, hd, rd) = self.layer_geom(li);
16506 let dash = self.graph_dash(li);
16507 let invf: &[f32] = if let Some(d) = self.dash.as_deref() {
16508 &d.angles
16509 } else if self.layer_is_local(li) {
16510 match &self.inv_freq_local {
16511 Some(f) => f.as_slice(),
16512 None => self.inv_freq.as_slice(),
16513 }
16514 } else {
16515 match &self.inv_freq_global {
16516 Some(f) => f.as_slice(),
16517 None => self.inv_freq.as_slice(),
16518 }
16519 };
16520 Some(crate::gpu::GraphAttnGeom {
16521 nkv,
16522 hd,
16523 dv: self.layer_v_dim(li),
16524 rd,
16525 invf,
16526 rope_scale: self.layer_rope_scale(li),
16527 window: self.layer_window(li),
16528 sink: self.kv_cache.layers[li].sinks.as_deref(),
16529 v_norm: self.attn_v_norm,
16530 dash,
16531 vres: None,
16532 })
16533 }
16534
16535 fn graph_attn_geom_vres<'a>(&'a self, li: usize, e0: &'a [f32]) -> Option<crate::gpu::GraphAttnGeom<'a>> {
16538 let mut g = self.graph_attn_geom(li)?;
16539 g.vres = self.layer_value_blend(li).map(|b| (b, e0));
16540 Some(g)
16541 }
16542
16543 #[cfg(feature = "gpu")]
16555 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
16556 if !self.proj_gate_sigmoid
16557 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
16558 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
16559 || self.graph_refused()
16560 || self.o1_active()
16561 || self.attn_softcap > 0.0
16562 || self.wgpu_graph_attn_decline().is_some()
16563 {
16564 return None;
16565 }
16566 let g = self.graph_attn_geom(li)?;
16567 let (nkv, hd, _) = self.layer_geom(li);
16568 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
16570 return None;
16571 }
16572 Some(crate::gpu::PrefillMirror {
16573 kv_id: self.graph_kv_id,
16574 layer: li,
16575 limit: self.kv_cache.max_seq_len,
16576 })
16577 }
16578
16579 #[cfg(feature = "gpu")]
16588 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
16589 let kv_id = self.graph_kv_id;
16590 for li in from..upto.min(self.num_layers) {
16591 if !matches!(
16592 self.weights.layers[self.phys_layer(li)].attn,
16593 AttnKind::Full { .. }
16594 ) {
16595 continue;
16596 }
16597 let host = self.kv_cache.layers[li].pos_len();
16599 if host >= position {
16600 continue;
16601 }
16602 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
16603 continue;
16604 };
16605 let to = dev.min(position);
16606 if to <= host {
16607 continue;
16608 }
16609 let (nkv, hd) = {
16610 let c = &self.kv_cache.layers[li];
16611 (c.num_kv_heads, c.head_dim)
16612 };
16613 let Some((k, v, first_valid)) =
16614 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
16615 else {
16616 continue;
16617 };
16618 let need_from = match self.layer_window(li) {
16621 Some(w) => host.max((position + 1).saturating_sub(w)),
16622 None => host,
16623 };
16624 if first_valid > need_from {
16625 tracing::warn!(
16626 "layer {li}: device KV rows {host}..{to} no longer resident \
16627 (from {first_valid}); host attention will miss them"
16628 );
16629 }
16630 let row = nkv * hd;
16631 let cache = &mut self.kv_cache.layers[li];
16632 for p in 0..to - host {
16633 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
16634 }
16635 }
16636 }
16637
16638 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
16641 let mut seen = self.graph_declines.borrow_mut();
16642 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
16643 tracing::warn!("{site} declined: {reason} (CPU attention path)");
16644 seen.push((site, reason));
16645 }
16646 }
16647
16648 pub fn graph_declines(&self) -> Vec<String> {
16651 self.graph_declines
16652 .borrow()
16653 .iter()
16654 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
16655 .collect()
16656 }
16657
16658 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
16667 let Some(dir) = &self.layer_dump else {
16668 return;
16669 };
16670 let mut bytes = Vec::with_capacity(row.len() * 4);
16671 for v in row {
16672 bytes.extend_from_slice(&v.to_le_bytes());
16673 }
16674 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
16675 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
16676 use std::sync::atomic::{AtomicBool, Ordering};
16677 static SAID: AtomicBool = AtomicBool::new(false);
16678 if !SAID.swap(true, Ordering::Relaxed) {
16679 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
16680 }
16681 }
16682 }
16683
16684 fn mimo_moe_prepare(&mut self) {
16687 if !self.mimo_moe.is_undecided() {
16688 return;
16689 }
16690 let slot = {
16691 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
16692 .filter_map(
16693 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
16694 FfnKind::Moe(m) => Some((li, m)),
16695 _ => None,
16696 },
16697 )
16698 .collect();
16699 if layers.is_empty()
16702 || self.physical_layers != self.num_layers
16703 || self.gpu_plan.is_some()
16704 {
16705 crate::mimo_moe::Slot::Off
16706 } else {
16707 let graph_prefix =
16711 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
16712 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
16713 }
16714 };
16715 self.mimo_moe = slot;
16716 }
16717
16718 #[cfg(test)]
16719 pub(crate) fn test_graph_kv_id(&self) -> u64 {
16720 self.graph_kv_id
16721 }
16722
16723 pub(crate) fn mimo_graph_layer_rows(
16727 &mut self,
16728 li: usize,
16729 h: &mut [f32],
16730 positions: &[usize],
16731 ) -> crate::gpu::BatchGraphOutcome {
16732 use crate::gpu::BatchGraphOutcome as Out;
16733 let b = positions.len();
16734 if !(1..=4).contains(&b)
16735 || h.len() != b * self.hidden_size
16736 || !self.mimo_moe.is_dynamic(li, true)
16737 || !crate::gpu::enabled_here()
16738 || !crate::gpu::wgpu_active()
16739 || self.o1_active()
16740 || self.physical_layers != self.num_layers
16741 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
16745 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
16746 || self.wgpu_graph_attn_decline().is_some()
16747 {
16748 return Out::Declined;
16749 }
16750 let attn_started = std::time::Instant::now();
16751 let outcome = {
16752 let lw = &self.weights.layers[li];
16753 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
16754 return Out::Declined;
16755 }
16756 let FfnKind::Moe(m) = &lw.ffn else {
16757 return Out::Declined;
16758 };
16759 let AttnKind::Full {
16760 wq,
16761 wk,
16762 wv,
16763 wo,
16764 q_norm,
16765 k_norm,
16766 output_gate,
16767 softplus_gate,
16768 bias,
16769 } = &lw.attn
16770 else {
16771 return Out::Declined;
16772 };
16773 if *output_gate || softplus_gate.is_some() {
16774 return Out::Declined;
16775 }
16776 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
16777 m.experts
16778 .first()?
16779 .gate_proj
16780 .mapped_q4tp()
16781 .map(|(m, _)| m.clone())
16782 }) else {
16783 return Out::Declined;
16784 };
16785 fn gw<'a>(
16786 t: &'a QTensor,
16787 owner: &std::sync::Arc<cortiq_core::CmfModel>,
16788 ) -> Option<crate::gpu::GraphW<'a>> {
16789 if let Some((m, idx, kind, rs)) = t.graph_weight() {
16790 if m.uid() != owner.uid() || t.has_prism_contract() {
16791 return None;
16792 }
16793 return Some(crate::gpu::GraphW {
16794 idx,
16795 kind,
16796 row_scale: rs,
16797 data: &[],
16798 prism: crate::gpu::GraphPrismOp::None,
16799 affine: false,
16800 });
16801 }
16802 t.as_f32().map(|data| crate::gpu::GraphW {
16803 idx: 0,
16804 kind: 4,
16805 row_scale: &[],
16806 data,
16807 prism: crate::gpu::GraphPrismOp::None,
16808 affine: false,
16809 })
16810 }
16811 let (Some(q), Some(k), Some(v), Some(o)) = (
16812 gw(wq, &model),
16813 gw(wk, &model),
16814 gw(wv, &model),
16815 gw(wo, &model),
16816 ) else {
16817 return Out::Declined;
16818 };
16819 let layer = crate::gpu::GraphLayer {
16820 mhc: None,
16821 input_norm: &lw.input_norm,
16822 post_norm: &lw.post_norm,
16823 attn_out_norm: None,
16824 ffn_out_norm: None,
16825 layer_scale: None,
16826 ffn: crate::gpu::GraphFfn::AttentionOnly,
16827 attn: crate::gpu::GraphAttn::Full {
16828 wq: q,
16829 wk: k,
16830 wv: v,
16831 wo: o,
16832 q_norm: q_norm.as_deref(),
16833 k_norm: k_norm.as_deref(),
16834 late_qk_norm: self.qk_norm_after_rope,
16835 bias: bias
16836 .as_ref()
16837 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
16838 output_gate: false,
16839 cpu_k: self.kv_cache.layers[li].k_heads(),
16840 cpu_v: self.kv_cache.layers[li].v_heads(),
16841 cpu_base: self.kv_cache.layers[li].base(),
16842 geom: self.graph_attn_geom(li),
16843 head_gate: None,
16844 },
16845 };
16846 let (nkv, hd, rd) = self.layer_geom(li);
16847 crate::gpu::forward_batch_graph_at(
16848 &model,
16849 self.graph_kv_id,
16850 li,
16851 &[layer],
16852 &self.inv_freq,
16853 h,
16854 self.layer_num_heads(li),
16855 nkv,
16856 hd,
16857 rd,
16858 self.hidden_size,
16859 1,
16860 positions,
16861 self.kv_cache.max_seq_len,
16862 self.norm_style == cortiq_core::NormStyle::Gemma,
16863 self.rms_eps as f32,
16864 self.attn_scale,
16865 b,
16866 &[],
16867 self.o1_epoch,
16868 None,
16869 None,
16870 )
16871 };
16872 match outcome {
16873 Out::Completed => {}
16874 Out::Declined => return Out::Declined,
16875 Out::Failed => {
16876 self.graph_failed
16877 .store(true, std::sync::atomic::Ordering::Relaxed);
16878 return Out::Failed;
16879 }
16880 }
16881 let attn_ns = attn_started.elapsed().as_nanos() as u64;
16882 let hs = self.hidden_size;
16883 let lw = &self.weights.layers[li];
16884 let FfnKind::Moe(m) = &lw.ffn else {
16885 unreachable!()
16886 };
16887 let mut post = vec![0.0; h.len()];
16888 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
16889 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
16890 }
16891 let mut ffn = if b == 1 {
16892 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
16893 } else {
16894 moe_ffn_banked_rows(
16895 &mut self.mimo_moe,
16896 li,
16897 m,
16898 &post,
16899 b,
16900 hs,
16901 self.pool.as_deref(),
16902 )
16903 };
16904 for (x, &f) in h.iter_mut().zip(&ffn) {
16905 *x += f;
16906 }
16907 attention::recycle_buf(&mut ffn);
16908 if self.layer_dump.is_some() {
16909 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
16910 self.dump_layer_row(pos, li, row);
16911 }
16912 }
16913 crate::mimo_moe::note_attention_graph(b, attn_ns);
16914 Out::Completed
16915 }
16916
16917 fn layer_attn_plain(&self, li: usize) -> bool {
16918 self.kv_heads_per_layer.is_none()
16919 && self.v_head_dim.is_none()
16920 && self.global_attn.is_none()
16921 && self.layer_window(li).is_none()
16922 && self.kv_cache.layers[li].sinks.is_none()
16923 }
16924
16925 fn forward_layers(
16927 &mut self,
16928 hidden: &[f32],
16929 position: usize,
16930 task_mask: Option<&TaskMask>,
16931 ) -> Vec<f32> {
16932 let out = self.forward_layers_upto(hidden, position, task_mask, None);
16933 self.o1_progress();
16934 self.swa_trim_tails();
16935 out
16936 }
16937
16938 pub fn embed_id(&self, id: u32) -> Vec<f32> {
16946 self.embed_single(id)
16947 }
16948
16949 pub fn split_supported(&self) -> Result<(), String> {
16953 if self.dsv4.is_some() {
16954 return Err(
16955 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
16956 );
16957 }
16958 if self.dory.is_some() {
16959 return Err("network split: Dory's looped stack is not splittable".into());
16960 }
16961 if self.dsv41.is_some() {
16962 return Err(
16963 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
16964 .into(),
16965 );
16966 }
16967 if self.qwen4_exp.is_some() {
16968 return Err(
16969 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
16970 );
16971 }
16972 if self.per_token_stack() {
16973 return Err(
16974 "network split: the per-token stack (gemma-3n AltUp / gemma-4 E-series) is not splittable yet".into(),
16975 );
16976 }
16977 if self.value_blend.is_some() {
16978 return Err(
16979 "network split: a value-residual stack (Ripple) reads the embedding row in every layer".into(),
16980 );
16981 }
16982 Ok(())
16983 }
16984
16985 pub fn forward_span(
16990 &mut self,
16991 hidden: &[f32],
16992 position: usize,
16993 from: usize,
16994 upto: usize,
16995 task_mask: Option<&TaskMask>,
16996 ) -> Result<Vec<f32>, String> {
16997 self.split_supported()?;
16998 if from > upto || upto >= self.num_layers {
16999 return Err(format!(
17000 "forward_span: layer range {from}..={upto} outside 0..{}",
17001 self.num_layers
17002 ));
17003 }
17004 if hidden.len() != self.hidden_size {
17005 return Err(format!(
17006 "forward_span: hidden len {} ≠ hidden_size {}",
17007 hidden.len(),
17008 self.hidden_size
17009 ));
17010 }
17011 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
17012 self.o1_progress();
17013 self.swa_trim_tails();
17014 if self
17015 .graph_failed
17016 .swap(false, std::sync::atomic::Ordering::Relaxed)
17017 {
17018 self.cancel
17019 .store(false, std::sync::atomic::Ordering::Relaxed);
17020 self.clear_sequence_state();
17021 return Err("forward_span: deferred O(1) transition failed".into());
17022 }
17023 Ok(out)
17024 }
17025
17026 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
17029 let normed = inference::rms_norm(
17030 hidden,
17031 &self.weights.final_norm,
17032 self.rms_eps,
17033 self.norm_style,
17034 );
17035 self.lm_head_forward(&normed)
17036 }
17037
17038 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
17040 sampler::sample_with_scratch(
17041 logits,
17042 &self.sampler_config,
17043 past_tokens,
17044 &mut self.rng,
17045 &mut self.sampler_scratch,
17046 )
17047 }
17048
17049 pub fn reset_session(&mut self) {
17051 self.clear_sequence_state();
17052 }
17053
17054 pub fn prefill_span_ids(
17060 &mut self,
17061 ids: &[u32],
17062 start_pos: usize,
17063 upto: usize,
17064 task_mask: Option<&TaskMask>,
17065 ) -> Result<Vec<f32>, String> {
17066 self.split_supported()?;
17067 if upto >= self.num_layers {
17068 return Err(format!(
17069 "prefill_span_ids: upto {upto} outside 0..{}",
17070 self.num_layers
17071 ));
17072 }
17073 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
17077 let out =
17078 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
17079 self.check_o1_progress_failure("prefill_span_ids")?;
17080 Ok(out)
17081 } else {
17082 let hs = self.hidden_size;
17083 let mut out = Vec::with_capacity(ids.len() * hs);
17084 for (i, &id) in ids.iter().enumerate() {
17085 let emb = self.embed_id(id);
17086 out.extend_from_slice(&self.forward_span(
17087 &emb,
17088 start_pos + i,
17089 0,
17090 upto,
17091 task_mask,
17092 )?);
17093 }
17094 Ok(out)
17095 }
17096 }
17097
17098 pub fn prefill_span_hidden(
17101 &mut self,
17102 hidden: &[f32],
17103 start_pos: usize,
17104 from: usize,
17105 upto: usize,
17106 task_mask: Option<&TaskMask>,
17107 ) -> Result<Vec<f32>, String> {
17108 self.split_supported()?;
17109 let hs = self.hidden_size;
17110 if hidden.is_empty() || hidden.len() % hs != 0 {
17111 return Err(format!(
17112 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
17113 hidden.len()
17114 ));
17115 }
17116 if from > upto || upto >= self.num_layers {
17117 return Err(format!(
17118 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
17119 self.num_layers
17120 ));
17121 }
17122 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
17123 let out = self.prefill_batch_span(
17124 PrefillIn::Hidden(hidden),
17125 start_pos,
17126 task_mask,
17127 from,
17128 upto + 1,
17129 );
17130 self.check_o1_progress_failure("prefill_span_hidden")?;
17131 Ok(out)
17132 } else {
17133 let b = hidden.len() / hs;
17134 let mut out = Vec::with_capacity(hidden.len());
17135 for i in 0..b {
17136 let h = self.forward_span(
17137 &hidden[i * hs..(i + 1) * hs],
17138 start_pos + i,
17139 from,
17140 upto,
17141 task_mask,
17142 )?;
17143 out.extend_from_slice(&h);
17144 }
17145 Ok(out)
17146 }
17147 }
17148
17149 fn try_token_graph_wgpu(
17153 &self,
17154 hidden: &[f32],
17155 position: usize,
17156 logits_out: &mut Vec<f32>,
17157 layers_run: &mut usize,
17158 ) -> Option<Result<Vec<f32>, ()>> {
17159 self.try_token_graph_wgpu_steps(
17160 hidden,
17161 position,
17162 logits_out,
17163 1,
17164 None,
17165 Some(layers_run),
17166 0,
17167 self.num_layers,
17168 )
17169 }
17170
17171 fn try_token_graph_wgpu_span(
17175 &self,
17176 hidden: &[f32],
17177 position: usize,
17178 logits_out: &mut Vec<f32>,
17179 from: usize,
17180 upto_excl: usize,
17181 layers_run: &mut usize,
17182 ) -> Option<Result<Vec<f32>, ()>> {
17183 self.try_token_graph_wgpu_steps(
17184 hidden,
17185 position,
17186 logits_out,
17187 1,
17188 None,
17189 Some(layers_run),
17190 from,
17191 upto_excl,
17192 )
17193 }
17194
17195 #[cfg(feature = "gpu")]
17211 fn graph_head_after_prompt(&self) -> bool {
17212 (crate::gpu::wgpu_active() || crate::gpu::cuda_active())
17213 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
17214 && !self.graph_refused()
17215 && std::env::var("CMF_HEAD_GRAPH").as_deref() != Ok("0")
17216 }
17217
17218 #[cfg(feature = "gpu")]
17224 fn m3_device_logits(&self, hidden: &[f32]) -> Option<Vec<f32>> {
17225 let pooled = crate::minicpm_tail::minimax_pool_on();
17226 if !(pooled || self.graph_head_after_prompt())
17227 || !crate::gpu::enabled_here()
17228 || self.head_clusters.is_some()
17229 || self.graph_failed.load(std::sync::atomic::Ordering::Relaxed)
17230 || std::env::var("CMF_GPU_LMHEAD").map(|v| v != "0") == Ok(false)
17231 {
17232 return None;
17233 }
17234 let (model, i, kind, rs) = self
17235 .weights
17236 .lm_head
17237 .graph_weight()
17238 .or_else(|| self.weights.lm_head.graph_weight_descriptor())?;
17239 if !pooled && !crate::gpu::cuda_active() && !matches!(kind, 0 | 7) {
17242 return None;
17243 }
17244 let name = &model.tensors[i].name;
17245 let prism = if crate::prism::is_inverse_embedding(&model, name) {
17246 crate::gpu::GraphPrismOp::InverseEmbedding
17247 } else if crate::prism::is_forward_weight(&model, name) {
17248 crate::gpu::GraphPrismOp::Forward
17249 } else {
17250 crate::gpu::GraphPrismOp::None
17251 };
17252 let gw = crate::gpu::GraphW {
17253 idx: i,
17254 kind,
17255 row_scale: rs,
17256 data: &[],
17257 prism,
17258 affine: crate::prism::is_affine_target(&model, name),
17259 };
17260 let rows = self.weights.lm_head.rows();
17261 let mut h = hidden.to_vec();
17262 let mut lg = Vec::new();
17263 let mut layers_run = 0usize;
17264 let outcome = crate::gpu::forward_token_graph(
17265 &model,
17266 self.graph_kv_id,
17267 &[],
17268 &[],
17269 self.o1_epoch,
17270 &self.inv_freq,
17271 &mut h,
17272 self.num_heads,
17273 self.num_kv_heads,
17274 self.head_dim,
17275 self.attn_scale,
17276 self.head_dim,
17277 self.hidden_size,
17278 self.intermediate_size,
17279 0,
17280 self.kv_cache.max_seq_len,
17281 self.norm_style == cortiq_core::NormStyle::Gemma,
17282 self.rms_eps as f32,
17283 Some((&gw, rows)),
17284 &self.weights.final_norm,
17285 &mut lg,
17286 &[],
17287 1,
17288 None,
17289 None,
17290 Some(&mut layers_run),
17291 0,
17292 false,
17293 );
17294 if outcome != crate::gpu::TokenGraphOutcome::Completed || lg.is_empty() {
17295 return None;
17296 }
17297 lg.resize(self.vocab_size, 0.0);
17300 if let Some(m) = self.logit_multiplier {
17301 for l in lg.iter_mut() {
17302 *l *= m;
17303 }
17304 }
17305 if let Some(c) = self.final_softcap {
17306 softcap_in_place(&mut lg, c, self.pool.as_deref());
17307 }
17308 Some(lg)
17309 }
17310
17311 fn m3_note_route(&mut self, li: usize, winners: &[u32]) {
17314 if winners.is_empty() {
17315 return;
17316 }
17317 if self.m3_n_exp == 0 {
17318 self.m3_n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
17319 self.m3_counts = vec![0; self.num_layers * self.m3_n_exp];
17320 }
17321 for &e in winners {
17322 let e = e as usize;
17323 if e < self.m3_n_exp && li < self.num_layers {
17324 self.m3_counts[li * self.m3_n_exp + e] = self.m3_counts[li * self.m3_n_exp + e].saturating_add(1);
17325 }
17326 }
17327 if li + 1 == self.num_layers {
17328 self.m3_tokens += 1;
17329 if self.m3_tokens % 16 == 0 {
17330 if let Ok(path) = std::env::var("CMF_M3_PROFILE_SAVE") {
17331 let bytes: Vec<u8> = self.m3_counts.iter().flat_map(|c| c.to_le_bytes()).collect();
17332 let _ = std::fs::write(&path, bytes);
17333 }
17334 }
17335 }
17336 }
17337
17338 fn m3_prewarm(&mut self) {
17343 let Ok(path) = std::env::var("CMF_M3_PROFILE") else {
17344 return;
17345 };
17346 let Ok(bytes) = std::fs::read(&path) else {
17347 tracing::warn!("m3 prewarm: cannot read profile {path}");
17348 return;
17349 };
17350 let counts: Vec<u32> = bytes
17351 .chunks_exact(4)
17352 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
17353 .collect();
17354 let n_exp = crate::minicpm_tail::minimax_n_experts(&self.weights.layers, self.phys_layer(0)).max(1);
17355 if counts.len() != self.num_layers * n_exp {
17356 tracing::warn!("m3 prewarm: profile size {} does not match {}x{}", counts.len(), self.num_layers, n_exp);
17357 return;
17358 }
17359 let limit: usize = std::env::var("CMF_M3_PREWARM_N")
17360 .ok()
17361 .and_then(|v| v.parse().ok())
17362 .unwrap_or(300);
17363 let mut ranked: Vec<(u32, usize, usize)> = counts
17364 .iter()
17365 .enumerate()
17366 .filter(|(_, c)| **c > 0)
17367 .map(|(i, &c)| (c, i / n_exp, i % n_exp))
17368 .collect();
17369 ranked.sort_unstable_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
17370 ranked.truncate(limit);
17371 let mut per_layer: Vec<Vec<usize>> = vec![Vec::new(); self.num_layers];
17372 for (_, li, e) in ranked {
17373 per_layer[li].push(e);
17374 }
17375 let mut admitted = 0usize;
17376 for (li, ids) in per_layer.iter().enumerate() {
17377 if ids.is_empty() {
17378 continue;
17379 }
17380 let lw = &self.weights.layers[self.phys_layer(li)];
17381 let FfnKind::Moe(m) = &lw.ffn else {
17382 continue;
17383 };
17384 let _ = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &[]);
17386 if crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, ids) {
17387 admitted += ids.len();
17388 }
17389 }
17390 tracing::info!("m3 prewarm: admitted {admitted} experts from {path}");
17391 }
17392
17393 #[cfg(feature = "gpu")]
17404 fn m3_pooled_layer(&mut self, li: usize, position: usize, h: &mut Vec<f32>) -> Result<bool, ()> {
17405 if self.o1_active() || self.attn_softcap > 0.0 {
17406 return Ok(false);
17407 }
17408 if !self.m3_prewarmed {
17409 self.m3_prewarmed = true;
17410 self.m3_prewarm();
17411 }
17412 let frame = {
17413 let lw = &self.weights.layers[self.phys_layer(li)];
17414 let FfnKind::Moe(m) = &lw.ffn else {
17415 return Ok(false);
17416 };
17417 if !crate::minicpm_tail::MiniCpmTail::is_requested_target(m) {
17418 return Ok(false);
17419 }
17420 let t_arm = std::time::Instant::now();
17421 if self.m3_winners.len() != self.num_layers {
17422 self.m3_winners = vec![Vec::new(); self.num_layers];
17423 }
17424 let pred = std::mem::take(&mut self.m3_winners[li]);
17425 let armed = crate::minicpm_tail::minimax_pool_arm(m, li, self.num_layers, &pred);
17426 if armed.is_none() {
17427 self.m3_winners[li] = pred; }
17429 M3_ARM_NS.fetch_add(t_arm.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17430 match armed {
17431 Some(frame) => frame,
17432 None => return Ok(false),
17435 }
17436 };
17437 crate::gpu_wgpu::pooled_frame_enter(frame);
17438 let want_logits = self.graph_want_logits;
17442 self.graph_want_logits = false;
17443 let mut lg = Vec::new();
17444 let mut gl = 0usize;
17445 let t_span = std::time::Instant::now();
17446 let span = crate::gpu::gpu_scope(|| {
17447 self.try_token_graph_wgpu_span(h, position, &mut lg, li, li + 1, &mut gl)
17448 });
17449 self.graph_want_logits = want_logits;
17450 M3_SPAN_NS.fetch_add(t_span.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17451 let hh = match span {
17452 Some(Ok(hh)) => hh,
17453 Some(Err(())) => return Err(()),
17454 None => {
17455 return Ok(false);
17458 }
17459 };
17460 if gl != 1 {
17461 return if gl == 0 { Ok(false) } else { Err(()) };
17464 }
17465 *h = hh;
17466 let winners = crate::gpu_wgpu::pooled_frame_take_winners();
17467 self.m3_note_route(li, &winners);
17468 if self.m3_winners.len() != self.num_layers {
17469 self.m3_winners = vec![Vec::new(); self.num_layers];
17470 }
17471 self.m3_winners[li] = winners;
17472 let mut cold = crate::gpu_wgpu::pooled_frame_take_cold();
17473 cold.retain(|&(_, w)| {
17476 if w.is_finite() {
17477 true
17478 } else {
17479 tracing::error!("pooled tail: non-finite cold weight from expert route");
17480 false
17481 }
17482 });
17483 if cold.is_empty() {
17484 return Ok(true);
17485 }
17486 let x = crate::gpu_wgpu::pooled_frame_take_input();
17487 if x.len() != self.hidden_size {
17488 return Err(());
17489 }
17490 let lw = &self.weights.layers[self.phys_layer(li)];
17496 let FfnKind::Moe(m) = &lw.ffn else {
17497 return Err(());
17498 };
17499 let t_cold = std::time::Instant::now();
17500 let force_host = std::env::var("CMF_M3_COLD_CPU").as_deref() == Ok("1");
17501 let mut out = crate::attention::take_buf(self.hidden_size);
17502 let on_device = !force_host
17503 && crate::minicpm_tail::minimax_cold_device(m, li, self.num_layers, &cold, &x, &mut out);
17504 if !on_device {
17505 crate::attention::recycle_buf(&mut out);
17506 let jobs: Vec<(&crate::pipeline::DenseFfn, f32)> = cold
17507 .iter()
17508 .map(|&(e, w)| m.experts.get(e).map(|ex| (ex, w)).ok_or(()))
17509 .collect::<Result<_, ()>>()?;
17510 out = crate::gpu::cpu_scope(|| {
17511 crate::pipeline::moe_cold_experts_cpu(&jobs, &x, self.pool.as_deref())
17512 });
17513 let ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
17514 let _ = crate::minicpm_tail::minimax_pool_ensure(m, li, self.num_layers, &ids);
17515 }
17516 M3_COLD_NS.fetch_add(t_cold.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
17517 M3_COLD_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
17518 if out.len() != h.len() {
17519 crate::attention::recycle_buf(&mut out);
17520 return Err(());
17521 }
17522 for (dst, src) in h.iter_mut().zip(out.iter()) {
17523 *dst += src;
17524 }
17525 crate::attention::recycle_buf(&mut out);
17526 if on_device {
17527 M3_COLD_DEV_N.fetch_add(cold.len() as u64, std::sync::atomic::Ordering::Relaxed);
17528 }
17529 Ok(true)
17530 }
17531
17532 #[cfg(not(feature = "gpu"))]
17533 fn m3_pooled_layer(
17534 &mut self,
17535 _li: usize,
17536 _position: usize,
17537 _h: &mut Vec<f32>,
17538 ) -> Result<bool, ()> {
17539 Ok(false)
17540 }
17541
17542 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
17546 if self.o1_active() || self.attn_softcap > 0.0 {
17547 return None;
17548 }
17549 if let Some(reason) = self.wgpu_graph_attn_decline() {
17552 self.note_graph_decline("wgpu multi-burst", reason);
17553 return None;
17554 }
17555 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
17556 if !graph_on || self.graph_refused() {
17557 return None;
17564 }
17565 let emb = self.embed_single(t_next);
17566 let mut lg = Vec::new();
17567 let mut ids = Vec::new();
17568 match self.try_token_graph_wgpu_steps(
17569 &emb,
17570 position,
17571 &mut lg,
17572 k,
17573 Some(&mut ids),
17574 None,
17575 0,
17576 self.num_layers,
17577 ) {
17578 Some(Ok(_)) => {}
17579 Some(Err(())) => {
17580 self.graph_failed
17585 .store(true, std::sync::atomic::Ordering::Relaxed);
17586 return None;
17587 }
17588 None => return None,
17589 }
17590 (ids.len() == k).then_some(ids)
17591 }
17592
17593 fn kv16_allowed(&self) -> bool {
17597 self.mtp.is_none()
17598 && self.dflash.is_none()
17599 && !self.o1_active()
17600 && !self.is_block_diffusion()
17601 && self.clover.is_none()
17602 }
17603
17604 fn try_token_graph_wgpu_steps(
17608 &self,
17609 hidden: &[f32],
17610 position: usize,
17611 logits_out: &mut Vec<f32>,
17612 steps: usize,
17613 ids_out: Option<&mut Vec<u32>>,
17614 layers_run: Option<&mut usize>,
17615 from: usize,
17616 upto_excl: usize,
17617 ) -> Option<Result<Vec<f32>, ()>> {
17618 crate::gpu::graph_kv16_note(self.graph_kv_id, self.kv16_allowed());
17619 if self.prompt_mrope.is_some()
17623 || self.mhc.as_deref().is_some_and(|m| {
17624 from < self.num_layers && m.on(self.phys_layer(from)) && !m.enters(self.phys_layer(from))
17625 })
17626 || self.mhc.as_ref().is_some_and(|m| m.s.streams != 2 || m.s.fixed_identity)
17627 || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
17628 && (from..upto_excl.min(self.num_layers)).any(|li| self.mhc_layer(li))
17629 {
17630 return None;
17631 }
17632 let upto_excl = match self.mimo_moe.graph_prefix_end() {
17635 Some(end) if end < upto_excl => {
17636 if steps != 1 || layers_run.is_none() || from >= end {
17637 return None;
17638 }
17639 end
17640 }
17641 _ => upto_excl,
17642 };
17643 let o1_gpu = self.o1_on_graph();
17646 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
17647 return None;
17651 }
17652 if let Some(reason) = self.wgpu_graph_attn_decline() {
17659 self.note_graph_decline("wgpu token graph", reason);
17660 return None;
17661 }
17662 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
17667 .map(|li| {
17668 if !o1_gpu {
17669 return None;
17670 }
17671 self.kv_cache.layers[self.phys_layer(li)].o1_views()
17672 })
17673 .collect();
17674 if self.o1_active() && o1_gpu {
17675 let want: usize = (from..upto_excl)
17678 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
17679 .count();
17680 let have = o1_views.iter().filter(|v| v.is_some()).count();
17681 if want == 0 || have != want {
17682 use std::sync::atomic::{AtomicUsize, Ordering};
17692 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
17693 let code = have * 1000 + want;
17694 if LAST.swap(code, Ordering::Relaxed) != code {
17695 tracing::warn!(
17696 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
17697 );
17698 }
17699 return None;
17700 }
17701 }
17702 let nh = self.num_heads;
17703 let (nkv, hd, rd) = self.layer_geom(0);
17704 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
17705 let vres_e0: &[f32] = if from == 0 { hidden } else { &self.vres_e0 };
17708 let mut layers = Vec::with_capacity(upto_excl - from);
17709 let mut model = None;
17710 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
17711 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
17712 if let Some((m, i, kind, rs)) = t.graph_weight_or_blr() {
17714 let name = &m.tensors[i].name;
17715 let prism = if crate::prism::is_inverse_embedding(m, name) {
17716 crate::gpu::GraphPrismOp::InverseEmbedding
17717 } else if crate::prism::is_forward_weight(m, name) {
17718 crate::gpu::GraphPrismOp::Forward
17719 } else {
17720 crate::gpu::GraphPrismOp::None
17721 };
17722 return Some(crate::gpu::GraphW {
17723 idx: i,
17724 kind,
17725 row_scale: rs,
17726 data: &[],
17727 prism,
17728 affine: crate::prism::is_affine_target(m, name),
17729 });
17730 }
17731 match t.as_f32() {
17733 Some(d) => Some(crate::gpu::GraphW {
17734 idx: 0,
17735 kind: 4,
17736 row_scale: &[],
17737 data: d,
17738 prism: crate::gpu::GraphPrismOp::None,
17739 affine: false,
17740 }),
17741 None => {
17742 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
17743 eprintln!("batch graph: weight has no graph/f32 representation");
17744 }
17745 None
17746 }
17747 }
17748 }
17749 for li in from..upto_excl {
17750 let lw = &self.weights.layers[self.phys_layer(li)];
17751 if dbg {
17752 let ak = match &lw.attn {
17753 AttnKind::Mla(_) => "Mla".into(),
17754 AttnKind::Full {
17755 output_gate, bias, ..
17756 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
17757 AttnKind::LinearGdn(_) => "LinearGdn".into(),
17758 AttnKind::Kda(_) => "Kda".into(),
17759 AttnKind::Linear(_) => "Linear".into(),
17760 AttnKind::ShortConv(_) => "ShortConv".into(),
17761 AttnKind::Bounded(_) => "Bounded".into(),
17762 };
17763 let fk = match &lw.ffn {
17764 FfnKind::Dense(_) => "Dense",
17765 FfnKind::Moe(_) => "Moe",
17766 FfnKind::DenseMoe(_) => "DenseMoe",
17767 };
17768 eprintln!("graph L{li}: attn={ak} ffn={fk}");
17769 }
17770 let gffn = match &lw.ffn {
17771 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if d.segs.len() > 1 => return None,
17776 FfnKind::Dense(d) => {
17777 let Some((act, limit)) = d.act.dense_graph_contract() else {
17781 self.note_graph_decline(
17782 "wgpu token graph",
17783 "dense FFN activation without a graph kernel",
17784 );
17785 return None;
17786 };
17787 crate::gpu::GraphFfn::Dense {
17788 gate: gw(&d.gate_proj)?,
17789 up: gw(&d.up_proj)?,
17790 down: gw(&d.down_proj)?,
17791 act,
17792 limit,
17793 hot: match d.segs.first() {
17794 Some(seg) => Some(crate::gpu::GraphMoeDetachedShared {
17795 gate: gw(&seg.gate)?,
17796 up: gw(&seg.up)?,
17797 down: gw(&seg.down)?,
17798 width: seg.width,
17799 }),
17800 None => None,
17801 },
17802 }
17803 }
17804 FfnKind::Moe(m) => {
17805 if m.route_tau.is_some() || m.mask.is_some() {
17813 return None;
17814 }
17815 let shared = m.shared.as_ref();
17816 let inter = m.experts.first()?.gate_proj.rows();
17822 let detached_shared = match shared {
17830 Some((se, None))
17831 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
17832 {
17833 let q4 = se.gate_proj.mapped_q4tp().is_some()
17834 && se.up_proj.mapped_q4tp().is_some()
17835 && se.down_proj.mapped_q4tp().is_some();
17836 let q8 = se.gate_proj.mapped_q8_2f().is_some()
17837 && se.up_proj.mapped_q8_2f().is_some()
17838 && se.down_proj.mapped_q8_2f().is_some();
17839 if !matches!(se.act, Act::Silu) || !(q4 || q8) {
17840 return None;
17841 }
17842 Some(crate::gpu::GraphMoeDetachedShared {
17843 gate: gw(&se.gate_proj)?,
17844 up: gw(&se.up_proj)?,
17845 down: gw(&se.down_proj)?,
17846 width: se.gate_proj.rows(),
17847 })
17848 }
17849 Some((se, Some(_)))
17853 if se.gate_proj.rows() != inter || se.up_proj.rows() != inter =>
17854 {
17855 return None;
17856 }
17857 _ => None,
17858 };
17859 let has_shared = shared.is_some() && detached_shared.is_none();
17860 let shared_gated = has_shared && matches!(shared, Some((_, Some(_))));
17861 let sgate = match shared {
17862 Some((_, Some(sg))) if has_shared => gw(sg)?,
17863 _ => gw(&m.router)?,
17867 };
17868 let router = gw(&m.router)?;
17869 if router.prism != crate::gpu::GraphPrismOp::None
17875 || sgate.prism != crate::gpu::GraphPrismOp::None
17876 || router.affine
17877 || sgate.affine
17878 {
17879 tracing::warn!(
17880 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
17881 );
17882 return None;
17883 }
17884 let in_bank_shared = shared.filter(|_| has_shared).map(|(se, _)| se);
17885 let edge = |t: &QTensor| {
17886 t.graph_weight()
17887 .or_else(|| t.graph_weight_descriptor())
17888 .map(|(mm, i, _, _)| (std::sync::Arc::as_ptr(mm) as usize, i))
17889 };
17890 let bank_key: Option<GraphMoeKey> = match (
17891 m.experts.first().and_then(|e| edge(&e.gate_proj)),
17892 m.experts.last().and_then(|e| edge(&e.down_proj)),
17893 ) {
17894 (Some((fm, fi)), Some((_, di))) if graph_moe_bank_on() => Some((
17895 self.phys_layer(li),
17896 m.experts.as_ptr() as usize,
17897 m.experts.len(),
17898 in_bank_shared.map_or(0, |se| std::ptr::from_ref(se) as usize),
17899 fm,
17900 fi,
17901 di,
17902 )),
17903 _ => None,
17904 };
17905 let cached =
17906 bank_key.and_then(|k| self.graph_moe_banks.lock().unwrap().get(&k).cloned());
17907 let bank = if let Some(b) = cached {
17908 b
17909 } else {
17910 let mut experts = Vec::with_capacity(m.experts.len() + 1);
17911 let mut bank_model: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
17912 let mut q4tp: Option<bool> = None;
17915 let mut gu_q2: Option<bool> = None;
17918 let mut q82: Option<bool> = None;
17920 let mut moe_act = None;
17924 for e in m.experts.iter().chain(in_bank_shared.into_iter()) {
17925 let Some(e_act) = e.act.wgpu_graph_act() else {
17926 return None;
17927 };
17928 match moe_act {
17929 Some(existing) if existing != e_act => return None,
17930 None => moe_act = Some(e_act),
17931 _ => {}
17932 }
17933 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
17934 return None;
17935 }
17936 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
17941 let Some((em, ei, _, _)) = expert_weight
17942 .graph_weight()
17943 .or_else(|| expert_weight.graph_weight_descriptor())
17944 else {
17945 return None;
17946 };
17947 let name = &em.tensors[ei].name;
17948 if crate::prism::is_forward_weight(em, name)
17949 || crate::prism::is_inverse_embedding(em, name)
17950 || crate::prism::is_affine_target(em, name)
17951 {
17952 tracing::warn!(
17953 "resident MoE declined: expert Prism/affine transform is not implemented"
17954 );
17955 return None;
17956 }
17957 }
17958 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
17959 Some((mm, gi)) => (
17960 mm,
17961 gi,
17962 e.up_proj.mapped_q4t()?.1,
17963 e.down_proj.mapped_q4t()?.1,
17964 false,
17965 false,
17966 false,
17967 ),
17968 None => match e.gate_proj.mapped_q2tp() {
17969 Some((mm, gi)) => (
17970 mm,
17971 gi,
17972 e.up_proj.mapped_q2tp()?.1,
17973 e.down_proj.mapped_q4tp()?.1,
17974 true,
17975 true,
17976 false,
17977 ),
17978 None => match e.gate_proj.mapped_q4tp() {
17979 Some((mm, gi)) => (
17980 mm,
17981 gi,
17982 e.up_proj.mapped_q4tp()?.1,
17983 e.down_proj.mapped_q4tp()?.1,
17984 true,
17985 false,
17986 false,
17987 ),
17988 None => {
17991 if !crate::gpu::moe_q82_graph_on() {
17992 return None;
17993 }
17994 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
17995 (
17996 mm,
17997 gi,
17998 e.up_proj.mapped_q8_2f()?.1,
17999 e.down_proj.mapped_q8_2f()?.1,
18000 false,
18001 false,
18002 true,
18003 )
18004 }
18005 },
18006 },
18007 };
18008 if *q4tp.get_or_insert(is_p) != is_p
18009 || *gu_q2.get_or_insert(is_q2) != is_q2
18010 || *q82.get_or_insert(is_8) != is_8
18011 {
18012 tracing::warn!(
18018 "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."
18019 );
18020 return None;
18021 }
18022 bank_model.get_or_insert_with(|| mm.clone());
18023 experts.push((gi, ui, di));
18024 }
18025 let b = GraphMoeBank {
18026 experts,
18027 q4tp,
18028 gu_q2,
18029 q82,
18030 act: moe_act,
18031 model: bank_model,
18032 };
18033 if let Some(k) = bank_key {
18034 self.graph_moe_banks.lock().unwrap().insert(k, b.clone());
18035 }
18036 b
18037 };
18038 if let Some(mm) = &bank.model {
18039 model.get_or_insert_with(|| mm.clone());
18040 }
18041 crate::gpu::GraphFfn::Moe {
18042 router,
18043 shared_gate: sgate,
18044 experts: bank.experts,
18045 n_exp: m.experts.len(),
18046 top_k: std::env::var("CMF_TOPK_PROBE")
18052 .ok()
18053 .and_then(|v| v.parse::<usize>().ok())
18054 .filter(|k| *k > 0 && *k <= m.top_k)
18055 .unwrap_or(m.top_k),
18056 inter,
18057 act: bank.act?,
18058 norm_topk: m.norm_topk_prob,
18059 q4tp: bank.q4tp?,
18060 gu_q2: bank.gu_q2.unwrap_or(false),
18061 q82: bank.q82.unwrap_or(false),
18062 sigmoid: m.router_sigmoid,
18063 sigmoid_denom_eps: m.sigmoid_denom_eps,
18064 bias: m.expert_bias.as_deref(),
18065 has_shared,
18066 detached_shared,
18067 shared_gated,
18068 route_scale: m.routed_scaling,
18069 }
18070 }
18071 };
18072 let attn = match &lw.attn {
18073 AttnKind::Full {
18074 wq,
18075 wk,
18076 wv,
18077 wo,
18078 q_norm,
18079 k_norm,
18080 output_gate,
18081 softplus_gate,
18082 bias,
18083 } => {
18084 if self.attention_heads_per_layer.is_some() {
18085 return None;
18086 }
18087 let head_gate = match softplus_gate {
18091 None => None,
18092 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
18093 Some(_) => {
18094 self.note_graph_decline(
18095 "wgpu token graph",
18096 "projected softplus / per-element output gate",
18097 );
18098 return None;
18099 }
18100 };
18101 let (m, _, _, _) = wq.graph_weight_or_blr()?;
18102 model = Some(m.clone());
18103 crate::gpu::GraphAttn::Full {
18104 wq: gw(wq)?,
18105 wk: gw(wk)?,
18106 wv: gw(wv)?,
18107 wo: gw(wo)?,
18108 q_norm: q_norm.as_deref(),
18109 k_norm: k_norm.as_deref(),
18110 late_qk_norm: self.qk_norm_after_rope,
18111 bias: bias
18112 .as_ref()
18113 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
18114 output_gate: *output_gate,
18115 cpu_k: self.kv_cache.layers[li].k_heads(),
18116 cpu_v: self.kv_cache.layers[li].v_heads(),
18117 cpu_base: self.kv_cache.layers[li].base(),
18118 geom: self.graph_attn_geom_vres(li, vres_e0),
18119 head_gate,
18120 }
18121 }
18122 AttnKind::LinearGdn(w) => {
18123 let cfg = self.gdn_cfg?;
18124 let (m, _, _, _) = w.in_proj_qkv.graph_weight_or_blr()?;
18125 model = Some(m.clone());
18126 crate::gpu::GraphAttn::Gdn {
18127 qkv: gw(&w.in_proj_qkv)?,
18128 z: gw(&w.in_proj_z)?,
18129 a: gw(&w.in_proj_a)?,
18130 b: gw(&w.in_proj_b)?,
18131 out: gw(&w.out_proj)?,
18132 conv1d: &w.conv1d,
18133 a_log: &w.a_log,
18134 dt_bias: &w.dt_bias,
18135 norm: &w.norm,
18136 nv: cfg.num_v_heads,
18137 nk: cfg.num_k_heads,
18138 dk: cfg.key_head_dim,
18139 dv: cfg.value_head_dim,
18140 kk: cfg.conv_kernel,
18141 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
18142 preview: None,
18143 }
18144 }
18145 AttnKind::ShortConv(w) => {
18146 let cfg = self.short_conv_cfg?;
18147 let (m, _, _, _) = w
18148 .in_proj
18149 .graph_weight()
18150 .or_else(|| w.in_proj.graph_weight_descriptor())?;
18151 model = Some(m.clone());
18152 crate::gpu::GraphAttn::ShortConv {
18153 inp: gw(&w.in_proj)?,
18154 out: gw(&w.out_proj)?,
18155 taps: &w.conv,
18156 kernel: cfg.kernel,
18157 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
18158 }
18159 }
18160 _ => return None,
18161 };
18162 layers.push(crate::gpu::GraphLayer {
18163 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
18164 input_norm: &lw.input_norm,
18165 attn,
18166 post_norm: &lw.post_norm,
18167 attn_out_norm: lw.attn_out_norm.as_deref(),
18168 ffn_out_norm: lw.ffn_out_norm.as_deref(),
18169 layer_scale: lw.layer_scale,
18170 ffn: gffn,
18171 });
18172 }
18173 let model = model?;
18174 let lm_gw = if upto_excl == self.num_layers
18180 && self.graph_want_logits
18181 && std::env::var("CMF_GPU_LMHEAD")
18182 .map(|v| v != "0")
18183 .unwrap_or(true)
18184 {
18185 self.weights
18186 .lm_head
18187 .graph_weight()
18188 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
18189 .map(|(m, i, kind, rs)| {
18190 let name = &m.tensors[i].name;
18191 let prism = if crate::prism::is_inverse_embedding(m, name) {
18192 crate::gpu::GraphPrismOp::InverseEmbedding
18193 } else if crate::prism::is_forward_weight(m, name) {
18194 crate::gpu::GraphPrismOp::Forward
18195 } else {
18196 crate::gpu::GraphPrismOp::None
18197 };
18198 (
18199 crate::gpu::GraphW {
18200 idx: i,
18201 kind,
18202 row_scale: rs,
18203 data: &[],
18204 prism,
18205 affine: crate::prism::is_affine_target(m, name),
18206 },
18207 self.weights.lm_head.rows(),
18208 )
18209 })
18210 } else {
18211 None
18212 };
18213 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
18214 let emb_gw = if steps > 1 {
18216 self.weights
18217 .embed_tokens
18218 .graph_weight()
18219 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
18220 .map(|(m, i, kind, rs)| {
18221 let name = &m.tensors[i].name;
18222 let prism = if crate::prism::is_inverse_embedding(m, name) {
18223 crate::gpu::GraphPrismOp::InverseEmbedding
18224 } else if crate::prism::is_forward_weight(m, name) {
18225 crate::gpu::GraphPrismOp::Forward
18226 } else {
18227 crate::gpu::GraphPrismOp::None
18228 };
18229 (
18230 crate::gpu::GraphW {
18231 idx: i,
18232 kind,
18233 row_scale: rs,
18234 data: &[],
18235 prism,
18236 affine: crate::prism::is_affine_target(m, name),
18237 },
18238 self.weights.embed_tokens.rows(),
18239 self.embed_multiplier,
18240 )
18241 })
18242 } else {
18243 None
18244 };
18245
18246 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
18252 (from..upto_excl.min(self.num_layers - 1))
18253 .filter(|&li| (li + 1) % self.physical_layers == 0)
18254 .map(|li| li - from)
18255 .collect()
18256 } else {
18257 Vec::new()
18258 };
18259 let mut h = hidden.to_vec();
18260 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
18266 let taps = if steps == 1 { self.dflash_taps_wanted(from, upto_excl) } else { Vec::new() };
18267 crate::gpu::layer_taps_arm(&taps);
18268 let outcome = crate::gpu::forward_token_graph(
18269 &model,
18270 self.graph_kv_id,
18271 &layers,
18272 &o1_views,
18273 self.o1_epoch,
18274 &self.inv_freq,
18275 &mut h,
18276 nh,
18277 nkv,
18278 hd,
18279 self.attn_scale,
18280 rd,
18281 self.hidden_size,
18282 self.intermediate_size,
18283 position,
18284 self.kv_cache.max_seq_len,
18285 gemma,
18286 self.rms_eps as f32,
18287 lm,
18288 &self.weights.final_norm,
18289 logits_out,
18290 &loop_norm_at,
18291 steps,
18292 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
18293 ids_out,
18294 layers_run,
18295 from,
18296 dump_hidden,
18297 );
18298 self.dflash_taps_keep(&taps, position, 1, outcome == crate::gpu::TokenGraphOutcome::Completed);
18299 match outcome {
18300 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
18301 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
18302 crate::gpu::TokenGraphOutcome::Declined => None,
18303 }
18304 }
18305
18306 #[cfg(target_os = "macos")]
18315 #[allow(clippy::type_complexity)]
18316 fn metal_rows_plan(
18317 &self,
18318 ) -> Option<(
18319 Vec<MetalRowsItem<'_>>,
18320 std::sync::Arc<cortiq_core::CmfModel>,
18321 Option<crate::gpu_metal::GdnGpuCfg>,
18322 )> {
18323 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
18324 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
18325 if !graph_force
18326 || !crate::gpu::enabled_here()
18327 || std::env::var("CMF_GPU_BLOCK")
18328 .map(|v| v == "0")
18329 .unwrap_or(false)
18330 || self.attn_softcap > 0.0
18331 || (self.o1_active() && !self.o1_on_graph())
18332 || (self.swa.is_some() && !self.metal_graph_swa())
18335 || self.global_attn.is_some()
18336 || self.attention_heads_per_layer.is_some()
18337 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa())
18339 || self.attn_v_norm
18340 || self.loop_final_norm
18341 || !self.metal_rope_scale_ok()
18342 {
18343 return None;
18344 }
18345 let attend_contract = self.head_dim % 4 == 0
18346 && self.head_dim <= 256
18347 && self.rotary_dim >= 2
18348 && self.rotary_dim <= self.head_dim
18349 && (self.rotary_dim / 2) % 32 == 0
18350 && self.num_kv_heads > 0
18351 && self.num_heads % self.num_kv_heads == 0;
18352 if !attend_contract {
18353 return None;
18354 }
18355 let mut plan: Vec<MetalRowsItem> = Vec::new();
18356 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
18357 for li in 0..self.num_layers {
18358 let lw = &self.weights.layers[self.phys_layer(li)];
18359 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
18360 return None;
18361 }
18362 let ffn = match &lw.ffn {
18363 FfnKind::Dense(d) if matches!(d.act, Act::Silu | Act::Gelu) && d.segs.is_empty() => {
18364 let (Some(g), Some(u), Some(dn)) = (
18365 d.gate_proj.metal_graph_parts(),
18366 d.up_proj.metal_graph_parts(),
18367 d.down_proj.metal_graph_parts(),
18368 ) else {
18369 return None;
18370 };
18371 MetalFfn::Dense {
18372 gate: g,
18373 up: u,
18374 down: dn,
18375 act: if d.act == Act::Gelu { 1 } else { 0 },
18377 }
18378 }
18379 _ => return None,
18380 };
18381 match &lw.attn {
18382 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
18383 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
18384 w.in_proj_qkv.metal_graph_parts(),
18385 w.in_proj_z.metal_graph_parts(),
18386 w.in_proj_a.f32_parts(),
18387 w.in_proj_b.f32_parts(),
18388 w.out_proj.metal_graph_parts(),
18389 ) else {
18390 return None;
18391 };
18392 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
18393 model_ref.get_or_insert_with(|| model.clone());
18394 }
18395 let gl = GdnGpuLayer {
18396 mhc: None,
18397 attn_norm: &lw.input_norm,
18398 post_norm: &lw.post_norm,
18399 qkv,
18400 z,
18401 a,
18402 b: bb,
18403 out,
18404 ffn,
18405 conv1d: &w.conv1d,
18406 a_log: &w.a_log,
18407 dt_bias: &w.dt_bias,
18408 gnorm: &w.norm,
18409 };
18410 match plan.last_mut() {
18411 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
18412 _ => plan.push(MetalRowsItem::Gdn {
18413 run: vec![gl],
18414 first: li,
18415 }),
18416 }
18417 }
18418 AttnKind::Full {
18419 wq,
18420 wk,
18421 wv,
18422 wo,
18423 q_norm,
18424 k_norm,
18425 output_gate,
18426 softplus_gate,
18427 bias: None,
18428 } => {
18429 let head_gate = match softplus_gate {
18431 None => None,
18432 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => match g.f32_parts() {
18433 Some((d, r, c)) if r == self.num_heads && c == self.hidden_size => Some(d),
18434 _ => return None,
18435 },
18436 Some(_) => return None,
18437 };
18438 let (Some(pq), Some(pk), Some(pv), Some(po)) = (
18439 wq.metal_graph_parts(),
18440 wk.metal_graph_parts(),
18441 wv.metal_graph_parts(),
18442 wo.metal_graph_parts(),
18443 ) else {
18444 return None;
18445 };
18446 if let QTensor::Mapped { model, .. } = wq {
18447 model_ref.get_or_insert_with(|| model.clone());
18448 }
18449 let cache = &self.kv_cache.layers[li];
18450 if cache.mode != crate::kv_cache::KvMode::F32 || (cache.o1.is_some() && !self.o1_on_graph()) {
18451 return None;
18452 }
18453 plan.push(MetalRowsItem::Attn {
18454 l: AttnGpuLayer {
18455 mhc: None,
18456 attn_norm: &lw.input_norm,
18457 post_norm: &lw.post_norm,
18458 wq: pq,
18459 wk: pk,
18460 wv: pv,
18461 wo: po,
18462 ffn,
18463 attn_out_norm: None,
18464 ffn_out_norm: None,
18465 layer_scale: None,
18466 },
18467 li,
18468 q_norm: q_norm.as_deref(),
18469 k_norm: k_norm.as_deref(),
18470 output_gate: *output_gate,
18471 head_gate,
18472 });
18473 }
18474 _ => return None,
18475 }
18476 }
18477 let model = model_ref?;
18478 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
18479 nv: cfg.num_v_heads,
18480 nk: cfg.num_k_heads,
18481 dk: cfg.key_head_dim,
18482 dv: cfg.value_head_dim,
18483 kk: cfg.conv_kernel,
18484 hidden: self.hidden_size,
18485 inter: self.intermediate_size,
18486 c_dim: cfg.conv_dim(),
18487 eps: cfg.rms_eps as f32,
18488 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
18489 });
18490 Some((plan, model, gcfg))
18491 }
18492
18493 #[cfg(target_os = "macos")]
18495 #[allow(clippy::too_many_arguments)]
18496 fn metal_attn_params<'a>(
18497 li: usize,
18498 cache: &'a crate::kv_cache::LayerKvCache,
18499 q_norm: Option<&'a [f32]>,
18500 k_norm: Option<&'a [f32]>,
18501 output_gate: bool,
18502 inv_freq: &'a [f32],
18503 rope_scale: f32,
18504 geom: (usize, usize, usize, usize),
18505 pos0: usize,
18506 kv_id: u64,
18507 scale: f32,
18508 eps: f32,
18509 gemma: bool,
18510 late_qk_norm: bool,
18511 o1_epoch: u64,
18512 head_gate: Option<&'a [f32]>,
18513 window: Option<usize>,
18514 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
18515 let (nh, nkv, hd, rd) = geom;
18516 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
18517 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
18518 let cpu_stored = cpu_k[0].len() / hd;
18519 (
18520 crate::gpu_metal::AttnDeviceParams {
18521 kv_id,
18522 layer: li,
18523 nh,
18524 nkv,
18525 hd,
18526 rd,
18527 position: pos0,
18528 scale,
18529 eps,
18530 gemma,
18531 late_qk_norm,
18532 output_gate,
18533 q_norm,
18534 k_norm,
18535 inv_freq,
18536 rope_scale,
18537 cpu_k,
18538 cpu_v,
18539 cpu_stored,
18540 cpu_gen: cache.generation(),
18541 o1: cache.o1_views().map(|views| crate::gpu_metal::O1AttnParams { views, epoch: o1_epoch }),
18543 window,
18544 softmax_sink: cache.sinks.as_deref(),
18545 head_gate,
18546 v_norm: false,
18547 dash: None,
18548 },
18549 cpu_stored,
18550 )
18551 }
18552
18553 #[cfg(target_os = "macos")]
18558 #[allow(clippy::type_complexity)]
18559 fn metal_rows_run(
18560 &mut self,
18561 hiddens: &mut [f32],
18562 pos0: usize,
18563 b: usize,
18564 prefill: bool,
18565 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18566 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
18570 ) -> MetalRowsRun {
18571 use crate::gpu_metal::{GraphDims, VerifyGraph};
18572 if !crate::gpu_metal::wait_replay() {
18578 tracing::error!("Metal rows graph: the pending async replay failed");
18579 return MetalRowsRun::Failed;
18580 }
18581 spec_stamp("v.wait");
18582 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
18588 if want > 0 {
18589 let phys = self.physical_layers.max(1);
18590 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
18591 let is_gdn = self
18592 .weights
18593 .layers
18594 .get(li % phys)
18595 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
18596 if is_gdn && l.linear_state.len() != want {
18597 l.linear_state = vec![0f32; want];
18598 }
18599 }
18600 }
18601 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
18602 return MetalRowsRun::Declined;
18603 };
18604 spec_stamp("v.plan");
18605 let dims = GraphDims {
18606 hidden: self.hidden_size,
18607 eps: self.rms_eps as f32,
18608 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
18609 };
18610 let hs = self.hidden_size;
18613 let relay = prefill
18614 && spec.is_none()
18615 && b >= 128
18616 && crate::gpu_metal::ane::wanted()
18617 && std::env::var("CMF_ANE_RELAY").as_deref() != Ok("0");
18618 let cuts: Vec<(usize, usize)> = if relay { vec![(0, b / 2), (b / 2, b - b / 2)] } else { vec![(0, b)] };
18619 let gate = crate::gpu_metal::LaneGate::new();
18620 let mut lanes: Vec<VerifyGraph> = Vec::with_capacity(cuts.len());
18621 for (ln, &(r0, n)) in cuts.iter().enumerate() {
18622 let h = &hiddens[r0 * hs..(r0 + n) * hs];
18623 let g = if relay {
18624 VerifyGraph::new_prefill_lane(&model, dims, h, n, ln, b - r0 - n, &gate)
18625 } else if prefill {
18626 VerifyGraph::new_prefill(&model, dims, h, n)
18627 } else {
18628 VerifyGraph::new(&model, dims, h, n)
18629 };
18630 let Some(g) = g else {
18631 return MetalRowsRun::Declined;
18632 };
18633 lanes.push(g);
18634 }
18635 if prefill {
18636 use crate::gpu_metal::{AneWants, MetalFfn};
18637 let mut ane_act = None;
18639 let ffn = |f: &MetalFfn<'_>, ane_act: &mut Option<u8>| match f {
18640 MetalFfn::Dense { gate, up, down, act } if *act <= 1 && ane_act.is_none_or(|a| a == *act) => {
18641 *ane_act = Some(*act);
18642 Some([*gate, *up, *down])
18643 }
18644 _ => None,
18645 };
18646 let mut wants = Vec::new();
18647 for item in &plan {
18648 match item {
18649 MetalRowsItem::Gdn { run, .. } => {
18650 wants.extend(run.iter().map(|l| AneWants {
18651 ffn: ffn(&l.ffn, &mut ane_act),
18652 mix: if crate::gpu_metal::ane_takes_qkv() { vec![l.qkv, l.z] } else { vec![l.z] },
18653 }))
18654 }
18655 MetalRowsItem::Attn { l, .. } => wants.push(AneWants {
18656 ffn: ffn(&l.ffn, &mut ane_act),
18657 mix: if crate::gpu_metal::ane_takes_qkv() {
18658 vec![l.wk, l.wv, l.wq]
18659 } else if crate::gpu_metal::ane_takes_q() {
18660 vec![l.wq]
18661 } else {
18662 Vec::new()
18663 },
18664 }),
18665 }
18666 }
18667 crate::gpu_metal::ane::set_act(ane_act.unwrap_or(0));
18668 for g in lanes.iter_mut() {
18669 g.ane_attach(&wants);
18670 }
18671 lanes[0].ane_begin(lanes.len());
18672 }
18673 let hard = relay || lanes[0].has_ane();
18676 let geom = (
18677 self.num_heads,
18678 self.num_kv_heads,
18679 self.head_dim,
18680 self.rotary_dim,
18681 );
18682 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
18683 let eps = self.rms_eps as f32;
18684 let kv_id = self.graph_kv_id;
18685 let tables: Vec<std::sync::Arc<Vec<f32>>> = (0..self.num_layers).map(|li| self.layer_inv_freq(li)).collect();
18687 let lgeom = |li: usize| (geom.0, geom.1, geom.2, self.layer_geom(li).2);
18688 for item in &plan {
18689 let ok = match item {
18690 MetalRowsItem::Gdn { run, .. } => gcfg
18691 .as_ref()
18692 .map(|gc| lanes.iter().all(|g| run.iter().all(|l| g.gdn_ok(l, gc))))
18693 .unwrap_or(false),
18694 MetalRowsItem::Attn {
18695 l,
18696 li,
18697 q_norm,
18698 k_norm,
18699 output_gate,
18700 head_gate,
18701 } => {
18702 let (p, _) = Self::metal_attn_params(
18703 *li,
18704 &self.kv_cache.layers[*li],
18705 *q_norm,
18706 *k_norm,
18707 *output_gate,
18708 &tables[*li],
18709 self.layer_rope_scale(*li),
18710 lgeom(*li),
18711 pos0,
18712 kv_id,
18713 self.attn_scale,
18714 eps,
18715 gemma,
18716 self.qk_norm_after_rope,
18717 self.o1_epoch,
18718 *head_gate,
18719 self.layer_window(*li),
18720 );
18721 lanes.iter().all(|g| g.attn_ok(l, &p))
18722 }
18723 };
18724 if !ok {
18725 use std::sync::atomic::{AtomicBool, Ordering};
18726 static SAID: AtomicBool = AtomicBool::new(false);
18727 if !SAID.swap(true, Ordering::Relaxed) {
18728 tracing::warn!("metal rows graph: a layer failed preflight — declining");
18729 }
18730 return MetalRowsRun::Declined;
18731 }
18732 }
18733 let lm = match &spec {
18734 Some((lm, _, _)) => {
18735 if !lanes[0].lm_head_ok(*lm) {
18736 return MetalRowsRun::Declined;
18737 }
18738 Some(*lm)
18739 }
18740 None => None,
18741 };
18742 enum Step<'a> {
18744 Gdn(&'a crate::gpu_metal::GdnGpuLayer<'a>, &'a [f32]),
18745 Attn(&'a crate::gpu_metal::AttnGpuLayer<'a>, crate::gpu_metal::AttnDeviceParams<'a>),
18746 }
18747 let mut gdn_layers = Vec::new();
18748 let mut attn_layers = Vec::new();
18749 let mut steps: Vec<Vec<Step<'_>>> = cuts.iter().map(|_| Vec::new()).collect();
18750 let mut step_layer: Vec<usize> = Vec::new();
18752 for item in &plan {
18753 match item {
18754 MetalRowsItem::Gdn { run, first } => {
18755 for (l, kv) in run.iter().zip(&self.kv_cache.layers[*first..*first + run.len()]) {
18756 for st in steps.iter_mut() {
18757 st.push(Step::Gdn(l, kv.linear_state.as_slice()));
18758 }
18759 }
18760 gdn_layers.extend(*first..*first + run.len());
18761 step_layer.extend(*first..*first + run.len());
18762 }
18763 MetalRowsItem::Attn {
18764 l,
18765 li,
18766 q_norm,
18767 k_norm,
18768 output_gate,
18769 head_gate,
18770 } => {
18771 for (ln, st) in steps.iter_mut().enumerate() {
18772 let (mut p, cpu_stored) = Self::metal_attn_params(
18773 *li,
18774 &self.kv_cache.layers[*li],
18775 *q_norm,
18776 *k_norm,
18777 *output_gate,
18778 &tables[*li],
18779 self.layer_rope_scale(*li),
18780 lgeom(*li),
18781 pos0 + cuts[ln].0,
18782 kv_id,
18783 self.attn_scale,
18784 eps,
18785 gemma,
18786 self.qk_norm_after_rope,
18787 self.o1_epoch,
18788 *head_gate,
18789 self.layer_window(*li),
18790 );
18791 p.cpu_stored += cuts[ln].0;
18793 if ln == 0 && p.o1.is_none() {
18795 attn_layers.push((*li, cpu_stored));
18796 }
18797 st.push(Step::Attn(l, p));
18798 }
18799 step_layer.push(*li);
18800 }
18801 }
18802 }
18803 let taps: Vec<usize> = match &self.dflash {
18806 Some(d) if !relay => d.g.taps.clone(),
18807 _ => Vec::new(),
18808 };
18809 let tap_slot: Vec<Option<usize>> = step_layer.iter().map(|l| taps.iter().position(|t| t == l)).collect();
18810 let tapped = std::sync::atomic::AtomicUsize::new(0);
18811 let gc = gcfg.as_ref();
18813 let walk = |g: &mut VerifyGraph, steps: &[Step<'_>]| -> Result<(), bool> {
18814 for (i, step) in steps.iter().enumerate() {
18815 let ok = match step {
18816 Step::Gdn(l, state) => match gc {
18817 Some(gc) => g.encode_gdn_run_b(std::slice::from_ref(*l), &[*state], gc),
18818 None => false,
18819 },
18820 Step::Attn(l, p) => g.encode_attn_b(l, p),
18821 };
18822 if !ok {
18823 return Err(i > 0);
18824 }
18825 if let Some(slot) = tap_slot.get(i).copied().flatten() {
18826 if g.tap(slot, taps.len()) {
18827 tapped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18828 }
18829 }
18830 }
18831 Ok(())
18832 };
18833 let refused = |started: bool| if hard && started { MetalRowsRun::Failed } else { MetalRowsRun::Declined };
18834 if relay {
18835 let (first, second) = lanes.split_at_mut(1);
18836 let (sa, sb) = (&steps[0], &steps[1]);
18837 let (ra, rb) = std::thread::scope(|sc| {
18838 let other = sc.spawn(|| {
18839 let r = walk(&mut second[0], sb);
18840 if r.is_err() {
18841 crate::gpu_metal::ane::retire("a half of the chunk stopped");
18843 }
18844 r
18845 });
18846 let r = walk(&mut first[0], sa);
18847 if r.is_err() {
18848 gate.fail();
18849 }
18850 (r, other.join().unwrap_or(Err(true)))
18851 });
18852 if ra.is_err() || rb.is_err() {
18853 return MetalRowsRun::Failed;
18854 }
18855 } else if let Err(started) = walk(&mut lanes[0], &steps[0]) {
18856 return refused(started);
18857 }
18858 drop(steps);
18859 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
18860 if !lanes[0].encode_lm_head_b(final_norm, lm) {
18861 return refused(true);
18862 }
18863 if let Some((n, _)) = argmax_out.as_ref() {
18868 if !lanes[0].encode_argmax_b(*n) {
18869 argmax_out = None;
18870 }
18871 }
18872 }
18873 spec_stamp("v.enc");
18874 for g in lanes.iter_mut() {
18875 if !g.sync() {
18876 return MetalRowsRun::Failed;
18877 }
18878 }
18879 spec_stamp("v.gpu");
18880 match (spec, argmax_out) {
18881 (Some(_), Some((_, ids))) => {
18882 ids.resize(b, 0);
18883 if !lanes[0].read_argmax(ids) {
18884 return MetalRowsRun::Failed;
18885 }
18886 spec_stamp("v.am");
18887 }
18888 (Some((lm, _, logits)), None) => {
18889 logits.resize(b * lm.1, 0.0);
18890 if !lanes[0].read_logits(logits) {
18891 return MetalRowsRun::Failed;
18892 }
18893 spec_stamp("v.lg");
18894 }
18895 (None, _) => {}
18896 }
18897 for (g, &(r0, n)) in lanes.iter().zip(&cuts) {
18898 if !g.read_hidden(&mut hiddens[r0 * hs..(r0 + n) * hs]) {
18899 return MetalRowsRun::Failed;
18900 }
18901 }
18902 spec_stamp("v.hid");
18903 self.dflash_rows = None;
18906 if !taps.is_empty() && tapped.load(std::sync::atomic::Ordering::Relaxed) == taps.len() && lanes.len() == 1 {
18907 let nt = taps.len();
18908 let mut raw = vec![0f32; nt * b * hs];
18909 if lanes[0].read_taps(&mut raw) {
18910 let mut joined = vec![0f32; b * nt * hs];
18911 for s in 0..nt {
18912 for r in 0..b {
18913 joined[(r * nt + s) * hs..][..hs].copy_from_slice(&raw[(s * b + r) * hs..][..hs]);
18914 }
18915 }
18916 if prefill {
18917 if let Some(d) = self.dflash.as_mut() {
18918 d.queue_context(pos0, &joined, self.pool.as_deref());
18919 }
18920 } else {
18921 self.dflash_rows = Some((pos0, joined));
18922 }
18923 spec_stamp("v.taps");
18924 }
18925 }
18926 let graph = lanes.pop().unwrap();
18928 MetalRowsRun::Completed(MetalVerifyPending {
18929 graph,
18930 gdn_layers,
18931 attn_layers,
18932 })
18933 }
18934
18935 #[cfg(target_os = "macos")]
18941 fn try_batch_graph_metal(
18942 &mut self,
18943 hiddens: &mut [f32],
18944 positions: &[usize],
18945 b: usize,
18946 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18947 argmax_out: Option<(usize, &mut Vec<u32>)>,
18948 ) -> crate::gpu::BatchGraphOutcome {
18949 let _t0 = std::time::Instant::now();
18950 if positions.len() != b
18951 || self.mhc.is_some()
18952 || self.prompt_mrope.is_some()
18953 || self.value_blend.is_some()
18954 || positions.windows(2).any(|w| w[1] != w[0] + 1)
18955 || hiddens.len() != b * self.hidden_size
18956 {
18957 return crate::gpu::BatchGraphOutcome::Declined;
18958 }
18959 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
18960 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
18961 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
18962 MetalRowsRun::Completed(pending) => pending,
18963 };
18964 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
18965 eprintln!(
18966 "metal-verify: {:.1} ms | b={b}",
18967 _t0.elapsed().as_secs_f64() * 1e3
18968 );
18969 }
18970 self.metal_verify = Some(pending);
18971 crate::gpu::BatchGraphOutcome::Completed
18972 }
18973
18974 #[cfg(target_os = "macos")]
18979 fn prefill_rows_metal(
18980 &mut self,
18981 ids: &[u32],
18982 start_pos: usize,
18983 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
18984 ) -> MetalPrefillOutcome {
18985 let b = ids.len();
18986 if b == 0 || b > 512 {
18987 return MetalPrefillOutcome::Declined;
18988 }
18989 let hs = self.hidden_size;
18990 let mut hiddens = vec![0f32; b * hs];
18991 for (j, &id) in ids.iter().enumerate() {
18992 let e = self.embed_single(id);
18993 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
18994 }
18995 self.prefill_hiddens_metal(hiddens, start_pos, spec)
18996 }
18997
18998 #[cfg(target_os = "macos")]
19000 fn prefill_hiddens_metal(
19001 &mut self,
19002 mut hiddens: Vec<f32>,
19003 start_pos: usize,
19004 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
19005 ) -> MetalPrefillOutcome {
19006 let b = hiddens.len() / self.hidden_size;
19007 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19008 let with_head = spec.is_some();
19009 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
19010 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
19011 MetalRowsRun::Failed => {
19012 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19013 return MetalPrefillOutcome::Failed;
19014 }
19015 MetalRowsRun::Completed(pending) => pending,
19016 };
19017 let idxs = pending.gdn_layers.clone();
19019 let mut outs: Vec<&mut [f32]> = self
19020 .kv_cache
19021 .layers
19022 .iter_mut()
19023 .enumerate()
19024 .filter(|(i, _)| idxs.binary_search(i).is_ok())
19025 .map(|(_, l)| l.linear_state.as_mut_slice())
19026 .collect();
19027 if !pending.graph.finish_states(&mut outs) {
19028 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19029 return MetalPrefillOutcome::Failed;
19030 }
19031 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19032 let mut rows = Vec::with_capacity(pending.attn_layers.len());
19036 for (li, cpu_stored) in &pending.attn_layers {
19037 let mut kbuf = vec![0f32; b * nkv * hd];
19038 let mut vbuf = vec![0f32; b * nkv * hd];
19039 if !crate::gpu_metal::kv_mirror_read_rows(
19040 self.graph_kv_id,
19041 *li,
19042 nkv,
19043 hd,
19044 *cpu_stored,
19045 b,
19046 &mut kbuf,
19047 &mut vbuf,
19048 ) {
19049 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
19050 return MetalPrefillOutcome::Failed;
19051 }
19052 rows.push((*li, *cpu_stored, kbuf, vbuf));
19053 }
19054 for (li, cpu_stored, kbuf, vbuf) in rows {
19055 let cache = &mut self.kv_cache.layers[li];
19056 for r in 0..b {
19057 cache.append(
19058 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19059 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19060 &[],
19061 );
19062 }
19063 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
19064 }
19065 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
19066 if with_head {
19067 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
19068 }
19069 self.swa_trim_tails();
19071 MetalPrefillOutcome::Completed(hiddens)
19072 }
19073
19074 #[cfg(target_os = "macos")]
19075 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
19076 self.prefill_rows_metal(ids, start_pos, None)
19077 }
19078
19079 #[cfg(target_os = "macos")]
19084 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
19085 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
19086 return MetalBatchNllOutcome::Declined;
19087 }
19088 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
19089 return MetalBatchNllOutcome::Declined;
19090 };
19091 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
19092 .ok()
19093 .and_then(|v| v.parse::<usize>().ok())
19094 .filter(|&v| (1..=512).contains(&v))
19095 .unwrap_or(32);
19096 let final_norm = self.weights.final_norm.clone();
19097 let mut nll = 0.0f64;
19098 let mut count = 0usize;
19099 let mut pos = 0usize;
19100 let mut completed = 0usize;
19101 while pos < ids.len() {
19102 let end = (pos + chunk).min(ids.len());
19103 let mut logits = Vec::new();
19104 let outcome =
19105 self.prefill_rows_metal(&ids[pos..end], pos, Some((lm, &final_norm, &mut logits)));
19106 match outcome {
19107 MetalPrefillOutcome::Declined => {
19108 return if completed == 0 {
19109 MetalBatchNllOutcome::Declined
19110 } else {
19111 MetalBatchNllOutcome::Failed(format!(
19112 "ordinary Metal NLL batch declined after {completed} chunks"
19113 ))
19114 };
19115 }
19116 MetalPrefillOutcome::Failed => {
19117 return MetalBatchNllOutcome::Failed(
19118 "ordinary Metal NLL batch failed after admission".to_string(),
19119 );
19120 }
19121 MetalPrefillOutcome::Completed(_) => {}
19122 }
19123 completed += 1;
19124 let vocab = self.vocab_size.min(lm.1);
19125 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
19126 return MetalBatchNllOutcome::Failed(
19127 "ordinary Metal NLL head returned an invalid shape".to_string(),
19128 );
19129 }
19130 for row in 0..(end - pos) {
19131 let absolute = pos + row;
19132 if absolute < start || absolute + 1 >= ids.len() {
19133 continue;
19134 }
19135 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
19136 if let Some(mu) = self.logit_multiplier {
19137 for v in lg.iter_mut() {
19138 *v *= mu;
19139 }
19140 }
19141 if let Some(c) = self.final_softcap {
19142 softcap_in_place(lg, c, self.pool.as_deref());
19143 }
19144 let target = ids[absolute + 1] as usize;
19145 if target >= vocab {
19146 return MetalBatchNllOutcome::Failed(format!(
19147 "target token {target} exceeds Metal head rows {vocab}"
19148 ));
19149 }
19150 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
19151 let lse: f64 = lg
19152 .iter()
19153 .map(|&v| ((v - max) as f64).exp())
19154 .sum::<f64>()
19155 .ln()
19156 + max as f64;
19157 nll += lse - lg[target] as f64;
19158 count += 1;
19159 }
19160 pos = end;
19161 }
19162 MetalBatchNllOutcome::Completed(nll, count)
19163 }
19164
19165 #[cfg(target_os = "macos")]
19169 fn metal_verify_commit(&mut self, a: usize) -> bool {
19170 let Some(mut pending) = self.metal_verify.take() else {
19171 return false;
19172 };
19173 let n = a + 1;
19174 let idxs = pending.gdn_layers.clone();
19176 let mut outs: Vec<&mut [f32]> = self
19177 .kv_cache
19178 .layers
19179 .iter_mut()
19180 .enumerate()
19181 .filter(|(i, _)| idxs.binary_search(i).is_ok())
19182 .map(|(_, l)| l.linear_state.as_mut_slice())
19183 .collect();
19184 if !pending.graph.commit(n, &mut outs) {
19185 return false;
19186 }
19187 spec_stamp("c.replay");
19188 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19189 let mut rows = Vec::with_capacity(pending.attn_layers.len());
19193 for (li, cpu_stored) in &pending.attn_layers {
19194 let mut kbuf = vec![0f32; n * nkv * hd];
19195 let mut vbuf = vec![0f32; n * nkv * hd];
19196 if !crate::gpu_metal::kv_mirror_read_rows(
19197 self.graph_kv_id,
19198 *li,
19199 nkv,
19200 hd,
19201 *cpu_stored,
19202 n,
19203 &mut kbuf,
19204 &mut vbuf,
19205 ) {
19206 return false;
19207 }
19208 rows.push((*li, *cpu_stored, kbuf, vbuf));
19209 }
19210 for (li, cpu_stored, kbuf, vbuf) in rows {
19211 let cache = &mut self.kv_cache.layers[li];
19212 for r in 0..n {
19213 cache.append(
19214 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19215 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19216 &[],
19217 );
19218 }
19219 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
19220 }
19221 spec_stamp("c.kv");
19222 true
19223 }
19224
19225 #[cfg(target_os = "macos")]
19232 fn mtp_warm_batch_submit(
19233 &mut self,
19234 m: &mut MtpModule,
19235 pairs: &[(&[f32], u32)],
19236 first_pos: usize,
19237 ) -> Option<MetalWarmPending> {
19238 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
19239 let first_pos = first_pos.saturating_sub(self.mtp_pos_base);
19240 let b = pairs.len();
19241 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
19242 return None;
19243 }
19244 let AttnKind::Full {
19245 wq,
19246 wk,
19247 wv,
19248 wo,
19249 q_norm,
19250 k_norm,
19251 output_gate,
19252 softplus_gate: None,
19253 bias: None,
19254 } = &m.layer.attn
19255 else {
19256 return None;
19257 };
19258 let FfnKind::Dense(d) = &m.layer.ffn else {
19259 return None;
19260 };
19261 if !d.segs.is_empty() {
19262 return None;
19263 }
19264 let (Some(pq), Some(pk), Some(pv), Some(po)) =
19265 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
19266 else {
19267 return None;
19268 };
19269 let (Some(g), Some(u), Some(dn)) = (
19270 d.gate_proj.q1_parts(),
19271 d.up_proj.q1_parts(),
19272 d.down_proj.q1_parts(),
19273 ) else {
19274 return None;
19275 };
19276 let Some(eh) = m.eh_proj.q1_parts() else {
19277 return None;
19278 };
19279 let QTensor::Mapped { model, .. } = wq else {
19280 return None;
19281 };
19282 let model = model.clone();
19283 let hs = self.hidden_size;
19284 let mut cat = vec![0f32; b * 2 * hs];
19286 for (j, (h, tok)) in pairs.iter().enumerate() {
19287 let e = self.embed_single(*tok);
19288 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
19289 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
19290 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
19291 }
19292 let dims = GraphDims {
19293 hidden: hs,
19294 eps: self.rms_eps as f32,
19295 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19296 };
19297 spec_stamp("w.cat");
19298 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
19299 return None;
19300 };
19301 spec_stamp("w.new");
19302 let l = AttnGpuLayer {
19303 mhc: None,
19304 attn_norm: &m.layer.input_norm,
19305 post_norm: &m.layer.post_norm,
19306 wq: pq,
19307 wk: pk,
19308 wv: pv,
19309 wo: po,
19310 ffn: MetalFfn::Dense {
19311 gate: g,
19312 up: u,
19313 down: dn,
19314 act: (d.act == Act::Gelu) as u8,
19315 },
19316 attn_out_norm: None,
19317 ffn_out_norm: None,
19318 layer_scale: None,
19319 };
19320 let (nh, nkv, hd, rd) = (
19321 self.num_heads,
19322 self.num_kv_heads,
19323 self.head_dim,
19324 self.rotary_dim,
19325 );
19326 let inv_freq = self.inv_freq.clone();
19327 let cpu_stored;
19328 {
19329 let cache = &m.kv;
19330 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19331 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19332 cpu_stored = cpu_k[0].len() / hd;
19333 if cpu_stored > first_pos {
19338 spec_stamp("w.decl");
19339 return None;
19340 }
19341 let p = AttnDeviceParams {
19342 kv_id: self.mtp_kv_id(),
19343 layer: Self::MTP_LAYER_BASE,
19344 nh,
19345 nkv,
19346 hd,
19347 rd,
19348 position: first_pos,
19349 scale: self.attn_scale,
19350 eps: self.rms_eps as f32,
19351 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19352 late_qk_norm: self.qk_norm_after_rope,
19353 output_gate: *output_gate,
19354 q_norm: q_norm.as_deref(),
19355 k_norm: k_norm.as_deref(),
19356 inv_freq: &inv_freq,
19357 rope_scale: self.rope_scale,
19358 cpu_k,
19359 cpu_v,
19360 cpu_stored,
19361 cpu_gen: cache.generation(),
19362 o1: None,
19363 window: None,
19364 softmax_sink: None,
19365 head_gate: None,
19366 v_norm: false,
19367 dash: None,
19368 };
19369 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
19370 return None;
19371 }
19372 }
19373 spec_stamp("w.enc");
19374 if !graph.submit() {
19375 return None;
19376 }
19377 spec_stamp("w.sub");
19378 Some(MetalWarmPending {
19379 graph,
19380 cpu_stored,
19381 b,
19382 })
19383 }
19384
19385 #[cfg(target_os = "macos")]
19388 fn mtp_warm_batch_metal(
19389 &mut self,
19390 m: &mut MtpModule,
19391 pairs: &[(&[f32], u32)],
19392 first_pos: usize,
19393 ) -> bool {
19394 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
19395 Some(p) => self.mtp_warm_batch_finish(m, p),
19396 None => false,
19397 }
19398 }
19399
19400 #[cfg(target_os = "macos")]
19405 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
19406 let MetalWarmPending {
19407 mut graph,
19408 cpu_stored,
19409 b,
19410 } = pending;
19411 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
19412 if !graph.sync() {
19413 return false;
19414 }
19415 spec_stamp("w.gpu");
19416 let mut kbuf = vec![0f32; b * nkv * hd];
19417 let mut vbuf = vec![0f32; b * nkv * hd];
19418 if !crate::gpu_metal::kv_mirror_read_rows(
19419 self.mtp_kv_id(),
19420 Self::MTP_LAYER_BASE,
19421 nkv,
19422 hd,
19423 cpu_stored,
19424 b,
19425 &mut kbuf,
19426 &mut vbuf,
19427 ) {
19428 return false;
19429 }
19430 for r in 0..b {
19431 m.kv.append(
19432 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19433 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19434 &[],
19435 );
19436 }
19437 crate::gpu_metal::kv_mirror_set_stored(
19438 self.mtp_kv_id(),
19439 Self::MTP_LAYER_BASE,
19440 cpu_stored + b,
19441 );
19442 spec_stamp("w.kv");
19443 true
19444 }
19445
19446 pub(crate) fn note_draft_id(&mut self, id: u32) {
19453 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
19454 if (id as usize) >= cut {
19455 self.draft_full_streak = 16;
19456 } else {
19457 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
19458 }
19459 }
19460
19461 fn draft_head_rows(&self, head_rows: usize) -> usize {
19464 if self.draft_full_streak > 0 {
19465 head_rows
19466 } else {
19467 Self::draft_vocab_rows(head_rows)
19468 }
19469 }
19470
19471 fn draft_vocab_rows(head_rows: usize) -> usize {
19474 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
19475 let n = *N.get_or_init(|| {
19476 std::env::var("CMF_DRAFT_VOCAB")
19477 .ok()
19478 .and_then(|v| v.parse().ok())
19479 .unwrap_or(65536)
19480 });
19481 if n == 0 { head_rows } else { n.min(head_rows) }
19482 }
19483
19484 #[cfg(target_os = "macos")]
19489 fn mtp_step_metal(
19490 &mut self,
19491 m: &mut MtpModule,
19492 hidden: &[f32],
19493 next_token: u32,
19494 position: usize,
19495 want_logits: bool,
19496 ) -> Option<(Vec<f32>, Vec<f32>)> {
19497 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
19498 let position = position.saturating_sub(self.mtp_pos_base);
19499 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
19500 || !crate::gpu::q1_force()
19501 || !crate::gpu::enabled_here()
19502 || self.attn_softcap > 0.0
19503 || self.attention_heads_per_layer.is_some()
19504 || m.kv.mode != crate::kv_cache::KvMode::F32
19505 || m.kv.o1.is_some()
19506 {
19507 return None;
19508 }
19509 let AttnKind::Full {
19510 wq,
19511 wk,
19512 wv,
19513 wo,
19514 q_norm,
19515 k_norm,
19516 output_gate,
19517 softplus_gate: None,
19518 bias: None,
19519 } = &m.layer.attn
19520 else {
19521 return None;
19522 };
19523 let FfnKind::Dense(d) = &m.layer.ffn else {
19524 return None;
19525 };
19526 if d.act != Act::Silu || !d.segs.is_empty() {
19527 return None;
19528 }
19529 let (pq, pk, pv, po) = (
19530 wq.q1_parts()?,
19531 wk.q1_parts()?,
19532 wv.q1_parts()?,
19533 wo.q1_parts()?,
19534 );
19535 let (g, u, dn) = (
19536 d.gate_proj.q1_parts()?,
19537 d.up_proj.q1_parts()?,
19538 d.down_proj.q1_parts()?,
19539 );
19540 let QTensor::Mapped { model, .. } = wq else {
19541 return None;
19542 };
19543 let model = model.clone();
19544 let lm = if want_logits {
19545 Some(self.weights.lm_head.q1_parts()?)
19546 } else {
19547 None
19548 };
19549 let dims = GraphDims {
19550 hidden: self.hidden_size,
19551 eps: self.rms_eps as f32,
19552 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19553 };
19554 let hs = self.hidden_size;
19557 let mut x = vec![0f32; hs];
19558 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
19565 crate::gpu_metal::DENSE_MV,
19566 );
19567 let mut graph = TokenGraph::new(&model, dims, &x)?;
19568 let mut folded = false;
19569 if let Some(eh) = m.eh_proj.q1_parts() {
19570 let e = self.embed_single(next_token);
19571 let mut cat = vec![0.0f32; 2 * hs];
19572 let (cat_e, cat_h) = cat.split_at_mut(hs);
19573 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
19574 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
19575 folded = graph.encode_input_proj(eh, &cat);
19576 }
19577 if !folded {
19578 x = self.mtp_block_input(m, hidden, next_token);
19579 graph = TokenGraph::new(&model, dims, &x)?;
19580 }
19581 spec_stamp("d.in");
19582 let l = AttnGpuLayer {
19583 mhc: None,
19584 attn_norm: &m.layer.input_norm,
19585 post_norm: &m.layer.post_norm,
19586 wq: pq,
19587 wk: pk,
19588 wv: pv,
19589 wo: po,
19590 ffn: MetalFfn::Dense {
19591 gate: g,
19592 up: u,
19593 down: dn,
19594 act: (d.act == Act::Gelu) as u8,
19595 },
19596 attn_out_norm: None,
19597 ffn_out_norm: None,
19598 layer_scale: None,
19599 };
19600 let (nh, nkv, hd, rd) = (
19601 self.num_heads,
19602 self.num_kv_heads,
19603 self.head_dim,
19604 self.rotary_dim,
19605 );
19606 let inv_freq = self.inv_freq.clone();
19607 {
19608 let cache = &m.kv;
19609 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19610 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19611 let cpu_stored = cpu_k[0].len() / hd;
19612 let p = AttnDeviceParams {
19613 kv_id: self.mtp_kv_id(),
19614 layer: Self::MTP_LAYER_BASE,
19615 nh,
19616 nkv,
19617 hd,
19618 rd,
19619 position,
19620 scale: self.attn_scale,
19621 eps: self.rms_eps as f32,
19622 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19623 late_qk_norm: self.qk_norm_after_rope,
19624 output_gate: *output_gate,
19625 q_norm: q_norm.as_deref(),
19626 k_norm: k_norm.as_deref(),
19627 inv_freq: &inv_freq,
19628 rope_scale: self.rope_scale,
19629 cpu_k,
19630 cpu_v,
19631 cpu_stored,
19632 cpu_gen: cache.generation(),
19633 o1: None,
19634 window: None,
19635 softmax_sink: None,
19636 head_gate: None,
19637 v_norm: false,
19638 dash: None,
19639 };
19640 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
19641 return None;
19642 }
19643 }
19644 let draft_rows = if let Some(lm) = lm {
19650 self.draft_head_rows(lm.1)
19651 } else {
19652 0
19653 };
19654 if let Some(lm) = lm {
19655 if !graph.lm_head_ok(lm) {
19656 return None;
19657 }
19658 if draft_rows < lm.1 {
19659 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
19660 return None;
19661 }
19662 } else {
19663 graph.encode_lm_head(&m.final_norm, lm);
19664 }
19665 }
19666 spec_stamp("d.enc");
19667 if graph.sync_checked().is_err() {
19668 return None;
19669 }
19670 spec_stamp("d.gpu");
19671 let mut logits = Vec::new();
19672 if let Some(lm) = lm {
19673 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
19674 logits = attention::take_buf(n_read);
19675 graph.read_logits(&mut logits);
19676 pad_mtp_draft_logits(&mut logits, self.vocab_size);
19678 }
19679 graph.finish(&mut x);
19680 let mut krow = attention::take_buf(nkv * hd);
19681 let mut vrow = attention::take_buf(nkv * hd);
19682 if crate::gpu_metal::kv_mirror_read_last(
19683 self.mtp_kv_id(),
19684 Self::MTP_LAYER_BASE,
19685 nkv,
19686 hd,
19687 &mut krow,
19688 &mut vrow,
19689 ) {
19690 m.kv.append(&krow, &vrow, &[]);
19691 }
19692 attention::recycle_buf(&mut krow);
19693 attention::recycle_buf(&mut vrow);
19694 spec_stamp("d.rd");
19695 Some((logits, x))
19696 }
19697
19698 fn mtp_chain_on() -> bool {
19711 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19712 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
19713 }
19714
19715 #[cfg(target_os = "macos")]
19727 fn mtp_draft_chain_metal(
19728 &mut self,
19729 m: &mut MtpModule,
19730 hidden: &[f32],
19731 t_next: u32,
19732 position: usize,
19733 k: usize,
19734 ) -> Result<Vec<u32>, bool> {
19735 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
19736 let position = position.saturating_sub(self.mtp_pos_base);
19737 if k == 0
19738 || k > 64
19739 || !Self::mtp_chain_on()
19740 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
19741 || !crate::gpu::q1_force()
19742 || !crate::gpu::enabled_here()
19743 || self.attn_softcap > 0.0
19744 || self.attention_heads_per_layer.is_some()
19745 || m.kv.mode != crate::kv_cache::KvMode::F32
19746 || m.kv.o1.is_some()
19747 || self.dsv4.is_some()
19749 || self.dsv41.is_some()
19750 || self.dory.is_some()
19751 || self.qwen4_exp.is_some()
19752 || self.per_token_stack()
19753 {
19754 return Err(false);
19755 }
19756 let AttnKind::Full {
19757 wq,
19758 wk,
19759 wv,
19760 wo,
19761 q_norm,
19762 k_norm,
19763 output_gate,
19764 softplus_gate: None,
19765 bias: None,
19766 } = &m.layer.attn
19767 else {
19768 return Err(false);
19769 };
19770 let FfnKind::Dense(d) = &m.layer.ffn else {
19771 return Err(false);
19772 };
19773 if d.act != Act::Silu || !d.segs.is_empty() {
19774 return Err(false);
19775 }
19776 let (Some(pq), Some(pk), Some(pv), Some(po)) =
19777 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
19778 else {
19779 return Err(false);
19780 };
19781 let (Some(g), Some(u), Some(dn)) = (
19782 d.gate_proj.q1_parts(),
19783 d.up_proj.q1_parts(),
19784 d.down_proj.q1_parts(),
19785 ) else {
19786 return Err(false);
19787 };
19788 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
19789 return Err(false);
19790 };
19791 let QTensor::Mapped { model, .. } = wq else {
19792 return Err(false);
19793 };
19794 let model = model.clone();
19795 let QTensor::Mapped {
19798 model: em,
19799 idx: eidx,
19800 dtype: cortiq_core::TensorDtype::Q4TiledP,
19801 ..
19802 } = &self.weights.embed_tokens
19803 else {
19804 return Err(false);
19805 };
19806 if !std::sync::Arc::ptr_eq(em, &model)
19807 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
19808 {
19809 return Err(false);
19810 }
19811 let embed = (
19812 *eidx,
19813 self.weights.embed_tokens.rows(),
19814 self.weights.embed_tokens.cols(),
19815 );
19816 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
19817 return Err(false);
19818 }
19819 let dims = GraphDims {
19820 hidden: self.hidden_size,
19821 eps: self.rms_eps as f32,
19822 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19823 };
19824 let _mtp_fast = crate::gpu_metal::MvFastGuard::set_bits(
19828 crate::gpu_metal::DENSE_MV,
19829 );
19830 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
19831 return Err(false);
19832 };
19833 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
19834 return Err(false);
19835 }
19836 let l = AttnGpuLayer {
19837 mhc: None,
19838 attn_norm: &m.layer.input_norm,
19839 post_norm: &m.layer.post_norm,
19840 wq: pq,
19841 wk: pk,
19842 wv: pv,
19843 wo: po,
19844 ffn: MetalFfn::Dense {
19845 gate: g,
19846 up: u,
19847 down: dn,
19848 act: (d.act == Act::Gelu) as u8,
19849 },
19850 attn_out_norm: None,
19851 ffn_out_norm: None,
19852 layer_scale: None,
19853 };
19854 let (nh, nkv, hd, rd) = (
19855 self.num_heads,
19856 self.num_kv_heads,
19857 self.head_dim,
19858 self.rotary_dim,
19859 );
19860 let inv_freq = self.inv_freq.clone();
19861 let draft_rows = self.draft_head_rows(lm.1);
19862 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
19863 if n_arg == 0 {
19864 return Err(false);
19865 }
19866 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
19873 let join = std::env::var("CMF_MTP_CHAIN_JOIN").as_deref() == Ok("1");
19880 let t_chain = std::time::Instant::now();
19881 graph.chain_ids_init(t_next, k);
19882 let cpu_stored;
19883 {
19884 let cache = &m.kv;
19885 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
19886 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
19887 cpu_stored = cpu_k[0].len() / hd;
19888 for j in 0..k {
19889 if !graph.encode_chain_input(
19890 embed,
19891 j as u32,
19892 &m.enorm,
19893 &m.hnorm,
19894 self.embed_multiplier,
19895 eh,
19896 ) {
19897 return Err(false);
19898 }
19899 let p = AttnDeviceParams {
19903 kv_id: self.mtp_kv_id(),
19904 layer: Self::MTP_LAYER_BASE,
19905 nh,
19906 nkv,
19907 hd,
19908 rd,
19909 position: position + j,
19910 scale: self.attn_scale,
19911 eps: self.rms_eps as f32,
19912 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
19913 late_qk_norm: self.qk_norm_after_rope,
19914 output_gate: *output_gate,
19915 q_norm: q_norm.as_deref(),
19916 k_norm: k_norm.as_deref(),
19917 inv_freq: &inv_freq,
19918 rope_scale: self.rope_scale,
19919 cpu_k: cpu_k.clone(),
19920 cpu_v: cpu_v.clone(),
19921 cpu_stored: cpu_stored + j,
19922 cpu_gen: cache.generation(),
19923 o1: None,
19924 window: None,
19925 softmax_sink: None,
19926 head_gate: None,
19927 v_norm: false,
19928 dash: None,
19929 };
19930 if !join {
19931 graph.commit();
19932 }
19933 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
19934 return Err(false);
19935 }
19936 if draft_rows < lm.1 {
19937 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
19938 return Err(false);
19939 }
19940 } else {
19941 graph.encode_lm_head(&m.final_norm, lm);
19942 }
19943 if !graph.encode_argmax(n_arg, j as u32 + 1) {
19944 return Err(false);
19945 }
19946 if split {
19947 graph.commit();
19950 }
19951 }
19952 }
19953 let t_enc = t_chain.elapsed();
19954 if graph.sync_checked().is_err() {
19955 return Err(true);
19956 }
19957 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
19958 eprintln!(
19959 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
19960 t_enc.as_secs_f64() * 1e3,
19961 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
19962 if split { ", split" } else { "" }
19963 );
19964 if std::env::var("CMF_METAL_CMD_TIMES").as_deref() == Ok("1") {
19965 eprintln!("metal-set: {}", crate::gpu_metal::working_set_report());
19966 }
19967 }
19968 let mut ids = vec![0u32; k];
19969 if !graph.chain_ids_read(&mut ids) {
19970 return Err(true);
19971 }
19972 let mut kbuf = vec![0f32; k * nkv * hd];
19973 let mut vbuf = vec![0f32; k * nkv * hd];
19974 if !crate::gpu_metal::kv_mirror_read_rows(
19975 self.mtp_kv_id(),
19976 Self::MTP_LAYER_BASE,
19977 nkv,
19978 hd,
19979 cpu_stored,
19980 k,
19981 &mut kbuf,
19982 &mut vbuf,
19983 ) {
19984 return Err(true);
19985 }
19986 for r in 0..k {
19987 m.kv.append(
19988 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
19989 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
19990 &[],
19991 );
19992 }
19993 Ok(ids)
19994 }
19995
19996 fn try_batch_graph_wgpu(
19997 &self,
19998 hiddens: &mut [f32],
19999 positions: &[usize],
20000 k: usize,
20001 spec: Option<crate::gpu::SpecTail<'_>>,
20002 ) -> crate::gpu::BatchGraphOutcome {
20003 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
20004 }
20005
20006 fn try_batch_graph_wgpu_prefix(
20011 &self,
20012 hiddens: &mut [f32],
20013 positions: &[usize],
20014 k: usize,
20015 spec: Option<crate::gpu::SpecTail<'_>>,
20016 layers_run: Option<&mut usize>,
20017 ) -> crate::gpu::BatchGraphOutcome {
20018 crate::gpu::graph_kv16_note(self.graph_kv_id, self.kv16_allowed());
20019 if self.mhc.as_ref().is_some_and(|m| {
20023 m.s.streams != 2 || m.s.fixed_identity || std::env::var("CMF_MHC_GRAPH").as_deref() == Ok("0")
20024 })
20025 {
20026 return crate::gpu::BatchGraphOutcome::Declined;
20027 }
20028 let graph_end = match self.mimo_moe.graph_prefix_end() {
20029 Some(end) if end < self.num_layers => {
20030 if layers_run.is_none() || spec.is_some() || end == 0 {
20031 return crate::gpu::BatchGraphOutcome::Declined;
20032 }
20033 end
20034 }
20035 _ => self.num_layers,
20036 };
20037 let _tb = std::time::Instant::now();
20038 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
20039 if self.attn_softcap > 0.0 {
20040 return crate::gpu::BatchGraphOutcome::Declined; }
20042 if let Some(reason) = self
20045 .wgpu_graph_attn_decline()
20046 .or_else(|| self.batch_graph_gemma_decline())
20047 {
20048 self.note_graph_decline("wgpu batch graph", reason);
20049 return crate::gpu::BatchGraphOutcome::Declined;
20050 }
20051 let nh = self.num_heads;
20052 let (nkv, hd, rd) = self.layer_geom(0);
20053 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
20054 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
20055 if let Some((m, i, kind, rs)) = t.graph_weight().or_else(|| t.graph_weight_descriptor())
20056 {
20057 let name = &m.tensors[i].name;
20058 let prism = if crate::prism::is_inverse_embedding(m, name) {
20059 crate::gpu::GraphPrismOp::InverseEmbedding
20060 } else if crate::prism::is_forward_weight(m, name) {
20061 crate::gpu::GraphPrismOp::Forward
20062 } else {
20063 crate::gpu::GraphPrismOp::None
20064 };
20065 return Some(crate::gpu::GraphW {
20066 idx: i,
20067 kind,
20068 row_scale: rs,
20069 data: &[],
20070 prism,
20071 affine: crate::prism::is_affine_target(m, name),
20072 });
20073 }
20074 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
20075 eprintln!(
20076 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
20077 t.rows(),
20078 t.cols()
20079 );
20080 }
20081 t.as_f32().map(|d| crate::gpu::GraphW {
20082 idx: 0,
20083 kind: 4,
20084 row_scale: &[],
20085 data: d,
20086 prism: crate::gpu::GraphPrismOp::None,
20087 affine: false,
20088 })
20089 }
20090 let vres_e0: Vec<f32> = if self.value_blend.is_some() {
20093 hiddens[..k * self.hidden_size].to_vec()
20094 } else {
20095 Vec::new()
20096 };
20097 let built: Option<(
20098 Vec<crate::gpu::GraphLayer<'_>>,
20099 std::sync::Arc<cortiq_core::CmfModel>,
20100 )> = (|| {
20101 let mut layers = Vec::with_capacity(graph_end);
20102 let mut model = None;
20103 for li in 0..graph_end {
20104 let lw = &self.weights.layers[self.phys_layer(li)];
20105 let gffn = match &lw.ffn {
20112 FfnKind::Dense(d) if d.segs.len() > 1 => {
20113 if batch_debug {
20114 eprintln!("batch graph: dense segmented FFN at layer {li}");
20115 }
20116 return None;
20117 }
20118 FfnKind::Dense(d) => {
20119 let Some((act, limit)) = d.act.dense_graph_contract() else {
20120 if batch_debug {
20121 eprintln!(
20122 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
20123 d.act
20124 );
20125 }
20126 return None;
20127 };
20128 crate::gpu::GraphFfn::Dense {
20129 gate: gw(&d.gate_proj)?,
20130 up: gw(&d.up_proj)?,
20131 down: gw(&d.down_proj)?,
20132 act,
20133 limit,
20134 hot: match d.segs.first() {
20136 Some(seg) => Some(crate::gpu::GraphMoeDetachedShared {
20137 gate: gw(&seg.gate)?,
20138 up: gw(&seg.up)?,
20139 down: gw(&seg.down)?,
20140 width: seg.width,
20141 }),
20142 None => None,
20143 },
20144 }
20145 }
20146 FfnKind::Moe(m) => {
20147 if m.route_tau.is_some() || m.mask.is_some() {
20154 return None;
20155 }
20156 let inter0 = m.experts.first()?.gate_proj.rows();
20160 let detached_shared = match m.shared.as_ref() {
20163 Some((se, None)) if se.gate_proj.rows() != inter0 || se.up_proj.rows() != inter0 => {
20164 if !matches!(se.act, Act::Silu) {
20165 return None;
20166 }
20167 Some(crate::gpu::GraphMoeDetachedShared {
20168 gate: gw(&se.gate_proj)?,
20169 up: gw(&se.up_proj)?,
20170 down: gw(&se.down_proj)?,
20171 width: se.gate_proj.rows(),
20172 })
20173 }
20174 _ => None,
20175 };
20176 let shared = m.shared.as_ref().filter(|_| detached_shared.is_none());
20177 let has_shared = shared.is_some();
20178 let shared_gated = matches!(shared, Some((_, Some(_))));
20179 let sgate = match shared {
20180 Some((_, Some(sg))) => gw(sg)?,
20181 _ => gw(&m.router)?,
20185 };
20186 let router = gw(&m.router)?;
20187 if router.prism != crate::gpu::GraphPrismOp::None
20193 || router.affine
20194 || sgate.prism != crate::gpu::GraphPrismOp::None
20195 || sgate.affine
20196 {
20197 return None;
20198 }
20199 let inter = m.experts.first()?.gate_proj.rows();
20200 let mut experts = Vec::with_capacity(m.experts.len() + 1);
20201 let mut q4tp: Option<bool> = None;
20202 let mut gu_q2: Option<bool> = None;
20203 let mut q82: Option<bool> = None;
20204 let mut moe_act = None;
20208 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
20209 let Some(e_act) = e.act.wgpu_graph_act() else {
20210 return None;
20211 };
20212 match moe_act {
20213 Some(existing) if existing != e_act => return None,
20214 None => moe_act = Some(e_act),
20215 _ => {}
20216 }
20217 if e.gate_proj.rows() != inter || e.up_proj.rows() != inter {
20218 return None;
20219 }
20220 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
20224 {
20225 Some((mm, gi)) => (
20226 mm,
20227 gi,
20228 e.up_proj.mapped_q4t()?.1,
20229 e.down_proj.mapped_q4t()?.1,
20230 false,
20231 false,
20232 false,
20233 ),
20234 None => match e.gate_proj.mapped_q2tp() {
20235 Some((mm, gi)) => (
20236 mm,
20237 gi,
20238 e.up_proj.mapped_q2tp()?.1,
20239 e.down_proj.mapped_q4tp()?.1,
20240 true,
20241 true,
20242 false,
20243 ),
20244 None => match e.gate_proj.mapped_q4tp() {
20245 Some((mm, gi)) => (
20246 mm,
20247 gi,
20248 e.up_proj.mapped_q4tp()?.1,
20249 e.down_proj.mapped_q4tp()?.1,
20250 true,
20251 false,
20252 false,
20253 ),
20254 None => {
20255 if !crate::gpu::moe_q82_graph_on() {
20256 return None;
20257 }
20258 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
20259 (
20260 mm,
20261 gi,
20262 e.up_proj.mapped_q8_2f()?.1,
20263 e.down_proj.mapped_q8_2f()?.1,
20264 false,
20265 false,
20266 true,
20267 )
20268 }
20269 },
20270 },
20271 };
20272 if *q4tp.get_or_insert(is_p) != is_p
20273 || *gu_q2.get_or_insert(is_q2) != is_q2
20274 || *q82.get_or_insert(is_8) != is_8
20275 {
20276 return None;
20277 }
20278 if [gi, ui, di].into_iter().any(|idx| {
20279 mm.tensors.get(idx).is_some_and(|t| {
20280 crate::prism::is_forward_weight(mm, &t.name)
20281 || crate::prism::is_affine_target(mm, &t.name)
20282 })
20283 }) {
20284 return None;
20285 }
20286 model.get_or_insert_with(|| mm.clone());
20287 experts.push((gi, ui, di));
20288 }
20289 crate::gpu::GraphFfn::Moe {
20290 router,
20291 shared_gate: sgate,
20292 experts,
20293 n_exp: m.experts.len(),
20294 top_k: m.top_k,
20295 inter,
20296 act: moe_act?,
20297 norm_topk: m.norm_topk_prob,
20298 q4tp: q4tp?,
20299 gu_q2: gu_q2.unwrap_or(false),
20300 q82: q82.unwrap_or(false),
20301 sigmoid: m.router_sigmoid,
20302 sigmoid_denom_eps: m.sigmoid_denom_eps,
20303 bias: m.expert_bias.as_deref(),
20304 has_shared,
20305 detached_shared,
20306 shared_gated,
20307 route_scale: m.routed_scaling,
20308 }
20309 }
20310 _ => return None,
20311 };
20312 let attn = match &lw.attn {
20313 AttnKind::Full {
20314 wq,
20315 wk,
20316 wv,
20317 wo,
20318 q_norm,
20319 k_norm,
20320 output_gate,
20321 softplus_gate,
20322 bias,
20323 } => {
20324 let head_gate = match softplus_gate {
20327 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => Some(gw(g)?),
20328 None => None,
20329 Some(_) => {
20330 if batch_debug {
20331 eprintln!("batch graph: unsupported Full attention gate at layer {li}");
20332 }
20333 return None;
20334 }
20335 };
20336 if self.attention_heads_per_layer.is_some() {
20337 if batch_debug {
20338 eprintln!("batch graph: per-layer Q heads at layer {li}");
20339 }
20340 return None;
20341 }
20342 let (m, _, _, _) =
20343 wq.graph_weight().or_else(|| wq.graph_weight_descriptor())?;
20344 model = Some(m.clone());
20345 crate::gpu::GraphAttn::Full {
20346 wq: gw(wq)?,
20347 wk: gw(wk)?,
20348 wv: gw(wv)?,
20349 wo: gw(wo)?,
20350 q_norm: q_norm.as_deref(),
20351 k_norm: k_norm.as_deref(),
20352 late_qk_norm: self.qk_norm_after_rope,
20353 bias: bias
20354 .as_ref()
20355 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
20356 output_gate: *output_gate,
20357 cpu_k: self.kv_cache.layers[li].k_heads(),
20358 cpu_v: self.kv_cache.layers[li].v_heads(),
20359 cpu_base: self.kv_cache.layers[li].base(),
20360 geom: self.graph_attn_geom_vres(li, &vres_e0),
20361 head_gate,
20362 }
20363 }
20364 AttnKind::LinearGdn(w) => {
20365 let Some(cfg) = self.gdn_cfg else {
20366 if batch_debug {
20367 eprintln!("batch graph: no GDN config at layer {li}");
20368 }
20369 return None;
20370 };
20371 let (m, _, _, _) = w
20372 .in_proj_qkv
20373 .graph_weight()
20374 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
20375 model = Some(m.clone());
20376 crate::gpu::GraphAttn::Gdn {
20377 qkv: gw(&w.in_proj_qkv)?,
20378 z: gw(&w.in_proj_z)?,
20379 a: gw(&w.in_proj_a)?,
20380 b: gw(&w.in_proj_b)?,
20381 out: gw(&w.out_proj)?,
20382 conv1d: &w.conv1d,
20383 a_log: &w.a_log,
20384 dt_bias: &w.dt_bias,
20385 norm: &w.norm,
20386 nv: cfg.num_v_heads,
20387 nk: cfg.num_k_heads,
20388 dk: cfg.key_head_dim,
20389 dv: cfg.value_head_dim,
20390 kk: cfg.conv_kernel,
20391 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
20392 preview: self.clover.as_ref().and_then(|c| c.graph_preview(li)),
20393 }
20394 }
20395 _ => return None,
20396 };
20397 let g4 = self.global_attn.is_some() || self.attn_v_norm;
20401 let sandwich = g4 || self.value_blend.is_some();
20402 layers.push(crate::gpu::GraphLayer {
20403 mhc: crate::sensenova::graph_mhc(self.mhc.as_deref(), self.phys_layer(li)),
20404 input_norm: &lw.input_norm,
20405 attn,
20406 post_norm: &lw.post_norm,
20407 attn_out_norm: if sandwich {
20408 lw.attn_out_norm.as_deref()
20409 } else {
20410 None
20411 },
20412 ffn_out_norm: if sandwich { lw.ffn_out_norm.as_deref() } else { None },
20413 layer_scale: if g4 { lw.layer_scale } else { None },
20414 ffn: gffn,
20415 });
20416 }
20417 Some((layers, model?))
20418 })();
20419 let Some((layers, model)) = built else {
20420 {
20421 use std::sync::atomic::{AtomicBool, Ordering};
20422 static SAID: AtomicBool = AtomicBool::new(false);
20423 if !SAID.swap(true, Ordering::Relaxed) {
20424 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
20425 }
20426 }
20427 return crate::gpu::BatchGraphOutcome::Declined;
20428 };
20429 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
20430 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
20431 }
20432 #[cfg(feature = "gpu")]
20435 crate::gpu_wgpu::set_batch_q82_mm(self.mhc.is_some() || self.onevision2_prefill());
20436 #[cfg(feature = "gpu")]
20438 let _mrope = self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| {
20439 let half = rd / 2;
20440 let mut tab = vec![0f32; k * 64];
20441 for (i, &p) in positions.iter().enumerate() {
20442 for j in 0..half.min(64) {
20443 let a = pos.get(p).map_or(p as f32, |q| q[crate::sensenova::mrope_axis(j, sections)] as f32);
20444 tab[i * 64 + j] = a * self.inv_freq[j];
20445 }
20446 }
20447 crate::gpu_wgpu::set_batch_mrope(Some(tab));
20448 struct Clear;
20449 impl Drop for Clear {
20450 fn drop(&mut self) {
20451 crate::gpu_wgpu::set_batch_mrope(None);
20452 }
20453 }
20454 Clear
20455 });
20456 let taps = if positions.last().is_some_and(|&p| p < self.dflash_tap_from) { Vec::new() } else { self.dflash_taps_wanted(0, graph_end) };
20458 crate::gpu::layer_taps_arm(&taps);
20459 let outcome = crate::gpu::forward_batch_graph(
20460 &model,
20461 self.graph_kv_id,
20462 &layers,
20463 &self.inv_freq,
20464 hiddens,
20465 nh,
20466 nkv,
20467 hd,
20468 rd,
20469 self.hidden_size,
20470 self.intermediate_size,
20471 positions,
20472 self.kv_cache.max_seq_len,
20473 gemma,
20474 self.rms_eps as f32,
20475 self.attn_scale,
20476 k,
20477 &(0..graph_end)
20478 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
20479 .collect::<Vec<_>>(),
20480 self.o1_epoch,
20481 spec,
20482 layers_run,
20483 );
20484 self.dflash_taps_keep(&taps, positions[0], k, outcome == crate::gpu::BatchGraphOutcome::Completed);
20485 outcome
20486 }
20487
20488 fn dflash_taps_wanted(&self, from: usize, upto_excl: usize) -> Vec<usize> {
20491 match &self.dflash {
20492 Some(d) if (self.dflash_solo || dflash_pick() == Some(true) || crate::gpu::cuda_active()) && from == 0 && upto_excl >= self.num_layers => d.g.taps.clone(),
20493 _ => Vec::new(),
20494 }
20495 }
20496
20497 fn dflash_taps_keep(&self, taps: &[usize], pos0: usize, rows: usize, ok: bool) {
20500 if taps.is_empty() {
20501 return;
20502 }
20503 let (nt, hs) = (taps.len(), self.hidden_size);
20504 let mut raw = Vec::new();
20505 if !crate::gpu::layer_taps_take(&mut raw) || !ok || raw.len() != nt * rows * hs {
20506 return;
20507 }
20508 let mut joined = vec![0f32; rows * nt * hs];
20509 for s in 0..nt {
20510 for r in 0..rows {
20511 joined[(r * nt + s) * hs..][..hs].copy_from_slice(&raw[(s * rows + r) * hs..][..hs]);
20512 }
20513 }
20514 if std::env::var_os("CMF_SPEC_DBG").is_some() {
20515 let rms = (joined.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / joined.len().max(1) as f64).sqrt();
20516 eprintln!("solo-dbg: {rows} rows of {nt} layers kept from position {pos0} (rms {rms:.3})");
20517 }
20518 if let Some(dir) = std::env::var_os("CMF_DFLASH_DUMP") {
20519 self.dflash_dump(std::path::Path::new(&dir), pos0, rows, &joined);
20520 }
20521 let mut kept = self.dflash_taps.lock().unwrap();
20522 kept.push((pos0, joined));
20523 let mut rows: usize = kept.iter().map(|(_, r)| r.len()).sum::<usize>() / (nt * hs);
20527 let cap = match &self.dflash {
20528 Some(d) if (0..d.g.layers).all(|l| d.g.window_of(l) > 0) => {
20529 DFLASH_TAPS_ROWS.min((0..d.g.layers).map(|l| d.g.window_of(l)).max().unwrap_or(0) + crate::dflash::BLOCK_SLACK)
20530 }
20531 _ => DFLASH_TAPS_ROWS,
20532 };
20533 while rows > cap && kept.len() > 1 {
20534 rows -= kept.remove(0).1.len() / (nt * hs);
20535 }
20536 }
20537
20538 fn dflash_dump(&self, dir: &std::path::Path, pos0: usize, rows: usize, joined: &[f32]) {
20544 use std::io::Write;
20545 let _ = std::fs::create_dir_all(dir);
20546 let table = |name: &str, t: &QTensor| {
20547 let p = dir.join(name);
20548 if p.exists() {
20549 return;
20550 }
20551 let Ok(f) = std::fs::File::create(&p) else { return };
20552 let mut w = std::io::BufWriter::new(f);
20553 let mut row = vec![0f32; t.cols()];
20554 for r in 0..t.rows() {
20555 t.row_f32(r, &mut row);
20556 let _ = w.write_all(&f32_bytes(&row));
20557 }
20558 };
20559 table("embed.f32", &self.weights.embed_tokens);
20560 table("head.f32", &self.weights.lm_head);
20561 let meta = dir.join("meta.json");
20562 if !meta.exists() {
20563 let _ = std::fs::write(
20564 &meta,
20565 format!(
20566 "{{\"hidden\": {}, \"vocab\": {}, \"embed_multiplier\": {}}}\n",
20567 self.hidden_size,
20568 self.weights.lm_head.rows(),
20569 self.embed_multiplier
20570 ),
20571 );
20572 }
20573 if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(dir.join("taps.bin")) {
20574 let _ = f.write_all(&(pos0 as u64).to_le_bytes());
20575 let _ = f.write_all(&(rows as u64).to_le_bytes());
20576 let _ = f.write_all(&f32_bytes(joined));
20577 }
20578 }
20579
20580 fn dflash_taps_absorb(&mut self) {
20582 let kept = std::mem::take(&mut *self.dflash_taps.lock().unwrap());
20583 if let Some(d) = self.dflash.as_mut() {
20584 for (pos0, rows) in kept {
20585 d.queue_context(pos0, &rows, self.pool.as_deref());
20586 }
20587 }
20588 }
20589
20590 fn draft_probe() -> bool {
20594 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20595 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
20596 }
20597
20598 #[cfg(feature = "gpu")]
20610 fn dsv4_spec_on() -> bool {
20611 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
20612 *ON.get_or_init(|| {
20613 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
20617 return v != "0";
20618 }
20619 std::env::var("CMF_DSV4_SPEC")
20626 .map(|v| v != "0")
20627 .unwrap_or_else(|_| {
20628 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
20629 })
20630 })
20631 }
20632
20633 #[cfg(feature = "gpu")]
20640 fn dsv4_spec_step(
20641 &mut self,
20642 tip_token: u32,
20643 t_next: u32,
20644 next_pos: usize,
20645 max_extra: usize,
20646 drafted: &mut usize,
20647 accepted_ctr: &mut usize,
20648 ) -> Option<(Vec<u32>, usize)> {
20649 let t_all = std::time::Instant::now();
20650 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20651 thread_local! {
20652 static LAST: std::cell::Cell<Option<std::time::Instant>> =
20653 const { std::cell::Cell::new(None) };
20654 }
20655 LAST.with(|l| {
20656 if let Some(prev) = l.get() {
20657 eprintln!(
20658 "between rounds {:.1} ms",
20659 prev.elapsed().as_secs_f64() * 1e3
20660 );
20661 }
20662 l.set(Some(std::time::Instant::now()));
20663 });
20664 }
20665 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
20666 eprintln!("spec_step: enter pos={next_pos}");
20667 }
20668 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
20669 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
20670 if self.dspark.is_none() {
20672 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20673 if t.is_empty() {
20674 return None;
20675 }
20676 crate::dsv4::dspark_arm(&t, cfg.dim);
20677 self.dspark = Some(crate::dsv4::DsparkState::new(
20678 self.dsv4_mtp.len(),
20679 &cfg,
20680 t.len(),
20681 ));
20682 }
20683 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20684 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
20685 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
20686 eprintln!("spec_step: the pack was not built (targets {targets:?})");
20687 }
20688 let pack = pack?;
20689 let block = crate::dsv4::dspark_block();
20690 let b_box = self.dsv4.as_mut()?;
20691 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
20692 let ds = self.dspark.as_mut()?;
20693 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
20696 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
20697 if dbg {
20698 eprintln!("spec_step: no capture");
20699 }
20700 return None;
20701 }
20702 ds.have_hidden = true;
20703 let tip_pos = next_pos.checked_sub(1)?;
20704 let draft_started = std::time::Instant::now();
20705 let mut conf = Vec::new();
20706 let props = crate::dsv4::dspark_draft_gpu(
20707 g,
20708 &self.dsv4_mtp,
20709 &cfg,
20710 ds,
20711 pack,
20712 st.kv_id,
20713 tip_token,
20714 tip_pos,
20715 self.pool.as_deref(),
20716 &mut conf,
20717 );
20718 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
20719 *drafted += block;
20720 if props.is_empty() || props[0] != t_next {
20721 if dbg {
20722 eprintln!(
20723 "spec_step: draft {} (props0={:?} t_next={t_next})",
20724 if props.is_empty() {
20725 "empty"
20726 } else {
20727 "miss"
20728 },
20729 props.first()
20730 );
20731 }
20732 return None;
20733 }
20734 let mut k_verify = crate::dsv4::dspark_verify_k()
20741 .min(props.len())
20742 .min(max_extra.saturating_add(1));
20743 let conf_min = {
20749 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
20750 *M.get_or_init(|| {
20751 std::env::var("CMF_DSPARK_CONF_MIN")
20752 .ok()
20753 .and_then(|v| v.parse().ok())
20754 .unwrap_or(0.0)
20755 })
20756 };
20757 if conf_min > 0.0 && conf.len() >= props.len() {
20758 let mut keep = 1usize;
20759 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
20760 keep += 1;
20761 }
20762 k_verify = k_verify.min(keep.max(2));
20763 }
20764 if k_verify < 2 {
20765 return None;
20766 }
20767 let mut fed = Vec::with_capacity(k_verify);
20768 fed.push(t_next);
20769 fed.extend_from_slice(&props[1..k_verify]);
20770 let mut argmax = Vec::new();
20771 let mut logits_all = Vec::new();
20772 let mut walked = Vec::new();
20773 let txn = crate::dsv4::dsv4_verify_chunk(
20774 g,
20775 layers,
20776 &cfg,
20777 st,
20778 &fed,
20779 next_pos,
20780 &self.inv_freq,
20781 self.pool.as_deref(),
20782 &targets,
20783 &mut argmax,
20784 &mut logits_all,
20785 &mut walked,
20786 );
20787 if txn.is_none() && dbg {
20788 eprintln!("spec_step: verify declined");
20789 }
20790 let txn = txn?;
20791 let spec_gpu_end = txn.gpu_end;
20792 let b = fed.len();
20793 let mut accepted = 1usize;
20794 while accepted < b && fed[accepted] == argmax[accepted - 1] {
20795 accepted += 1;
20796 }
20797 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
20802 accepted = 1;
20803 }
20804 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
20805 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
20806 }
20807 let t_fin = std::time::Instant::now();
20808 if !crate::dsv4::dsv4_spec_finish(
20809 g,
20810 layers,
20811 &cfg,
20812 st,
20813 txn,
20814 accepted,
20815 &fed,
20816 &self.inv_freq,
20817 self.pool.as_deref(),
20818 ) {
20819 tracing::warn!("dsv4: the speculative rollback failed — the state is suspect");
20820 return None;
20821 }
20822 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20823 eprintln!(
20824 "finish(k={accepted}): {:.1} ms",
20825 t_fin.elapsed().as_secs_f64() * 1e3
20826 );
20827 }
20828 *accepted_ctr += accepted - 1;
20829 let (hc, dim) = (cfg.hc_mult, cfg.dim);
20834 let dev_caps: Vec<usize> = targets
20839 .iter()
20840 .copied()
20841 .filter(|&t| t < spec_gpu_end)
20842 .collect();
20843 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
20844 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
20845 return None;
20846 }
20847 for t in 0..accepted {
20848 let tip = t + 1 == accepted;
20849 for (slot, &tl) in targets.iter().enumerate() {
20850 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
20851 let lo = (di * b + t) * hc * dim;
20852 crate::dsv4::dspark_capture(
20853 &caps_all[lo..lo + hc * dim],
20854 &cfg,
20855 slot,
20856 &mut ds.main_hidden,
20857 );
20858 } else if tip
20859 && crate::dsv4::dspark_peek_slot(slot, dim, {
20860 let lo = slot * dim;
20861 &mut ds.main_hidden[lo..lo + dim]
20862 })
20863 {
20864 } else {
20869 crate::dsv4::dspark_capture(
20873 &walked[t * hc * dim..(t + 1) * hc * dim],
20874 &cfg,
20875 slot,
20876 &mut ds.main_hidden,
20877 );
20878 }
20879 }
20880 crate::dsv4::dspark_ring_append(
20881 g,
20882 &self.dsv4_mtp,
20883 &cfg,
20884 ds,
20885 next_pos + t,
20886 self.pool.as_deref(),
20887 );
20888 }
20889 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
20890 self.graph_logits = Some(row);
20891 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
20896 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
20897 crate::dsv4::pick_tally_arm();
20898 }
20899 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
20900 eprintln!(
20901 "spec_step total {:.1} ms (k={accepted})",
20902 t_all.elapsed().as_secs_f64() * 1e3
20903 );
20904 }
20905 Some((fed[1..accepted].to_vec(), next_pos + accepted))
20906 }
20907
20908 fn dspark_probe(&mut self, position: usize, token_id: u32) {
20909 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
20910 return;
20911 }
20912 let trunk_now = crate::dsv4::pick_tally_take();
20914 crate::dsv4::trunk_freq_note(&trunk_now);
20915 if !trunk_now.is_empty() {
20916 self.dspark_trunk_picks.push(trunk_now);
20917 let keep = crate::dsv4::dspark_block();
20918 if self.dspark_trunk_picks.len() > keep {
20919 self.dspark_trunk_picks.remove(0);
20920 }
20921 }
20922 for p in std::mem::take(&mut self.dspark_pending) {
20925 let Some(i) = position.checked_sub(p.0 + 1) else {
20926 continue;
20927 };
20928 let mut p = p;
20929 if i < p.1.len() {
20930 if p.2 && p.1[i] == token_id {
20931 p.3 = i + 1;
20932 } else {
20933 p.2 = false;
20934 }
20935 if i + 1 < p.1.len() {
20936 self.dspark_pending.push(p);
20937 continue;
20938 }
20939 }
20940 self.dspark_hist.push(p.3);
20941 self.dspark_real.push(token_id);
20942 }
20943 let Some(b) = &mut self.dsv4 else { return };
20944 let (g, layers, cfg) = (&b.0, &b.1, b.2);
20945 let n_layers = layers.len();
20946 if self.dspark.is_none() {
20947 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
20948 if t.is_empty() {
20949 return;
20950 }
20951 eprintln!(
20952 "DSpark: capture from layers {t:?}, block {}",
20953 crate::dsv4::dspark_block()
20954 );
20955 crate::dsv4::dspark_arm(&t, cfg.dim);
20956 self.dspark = Some(crate::dsv4::DsparkState::new(
20957 self.dsv4_mtp.len(),
20958 &cfg,
20959 t.len(),
20960 ));
20961 }
20962 let ds = self.dspark.as_mut().unwrap();
20963 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
20964 return; }
20966 let mut conf = Vec::new();
20967 crate::dsv4::pick_tally_arm();
20968 let draft_started = std::time::Instant::now();
20973 #[cfg(feature = "gpu")]
20974 let gpu_draft = crate::dsv4::dspark_gpu_on();
20975 #[cfg(not(feature = "gpu"))]
20976 let gpu_draft = false;
20977 let props = if gpu_draft {
20978 #[cfg(feature = "gpu")]
20979 {
20980 let kv_id = b.3.kv_id;
20981 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
20982 Some(pk) => crate::dsv4::dspark_draft_gpu(
20983 g,
20984 &self.dsv4_mtp,
20985 &cfg,
20986 ds,
20987 pk,
20988 kv_id,
20989 token_id,
20990 position,
20991 self.pool.as_deref(),
20992 &mut conf,
20993 ),
20994 None => Vec::new(),
20995 }
20996 }
20997 #[cfg(not(feature = "gpu"))]
20998 Vec::new()
20999 } else {
21000 crate::gpu::cpu_scope(|| {
21001 crate::dsv4::dspark_draft(
21002 g,
21003 &self.dsv4_mtp,
21004 &cfg,
21005 ds,
21006 token_id,
21007 position,
21008 self.pool.as_deref(),
21009 &mut conf,
21010 )
21011 })
21012 };
21013 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
21014 let draft_picks = crate::dsv4::pick_tally_take();
21015 crate::dsv4::dspark_freq_note(&draft_picks);
21016 crate::dsv4::pick_tally_arm();
21019 if !props.is_empty() {
21020 let (tu, tt) = {
21024 let flat: Vec<(usize, Vec<usize>)> = self
21025 .dspark_trunk_picks
21026 .iter()
21027 .flat_map(|v| v.iter().cloned())
21028 .collect();
21029 let mut per: std::collections::HashMap<usize, Vec<usize>> =
21031 std::collections::HashMap::new();
21032 for (li, picks) in flat {
21033 per.entry(li).or_default().extend(picks);
21034 }
21035 let n = per.len().max(1);
21036 let mut u = 0usize;
21037 let mut t = 0usize;
21038 for (_, v) in per {
21039 t += v.len();
21040 u += v.iter().collect::<std::collections::HashSet<_>>().len();
21041 }
21042 (u / n, t / n)
21043 };
21044 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
21045 self.dspark_exp.push((tu, tt, du, dt));
21046 self.dspark_pending.push((position, props, true, 0));
21047 }
21048 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
21049 let n = self.dspark_hist.len() as f32;
21050 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
21051 let block = crate::dsv4::dspark_block();
21052 let mut at = vec![0usize; block + 1];
21053 for &k in &self.dspark_hist {
21054 at[k] += 1;
21055 }
21056 let mut surv = Vec::with_capacity(block);
21058 for i in 1..=block {
21059 let k = at[i..].iter().sum::<usize>() as f32 / n;
21060 surv.push(format!("{k:.2}"));
21061 }
21062 let distinct = self
21063 .dspark_real
21064 .iter()
21065 .collect::<std::collections::HashSet<_>>()
21066 .len();
21067 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
21068 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
21069 });
21070 let m = self.dspark_exp.len().max(1);
21071 eprintln!(
21072 "DSpark: {} drafts, {mean:.2} of {block} accepted on average \
21073 (tokens per pass {:.2}), distribution {at:?}, survival [{}]",
21074 self.dspark_hist.len(),
21075 mean + 1.0,
21076 surv.join(" ")
21077 );
21078 eprintln!(
21079 "DSpark: {distinct} distinct tokens of {} (degeneracy), \
21080 trunk experts {}/{} per layer over {block} tokens, \
21081 draft {}/{} per block, draft {:.2} ms/block",
21082 self.dspark_real.len(),
21083 tu / m,
21084 tt / m,
21085 du / m,
21086 dt / m,
21087 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
21088 );
21089 }
21090 }
21091
21092 fn forward_layers_upto(
21093 &mut self,
21094 hidden: &[f32],
21095 position: usize,
21096 task_mask: Option<&TaskMask>,
21097 upto: Option<usize>,
21098 ) -> Vec<f32> {
21099 if let Some(plan) = self.gpu_plan.clone() {
21105 if upto.is_none() && plan.len() > 1 {
21106 let mut h = hidden.to_vec();
21107 for &(dev, from, upto_incl) in plan.iter() {
21108 h = crate::gpu::with_device(dev, || {
21109 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
21110 });
21111 }
21112 return h;
21113 }
21114 }
21115 self.forward_layers_span(hidden, position, task_mask, 0, upto)
21116 }
21117
21118 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
21123 self.set_gpu_plan_at(devices, None)
21124 }
21125
21126 pub fn set_gpu_plan_at(
21130 &mut self,
21131 devices: Option<&[usize]>,
21132 at: Option<usize>,
21133 ) -> Result<(), String> {
21134 let Some(devs) = devices.filter(|d| d.len() > 1) else {
21135 self.gpu_plan = None;
21136 return Ok(());
21137 };
21138 self.split_supported()?;
21139 let n = self.num_layers;
21140 if devs.len() > n {
21141 return Err(format!("{} devices for {n} layers", devs.len()));
21142 }
21143 if let Some(k) = at {
21144 if k == 0 || k >= n {
21145 return Err(format!("split at {k}: the model has {n} layers"));
21146 }
21147 if devs.len() == 2 {
21148 self.gpu_plan = Some(std::sync::Arc::new(vec![
21149 (devs[0], 0, k - 1),
21150 (devs[1], k, n - 1),
21151 ]));
21152 return Ok(());
21153 }
21154 return Err(format!(
21155 "an explicit split point takes exactly 2 devices, got {}",
21156 devs.len()
21157 ));
21158 }
21159 let per = n.div_ceil(devs.len());
21160 let mut plan = Vec::with_capacity(devs.len());
21161 let mut from = 0usize;
21162 for &d in devs {
21163 if from >= n {
21164 break;
21165 }
21166 let upto = (from + per - 1).min(n - 1);
21167 plan.push((d, from, upto));
21168 from = upto + 1;
21169 }
21170 self.gpu_plan = Some(std::sync::Arc::new(plan));
21171 Ok(())
21172 }
21173
21174 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
21176 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
21177 }
21178
21179 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
21185 if let Some(x) = t.as_f32() {
21186 return x.to_vec();
21187 }
21188 let mut out = vec![0.0; t.rows() * t.cols()];
21189 for r in 0..t.rows() {
21190 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
21191 }
21192 out
21193 }
21194
21195 fn embryo_resident_eligible(&self) -> bool {
21196 fn served(t: &QTensor) -> bool {
21197 t.as_f32().is_some() || t.q8_2f_codes().is_some()
21198 }
21199 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
21202 || self.num_layers != self.physical_layers
21203 || self.loop_final_norm
21204 || self.weights.layers.len() != self.num_layers
21205 || self.head_clusters.is_none()
21206 || self.final_softcap.is_some()
21207 || self.logit_multiplier.is_some()
21208 || self.attn_softcap != 0.0
21209 || self.mtp.is_some()
21210 || self.per_token_stack()
21211 || self.dsv4.is_some()
21212 || self.dsv41.is_some()
21213 || self.dory.is_some()
21214 || self.qwen4_exp.is_some()
21215 || self.dyn_router.is_some()
21222 || self.dyn_phi_layer.is_some()
21223 || self.dyn_blend_loaded
21224 || self.o1_cfg.is_some()
21225 || self.swa.is_some()
21226 || self.sliding_layers.is_some()
21227 || self.global_attn.is_some()
21228 || self.attention_heads_per_layer.is_some()
21229 || self.attn_v_norm
21230 || self
21231 .kv_cache
21232 .layers
21233 .iter()
21234 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
21235 || self.rope_scale != 1.0
21236 || self.rope_scale_local != 1.0
21237 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
21238 || self.hidden_size == 0
21239 || self.hidden_size > 1024
21240 || self.intermediate_size > 1024
21241 || self.num_heads == 0
21242 || self.num_kv_heads == 0
21243 || self.num_heads % self.num_kv_heads != 0
21244 || self.num_heads.saturating_mul(self.head_dim) > 1024
21245 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
21246 || self.vocab_size == 0
21247 || self.kv_cache.max_seq_len == 0
21248 || self.rotary_dim == 0
21249 || self.rotary_dim > self.head_dim
21250 || self.rotary_dim % 2 != 0
21251 || self.inv_freq.len() < self.rotary_dim / 2
21252 {
21253 return false;
21254 }
21255 if self.weights.lm_head.q8_2f_codes().is_some() && !self.embryo_wide_candidate(true) {
21264 return false;
21265 }
21266 if !served(&self.weights.lm_head)
21267 || !served(&self.weights.embed_tokens)
21268 || self.weights.lm_head.rows() < self.vocab_size
21269 || self.weights.lm_head.cols() != self.hidden_size
21270 || self.weights.embed_tokens.rows() < self.vocab_size
21271 || self.weights.embed_tokens.cols() != self.hidden_size
21272 || self.weights.final_norm.len() != self.hidden_size
21273 {
21274 return false;
21275 }
21276 if let Some(cfg) = self.vmf_cfg {
21277 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
21278 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
21279 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
21280 || cfg.state_len() == 0
21281 {
21282 return false;
21283 }
21284 }
21285 if let Some(g) = self.gdn_cfg {
21286 if g.num_v_heads == 0
21291 || g.num_k_heads == 0
21292 || g.num_v_heads % g.num_k_heads != 0
21293 || g.key_head_dim == 0
21294 || g.key_head_dim > 128
21295 || g.value_head_dim == 0
21296 || g.value_head_dim > 256
21297 || g.value_head_dim % 4 != 0
21298 || g.conv_kernel == 0
21299 || g.num_v_heads > 512
21300 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
21301 || g.conv_dim() > 2048
21302 || g.conv_dim() % 4 != 0
21303 || g.hidden_size != self.hidden_size
21304 || g.output_gate_sigmoid
21305 || g.rms_eps != self.rms_eps
21306 || g.state_len() == 0
21307 {
21308 return false;
21309 }
21310 }
21311 let mut full_seen = false;
21312 for lw in &self.weights.layers {
21313 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
21314 return false;
21315 }
21316 match &lw.attn {
21317 AttnKind::LinearGdn(w) => {
21318 let Some(g) = self.gdn_cfg else {
21319 return false;
21320 };
21321 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
21322 if w.in_proj_qkv.rows() != g.conv_dim()
21323 || w.in_proj_qkv.cols() != self.hidden_size
21324 || !served(&w.in_proj_qkv)
21325 || w.in_proj_z.rows() != nv * dv
21326 || w.in_proj_z.cols() != self.hidden_size
21327 || !served(&w.in_proj_z)
21328 || w.in_proj_a.rows() != nv
21329 || w.in_proj_a.cols() != self.hidden_size
21330 || !served(&w.in_proj_a)
21331 || w.in_proj_b.rows() != nv
21332 || w.in_proj_b.cols() != self.hidden_size
21333 || !served(&w.in_proj_b)
21334 || w.conv1d.len() != g.conv_dim() * kk
21335 || w.a_log.len() != nv
21336 || w.dt_bias.len() != nv
21337 || w.norm.len() != dv
21338 || w.out_proj.rows() != self.hidden_size
21339 || w.out_proj.cols() != nv * dv
21340 || !served(&w.out_proj)
21341 {
21342 return false;
21343 }
21344 }
21345 AttnKind::Linear(w) => {
21346 let Some(cfg) = self.vmf_cfg else {
21347 return false;
21348 };
21349 if w.thq.rows() != cfg.num_heads * cfg.nphase
21350 || w.thq.cols() != self.hidden_size
21351 || !served(&w.thq)
21352 || w.thk.rows() != cfg.num_heads * cfg.nphase
21353 || w.thk.cols() != self.hidden_size
21354 || !served(&w.thk)
21355 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
21356 || w.v_proj.cols() != self.hidden_size
21357 || !served(&w.v_proj)
21358 || w.out_proj.rows() != self.hidden_size
21359 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
21360 || !served(&w.out_proj)
21361 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
21362 {
21363 return false;
21364 }
21365 if let Some((kg, kb)) = &w.k_gate {
21366 if kg.rows() != cfg.num_heads
21367 || kg.cols() != self.hidden_size
21368 || !served(&kg)
21369 || kb.len() != cfg.num_heads
21370 {
21371 return false;
21372 }
21373 }
21374 if let Some(conv) = &w.conv {
21375 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
21376 return false;
21377 }
21378 }
21379 }
21380 AttnKind::Full {
21381 wq,
21382 wk,
21383 wv,
21384 wo,
21385 q_norm,
21386 k_norm,
21387 output_gate,
21388 softplus_gate,
21389 bias,
21390 } => {
21391 if full_seen
21392 || q_norm.is_some()
21393 || k_norm.is_some()
21394 || *output_gate
21395 || softplus_gate.is_some()
21396 || bias.is_some()
21397 || !served(&wq)
21398 || !served(&wk)
21399 || !served(&wv)
21400 || !served(&wo)
21401 || wq.rows() != self.num_heads * self.head_dim
21402 || wk.rows() != self.num_kv_heads * self.head_dim
21403 || wv.rows() != self.num_kv_heads * self.head_dim
21404 || wq.cols() != self.hidden_size
21405 || wk.cols() != self.hidden_size
21406 || wv.cols() != self.hidden_size
21407 || wo.rows() != self.hidden_size
21408 || wo.cols() != self.num_heads * self.head_dim
21409 {
21410 return false;
21411 }
21412 full_seen = true;
21413 }
21414 AttnKind::Bounded(w) => {
21415 let Some(ac) = self.anchor_core.as_ref() else {
21419 return false;
21420 };
21421 if self.bounded_rope.is_none()
21422 || w.window != ac.window
21423 || w.sink != ac.sink
21424 || w.group_windows != ac.group_windows
21425 || w.window == 0
21426 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
21427 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
21428 || !served(&w.wq)
21429 || !served(&w.wk)
21430 || !served(&w.wv)
21431 || !served(&w.wo)
21432 || w.wq.rows() != self.num_heads * self.head_dim
21433 || w.wk.rows() != self.num_kv_heads * self.head_dim
21434 || w.wv.rows() != self.num_kv_heads * self.head_dim
21435 || w.wq.cols() != self.hidden_size
21436 || w.wk.cols() != self.hidden_size
21437 || w.wv.cols() != self.hidden_size
21438 || w.wo.rows() != self.hidden_size
21439 || w.wo.cols() != self.num_heads * self.head_dim
21440 {
21441 return false;
21442 }
21443 }
21444 _ => return false,
21445 }
21446 match &lw.ffn {
21447 FfnKind::Dense(d) => {
21448 if d.act != Act::Silu
21449 || !d.segs.is_empty()
21450 || !served(&d.gate_proj)
21451 || !served(&d.up_proj)
21452 || !served(&d.down_proj)
21453 || d.gate_proj.rows() != self.intermediate_size
21454 || d.gate_proj.cols() != self.hidden_size
21455 || d.up_proj.rows() != self.intermediate_size
21456 || d.up_proj.cols() != self.hidden_size
21457 || d.down_proj.rows() != self.hidden_size
21458 || d.down_proj.cols() != self.intermediate_size
21459 {
21460 return false;
21461 }
21462 }
21463 FfnKind::Moe(m) => {
21464 if m.resonance.is_none()
21465 || m.top_k != 1
21466 || m.router_sigmoid
21467 || !m.norm_topk_prob
21468 || m.expert_bias.is_some()
21469 || m.routed_scaling != 1.0
21470 || m.route_tau.is_some()
21471 || m.shared.is_none()
21472 || m.mask.is_some()
21473 || m.per_expert_scale.is_some()
21474 || m.router_input_norm
21475 || m.experts.is_empty()
21476 || m.experts.len() > 8
21477 {
21478 return false;
21479 }
21480 let r = m.resonance.as_ref().unwrap();
21481 if r.mu.len() != m.experts.len() * self.hidden_size
21482 || r.bias.len() != m.experts.len()
21483 || r.u.len() != m.experts.len() * r.k * self.hidden_size
21484 || r.k > 128
21485 {
21486 return false;
21487 }
21488 let Some((shared, gate)) = &m.shared else {
21489 return false;
21490 };
21491 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
21492 return false;
21493 }
21494 if !served(&shared.gate_proj)
21495 || !served(&shared.up_proj)
21496 || !served(&shared.down_proj)
21497 || shared.gate_proj.rows() != self.intermediate_size
21498 || shared.gate_proj.cols() != self.hidden_size
21499 || shared.up_proj.rows() != self.intermediate_size
21500 || shared.up_proj.cols() != self.hidden_size
21501 || shared.down_proj.rows() != self.hidden_size
21502 || shared.down_proj.cols() != self.intermediate_size
21503 {
21504 return false;
21505 }
21506 for e in &m.experts {
21507 if e.act != Act::Silu
21508 || !e.segs.is_empty()
21509 || !served(&e.gate_proj)
21510 || !served(&e.up_proj)
21511 || !served(&e.down_proj)
21512 || e.gate_proj.rows() != self.intermediate_size
21513 || e.gate_proj.cols() != self.hidden_size
21514 || e.up_proj.rows() != self.intermediate_size
21515 || e.up_proj.cols() != self.hidden_size
21516 || e.down_proj.rows() != self.hidden_size
21517 || e.down_proj.cols() != self.intermediate_size
21518 {
21519 return false;
21520 }
21521 }
21522 }
21523 FfnKind::DenseMoe(_) => return false,
21524 }
21525 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
21526 return false;
21527 }
21528 }
21529 if full_seen && self.anchor_core.is_some() {
21530 return false;
21531 }
21532 full_seen || self.num_layers > 0
21533 }
21534
21535 fn embryo_resident_wanted(&self) -> bool {
21540 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
21541 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
21542 && matches!(
21543 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
21544 Ok("1") | Ok("parallel")
21545 )
21546 && crate::gpu::enabled_here()
21547 && !self.graph_refused()
21548 && self.embryo_resident_eligible()
21549 }
21550
21551 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
21559 if ids.len() < 2
21560 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
21561 || !self.embryo_resident_wanted()
21562 {
21563 return None;
21564 }
21565 let model = self.ensure_embryo_graph()?;
21566 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
21567 .ok()
21568 .and_then(|v| v.parse::<usize>().ok())
21569 .filter(|&v| v >= 1)
21570 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
21571 .min(crate::gpu::EMBRYO_CHUNK_MAX);
21572 let hs = self.hidden_size;
21573 let n = ids.len();
21574 let mut pos = start;
21575 let mut last = None;
21576 let mut rows = Vec::with_capacity(cmax * hs);
21577 while pos < n {
21578 let end = (pos + cmax).min(n);
21579 rows.clear();
21580 for &id in &ids[pos..end] {
21581 rows.extend_from_slice(&self.embed_single(id));
21582 }
21583 let mut lg = Vec::new();
21584 if !crate::gpu::forward_embryo_graph_chunk(
21585 &model,
21586 self.graph_kv_id,
21587 &rows,
21588 pos,
21589 end - pos,
21590 &mut lg,
21591 ) {
21592 if pos == start {
21593 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
21594 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
21595 }
21596 return None;
21597 }
21598 self.kv_cache.clear();
21602 self.clear_history();
21603 crate::gpu::graph_kv_reset(self.graph_kv_id);
21604 panic!(
21605 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
21606 );
21607 }
21608 last = Some(lg);
21609 pos = end;
21610 }
21611 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
21612 eprintln!(
21613 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
21614 n - start,
21615 (n - start).div_ceil(cmax)
21616 );
21617 }
21618 last
21619 }
21620
21621 fn embryo_wide_candidate(&self, q8: bool) -> bool {
21624 if self.embryo_wide_refused
21625 || std::env::var("CMF_EMBRYO_WIDE").as_deref() == Ok("0")
21626 || !crate::gpu::embryo_wide_ready(q8)
21627 {
21628 return false;
21629 }
21630 let Some(g) = self.gdn_cfg else {
21631 return false;
21632 };
21633 let (hidden, inter, hd) = (self.hidden_size, self.intermediate_size, self.head_dim);
21634 let (nv, nk, dk, dv) = (g.num_v_heads, g.num_k_heads, g.key_head_dim, g.value_head_dim);
21635 let (q, kv) = (self.num_heads, self.num_kv_heads);
21636 let step = if q8 { 16 } else { 8 };
21638 let geometry = hidden % step == 0
21639 && hidden <= 1024
21640 && inter % step == 0
21641 && inter <= 1024
21642 && self.vocab_size % 4 == 0
21643 && self.head_clusters.as_ref().is_some_and(|c| {
21644 let nc = c.len() / hidden.max(1);
21645 nc > 0 && nc <= 256 && self.vocab_size % nc == 0 && self.vocab_size / nc <= 256
21647 })
21648 && dk % 4 == 0
21649 && dv % 4 == 0
21650 && (nv * dv) % step == 0
21651 && nv * dv <= 1024
21652 && nv <= 508
21653 && nk > 0
21654 && nv % nk == 0
21655 && g.conv_dim() % 4 == 0
21656 && g.conv_dim() <= 2048
21657 && hd % 4 == 0
21658 && (q * hd) % step == 0
21659 && q * hd <= 1024
21660 && kv > 0
21661 && q % kv == 0
21662 && (kv * hd) % 4 == 0
21663 && (self.rotary_dim / 2) % 4 == 0;
21664 if !geometry {
21665 return false;
21666 }
21667 self.weights.layers.iter().all(|lw| {
21668 matches!(lw.attn, AttnKind::Bounded(_) | AttnKind::LinearGdn(_))
21669 && match &lw.ffn {
21670 FfnKind::Moe(m) => m.resonance.as_ref().is_some_and(|r| m.experts.len() * (r.k + 1) <= 256) && m.shared.is_some(),
21671 FfnKind::Dense(_) => true,
21672 FfnKind::DenseMoe(_) => false,
21673 }
21674 })
21675 }
21676
21677 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
21678 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
21679 const UMAX: u32 = u32::MAX;
21680 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
21681 const REC: usize = 64;
21682 struct Pack {
21683 data: Vec<f32>,
21684 wide: bool,
21686 q8: bool,
21687 mats: Vec<u8>,
21688 table: Vec<u32>,
21691 table_base: u32,
21692 refused: bool,
21693 }
21694 impl Pack {
21695 fn put(&mut self, x: &[f32]) -> u32 {
21696 if x.is_empty() {
21697 return u32::MAX;
21698 }
21699 if self.wide {
21701 self.data.resize(self.data.len().next_multiple_of(4), 0.0);
21702 }
21703 let off = self.data.len();
21704 self.data.extend_from_slice(x);
21705 off as u32
21706 }
21707 fn mat(&mut self, t: &QTensor) -> u32 {
21709 if !self.wide {
21710 return self.put(&Pipeline::embryo_qtensor_f32(t));
21711 }
21712 if self.q8 {
21713 let Some((q, rows, cols)) = t.q8_2f_codes() else {
21714 self.refused = true;
21715 return 0;
21716 };
21717 self.mats.resize(self.mats.len().next_multiple_of(16), 0);
21718 let off = self.mats.len() as u32;
21719 self.mats.extend_from_slice(&q[..t.rows() * t.cols()]);
21720 let (rs, cs) = (self.put(rows), self.put(cols));
21721 let e = self.table_base + self.table.len() as u32;
21722 self.table.extend_from_slice(&[off, rs, cs]);
21723 return e;
21724 }
21725 self.mats.resize(self.mats.len().next_multiple_of(16), 0);
21726 let off = self.mats.len() / 2;
21727 for v in Pipeline::embryo_qtensor_f32(t) {
21728 let h = cortiq_core::quant::f32_to_f16(v);
21729 self.refused |= cortiq_core::quant::f16_to_f32(h).to_bits() != v.to_bits();
21730 self.mats.extend_from_slice(&h.to_le_bytes());
21731 }
21732 off as u32
21733 }
21734 }
21735 let vmf = self.vmf_cfg;
21739 let gdn = self.gdn_cfg;
21740 let q8 = self.weights.lm_head.q8_2f_codes().is_some();
21742 let wide = self.embryo_wide_candidate(q8);
21743 let mut pack = Pack {
21744 data: Vec::new(),
21745 wide,
21746 q8,
21747 mats: Vec::new(),
21748 table: Vec::new(),
21749 table_base: (HEADER + self.num_layers * REC) as u32,
21750 refused: false,
21751 };
21752 let mut meta = vec![0u32; HEADER];
21753 meta[0] = self.hidden_size as u32;
21754 meta[1] = self.intermediate_size as u32;
21755 meta[2] = self.vocab_size as u32;
21756 meta[3] = self.num_layers as u32;
21757 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
21758 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
21759 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
21760 if let Some(g) = gdn {
21761 meta[24] = g.num_v_heads as u32;
21762 meta[25] = g.num_k_heads as u32;
21763 meta[26] = g.key_head_dim as u32;
21764 meta[27] = g.value_head_dim as u32;
21765 meta[28] = g.conv_kernel as u32;
21766 meta[29] = g.conv_dim() as u32;
21767 }
21768 meta[7] = self.num_heads as u32;
21769 meta[8] = self.num_kv_heads as u32;
21770 meta[9] = self.head_dim as u32;
21771 meta[10] = self.kv_cache.max_seq_len as u32;
21772 let clusters = self.head_clusters.as_ref().unwrap();
21773 let cluster_count = clusters.len() / self.hidden_size;
21774 if clusters.len() % self.hidden_size != 0
21775 || cluster_count == 0
21776 || cluster_count > 1024
21777 || self.vocab_size % cluster_count != 0
21778 || self.weights.lm_head.rows() < self.vocab_size
21779 || self.weights.final_norm.len() != self.hidden_size
21780 {
21781 return None;
21782 }
21783 meta[11] = cluster_count as u32;
21784 meta[12] = (self.vocab_size / cluster_count) as u32;
21785 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
21786 meta[16] = self.rotary_dim as u32;
21787 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
21788 meta[19] = (self.rms_eps as f32).to_bits();
21789 let max_conv = self
21790 .weights
21791 .layers
21792 .iter()
21793 .filter_map(|lw| match &lw.attn {
21794 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
21795 _ => None,
21796 })
21797 .max()
21798 .unwrap_or(1);
21799 let phase_stride = vmf
21805 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
21806 .unwrap_or(0);
21807 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
21808 let state_stride = phase_stride.max(gdn_stride);
21809 let bounded = self.anchor_core.clone();
21813 let (anchor_window, anchor_sink) = bounded
21814 .as_ref()
21815 .map(|ac| (ac.window, ac.sink))
21816 .unwrap_or((0, 0));
21817 let kv_stride = if bounded.is_some() {
21818 2usize
21819 .saturating_mul(self.num_kv_heads)
21820 .saturating_mul(anchor_window)
21821 .saturating_mul(self.head_dim)
21822 } else {
21823 2usize
21824 .saturating_mul(self.num_kv_heads)
21825 .saturating_mul(self.kv_cache.max_seq_len)
21826 .saturating_mul(self.head_dim)
21827 };
21828 meta[14] = state_stride as u32;
21829 meta[15] = kv_stride as u32;
21830 meta[18] = anchor_window as u32;
21831 meta[20] = anchor_sink as u32;
21832 pack.refused |= pack.wide
21834 && 3 * self.num_heads * (anchor_window + anchor_sink + 3) + 256 > 65_536;
21835 meta[21] = match &self.bounded_rope {
21836 _ if pack.wide => UMAX,
21838 Some(rope) => {
21839 let off = pack.put(&rope.cos);
21841 let _ = pack.put(&rope.sin);
21842 off
21843 }
21844 None => UMAX,
21845 };
21846 meta[22] = match bounded.as_ref().filter(|ac| !ac.group_windows.is_empty()) {
21849 Some(ac) => {
21850 let gw: Vec<f32> = ac.group_windows.iter().map(|&w| w as f32).collect();
21851 pack.put(&gw)
21852 }
21853 None => UMAX,
21854 };
21855 let mut full_seen = false;
21856 let mut bounded_seen = 0usize;
21857 let mut phase_seen = 0usize;
21861 let mut gdn_seen = 0usize;
21862 for (li, lw) in self.weights.layers.iter().enumerate() {
21863 let base = meta.len();
21864 meta.resize(base + REC, UMAX);
21865 meta[base] = match &lw.attn {
21866 AttnKind::Linear(w) if w.phase_delta => 1,
21867 AttnKind::Linear(_) => 0,
21868 AttnKind::Full { .. } => 2,
21869 AttnKind::Bounded(_) => 3,
21870 AttnKind::LinearGdn(_) => 4,
21871 _ => UMAX,
21872 };
21873 meta[base + 1] = pack.put(&lw.input_norm);
21874 meta[base + 2] = pack.put(&lw.post_norm);
21875 meta[base + 25] = match &lw.attn {
21876 AttnKind::Linear(_) => {
21877 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
21878 phase_seen += 1;
21879 off
21880 }
21881 AttnKind::LinearGdn(_) => {
21882 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
21883 gdn_seen += 1;
21884 off
21885 }
21886 _ => UMAX,
21887 };
21888 match &lw.attn {
21889 AttnKind::LinearGdn(w) => {
21890 meta[base + 56] = pack.mat(&w.in_proj_qkv);
21893 meta[base + 57] = pack.mat(&w.in_proj_z);
21894 meta[base + 58] = pack.mat(&w.in_proj_a);
21895 meta[base + 59] = pack.mat(&w.in_proj_b);
21896 meta[base + 60] = pack.put(&w.conv1d);
21897 meta[base + 61] = pack.put(&w.a_log);
21898 meta[base + 62] = pack.put(&w.dt_bias);
21899 meta[base + 63] = pack.put(&w.norm);
21900 meta[base + 29] = pack.mat(&w.out_proj);
21901 meta[base + 24] = 0;
21902 }
21903 AttnKind::Linear(w) => {
21904 meta[base + 3] = pack.mat(&w.thq);
21905 meta[base + 4] = pack.mat(&w.thk);
21906 meta[base + 5] = pack.mat(&w.v_proj);
21907 meta[base + 6] = pack.mat(&w.out_proj);
21908 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
21909 meta[base + 7] = pack.put(&decay);
21910 if let Some((kg, kb)) = &w.k_gate {
21911 meta[base + 8] = pack.mat(&kg);
21912 meta[base + 9] = pack.put(kb);
21913 }
21914 if let Some(conv) = &w.conv {
21915 meta[base + 10] = pack.put(conv);
21916 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
21917 } else {
21918 meta[base + 24] = 0;
21919 }
21920 }
21921 AttnKind::Full { wq, wk, wv, wo, .. } => {
21922 full_seen = true;
21923 meta[base + 11] = pack.mat(&wq);
21924 meta[base + 12] = pack.mat(&wk);
21925 meta[base + 13] = pack.mat(&wv);
21926 meta[base + 14] = pack.mat(&wo);
21927 meta[base + 26] = (li * kv_stride) as u32;
21928 }
21929 AttnKind::Bounded(w) => {
21930 meta[base + 11] = pack.mat(&w.wq);
21931 meta[base + 12] = pack.mat(&w.wk);
21932 meta[base + 13] = pack.mat(&w.wv);
21933 meta[base + 14] = pack.mat(&w.wo);
21934 meta[base + 26] = (bounded_seen * kv_stride) as u32;
21936 meta[base + 27] = pack.put(&w.sink_k);
21937 meta[base + 28] = pack.put(&w.sink_v);
21938 bounded_seen += 1;
21939 }
21940 _ => return None,
21941 }
21942 match &lw.ffn {
21943 FfnKind::Dense(d) => {
21944 meta[base + 15] = 0;
21945 meta[base + 16] = 0;
21946 meta[base + 21] = pack.mat(&d.gate_proj);
21947 meta[base + 22] = pack.mat(&d.up_proj);
21948 meta[base + 23] = pack.mat(&d.down_proj);
21949 }
21950 FfnKind::Moe(m) => {
21951 let r = m.resonance.as_ref().unwrap();
21952 let (shared, _) = m.shared.as_ref().unwrap();
21953 meta[base + 15] = 1;
21954 meta[base + 16] = m.experts.len() as u32;
21955 meta[base + 17] = pack.put(&r.mu);
21956 meta[base + 18] = pack.put(&r.u);
21957 meta[base + 19] = pack.put(&r.bias);
21958 meta[base + 20] = r.k as u32;
21959 let mut shell = r.effective_shell(m.experts.len());
21968 shell.push(f32::NEG_INFINITY);
21969 meta[base + 30] = pack.put(&shell);
21970 meta[base + 21] = pack.mat(&shared.gate_proj);
21971 meta[base + 22] = pack.mat(&shared.up_proj);
21972 meta[base + 23] = pack.mat(&shared.down_proj);
21973 for (e, ex) in m.experts.iter().enumerate() {
21974 meta[base + 32 + e * 3] =
21975 pack.mat(&ex.gate_proj);
21976 meta[base + 33 + e * 3] =
21977 pack.mat(&ex.up_proj);
21978 meta[base + 34 + e * 3] =
21979 pack.mat(&ex.down_proj);
21980 }
21981 }
21982 FfnKind::DenseMoe(_) => return None,
21983 }
21984 }
21985 if !full_seen && self.num_layers == 0 {
21986 return None;
21987 }
21988 let mut lm_head = Vec::new();
21989 let mut lm_head_packed = Vec::new();
21990 if wide && q8 {
21991 let parts = self.weights.lm_head.q8_2f_codes();
21992 if let Some((q, rows, cols)) = parts {
21993 lm_head_packed = q[..self.weights.lm_head.rows() * self.weights.lm_head.cols()].to_vec();
21994 let (rs, cs) = (pack.put(rows), pack.put(cols));
21995 meta[30] = pack.table_base + pack.table.len() as u32;
21996 pack.table.extend_from_slice(&[0, rs, cs]);
21997 }
21998 meta.extend_from_slice(&pack.table);
21999 } else if wide {
22000 for v in Self::embryo_qtensor_f32(&self.weights.lm_head) {
22001 let h = cortiq_core::quant::f32_to_f16(v);
22002 pack.refused |= cortiq_core::quant::f16_to_f32(h).to_bits() != v.to_bits();
22003 lm_head_packed.extend_from_slice(&h.to_le_bytes());
22004 }
22005 } else {
22006 lm_head = Self::embryo_qtensor_f32(&self.weights.lm_head);
22007 }
22008 if pack.refused {
22009 self.embryo_wide_refused = true;
22010 return self.ensure_embryo_graph();
22011 }
22012 let id = {
22013 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
22014 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
22015 };
22016 let model = crate::gpu::EmbryoGraphModel {
22017 id,
22018 hidden: self.hidden_size,
22019 intermediate: self.intermediate_size,
22020 vocab: self.vocab_size,
22021 layers: self.num_layers,
22022 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
22023 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
22024 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
22025 anchor_q_heads: self.num_heads,
22026 anchor_kv_heads: self.num_kv_heads,
22027 anchor_head_dim: self.head_dim,
22028 rotary_dim: self.rotary_dim,
22029 rope_freq: if wide { self.inv_freq[..self.rotary_dim / 2].to_vec() } else { Vec::new() },
22030 max_seq: self.kv_cache.max_seq_len,
22031 cluster_count,
22032 cluster_size: self.vocab_size / cluster_count,
22033 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
22034 state_stride,
22035 kv_stride,
22036 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
22037 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
22038 weights: pack.data,
22039 meta,
22040 lm_head,
22041 wide,
22042 wide_q8: wide && q8,
22043 mats: pack.mats,
22044 lm_head_packed,
22045 clusters: clusters.as_ref().clone(),
22046 final_norm: self.weights.final_norm.clone(),
22047 inv_freq: self.inv_freq.as_ref().clone(),
22048 bounded: bounded.is_some(),
22049 kv_layers: if bounded.is_some() {
22050 bounded_seen
22051 } else {
22052 self.num_layers
22053 },
22054 state_layers: phase_seen + gdn_seen,
22055 anchor_window,
22056 anchor_sink,
22057 phase_layers: phase_seen,
22058 gdn_layers: gdn_seen,
22059 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
22060 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
22061 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
22062 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
22063 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
22064 };
22065 self.embryo_graph = Some(std::sync::Arc::new(model));
22066 }
22067 self.embryo_graph.clone()
22068 }
22069
22070 fn forward_layers_span(
22071 &mut self,
22072 hidden: &[f32],
22073 position: usize,
22074 task_mask: Option<&TaskMask>,
22075 from: usize,
22076 upto: Option<usize>,
22077 ) -> Vec<f32> {
22078 #[cfg(target_os = "macos")]
22079 let _metal_pager_token_prof = crate::gpu_metal::MoePagerTokenScope::new(
22080 position,
22081 from,
22082 upto.map_or(self.num_layers, |u| u.saturating_add(1)),
22083 );
22084 debug_assert!(
22085 from == 0
22086 || (self.dsv4.is_none()
22087 && self.dsv41.is_none()
22088 && self.qwen4_exp.is_none()
22089 && !self.per_token_stack())
22090 );
22091 #[cfg(target_os = "macos")]
22097 if !crate::gpu_metal::wait_replay() {
22098 self.fail_metal_graph("the pending async replay failed before a plain forward");
22099 return vec![0.0; self.hidden_size];
22100 }
22101 if let Some(b) = &mut self.qwen4_exp {
22102 let _ = (task_mask, upto);
22103 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22104 let mut logits = Vec::new();
22105 crate::qwen4_exp::forward_token(
22106 &b.0,
22107 &b.1,
22108 &b.2,
22109 &mut b.3,
22110 token_id,
22111 position,
22112 &self.inv_freq,
22113 self.pool.as_deref(),
22114 &mut logits,
22115 true,
22116 );
22117 self.graph_logits = Some(logits);
22118 return vec![0.0; self.hidden_size];
22119 }
22120 if let Some(b) = &mut self.dsv4 {
22126 let _ = (task_mask, upto);
22127 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22128 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
22129 st.pos = position;
22130 let mut logits = Vec::new();
22131 crate::dsv4::forward_token(
22132 g,
22133 layers,
22134 &cfg,
22135 st,
22136 token_id,
22137 &self.inv_freq,
22138 self.pool.as_deref(),
22139 &mut logits,
22140 );
22141 self.graph_logits = Some(logits);
22142 self.dspark_probe(position, token_id);
22143 return vec![0.0; self.hidden_size];
22146 }
22147 if self.dory.is_some() {
22149 let _ = (task_mask, upto);
22150 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22151 let lg = self.dory_forward(&[token_id], position, crate::dory::Want::Last);
22152 self.graph_logits = Some(lg);
22153 return vec![0.0; self.hidden_size];
22154 }
22155 if let Some(b) = &mut self.dsv41 {
22157 let _ = (task_mask, upto);
22158 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
22159 let mut logits = Vec::new();
22160 crate::dsv41::forward_token(
22161 &b.0,
22162 &b.1,
22163 &b.2,
22164 &mut b.3,
22165 token_id,
22166 position,
22167 self.pool.as_deref(),
22168 &mut logits,
22169 );
22170 self.graph_logits = Some(logits);
22171 return vec![0.0; self.hidden_size];
22172 }
22173 if let Some(b) = &self.g4e {
22179 let _ = (task_mask, upto);
22180 return crate::g4e::g4e_forward(
22181 &b.0,
22182 &b.1,
22183 hidden,
22184 position,
22185 &mut self.kv_cache.layers,
22186 self.num_heads,
22187 self.num_kv_heads,
22188 self.pool.as_deref(),
22189 );
22190 }
22191 if let Some(b) = &self.g3n {
22192 let _ = (task_mask, upto);
22193 return crate::g3n::g3n_forward(
22194 &b.0,
22195 &b.1,
22196 hidden,
22197 position,
22198 &mut self.kv_cache.layers,
22199 self.num_heads,
22200 self.num_kv_heads,
22201 self.head_dim,
22202 self.pool.as_deref(),
22203 );
22204 }
22205 if from == 0
22212 && upto.is_none()
22213 && task_mask.is_none()
22214 && self.anchor_core.is_some()
22215 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
22216 {
22217 static ONCE: std::sync::Once = std::sync::Once::new();
22218 ONCE.call_once(|| {
22219 eprintln!(
22220 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
22221 unsupported={} eligible={}",
22222 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
22223 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
22224 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
22225 crate::gpu::enabled_here(),
22226 self.graph_refused(),
22227 self.embryo_resident_eligible(),
22228 );
22229 });
22230 }
22231 if from == 0
22232 && upto.is_none()
22233 && task_mask.is_none()
22234 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
22238 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
22239 && matches!(
22243 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
22244 Ok("1") | Ok("parallel")
22245 )
22246 && crate::gpu::enabled_here()
22247 && !self.graph_refused()
22248 && (position == 0 || self.device_sequence_position().is_some())
22254 && self.embryo_resident_eligible()
22255 && let Some(model) = self.ensure_embryo_graph()
22256 {
22257 let mut lg = Vec::new();
22258 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
22259 {
22260 self.graph_logits = Some(lg);
22261 return vec![0.0; self.hidden_size];
22262 }
22263 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
22264 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
22265 }
22266 self.mark_graph_refused();
22272 if position != 0 {
22273 self.kv_cache.clear();
22277 self.clear_history();
22278 crate::gpu::graph_kv_reset(self.graph_kv_id);
22279 panic!(
22280 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
22281 );
22282 }
22283 }
22284 let mut h = hidden.to_vec();
22285 if self.value_blend.is_some() {
22289 if from == 0 {
22290 self.vres_e0.clear();
22291 self.vres_e0.extend_from_slice(hidden);
22292 }
22293 assert!(
22294 self.vres_e0.len() == hidden.len(),
22295 "value residual: a layer span past the first layer has no embedding row"
22296 );
22297 }
22298 self.mimo_moe_prepare();
22301 let _mimo_q8 = self
22302 .mimo_moe
22303 .is_on()
22304 .then(crate::qtensor::enter_full_gpu_q8_scope);
22305 let (nh, _nkv, _hd, hs, _rd, eps) = (
22308 self.num_heads,
22309 self.num_kv_heads,
22310 self.head_dim,
22311 self.hidden_size,
22312 self.rotary_dim,
22313 self.rms_eps,
22314 );
22315 let pool = self.pool.clone();
22316 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
22328 let graph_on = match graph_env.as_deref() {
22329 Some("0") => false,
22330 Some("prefill") => false, Some(_) => true,
22332 None => crate::gpu::wgpu_graph_default(),
22338 };
22339 let graph_trusted =
22340 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
22341 let race_eligible =
22342 graph_on && upto.is_none() && task_mask.is_none() && from == 0 && !self.graph_refused();
22343 let mut tail_start = 0usize;
22344 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
22345 let t_graph = std::time::Instant::now();
22346 let mut lg = Vec::new();
22347 let mut gl = 0usize;
22348 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
22349 let declined = built.is_none();
22350 let built = match built {
22351 Some(Ok(hh)) => Some(hh),
22352 Some(Err(())) => {
22353 self.clear_sequence_state();
22357 self.graph_failed
22358 .store(true, std::sync::atomic::Ordering::Relaxed);
22359 self.cancel
22360 .store(true, std::sync::atomic::Ordering::Relaxed);
22361 tracing::error!("token graph failed after admission; sequence state cleared");
22362 return vec![0.0; self.hidden_size];
22363 }
22364 None => None,
22365 };
22366 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
22371 self.mark_graph_refused();
22372 }
22373 graph_note(built.is_some(), gl, self.num_layers);
22374 if let Some(hh) = built {
22375 let dur = t_graph.elapsed();
22376 if std::env::var("CMF_GRAPH_PROF").is_ok() {
22377 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
22378 }
22379 if gl > 0 && gl < self.num_layers {
22380 h = hh;
22386 tail_start = gl;
22387 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
22388 if !graph_trusted {
22389 crate::gpu::graph_race_record(true, dur);
22390 }
22391 if !lg.is_empty() {
22392 lg.resize(self.vocab_size, 0.0);
22395 if let Some(c) = self.final_softcap {
22396 softcap_in_place(&mut lg, c, self.pool.as_deref());
22397 }
22398 self.graph_logits = Some(lg);
22399 }
22400 return hh;
22401 }
22402 }
22408 }
22409 let span = from > 0 || upto.is_some();
22433 if span && graph_on && task_mask.is_none() && graph_trusted {
22434 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
22435 let mut lg = Vec::new();
22436 let mut gl = 0usize;
22437 let span_res =
22438 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
22439 let span_res = match span_res {
22440 Some(Ok(hh)) => Some(hh),
22441 Some(Err(())) => {
22442 self.clear_sequence_state();
22443 self.graph_failed
22444 .store(true, std::sync::atomic::Ordering::Relaxed);
22445 self.cancel
22446 .store(true, std::sync::atomic::Ordering::Relaxed);
22447 tracing::error!(
22448 "span token graph failed after admission; sequence state cleared"
22449 );
22450 return vec![0.0; self.hidden_size];
22451 }
22452 None => None,
22453 };
22454 graph_note(span_res.is_some(), gl, upto_excl - from);
22455 if std::env::var("CMF_GPU_DEBUG").is_ok() {
22456 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
22460 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
22461 eprintln!(
22462 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
22463 upto_excl - from,
22464 span_res.is_some()
22465 );
22466 }
22467 }
22468 if let Some(hh) = span_res {
22469 if gl == upto_excl - from {
22470 if !lg.is_empty() {
22471 lg.resize(self.vocab_size, 0.0);
22472 if let Some(c) = self.final_softcap {
22473 softcap_in_place(&mut lg, c, self.pool.as_deref());
22474 }
22475 self.graph_logits = Some(lg);
22476 }
22477 crate::gpu::set_layer(-1);
22478 return hh;
22479 }
22480 h = hh;
22482 tail_start = from + gl;
22483 }
22484 }
22485 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
22490
22491 let host_tail = tail_start > from;
22501 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
22502 let automatic_gpu_prefix = self.automatic_gpu_prefix();
22503
22504 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
22505 #[cfg(target_os = "macos")]
22506 let mut gpu_skip_until = 0usize;
22507 for li in tail_start.max(from)..self.num_layers {
22508 let _capacity_tail = automatic_gpu_prefix
22509 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
22510 .map(|_| crate::gpu::enter_cpu_scope());
22511 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
22513 if li > u {
22514 break;
22515 }
22516 }
22517 if let Some(mask) = task_mask {
22518 if !mask.layer_alive(li) {
22519 continue; }
22521 }
22522 if host_tail && task_mask.is_none() {
22527 match self.m3_pooled_layer(li, position, &mut h) {
22528 Ok(true) => {
22529 crate::gpu::set_layer(-1);
22530 continue;
22531 }
22532 Ok(false) => {}
22533 Err(()) => {
22534 self.clear_sequence_state();
22535 self.graph_failed
22536 .store(true, std::sync::atomic::Ordering::Relaxed);
22537 self.cancel
22538 .store(true, std::sync::atomic::Ordering::Relaxed);
22539 tracing::error!(
22540 "pooled tail token graph failed after admission; sequence state cleared"
22541 );
22542 return vec![0.0; self.hidden_size];
22543 }
22544 }
22545 }
22546 #[cfg(target_os = "macos")]
22550 {
22551 if li < gpu_skip_until {
22552 continue;
22553 }
22554 if task_mask.is_none() {
22555 let end = self.q1_graph_gpu(li, upto, position, &mut h);
22556 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
22557 return vec![0.0; self.hidden_size];
22561 }
22562 if end > li {
22563 gpu_skip_until = end;
22564 if self.is_loop_end(end - 1) && end < self.num_layers {
22567 h = inference::rms_norm(
22568 &h,
22569 &self.weights.final_norm,
22570 self.rms_eps,
22571 self.norm_style,
22572 );
22573 }
22574 continue;
22575 }
22576 }
22577 }
22578
22579 if task_mask.is_none() {
22580 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
22581 crate::gpu::BatchGraphOutcome::Completed => continue,
22582 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
22583 crate::gpu::BatchGraphOutcome::Declined => {}
22584 }
22585 }
22586 #[cfg(feature = "gpu")]
22587 self.pull_lagging_host_kv(li, li + 1, position);
22588 let mhc_attn = self.mhc_layer(li).then(|| self.mhc_pre(li, false, &mut h));
22591 let lw = &self.weights.layers[self.phys_layer(li)];
22592 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
22593 if tp.parse::<usize>().ok() == Some(position) {
22594 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
22595 eprintln!(
22596 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
22597 h[0], h[1]
22598 );
22599 }
22600 }
22601 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
22604 inference::rms_norm_into(
22605 &h,
22606 &lw.input_norm,
22607 self.rms_eps,
22608 self.norm_style,
22609 &mut self.ws.n1,
22610 );
22611 drop(prof);
22612 let vres = match self.layer_value_blend(li) {
22614 Some(blend) => {
22615 vres_lerp(&self.vres_e0, &self.ws.n1, blend, &mut self.vres_in);
22616 true
22617 }
22618 None => false,
22619 };
22620
22621 let attn_out = match &lw.attn {
22622 AttnKind::Mla(w) => {
22623 let inv_freq_l = self.layer_inv_freq(li);
22624 let rs = self.layer_rope_scale(li);
22625 let eps = self.rms_eps;
22626 let pool = self.pool.clone();
22627 mla_attention(
22628 w,
22629 &self.ws.n1,
22630 &mut self.kv_cache.layers[li],
22631 position,
22632 &inv_freq_l,
22633 rs,
22634 eps,
22635 pool.as_deref(),
22636 )
22637 }
22638 AttnKind::Linear(w) => {
22639 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
22640 vmf_phase_forward(
22641 &self.ws.n1,
22642 w,
22643 &cfg,
22644 &mut self.kv_cache.layers[li].linear_state,
22645 self.pool.as_deref(),
22646 )
22647 }
22648 AttnKind::Kda(w) => {
22649 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
22650 crate::linear_core::kda_forward(
22651 &self.ws.n1,
22652 w,
22653 &cfg,
22654 &mut self.kv_cache.layers[li].linear_state,
22655 self.pool.as_deref(),
22656 )
22657 }
22658 AttnKind::LinearGdn(w) => {
22659 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
22660 gdn_forward(
22661 &self.ws.n1,
22662 w,
22663 &cfg,
22664 &mut self.kv_cache.layers[li].linear_state,
22665 self.pool.as_deref(),
22666 )
22667 }
22668 AttnKind::ShortConv(w) => {
22669 let cfg = self
22670 .short_conv_cfg
22671 .expect("short-conv layer without short_conv_cfg");
22672 short_conv_forward(
22673 &self.ws.n1,
22674 w,
22675 &cfg,
22676 &mut self.kv_cache.layers[li].linear_state,
22677 self.pool.as_deref(),
22678 )
22679 }
22680 AttnKind::Bounded(w) => {
22681 let rope = self
22684 .bounded_rope
22685 .clone()
22686 .expect("bounded layer without an installed rotation table");
22687 let cfg = crate::bounded::BoundedAttnCfg {
22688 num_heads: self.num_heads,
22689 num_kv_heads: self.num_kv_heads,
22690 head_dim: self.head_dim,
22691 hidden_size: hs,
22692 scale: self.attn_scale,
22693 rope: &rope,
22694 pool: pool.as_deref(),
22695 };
22696 crate::bounded::bounded_attention(
22697 &self.ws.n1,
22698 w,
22699 &mut self.kv_cache.layers[li],
22700 &cfg,
22701 )
22702 }
22703 AttnKind::Full { wq, wk, wv, wo, .. } if self.dash.is_some() => {
22704 let (nh_l, hd_l) = (self.num_heads, self.head_dim);
22705 let mut ao = vec![0f32; nh_l * hd_l];
22706 let dash = self.dash.as_mut().expect("guarded");
22707 crate::uocr::attention_host(
22708 dash,
22709 li,
22710 &self.ws.n1,
22711 wq,
22712 wk,
22713 wv,
22714 &mut self.kv_cache.layers[li],
22715 position,
22716 nh_l,
22717 hd_l,
22718 self.rms_eps as f32,
22719 pool.as_deref(),
22720 &mut ao,
22721 );
22722 let mut out = vec![0f32; hs];
22723 wo.matvec(&ao, &mut out, pool.as_deref());
22724 out
22725 }
22726 AttnKind::Full {
22727 wq,
22728 wk,
22729 wv,
22730 wo,
22731 q_norm,
22732 k_norm,
22733 output_gate,
22734 softplus_gate,
22735 bias,
22736 } if self.kv_cache.layers[li].o1_sealed() => {
22737 let inv_freq_l = self.layer_inv_freq(li);
22740 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22741 let cfg = QwenAttnCfg {
22742 mrope: None,
22743 num_heads: self.layer_num_heads(li),
22744 num_kv_heads: nkv_l,
22745 head_dim: hd_l,
22746 hidden_size: hs,
22747 position,
22748 inv_freq: &inv_freq_l,
22749 rotary_dim: rd_l,
22750 scale: self.attn_scale,
22751 softcap: self.attn_softcap,
22752 window: None,
22753 v_norm: self.attn_v_norm,
22754 qk_norm_after_rope: self.qk_norm_after_rope,
22755 gate_sigmoid: self.proj_gate_sigmoid,
22756 q_norm: q_norm.as_deref(),
22757 k_norm: k_norm.as_deref(),
22758 output_gate: *output_gate,
22759 softplus_gate: softplus_gate
22760 .as_ref()
22761 .map(|(gate, per_head)| (gate, *per_head)),
22762 rope_scale: self.layer_rope_scale(li),
22763 bias: bias
22764 .as_ref()
22765 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
22766 rms_eps: eps,
22767 norm_style: self.norm_style,
22768 pool: pool.as_deref(),
22769 v_head_dim: self.layer_v_dim(li),
22770 value_in: vres.then(|| self.vres_in.as_slice()),
22771 };
22772 attention::qwen_attention_nystrom(
22773 &self.ws.n1,
22774 wq,
22775 wk,
22776 wv,
22777 wo,
22778 &mut self.kv_cache.layers[li],
22779 &cfg,
22780 )
22781 }
22782 AttnKind::Full {
22783 wq,
22784 wk,
22785 wv,
22786 wo,
22787 q_norm,
22788 k_norm,
22789 output_gate,
22790 softplus_gate,
22791 bias,
22792 } => 'attn: {
22793 let dropin_reason =
22798 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
22799 if let Some(reason) = dropin_reason {
22800 self.note_graph_decline("wgpu attn dropin", reason);
22801 }
22802 if graph_on
22803 && dropin_reason.is_none()
22804 && !*output_gate
22805 && softplus_gate.is_none()
22806 && self.attention_heads_per_layer.is_none()
22807 && bias.is_none()
22808 && task_mask.is_none()
22809 {
22810 let inv_freq_l = self.layer_inv_freq(li);
22811 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22812 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
22813 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
22814 wq.mapped_q1(),
22815 wk.mapped_q1(),
22816 wv.mapped_q1(),
22817 wo.mapped_q1(),
22818 ) {
22819 let gm = gm.clone();
22820 let mut out = vec![0f32; hs];
22821 let cache = &self.kv_cache.layers[li];
22822 if crate::gpu::attn_dropin(
22823 &gm,
22824 self.graph_kv_id,
22825 li,
22826 &self.ws.n1,
22827 qi,
22828 ki,
22829 vi,
22830 oi,
22831 q_norm.as_deref(),
22832 k_norm.as_deref(),
22833 self.qk_norm_after_rope,
22834 &inv_freq_l,
22835 nh,
22836 nkv_l,
22837 hd_l,
22838 rd_l,
22839 hs,
22840 position,
22841 self.kv_cache.max_seq_len,
22842 gemma,
22843 eps as f32,
22844 cache.k_heads(),
22845 cache.v_heads(),
22846 &mut out,
22847 ) {
22848 break 'attn out;
22849 }
22850 }
22851 }
22852 let masked = task_mask
22853 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
22854 .unwrap_or(false);
22855 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
22856 let plain = self.layer_attn_plain(li);
22859 match (masked, f32_view) {
22860 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
22863 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
22864 attention::multi_head_attention(
22865 &self.ws.n1,
22866 q,
22867 k,
22868 v,
22869 o,
22870 &mut self.kv_cache.layers[li],
22871 self.num_heads,
22872 self.num_kv_heads,
22873 self.head_dim,
22874 self.hidden_size,
22875 position,
22876 &active_heads,
22877 &self.inv_freq,
22878 )
22879 }
22880 (masked, _) => {
22881 if masked {
22882 tracing::warn!(
22883 "layer {li}: head mask on quantized weights or on a \
22884 window/sink/per-layer-geometry layer not supported \
22885 yet — executing dense"
22886 );
22887 }
22888 let inv_freq_l = self.layer_inv_freq(li);
22889 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
22890 let cfg = QwenAttnCfg {
22891 mrope: self.prompt_mrope.as_deref().zip(self.mrope_sections).map(|(pos, sections)| attention::MropeView { base: 0, pos, sections }),
22892 num_heads: self.layer_num_heads(li),
22893 num_kv_heads: nkv_l,
22894 head_dim: hd_l,
22895 hidden_size: hs,
22896 position,
22897 inv_freq: &inv_freq_l,
22898 rotary_dim: rd_l,
22899 scale: self.attn_scale,
22900 softcap: self.attn_softcap,
22901 window: self.layer_window(li),
22902 v_norm: self.attn_v_norm,
22903 qk_norm_after_rope: self.qk_norm_after_rope,
22904 gate_sigmoid: self.proj_gate_sigmoid,
22905 q_norm: q_norm.as_deref(),
22906 k_norm: k_norm.as_deref(),
22907 output_gate: *output_gate,
22908 softplus_gate: softplus_gate
22909 .as_ref()
22910 .map(|(gate, per_head)| (gate, *per_head)),
22911 rope_scale: self.layer_rope_scale(li),
22912 bias: bias
22913 .as_ref()
22914 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
22915 rms_eps: eps,
22916 norm_style: self.norm_style,
22917 pool: pool.as_deref(),
22918 v_head_dim: self.layer_v_dim(li),
22919 value_in: vres.then(|| self.vres_in.as_slice()),
22920 };
22921 attention::qwen_attention(
22922 &self.ws.n1,
22923 wq,
22924 wk,
22925 wv,
22926 wo,
22927 &mut self.kv_cache.layers[li],
22928 &cfg,
22929 )
22930 }
22931 }
22932 }
22933 };
22934 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
22937 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
22938 None => attn_out,
22939 };
22940 let mhc_ffn = match mhc_attn {
22941 Some(c) => {
22942 self.mhc_post(li, false, &attn_out, &c, &mut h);
22945 let cf = self.mhc_pre(li, true, &mut h);
22946 let lw = &self.weights.layers[self.phys_layer(li)];
22947 inference::rms_norm_into(&h, &lw.post_norm, self.rms_eps, self.norm_style, &mut self.ws.p1);
22948 Some(cf)
22949 }
22950 None => {
22951 let lw = &self.weights.layers[self.phys_layer(li)];
22952 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
22953 inference::add_rmsnorm_fused_into(
22954 &mut h,
22955 &attn_out,
22956 &lw.post_norm,
22957 self.rms_eps,
22958 self.norm_style,
22959 &mut self.ws.p1,
22960 );
22961 drop(prof);
22962 None
22963 }
22964 };
22965 let lw = &self.weights.layers[self.phys_layer(li)];
22966 let mut attn_out = attn_out;
22967 attention::recycle_buf(&mut attn_out);
22968 let post_normed = &self.ws.p1;
22969
22970 let ffn_masked = task_mask
22971 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
22972 .unwrap_or(false);
22973 let ffn_out = match (ffn_masked, &lw.ffn) {
22985 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
22989 let row = task_mask
22990 .and_then(|tm| tm.ffn_masks.get(li))
22991 .map(|v| v.as_slice());
22992 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
22993 }
22994 (true, FfnKind::Dense(d)) => {
22995 let tm = task_mask.unwrap();
22996 let alive = tm.ffn_active_count(li);
22997 let deep = alive * 2 <= self.intermediate_size;
22998 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
22999 let active = tm.ffn_active_indices(li);
23000 sparse_ffn_quant(
23001 d,
23002 post_normed,
23003 &active,
23004 self.hidden_size,
23005 self.pool.as_deref(),
23006 )
23007 } else if deep
23008 && let (Some(g), Some(u), Some(dn)) = (
23009 d.gate_proj.as_f32(),
23010 d.up_proj.as_f32(),
23011 d.down_proj.as_f32(),
23012 )
23013 {
23014 let active = tm.ffn_active_indices(li);
23015 inference::sparse_ffn_forward(
23016 post_normed,
23017 g,
23018 u,
23019 dn,
23020 self.hidden_size,
23021 self.intermediate_size,
23022 &active,
23023 self.pool.as_deref(),
23024 )
23025 } else {
23026 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
23027 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
23028 }
23029 }
23030 (true, FfnKind::Moe(m)) => {
23031 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
23035 ffn_forward(
23036 &lw.ffn,
23037 post_normed,
23038 self.pool.as_deref(),
23039 allowed.as_deref(),
23040 )
23041 }
23042 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
23043 dm,
23044 post_normed,
23045 &h,
23046 self.rms_eps,
23047 self.norm_style,
23048 self.pool.as_deref(),
23049 ),
23050 (false, _) => match &lw.ffn {
23051 FfnKind::DenseMoe(dm) => dense_moe_ffn(
23052 dm,
23053 post_normed,
23054 &h,
23055 self.rms_eps,
23056 self.norm_style,
23057 self.pool.as_deref(),
23058 ),
23059 FfnKind::Moe(m)
23060 if task_mask.is_none()
23061 && host_tail
23062 && li >= tail_start
23063 && crate::minicpm_tail::MiniCpmTail::is_requested_target(m) =>
23064 {
23065 moe_ffn_minicpm_tail(
23066 &mut self.minicpm_tail,
23067 li,
23068 self.num_layers,
23069 m,
23070 post_normed,
23071 self.pool.as_deref(),
23072 )
23073 }
23074 FfnKind::Moe(m)
23075 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
23076 {
23077 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
23078 }
23079 _ => {
23080 let allowed = match (&lw.ffn, task_mask) {
23081 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
23082 _ => None,
23083 };
23084 ffn_forward(
23085 &lw.ffn,
23086 post_normed,
23087 self.pool.as_deref(),
23088 allowed.as_deref(),
23089 )
23090 }
23091 },
23092 };
23093 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
23094 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
23095 None => ffn_out,
23096 };
23097 match mhc_ffn {
23098 Some(c) => self.mhc_post(li, true, &ffn_out, &c, &mut h),
23099 None => {
23100 for (i, &f) in ffn_out.iter().enumerate() {
23101 h[i] += f;
23102 }
23103 }
23104 }
23105 let mut ffn_out = ffn_out;
23106 attention::recycle_buf(&mut ffn_out);
23107
23108 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
23110 for v in h.iter_mut() {
23111 *v *= sc;
23112 }
23113 }
23114 if self.layer_dump.is_some() {
23116 self.dump_layer_row(position, li, &h);
23117 }
23118
23119 if self.is_loop_end(li) && li + 1 < self.num_layers {
23122 h = inference::rms_norm(
23123 &h,
23124 &self.weights.final_norm,
23125 self.rms_eps,
23126 self.norm_style,
23127 );
23128 }
23129
23130 if self.dyn_phi_layer == Some(li) {
23134 self.update_dyn_phi(&h);
23135 }
23136 }
23137 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
23139 crate::gpu::graph_race_record(false, t.elapsed());
23140 }
23141
23142 h
23143 }
23144
23145 fn update_dyn_phi(&mut self, h: &[f32]) {
23148 const A: f32 = 0.2;
23149 if self.dyn_phi_ema.len() != h.len() {
23150 self.dyn_phi_ema = vec![0.0; h.len()];
23151 self.dyn_phi_seen = 0;
23152 }
23153 if self.dyn_phi_seen == 0 {
23154 self.dyn_phi_ema.copy_from_slice(h);
23155 } else {
23156 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
23157 *e = (1.0 - A) * *e + A * v;
23158 }
23159 }
23160 self.dyn_phi_seen += 1;
23161 }
23162
23163 pub fn dyn_phi(&self) -> &[f32] {
23165 &self.dyn_phi_ema
23166 }
23167
23168 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
23170 self.dyn_phi_layer = layer;
23171 self.dyn_phi_ema.clear();
23172 self.dyn_phi_seen = 0;
23173 }
23174
23175 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
23177 let Some(model) = &self.model else {
23178 return Vec::new();
23179 };
23180 model
23181 .header
23182 .skills
23183 .iter()
23184 .enumerate()
23185 .filter_map(|(i, sk)| {
23186 if sk.is_v2() {
23190 return None;
23191 }
23192 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
23193 let sel = sk.selection.as_ref()?;
23194 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
23195 })
23196 .collect()
23197 }
23198
23199 pub fn active_skill(&self) -> Option<usize> {
23201 self.dyn_active
23202 }
23203
23204 pub fn enable_dynamic_routing(&mut self) -> usize {
23209 use crate::swarm::{DynRouter, RoutableSkill};
23210 let Some(model) = self.model.clone() else {
23211 return 0;
23212 };
23213 if let Some(r) = &model.header.router {
23220 tracing::warn!(
23221 "dynamic routing disabled: this file declares router policy '{}' with \
23222 granularity \"{}\" — the request-level decision applies instead",
23223 r.policy,
23224 r.granularity
23225 );
23226 return 0;
23227 }
23228 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
23234 || model.header.skills.iter().any(|s| s.is_v2())
23235 {
23236 tracing::warn!(
23237 "dynamic routing disabled: this file carries format-v2 skill records \
23238 (SKILLS_V2) — they route per request through a router policy only"
23239 );
23240 return 0;
23241 }
23242 if self.dyn_blend_loaded {
23245 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
23246 return 0;
23247 }
23248 if let Some(a) = self.dyn_active {
23252 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
23253 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
23254 return 0;
23255 }
23256 }
23257 let hidden = self.hidden_size;
23258 let mut skills = Vec::new();
23259 for (idx, id, _phi) in self.dynamic_skills() {
23260 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
23261 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
23262 skills.push(rs);
23263 }
23264 }
23265 }
23266 if skills.is_empty() {
23267 return 0;
23268 }
23269 let phi = skills[0].phi_layer;
23271 if skills.iter().any(|s| s.phi_layer != phi) {
23272 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
23273 }
23274 let n = skills.len();
23275 self.set_dyn_phi_layer(Some(phi));
23276 self.dyn_router = Some(DynRouter::new(skills));
23277 n
23278 }
23279
23280 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
23282 self.dyn_router
23283 .as_ref()
23284 .map(|r| r.switches.clone())
23285 .unwrap_or_default()
23286 }
23287
23288 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
23291 let _mimo_q8 = self
23292 .mimo_moe
23293 .is_on()
23294 .then(crate::qtensor::enter_full_gpu_q8_scope);
23295 let rows = self.weights.lm_head.rows();
23296 let mut logits = attention::take_buf(rows.min(self.vocab_size));
23297 let served = self.mimo_moe.is_on()
23301 && crate::gpu::mimo_q8_short_enabled()
23302 && rows == self.vocab_size
23303 && !self.weights.lm_head.has_prism_contract()
23304 && self
23305 .weights
23306 .lm_head
23307 .graph_weight()
23308 .is_some_and(|(model, idx, kind, _)| {
23309 kind == 7
23310 && crate::gpu::q82_short_rows(
23311 model,
23312 idx,
23313 hidden,
23314 1,
23315 rows,
23316 self.hidden_size,
23317 &mut logits,
23318 )
23319 });
23320 if !served {
23321 self.weights
23322 .lm_head
23323 .matvec(hidden, &mut logits, self.pool.as_deref());
23324 }
23325 logits.resize(self.vocab_size, 0.0);
23326 if let Some(m) = self.logit_multiplier {
23327 for l in logits.iter_mut() {
23328 *l *= m;
23329 }
23330 }
23331 if let Some(c) = self.final_softcap {
23332 softcap_in_place(&mut logits, c, self.pool.as_deref());
23333 }
23334 if let Some(cm) = self.head_clusters.as_ref() {
23335 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
23336 }
23337 logits
23338 }
23339
23340 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
23343 let h = hidden.len();
23344 let ncl = cm.len() / h.max(1);
23345 if ncl == 0 || logits.len() % ncl != 0 {
23346 return;
23347 }
23348 let cs = logits.len() / ncl;
23349 let mut lc = vec![0.0f32; ncl];
23351 for c in 0..ncl {
23352 let row = &cm[c * h..(c + 1) * h];
23353 let mut s = 0.0f32;
23354 for j in 0..h {
23355 s += row[j] * hidden[j];
23356 }
23357 lc[c] = s;
23358 }
23359 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
23360 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
23361 for c in 0..ncl {
23362 let blk = &mut logits[c * cs..(c + 1) * cs];
23363 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
23364 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
23365 let add = lc[c] - lse - bl;
23366 for v in blk.iter_mut() {
23367 *v += add;
23368 }
23369 }
23370 }
23371
23372 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
23377 #[cfg(target_os = "macos")]
23378 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
23379 self.clear_sequence_state();
23380 crate::gpu::graph_race_begin_generation();
23384 if task_mask.is_none() {
23385 self.o1_begin();
23386 }
23387 let mut hidden = vec![0.0f32; self.hidden_size];
23388 for (pos, &id) in ids.iter().enumerate() {
23389 let emb = self.embed_single(id);
23390 hidden = self.forward_layers(&emb, pos, task_mask);
23391 }
23392 if let Err(err) = self.o1_seal_checked() {
23393 self.o1_fail(err);
23394 }
23395 if let Some(logits) = self.graph_logits.take() {
23398 return logits;
23399 }
23400 inference::rms_norm_into(
23401 &hidden,
23402 &self.weights.final_norm,
23403 self.rms_eps,
23404 self.norm_style,
23405 &mut self.ws.n1,
23406 );
23407 self.lm_head_forward(&self.ws.n1)
23408 }
23409}
23410
23411pub fn create_test_pipeline(
23413 hidden_size: usize,
23414 intermediate_size: usize,
23415 num_heads: usize,
23416 num_kv_heads: usize,
23417 head_dim: usize,
23418 num_layers: usize,
23419 vocab_size: usize,
23420) -> Pipeline {
23421 let synth = |n: usize, salt: usize| -> Vec<f32> {
23424 (0..n)
23425 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
23426 .collect()
23427 };
23428 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
23429 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
23430 };
23431 let layer_weights: Vec<LayerWeights> = (0..num_layers)
23432 .map(|li| LayerWeights {
23433 input_norm: vec![1.0; hidden_size],
23434 post_norm: vec![1.0; hidden_size],
23435 attn_out_norm: None,
23436 ffn_out_norm: None,
23437 layer_scale: None,
23438 ffn: FfnKind::Dense(DenseFfn {
23439 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
23440 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
23441 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
23442 act: Act::Silu,
23443 down_t: None,
23444 segs: Vec::new(),
23445 }),
23446 attn: AttnKind::Full {
23447 bias: None,
23448 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
23449 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
23450 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
23451 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
23452 q_norm: None,
23453 k_norm: None,
23454 output_gate: false,
23455 softplus_gate: None,
23456 },
23457 })
23458 .collect();
23459
23460 Pipeline::new(
23461 Tokenizer::byte_level(),
23462 PipelineWeights {
23463 embed_tokens: qt(vocab_size, hidden_size, 100),
23464 layers: layer_weights,
23465 lm_head: qt(vocab_size, hidden_size, 200),
23466 final_norm: vec![1.0; hidden_size],
23467 },
23468 hidden_size,
23469 intermediate_size,
23470 num_heads,
23471 num_kv_heads,
23472 head_dim,
23473 num_layers,
23474 num_layers, false, vocab_size,
23477 1e-6,
23478 10_000.0,
23479 NormStyle::Qwen,
23480 4096,
23481 SamplerConfig {
23482 seed: Some(42),
23483 ..Default::default()
23484 },
23485 )
23486}
23487
23488#[inline]
23493fn mask_bit(row: &[u8], j: usize) -> bool {
23494 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
23495}
23496
23497fn mask_gain() -> f32 {
23508 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
23509 *G.get_or_init(|| {
23510 std::env::var("CMF_FFN_MASK_GAIN")
23511 .ok()
23512 .and_then(|v| v.parse().ok())
23513 .unwrap_or(1.0)
23514 })
23515}
23516
23517fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
23518 let fill = meanfill().and_then(|(i, v)| {
23521 let li = crate::gpu::cur_layer();
23522 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
23523 });
23524 for r in 0..rows {
23525 let base = r * inter;
23526 for (bi, &byte) in row.iter().enumerate() {
23527 if byte == 0xFF {
23528 continue;
23529 }
23530 let j0 = bi * 8;
23531 for bit in 0..8 {
23532 let j = j0 + bit;
23533 if j < inter && byte & (1 << bit) == 0 {
23534 g[base + j] = fill.map_or(0.0, |f| f[j]);
23535 }
23536 }
23537 }
23538 }
23539 let gain = mask_gain();
23540 if gain != 1.0 {
23541 for v in g[..rows * inter].iter_mut() {
23542 *v *= gain;
23543 }
23544 }
23545}
23546
23547#[inline]
23549fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
23550 row.is_none_or(|r| mask_bit(r, i))
23551}
23552
23553fn all_bits_on(row: &[u8], n: usize) -> bool {
23556 (0..n).all(|i| mask_bit(row, i))
23557}
23558
23559fn tube_topk() -> usize {
23567 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
23568 *K.get_or_init(|| {
23569 std::env::var("CMF_TUBE_TOPK")
23570 .ok()
23571 .and_then(|v| v.parse().ok())
23572 .unwrap_or(0)
23573 })
23574}
23575
23576fn tube_score_oracle() -> bool {
23577 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
23578 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
23579}
23580
23581fn tube_ffn_routed(
23588 d: &DenseFfn,
23589 xs: &[f32],
23590 b: usize,
23591 pool: Option<&Pool>,
23592 mask_row: Option<&[u8]>,
23593 k: usize,
23594) -> Vec<f32> {
23595 let hidden = d.down_proj.rows();
23596 let core = d.gate_proj.rows();
23597 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
23598 let mut out = match (b, core_full, mask_row) {
23599 (1, true, _) => dense_ffn(d, xs, pool),
23600 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
23601 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
23602 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
23603 };
23604 let cand: Vec<usize> = (0..d.segs.len())
23605 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
23606 .collect();
23607 if cand.is_empty() {
23608 return out;
23609 }
23610 let oracle = tube_score_oracle();
23614 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
23615 let mut scores = vec![0f32; b * cand.len()];
23616 for (ci, &i) in cand.iter().enumerate() {
23617 let seg = &d.segs[i];
23618 let w = seg.width;
23619 let mut g = vec![0.0f32; b * w];
23620 if b == 1 {
23621 seg.gate.matvec(xs, &mut g, pool);
23622 } else {
23623 seg.gate.matmat(xs, b, &mut g, pool);
23624 }
23625 for v in g.iter_mut() {
23626 *v = Act::Silu.combine(*v, 1.0);
23627 }
23628 if !oracle {
23629 for t in 0..b {
23630 scores[t * cand.len() + ci] =
23631 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
23632 }
23633 }
23634 if oracle || b > 1 {
23635 let mut u = vec![0.0f32; b * w];
23636 if b == 1 {
23637 seg.up.matvec(xs, &mut u, pool);
23638 } else {
23639 seg.up.matmat(xs, b, &mut u, pool);
23640 }
23641 for (a, &v) in g.iter_mut().zip(u.iter()) {
23642 *a *= v;
23643 }
23644 if oracle {
23645 for t in 0..b {
23646 scores[t * cand.len() + ci] =
23647 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
23648 }
23649 }
23650 }
23651 acts.push(g);
23652 }
23653 let keep = k.min(cand.len());
23655 let mut scratch: Vec<f32> = Vec::new();
23656 for t in 0..b {
23657 let mut sc: Vec<(f32, usize)> = (0..cand.len())
23658 .map(|ci| (scores[t * cand.len() + ci], ci))
23659 .collect();
23660 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
23661 let mut alive = vec![false; cand.len()];
23662 for &(_, ci) in sc.iter().take(keep) {
23663 alive[ci] = true;
23664 }
23665 if b > 1 {
23666 for (ci, a) in acts.iter_mut().enumerate() {
23667 if !alive[ci] {
23668 let w = d.segs[cand[ci]].width;
23669 a[t * w..(t + 1) * w].fill(0.0);
23670 }
23671 }
23672 } else {
23673 for (ci, &i) in cand.iter().enumerate() {
23677 if !alive[ci] {
23678 continue;
23679 }
23680 let seg = &d.segs[i];
23681 let w = seg.width;
23682 let g = &mut acts[ci];
23683 if !tube_score_oracle() {
23684 scratch.clear();
23685 scratch.resize(w, 0.0);
23686 seg.up.matvec(xs, &mut scratch, pool);
23687 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
23688 *a *= v;
23689 }
23690 }
23691 let mut acc = vec![0.0f32; hidden];
23692 seg.down.matvec(g, &mut acc, pool);
23693 for (o, a) in out.iter_mut().zip(&acc) {
23694 *o += *a;
23695 }
23696 }
23697 }
23698 }
23699 if b > 1 {
23700 for (ci, &i) in cand.iter().enumerate() {
23701 let seg = &d.segs[i];
23702 let mut acc = vec![0.0f32; b * hidden];
23703 seg.down.matmat(&acts[ci], b, &mut acc, pool);
23704 for (o, a) in out.iter_mut().zip(&acc) {
23705 *o += *a;
23706 }
23707 }
23708 }
23709 out
23710}
23711
23712fn tube_ffn(
23718 d: &DenseFfn,
23719 xs: &[f32],
23720 b: usize,
23721 pool: Option<&Pool>,
23722 mask_row: Option<&[u8]>,
23723) -> Vec<f32> {
23724 if tube_topk() > 0 {
23725 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
23726 }
23727 let hidden = d.down_proj.rows();
23728 let core = d.gate_proj.rows();
23729 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
23730 let mut out = match (b, core_full, mask_row) {
23731 (1, true, _) => dense_ffn(d, xs, pool),
23732 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
23733 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
23734 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
23735 };
23736 TUBE_SCRATCH.with(|sc| {
23737 let mut sc = sc.borrow_mut();
23738 let [g, u, acc] = &mut *sc;
23739 for seg in &d.segs {
23740 if !tube_bit(mask_row, seg.start) {
23741 continue;
23742 }
23743 let w = seg.width;
23744 g.resize(b * w, 0.0);
23745 if b == 1
23746 && d.act == Act::Silu
23747 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
23748 {
23749 } else {
23751 u.resize(b * w, 0.0);
23752 if b == 1 {
23753 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
23754 } else {
23755 seg.gate.matmat(xs, b, g, pool);
23756 seg.up.matmat(xs, b, u, pool);
23757 }
23758 for i in 0..b * w {
23759 g[i] = d.act.combine(g[i], u[i]);
23760 }
23761 }
23762 acc.resize(b * hidden, 0.0);
23763 acc.fill(0.0);
23764 if b == 1 {
23765 seg.down.matvec(g, acc, pool);
23766 } else {
23767 seg.down.matmat(g, b, acc, pool);
23768 }
23769 for (o, a) in out.iter_mut().zip(acc.iter()) {
23770 *o += *a;
23771 }
23772 }
23773 out
23774 })
23775}
23776
23777thread_local! {
23778 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
23782 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
23783}
23784
23785pub(crate) fn expert_tubes_add(d: &DenseFfn, x: &[f32], weight: f32, out: &mut [f32], pool: Option<&Pool>) {
23789 if d.segs.is_empty() {
23790 return;
23791 }
23792 TUBE_SCRATCH.with(|sc| {
23793 let mut sc = sc.borrow_mut();
23794 let [g, u, acc] = &mut *sc;
23795 for seg in &d.segs {
23796 let w = seg.width;
23797 g.resize(w, 0.0);
23798 if !(d.act == Act::Silu && QTensor::matvec_silu_mul(&seg.gate, &seg.up, x, g, pool)) {
23799 u.resize(w, 0.0);
23800 QTensor::matvec_many([&seg.gate, &seg.up], x, [g, u], pool);
23801 for i in 0..w {
23802 g[i] = d.act.combine(g[i], u[i]);
23803 }
23804 }
23805 acc.resize(out.len(), 0.0);
23806 acc.fill(0.0);
23807 seg.down.matvec(g, acc, pool);
23808 for (o, a) in out.iter_mut().zip(acc.iter()) {
23809 *o += weight * *a;
23810 }
23811 }
23812 });
23813}
23814
23815pub(crate) fn shared_expert_weight(gate: Option<&QTensor>, x: &[f32], pool: Option<&Pool>) -> f32 {
23817 gate.map_or(1.0, |gate| {
23818 let mut gl = [0.0f32; 1];
23819 gate.matvec(x, &mut gl, pool);
23820 1.0 / (1.0 + (-gl[0]).exp())
23821 })
23822}
23823
23824pub(crate) fn shared_tubes_add(m: &MoeFfn, x: &[f32], out: &mut [f32], pool: Option<&Pool>) {
23827 if let Some((se, gate)) = &m.shared
23828 && !se.segs.is_empty()
23829 {
23830 let g = shared_expert_weight(gate.as_ref(), x, pool);
23831 expert_tubes_add(se, x, g, out, pool);
23832 }
23833}
23834
23835static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] = [
23838 std::sync::atomic::AtomicU64::new(0),
23839 std::sync::atomic::AtomicU64::new(0),
23840];
23841
23842fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
23846 #[cfg(feature = "gpu")]
23847 {
23848 use std::sync::atomic::Ordering::Relaxed;
23849 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
23850 (
23851 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
23852 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
23853 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
23854 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
23855 )
23856 }
23857 #[cfg(not(feature = "gpu"))]
23858 (0.0, 0.0, 0.0, 0)
23859}
23860
23861fn wgpu_mirror_counters() -> [u64; 6] {
23864 #[cfg(feature = "gpu")]
23865 {
23866 crate::gpu_wgpu::MIRROR_EVENTS
23867 .each_ref()
23868 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
23869 }
23870 #[cfg(not(feature = "gpu"))]
23871 [0; 6]
23872}
23873
23874fn prefill_prof_on() -> bool {
23875 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
23876 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
23877}
23878
23879pub(crate) fn accumulate_dense_ffn_activation_mass(
23888 d: &DenseFfn,
23889 xs: &[f32],
23890 b: usize,
23891 pool: Option<&Pool>,
23892 mass: &mut [f64],
23893 squared_mass: &mut [f64],
23894) -> Result<(), String> {
23895 let inter = d.gate_proj.rows();
23896 let hidden = d.gate_proj.cols();
23897 if mass.len() != inter || squared_mass.len() != inter {
23898 return Err(format!(
23899 "MTP FFN probe: accumulator widths ({}, {}) != gate width {inter}",
23900 mass.len(),
23901 squared_mass.len()
23902 ));
23903 }
23904 if d.up_proj.rows() != inter
23905 || d.up_proj.cols() != hidden
23906 || d.down_proj.cols() != inter
23907 || d.down_proj.rows() != hidden
23908 {
23909 return Err("MTP FFN probe: inconsistent dense FFN shapes".into());
23910 }
23911 if !d.segs.is_empty() {
23912 return Err("MTP FFN probe: tube-segmented draft FFNs are not supported".into());
23913 }
23914 if xs.len() != b.saturating_mul(hidden) {
23915 return Err(format!(
23916 "MTP FFN probe: {} input values for {b} × {hidden} rows",
23917 xs.len()
23918 ));
23919 }
23920 if b == 0 {
23921 return Ok(());
23922 }
23923 let mut gate = vec![0.0f32; b * inter];
23924 let mut up = vec![0.0f32; b * inter];
23925 d.gate_proj.matmat(xs, b, &mut gate, pool);
23926 d.up_proj.matmat(xs, b, &mut up, pool);
23927 for row in 0..b {
23928 for i in 0..inter {
23929 let a = d.act.combine(gate[row * inter + i], up[row * inter + i]);
23930 let a = a as f64;
23931 mass[i] += a.abs();
23932 squared_mass[i] += a * a;
23933 }
23934 }
23935 Ok(())
23936}
23937
23938fn dense_ffn_batch(
23939 d: &DenseFfn,
23940 xs: &[f32],
23941 b: usize,
23942 pool: Option<&Pool>,
23943 mask_row: Option<&[u8]>,
23944) -> Vec<f32> {
23945 let inter = d.gate_proj.rows();
23946 let hidden = d.down_proj.rows();
23947 let fused_act = d.act.wgpu_graph_act();
23959 if mask_row.is_none()
23960 && fused_act.is_some()
23961 && b >= 32
23962 && crate::gpu::enabled_here()
23963 && !crate::gpu::mm_killed()
23964 && refit_dir().is_none()
23969 && !ffn_probe_active()
23974 && !crate::gptq_capture::capturing()
23978 {
23979 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
23980 d.gate_proj.mapped_q4t(),
23981 d.up_proj.mapped_q4t(),
23982 d.down_proj.mapped_q4t(),
23983 ) {
23984 let mut out = vec![0.0f32; b * hidden];
23985 let act = fused_act.expect("checked above");
23986 if crate::gpu::q4_ffn_act(
23987 model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out,
23988 ) {
23989 return out;
23990 }
23991 }
23992 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
23997 d.gate_proj.mapped_q4tp(),
23998 d.up_proj.mapped_q4tp(),
23999 d.down_proj.mapped_q4tp(),
24000 ) {
24001 let mut out = vec![0.0f32; b * hidden];
24002 let act = fused_act.expect("checked above");
24003 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
24004 {
24005 return out;
24006 }
24007 }
24008 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
24014 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
24015 d.gate_proj.mapped_device_gemm(),
24016 d.up_proj.mapped_device_gemm(),
24017 d.down_proj.mapped_device_gemm(),
24018 ) {
24019 let mut out = vec![0.0f32; b * hidden];
24020 let act = fused_act.expect("checked above");
24021 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
24022 return out;
24023 }
24024 }
24025 }
24026 let mut g = vec![0.0f32; b * inter];
24027 d.gate_proj.matmat(xs, b, &mut g, pool);
24028 let mut u = vec![0.0f32; b * inter];
24029 d.up_proj.matmat(xs, b, &mut u, pool);
24030 if gate_topk() > 0 && d.act == Act::Silu {
24031 for t in 0..b {
24032 let row = &mut g[t * inter..(t + 1) * inter];
24033 for v in row.iter_mut() {
24034 *v = Act::Silu.combine(*v, 1.0);
24035 }
24036 keep_top_k(row, gate_topk());
24037 }
24038 for i in 0..b * inter {
24039 g[i] *= u[i];
24040 }
24041 } else {
24042 combine_pooled(d.act, &mut g[..b * inter], &u[..b * inter], pool);
24043 }
24044 if let Some(row) = mask_row {
24045 zero_masked_cols(&mut g, b, inter, row);
24046 }
24047 if oracle_topk() > 0 {
24048 for t in 0..b {
24049 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
24050 }
24051 }
24052 let mut out = vec![0.0f32; b * hidden];
24053 d.down_proj.matmat(&g, b, &mut out, pool);
24054 if refit_dir().is_some() {
24055 let li = crate::gpu::cur_layer();
24056 if li >= 0 {
24057 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
24058 }
24059 }
24060 FFN_PROBE.with(|pr| {
24064 if let Some(acc) = pr.borrow_mut().as_mut() {
24065 let li = crate::gpu::cur_layer();
24066 if li < 0 {
24067 return;
24068 }
24069 let Some(row) = acc.get_mut(li as usize) else {
24070 return;
24071 };
24072 let (sq, topk) = (probe_sq(), probe_topk().min(inter));
24073 let mut mag: Vec<f32> = Vec::new();
24074 for t in 0..b {
24075 let gr = &g[t * inter..(t + 1) * inter];
24076 if topk > 0 {
24077 mag.clear();
24080 mag.extend(gr.iter().map(|v| v.abs()));
24081 let (_, kth, _) = mag.select_nth_unstable_by(topk - 1, |a, b| {
24082 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
24083 });
24084 let thr = *kth;
24085 for (a, &v) in row.iter_mut().zip(gr) {
24086 if v.abs() >= thr {
24087 *a += 1.0;
24088 }
24089 }
24090 continue;
24091 }
24092 for (a, &v) in row.iter_mut().zip(gr) {
24093 *a += if sq {
24094 (v as f64) * (v as f64)
24095 } else {
24096 (v as f64).abs()
24097 };
24098 }
24099 }
24100 }
24101 });
24102 out
24103}
24104
24105fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
24110 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
24111 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
24112 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
24113 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
24114 if (!on && !dump) || b == 0 {
24115 return;
24116 }
24117 let hidden = xs.len() / b;
24118 if on {
24119 let mut acc = m.act_sq.borrow_mut();
24120 if acc.len() < hidden {
24121 acc.resize(hidden, 0.0);
24122 }
24123 for t in 0..b {
24124 let row = &xs[t * hidden..(t + 1) * hidden];
24125 for (a, &v) in acc.iter_mut().zip(row) {
24126 *a += (v as f64) * (v as f64);
24127 }
24128 }
24129 }
24130 if dump {
24131 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
24134 .ok()
24135 .and_then(|v| v.parse().ok())
24136 .unwrap_or(4096);
24137 let mut rows = m.act_rows.borrow_mut();
24138 if rows.len() < cap * hidden {
24139 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
24140 rows.extend_from_slice(&xs[..take * hidden]);
24141 }
24142 }
24143}
24144
24145#[derive(Clone, Copy)]
24148struct SendVecs(*mut Vec<f32>);
24149unsafe impl Send for SendVecs {}
24150unsafe impl Sync for SendVecs {}
24151impl SendVecs {
24152 #[inline]
24153 fn at(self, i: usize) -> *mut Vec<f32> {
24154 unsafe { self.0.add(i) }
24155 }
24156}
24157
24158fn moe_ffn_batch(
24159 m: &MoeFfn,
24160 xs: &[f32],
24161 b: usize,
24162 hidden: usize,
24163 pool: Option<&Pool>,
24164 allowed: Option<&[bool]>,
24165) -> Vec<f32> {
24166 accumulate_act(m, xs, b);
24167 let ne = m.experts.len();
24168 let mut logits = vec![0.0f32; b * ne];
24169 match &m.resonance {
24170 Some(r) => {
24171 let hdim = xs.len() / b.max(1);
24172 for bi in 0..b {
24173 r.scores(
24174 &xs[bi * hdim..(bi + 1) * hdim],
24175 &mut logits[bi * ne..(bi + 1) * ne],
24176 );
24177 }
24178 }
24179 None => m.router.matmat(xs, b, &mut logits, pool),
24180 }
24181
24182 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
24185 {
24186 let mut st = m.stats.borrow_mut();
24187 if st.len() < ne {
24188 st.resize(ne, 0);
24189 }
24190 for bi in 0..b {
24191 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
24192 for &e in &idx {
24193 st[e] += 1;
24194 assign[e].push((bi, p[e] / wsum));
24195 }
24196 }
24197 }
24198
24199 let mut out = vec![0.0f32; b * hidden];
24200 let cols = m.experts[0].gate_proj.cols();
24201 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
24202 let sb = list.len();
24203 let mut sub = vec![0.0f32; sb * cols];
24204 for (k, &(bi, _)) in list.iter().enumerate() {
24205 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
24206 }
24207 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
24208 for (k, &(bi, w)) in list.iter().enumerate() {
24209 for i in 0..hidden {
24210 out[bi * hidden + i] += w * eo[k * hidden + i];
24211 }
24212 }
24213 };
24214 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
24220 if pool.is_some() && active.len() >= 8 {
24221 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
24222 {
24223 let panel_ptr = SendVecs(panels.as_mut_ptr());
24224 let experts = &m.experts;
24227 let (active_r, assign_r) = (&active, &assign);
24228 let inherit_cpu = crate::gpu::inherit_cpu_scope();
24229 let run = |start: usize, end: usize| {
24230 let _cpu_scope = inherit_cpu();
24231 #[cfg(target_os = "macos")]
24239 let _metal_cpu = crate::gpu::enter_cpu_scope();
24240 for ai in start..end {
24241 let e = active_r[ai];
24242 let list = &assign_r[e];
24243 let sb = list.len();
24244 let mut sub = vec![0.0f32; sb * cols];
24245 for (k, &(bi, _)) in list.iter().enumerate() {
24246 sub[k * cols..(k + 1) * cols]
24247 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
24248 }
24249 unsafe {
24251 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
24252 }
24253 }
24254 };
24255 match pool {
24256 Some(p) => p.run_rows(active.len(), &run),
24257 None => run(0, active.len()),
24258 }
24259 }
24260 for (ai, &e) in active.iter().enumerate() {
24261 for (k, &(bi, w)) in assign[e].iter().enumerate() {
24262 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
24263 for i in 0..hidden {
24264 out[bi * hidden + i] += w * eo[i];
24265 }
24266 }
24267 }
24268 } else {
24269 for &e in &active {
24270 run_expert(&m.experts[e], &assign[e], &mut out);
24271 }
24272 }
24273 if let Some((se, gate)) = &m.shared {
24274 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
24275 let mut gl = vec![0.0f32; b];
24276 gate.matmat(xs, b, &mut gl, pool);
24277 (0..b)
24278 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
24279 .collect()
24280 } else {
24281 (0..b).map(|bi| (bi, 1.0)).collect()
24282 };
24283 run_expert(se, &all, &mut out);
24284 }
24285 out
24286}
24287
24288fn moe_ffn_rows_exact(
24300 m: &MoeFfn,
24301 xs: &[f32],
24302 b: usize,
24303 hidden: usize,
24304 pool: Option<&Pool>,
24305) -> Vec<f32> {
24306 let mut out = vec![0.0f32; b * hidden];
24307 let per_row = |out: &mut [f32]| {
24308 for r in 0..b {
24309 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
24310 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
24311 }
24312 };
24313 let covered = !crate::gpu::enabled_here()
24314 && moe_batch_enabled()
24315 && m.shared.is_none()
24316 && m.resonance.is_none()
24317 && FFN_PROBE.with(|pr| pr.borrow().is_none())
24318 && m.experts.iter().all(|d| d.act == Act::Silu);
24319 if !covered {
24320 per_row(&mut out);
24321 return out;
24322 }
24323 let ne = m.experts.len();
24324 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
24326 for r in 0..b {
24327 let x = &xs[r * hidden..(r + 1) * hidden];
24328 accumulate_act(m, x, 1);
24329 let mut logits = vec![0.0f32; ne];
24330 m.router.matvec(x, &mut logits, pool);
24331 let (idx, p, wsum) = moe_route(&logits, m, None);
24332 {
24333 let mut st = m.stats.borrow_mut();
24334 if st.len() < ne {
24335 st.resize(ne, 0);
24336 }
24337 for &e in &idx {
24338 st[e] += 1;
24339 }
24340 }
24341 let w: Vec<f32> = idx
24342 .iter()
24343 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
24344 .collect();
24345 routes.push((idx, w));
24346 }
24347 if routes.iter().any(|(idx, _)| idx.is_empty()) {
24348 per_row(&mut out);
24349 return out;
24350 }
24351 let mut experts: Vec<usize> = Vec::new();
24353 let mut groups: Vec<Vec<usize>> = Vec::new();
24354 for (r, (idx, _)) in routes.iter().enumerate() {
24355 for &e in idx {
24356 match experts.iter().position(|&x| x == e) {
24357 Some(g) => groups[g].push(r),
24358 None => {
24359 experts.push(e);
24360 groups.push(vec![r]);
24361 }
24362 }
24363 }
24364 }
24365 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
24366 let inter = m.experts[experts[0]].gate_proj.rows();
24367 let pairs: Vec<(&QTensor, &QTensor)> = experts
24368 .iter()
24369 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
24370 .collect();
24371 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
24372 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
24373 per_row(&mut out);
24374 return out;
24375 }
24376 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
24377 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
24378 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
24379 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
24380 per_row(&mut out);
24381 return out;
24382 }
24383 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
24385 let mut p = 0usize;
24386 for (g, &e) in experts.iter().enumerate() {
24387 for &r in &groups[g] {
24388 slot.insert((r, e), p);
24389 p += 1;
24390 }
24391 }
24392 for (r, (idx, w)) in routes.iter().enumerate() {
24393 let terms: Vec<(&[f32], f32)> = idx
24394 .iter()
24395 .zip(w)
24396 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
24397 .collect();
24398 let row = &mut out[r * hidden..(r + 1) * hidden];
24399 for (i, dst) in row.iter_mut().enumerate() {
24400 let mut acc = 0f32;
24402 for (d, we) in &terms {
24403 acc += we * d[i];
24404 }
24405 *dst = acc;
24406 }
24407 }
24408 out
24409}
24410
24411pub(crate) fn vres_lerp(e0: &[f32], normed: &[f32], blend: f32, out: &mut Vec<f32>) {
24417 debug_assert_eq!(e0.len(), normed.len());
24418 out.clear();
24419 if blend < 0.5 {
24420 out.extend(e0.iter().zip(normed).map(|(&e, &n)| e + blend * (n - e)));
24421 } else {
24422 out.extend(e0.iter().zip(normed).map(|(&e, &n)| n - (n - e) * (1.0 - blend)));
24423 }
24424}
24425
24426pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
24427 match pool {
24428 Some(p) if v.len() >= 1 << 15 => {
24429 let base = v.as_mut_ptr() as usize;
24430 p.run_rows(v.len(), &|s, e| {
24431 let sl =
24434 unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
24435 for x in sl {
24436 *x = c * (*x / c).tanh();
24437 }
24438 });
24439 }
24440 _ => {
24441 for x in v.iter_mut() {
24442 *x = c * (*x / c).tanh();
24443 }
24444 }
24445 }
24446}
24447
24448thread_local! {
24449 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
24453 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
24454}
24455
24456fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
24458 if gate_topk() > 0
24461 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
24462 {
24463 return out;
24464 }
24465 let prism_body = d.gate_proj.has_prism_contract()
24481 || d.up_proj.has_prism_contract()
24482 || d.down_proj.has_prism_contract();
24483 if !prism_body
24484 && crate::gpu::enabled_here()
24485 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
24486 {
24487 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
24488 crate::gpu::ProbeArm::Gpu
24489 } else {
24490 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
24491 };
24492 match arm {
24493 crate::gpu::ProbeArm::Gpu => {
24494 let t0 = std::time::Instant::now();
24495 if let Some(out) = dense_ffn_gpu(d, x, pool) {
24496 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
24497 return out;
24498 }
24499 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
24503 }
24504 crate::gpu::ProbeArm::CpuTimed => {
24505 let t0 = std::time::Instant::now();
24506 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
24507 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
24508 return out;
24509 }
24510 crate::gpu::ProbeArm::Cpu => {
24511 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
24512 }
24513 }
24514 }
24515 dense_ffn_cpu(d, x, pool)
24516}
24517
24518fn combine_pooled(act: Act, g: &mut [f32], u: &[f32], pool: Option<&Pool>) {
24524 debug_assert_eq!(g.len(), u.len());
24525 match pool {
24526 Some(p) if g.len() >= 8192 => {
24527 let gp = crate::pool::SendMut::new(g.as_mut_ptr());
24528 p.run_rows(g.len(), &|start, end| {
24529 for i in start..end {
24530 unsafe {
24532 let v = gp.at(i);
24533 *v = act.combine(*v, u[i]);
24534 }
24535 }
24536 });
24537 }
24538 _ => {
24539 for (gv, &uv) in g.iter_mut().zip(u) {
24540 *gv = act.combine(*gv, uv);
24541 }
24542 }
24543 }
24544}
24545
24546fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
24548 let inter = d.gate_proj.rows();
24549 FFN_SCRATCH.with(|s| {
24550 let mut s = s.borrow_mut();
24551 let [g, u, ..] = &mut *s;
24552 g.resize(inter, 0.0);
24553 if gate_topk() > 0 {
24556 u.resize(inter, 0.0);
24560 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
24561 for i in 0..inter {
24562 g[i] = Act::Silu.combine(g[i], 1.0);
24563 }
24564 keep_top_k(g, gate_topk());
24565 for i in 0..inter {
24566 g[i] *= u[i];
24567 }
24568 } else if d.act == Act::Silu && {
24569 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24570 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
24571 } {
24572 } else if d.act.clamp_limit() > 0.0 && {
24574 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24575 QTensor::matvec_silu_mul_limited(&d.gate_proj, &d.up_proj, x, g, d.act.clamp_limit(), pool)
24576 } {
24577 } else {
24579 u.resize(inter, 0.0);
24580 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
24582 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
24583 let _c = crate::cpuprof::time(crate::cpuprof::Slot::FfnCombine);
24584 combine_pooled(d.act, &mut g[..inter], &u[..inter], pool);
24585 }
24586 FFN_PROBE.with(|pr| {
24594 if let Some(acc) = pr.borrow_mut().as_mut() {
24595 let li = crate::gpu::cur_layer();
24596 if li >= 0 {
24597 if let Some(row) = acc.get_mut(li as usize) {
24598 match probe_topk() {
24599 0 if probe_sq() => {
24600 for (a, &v) in row.iter_mut().zip(g.iter()) {
24601 *a += (v as f64) * (v as f64);
24602 }
24603 }
24604 0 if probe_signed() => {
24605 for (a, &v) in row.iter_mut().zip(g.iter()) {
24606 *a += v as f64;
24607 }
24608 }
24609 0 => {
24610 for (a, &v) in row.iter_mut().zip(g.iter()) {
24611 *a += (v as f64).abs();
24612 }
24613 }
24614 k => {
24615 let n = g.len();
24616 let k = k.min(n);
24617 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
24618 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
24619 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
24620 });
24621 let thr = *kth;
24622 for (a, &v) in row.iter_mut().zip(g.iter()) {
24623 if v.abs() >= thr {
24624 *a += 1.0;
24625 }
24626 }
24627 }
24628 }
24629 }
24630 }
24631 }
24632 });
24633 MOE_PROBE.with(|pr| {
24634 if let Some(pr) = pr.borrow_mut().as_mut()
24635 && let Some((e, w)) = pr.cur
24636 {
24637 let li = crate::gpu::cur_layer();
24638 if li >= 0
24639 && let Some(row) = pr.acc.get_mut(li as usize)
24640 && let Some(cell) = row.get_mut(e * pr.inter..(e + 1) * pr.inter)
24641 {
24642 for (a, &v) in cell.iter_mut().zip(g.iter()) {
24643 let s = (w * v) as f64;
24644 *a += s * s;
24645 }
24646 }
24647 }
24648 });
24649 if oracle_topk() > 0 {
24650 keep_top_k(g, oracle_topk());
24651 }
24652 {
24653 let li = crate::gpu::cur_layer();
24654 if li >= 0 {
24655 adump_row(li as usize, g);
24656 }
24657 }
24658 let mut out = attention::take_buf(d.down_proj.rows());
24659 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
24660 d.down_proj.matvec(g, &mut out, pool);
24661 out
24662 })
24663}
24664
24665pub struct RefitAcc {
24678 pub support: Vec<u32>,
24679 pub gss: Vec<f32>,
24680 pub ya: Vec<f32>,
24681 pub hidden: usize,
24682 pub tokens: u64,
24683 pub buf_g: Vec<f32>,
24689 pub buf_o: Vec<f32>,
24690 pub buf_t: usize,
24691}
24692
24693type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
24697
24698static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
24699 std::sync::OnceLock::new();
24700
24701fn ffn_probe_active() -> bool {
24704 FFN_PROBE.with(|p| p.borrow().is_some())
24705}
24706
24707fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
24708 REFIT
24709 .get_or_init(|| {
24710 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
24711 (
24712 d,
24713 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
24714 )
24715 })
24716 })
24717 .as_ref()
24718}
24719
24720fn refit_accumulate(
24722 li: usize,
24723 g: &[f32],
24724 b: usize,
24725 inter: usize,
24726 out: &[f32],
24727 hidden: usize,
24728 pool: Option<&Pool>,
24729) {
24730 let Some((dir, map)) = refit_dir() else {
24731 return;
24732 };
24733 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
24734 let (from, to) = *SPAN.get_or_init(|| {
24735 let g = |k: &str, d: usize| {
24736 std::env::var(k)
24737 .ok()
24738 .and_then(|v| v.parse().ok())
24739 .unwrap_or(d)
24740 };
24741 (
24742 g("CMF_FFN_REFIT_FROM", 0),
24743 g("CMF_FFN_REFIT_TO", usize::MAX),
24744 )
24745 });
24746 if li < from || li > to {
24747 return;
24748 }
24749 let mut guard = map.lock().unwrap();
24750 let (map, shared) = &mut *guard;
24751 let acc = match map.entry(li) {
24752 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
24753 std::collections::hash_map::Entry::Vacant(e) => {
24754 let path = format!("{dir}/support.{li}.u32");
24755 let Ok(bytes) = std::fs::read(&path) else {
24756 eprintln!("refit: no {path} — layer {li} skipped");
24757 return;
24758 };
24759 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
24760 let support: Vec<u32> = bytes[4..4 + n * 4]
24761 .chunks_exact(4)
24762 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
24763 .collect();
24764 eprintln!(
24765 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
24766 (n * n + hidden * n) as f64 * 4.0 / 1e6
24767 );
24768 e.insert(RefitAcc {
24769 gss: vec![0.0; n * n],
24770 ya: vec![0.0; hidden * n],
24771 buf_g: Vec::new(),
24772 buf_o: Vec::new(),
24773 buf_t: 0,
24774 support,
24775 hidden,
24776 tokens: 0,
24777 })
24778 }
24779 };
24780 let ns = acc.support.len();
24781 let cap = refit_batch();
24783 if acc.buf_g.is_empty() {
24784 acc.buf_g = vec![0.0; ns * cap];
24785 acc.buf_o = vec![0.0; hidden * cap];
24786 }
24787 let take = b.min(cap - acc.buf_t);
24788 for t in 0..take {
24789 let col = acc.buf_t + t;
24790 for (j, &n) in acc.support.iter().enumerate() {
24791 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
24792 }
24793 for h in 0..hidden {
24794 acc.buf_o[h * cap + col] = out[t * hidden + h];
24795 }
24796 }
24797 acc.buf_t += take;
24798 acc.tokens += take as u64;
24799 if acc.buf_t < cap {
24800 return;
24801 }
24802 let bt = acc.buf_t;
24803 acc.buf_t = 0;
24804 let RefitAcc {
24814 gss,
24815 ya,
24816 buf_g,
24817 buf_o,
24818 ..
24819 } = acc;
24820 let need = (ns * ns).max(hidden * ns);
24821 if shared.len() < need {
24822 shared.resize(need, 0.0);
24823 }
24824 let scratch = &mut shared[..];
24825 let _ = bt;
24826 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
24827 add_into(gss, &scratch[..ns * ns], pool);
24828 if crate::gpu::gemm_nt_f32_transient(
24829 buf_o,
24830 buf_g,
24831 &mut scratch[..hidden * ns],
24832 hidden,
24833 cap,
24834 ns,
24835 ) {
24836 add_into(ya, &scratch[..hidden * ns], pool);
24837 } else {
24838 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
24839 }
24840 } else {
24841 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
24842 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
24843 }
24844 }
24848
24849fn refit_batch() -> usize {
24851 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
24852 *B.get_or_init(|| {
24853 std::env::var("CMF_FFN_REFIT_BATCH")
24854 .ok()
24855 .and_then(|v| v.parse().ok())
24856 .unwrap_or(4096)
24857 })
24858}
24859
24860fn accum_outer_t(
24863 c: &mut [f32],
24864 m: usize,
24865 n: usize,
24866 b: usize,
24867 left: &[f32],
24868 right: &[f32],
24869 pool: Option<&Pool>,
24870) {
24871 let ptr = SendMut(c.as_mut_ptr());
24872 let body = |i: usize| {
24873 let ptr = &ptr;
24874 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
24875 for t in 0..b {
24876 let a = left[i * b + t];
24877 if a == 0.0 {
24878 continue;
24879 }
24880 for (j, o) in row.iter_mut().enumerate() {
24881 *o += a * right[j * b + t];
24882 }
24883 }
24884 };
24885 match pool {
24886 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
24887 for i in s..e {
24888 body(i);
24889 }
24890 }),
24891 _ => {
24892 for i in 0..m {
24893 body(i);
24894 }
24895 }
24896 }
24897}
24898
24899fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
24902 let n = dst.len().min(src.len());
24903 match pool {
24904 Some(p) if n >= 1 << 16 => {
24905 let ptr = SendMut(dst.as_mut_ptr());
24906 let f = |s: usize, e: usize| {
24907 let ptr = &ptr;
24908 for blk in s..e {
24909 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
24910 for i in a..b {
24911 unsafe { *ptr.0.add(i) += src[i] };
24912 }
24913 }
24914 };
24915 p.run_rows(n.div_ceil(4096), &f);
24916 }
24917 _ => {
24918 for (d, v) in dst.iter_mut().zip(&src[..n]) {
24919 *d += *v;
24920 }
24921 }
24922 }
24923}
24924
24925fn accum_outer(
24930 c: &mut [f32],
24931 m: usize,
24932 n: usize,
24933 b: usize,
24934 left: &[f32],
24935 right: &[f32],
24936 pool: Option<&Pool>,
24937) {
24938 const TILE: usize = 32;
24939 let tiles = m.div_ceil(TILE);
24940 let cp = SendMut(c.as_mut_ptr());
24941 let body = |ti: usize| {
24942 let cp = &cp;
24943 let i0 = ti * TILE;
24944 let i1 = (i0 + TILE).min(m);
24945 for t in 0..b {
24946 let r = &right[t * n..t * n + n];
24947 for i in i0..i1 {
24948 let a = left[i * b + t];
24949 if a == 0.0 {
24950 continue;
24951 }
24952 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
24954 for (o, v) in row.iter_mut().zip(r) {
24955 *o += a * *v;
24956 }
24957 }
24958 }
24959 };
24960 match pool {
24961 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
24962 for ti in s..e {
24963 body(ti);
24964 }
24965 }),
24966 _ => {
24967 for ti in 0..tiles {
24968 body(ti);
24969 }
24970 }
24971 }
24972}
24973
24974pub fn refit_flush() -> usize {
24976 let Some((dir, map)) = refit_dir() else {
24977 return 0;
24978 };
24979 let guard = map.lock().unwrap();
24980 let mut n = 0;
24981 for (li, acc) in guard.0.iter() {
24982 let w = |name: &str, v: &[f32]| {
24985 let path = format!("{dir}/{name}.{li}.f32");
24986 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
24987 match std::fs::write(&path, &bytes) {
24988 Ok(()) => {}
24989 Err(e) => eprintln!(
24990 "refit: FAILED to write {path} ({} MB): {e}",
24991 bytes.len() / 1_000_000
24992 ),
24993 }
24994 };
24995 w("gss", &acc.gss);
24996 w("ya", &acc.ya);
24997 println!(
24998 "refit L{li}: {} support, {} tokens, hidden {}",
24999 acc.support.len(),
25000 acc.tokens,
25001 acc.hidden
25002 );
25003 n += 1;
25004 }
25005 n
25006}
25007
25008fn adump_row(li: usize, g: &[f32]) {
25013 use std::io::Write as _;
25014 static FILES: std::sync::OnceLock<
25015 Option<(
25016 String,
25017 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
25018 )>,
25019 > = std::sync::OnceLock::new();
25020 let Some((prefix, map)) = FILES
25021 .get_or_init(|| {
25022 std::env::var("CMF_FFN_ADUMP")
25023 .ok()
25024 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
25025 })
25026 .as_ref()
25027 else {
25028 return;
25029 };
25030 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
25033 let (from, to) = *SPAN.get_or_init(|| {
25034 let g = |k: &str, d: usize| {
25035 std::env::var(k)
25036 .ok()
25037 .and_then(|v| v.parse().ok())
25038 .unwrap_or(d)
25039 };
25040 (
25041 g("CMF_FFN_ADUMP_FROM", 0),
25042 g("CMF_FFN_ADUMP_TO", usize::MAX),
25043 )
25044 });
25045 if li < from || li > to {
25046 return;
25047 }
25048 let mut map = map.lock().unwrap();
25049 let f = map.entry(li).or_insert_with(|| {
25050 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
25051 });
25052 let mut bytes = Vec::with_capacity(g.len() * 2);
25053 for v in g {
25054 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
25055 }
25056 let _ = f.write_all(&bytes);
25057}
25058
25059fn oracle_topk() -> usize {
25065 if exact_mtp_calibration_active() {
25066 return 0;
25067 }
25068 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25069 *K.get_or_init(|| {
25070 std::env::var("CMF_FFN_ORACLE_TOPK")
25071 .ok()
25072 .and_then(|v| v.parse().ok())
25073 .unwrap_or(0)
25074 })
25075}
25076
25077fn gate_topk() -> usize {
25083 if exact_mtp_calibration_active() {
25084 return 0;
25085 }
25086 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25087 *K.get_or_init(|| {
25088 std::env::var("CMF_FFN_GATE_TOPK")
25089 .ok()
25090 .and_then(|v| v.parse().ok())
25091 .unwrap_or(0)
25092 })
25093}
25094
25095fn gate_block() -> usize {
25102 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25103 *B.get_or_init(|| {
25104 std::env::var("CMF_FFN_GATE_BLOCK")
25105 .ok()
25106 .and_then(|v| v.parse().ok())
25107 .unwrap_or(1)
25108 })
25109}
25110
25111fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
25113 let n = g.len();
25114 let nb = n.div_ceil(block);
25115 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
25116 if kb >= nb {
25117 return;
25118 }
25119 let mut score: Vec<f32> = (0..nb)
25120 .map(|b| {
25121 g[b * block..((b + 1) * block).min(n)]
25122 .iter()
25123 .map(|v| v * v)
25124 .sum::<f32>()
25125 })
25126 .collect();
25127 let mut ord = score.clone();
25128 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
25129 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25130 });
25131 let thr = *kth;
25132 for b in 0..nb {
25133 if score[b] < thr {
25134 g[b * block..((b + 1) * block).min(n)].fill(0.0);
25135 }
25136 }
25137 score.clear();
25138}
25139
25140fn keep_top_k(g: &mut [f32], k: usize) {
25142 if gate_block() > 1 {
25143 return keep_top_blocks(g, k, gate_block());
25144 }
25145 let n = g.len();
25146 if k == 0 || k >= n {
25147 return;
25148 }
25149 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
25150 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
25151 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25152 });
25153 let thr = *kth;
25154 for v in g.iter_mut() {
25155 if v.abs() < thr {
25156 *v = 0.0;
25157 }
25158 }
25159}
25160
25161fn probe_sq() -> bool {
25165 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
25166 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
25167}
25168
25169fn probe_signed() -> bool {
25173 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
25174 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
25175}
25176
25177fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
25185 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
25186 M.get_or_init(|| {
25187 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
25188 let b = std::fs::read(&p).ok()?;
25189 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
25190 let vals: Vec<f32> = b[8..]
25191 .chunks_exact(4)
25192 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
25193 .collect();
25194 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
25195 Some((inter, vals))
25196 })
25197 .as_ref()
25198}
25199
25200fn probe_topk() -> usize {
25203 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
25204 *K.get_or_init(|| {
25205 std::env::var("CMF_FFN_PROBE_TOPK")
25206 .ok()
25207 .and_then(|v| v.parse().ok())
25208 .unwrap_or(0)
25209 })
25210}
25211
25212thread_local! {
25213 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
25216 const { std::cell::RefCell::new(None) };
25217}
25218
25219pub(crate) struct MoeProbe {
25223 acc: Vec<Vec<f64>>,
25225 inter: usize,
25226 cur: Option<(usize, f32)>,
25229}
25230
25231thread_local! {
25232 static MOE_PROBE: std::cell::RefCell<Option<MoeProbe>> = const { std::cell::RefCell::new(None) };
25234}
25235
25236fn moe_probe_active() -> bool {
25237 MOE_PROBE.with(|p| p.borrow().is_some())
25238}
25239
25240fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
25253 if d.gate_proj.has_prism_contract()
25258 || d.up_proj.has_prism_contract()
25259 || d.down_proj.has_prism_contract()
25260 {
25261 return None;
25262 }
25263 let dt = d.down_t.as_ref()?;
25264 let inter = d.gate_proj.rows();
25265 let hidden = dt.cols();
25266 if k == 0 || k >= inter || d.act != Act::Silu {
25267 return None;
25268 }
25269 DYN_SCRATCH.with(|sc| {
25270 let mut sc = sc.borrow_mut();
25271 let DynScratch {
25272 g,
25273 mag,
25274 live,
25275 parts,
25276 } = &mut *sc;
25277 g.resize(inter, 0.0);
25278 d.gate_proj.matvec(x, g, pool);
25279 for v in g.iter_mut() {
25280 *v = inference::silu(*v);
25281 }
25282 mag.clear();
25285 mag.extend(g.iter().map(|v| v.abs()));
25286 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
25287 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
25288 });
25289 let thr = *kth;
25290 live.clear();
25291 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
25292 let mut out = vec![0.0f32; hidden];
25293 match pool {
25294 Some(p) if live.len() >= 64 => {
25295 let nw = p.n_workers() + 1;
25296 parts.clear();
25297 parts.resize(nw * hidden, 0.0);
25298 let ptr = SendMut(parts.as_mut_ptr());
25299 let n = live.len();
25300 let live_ref: &[u32] = live;
25301 let g_ref: &[f32] = g;
25302 p.run(&|w, workers| {
25303 let chunk = n.div_ceil(workers);
25304 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
25305 if s >= e {
25306 return;
25307 }
25308 WORKER_SCRATCH.with(|ws| {
25309 let mut ws = ws.borrow_mut();
25310 let [scratch, acc] = &mut *ws;
25311 scratch.resize(hidden.max(x.len()), 0.0);
25312 acc.clear();
25313 acc.resize(hidden, 0.0);
25314 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
25315 if let Some(&nx) = live_ref[s..e].get(o + 1) {
25318 d.up_proj.prefetch_row(nx as usize);
25319 dt.prefetch_row(nx as usize);
25320 }
25321 let idx = nrm as usize;
25322 let up = d.up_proj.row_dot(idx, x, scratch);
25323 let a = g_ref[idx] * up;
25324 if a != 0.0 {
25325 dt.add_row_scaled(idx, a, acc, scratch);
25326 }
25327 }
25328 for (j, v) in acc.iter().enumerate() {
25329 unsafe { *ptr.at(w * hidden + j) = *v };
25330 }
25331 });
25332 });
25333 for w in 0..nw {
25334 for (j, o) in out.iter_mut().enumerate() {
25335 *o += parts[w * hidden + j];
25336 }
25337 }
25338 }
25339 _ => {
25340 WORKER_SCRATCH.with(|ws| {
25341 let mut ws = ws.borrow_mut();
25342 let [scratch, _acc] = &mut *ws;
25343 scratch.resize(hidden.max(x.len()), 0.0);
25344 for &nrm in live.iter() {
25345 let idx = nrm as usize;
25346 let up = d.up_proj.row_dot(idx, x, scratch);
25347 let a = g[idx] * up;
25348 if a != 0.0 {
25349 dt.add_row_scaled(idx, a, &mut out, scratch);
25350 }
25351 }
25352 });
25353 }
25354 }
25355 Some(out)
25356 })
25357}
25358
25359struct DynScratch {
25362 g: Vec<f32>,
25363 mag: Vec<f32>,
25364 live: Vec<u32>,
25365 parts: Vec<f32>,
25366}
25367
25368thread_local! {
25369 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
25370 std::cell::RefCell::new(DynScratch {
25371 g: Vec::new(),
25372 mag: Vec::new(),
25373 live: Vec::new(),
25374 parts: Vec::new(),
25375 })
25376 };
25377 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
25379 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
25380}
25381
25382fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
25387 let inter = d.gate_proj.rows();
25388 FFN_SCRATCH.with(|s| {
25389 let mut s = s.borrow_mut();
25390 let [g, u, ..] = &mut *s;
25391 g.resize(inter, 0.0);
25392 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
25393 } else {
25395 u.resize(inter, 0.0);
25396 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
25397 for i in 0..inter {
25398 g[i] = d.act.combine(g[i], u[i]);
25399 }
25400 }
25401 zero_masked_cols(g, 1, inter, mask_row);
25402 let mut out = attention::take_buf(d.down_proj.rows());
25403 d.down_proj.matvec(g, &mut out, pool);
25404 out
25405 })
25406}
25407
25408fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
25414 if d.gate_proj.has_prism_contract()
25415 || d.up_proj.has_prism_contract()
25416 || d.down_proj.has_prism_contract()
25417 {
25418 return None;
25419 }
25420 d.act.moe_job_graph_contract()?;
25423 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
25426 return None;
25427 }
25428 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
25429 let mut model_ref = None;
25430 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
25431 let model = model_ref?;
25432 let hidden = jobs[0].down.1;
25433 let mut out = attention::take_buf(hidden);
25434 if crate::gpu::moe_block(&model, &jobs, &mut out) {
25435 Some(out)
25436 } else {
25437 let mut out = out;
25438 attention::recycle_buf(&mut out);
25439 None
25440 }
25441}
25442
25443#[allow(clippy::type_complexity)]
25448#[allow(clippy::type_complexity)]
25449pub(crate) fn moe_parts(
25450 t: &QTensor,
25451) -> Option<(
25452 &std::sync::Arc<cortiq_core::CmfModel>,
25453 usize,
25454 usize,
25455 usize,
25456 &[f32],
25457 &[f32],
25458 bool,
25459 bool,
25460 bool,
25461)> {
25462 match t {
25463 QTensor::Mapped {
25464 model,
25465 idx,
25466 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
25467 rows,
25468 cols,
25469 row_scale,
25470 col_field,
25471 ..
25472 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
25473 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
25474 )),
25475 QTensor::Mapped {
25477 model,
25478 idx,
25479 dtype: cortiq_core::TensorDtype::Q1,
25480 rows,
25481 cols,
25482 ..
25483 } => Some((
25484 model,
25485 *idx,
25486 *rows,
25487 *cols,
25488 &[][..],
25489 &[][..],
25490 true,
25491 false,
25492 false,
25493 )),
25494 QTensor::Mapped {
25496 model,
25497 idx,
25498 dtype: cortiq_core::TensorDtype::Q4Tiled,
25499 rows,
25500 cols,
25501 ..
25502 } => Some((
25503 model,
25504 *idx,
25505 *rows,
25506 *cols,
25507 &[][..],
25508 &[][..],
25509 false,
25510 true,
25511 false,
25512 )),
25513 QTensor::Mapped {
25515 model,
25516 idx,
25517 dtype: cortiq_core::TensorDtype::Q4TiledP,
25518 rows,
25519 cols,
25520 ..
25521 } => Some((
25522 model,
25523 *idx,
25524 *rows,
25525 *cols,
25526 &[][..],
25527 &[][..],
25528 false,
25529 true,
25530 false,
25531 )),
25532 QTensor::Mapped {
25536 model,
25537 idx,
25538 dtype: cortiq_core::TensorDtype::Q2TiledP,
25539 rows,
25540 cols,
25541 ..
25542 } => Some((
25543 model,
25544 *idx,
25545 *rows,
25546 *cols,
25547 &[][..],
25548 &[][..],
25549 false,
25550 true,
25551 true,
25552 )),
25553 _ => None,
25554 }
25555}
25556
25557#[cfg(target_os = "macos")]
25564#[allow(clippy::type_complexity)]
25565fn chunk_moe_parts(
25566 m: &MoeFfn,
25567 hidden: usize,
25568) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
25569 if m.shared.as_ref().is_some_and(|(s, g)| {
25572 g.is_some()
25573 || m.experts.is_empty()
25574 || s.gate_proj.rows() == m.experts[0].gate_proj.rows()
25575 || !(s.gate_proj.q8_2f_parts().is_some() || s.gate_proj.q4tp_parts().is_some())
25576 || !(s.up_proj.q8_2f_parts().is_some() || s.up_proj.q4tp_parts().is_some())
25577 || !(s.down_proj.q8_2f_parts().is_some() || s.down_proj.q4tp_parts().is_some())
25578 }) || m.per_expert_scale.is_some()
25579 || m.resonance.is_some()
25580 || m.route_tau.is_some()
25581 || m.router_input_norm
25582 || m.experts.is_empty()
25583 || m.top_k == 0
25584 || std::env::var("CMF_RMS_TRACE").is_ok()
25585 || std::env::var("CMF_ACT_DUMP").is_ok()
25586 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
25587 {
25588 return None;
25589 }
25590 let (rf, rr, rc) = m.router.f32_parts()?;
25591 if rr != m.experts.len() || rc != hidden {
25592 return None;
25593 }
25594 let inter = m.experts[0].gate_proj.rows();
25595 let q8 = crate::gpu_metal::moe_q8::enabled() && m.experts[0].gate_proj.q8_2f_parts().is_some();
25598 let experts = m
25599 .experts
25600 .iter()
25601 .map(|e| {
25602 if e.act != Act::Silu
25603 || !e.segs.is_empty()
25604 || e.gate_proj.rows() != inter
25605 || e.up_proj.rows() != inter
25606 || e.down_proj.cols() != inter
25607 || e.gate_proj.cols() != hidden
25608 || e.down_proj.rows() != hidden
25609 {
25610 return None;
25611 }
25612 if q8 {
25613 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
25614 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
25615 }
25616 Some((
25617 e.gate_proj.mapped_q4tp()?.1,
25618 e.up_proj.mapped_q4tp()?.1,
25619 e.down_proj.mapped_q4tp()?.1,
25620 ))
25621 })
25622 .collect::<Option<Vec<_>>>()?;
25623 Some((rf, experts, q8))
25624}
25625
25626#[cfg(target_os = "macos")]
25639fn metal_flint_moe_graph_parts(
25640 m: &MoeFfn,
25641 hidden: usize,
25642 scratch_key: usize,
25643) -> Option<crate::gpu::GpuFlintMoe<'_>> {
25644 use std::sync::Arc;
25645
25646 const HIDDEN: usize = 2_048;
25647 const ROUTED_INTER: usize = 512;
25648 const SHARED_INTER: usize = 1_024;
25649 const EXPERTS: usize = 256;
25650 const TOP_K: usize = 6;
25651
25652 if hidden != HIDDEN || !strict_flint_metal_pager_candidate(m) {
25653 return None;
25654 }
25655 let routed0 = m.experts.first()?;
25656 let (model, _) = routed0.gate_proj.mapped_q2tp()?;
25657 let (router, router_rows, router_cols) = m.router.f32_parts()?;
25658 if router_rows != EXPERTS || router_cols != HIDDEN {
25659 return None;
25660 }
25661 if m.expert_bias.as_ref().is_some_and(|b| b.len() != EXPERTS) {
25662 return None;
25663 }
25664 let same_q2 = |t: &QTensor| -> Option<usize> {
25665 let (owner, idx) = t.mapped_q2tp()?;
25666 Arc::ptr_eq(&owner, &model).then_some(idx)
25667 };
25668 let same_q4 = |t: &QTensor| -> Option<usize> {
25669 let (owner, idx) = t.mapped_q4tp()?;
25670 Arc::ptr_eq(&owner, &model).then_some(idx)
25671 };
25672 let routed = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25673 if e.act != Act::Silu
25674 || !e.segs.is_empty()
25675 || e.gate_proj.rows() != ROUTED_INTER
25676 || e.gate_proj.cols() != HIDDEN
25677 || e.up_proj.rows() != ROUTED_INTER
25678 || e.up_proj.cols() != HIDDEN
25679 || e.down_proj.rows() != HIDDEN
25680 || e.down_proj.cols() != ROUTED_INTER
25681 {
25682 return None;
25683 }
25684 Some((
25685 same_q2(&e.gate_proj)?,
25686 same_q2(&e.up_proj)?,
25687 same_q4(&e.down_proj)?,
25688 ))
25689 };
25690 let shared = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25691 if e.act != Act::Silu
25692 || !e.segs.is_empty()
25693 || e.gate_proj.rows() != SHARED_INTER
25694 || e.gate_proj.cols() != HIDDEN
25695 || e.up_proj.rows() != SHARED_INTER
25696 || e.up_proj.cols() != HIDDEN
25697 || e.down_proj.rows() != HIDDEN
25698 || e.down_proj.cols() != SHARED_INTER
25699 {
25700 return None;
25701 }
25702 Some((
25703 same_q4(&e.gate_proj)?,
25704 same_q4(&e.up_proj)?,
25705 same_q4(&e.down_proj)?,
25706 ))
25707 };
25708 let experts = m.experts.iter().map(routed).collect::<Option<Vec<_>>>()?;
25709 if experts.len() != EXPERTS {
25710 return None;
25711 }
25712 let (shared_ffn, shared_gate) = m.shared.as_ref()?;
25713 if shared_gate.is_some() {
25714 return None;
25715 }
25716 Some(crate::gpu::GpuFlintMoe {
25717 router,
25718 bias: m.expert_bias.as_deref(),
25719 experts,
25720 scratch_key,
25721 shared: shared(shared_ffn)?,
25722 top_k: m.top_k,
25723 norm_topk: m.norm_topk_prob,
25724 route_scale: m.routed_scaling,
25725 sigmoid: m.router_sigmoid,
25726 sigmoid_denom_eps: m.sigmoid_denom_eps,
25727 })
25728}
25729
25730#[cfg(target_os = "macos")]
25731fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
25732 if m.router_input_norm
25733 || m.route_tau.is_some()
25734 || m.mask.is_some()
25735 || m.per_expert_scale.is_some()
25736 || m.experts.is_empty()
25737 || m.top_k == 0
25738 || m.resonance.is_some()
25739 {
25740 return None;
25741 }
25742 let detached = m.shared.as_ref().filter(|(s, g)| {
25745 g.is_none() && !m.router_sigmoid && s.gate_proj.rows() != m.experts[0].gate_proj.rows()
25746 });
25747 let (sh, sg) = match &m.shared {
25752 Some((sh, sg)) if detached.is_none() => (Some(sh), sg.as_ref()),
25753 _ if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
25754 _ => return None,
25755 };
25756 let (rf, rr, rc) = m.router.f32_parts()?;
25757 if rr != m.experts.len() || rc != hidden {
25758 return None;
25759 }
25760 let shared_gated = sg.is_some();
25761 let sf = match sg {
25762 Some(sg) => {
25763 let (sf, sr, sc) = sg.f32_parts()?;
25764 if sr * sc != hidden {
25765 return None;
25766 }
25767 sf
25768 }
25769 None => &rf[..hidden],
25772 };
25773 if let Some(b) = &m.expert_bias {
25774 if b.len() != m.experts.len() {
25775 return None;
25776 }
25777 }
25778 let inter = m.experts[0].gate_proj.rows();
25779 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
25782 let q8 = crate::gpu_metal::moe_q8::enabled()
25786 && (m.shared.is_none() || detached.is_some())
25787 && m.experts[0].gate_proj.q8_2f_parts().is_some();
25788 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
25789 if e.act != Act::Silu
25790 || e.gate_proj.rows() != inter
25791 || e.gate_proj.cols() != hidden
25792 || e.up_proj.rows() != inter
25793 || e.up_proj.cols() != hidden
25794 || e.down_proj.rows() != hidden
25795 || e.down_proj.cols() != inter
25796 {
25797 return None;
25798 }
25799 if q8 {
25800 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
25801 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
25802 }
25803 let pick = |t: &QTensor| -> Option<usize> {
25804 if gu_q2 {
25805 t.mapped_q2tp().map(|(_, i)| i)
25806 } else {
25807 t.mapped_q4tp().map(|(_, i)| i)
25808 }
25809 };
25810 Some((
25811 pick(&e.gate_proj)?,
25812 pick(&e.up_proj)?,
25813 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
25814 ))
25815 };
25816 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
25817 let shared = match sh {
25818 Some(sh) => Some(trio(sh)?),
25819 None => None,
25820 };
25821 let detached = match detached {
25822 Some((s, _)) => Some([
25823 s.gate_proj.metal_graph_parts()?,
25824 s.up_proj.metal_graph_parts()?,
25825 s.down_proj.metal_graph_parts()?,
25826 ]),
25827 None => None,
25828 };
25829 Some(crate::gpu::GpuMoe {
25830 router: rf,
25831 sgate: sf,
25832 experts,
25833 shared,
25834 n_exp: m.experts.len(),
25835 top_k: m.top_k,
25836 inter,
25837 norm_topk: m.norm_topk_prob,
25838 route_scale: m.routed_scaling,
25839 gu_q2,
25840 q8,
25841 sigmoid: m.router_sigmoid,
25842 sigmoid_denom_eps: m.sigmoid_denom_eps,
25843 bias: m.expert_bias.as_deref(),
25844 shared_gated,
25845 detached,
25846 })
25847}
25848
25849fn forced_ids_at(step: usize) -> Option<u32> {
25855 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
25856 IDS.get_or_init(|| {
25857 std::env::var("CMF_FORCE_IDS")
25858 .ok()
25859 .and_then(|p| std::fs::read_to_string(p).ok())
25860 .map(|s| {
25861 s.split_whitespace()
25862 .filter_map(|t| t.parse().ok())
25863 .collect()
25864 })
25865 .unwrap_or_default()
25866 })
25867 .get(step)
25868 .copied()
25869}
25870
25871pub(crate) fn moe_push_job_parts<'a>(
25875 gate: &'a QTensor,
25876 up: &'a QTensor,
25877 down: &'a QTensor,
25878 x: &[f32],
25879 w: f32,
25880 swiglu_limit: f32,
25881 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
25882 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
25883) -> Option<()> {
25884 use crate::qtensor::prescale;
25885 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
25886 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
25887 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
25888 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
25889 return None; }
25891 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
25894 return None;
25895 }
25896 if !gq2 && dq2 {
25897 return None;
25898 }
25899 model_ref.get_or_insert_with(|| gm.clone());
25900 let dt = |cf: &[f32]| {
25901 if cf.is_empty() {
25902 cortiq_core::TensorDtype::Q8Row
25903 } else {
25904 cortiq_core::TensorDtype::Q8_2f
25905 }
25906 };
25907 jobs.push(crate::gpu::MoeJob {
25908 gate: (gi, gr, gc, grs),
25909 up: (ui, ur, uc, urs),
25910 down: (di, dr, dc, drs),
25911 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
25912 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
25913 down_col: dcf,
25914 w,
25915 q1: gq1,
25916 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
25917 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
25918 gu_q2: gq2,
25919 swiglu_limit,
25920 act: 0,
25921 });
25922 Some(())
25923}
25924
25925fn moe_push_job<'a>(
25927 d: &'a DenseFfn,
25928 x: &[f32],
25929 w: f32,
25930 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
25931 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
25932) -> Option<()> {
25933 use crate::qtensor::prescale;
25934 let (graph_act, swiglu_limit) = d.act.moe_job_graph_contract()?;
25939 let act = graph_act.code();
25940 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
25941 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
25942 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
25943 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
25944 return None; }
25946 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
25947 return None;
25948 }
25949 if !gq2 && dq2 {
25950 return None;
25951 }
25952 model_ref.get_or_insert_with(|| gm.clone());
25953 let gdt = if gcf.is_empty() {
25954 cortiq_core::TensorDtype::Q8Row
25955 } else {
25956 cortiq_core::TensorDtype::Q8_2f
25957 };
25958 let udt = if ucf.is_empty() {
25959 cortiq_core::TensorDtype::Q8Row
25960 } else {
25961 cortiq_core::TensorDtype::Q8_2f
25962 };
25963 jobs.push(crate::gpu::MoeJob {
25964 gate: (gi, gr, gc, grs),
25965 up: (ui, ur, uc, urs),
25966 down: (di, dr, dc, drs),
25967 xs_gate: prescale(x, gcf, gdt).into_owned(),
25968 xs_up: prescale(x, ucf, udt).into_owned(),
25969 down_col: dcf,
25970 w,
25971 q1: gq1,
25972 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
25973 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
25974 gu_q2: gq2,
25975 swiglu_limit,
25976 act,
25977 });
25978 Some(())
25979}
25980
25981fn sparse_ffn_quant(
25988 d: &DenseFfn,
25989 x: &[f32],
25990 active: &[u16],
25991 hidden: usize,
25992 pool: Option<&Pool>,
25993) -> Vec<f32> {
25994 let n = active.len();
25995 let inter = d.gate_proj.rows();
25996 let mut act = vec![0.0f32; n];
25997 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
26000 let compute = |ai: usize| -> f32 {
26001 let idx = active[ai] as usize;
26002 if idx >= inter {
26003 return 0.0; }
26005 let mut s = if need_scratch {
26006 vec![0.0f32; hidden]
26007 } else {
26008 Vec::new()
26009 };
26010 let gate = d.gate_proj.row_dot(idx, x, &mut s);
26011 let up = d.up_proj.row_dot(idx, x, &mut s);
26012 d.act.combine(gate, up)
26013 };
26014 match pool {
26015 Some(p) if n >= 256 => {
26016 let ptr = SendMut(act.as_mut_ptr());
26017 p.run(&|widx, nw| {
26018 let chunk = n.div_ceil(nw);
26019 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
26020 for ai in s..e {
26021 unsafe { *ptr.at(ai) = compute(ai) };
26022 }
26023 });
26024 }
26025 _ => {
26026 for (ai, a) in act.iter_mut().enumerate() {
26027 *a = compute(ai);
26028 }
26029 }
26030 }
26031 let mut out = vec![0.0f32; hidden];
26033 for (ai, &idx) in active.iter().enumerate() {
26034 let w = act[ai];
26035 if w.abs() >= 1e-12 && (idx as usize) < inter {
26036 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
26037 }
26038 }
26039 out
26040}
26041
26042#[doc(hidden)]
26044pub fn sparse_ffn_quant_for_test(
26045 d: &DenseFfn,
26046 x: &[f32],
26047 active: &[u16],
26048 hidden: usize,
26049) -> Vec<f32> {
26050 sparse_ffn_quant(d, x, active, hidden, None)
26051}
26052
26053fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
26057 let deq = |t: &QTensor| -> Vec<f32> {
26058 let (rows, cols) = (t.rows(), t.cols());
26059 let mut out = vec![0.0f32; rows * cols];
26060 for r in 0..rows {
26061 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
26062 }
26063 out
26064 };
26065 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
26066}
26067
26068struct SendMut(*mut f32);
26070unsafe impl Send for SendMut {}
26071unsafe impl Sync for SendMut {}
26072impl SendMut {
26073 #[inline]
26074 #[allow(clippy::mut_from_ref)]
26077 unsafe fn at(&self, i: usize) -> &mut f32 {
26078 unsafe { &mut *self.0.add(i) }
26079 }
26080}
26081
26082pub(crate) fn moe_route(
26094 logits: &[f32],
26095 m: &MoeFfn,
26096 allowed: Option<&[bool]>,
26097) -> (Vec<usize>, Vec<f32>, f32) {
26098 moe_route_with_eps(logits, m, allowed, m.sigmoid_denom_eps)
26099}
26100
26101pub(crate) fn moe_route_with_eps(
26111 logits: &[f32],
26112 m: &MoeFfn,
26113 allowed: Option<&[bool]>,
26114 sigmoid_denom_eps: f32,
26115) -> (Vec<usize>, Vec<f32>, f32) {
26116 let ne = logits.len();
26117 let admit = |e: usize| {
26123 m.mask.as_ref().is_none_or(|mk| mk[e])
26124 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
26125 };
26126 if m.resonance.is_some() && m.top_k == 1 {
26139 let mut best: Option<usize> = None;
26140 for e in (0..ne).filter(|&e| admit(e)) {
26141 let l = logits[e];
26142 if l == f32::NEG_INFINITY || l.is_nan() {
26143 continue;
26144 }
26145 if best.is_none_or(|b| l > logits[b]) {
26146 best = Some(e);
26147 }
26148 }
26149 if let Some(b) = best {
26150 let mut p = vec![0.0f32; ne];
26151 p[b] = 1.0;
26152 return (vec![b], p, 1.0 / m.routed_scaling);
26153 }
26154 }
26155 let p: Vec<f32> = if m.router_sigmoid {
26161 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
26162 } else {
26163 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
26164 if mx == f32::NEG_INFINITY {
26165 vec![1.0 / ne.max(1) as f32; ne]
26166 } else {
26167 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
26168 let s: f32 = e.iter().sum();
26169 for v in &mut e {
26170 *v /= s;
26171 }
26172 e
26173 }
26174 };
26175 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
26176 match &m.expert_bias {
26178 Some(b) => idx.sort_unstable_by(|&x, &y| {
26179 (p[y] + b[y])
26180 .partial_cmp(&(p[x] + b[x]))
26181 .unwrap()
26182 .then(x.cmp(&y))
26183 }),
26184 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
26185 }
26186 idx.truncate(m.top_k);
26187 if let Some(tau) = m.route_tau {
26191 let total: f32 = idx.iter().map(|&e| p[e]).sum();
26192 if total > 0.0 {
26193 let mut acc = 0.0f32;
26194 let mut keep = idx.len();
26195 for (i, &e) in idx.iter().enumerate() {
26196 acc += p[e];
26197 if acc >= tau * total {
26198 keep = i + 1;
26199 break;
26200 }
26201 }
26202 idx.truncate(keep);
26203 }
26204 }
26205 let wsum: f32 = if m.norm_topk_prob {
26206 let s: f32 = idx.iter().map(|&e| p[e]).sum();
26207 (if m.router_sigmoid {
26211 s + sigmoid_denom_eps
26212 } else {
26213 s
26214 }) / m.routed_scaling
26215 } else {
26216 1.0 / m.routed_scaling
26217 };
26218 (idx, p, wsum)
26219}
26220
26221fn moe_trace(idx: &[usize]) {
26223 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
26224}
26225
26226pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
26229 use std::io::Write;
26230 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
26231 std::sync::OnceLock::new();
26232 let Some(f) = F.get_or_init(|| {
26233 let p = std::env::var("CMF_MOE_TRACE").ok()?;
26234 Some(std::sync::Mutex::new(
26235 std::fs::OpenOptions::new()
26236 .create(true)
26237 .append(true)
26238 .open(p)
26239 .ok()?,
26240 ))
26241 }) else {
26242 return;
26243 };
26244 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
26245 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
26246}
26247
26248pub(crate) fn moe_ffn(
26251 m: &MoeFfn,
26252 x: &[f32],
26253 pool: Option<&Pool>,
26254 allowed: Option<&[bool]>,
26255) -> Vec<f32> {
26256 let r = moe_ffn_route(m, x, pool, allowed);
26257 moe_ffn_experts(m, x, &r, pool)
26258}
26259
26260pub(crate) struct MoeRoute {
26264 pub idx: Vec<usize>,
26265 pub p: Vec<f32>,
26266 pub wsum: f32,
26267 pub logits: Vec<f32>,
26268}
26269
26270pub(crate) fn moe_ffn_route(
26276 m: &MoeFfn,
26277 x: &[f32],
26278 pool: Option<&Pool>,
26279 allowed: Option<&[bool]>,
26280) -> MoeRoute {
26281 accumulate_act(m, x, 1);
26282 let ne = m.experts.len();
26283 let mut logits = vec![0.0f32; ne];
26284 match &m.resonance {
26285 Some(r) => r.scores(x, &mut logits),
26286 None => m.router.matvec(x, &mut logits, pool),
26287 }
26288 let (idx, p, wsum) = moe_route(&logits, m, allowed);
26289 {
26290 let mut st = m.stats.borrow_mut();
26291 if st.len() < ne {
26292 st.resize(ne, 0);
26293 }
26294 for &e in &idx {
26295 st[e] += 1;
26296 }
26297 }
26298 moe_trace(&idx);
26304 MoeRoute {
26305 idx,
26306 p,
26307 wsum,
26308 logits,
26309 }
26310}
26311
26312#[cfg(target_os = "macos")]
26321fn strict_flint_metal_pager_candidate(m: &MoeFfn) -> bool {
26322 const HIDDEN: usize = 2_048;
26323 const ROUTED_INTER: usize = 512;
26324 const SHARED_INTER: usize = 1_024;
26325 const EXPERTS: usize = 256;
26326 const TOP_K: usize = 6;
26327
26328 let Some(routed) = m.experts.first() else {
26329 return false;
26330 };
26331 let Some((model, _)) = routed.gate_proj.mapped_q2tp() else {
26332 return false;
26333 };
26334 let Some((shared, shared_gate)) = &m.shared else {
26335 return false;
26336 };
26337 if shared_gate.is_some()
26338 || model.header.arch.arch_name != "flint"
26339 || model.header.arch.prism_hadamard.is_some()
26340 || model.header.arch.hidden_size != HIDDEN
26341 || m.experts.len() != EXPERTS
26342 || m.top_k != TOP_K
26343 || !m.router_sigmoid
26344 || !m.norm_topk_prob
26345 || m.sigmoid_denom_eps.to_bits() != (1e-20f32).to_bits()
26346 || m.routed_scaling.to_bits() != 1.0f32.to_bits()
26347 || m.route_tau.is_some()
26348 || m.mask.is_some()
26349 || m.per_expert_scale.is_some()
26350 || m.router_input_norm
26351 || m.resonance.is_some()
26352 || !m.grown.is_empty()
26353 {
26354 return false;
26355 }
26356
26357 let routed_q2 = routed.act == Act::Silu
26358 && routed.segs.is_empty()
26359 && routed.gate_proj.rows() == ROUTED_INTER
26360 && routed.gate_proj.cols() == HIDDEN
26361 && routed.up_proj.rows() == ROUTED_INTER
26362 && routed.up_proj.cols() == HIDDEN
26363 && routed.down_proj.rows() == HIDDEN
26364 && routed.down_proj.cols() == ROUTED_INTER
26365 && routed.up_proj.mapped_q2tp().is_some()
26366 && routed.down_proj.mapped_q4tp().is_some();
26367 let shared_q4 = shared.act == Act::Silu
26368 && shared.segs.is_empty()
26369 && shared.gate_proj.rows() == SHARED_INTER
26370 && shared.gate_proj.cols() == HIDDEN
26371 && shared.up_proj.rows() == SHARED_INTER
26372 && shared.up_proj.cols() == HIDDEN
26373 && shared.down_proj.rows() == HIDDEN
26374 && shared.down_proj.cols() == SHARED_INTER
26375 && shared.gate_proj.mapped_q4tp().is_some()
26376 && shared.up_proj.mapped_q4tp().is_some()
26377 && shared.down_proj.mapped_q4tp().is_some();
26378 routed_q2 && shared_q4
26379}
26380
26381pub(crate) fn moe_ffn_experts(
26384 m: &MoeFfn,
26385 x: &[f32],
26386 r: &MoeRoute,
26387 pool: Option<&Pool>,
26388) -> Vec<f32> {
26389 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
26390 if crate::gpu::enabled_here() {
26395 #[cfg(target_os = "macos")]
26396 if crate::gpu_metal::enabled() && strict_flint_metal_pager_candidate(m) {
26397 return moe_ffn_gpu(m, x, &idx, &p, wsum, pool).unwrap_or_else(|| {
26403 crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool))
26404 });
26405 }
26406 match crate::gpu::probe_arm(crate::gpu::OpClass::Moe) {
26407 crate::gpu::ProbeArm::Gpu => {
26408 let t0 = std::time::Instant::now();
26409 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
26410 crate::gpu::probe_record(crate::gpu::OpClass::Moe, true, t0.elapsed());
26411 return out;
26412 }
26413 crate::gpu::probe_note_decline(crate::gpu::OpClass::Moe);
26414 }
26415 crate::gpu::ProbeArm::CpuTimed => {
26416 let t0 = std::time::Instant::now();
26417 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
26418 crate::gpu::probe_record(crate::gpu::OpClass::Moe, false, t0.elapsed());
26419 return out;
26420 }
26421 crate::gpu::ProbeArm::Cpu => {
26422 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
26423 }
26424 }
26425 }
26426 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
26427}
26428
26429fn moe_ffn_minicpm_tail(
26433 slot: &mut crate::minicpm_tail::MiniCpmTail,
26434 li: usize,
26435 total_layers: usize,
26436 m: &MoeFfn,
26437 x: &[f32],
26438 pool: Option<&Pool>,
26439) -> Vec<f32> {
26440 let route = moe_ffn_route(m, x, pool, None);
26441 match slot.forward(li, m, x, &route, pool, total_layers) {
26442 Some(out) => out,
26443 None => moe_ffn_experts(m, x, &route, pool),
26448 }
26449}
26450
26451fn moe_ffn_banked(
26455 slot: &mut crate::mimo_moe::Slot,
26456 li: usize,
26457 m: &MoeFfn,
26458 x: &[f32],
26459 pool: Option<&Pool>,
26460) -> Vec<f32> {
26461 let t0 = std::time::Instant::now();
26462 let r = moe_ffn_route(m, x, pool, None);
26463 slot.note_route(t0.elapsed().as_nanos() as u64);
26464 match slot.forward(li, m, x, &r, pool) {
26465 Some(out) => out,
26466 None => crate::qtensor::float_activations_scope(|| {
26467 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
26468 }),
26469 }
26470}
26471
26472fn moe_ffn_banked_rows(
26474 slot: &mut crate::mimo_moe::Slot,
26475 li: usize,
26476 m: &MoeFfn,
26477 xs: &[f32],
26478 b: usize,
26479 hidden: usize,
26480 pool: Option<&Pool>,
26481) -> Vec<f32> {
26482 let t0 = std::time::Instant::now();
26483 let routes: Vec<_> = xs
26484 .chunks_exact(hidden)
26485 .map(|x| moe_ffn_route(m, x, pool, None))
26486 .collect();
26487 slot.note_route(t0.elapsed().as_nanos() as u64);
26488 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
26489 return out;
26490 }
26491 let mut out = Vec::with_capacity(b * hidden);
26492 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
26493 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
26494 crate::qtensor::float_activations_scope(|| {
26496 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
26497 })
26498 });
26499 out.extend(row);
26500 }
26501 out
26502}
26503
26504fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
26509 use std::sync::atomic::{AtomicBool, Ordering};
26510 if built {
26511 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
26512 if total_layers > 0 && layers_run < total_layers {
26513 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
26514 } else {
26515 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
26516 }
26517 } else {
26518 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
26519 }
26520 static SAID: AtomicBool = AtomicBool::new(false);
26521 if !SAID.swap(true, Ordering::Relaxed) {
26522 if built {
26523 tracing::info!("wgpu whole-token graph: ACTIVE");
26524 } else {
26525 tracing::warn!("wgpu whole-token graph refused — per-op path");
26526 }
26527 }
26528}
26529
26530pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26534pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26535pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26539pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26541
26542pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26546pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26547pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
26548 std::sync::atomic::AtomicU64::new(0);
26549pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26550pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26551pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
26555 std::sync::atomic::AtomicU64::new(0);
26556pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26557pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
26558 std::sync::atomic::AtomicU64::new(0);
26559pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
26560 std::sync::atomic::AtomicU64::new(0);
26561
26562fn moe_batch_enabled() -> bool {
26565 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
26566 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
26567}
26568
26569fn moe_ffn_cpu_batched(
26575 m: &MoeFfn,
26576 x: &[f32],
26577 idx: &[usize],
26578 p: &[f32],
26579 wsum: f32,
26580 pool: Option<&Pool>,
26581) -> Option<Vec<f32>> {
26582 if idx.is_empty() || !moe_batch_enabled() {
26583 return None;
26584 }
26585 if FFN_PROBE.with(|pr| pr.borrow().is_some()) || moe_probe_active() {
26589 return None;
26590 }
26591 let n = idx.len() + usize::from(m.shared.is_some());
26592 let mut pairs = Vec::with_capacity(n);
26593 let mut downs = Vec::with_capacity(n);
26594 let mut ws = Vec::with_capacity(n);
26595 for &e in idx {
26596 let d = &m.experts[e];
26597 if d.act != Act::Silu {
26598 return None;
26599 }
26600 pairs.push((&d.gate_proj, &d.up_proj));
26601 downs.push(&d.down_proj);
26602 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
26603 }
26604 if let Some((se, gate)) = &m.shared {
26607 if se.act != Act::Silu {
26608 return None;
26609 }
26610 let g = gate.as_ref().map_or(1.0, |gate| {
26611 let mut gl = [0.0f32; 1];
26612 gate.matvec(x, &mut gl, pool);
26613 1.0 / (1.0 + (-gl[0]).exp())
26614 });
26615 pairs.push((&se.gate_proj, &se.up_proj));
26616 downs.push(&se.down_proj);
26617 ws.push(g);
26618 }
26619 let inter = pairs[0].0.rows();
26620 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
26621 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
26622 return None;
26623 }
26624 let mut out = attention::take_buf(x.len());
26625 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
26626 attention::recycle_buf(&mut out);
26627 return None;
26628 }
26629 Some(out)
26630}
26631
26632pub static M3_SPAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26640pub static M3_COLD_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26641pub static M3_ARM_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26642pub static M3_COLD_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26643pub static M3_COLD_DEV_N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
26644
26645pub(crate) fn moe_cold_experts_cpu(
26646 experts: &[(&DenseFfn, f32)],
26647 x: &[f32],
26648 pool: Option<&Pool>,
26649) -> Vec<f32> {
26650 moe_cold_experts_cpu_with(experts, x, pool, true)
26651}
26652
26653pub(crate) fn moe_cold_experts_cpu_with(
26656 experts: &[(&DenseFfn, f32)],
26657 x: &[f32],
26658 pool: Option<&Pool>,
26659 tubes: bool,
26660) -> Vec<f32> {
26661 let mut out = attention::take_buf(x.len());
26662 if experts.is_empty() {
26663 return out;
26664 }
26665 let pairs: Vec<_> = experts
26666 .iter()
26667 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
26668 .collect();
26669 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
26670 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
26671 let inter = experts[0].0.gate_proj.rows();
26672 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
26673 let silu_only = experts
26681 .iter()
26682 .all(|(e, _)| matches!(e.act, crate::pipeline::Act::Silu));
26683 if silu_only
26684 && QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
26685 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
26686 {
26687 if tubes {
26688 for &(expert, weight) in experts {
26689 expert_tubes_add(expert, x, weight, &mut out, pool);
26690 }
26691 }
26692 return out;
26693 }
26694 out.fill(0.0);
26695 for &(expert, weight) in experts {
26696 if tubes {
26697 expert_tubes_add(expert, x, weight, &mut out, pool);
26698 }
26699 let mut one = dense_ffn(expert, x, pool);
26700 for (o, v) in out.iter_mut().zip(&one) {
26701 *o += weight * v;
26702 }
26703 attention::recycle_buf(&mut one);
26704 }
26705 out
26706}
26707
26708pub(crate) fn moe_cold_experts_rows_cpu(
26712 jobs: &[Vec<(&DenseFfn, f32)>],
26713 xs: &[f32],
26714 hidden: usize,
26715 pool: Option<&Pool>,
26716) -> Vec<f32> {
26717 let mut out = vec![0.0; xs.len()];
26718 let mut experts: Vec<&DenseFfn> = Vec::new();
26719 let mut groups: Vec<Vec<usize>> = Vec::new();
26720 let mut terms = vec![Vec::new(); jobs.len()];
26721 for (r, row) in jobs.iter().enumerate() {
26722 for &(e, w) in row {
26723 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
26724 Some(g) => g,
26725 None => {
26726 experts.push(e);
26727 groups.push(Vec::new());
26728 groups.len() - 1
26729 }
26730 };
26731 terms[r].push((g, groups[g].len(), w));
26732 groups[g].push(r);
26733 }
26734 }
26735 if experts.is_empty() {
26736 return out;
26737 }
26738 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
26739 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
26740 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
26741 let count: usize = lens.iter().sum();
26742 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
26743 let mut ds = vec![vec![0.0; hidden]; count];
26744 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
26745 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
26746 {
26747 let mut offset = 0;
26748 let offsets: Vec<_> = lens
26749 .iter()
26750 .map(|&n| {
26751 let start = offset;
26752 offset += n;
26753 start
26754 })
26755 .collect();
26756 for (r, terms) in terms.iter().enumerate() {
26757 for &(g, slot, w) in terms {
26758 for (o, &v) in out[r * hidden..(r + 1) * hidden]
26759 .iter_mut()
26760 .zip(&ds[offsets[g] + slot])
26761 {
26762 *o += w * v;
26763 }
26764 }
26765 }
26766 } else {
26767 for (r, jobs) in jobs.iter().enumerate() {
26768 if !jobs.is_empty() {
26769 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
26770 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
26771 attention::recycle_buf(&mut row);
26772 }
26773 }
26774 }
26775 out
26776}
26777
26778fn moe_ffn_cpu(
26780 m: &MoeFfn,
26781 x: &[f32],
26782 idx: &[usize],
26783 p: &[f32],
26784 wsum: f32,
26785 pool: Option<&Pool>,
26786) -> Vec<f32> {
26787 if let Some(mut out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
26788 for &e in idx {
26789 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
26790 expert_tubes_add(&m.experts[e], x, w, &mut out, pool);
26791 }
26792 shared_tubes_add(m, x, &mut out, pool);
26793 return out;
26794 }
26795 let mut out = attention::take_buf(x.len());
26796 for &e in idx {
26797 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
26798 MOE_PROBE.with(|pr| {
26799 if let Some(pr) = pr.borrow_mut().as_mut() {
26800 pr.cur = Some((e, w));
26801 }
26802 });
26803 let mut eo = dense_ffn(&m.experts[e], x, pool);
26804 MOE_PROBE.with(|pr| {
26805 if let Some(pr) = pr.borrow_mut().as_mut() {
26806 pr.cur = None;
26807 }
26808 });
26809 for i in 0..out.len() {
26810 out[i] += w * eo[i];
26811 }
26812 attention::recycle_buf(&mut eo);
26813 expert_tubes_add(&m.experts[e], x, w, &mut out, pool);
26814 }
26815 if let Some((se, gate)) = &m.shared {
26816 let mut so = dense_ffn(se, x, pool);
26817 let g = gate.as_ref().map_or(1.0, |gate| {
26818 let mut gl = [0.0f32; 1];
26819 gate.matvec(x, &mut gl, pool);
26820 1.0 / (1.0 + (-gl[0]).exp())
26821 });
26822 for i in 0..out.len() {
26823 out[i] += g * so[i];
26824 }
26825 attention::recycle_buf(&mut so);
26826 expert_tubes_add(se, x, g, &mut out, pool);
26827 }
26828 out
26829}
26830
26831#[allow(clippy::too_many_arguments)]
26839pub(crate) fn mla_attention(
26840 w: &MlaWeights,
26841 normed: &[f32],
26842 cache: &mut crate::kv_cache::LayerKvCache,
26843 position: usize,
26844 inv_freq: &[f32],
26845 rope_scale: f32,
26846 eps: f64,
26847 pool: Option<&Pool>,
26848) -> Vec<f32> {
26849 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
26850 let hd = dr + dn;
26851 let mut q = vec![0.0f32; nh * hd];
26852 match (&w.q_a, &w.q_a_norm) {
26853 (Some(qa), Some(qn)) => {
26854 let mut t = vec![0.0f32; qa.rows()];
26855 qa.matvec(normed, &mut t, pool);
26856 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
26857 w.q_proj.matvec(&tn, &mut q, pool);
26858 }
26859 _ => w.q_proj.matvec(normed, &mut q, pool),
26860 }
26861 let mut ca = vec![0.0f32; lora + dr];
26862 w.kv_a.matvec(normed, &mut ca, pool);
26863 let (c_lat, k_rope) = ca.split_at_mut(lora);
26864 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
26865 let mut kvb = vec![0.0f32; nh * (dn + dv)];
26866 w.kv_b.matvec(&latn, &mut kvb, pool);
26867 if !w.nope {
26868 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
26869 }
26870 for h in 0..nh {
26871 if !w.nope {
26872 attention::rope_rotate_scaled(
26873 &mut q[h * hd..h * hd + dr],
26874 position,
26875 inv_freq,
26876 rope_scale,
26877 );
26878 }
26879 }
26880 let mut k = vec![0.0f32; nh * hd];
26881 let mut v = vec![0.0f32; nh * hd];
26882 for h in 0..nh {
26883 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
26884 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
26885 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
26886 }
26887 cache.append(&k, &v, &vec![true; nh]);
26888 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
26889 attention::recycle_buf(&mut imp);
26890 let mut ov = vec![0.0f32; nh * dv];
26891 for h in 0..nh {
26892 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
26893 }
26894 let mut out = vec![0.0f32; w.o_proj.rows()];
26895 w.o_proj.matvec(&ov, &mut out, pool);
26896 out
26897}
26898
26899fn dense_moe_ffn(
26906 dm: &DenseMoeFfn,
26907 x_normed: &[f32],
26908 h_raw: &[f32],
26909 eps: f64,
26910 norm_style: NormStyle,
26911 pool: Option<&Pool>,
26912) -> Vec<f32> {
26913 let mut d = dense_ffn(&dm.dense, x_normed, pool);
26914 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
26915 let m = &dm.moe;
26916 let ne = m.experts.len();
26917 let mut logits = vec![0.0f32; ne];
26918 if m.router_input_norm {
26919 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
26920 let inv = 1.0 / (ss + eps as f32).sqrt();
26921 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
26922 m.router.matvec(&xr, &mut logits, pool);
26923 } else {
26924 m.router.matvec(h_raw, &mut logits, pool);
26925 }
26926 let (idx, p, wsum) = moe_route(&logits, m, None);
26927 {
26928 let mut st = m.stats.borrow_mut();
26929 if st.len() < ne {
26930 st.resize(ne, 0);
26931 }
26932 for &e in &idx {
26933 st[e] += 1;
26934 }
26935 }
26936 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
26937 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
26938 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
26939 for (di, mi) in d.iter_mut().zip(&mo) {
26940 *di += mi;
26941 }
26942 d
26943}
26944
26945fn moe_gpu_refused(why: &'static str) {
26952 use std::sync::atomic::{AtomicBool, Ordering};
26953 static SAID: AtomicBool = AtomicBool::new(false);
26954 if !SAID.swap(true, Ordering::Relaxed) {
26955 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
26956 }
26957}
26958
26959fn moe_ffn_gpu(
26960 m: &MoeFfn,
26961 x: &[f32],
26962 idx: &[usize],
26963 p: &[f32],
26964 wsum: f32,
26965 pool: Option<&Pool>,
26966) -> Option<Vec<f32>> {
26967 use crate::gpu::MoeJob;
26968
26969 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
26970 let mut model_ref = None;
26971 for &e in idx {
26972 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
26973 moe_gpu_refused("push_job(expert)");
26974 return None;
26975 }
26976 }
26977 if let Some((se, gate)) = &m.shared {
26978 let g = gate.as_ref().map_or(1.0, |gate| {
26979 let mut gl = [0.0f32; 1];
26980 gate.matvec(x, &mut gl, pool);
26981 1.0 / (1.0 + (-gl[0]).exp())
26982 });
26983 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
26984 moe_gpu_refused("push_job(shared)");
26985 return None;
26986 }
26987 }
26988 let Some(model) = model_ref else {
26989 moe_gpu_refused("no model_ref");
26990 return None;
26991 };
26992 let hidden = jobs[0].down.1;
26993 let mut out = vec![0.0f32; hidden];
26994 if crate::gpu::moe_block(&model, &jobs, &mut out) {
26995 for &e in idx {
26998 expert_tubes_add(&m.experts[e], x, p[e] / wsum, &mut out, pool);
26999 }
27000 shared_tubes_add(m, x, &mut out, pool);
27001 Some(out)
27002 } else {
27003 moe_gpu_refused("gpu::moe_block");
27004 None
27005 }
27006}
27007
27008fn ffn_forward(
27010 ffn: &FfnKind,
27011 x: &[f32],
27012 pool: Option<&Pool>,
27013 experts_allowed: Option<&[bool]>,
27014) -> Vec<f32> {
27015 match ffn {
27016 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
27017 FfnKind::Dense(d) => dense_ffn(d, x, pool),
27018 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
27019 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
27023 }
27024}
27025
27026fn ffn_forward_pair(
27030 ffn: &FfnKind,
27031 x1: &[f32],
27032 x2: &[f32],
27033 pool: Option<&Pool>,
27034 experts_allowed: Option<&[bool]>,
27035) -> (Vec<f32>, Vec<f32>) {
27036 let d = match ffn {
27037 FfnKind::Dense(d) if !d.segs.is_empty() => {
27040 return (
27041 tube_ffn(d, x1, 1, pool, None),
27042 tube_ffn(d, x2, 1, pool, None),
27043 );
27044 }
27045 FfnKind::Dense(d) => d,
27046 FfnKind::Moe(m) => {
27047 return (
27048 moe_ffn(m, x1, pool, experts_allowed),
27049 moe_ffn(m, x2, pool, experts_allowed),
27050 );
27051 }
27052 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
27053 };
27054 let inter = d.gate_proj.rows();
27055 FFN_SCRATCH.with(|s| {
27056 let mut s = s.borrow_mut();
27057 let [g1, g2, u1, u2] = &mut *s;
27058 g1.resize(inter, 0.0);
27059 g2.resize(inter, 0.0);
27060 u1.resize(inter, 0.0);
27061 u2.resize(inter, 0.0);
27062 QTensor::matvec2_many(
27065 [&d.gate_proj, &d.up_proj],
27066 x1,
27067 x2,
27068 [g1.as_mut_slice(), u1.as_mut_slice()],
27069 [g2.as_mut_slice(), u2.as_mut_slice()],
27070 pool,
27071 );
27072 for i in 0..inter {
27073 g1[i] = d.act.combine(g1[i], u1[i]);
27074 g2[i] = d.act.combine(g2[i], u2[i]);
27075 }
27076 let mut o1 = attention::take_buf(d.down_proj.rows());
27077 let mut o2 = attention::take_buf(d.down_proj.rows());
27078 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
27079 (o1, o2)
27080 })
27081}
27082
27083#[cfg(test)]
27084mod tests {
27085 use std::ffi::OsString;
27086 use std::sync::Mutex;
27087
27088 static MTP_ENV_LOCK: Mutex<()> = Mutex::new(());
27091
27092 struct ScopedTestEnv {
27093 previous: Vec<(&'static str, Option<OsString>)>,
27094 }
27095
27096 impl ScopedTestEnv {
27097 fn set(vars: &[(&'static str, Option<&'static str>)]) -> Self {
27098 let previous = vars
27099 .iter()
27100 .map(|(name, _)| (*name, std::env::var_os(name)))
27101 .collect();
27102 for &(name, value) in vars {
27103 unsafe {
27104 match value {
27105 Some(value) => std::env::set_var(name, value),
27106 None => std::env::remove_var(name),
27107 }
27108 }
27109 }
27110 Self { previous }
27111 }
27112 }
27113
27114 impl Drop for ScopedTestEnv {
27115 fn drop(&mut self) {
27116 for (name, value) in self.previous.drain(..).rev() {
27117 unsafe {
27118 match value {
27119 Some(value) => std::env::set_var(name, value),
27120 None => std::env::remove_var(name),
27121 }
27122 }
27123 }
27124 }
27125 }
27126
27127 #[test]
27128 fn tail_repeat_counts_the_copied_stretch() {
27129 assert_eq!(tail_repeat(&[]), 0);
27130 assert_eq!(tail_repeat(&[5]), 0);
27131 assert_eq!(tail_repeat(&[1, 2, 3, 4]), 0);
27132 assert_eq!(tail_repeat(&[1, 2, 3, 9, 1, 2, 3]), 3);
27134 assert_eq!(tail_repeat(&[1, 2, 3, 4, 8, 3, 4, 7, 1, 2, 3, 4]), 4);
27136 assert_eq!(tail_repeat(&[1, 2, 1, 2]), 2);
27138 let long: Vec<u32> = (0..40).chain(0..40).collect();
27139 assert_eq!(tail_repeat(&long), 32);
27140 }
27141
27142 #[test]
27146 fn wgpu_spec_depth_follows_the_round_prices() {
27147 let steep = [0.0, 100.5, 111.3, 126.3, 144.6, 0.0, 0.0];
27149 let hold = [0.90, 0.85, 0.80, 0.60, 0.60, 0.60, 0.60];
27150 assert_eq!(wgpu_spec_depth(&hold, &steep, 5, 2, 5), Some(3));
27151 assert_eq!(wgpu_spec_depth(&hold, &steep, 4, 2, 5), Some(4));
27153 let flat = [0.0, 0.0, 60.0, 62.0, 64.0, 0.0, 0.0];
27154 assert_eq!(wgpu_spec_depth(&[0.9; 7], &flat, 3, 2, 5), Some(5));
27155 let two = [0.0, 0.0, 111.3, 126.3, 0.0, 0.0, 0.0];
27157 assert_eq!(wgpu_spec_depth(&[0.5; 7], &two, 4, 2, 5), Some(2));
27158 assert_eq!(wgpu_spec_depth(&hold, &[0.0, 0.0, 111.3, 0.0, 0.0, 0.0, 0.0], 3, 2, 5), None);
27160 }
27161
27162 #[test]
27163 fn dflash_round_values_price_a_round_of_each_drafter() {
27164 let after: Vec<u32> = (10..30).collect();
27166 let seq: Vec<u32> = (100..140).collect();
27167 let block: Vec<u32> = (10..17).collect();
27168 let chain: Vec<u32> = vec![10, 99, 99, 99, 99, 99, 99];
27169 let starts: Vec<DflashStart<'_>> =
27170 (0..40).map(|_| DflashStart { seq: &seq, after: &after, block: &block, chain: &chain }).collect();
27171 let v = dflash_round_values(&starts);
27172 let (b, c) = v[39];
27173 assert_eq!(b, (8.0, METAL_ROUND_COST[6] + DFLASH_ROUND_DRAFT));
27175 assert_eq!(c, (2.0, METAL_ROUND_COST[0]));
27177 assert!(b.0 / b.1 > c.0 / c.1);
27178 }
27179
27180 #[test]
27181 fn lookup_continues_a_repeated_stretch() {
27182 let seq = [1, 2, 3, 4, 5, 6, 7, 9, 9, 2, 3, 4];
27184 assert_eq!(lookup_continuation(&seq, 3, 3), &[5, 6, 7]);
27185 assert_eq!(lookup_continuation(&seq, 3, 2), &[5, 6]);
27186 assert!(lookup_continuation(&seq, 4, 3).is_empty());
27188 let seq = [5, 2, 3, 4, 8, 0, 3, 4, 7, 0, 5, 2, 3, 4];
27190 assert_eq!(lookup_continuation(&seq, 2, 1), &[8]);
27191 let seq = [2, 3, 4, 8, 0, 2, 3, 4, 7, 0, 2, 3, 4];
27193 assert_eq!(lookup_continuation(&seq, 2, 1), &[7]);
27194 let seq = [1, 2, 1, 2, 1, 2];
27196 assert_eq!(lookup_continuation(&seq, 3, 4), &[1, 2]);
27197 assert!(lookup_continuation(&[1, 2, 3], 3, 2).is_empty());
27198 assert!(lookup_continuation(&[], 1, 2).is_empty());
27199 }
27200
27201 #[test]
27202 fn metal_spec_depth_follows_what_the_chain_holds() {
27203 assert_eq!(metal_spec_depth(&SPEC_HOLD_PRIOR), 2);
27205 assert_eq!(metal_spec_depth(&[0.6, 0.15, 0.5, 0.5, 0.5, 0.5, 0.5]), 1);
27207 assert_eq!(metal_spec_depth(&[0.97; 7]), 7);
27209 let mut h = SPEC_HOLD_PRIOR;
27211 let mut k = metal_spec_depth(&h);
27212 let mut rounds = 0;
27213 while k == 2 && rounds < 200 {
27214 spec_hold_update(&mut h, k, 2);
27215 k = metal_spec_depth(&h);
27216 rounds += 1;
27217 }
27218 assert!(k > 2 && rounds < 60, "depth {k} after {rounds} rounds");
27219 let mut back = 0;
27221 while k > 2 && back < 200 {
27222 spec_hold_update(&mut h, k, 2);
27223 k = metal_spec_depth(&h);
27224 back += 1;
27225 }
27226 assert!(k == 2 && back < 10, "depth {k} after {back} rounds");
27227 let mut deep = 0;
27229 for _ in 0..400 {
27230 let k = metal_spec_depth(&h);
27231 deep += (k > 2) as usize;
27232 spec_hold_update(&mut h, k, 2);
27233 }
27234 assert!(deep < 40, "{deep} of 400 rounds went deeper");
27235 let mut h = SPEC_HOLD_PRIOR;
27237 for i in 0..300 {
27238 let k = metal_spec_depth(&h);
27239 assert!(k <= 2, "depth {k} at round {i}");
27240 spec_hold_update(&mut h, k, (i % 3).min(k));
27241 }
27242 }
27243
27244 #[test]
27245 fn mtp_draft_shortlist_masks_unevaluated_vocab_rows() {
27246 use super::pad_mtp_draft_logits;
27247
27248 let mut logits = vec![-3.0, -1.0, -2.0];
27249 pad_mtp_draft_logits(&mut logits, 6);
27250 assert_eq!(&logits[..3], &[-3.0, -1.0, -2.0]);
27251 assert!(logits[3..].iter().all(|&x| x == f32::NEG_INFINITY));
27252 assert_eq!(
27253 logits
27254 .iter()
27255 .enumerate()
27256 .max_by(|(_, a), (_, b)| a.total_cmp(b))
27257 .map(|(i, _)| i),
27258 Some(1),
27259 "an unevaluated row cannot win an all-negative shortlist"
27260 );
27261 }
27262
27263 #[test]
27264 fn speculative_trial_fast_exits_after_three_zero_acceptance_rounds() {
27265 use super::{Pipeline, SpecMon, SpecTrial};
27266
27267 let mut mon = SpecMon::default();
27268 let start = std::time::Instant::now();
27269 mon.round(1.0, 1);
27270 let first = Pipeline::spec_trial_round(
27271 SpecTrial::Spec {
27272 t0: start,
27273 gen0: 0,
27274 rounds: 0,
27275 },
27276 &mut mon,
27277 1,
27278 );
27279 assert!(matches!(first, SpecTrial::Spec { rounds: 1, .. }));
27280
27281 mon.round(1.0, 1);
27283 let second = Pipeline::spec_trial_round(first, &mut mon, 2);
27284 assert!(
27285 matches!(second, SpecTrial::Spec { rounds: 2, .. }),
27286 "two empty rounds are not evidence enough to leave the drafts"
27287 );
27288 mon.round(1.0, 1);
27289 let third = Pipeline::spec_trial_round(second, &mut mon, 3);
27290 assert!(
27291 matches!(third, SpecTrial::Plain { gen0: 3, .. }),
27292 "three verifier-tip-only rounds must stop the losing MTP trial"
27293 );
27294 assert_eq!(mon.recheck_plain_at(3), 3 + super::SPEC_RECHECK_THIN);
27296
27297 let mut viable = SpecMon::default();
27300 viable.round(1.0, 1);
27301 viable.round(1.0, 2);
27302 let continued = Pipeline::spec_trial_round(
27303 SpecTrial::Spec {
27304 t0: start,
27305 gen0: 0,
27306 rounds: 1,
27307 },
27308 &mut viable,
27309 3,
27310 );
27311 assert!(matches!(continued, SpecTrial::Spec { rounds: 2, .. }));
27312 }
27313
27314 #[test]
27315 fn fresh_mtp_request_does_not_inherit_full_head_streak() {
27316 let mut p = spark_test_pipeline(None);
27317 let head_rows = 248_070;
27318 p.draft_full_streak = 7;
27319 assert_eq!(p.draft_head_rows(head_rows), head_rows);
27320
27321 p.reset_fresh_mtp_draft_state();
27322 assert_eq!(
27323 p.draft_head_rows(head_rows),
27324 super::Pipeline::draft_vocab_rows(head_rows),
27325 "a fresh request returns to its configured shortlist"
27326 );
27327
27328 p.note_draft_id(u32::MAX);
27331 assert_eq!(p.draft_head_rows(head_rows), head_rows);
27332 }
27333
27334 #[test]
27335 fn native_metal_batch_spec_defaults_on_over_a_q4tp_trunk() {
27336 use super::graph_spec_wanted_by_policy;
27337
27338 assert!(graph_spec_wanted_by_policy(None, true, false, false, true));
27341 assert!(graph_spec_wanted_by_policy(
27342 Some("anything-but-1"),
27343 true,
27344 false,
27345 false,
27346 true,
27347 ));
27348 assert!(!graph_spec_wanted_by_policy(None, false, false, false, true));
27350 assert!(!graph_spec_wanted_by_policy(
27351 Some("anything-but-1"),
27352 false,
27353 false,
27354 false,
27355 true,
27356 ));
27357 assert!(graph_spec_wanted_by_policy(
27358 Some("1"),
27359 false,
27360 true,
27361 false,
27362 true,
27363 ));
27364 assert!(!graph_spec_wanted_by_policy(
27365 Some("0"),
27366 true,
27367 false,
27368 false,
27369 true,
27370 ));
27371
27372 assert!(graph_spec_wanted_by_policy(None, true, false, false, false));
27375 assert!(graph_spec_wanted_by_policy(
27376 Some("legacy-force"),
27377 false,
27378 true,
27379 true,
27380 false,
27381 ));
27382 }
27383
27384 #[test]
27385 fn encoder_hidden_states_are_normalized_and_reusable() {
27386 let mut p = super::create_test_pipeline(8, 12, 2, 1, 4, 2, 32);
27387 let ids = [1, 7, 11, 19];
27388
27389 let first = p.encode_hidden_states(&ids).expect("encoder forward");
27390 let second = p
27391 .encode_hidden_states(&ids)
27392 .expect("reusable encoder forward");
27393 assert_eq!(first.len(), ids.len() * 8);
27394 assert!(first.iter().all(|v| v.is_finite()));
27395 assert_eq!(
27396 first, second,
27397 "encoder forward must start from a fresh state"
27398 );
27399
27400 assert!(p.forward_ids(&ids, None).is_ok());
27403 }
27404
27405 #[test]
27410 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
27411 use super::{ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK, prefill_chunk_rule};
27412 let dense_card = ChunkStackFacts {
27413 plain_dense: true,
27414 discrete: true,
27415 gpu_on: true,
27416 ..Default::default()
27417 };
27418 assert!(dense_card.dense_on_discrete());
27419 assert_eq!(
27421 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
27422 DISCRETE_DENSE_PREFILL_CHUNK
27423 );
27424 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
27425 for (label, facts) in [
27426 (
27427 "GDN hybrid / MoE / DeepSeek stack",
27428 ChunkStackFacts {
27429 plain_dense: false,
27430 ..dense_card
27431 },
27432 ),
27433 (
27434 "integrated GPU",
27435 ChunkStackFacts {
27436 discrete: false,
27437 ..dense_card
27438 },
27439 ),
27440 (
27441 "CPU only",
27442 ChunkStackFacts {
27443 gpu_on: false,
27444 discrete: false,
27445 ..dense_card
27446 },
27447 ),
27448 (
27449 "capacity split",
27450 ChunkStackFacts {
27451 capacity_split: true,
27452 ..dense_card
27453 },
27454 ),
27455 (
27456 "multi-GPU plan",
27457 ChunkStackFacts {
27458 multi_gpu: true,
27459 ..dense_card
27460 },
27461 ),
27462 (
27463 "O(1) layers",
27464 ChunkStackFacts {
27465 o1: true,
27466 ..dense_card
27467 },
27468 ),
27469 ] {
27470 assert!(!facts.dense_on_discrete(), "{label}");
27471 assert_eq!(
27472 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
27473 48,
27474 "{label} keeps the historical x86 chunk"
27475 );
27476 }
27477 for dense in [false, true] {
27479 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
27480 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
27481 }
27482 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
27484 for dense in [false, true] {
27485 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
27486 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
27487 }
27488 }
27489 }
27490
27491 #[test]
27492 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
27493 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
27494 let full = |host_rows, device_rows| ReuseLayer {
27495 full: true,
27496 host_rows,
27497 device_rows,
27498 device_state: false,
27499 };
27500 assert_eq!(
27503 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
27504 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
27505 );
27506 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
27508 assert_eq!(
27510 kv_reuse_plan(339, &[full(339, Some(345))]),
27511 ReusePlan::Ready
27512 );
27513 assert_eq!(
27515 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
27516 ReusePlan::Pull(vec![(0, 300, 339)])
27517 );
27518 assert_eq!(
27520 kv_reuse_plan(339, &[full(300, Some(320))]),
27521 ReusePlan::Fresh
27522 );
27523 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
27524 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
27525 let conv = |device_state| ReuseLayer {
27528 full: false,
27529 host_rows: 0,
27530 device_rows: None,
27531 device_state,
27532 };
27533 assert_eq!(
27534 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
27535 ReusePlan::Fresh
27536 );
27537 assert_eq!(
27538 kv_reuse_plan(339, &[conv(false), full(339, None)]),
27539 ReusePlan::Ready
27540 );
27541 }
27542
27543 #[test]
27544 fn nll_graph_policy_scopes_only_the_fused_head() {
27545 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
27546 ("vulkan graph", true, true, false, true, false),
27548 ("native Metal graph", true, true, true, true, true),
27550 ("masked", false, true, false, false, false),
27552 ("graph disabled", true, false, true, false, false),
27553 ] {
27554 let (graph_quality, graph_head_required) =
27555 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
27556 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
27557 assert_eq!(graph_head_required, want_head, "{label}: fused head");
27558 }
27559 }
27560
27561 #[test]
27562 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
27563 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
27564 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
27565 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
27566 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
27567 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
27568 }
27569
27570 #[test]
27571 fn cancel_flag_stops_generation() {
27572 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
27573 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
27576 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
27577 assert_eq!(r.finish_reason, "cancelled");
27578 assert!(
27579 r.token_ids.is_empty(),
27580 "no tokens after cancel: {:?}",
27581 r.token_ids
27582 );
27583 assert_eq!(p.kv_cache.seq_len(), 0);
27584 assert!(p.kv_history.is_empty());
27585 assert!(!p.graph_want_logits);
27586 assert!(p.graph_logits.is_none());
27587 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
27589 assert_ne!(r2.finish_reason, "cancelled");
27590 }
27591 use super::*;
27592
27593 #[test]
27594 fn minimax_m3_arch_selects_checked_oai_activation_only() {
27595 let arch = |arch_name: &str, hidden_act: &str| {
27599 serde_json::from_value::<cortiq_core::ModelArch>(serde_json::json!({
27600 "arch_name": arch_name,
27601 "hidden_size": 8,
27602 "intermediate_size": 16,
27603 "num_layers": 1,
27604 "num_attention_heads": 1,
27605 "num_kv_heads": 1,
27606 "head_dim": 8,
27607 "vocab_size": 16,
27608 "layer_types": ["FullAttention"],
27609 "rms_norm_eps": 1e-6,
27610 "max_position_embeddings": 16,
27611 "hidden_act": hidden_act
27612 }))
27613 .expect("minimal architecture")
27614 };
27615 let minimax = arch("minimax_m3_vl", "swigluoai");
27616 assert_eq!(
27617 Act::from_arch_full(&minimax),
27618 Act::SwiGluOai {
27619 alpha: 1.702,
27620 limit: 7.0,
27621 }
27622 );
27623
27624 let unrelated = arch("qwen2", "swigluoai");
27627 assert_eq!(Act::from_arch_full(&unrelated), Act::Silu);
27628
27629 let non_oai = arch("minimax_m3_vl", "gelu");
27633 assert_eq!(Act::from_arch_full(&non_oai), Act::Gelu);
27634 }
27635
27636 #[test]
27637 fn minimax_swiglu_oai_act_matches_scalar_contract() {
27638 let act = Act::swiglu_oai(1.702, 7.0);
27639 let got = act.combine(10.0, -10.0);
27640 let expected = crate::inference::swiglu_oai(10.0, -10.0, 1.702, 1.0, Some(7.0));
27641 assert!((got - expected).abs() < 1e-6);
27642 assert_eq!(act.graph_act(), None, "no inexact generic graph fallback");
27643 assert_eq!(
27644 act.wgpu_graph_act(),
27645 Some(crate::gpu::GraphAct::SwiGluOaiM3),
27646 "the WGPU-only arm accepts only the frozen M3 constants"
27647 );
27648 assert_eq!(
27649 Act::swiglu_oai(1.701, 7.0).wgpu_graph_act(),
27650 None,
27651 "nearby OAI constants must not borrow MiniMax's kernel"
27652 );
27653 assert_eq!(
27654 act.moe_job_graph_contract(),
27655 Some((crate::gpu::GraphAct::SwiGluOaiM3, 7.0)),
27656 "the direct MoE path carries the frozen clamp with the marker"
27657 );
27658 assert_eq!(
27659 Act::swiglu_oai(1.702, 6.999).moe_job_graph_contract(),
27660 None,
27661 "a nearby clamp must not borrow MiniMax's device contract"
27662 );
27663
27664 let gate_only = act.apply(-10.0);
27667 let expected_gate = -10.0 / (1.0 + (17.02f32).exp());
27668 assert!((gate_only - expected_gate).abs() < 1e-10);
27669 }
27670
27671 #[test]
27672 fn explicit_sliding_schedule_selects_local_rope_profile() {
27673 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
27678 p.swa = Some((1024, usize::MAX));
27679 p.sliding_layers = Some(vec![true, true, true, false]);
27680 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
27681 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
27682 p.rope_scale = 1.277_258_9;
27683 p.rope_scale_local = 1.0;
27684
27685 for li in 0..3 {
27686 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
27687 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
27688 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
27689 }
27690 assert_eq!(p.layer_window(3), None);
27691 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
27692 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
27693 }
27694
27695 #[test]
27703 fn dynamic_ffn_equals_the_zeroing_arm() {
27704 let (hidden, inter) = (8usize, 32usize);
27705 let synth = |n: usize, salt: usize| -> Vec<f32> {
27706 (0..n)
27707 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
27708 .collect()
27709 };
27710 let down = synth(hidden * inter, 3);
27711 let mut down_t = vec![0.0f32; inter * hidden];
27712 for r in 0..hidden {
27713 for c in 0..inter {
27714 down_t[c * hidden + r] = down[r * inter + c];
27715 }
27716 }
27717 let d = DenseFfn {
27718 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
27719 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
27720 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
27721 act: Act::Silu,
27722 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
27723 segs: Vec::new(),
27724 };
27725 let x = synth(hidden, 11);
27726 let k = 12usize;
27727 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
27728 let mut g = vec![0.0f32; inter];
27730 d.gate_proj.matvec(&x, &mut g, None);
27731 let mut u = vec![0.0f32; inter];
27732 d.up_proj.matvec(&x, &mut u, None);
27733 for v in g.iter_mut() {
27734 *v = inference::silu(*v);
27735 }
27736 keep_top_k(&mut g, k);
27737 for i in 0..inter {
27738 g[i] *= u[i];
27739 }
27740 let mut want = vec![0.0f32; hidden];
27741 d.down_proj.matvec(&g, &mut want, None);
27742 for (a, b) in want.iter().zip(&got) {
27743 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
27744 }
27745 }
27746
27747 #[test]
27753 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
27754 let (hidden, core, tube) = (8usize, 12usize, 8usize);
27755 let inter = core + tube;
27756 let synth = |n: usize, salt: usize| -> Vec<f32> {
27757 (0..n)
27758 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
27759 .collect()
27760 };
27761 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
27762 let d_all = synth(hidden * inter, 3);
27763 let dense = DenseFfn {
27765 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
27766 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
27767 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
27768 act: Act::Silu,
27769 down_t: None,
27770 segs: Vec::new(),
27771 };
27772 let rows =
27773 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
27774 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
27775 let mut o = Vec::with_capacity(hidden * (b - a));
27776 for r in 0..hidden {
27777 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
27778 }
27779 o
27780 };
27781 let tubed = DenseFfn {
27782 down_t: None,
27783 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
27784 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
27785 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
27786 act: Act::Silu,
27787 segs: vec![FfnSeg {
27788 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
27789 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
27790 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
27791 start: core,
27792 width: tube,
27793 }],
27794 };
27795 let x = synth(hidden, 7);
27796 let want = dense_ffn(&dense, &x, None);
27797 let got = tube_ffn(&tubed, &x, 1, None, None);
27798 for (a, b) in want.iter().zip(&got) {
27799 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
27800 }
27801 let mut bits = vec![0u8; inter.div_ceil(8)];
27803 for n in 0..core {
27804 bits[n / 8] |= 1 << (n % 8);
27805 }
27806 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
27807 let masked = dense_ffn_masked(&dense, &x, None, &bits);
27808 for (a, b) in masked.iter().zip(&closed) {
27809 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
27810 }
27811 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
27813 for (a, b) in closed.iter().zip(&batch) {
27814 assert_eq!(a, b, "batch arm disagrees with decode arm");
27815 }
27816 }
27817
27818 #[test]
27820 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
27821 let (hidden, inter) = (16usize, 40usize);
27822 let synth = |n: usize, salt: usize| -> Vec<f32> {
27823 (0..n)
27824 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
27825 .collect()
27826 };
27827 let d = DenseFfn {
27828 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
27829 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
27830 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
27831 act: Act::Silu,
27832 down_t: None,
27833 segs: Vec::new(),
27834 };
27835 let x = synth(hidden, 9);
27836 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
27838
27839 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
27840
27841 let mut g = vec![0.0f32; inter];
27843 d.gate_proj.matvec(&x, &mut g, None);
27844 let mut u = vec![0.0f32; inter];
27845 d.up_proj.matvec(&x, &mut u, None);
27846 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
27847 for i in 0..inter {
27848 g[i] = if act_set.contains(&(i as u16)) {
27849 inference::silu(g[i]) * u[i]
27850 } else {
27851 0.0
27852 };
27853 }
27854 let mut reference = vec![0.0f32; hidden];
27855 d.down_proj.matvec(&g, &mut reference, None);
27856
27857 let max_d = sparse
27858 .iter()
27859 .zip(&reference)
27860 .map(|(a, b)| (a - b).abs())
27861 .fold(0.0f32, f32::max);
27862 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
27863 }
27864
27865 fn attach_test_mtp_width(p: &mut Pipeline, inter: usize) {
27868 let (h, inter, heads, kv, hd) = (
27869 p.hidden_size,
27870 inter,
27871 p.num_heads,
27872 p.num_kv_heads,
27873 p.head_dim,
27874 );
27875 let synth = |n: usize, salt: usize| -> Vec<f32> {
27876 (0..n)
27877 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
27878 .collect()
27879 };
27880 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
27881 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
27882 };
27883 p.mtp = Some(MtpModule {
27884 enorm: vec![1.0; h],
27885 hnorm: vec![1.0; h],
27886 eh_proj: qt(h, 2 * h, 301),
27887 layer: LayerWeights {
27888 input_norm: vec![1.0; h],
27889 post_norm: vec![1.0; h],
27890 attn_out_norm: None,
27891 ffn_out_norm: None,
27892 layer_scale: None,
27893 ffn: FfnKind::Dense(DenseFfn {
27894 gate_proj: qt(inter, h, 315),
27895 up_proj: qt(inter, h, 316),
27896 down_proj: qt(h, inter, 317),
27897 act: Act::Silu,
27898 down_t: None,
27899 segs: Vec::new(),
27900 }),
27901 attn: AttnKind::Full {
27902 bias: None,
27903 wq: qt(heads * hd, h, 311),
27904 wk: qt(kv * hd, h, 312),
27905 wv: qt(kv * hd, h, 313),
27906 wo: qt(h, heads * hd, 314),
27907 q_norm: None,
27908 k_norm: None,
27909 output_gate: false,
27910 softplus_gate: None,
27911 },
27912 },
27913 final_norm: vec![1.0; h],
27914 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
27915 });
27916 }
27917
27918 fn attach_test_mtp(p: &mut Pipeline) {
27920 attach_test_mtp_width(p, p.intermediate_size);
27921 }
27922
27923 #[test]
27924 fn canonical_mtp_ffn_probe_uses_draft_width_and_cleans_state() {
27925 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 64);
27926 attach_test_mtp_width(&mut p, 11);
27929 let ids = [3, 9, 4, 12, 7, 2];
27930 let trunk = p.probe_ffn_mass(&ids);
27931 assert_eq!(trunk[0].len(), p.intermediate_size);
27932
27933 let got = p.probe_mtp_ffn(&ids).expect("canonical MTP probe");
27934 assert_eq!(got.len(), 1);
27935 assert_eq!(got[0].mass.len(), 11);
27936 assert_eq!(got[0].squared_mass.len(), 11);
27937 assert_eq!(got[0].observed_positions, ids.len() - 1);
27938 assert!(got[0].mass.iter().any(|&v| v > 0.0));
27939 assert!(got[0]
27940 .mass
27941 .iter()
27942 .zip(&got[0].squared_mass)
27943 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
27944 assert_eq!(p.kv_cache.seq_len(), 0, "probe must leave a fresh trunk");
27945 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0, "and fresh MTP KV");
27946
27947 let h = p.hidden_size;
27951 let FfnKind::Dense(dense) = &mut p.mtp.as_mut().unwrap().layer.ffn else {
27952 panic!("synthetic MTP must be dense")
27953 };
27954 dense.segs.push(FfnSeg {
27955 gate: QTensor::from_f32(vec![0.0; h], 1, h),
27956 up: QTensor::from_f32(vec![0.0; h], 1, h),
27957 down: QTensor::from_f32(vec![0.0; h], h, 1),
27958 start: dense.gate_proj.rows(),
27959 width: 1,
27960 });
27961 let err = p.probe_mtp_ffn(&ids);
27962 assert!(err
27963 .unwrap_err()
27964 .contains("tube-segmented draft FFNs are not supported"));
27965 assert_eq!(p.kv_cache.seq_len(), 0);
27966 assert_eq!(p.mtp.as_ref().unwrap().kv.seq_len, 0);
27967 }
27968
27969 #[test]
27970 fn speculative_equals_vanilla_greedy() {
27971 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
27972 let _env = ScopedTestEnv::set(&[
27975 ("CMF_GPU", Some("0")),
27976 ("CMF_GPU_WGPU_GRAPH", Some("0")),
27977 ("CMF_MTP_PAIR", Some("1")),
27978 ]);
27979 let run = |spec: bool| {
27980 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
27981 p.sampler_config.temperature = 0.0;
27982 attach_test_mtp(&mut p);
27983 p.speculative = spec;
27984 let r = p.generate("abcdef", 12, None, None).unwrap();
27985 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
27986 };
27987 let (vanilla, d0, _) = run(false);
27988 let (spec, d1, a1) = run(true);
27989 assert_eq!(d0, 0, "vanilla path must not draft");
27990 assert!(d1 > 0, "speculative path must draft");
27991 assert!(a1 <= d1, "accepted drafts cannot exceed proposals");
27992 assert_eq!(
27993 vanilla, spec,
27994 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
27995 );
27996 }
27997
27998 #[test]
27999 fn speculative_accepts_constant_oracle() {
28000 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28001 let _env = ScopedTestEnv::set(&[
28002 ("CMF_GPU", Some("0")),
28003 ("CMF_GPU_WGPU_GRAPH", Some("0")),
28004 ("CMF_MTP_PAIR", Some("1")),
28005 ]);
28006 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28007 p.sampler_config.temperature = 0.0;
28008 p.sampler_config.repetition_penalty = 1.0;
28009 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
28012 attach_test_mtp(&mut p);
28013 p.speculative = true;
28014 let r = p.generate("abcd", 10, None, None).unwrap();
28015 assert!(r.mtp_drafted > 0);
28016 assert_eq!(
28017 r.mtp_accepted, r.mtp_drafted,
28018 "constant logits → every draft accepted"
28019 );
28020 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
28023 }
28024
28025 #[test]
28026 fn host_fused_mtp_pair_is_opt_in() {
28027 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28028 let _env = ScopedTestEnv::set(&[
28029 ("CMF_GPU", Some("0")),
28030 ("CMF_GPU_WGPU_GRAPH", Some("0")),
28031 ("CMF_MTP_PAIR", None),
28032 ]);
28033 let run = |spec: bool| {
28034 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28035 p.sampler_config.temperature = 0.0;
28036 attach_test_mtp(&mut p);
28037 p.speculative = spec;
28038 let r = p.generate("abcdef", 12, None, None).unwrap();
28039 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
28040 };
28041 let (plain, d0, _) = run(false);
28042 let (guarded, d1, a1) = run(true);
28043 assert_eq!(d0, 0);
28044 assert_eq!(d1, 0, "host MTP pair must remain opt-in");
28045 assert_eq!(a1, 0);
28046 assert_eq!(guarded, plain, "the safe default is ordinary greedy decode");
28047 }
28048
28049 #[test]
28050 fn empty_prompt_is_an_error_not_a_panic() {
28051 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
28052 let r = p.generate("", 4, None, None);
28053 assert!(r.is_err(), "empty prompt must be a clean error");
28054 }
28055
28056 #[test]
28057 fn every_token_enters_kv_exactly_once() {
28058 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28059 p.sampler_config.temperature = 0.0;
28061 let r = p.generate("abc", 2, None, None).unwrap();
28062 assert_eq!(r.prompt_tokens, 3);
28063 assert_eq!(
28067 p.kv_cache.seq_len(),
28068 3 + r.tokens_generated - 1,
28069 "each token must be cached exactly once (v1 cached the last prompt token twice)"
28070 );
28071 }
28072
28073 #[test]
28074 fn generation_is_reproducible_with_seed() {
28075 let run = || {
28076 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28077 p.generate("hello", 8, None, None).unwrap().token_ids
28078 };
28079 assert_eq!(run(), run());
28080 }
28081
28082 #[test]
28083 fn resetting_sampler_restarts_the_seeded_stream() {
28084 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
28085 let config = SamplerConfig {
28086 seed: Some(1234),
28087 ..SamplerConfig::default()
28088 };
28089 p.set_sampler_config(config.clone());
28090 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
28091 p.set_sampler_config(config);
28092 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
28093 assert_eq!(first, second);
28094 }
28095
28096 #[test]
28097 fn eviction_bounds_the_cache() {
28098 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
28099 p.kv_cache.max_seq_len = 6;
28100 p.sampler_config.temperature = 0.0;
28101 let _ = p.generate("abcd", 12, None, None).unwrap();
28102 assert!(
28103 p.kv_cache.seq_len() <= 6 + 1,
28104 "cache must stay bounded by max_seq_len (got {})",
28105 p.kv_cache.seq_len()
28106 );
28107 }
28108
28109 #[test]
28110 fn confidence_matches_tokens_and_is_a_probability() {
28111 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28112 p.sampler_config.temperature = 0.0;
28113 p.sampler_config.repetition_penalty = 1.0;
28114 let r = p.generate("abcd", 10, None, None).unwrap();
28115 assert_eq!(
28116 r.token_confidence.len(),
28117 r.token_ids.len(),
28118 "one confidence per emitted token"
28119 );
28120 for &c in &r.token_confidence {
28121 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
28122 }
28123 let logits = [1.0f32, 3.0, 0.5, 3.0];
28125 let p0 = top1_prob_t(&logits, 1, 1.0);
28126 let p1 = top1_prob_t(&logits, 3, 1.0);
28127 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
28128 assert!(p0 > 0.0 && p0 < 1.0);
28129 let sharp = top1_prob_t(&logits, 1, 1.0);
28131 let soft = top1_prob_t(&logits, 1, 2.0);
28132 assert!(soft < sharp, "higher temperature lowers peak confidence");
28133 }
28134
28135 #[test]
28136 fn trace_is_opt_in_and_parallels_the_output() {
28137 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28139 p.sampler_config.temperature = 0.0;
28140 p.sampler_config.repetition_penalty = 1.0;
28141 let r = p.generate("abcd", 10, None, None).unwrap();
28142 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
28143
28144 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28146 p.sampler_config.temperature = 0.0;
28147 p.sampler_config.repetition_penalty = 1.0;
28148 p.set_trace(true);
28149 let r = p.generate("abcd", 10, None, None).unwrap();
28150 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
28151 for (i, tr) in r.traces.iter().enumerate() {
28152 assert_eq!(tr.t, i, "trace index is sequential");
28153 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
28154 assert_eq!(
28155 tr.confidence, r.token_confidence[i],
28156 "trace confidence matches the confidence channel"
28157 );
28158 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
28160 }
28161 }
28162
28163 #[test]
28164 fn explain_prefill_logits_match_greedy_first_token() {
28165 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
28169 p.sampler_config.temperature = 0.0;
28170 p.sampler_config.repetition_penalty = 1.0;
28171 let ids = p.tokenizer.encode("abcd");
28172 let logits = p.prefill_next_logits(&ids, None);
28173 let argmax = logits
28174 .iter()
28175 .enumerate()
28176 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
28177 .unwrap()
28178 .0 as u32;
28179 let r = p.generate("abcd", 1, None, None).unwrap();
28180 assert_eq!(
28181 argmax, r.token_ids[0],
28182 "explain preview must match greedy emit"
28183 );
28184 }
28185
28186 #[test]
28187 fn laguna_shared_expert_is_unconditionally_added() {
28188 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
28189 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
28190 let zero_dense = || DenseFfn {
28191 gate_proj: matrix(vec![0.0; 4]),
28192 up_proj: matrix(vec![0.0; 4]),
28193 down_proj: matrix(vec![0.0; 4]),
28194 act: Act::Silu,
28195 down_t: None,
28196 segs: Vec::new(),
28197 };
28198 let shared = DenseFfn {
28199 gate_proj: identity(),
28200 up_proj: identity(),
28201 down_proj: identity(),
28202 act: Act::Silu,
28203 down_t: None,
28204 segs: Vec::new(),
28205 };
28206 let x = [1.0, 2.0];
28207 let expected = dense_ffn(&shared, &x, None);
28208 let moe = MoeFfn {
28209 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
28210 experts: vec![zero_dense()],
28211 top_k: 1,
28212 norm_topk_prob: true,
28213 router_sigmoid: true,
28214 sigmoid_denom_eps: 1e-6,
28215 expert_bias: None,
28216 routed_scaling: 1.0,
28217 route_tau: None,
28218 shared: Some((shared, None)),
28219 stats: std::cell::RefCell::new(Vec::new()),
28220 act_sq: std::cell::RefCell::new(Vec::new()),
28221 act_rows: std::cell::RefCell::new(Vec::new()),
28222 mask: None,
28223 per_expert_scale: None,
28224 router_input_norm: false,
28225 resonance: None,
28226 grown: Vec::new(),
28227 };
28228 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
28229 for (actual, expected) in actual.iter().zip(expected) {
28230 assert!((actual - expected).abs() < 1e-6);
28231 }
28232 }
28233
28234 fn mimo_test_pipeline() -> Pipeline {
28243 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
28244 let kvh = [1usize, 2, 2, 1];
28245 let synth = |n: usize, salt: usize| -> Vec<f32> {
28246 (0..n)
28247 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
28248 .collect()
28249 };
28250 let qt = |rows: usize, cols: usize, salt: usize| {
28251 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
28252 };
28253 let dense = |inter: usize, salt: usize| DenseFfn {
28254 gate_proj: qt(inter, hs, salt),
28255 up_proj: qt(inter, hs, salt + 1),
28256 down_proj: qt(hs, inter, salt + 2),
28257 act: Act::Silu,
28258 down_t: None,
28259 segs: Vec::new(),
28260 };
28261 let layers: Vec<LayerWeights> = (0..4)
28262 .map(|li| LayerWeights {
28263 input_norm: vec![1.0; hs],
28264 post_norm: vec![1.0; hs],
28265 attn_out_norm: None,
28266 ffn_out_norm: None,
28267 layer_scale: None,
28268 ffn: if li == 0 {
28269 FfnKind::Dense(dense(inter, 50))
28270 } else {
28271 FfnKind::Moe(MoeFfn {
28272 router: qt(4, hs, 60 + li),
28273 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
28274 top_k: 2,
28275 norm_topk_prob: true,
28276 router_sigmoid: true,
28277 sigmoid_denom_eps: 1e-6,
28278 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
28279 routed_scaling: 1.0,
28280 route_tau: None,
28281 shared: None,
28282 stats: std::cell::RefCell::new(Vec::new()),
28283 act_sq: std::cell::RefCell::new(Vec::new()),
28284 act_rows: std::cell::RefCell::new(Vec::new()),
28285 mask: None,
28286 per_expert_scale: None,
28287 router_input_norm: false,
28288 resonance: None,
28289 grown: Vec::new(),
28290 })
28291 },
28292 attn: AttnKind::Full {
28293 wq: qt(nh * hd, hs, li * 10 + 1),
28294 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
28295 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
28296 wo: qt(hs, nh * vd, li * 10 + 4),
28297 q_norm: None,
28298 k_norm: None,
28299 output_gate: false,
28300 softplus_gate: None,
28301 bias: None,
28302 },
28303 })
28304 .collect();
28305 let mut p = Pipeline::new(
28306 Tokenizer::byte_level(),
28307 PipelineWeights {
28308 embed_tokens: qt(vocab, hs, 100),
28309 layers,
28310 lm_head: qt(vocab, hs, 200),
28311 final_norm: vec![1.0; hs],
28312 },
28313 hs,
28314 inter,
28315 nh,
28316 1, hd,
28318 4,
28319 4,
28320 false,
28321 vocab,
28322 1e-6,
28323 1e7,
28324 NormStyle::Qwen,
28325 4096,
28326 SamplerConfig {
28327 seed: Some(7),
28328 ..Default::default()
28329 },
28330 );
28331 p.layer_dump = None;
28333 p.set_rotary(4, 1e7);
28334 p.sliding_layers = Some(vec![false, true, true, false]);
28335 p.swa = Some((3, usize::MAX));
28336 p.rotary_dim_local = Some(4);
28337 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
28338 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
28339 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
28340 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
28341 p
28342 }
28343
28344 #[test]
28345 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
28346 let mut p = mimo_test_pipeline();
28347 p.speculative = false;
28348 p.ignore_eos = true;
28349 p.sampler_config.temperature = 0.0;
28350 p.sampler_config.repetition_penalty = 1.0;
28351 let a = vec![3, 5, 7, 9, 11, 13];
28352 let b = vec![4, 8, 12, 16, 20, 24];
28353 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
28354 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
28355 let actual = p
28359 .generate_from_embeds(&a, &rows, 8, None, None)
28360 .unwrap()
28361 .token_ids;
28362 assert_eq!(actual, expected);
28363 assert!(p.kv_history.is_empty());
28364 let mut extended = a.clone();
28365 extended.push(17);
28366 let after_media = p
28367 .generate_from_ids(&extended, 8, None, None)
28368 .unwrap()
28369 .token_ids;
28370 p.reset_session();
28371 let fresh = p
28372 .generate_from_ids(&extended, 8, None, None)
28373 .unwrap()
28374 .token_ids;
28375 assert_eq!(after_media, fresh);
28376 assert!(
28377 p.generate_from_embeds(&a, &rows[..rows.len() - 1], 1, None, None)
28378 .is_err()
28379 );
28380 p.reset_session();
28384 p.generate_from_ids(&a, 1, None, None).unwrap();
28385 let mut media_ids = p.kv_history.clone();
28386 assert!(!media_ids.is_empty());
28387 media_ids.extend_from_slice(&[19, 21, 23]);
28388 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
28389 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
28390 let mut oracle = mimo_test_pipeline();
28391 oracle.speculative = false;
28392 oracle.ignore_eos = true;
28393 oracle.sampler_config.temperature = 0.0;
28394 oracle.sampler_config.repetition_penalty = 1.0;
28395 let expected = oracle
28396 .generate_from_ids(&source_ids, 8, None, None)
28397 .unwrap()
28398 .token_ids;
28399 assert_eq!(
28400 p.generate_from_embeds(&media_ids, &source_rows, 8, None, None)
28401 .unwrap()
28402 .token_ids,
28403 expected
28404 );
28405 assert!(p.kv_history.is_empty());
28406 let mut bad = rows;
28407 bad[0] = f32::NAN;
28408 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
28409 }
28410
28411 fn f32_bits(v: &[f32]) -> Vec<u32> {
28412 v.iter().map(|x| x.to_bits()).collect()
28413 }
28414
28415 #[test]
28422 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
28423 let mut p = mimo_test_pipeline();
28424 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
28425 assert_eq!(kv, vec![1, 2, 2, 1]);
28426 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
28427 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
28428 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
28429 let hs = p.hidden_size;
28430 let mut decode = Vec::new();
28431 for (pos, &id) in ids.iter().enumerate() {
28432 let e = p.embed_single(id);
28433 let h = p.forward_layers(&e, pos, None);
28434 decode.push(p.logits_from_hidden(&h));
28435 }
28436 for l in &p.kv_cache.layers {
28437 assert_eq!(l.seq_len, 12);
28438 assert_eq!(l.head_values(0).len(), 12 * 8);
28440 }
28441 assert!(decode.iter().flatten().all(|v| v.is_finite()));
28442
28443 p.clear_sequence_state();
28444 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28445 for pos in 0..ids.len() {
28446 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
28447 assert_eq!(
28448 f32_bits(&decode[pos]),
28449 f32_bits(&lg),
28450 "whole prompt, pos {pos}"
28451 );
28452 }
28453
28454 p.clear_sequence_state();
28455 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
28456 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
28457 for pos in 0..ids.len() {
28458 let row = if pos < 5 {
28459 &a[pos * hs..(pos + 1) * hs]
28460 } else {
28461 &b[(pos - 5) * hs..(pos - 4) * hs]
28462 };
28463 let lg = p.logits_from_hidden(row);
28464 assert_eq!(
28465 f32_bits(&decode[pos]),
28466 f32_bits(&lg),
28467 "two chunks, pos {pos}"
28468 );
28469 }
28470
28471 let last = |p: &mut Pipeline| {
28474 p.clear_sequence_state();
28475 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28476 p.logits_from_hidden(&hb[11 * hs..12 * hs])
28477 };
28478 let base = last(&mut p);
28479 let mut no_sinks = mimo_test_pipeline();
28480 for l in &mut no_sinks.kv_cache.layers {
28481 l.sinks = None;
28482 }
28483 assert_ne!(
28484 f32_bits(&last(&mut no_sinks)),
28485 f32_bits(&base),
28486 "sinks are live"
28487 );
28488 let mut wide = mimo_test_pipeline();
28489 wide.swa = Some((64, usize::MAX));
28490 assert_ne!(
28491 f32_bits(&last(&mut wide)),
28492 f32_bits(&base),
28493 "window is live"
28494 );
28495
28496 p.clear_sequence_state();
28498 p.ignore_eos = true;
28499 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
28500 assert_eq!(r.token_ids.len(), 4);
28501 }
28502
28503 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
28506 let (hs, nh) = (16usize, 4usize);
28507 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
28508 p.layer_dump = None;
28509 p.swa = Some((6, 4));
28510 p.proj_gate_sigmoid = true;
28511 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
28512 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
28513 let g: Vec<f32> = (0..nh * hs)
28514 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
28515 .collect();
28516 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
28517 }
28518 }
28519 p.swa_trim = trim;
28520 p
28521 }
28522
28523 #[test]
28529 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
28530 let mut a = spark_test_pipeline(None);
28531 let mut b = spark_test_pipeline(Some((2, 4)));
28532 assert_eq!(
28533 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
28534 vec![Some(6), Some(6), Some(6), None]
28535 );
28536 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
28537 let mut pos = 0usize;
28538 for &n in [5usize, 13, 7, 16].iter().cycle() {
28539 if pos >= ids.len() {
28540 break;
28541 }
28542 let end = (pos + n).min(ids.len());
28543 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
28544 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
28545 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
28546 pos = end;
28547 }
28548 for _ in 0..23 {
28549 let e = a.embed_single(((pos * 7) % 64) as u32);
28550 let ha = a.forward_layers(&e, pos, None);
28551 let hb = b.forward_layers(&e, pos, None);
28552 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
28553 for li in 0..3 {
28554 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
28555 }
28556 pos += 1;
28557 }
28558 for round in 0..9 {
28559 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
28560 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
28561 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
28562 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
28563 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
28564 if round % 2 == 1 {
28565 for p in [&mut a, &mut b] {
28567 for l in &mut p.kv_cache.layers {
28568 l.truncate_last(2);
28569 }
28570 }
28571 } else {
28572 pos += 2;
28573 }
28574 }
28575 let e = a.embed_single(9);
28576 let (ha, hb) = (
28577 a.forward_layers(&e, pos, None),
28578 b.forward_layers(&e, pos, None),
28579 );
28580 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
28581 for li in 0..3 {
28582 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
28583 assert!(lb.base() > 0, "layer {li} trimmed");
28584 assert_eq!(la.base(), 0);
28585 assert_eq!(lb.pos_len(), la.seq_len);
28586 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
28587 }
28588 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
28589 assert_eq!(
28590 (gb.base(), gb.seq_len),
28591 (0, ga.seq_len),
28592 "the global layer keeps all"
28593 );
28594 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
28595 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
28596 }
28597
28598 #[test]
28603 fn swa_trim_wire_handoff_continues_bitwise() {
28604 let mut a = spark_test_pipeline(None);
28605 let mut b = spark_test_pipeline(Some((2, 4)));
28606 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
28607 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
28608 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
28609 assert_eq!(f32_bits(&ha), f32_bits(&hb));
28610 let mut pos = ids.len();
28611 for _ in 0..5 {
28612 let e = a.embed_single(((pos * 5) % 64) as u32);
28613 assert_eq!(
28614 f32_bits(&a.forward_layers(&e, pos, None)),
28615 f32_bits(&b.forward_layers(&e, pos, None))
28616 );
28617 pos += 1;
28618 }
28619 let mut c = spark_test_pipeline(Some((2, 4)));
28620 for li in 0..4 {
28621 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
28622 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
28623 }
28624 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
28625 assert_eq!(c.kv_cache.seq_len(), pos);
28626 for step in 0..17 {
28627 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
28628 let ha = a.forward_layers(&e, pos, None);
28629 let hc = c.forward_layers(&e, pos, None);
28630 assert_eq!(
28631 f32_bits(&ha),
28632 f32_bits(&hc),
28633 "after the hand-off, step {step}"
28634 );
28635 pos += 1;
28636 }
28637 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
28638 }
28639
28640 fn mimo_test_mtp_widths(widths: &[usize], gain: f32) -> mimo_mtp::MimoMtp {
28645 let (hs, nh, hd, vd, nkv) = (16usize, 4usize, 8usize, 4usize, 2usize);
28646 let synth = |len: usize, salt: usize| -> Vec<f32> {
28647 (0..len)
28648 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
28649 .collect()
28650 };
28651 let qt = |rows: usize, cols: usize, salt: usize| {
28652 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
28653 };
28654 let layers = widths
28655 .iter()
28656 .copied()
28657 .enumerate()
28658 .map(|(k, inter)| {
28659 let s = 500 + k * 40;
28660 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
28661 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
28662 MtpModule {
28663 enorm: vec![1.0; hs],
28664 hnorm: vec![1.0; hs],
28665 eh_proj: qt(hs, 2 * hs, s),
28666 layer: LayerWeights {
28667 input_norm: vec![1.0; hs],
28668 post_norm: vec![1.0; hs],
28669 attn_out_norm: None,
28670 ffn_out_norm: None,
28671 layer_scale: None,
28672 attn: AttnKind::Full {
28673 wq: qt(nh * hd, hs, s + 1),
28674 wk: qt(nkv * hd, hs, s + 2),
28675 wv: qt(nkv * vd, hs, s + 3),
28676 wo: qt(hs, nh * vd, s + 4),
28677 q_norm: None,
28678 k_norm: None,
28679 output_gate: false,
28680 softplus_gate: None,
28681 bias: None,
28682 },
28683 ffn: FfnKind::Dense(DenseFfn {
28684 gate_proj: qt(inter, hs, s + 5),
28685 up_proj: qt(inter, hs, s + 6),
28686 down_proj: qt(hs, inter, s + 7),
28687 act: Act::Silu,
28688 down_t: None,
28689 segs: Vec::new(),
28690 }),
28691 },
28692 final_norm: vec![1.0; hs],
28693 kv,
28694 }
28695 })
28696 .collect();
28697 mimo_mtp::MimoMtp::from_layers(layers)
28698 }
28699
28700 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
28701 mimo_test_mtp_widths(&vec![24; n], gain)
28702 }
28703
28704 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
28705 p.clear_sequence_state();
28706 p.speculative = spec;
28707 p.ignore_eos = true;
28708 p.sampler_config.temperature = 0.0;
28709 p.generate_from_ids(ids, n, None, None).unwrap()
28710 }
28711
28712 #[test]
28713 fn mimo_mtp_ffn_probe_tracks_each_independent_draft_width() {
28714 let mut p = mimo_test_pipeline();
28715 let widths = [7usize, 13, 29];
28716 assert_ne!(widths[0], p.intermediate_size);
28717 assert_ne!(widths[1], p.intermediate_size);
28718 assert_ne!(widths[2], p.intermediate_size);
28719 p.mimo_mtp = Some(mimo_test_mtp_widths(&widths, 1.0));
28720 let ids: Vec<u32> = (0..11).map(|i| (i * 9 + 5) % 64).collect();
28721
28722 let trunk = p.probe_ffn_mass(&ids);
28723 assert!(trunk.iter().all(|v| v.len() == p.intermediate_size));
28724 let got = p.probe_mtp_ffn(&ids).expect("MiMo MTP probe");
28725 assert_eq!(got.len(), widths.len());
28726 for (k, (stat, &width)) in got.iter().zip(&widths).enumerate() {
28727 assert_eq!(stat.mass.len(), width, "draft layer {k}");
28728 assert_eq!(stat.squared_mass.len(), width, "draft layer {k}");
28729 assert_eq!(stat.observed_positions, ids.len() - k - 1);
28730 assert!(stat.mass.iter().any(|&v| v > 0.0), "draft layer {k}");
28731 assert!(stat
28732 .mass
28733 .iter()
28734 .zip(&stat.squared_mass)
28735 .all(|(&m, &s)| m.is_finite() && s.is_finite() && m >= 0.0 && s >= 0.0));
28736 }
28737 assert_eq!(p.kv_cache.seq_len(), 0, "probe leaves a fresh trunk");
28738 let st = p.mimo_mtp.as_ref().unwrap();
28739 assert!(st.layers.iter().all(|m| m.kv.seq_len == 0));
28740
28741 let h = p.hidden_size;
28744 let FfnKind::Dense(dense) = &mut p.mimo_mtp.as_mut().unwrap().layers[0].layer.ffn else {
28745 panic!("synthetic MiMo MTP must be dense")
28746 };
28747 dense.segs.push(FfnSeg {
28748 gate: QTensor::from_f32(vec![0.0; h], 1, h),
28749 up: QTensor::from_f32(vec![0.0; h], 1, h),
28750 down: QTensor::from_f32(vec![0.0; h], h, 1),
28751 start: dense.gate_proj.rows(),
28752 width: 1,
28753 });
28754 assert!(p
28755 .probe_mtp_ffn(&ids)
28756 .unwrap_err()
28757 .contains("tube-segmented draft FFNs are not supported"));
28758 assert_eq!(p.kv_cache.seq_len(), 0);
28759 assert!(p
28760 .mimo_mtp
28761 .as_ref()
28762 .unwrap()
28763 .layers
28764 .iter()
28765 .all(|m| m.kv.seq_len == 0));
28766 }
28767
28768 #[test]
28769 fn mimo_mtp_ffn_probe_chunking_preserves_teacher_forced_statistics() {
28770 fn collect(p: &mut Pipeline, ids: &[u32], rows: usize) -> Vec<MtpFfnProbe> {
28771 p.clear_sequence_state();
28772 let mut st = p.mimo_mtp.take().expect("synthetic MiMo draft");
28773 let _exact = ExactMtpCalibrationGuard::enter();
28774 let got = crate::gpu::cpu_scope(|| {
28775 let trunk = p.probe_trunk_rows(ids)?;
28776 p.mimo_mtp_ffn_probe_teacher_forced_with_chunk_rows(&mut st, ids, &trunk, rows)
28777 })
28778 .expect("teacher-forced MiMo probe");
28779 st.reset();
28780 p.mimo_mtp = Some(st);
28781 p.reset_mtp_probe_state();
28782 got
28783 }
28784
28785 for post_norm_hidden in [false, true] {
28786 let ids: Vec<u32> = (0..263u32).map(|i| (i * 17 + 5) % 64).collect();
28787 let make = || {
28788 let mut p = mimo_test_pipeline();
28789 let mut st = mimo_test_mtp_widths(&[7, 13, 29], 1.0);
28790 st.post_norm_hidden = post_norm_hidden;
28791 p.mimo_mtp = Some(st);
28792 p
28793 };
28794 let mut full = make();
28795 let expected = collect(&mut full, &ids, usize::MAX);
28796 for rows in [1usize, 7, 128] {
28797 let mut chunked = make();
28798 let got = collect(&mut chunked, &ids, rows);
28799 assert_eq!(got.len(), expected.len(), "post={post_norm_hidden}, rows={rows}");
28800 for (layer, (got, expected)) in got.iter().zip(&expected).enumerate() {
28801 assert_eq!(
28802 got.observed_positions, expected.observed_positions,
28803 "post={post_norm_hidden}, rows={rows}, layer={layer}"
28804 );
28805 assert_eq!(
28806 got.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
28807 expected.mass.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
28808 "post={post_norm_hidden}, rows={rows}, layer={layer} mass"
28809 );
28810 assert_eq!(
28811 got.squared_mass
28812 .iter()
28813 .map(|v| v.to_bits())
28814 .collect::<Vec<_>>(),
28815 expected
28816 .squared_mass
28817 .iter()
28818 .map(|v| v.to_bits())
28819 .collect::<Vec<_>>(),
28820 "post={post_norm_hidden}, rows={rows}, layer={layer} squared mass"
28821 );
28822 }
28823 }
28824 }
28825 }
28826
28827 #[test]
28833 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
28834 for post in [false, true] {
28837 let mut p = mimo_test_pipeline();
28838 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
28840 let mut st0 = mimo_test_mtp(3, 1.0);
28841 st0.post_norm_hidden = post;
28842 p.mimo_mtp = Some(st0);
28843 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
28844 let hs = p.hidden_size;
28845 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
28846 p.mimo_note_rows(&hb, 0);
28847 let mut st = p.mimo_mtp.take().unwrap();
28848 let k = 3;
28851 let mut inc = Vec::new();
28852 for t in 0..ids.len() - k - 1 {
28853 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
28854 }
28855 let s = ids.len();
28858 let mut reference = vec![vec![0u32; k]; s - k - 1];
28859 let mut fresh = mimo_test_mtp(3, 1.0);
28860 for (layer, m) in fresh.layers.iter_mut().enumerate() {
28861 let n = s - layer - 1;
28862 let mut cats = vec![0.0f32; n * 2 * hs];
28863 for j in 0..n {
28864 let e = p.embed_single(ids[j + layer + 1]);
28865 let raw = &hb[j * hs..(j + 1) * hs];
28866 let g = if post {
28867 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
28868 } else {
28869 raw.to_vec()
28870 };
28871 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
28872 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
28873 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
28874 }
28875 let mut x = vec![0.0f32; n * hs];
28876 m.eh_proj.matmat(&cats, n, &mut x, None);
28877 p.mimo_mtp_block(m, &mut x, n, 0);
28878 for (t, row) in reference.iter_mut().enumerate() {
28879 let y = inference::rms_norm(
28880 &x[t * hs..(t + 1) * hs],
28881 &m.final_norm,
28882 p.rms_eps,
28883 p.norm_style,
28884 );
28885 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
28886 }
28887 }
28888 assert_eq!(inc, reference, "post_norm_hidden = {post}");
28889 let distinct: std::collections::HashSet<u32> = inc.iter().flatten().copied().collect();
28891 assert!(distinct.len() > 3, "{inc:?}");
28892 let last_t = ids.len() - k - 2;
28894 for m in &st.layers {
28895 assert_eq!(m.kv.seq_len, last_t + 1);
28896 }
28897 }
28898 }
28899
28900 #[test]
28907 fn mimo_speculative_greedy_equals_plain_greedy() {
28908 let _env_lock = MTP_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
28909 let _env = ScopedTestEnv::set(&[
28910 ("CMF_GPU", Some("0")),
28911 ("CMF_GPU_WGPU_GRAPH", Some("0")),
28912 ]);
28913 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
28914 let n = 24;
28915 let mut p = mimo_test_pipeline();
28916 let plain = mimo_greedy(&mut p, &ids, n, false);
28917 assert_eq!(plain.mtp_drafted, 0);
28918 assert_eq!(plain.token_ids.len(), n);
28919 let plain_kv = p.kv_cache.layers[0].seq_len;
28920
28921 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
28923 let spec = mimo_greedy(&mut p, &ids, n, true);
28924 assert!(spec.mtp_drafted > 0, "the round must draft");
28925 assert_eq!(spec.token_ids, plain.token_ids);
28926 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28927
28928 let mut truth: Vec<u32> = ids.clone();
28931 truth.extend(&plain.token_ids);
28932 let mut noisy = truth.clone();
28933 for (i, t) in noisy.iter_mut().enumerate() {
28934 if i % 5 == 0 {
28935 *t = (*t + 1) % 64;
28936 }
28937 }
28938 let mut st = mimo_test_mtp(3, 1.0);
28939 st.draft_override = Some(noisy);
28940 p.mimo_mtp = Some(st);
28941 let spec = mimo_greedy(&mut p, &ids, n, true);
28942 assert_eq!(spec.token_ids, plain.token_ids);
28943 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28944 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
28945 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
28946 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
28947 assert!(
28948 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
28949 "{:?}",
28950 stats.accept_hist
28951 );
28952 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
28953
28954 let mut st = mimo_test_mtp(3, 1.0);
28957 st.draft_override = Some(truth);
28958 p.mimo_mtp = Some(st);
28959 let spec = mimo_greedy(&mut p, &ids, n, true);
28960 assert_eq!(spec.token_ids, plain.token_ids);
28961 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
28962 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
28963
28964 let off = mimo_greedy(&mut p, &ids, n, false);
28966 assert_eq!(off.token_ids, plain.token_ids);
28967 assert_eq!(off.mtp_drafted, 0);
28968 }
28969
28970 #[test]
28979 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
28980 let p = mimo_test_pipeline();
28981 assert_eq!(
28982 p.graph_attn_decline_reason(),
28983 Some("per-layer KV head counts")
28984 );
28985 assert_eq!(p.wgpu_graph_attn_decline(), None);
28986 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
28987 assert_eq!(
28988 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
28989 (1, 4, 4, None, false)
28990 );
28991 assert_eq!(g0.invf, p.inv_freq.as_slice());
28992 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
28993 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
28994 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
28995 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
28996 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
28997 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
28998 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
28999
29000 let emb = p.embed_single(3);
29003 let mut lg = Vec::new();
29004 assert!(
29005 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
29006 .is_none()
29007 );
29008 let mut hid = emb.clone();
29009 assert_eq!(
29010 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
29011 crate::gpu::BatchGraphOutcome::Declined
29012 );
29013 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
29014 assert!(p.try_multi_burst(3, 0, 4).is_none());
29015 assert!(
29016 p.graph_declines().is_empty(),
29017 "no attention decline logged: {:?}",
29018 p.graph_declines()
29019 );
29020 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
29025 assert_eq!(plain().graph_attn_decline_reason(), None);
29026 assert_eq!(plain().wgpu_graph_attn_decline(), None);
29027 assert!(
29028 plain().graph_attn_geom(0).is_none(),
29029 "uniform models keep the historical arms"
29030 );
29031 let mut q = plain();
29032 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
29033 assert_eq!(
29034 q.graph_attn_decline_reason(),
29035 Some("learned attention sinks")
29036 );
29037 assert_eq!(
29038 q.graph_attn_geom(1).unwrap().sink,
29039 Some(&[0.25f32, -0.25][..])
29040 );
29041 let mut q = plain();
29042 q.set_attn_geometry(None, Some(2)).unwrap();
29043 assert_eq!(
29044 q.graph_attn_decline_reason(),
29045 Some("V heads narrower than Q/K heads")
29046 );
29047 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
29048 let mut q = plain();
29049 q.sliding_layers = Some(vec![true, false]);
29050 q.swa = Some((4, usize::MAX));
29051 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
29052 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
29053 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
29054
29055 let mut q = mimo_test_pipeline();
29059 q.rope_scale = 2.0;
29060 q.rope_scale_local = 1.25;
29061 assert_eq!(q.wgpu_graph_attn_decline(), None);
29062 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
29063 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
29064
29065 let mut q = plain();
29068 q.rope_scale = 2.0;
29069 assert_eq!(
29070 q.wgpu_graph_attn_decline(),
29071 Some("scaled RoPE without per-layer geometry")
29072 );
29073 }
29074
29075 #[cfg(target_os = "macos")]
29081 #[test]
29082 fn metal_swa_admits_uniform_learned_sinks_for_q1_decode() {
29083 let mut p = create_test_pipeline(256, 512, 4, 1, 64, 2, 64);
29087 p.swa = Some((16, usize::MAX));
29088 p.sliding_layers = Some(vec![true, true]);
29089 p.set_layer_sinks(0, vec![0.25, -0.5, 1.0, -1.25]).unwrap();
29090 p.set_layer_sinks(1, vec![-0.75, 0.5, 0.0, 1.5]).unwrap();
29091
29092 assert_eq!(
29093 p.graph_attn_decline_reason(),
29094 Some("learned attention sinks")
29095 );
29096 let swa_graph = p.metal_graph_swa();
29097 assert!(swa_graph, "uniform sliding sink geometry must reach q1");
29098 let native_sink_attend = p.graph_attn_decline_reason() == Some("learned attention sinks")
29099 && (swa_graph || (p.swa.is_none() && p.sliding_layers.is_none()));
29100 assert!(
29101 native_sink_attend,
29102 "the q1 sink admission must follow the SWA door"
29103 );
29104 assert!(p.kv_cache.layers.iter().any(|l| l.sinks.is_some()));
29107 }
29108
29109 #[test]
29110 fn mimo_verify_rewind_preserves_lagging_host_caches() {
29111 let mut p = mimo_test_pipeline();
29112 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
29113 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
29114 for _ in 0..if li == 0 { 2 } else { 12 } {
29115 layer.append(&row, &row, &[]);
29116 }
29117 }
29118 p.mimo_verify_rewind(9).unwrap();
29119 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
29120 for layer in &p.kv_cache.layers[1..] {
29121 assert_eq!(layer.seq_len, 9);
29122 }
29123 }
29124
29125 #[test]
29129 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
29130 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
29131 let _ = std::fs::remove_dir_all(&dir);
29132 let mut p = mimo_test_pipeline();
29133 let hs = p.hidden_size;
29134 let ids = [5u32, 9, 11, 2, 40];
29135 p.layer_dump = Some(dir.join("decode"));
29136 for (pos, &id) in ids.iter().enumerate() {
29137 let e = p.embed_single(id);
29138 let _ = p.forward_layers(&e, pos, None);
29139 }
29140 p.clear_sequence_state();
29141 p.layer_dump = Some(dir.join("prefill"));
29142 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29143 for pos in 0..ids.len() {
29144 for li in 0..p.num_layers {
29145 let name = format!("p{pos:06}_l{li:02}.f32");
29146 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
29147 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
29148 assert_eq!(a.len(), hs * 4, "{name}");
29149 assert_eq!(a, b, "{name}");
29150 }
29151 }
29152 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
29153 let vals: Vec<f32> = last
29154 .chunks(4)
29155 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
29156 .collect();
29157 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
29158 let _ = std::fs::remove_dir_all(&dir);
29159 }
29160
29161 #[test]
29162 fn attn_geometry_and_sinks_are_validated() {
29163 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
29164 assert!(
29165 p.set_attn_geometry(Some(vec![2]), None).is_err(),
29166 "one entry per layer"
29167 );
29168 assert!(
29169 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
29170 "3 does not divide 4"
29171 );
29172 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
29173 assert!(p.set_attn_geometry(None, Some(0)).is_err());
29174 assert!(
29175 p.set_attn_geometry(None, Some(5)).is_err(),
29176 "V wider than the head"
29177 );
29178 p.set_attn_geometry(None, Some(4)).unwrap();
29179 assert_eq!(
29180 p.v_head_dim, None,
29181 "v_head_dim == head_dim is the uniform case"
29182 );
29183 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
29184 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
29185 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
29186 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
29187 assert!(
29188 p.kv_cache.layers[1].sinks.is_some(),
29189 "a reshape keeps the layer's sinks"
29190 );
29191 assert_eq!(p.layer_geom(1).0, 4);
29192 assert!(
29193 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
29194 "one sink per Q head"
29195 );
29196 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
29197 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
29198 }
29199
29200 #[test]
29203 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
29204 let cfg = || {
29205 Some(crate::nystrom::O1Cfg {
29206 layers: crate::nystrom::O1Layers::All,
29207 m: 4,
29208 w: 8,
29209 sink: 2,
29210 rect: crate::nystrom::O1Rect::Aggregate,
29211 })
29212 };
29213 let mut p = mimo_test_pipeline();
29214 p.set_o1(cfg());
29215 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
29216 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
29217 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
29218 q.sliding_layers = Some(vec![false, false, true]);
29219 q.swa = Some((4, usize::MAX));
29220 q.set_o1(cfg());
29221 assert_eq!(q.o1_flags, vec![true, false, false]);
29222 }
29223
29224 #[test]
29225 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
29226 const B: usize = 19;
29227 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29228 p.set_o1(Some(crate::nystrom::O1Cfg {
29229 layers: crate::nystrom::O1Layers::All,
29230 m: 4,
29231 w: 8,
29232 sink: 2,
29233 rect: crate::nystrom::O1Rect::Aggregate,
29234 }));
29235 p.o1_begin_with_prefix(Some(B));
29236 let ids: Vec<u32> = (0..B as u32).collect();
29237 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29238
29239 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
29240 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
29241 let next = p.embed_single(B as u32);
29242 let _ = p.forward_layers(&next, B, None);
29243 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
29244 }
29245
29246 #[test]
29251 fn o1_exact_rows_are_chosen_before_a_batched_boundary_seals() {
29252 const B: usize = 19;
29253 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29254 let cfg = crate::nystrom::O1Cfg {
29255 layers: crate::nystrom::O1Layers::All,
29256 m: 4,
29257 w: 8,
29258 sink: 2,
29259 rect: crate::nystrom::O1Rect::Aggregate,
29260 };
29261 p.set_o1(Some(cfg.clone()));
29262 p.o1_turbo = Some(O1Turbo {
29263 cfg,
29264 seal: B,
29265 exact: crate::dory::ExactRows { weights: std::sync::Arc::new(vec![1.0; 2 * 2]), budget: 3 },
29266 });
29267 p.o1_begin_with_prefix(Some(B));
29268 let ids: Vec<u32> = (0..B as u32).collect();
29269 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
29270 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
29272 let keep = p.kv_cache.layers[0].o1_keep.clone();
29273 assert_eq!(keep.len(), 3, "the budget of exact rows is spent");
29274 assert_eq!(keep, p.kv_cache.layers[1].o1_keep, "the same rows in every layer");
29275 assert!(keep.iter().all(|&r| (2..B - 8).contains(&r)), "never a sink or a window row");
29276 }
29277
29278 #[test]
29279 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
29280 const B: usize = 19;
29281 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
29282 let gdn_cfg = crate::linear_core::GdnCfg {
29286 num_v_heads: 2,
29287 num_k_heads: 1,
29288 key_head_dim: 2,
29289 value_head_dim: 4,
29290 conv_kernel: 3,
29291 hidden_size: 8,
29292 rms_eps: 1e-6,
29293 output_gate_sigmoid: false,
29294 };
29295 let synth = |n: usize, salt: usize| -> Vec<f32> {
29296 (0..n)
29297 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
29298 .collect()
29299 };
29300 let qt = |rows: usize, cols: usize, salt: usize| {
29301 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
29302 };
29303 let c_dim = gdn_cfg.conv_dim();
29304 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
29305 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
29306 in_proj_qkv: qt(c_dim, 8, 1),
29307 in_proj_z: qt(vd, 8, 2),
29308 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
29309 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
29310 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
29311 a_log: vec![0.2, 0.5],
29312 dt_bias: synth(gdn_cfg.num_v_heads, 6),
29313 norm: vec![1.0; gdn_cfg.value_head_dim],
29314 out_proj: qt(8, vd, 7),
29315 });
29316 p.gdn_cfg = Some(gdn_cfg);
29317 p.set_o1(Some(crate::nystrom::O1Cfg {
29318 layers: crate::nystrom::O1Layers::All,
29319 m: 4,
29320 w: 8,
29321 sink: 2,
29322 rect: crate::nystrom::O1Rect::Aggregate,
29323 }));
29324 p.o1_begin_with_prefix(Some(B));
29325 for pos in 0..B - 2 {
29326 let emb = p.embed_single(pos as u32);
29327 let _ = p.forward_layers(&emb, pos, None);
29328 }
29329 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
29330
29331 let e1 = p.embed_single((B - 2) as u32);
29332 let e2 = p.embed_single((B - 1) as u32);
29333 let _ = p.forward_pair(&e1, &e2, B - 2);
29334
29335 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
29336 assert!(
29337 p.kv_cache
29338 .layers
29339 .iter()
29340 .enumerate()
29341 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
29342 );
29343 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
29344 assert_ne!(
29345 p.kv_cache.layers[0].linear_state, lane1_state,
29346 "real pair must commit GDN lane 2 before returning"
29347 );
29348 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
29349 let next = p.embed_single(B as u32);
29350 let _ = p.forward_layers(&next, B, None);
29351 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
29352 }
29353
29354 #[test]
29355 fn o1_error_observation_stays_terminal_until_reset() {
29356 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29357 p.set_o1(Some(crate::nystrom::O1Cfg {
29358 layers: crate::nystrom::O1Layers::All,
29359 m: 4,
29360 w: 8,
29361 sink: 2,
29362 rect: crate::nystrom::O1Rect::Aggregate,
29363 }));
29364 p.o1_begin();
29365 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
29366
29367 assert!(p.o1_seal_checked().is_err());
29368 assert!(
29369 p.o1_seal_checked().is_err(),
29370 "retry must see the sticky error"
29371 );
29372 let k = vec![0.2f32; 4];
29373 let v = vec![0.3f32; 4];
29374 p.kv_cache.layers[0].append(&k, &v, &[]);
29375 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
29376
29377 p.reset_session();
29378 p.o1_begin();
29379 p.kv_cache.layers[0].append(&k, &v, &[]);
29380 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
29381 }
29382
29383 #[test]
29384 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
29385 let ids = vec![1u32, 2, 3, 4, 5, 6];
29386 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29387 p.graph_logits = Some(vec![123.0]);
29388 p.graph_want_logits = true;
29389 p.graph_failed
29390 .store(true, std::sync::atomic::Ordering::Relaxed);
29391 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
29392 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
29393 assert!(err.contains("before NLL"));
29394 assert!(p.graph_logits.is_none());
29395 assert!(!p.graph_want_logits);
29396 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29397 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29398
29399 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29400 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
29401 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
29402 assert_eq!(actual.1, expected.1);
29403 assert!((actual.0 - expected.0).abs() < 1e-9);
29404 }
29405
29406 #[test]
29407 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
29408 let ids = vec![1u32, 2, 3, 4, 5, 6];
29409 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29410 p.nll_test_fail_at = Some(1);
29411 let err = p
29412 .nll_ids_from(&ids, 0)
29413 .expect_err("one-shot forward failure");
29414 assert!(err.contains("forward") || err.contains("score row"));
29415 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29416 assert!(!p.graph_want_logits);
29417 assert!(p.graph_logits.is_none());
29418 assert!(p.kv_history.is_empty());
29419
29420 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29421 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
29422 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
29423 assert_eq!(actual.1, expected.1);
29424 assert!((actual.0 - expected.0).abs() < 1e-9);
29425 }
29426
29427 #[test]
29428 fn nll_serial_failure_before_first_row_is_reported() {
29429 let ids = vec![1u32, 2, 3, 4];
29430 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29431 p.nll_test_force_serial = true;
29432 p.nll_test_fail_at = Some(0);
29433 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
29434 assert!(err.contains("serial forward"));
29435 assert!(p.kv_history.is_empty());
29436 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29437 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29438 }
29439
29440 #[test]
29441 fn ffn_probe_failure_discards_recorder_and_state() {
29442 let ids = vec![1u32, 2, 3, 4];
29443 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29444 p.nll_test_fail_at = Some(0);
29445 let err = p
29446 .probe_ffn_mass_batch(&ids)
29447 .expect_err("probe forward failure");
29448 assert!(err.contains("NLL"));
29449 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
29450 assert!(p.kv_history.is_empty());
29451 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29452 }
29453
29454 #[test]
29455 fn nll_test_controls_are_pipeline_scoped() {
29456 let ids = vec![1u32, 2, 3, 4];
29457 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29458 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29459 failing.nll_test_force_serial = true;
29460 failing.nll_test_fail_at = Some(0);
29461
29462 assert!(!failing.can_prefill_batched());
29463 assert!(unaffected.can_prefill_batched());
29464 let expected = unaffected
29465 .nll_ids_from(&ids, 0)
29466 .expect("unaffected pipeline remains usable");
29467 let err = failing
29468 .nll_ids_from(&ids, 0)
29469 .expect_err("failure injection belongs to failing pipeline");
29470 assert!(err.contains("serial forward"));
29471 assert!(failing.nll_test_fail_at.is_none());
29472 assert!(unaffected.can_prefill_batched());
29473 let actual = unaffected
29474 .nll_ids_from(&ids, 0)
29475 .expect("unaffected pipeline remains reusable");
29476 assert_eq!(actual.1, expected.1);
29477 assert!((actual.0 - expected.0).abs() < 1e-9);
29478 }
29479
29480 #[test]
29481 fn forward_ids_failure_channel_is_terminal_and_reusable() {
29482 let ids = vec![1u32, 2, 3, 4, 5, 6];
29483 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
29484 p.graph_logits = Some(vec![123.0]);
29485 p.graph_want_logits = true;
29486 p.graph_failed
29487 .store(true, std::sync::atomic::Ordering::Relaxed);
29488 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
29489
29490 let err = p
29491 .forward_ids(&ids, None)
29492 .expect_err("a failed forward must not become a valid head result");
29493 assert!(err.contains("forward_ids setup"));
29494 assert!(p.graph_logits.is_none());
29495 assert!(!p.graph_want_logits);
29496 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
29497 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
29498 assert_eq!(p.kv_cache.seq_len(), 0);
29499
29500 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
29501 .forward_ids(&ids, None)
29502 .expect("fresh forward_ids");
29503 let actual = p
29504 .forward_ids(&ids, None)
29505 .expect("pipeline remains reusable after a failed forward");
29506 assert_eq!(actual.len(), expected.len());
29507 assert!(
29508 actual
29509 .iter()
29510 .zip(expected)
29511 .all(|(a, b)| (a - b).abs() < 1e-9)
29512 );
29513 assert_eq!(p.kv_cache.seq_len(), ids.len());
29514 }
29515
29516 #[test]
29517 fn sigmoid_router_floor_is_explicit_per_architecture() {
29518 let zero = || DenseFfn {
29524 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29525 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29526 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29527 act: Act::Silu,
29528 down_t: None,
29529 segs: Vec::new(),
29530 };
29531 let m = MoeFfn {
29532 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29533 experts: vec![zero(), zero()],
29534 top_k: 1,
29535 norm_topk_prob: true,
29536 router_sigmoid: true,
29537 sigmoid_denom_eps: 1e-20,
29538 expert_bias: None,
29539 routed_scaling: 2.5,
29540 route_tau: None,
29541 shared: None,
29542 stats: std::cell::RefCell::new(Vec::new()),
29543 act_sq: std::cell::RefCell::new(Vec::new()),
29544 act_rows: std::cell::RefCell::new(Vec::new()),
29545 mask: None,
29546 per_expert_scale: None,
29547 router_input_norm: false,
29548 resonance: None,
29549 grown: Vec::new(),
29550 };
29551 let logits = [-20.0f32, -20.0];
29552 let (_, p, flint_wsum) = moe_route(&logits, &m, None);
29553 let (_, _, minimax_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29554 let (_, _, legacy_wsum) = moe_route_with_eps(&logits, &m, None, 1e-6);
29555 let reference_expected = (p[0] + 1e-20) / m.routed_scaling;
29556 assert!((flint_wsum - reference_expected).abs() < 1e-15);
29557 assert_eq!(minimax_wsum.to_bits(), reference_expected.to_bits());
29558 assert!(legacy_wsum > minimax_wsum * 100.0);
29559 }
29560
29561 #[test]
29562 fn resonance_scores_match_formula_and_stable_tie() {
29563 let r = Resonance {
29564 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
29566 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
29567 k: 1,
29568 bias: vec![1.5, 0.5, 0.0],
29569 shell: Vec::new(),
29570 };
29571 let x = [1.0f32, 1.0];
29572 let mut got = vec![0.0; 3];
29573 r.scores(&x, &mut got);
29574 assert!((got[0] - 0.5).abs() < 1e-6);
29578 assert!((got[1] - 0.5).abs() < 1e-6);
29579 assert!(got[2].abs() < 1e-6);
29580 let best = got
29581 .iter()
29582 .enumerate()
29583 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
29584 .map(|(i, _)| i);
29585 assert_eq!(best, Some(0));
29586 assert!(got.iter().all(|v| v.is_finite()));
29587 }
29588
29589 #[test]
29594 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
29595 let plain = Resonance {
29601 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
29602 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
29603 k: 1,
29604 bias: vec![1.5, 0.5, 0.0, 0.0],
29605 shell: Vec::new(),
29606 };
29607 let shelled = Resonance {
29608 mu: plain.mu.clone(),
29609 u: plain.u.clone(),
29610 k: 1,
29611 bias: plain.bias.clone(),
29612 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
29613 };
29614 assert!(!plain.has_shell());
29615 assert!(shelled.has_shell());
29616 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
29617 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
29618 set_growth_shell(Some(true));
29619 assert!(growth_shell_enabled());
29620 let x = [1.0f32, 1.0];
29623 plain.scores(&x, &mut a);
29624 shelled.scores(&x, &mut b);
29625 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
29626 assert!(a[2] == 0.0 && a[3] == 0.0);
29627 let xo = [3.0f32, 0.0];
29630 plain.scores(&xo, &mut a);
29631 shelled.scores(&xo, &mut b);
29632 assert_eq!(a[2], -6.0);
29633 assert_eq!(a[3], -6.0);
29634 assert_eq!(
29635 bits(&a[..3]),
29636 bits(&b[..3]),
29637 "trunk rows + the boundary row"
29638 );
29639 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
29640 assert_eq!(shelled.effective_shell(4), shelled.shell);
29641 set_growth_shell(Some(false));
29644 assert!(!growth_shell_enabled());
29645 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
29646 shelled.scores(&xo, &mut b);
29647 assert_eq!(bits(&a), bits(&b));
29648 set_growth_shell(None);
29649 let short = Resonance {
29652 shell: vec![f32::INFINITY, f32::INFINITY],
29653 ..shelled
29654 };
29655 set_growth_shell(Some(true));
29656 short.scores(&xo, &mut b);
29657 assert_eq!(bits(&a), bits(&b));
29658 set_growth_shell(None);
29659 }
29660
29661 #[test]
29665 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
29666 let zero = || DenseFfn {
29667 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29668 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29669 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29670 act: Act::Silu,
29671 down_t: None,
29672 segs: Vec::new(),
29673 };
29674 let moe = |sigmoid: bool| MoeFfn {
29675 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
29676 experts: vec![zero(), zero(), zero(), zero()],
29677 top_k: 1,
29678 norm_topk_prob: true,
29679 router_sigmoid: sigmoid,
29680 sigmoid_denom_eps: 1e-6,
29681 expert_bias: None,
29682 routed_scaling: 1.0,
29683 route_tau: None,
29684 shared: None,
29685 stats: std::cell::RefCell::new(Vec::new()),
29686 act_sq: std::cell::RefCell::new(Vec::new()),
29687 act_rows: std::cell::RefCell::new(Vec::new()),
29688 mask: None,
29689 per_expert_scale: None,
29690 router_input_norm: false,
29691 resonance: None,
29692 grown: Vec::new(),
29693 };
29694 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
29695 for sigmoid in [false, true] {
29696 let m = moe(sigmoid);
29697 let (idx, p, wsum) = moe_route(&logits, &m, None);
29698 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
29699 assert_eq!(p[1], 0.0);
29700 assert_eq!(p[3], 0.0);
29701 assert!(p[2] > p[0] && p[0] > 0.0);
29702 assert!(p.iter().all(|v| v.is_finite()));
29703 let w = p[2] / wsum;
29704 if sigmoid {
29705 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
29707 } else {
29708 assert_eq!(
29709 w, 1.0,
29710 "softmax: the renormalized top-1 weight is exactly 1"
29711 );
29712 }
29713 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
29716 assert_eq!(idx, vec![2]);
29717 }
29718 let m = moe(false);
29720 let (idx, _, _) = moe_route(
29721 &[
29722 f32::NEG_INFINITY,
29723 f32::NEG_INFINITY,
29724 f32::NEG_INFINITY,
29725 -9.0,
29726 ],
29727 &m,
29728 None,
29729 );
29730 assert_eq!(idx, vec![3]);
29731 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
29734 assert_eq!(idx, vec![0]);
29735 assert!(p.iter().all(|&v| v == 0.25));
29736 assert!(wsum.is_finite() && wsum > 0.0);
29737 }
29738
29739 #[cfg(feature = "gpu")]
29745 #[test]
29746 fn minimax_device_route_matches_host_sigmoid_contract() {
29747 match crate::gpu_wgpu::selected_and_up() {
29748 None => {
29749 crate::gpu_wgpu::skip_or_fail(module_path!());
29750 return;
29751 }
29752 Some(false) => {
29760 eprintln!(
29761 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
29762 module_path!()
29763 );
29764 return;
29765 }
29766 Some(true) => {}
29767 }
29768 let n = 128usize;
29769 let top_k = 4usize;
29770 let logits: Vec<f32> = (0..n)
29771 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
29772 .collect();
29773 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
29774 let m = MoeFfn {
29775 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29776 experts: Vec::new(),
29777 top_k,
29778 norm_topk_prob: true,
29779 router_sigmoid: true,
29780 sigmoid_denom_eps: 1e-20,
29781 expert_bias: Some(bias.clone()),
29782 routed_scaling: 2.0,
29783 route_tau: None,
29784 shared: None,
29785 stats: std::cell::RefCell::new(Vec::new()),
29786 act_sq: std::cell::RefCell::new(Vec::new()),
29787 act_rows: std::cell::RefCell::new(Vec::new()),
29788 mask: None,
29789 per_expert_scale: None,
29790 router_input_norm: false,
29791 resonance: None,
29792 grown: Vec::new(),
29793 };
29794 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29795 let cw: Vec<f32> = ci.iter().map(|&e| p[e] / wsum).collect();
29796
29797 for shared in [Some(n as u32), None] {
29798 let mut gi = Vec::new();
29799 let mut gw = Vec::new();
29800 assert!(
29801 crate::gpu_wgpu::minimax_moe_route_for_test(
29802 &logits,
29803 Some(&bias),
29804 top_k,
29805 2.0,
29806 shared,
29807 &mut gi,
29808 &mut gw,
29809 ),
29810 "the device did not come up — the test checked nothing"
29811 );
29812 let routed = if shared.is_some() { top_k } else { gi.len() };
29813 assert_eq!(
29814 &gi[..routed],
29815 &ci[..],
29816 "different experts or order (shared={shared:?})"
29817 );
29818 let d = cw
29819 .iter()
29820 .zip(&gw[..routed])
29821 .map(|(a, b)| (a - b).abs())
29822 .fold(0.0f32, f32::max);
29823 println!("minimax route shared={shared:?}: {ci:?} max|Δw|={d:.3e}");
29824 assert!(d < 1e-6, "the weights diverged by {d:.3e}");
29825 if let Some(s) = shared {
29826 assert_eq!(gi[top_k], s as usize, "shared pin slot");
29827 assert_eq!(gw[top_k], 1.0, "shared weight");
29828 }
29829 }
29830 }
29831
29832 #[cfg(feature = "gpu")]
29837 #[test]
29838 fn minimax_device_cold_weights_match_host_contract() {
29839 match crate::gpu_wgpu::selected_and_up() {
29840 None => {
29841 crate::gpu_wgpu::skip_or_fail(module_path!());
29842 return;
29843 }
29844 Some(false) => {
29852 eprintln!(
29853 "SKIPPED ({}): CMF_GPU names wgpu but no adapter came up in this process",
29854 module_path!()
29855 );
29856 return;
29857 }
29858 Some(true) => {}
29859 }
29860 let n = 128usize;
29861 let top_k = 4usize;
29862 let logits: Vec<f32> = (0..n)
29863 .map(|i| ((i as f32 * 0.61).sin() * 3.4) + ((i % 23) as f32 * 0.13))
29864 .collect();
29865 let bias: Vec<f32> = (0..n).map(|i| ((i as f32 * 2.3).cos()) * 0.5).collect();
29866 let m = MoeFfn {
29867 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
29868 experts: Vec::new(),
29869 top_k,
29870 norm_topk_prob: true,
29871 router_sigmoid: true,
29872 sigmoid_denom_eps: 1e-20,
29873 expert_bias: Some(bias.clone()),
29874 routed_scaling: 2.0,
29875 route_tau: None,
29876 shared: None,
29877 stats: std::cell::RefCell::new(Vec::new()),
29878 act_sq: std::cell::RefCell::new(Vec::new()),
29879 act_rows: std::cell::RefCell::new(Vec::new()),
29880 mask: None,
29881 per_expert_scale: None,
29882 router_input_norm: false,
29883 resonance: None,
29884 grown: Vec::new(),
29885 };
29886 let (ci, p, wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
29887 assert_eq!(ci.len(), top_k);
29888
29889 let resident: Vec<u32> = (0..n).map(|e| e as u32).collect();
29891 let mut cold = Vec::new();
29892 let mut idx = Vec::new();
29893 let mut dw = Vec::new();
29894 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
29895 &logits,
29896 Some(&bias),
29897 top_k,
29898 2.0,
29899 &resident,
29900 n as u32,
29901 &mut cold,
29902 &mut idx,
29903 &mut dw,
29904 ));
29905 assert!(cold.is_empty(), "fully resident bank: {cold:?}");
29906 for (r, &e) in ci.iter().enumerate() {
29911 assert_eq!(idx[r] as usize, e, "resident slot mapping r={r}");
29912 let host = p[e] / wsum;
29913 let d = (dw[r] - host).abs();
29914 assert!(
29915 d <= 1e-6 * host.abs().max(1.0),
29916 "resident weight r={r} expert {e}: device={:.9e} host={host:.9e}",
29917 dw[r]
29918 );
29919 }
29920 assert_eq!(idx[top_k] as usize, n, "shared pin slot");
29921 assert_eq!(dw[top_k], 1.0, "shared weight");
29922
29923 let mut remap = resident.clone();
29925 remap[ci[0]] = u32::MAX;
29926 remap[ci[2]] = u32::MAX;
29927 let mut cold = Vec::new();
29928 let mut idx2 = Vec::new();
29929 let mut dw2 = Vec::new();
29930 assert!(crate::gpu_wgpu::minimax_moe_route_cold_for_test(
29931 &logits,
29932 Some(&bias),
29933 top_k,
29934 2.0,
29935 &remap,
29936 n as u32,
29937 &mut cold,
29938 &mut idx2,
29939 &mut dw2,
29940 ));
29941 let mut resident_weight_checked = 0usize;
29949 for (r, &slot) in idx2.iter().enumerate().take(top_k) {
29950 if dw2[r] == 0.0 {
29951 continue; }
29953 let e = slot as usize;
29954 assert!(e < n && remap[e] != u32::MAX, "resident slot {e} escaped the remap");
29955 let host = p[e] / wsum;
29956 let d = (dw2[r] - host).abs();
29957 assert!(
29958 d <= 1e-6 * host.abs().max(1.0),
29959 "partial-bank resident weight expert {e}: Δ={d:.3e}"
29960 );
29961 resident_weight_checked += 1;
29962 }
29963 assert_eq!(resident_weight_checked, 2, "exactly two resident peaks out of four");
29964 println!("partial bank: checked {resident_weight_checked} resident weights");
29965 assert_eq!(dw2.get(top_k).copied(), Some(1.0), "shared weight under subset");
29966 let mut ids: Vec<usize> = cold.iter().map(|&(e, _)| e).collect();
29967 ids.sort_unstable();
29968 let mut want = vec![ci[0], ci[2]];
29969 want.sort_unstable();
29970 assert_eq!(ids, want, "the cold experts diverged: {cold:?} versus {want:?}");
29971 for &(e, w) in &cold {
29972 let host = p[e] / wsum;
29973 let d = (w - host).abs();
29974 println!("cold expert {e}: device={w:.9e} host={host:.9e} Δ={d:.3e}");
29975 assert!(d <= 1e-6 * host.abs().max(1.0), "cold weight of expert {e}: Δ={d:.3e}");
29976 }
29977 }
29978
29979 #[test]
29984 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
29985 let zero = || DenseFfn {
29986 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29987 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29988 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
29989 act: Act::Silu,
29990 down_t: None,
29991 segs: Vec::new(),
29992 };
29993 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
29994 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
29995 experts: vec![zero(), zero(), zero()],
29996 top_k: 1,
29997 norm_topk_prob: norm_topk,
29998 router_sigmoid: false,
29999 sigmoid_denom_eps: 1e-6,
30000 expert_bias: None,
30001 routed_scaling: 1.0,
30002 route_tau: None,
30003 shared: None,
30004 stats: std::cell::RefCell::new(Vec::new()),
30005 act_sq: std::cell::RefCell::new(Vec::new()),
30006 act_rows: std::cell::RefCell::new(Vec::new()),
30007 mask: None,
30008 per_expert_scale: None,
30009 router_input_norm: false,
30010 resonance: resonant.then(|| Resonance {
30011 mu: vec![0.0; 6],
30012 u: Vec::new(),
30013 k: 0,
30014 bias: vec![0.0; 3],
30015 shell: Vec::new(),
30016 }),
30017 grown: Vec::new(),
30018 };
30019 let lo = -0.1f32;
30022 let hi = f32::from_bits(lo.to_bits() - 1);
30023 assert!(hi > lo && hi - lo < 2f32.powi(-25));
30024 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
30025 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
30027 assert_eq!(
30028 idx,
30029 vec![0],
30030 "softmax tie → lower index (the gated contract)"
30031 );
30032 for norm in [true, false] {
30035 let m = moe(true, norm);
30036 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
30037 assert_eq!(idx, vec![1], "norm_topk {norm}");
30038 assert_eq!(p, vec![0.0, 1.0, 0.0]);
30039 assert_eq!(p[1] / wsum, 1.0);
30040 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
30043 assert_eq!(idx, vec![0]);
30044 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
30046 assert_eq!(idx, vec![2]);
30047 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
30048 assert_eq!(idx, vec![0]);
30049 assert_eq!(p[0] / wsum, 1.0);
30050 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
30053 assert_eq!(idx, vec![0]);
30054 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
30055 }
30056 }
30057}