1use crate::attention::{self, QwenAttnCfg};
10use crate::inference;
11
12#[path = "mimo_mtp.rs"]
15pub mod mimo_mtp;
16use crate::kv_cache::KvCache;
17use crate::linear_core::{
18 GdnCfg, GdnWeights, ShortConvCfg, ShortConvWeights, VmfPhaseCfg, VmfPhaseWeights, gdn_forward,
19 gdn_pair, short_conv_forward, short_conv_forward_batch, short_conv_pair, vmf_phase_forward,
20 vmf_phase_pair,
21};
22use crate::pool::Pool;
23use crate::qtensor::QTensor;
24use crate::sampler::{self, SamplerConfig, SamplerScratch, SplitMix64};
25use crate::tokenizer::Tokenizer;
26use cortiq_core::mask::TaskMask;
27use cortiq_core::types::NormStyle;
28
29pub static GLOBAL_USE_GPU: std::sync::atomic::AtomicBool =
30 std::sync::atomic::AtomicBool::new(false);
31
32struct ForwardScratch {
36 n1: Vec<f32>,
37 n2: Vec<f32>,
38 p1: Vec<f32>,
39 p2: Vec<f32>,
40}
41
42impl ForwardScratch {
43 fn new(hidden: usize) -> Self {
44 Self {
45 n1: vec![0.0; hidden],
46 n2: vec![0.0; hidden],
47 p1: vec![0.0; hidden],
48 p2: vec![0.0; hidden],
49 }
50 }
51}
52
53pub struct Pipeline {
55 gpu_plan: Option<std::sync::Arc<Vec<(usize, usize, usize)>>>,
60 pub tokenizer: std::sync::Arc<Tokenizer>,
63 pub kv_cache: KvCache,
64 pub sampler_config: SamplerConfig,
65 pub weights: PipelineWeights,
66 pub hidden_size: usize,
67 pub intermediate_size: usize,
68 pub num_heads: usize,
69 pub num_kv_heads: usize,
70 pub head_dim: usize,
71 pub num_layers: usize,
73 pub physical_layers: usize,
75 pub loop_final_norm: bool,
77 pub vocab_size: usize,
78 pub rms_eps: f64,
79 pub rope_base: f32,
80 pub norm_style: NormStyle,
81 pub rotary_dim: usize,
83 pub attention_heads_per_layer: Option<Vec<usize>>,
85 pub kv_heads_per_layer: Option<Vec<usize>>,
90 pub v_head_dim: Option<usize>,
95 pub layer_dump: Option<std::path::PathBuf>,
108 graph_declines: std::cell::RefCell<Vec<(&'static str, &'static str)>>,
111 pub(crate) mimo_moe: crate::mimo_moe::Slot,
114 pub vmf_cfg: Option<VmfPhaseCfg>,
116 pub gdn_cfg: Option<GdnCfg>,
118 pub logit_multiplier: Option<f32>,
120 pub cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
125 graph_failed: std::sync::atomic::AtomicBool,
130 pub kv_history: Vec<u32>,
135 pub kv_history_device: bool,
138 pub kda_cfg: Option<crate::linear_core::KdaCfg>,
140 pub g3n: Option<Box<(crate::g3n::G3nGlobals, Vec<crate::g3n::G3nLayer>)>>,
143 pub dsv4: Option<
147 Box<(
148 crate::dsv4::Dsv4Globals,
149 Vec<crate::dsv4::Dsv4Layer>,
150 crate::dsv4::Dsv4Cfg,
151 crate::dsv4::Dsv4State,
152 )>,
153 >,
154 pub dsv41: Option<
158 Box<(
159 crate::dsv41::Dsv41Globals,
160 Vec<crate::dsv41::Dsv41Layer>,
161 crate::dsv41::Dsv41Cfg,
162 crate::dsv41::Dsv41State,
163 )>,
164 >,
165 pub dsv41_vision: Option<crate::dsv41_vision::VisionModel>,
167 dsv41_prefill: Option<(Vec<Option<Vec<f32>>>, Vec<bool>)>,
169 pub qwen4_exp: Option<
172 Box<(
173 crate::qwen4_exp::Globals,
174 Vec<crate::qwen4_exp::Layer>,
175 crate::qwen4_exp::Cfg,
176 crate::qwen4_exp::State,
177 )>,
178 >,
179 pub dsv4_mtp: Vec<crate::dsv4::Dsv4Mtp>,
183 pub dspark: Option<crate::dsv4::DsparkState>,
185 pub dspark_pending: Vec<(usize, Vec<u32>, bool, usize)>,
188 pub dspark_hist: Vec<usize>,
190 pub dspark_real: Vec<u32>,
194 pub dspark_trunk_picks: Vec<Vec<(usize, Vec<usize>)>>,
198 pub dspark_exp: Vec<(usize, usize, usize, usize)>,
200 pub dspark_draft_ns: u128,
204 pub short_conv_cfg: Option<ShortConvCfg>,
207 pub mtp: Option<MtpModule>,
209 pub mimo_mtp: Option<mimo_mtp::MimoMtp>,
213 verify_exact_moe: bool,
216 pub speculative: bool,
218 pub ignore_eos: bool,
224 pub draft_full_streak: u32,
230 pub spec_k_adapt: Option<usize>,
238 pub spec_acc_ewma: f32,
240 rng: SplitMix64,
241 sampler_scratch: SamplerScratch,
242 spec_forced: Option<u32>,
248 spec_q: Vec<Vec<f32>>,
249 spec_p: Vec<f32>,
250 spec_res: Vec<f32>,
251 spec_qs: Vec<sampler::Sparse>,
253 spec_ps: sampler::Sparse,
254 spec_ress: sampler::Sparse,
255 mtp_graph_mode: Option<bool>,
262 #[cfg(target_os = "macos")]
265 metal_verify: Option<MetalVerifyPending>,
266 pub(crate) inv_freq: std::sync::Arc<Vec<f32>>,
270 ws: ForwardScratch,
274 pool: Option<std::sync::Arc<Pool>>,
276 pub(crate) model: Option<std::sync::Arc<cortiq_core::CmfModel>>,
280 pub(crate) dyn_force_f32: bool,
282 pub(crate) dyn_skill_layers: Vec<Option<Vec<usize>>>,
287 pub(crate) dyn_active: Option<usize>,
293 pub(crate) dyn_blend_loaded: bool,
297 pub(crate) dyn_phi_layer: Option<usize>,
300 dyn_phi_ema: Vec<f32>,
302 dyn_phi_seen: usize,
303 pub dyn_router: Option<crate::swarm::DynRouter>,
306 o1_cfg: Option<crate::nystrom::O1Cfg>,
309 o1_epoch: u64,
312 o1_flags: Vec<bool>,
314 trace: bool,
317 calib_temp: f32,
320 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
322 graph_kv_id: u64,
323 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
326 graph_want_logits: bool,
327 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
331 graph_head_required: bool,
332 graph_logits: Option<Vec<f32>>,
335 embryo_graph: Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>>,
338 graph_refused: std::sync::atomic::AtomicBool,
344 pub embed_multiplier: f32,
346 pub attn_scale: f32,
349 pub swa: Option<(usize, usize)>,
352 pub swa_trim: Option<(usize, usize)>,
359 pub sliding_layers: Option<Vec<bool>>,
362 pub anchor_core: Option<cortiq_core::AnchorCoreConfig>,
368 bounded_rope: Option<std::sync::Arc<crate::bounded::BoundedRope>>,
371 pub kv_prefix: KvPrefix,
375 pub last_prefill_tokens: usize,
378 pub inv_freq_local: Option<std::sync::Arc<Vec<f32>>>,
381 pub rotary_dim_local: Option<usize>,
382 pub rope_scale: f32,
383 pub rope_scale_local: f32,
384 pub global_attn: Option<(usize, usize)>,
387 pub inv_freq_global: Option<std::sync::Arc<Vec<f32>>>,
390 pub attn_v_norm: bool,
392 pub qk_norm_after_rope: bool,
394 pub proj_gate_sigmoid: bool,
397 pub final_softcap: Option<f32>,
399 pub head_clusters: Option<std::sync::Arc<Vec<f32>>>,
403 pub attn_softcap: f32,
405 confidence_on: bool,
409 #[cfg(test)]
412 nll_test_fail_at: Option<usize>,
413 #[cfg(test)]
416 nll_test_force_serial: bool,
417}
418
419#[cfg(target_os = "macos")]
420impl Drop for Pipeline {
421 fn drop(&mut self) {
422 let _ = crate::gpu_metal::wait_replay();
424 crate::gpu::kv_mirror_drop(self.graph_kv_id);
425 }
426}
427
428#[cfg(not(target_os = "macos"))]
429impl Drop for Pipeline {
430 fn drop(&mut self) {
431 crate::gpu::graph_kv_reset(self.graph_kv_id);
435 }
436}
437
438pub struct PipelineWeights {
443 pub embed_tokens: QTensor,
445 pub layers: Vec<LayerWeights>,
447 pub lm_head: QTensor,
449 pub final_norm: Vec<f32>,
451}
452
453pub struct LayerWeights {
455 pub input_norm: Vec<f32>,
456 pub post_norm: Vec<f32>,
459 pub attn_out_norm: Option<Vec<f32>>,
462 pub layer_scale: Option<f32>,
464 pub ffn_out_norm: Option<Vec<f32>>,
467 pub ffn: FfnKind,
468 pub attn: AttnKind,
469}
470
471#[derive(Clone, Copy, PartialEq, Debug, Default)]
474pub enum Act {
475 #[default]
476 Silu,
477 GeluTanh,
478 Gelu,
480 Situ {
483 beta: f32,
484 linear_beta: f32,
485 },
486}
487
488impl Act {
489 pub fn from_arch(name: &str) -> Self {
490 if name == "gelu_tanh" {
491 Self::GeluTanh
492 } else if name == "gelu" {
493 Self::Gelu
494 } else {
495 Self::Silu
496 }
497 }
498
499 pub fn from_arch_full(arch: &cortiq_core::ModelArch) -> Self {
501 match arch.hidden_act.as_str() {
502 "situ" => Self::Situ {
503 beta: arch.activation_situ_beta.unwrap_or(1.0) as f32,
504 linear_beta: arch.activation_situ_linear_beta.unwrap_or(0.0) as f32,
505 },
506 other => Self::from_arch(other),
507 }
508 }
509
510 #[inline]
511 pub fn apply(self, x: f32) -> f32 {
512 match self {
513 Self::Silu => inference::silu(x),
514 Self::GeluTanh => inference::gelu_tanh(x),
515 Self::Gelu => inference::gelu_erf(x),
516 Self::Situ { beta, .. } => beta * (x / beta).tanh() * (1.0 / (1.0 + (-x).exp())),
517 }
518 }
519
520 #[inline]
523 pub fn combine(self, g: f32, u: f32) -> f32 {
524 match self {
525 Self::Situ { linear_beta, .. } if linear_beta > 0.0 => {
526 self.apply(g) * (linear_beta * (u / linear_beta).tanh())
527 }
528 _ => self.apply(g) * u,
529 }
530 }
531
532 pub fn graph_act(self) -> Option<crate::gpu::GraphAct> {
536 match self {
537 Self::Silu => Some(crate::gpu::GraphAct::Silu),
538 Self::Gelu => Some(crate::gpu::GraphAct::GeluErf),
539 Self::GeluTanh => Some(crate::gpu::GraphAct::GeluTanh),
540 Self::Situ { .. } => None,
541 }
542 }
543}
544
545pub struct DenseFfn {
547 pub gate_proj: QTensor,
548 pub up_proj: QTensor,
549 pub down_proj: QTensor,
550 pub act: Act,
552 pub down_t: Option<QTensor>,
558 pub segs: Vec<FfnSeg>,
565}
566
567pub struct FfnSeg {
572 pub gate: QTensor,
573 pub up: QTensor,
574 pub down: QTensor,
575 pub start: usize,
576 pub width: usize,
577}
578
579pub enum FfnKind {
582 Dense(DenseFfn),
583 Moe(MoeFfn),
587 DenseMoe(Box<DenseMoeFfn>),
594}
595
596pub struct DenseMoeFfn {
598 pub dense: DenseFfn,
599 pub moe: MoeFfn,
600 pub post_norm_1: Vec<f32>,
602 pub pre_norm_2: Vec<f32>,
605 pub post_norm_2: Vec<f32>,
607}
608
609pub struct MoeFfn {
610 pub router: QTensor,
612 pub experts: Vec<DenseFfn>,
613 pub top_k: usize,
614 pub norm_topk_prob: bool,
615 pub router_sigmoid: bool,
618 pub expert_bias: Option<Vec<f32>>,
622 pub routed_scaling: f32,
625 pub route_tau: Option<f32>,
631 pub shared: Option<(DenseFfn, Option<QTensor>)>,
634 pub stats: std::cell::RefCell<Vec<u64>>,
638 pub act_sq: std::cell::RefCell<Vec<f64>>,
645 pub act_rows: std::cell::RefCell<Vec<f32>>,
651 pub mask: Option<Vec<bool>>,
656 pub per_expert_scale: Option<Vec<f32>>,
659 pub router_input_norm: bool,
663 pub resonance: Option<Resonance>,
667 pub grown: Vec<GrownExpert>,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq)]
677pub struct GrownExpert {
678 pub record: String,
680 pub record_index: usize,
682 pub layer: usize,
683 pub expert: usize,
687}
688
689pub struct Resonance {
691 pub mu: Vec<f32>,
693 pub u: Vec<f32>,
695 pub k: usize,
696 pub bias: Vec<f32>,
698 pub shell: Vec<f32>,
704}
705
706static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
709
710pub fn growth_shell_enabled() -> bool {
715 use std::sync::atomic::Ordering;
716 match GROWTH_SHELL.load(Ordering::Relaxed) {
717 1 => true,
718 2 => false,
719 _ => {
720 let off = std::env::var("CMF_GROWTH_SHELL")
721 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
722 .unwrap_or(false);
723 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
724 !off
725 }
726 }
727}
728
729pub fn set_growth_shell(on: Option<bool>) {
734 GROWTH_SHELL.store(
735 match on {
736 Some(true) => 1,
737 Some(false) => 2,
738 None => 0,
739 },
740 std::sync::atomic::Ordering::Relaxed,
741 );
742}
743
744impl Resonance {
745 pub fn has_shell(&self) -> bool {
747 self.shell.iter().any(|s| s.is_finite())
748 }
749
750 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
753 let mut out = vec![f32::INFINITY; ne];
754 if growth_shell_enabled() {
755 for (o, s) in out.iter_mut().zip(&self.shell) {
756 *o = *s;
757 }
758 }
759 out
760 }
761
762 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
767 let h = x.len();
768 let ne = out.len();
769 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
770 for e in 0..ne {
771 let mu = &self.mu[e * h..(e + 1) * h];
772 let mut d2 = 0.0f32;
773 for j in 0..h {
774 let d = x[j] - mu[j];
775 d2 += d * d;
776 }
777 let mut proj = 0.0f32;
778 for i in 0..self.k {
779 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
780 let mut p = 0.0f32;
781 for j in 0..h {
782 p += (x[j] - mu[j]) * u[j];
783 }
784 proj += p * p;
785 }
786 let err = d2 - proj;
787 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
788 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
789 out[e] = f32::NEG_INFINITY;
790 }
791 }
792 }
793}
794
795pub enum AttnKind {
798 Full {
800 wq: QTensor,
801 wk: QTensor,
802 wv: QTensor,
803 wo: QTensor,
804 q_norm: Option<Vec<f32>>,
805 k_norm: Option<Vec<f32>>,
806 output_gate: bool,
807 softplus_gate: Option<(QTensor, bool)>,
811 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
813 },
814 Linear(VmfPhaseWeights),
816 LinearGdn(GdnWeights),
818 ShortConv(ShortConvWeights),
821 Mla(Box<MlaWeights>),
829 Kda(Box<crate::linear_core::KdaWeights>),
833 Bounded(Box<crate::bounded::BoundedWeights>),
838}
839
840pub struct MlaWeights {
842 pub q_proj: QTensor,
846 pub q_a: Option<QTensor>,
849 pub q_a_norm: Option<Vec<f32>>,
850 pub kv_a: QTensor,
852 pub kv_a_norm: Vec<f32>,
854 pub kv_b: QTensor,
856 pub o_proj: QTensor,
858 pub nh: usize,
859 pub qk_rope: usize,
860 pub qk_nope: usize,
861 pub v_dim: usize,
862 pub lora: usize,
863 pub scale: f32,
865 pub nope: bool,
867}
868
869pub struct MtpModule {
874 pub enorm: Vec<f32>,
875 pub hnorm: Vec<f32>,
876 pub eh_proj: QTensor,
878 pub layer: LayerWeights,
879 pub final_norm: Vec<f32>,
880 pub kv: crate::kv_cache::LayerKvCache,
881}
882
883#[cfg(target_os = "macos")]
890enum MetalRowsItem<'a> {
891 Gdn {
892 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
893 first: usize,
894 },
895 Attn {
896 l: crate::gpu_metal::AttnGpuLayer<'a>,
897 li: usize,
898 q_norm: Option<&'a [f32]>,
899 k_norm: Option<&'a [f32]>,
900 output_gate: bool,
901 },
902}
903
904#[cfg(target_os = "macos")]
905struct MetalVerifyPending {
906 graph: crate::gpu_metal::VerifyGraph,
907 gdn_layers: Vec<usize>,
908 attn_layers: Vec<(usize, usize)>,
909}
910
911#[cfg(target_os = "macos")]
915struct MetalWarmPending {
916 graph: crate::gpu_metal::VerifyGraph,
917 cpu_stored: usize,
918 b: usize,
919}
920
921#[cfg(target_os = "macos")]
922enum MetalRowsRun {
923 Declined,
925 Failed,
928 Completed(MetalVerifyPending),
929}
930
931#[cfg(target_os = "macos")]
932enum MetalPrefillOutcome {
933 Declined,
934 Failed,
935 Completed(Vec<f32>),
936}
937
938#[cfg(target_os = "macos")]
939enum MetalBatchNllOutcome {
940 Declined,
941 Failed(String),
942 Completed(f64, usize),
943}
944
945#[derive(Clone, Copy)]
949enum SpecTrial {
950 Spec {
951 t0: std::time::Instant,
952 gen0: usize,
953 rounds: usize,
954 },
955 Plain {
956 t0: std::time::Instant,
957 gen0: usize,
958 },
959 Decided {
960 spec: bool,
961 recheck_at: usize,
962 },
963}
964
965pub(crate) fn spec_time_level() -> u8 {
969 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
970 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
971 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
972 Err(_) => 0,
973 })
974}
975
976struct SpecStampLog {
982 t_last: std::time::Instant,
983 items: Vec<(&'static str, f32)>,
984}
985
986static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
987
988pub(crate) fn spec_stamp(name: &'static str) {
989 if spec_time_level() == 0 {
990 return;
991 }
992 if let Ok(mut g) = SPEC_STAMPS.lock() {
993 if let Some(log) = g.as_mut() {
994 let now = std::time::Instant::now();
995 log.items
996 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
997 log.t_last = now;
998 }
999 }
1000}
1001
1002fn spec_stamps_begin() {
1003 if spec_time_level() == 0 {
1004 return;
1005 }
1006 if let Ok(mut g) = SPEC_STAMPS.lock() {
1007 *g = Some(SpecStampLog {
1008 t_last: std::time::Instant::now(),
1009 items: Vec::with_capacity(64),
1010 });
1011 }
1012}
1013
1014fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1015 SPEC_STAMPS
1016 .lock()
1017 .ok()
1018 .and_then(|mut g| g.take())
1019 .map(|l| l.items)
1020 .unwrap_or_default()
1021}
1022
1023fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1026 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1027 for &(n, ms) in items {
1028 match agg.iter_mut().find(|e| e.0 == n) {
1029 Some(e) => {
1030 e.1 += ms;
1031 e.2 += 1;
1032 }
1033 None => agg.push((n, ms, 1)),
1034 }
1035 }
1036 let mut s = String::with_capacity(agg.len() * 16);
1037 for (n, ms, k) in agg {
1038 if k > 1 {
1039 s.push_str(&format!("{n} {ms:.1}/{k} "));
1040 } else {
1041 s.push_str(&format!("{n} {ms:.1} "));
1042 }
1043 }
1044 s
1045}
1046
1047#[derive(Default, Clone, Copy)]
1069struct SpecMon {
1070 round_ms: f64,
1071 tokens: f64,
1072 plain_ms: f64,
1073 n: u32,
1074 fails: u32,
1075 metal: bool,
1076}
1077
1078const SPEC_PROXY_TOKENS: f64 = 3.5;
1081const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1084
1085impl SpecMon {
1086 fn round(&mut self, dt_ms: f64, produced: usize) {
1087 self.n += 1;
1088 if self.n == 1 {
1089 return; }
1091 let a = if self.n == 2 { 1.0 } else { 0.3 };
1092 self.round_ms += a * (dt_ms - self.round_ms);
1093 self.tokens += a * (produced as f64 - self.tokens);
1094 }
1095 fn pays(&self) -> bool {
1096 if self.plain_ms > 0.0 {
1097 self.tokens * self.plain_ms > self.round_ms * 1.03
1098 } else {
1099 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1100 }
1101 }
1102 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1104 let n = generated.saturating_sub(gen0);
1105 if n >= 8 {
1106 return true;
1107 }
1108 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1109 }
1110}
1111
1112pub const KV_PREFIX_TAIL: usize = 128;
1115
1116#[derive(Debug, Clone, Default)]
1122pub struct KvPrefix {
1123 len: usize,
1124 hash: u64,
1125 tail: Vec<u32>,
1126 device: bool,
1131}
1132
1133impl KvPrefix {
1134 #[inline]
1135 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1136 for &id in ids {
1137 h ^= id as u64;
1138 h = h.wrapping_mul(0x100000001b3);
1139 h ^= h >> 29;
1140 }
1141 h
1142 }
1143
1144 pub fn clear(&mut self) {
1145 self.len = 0;
1146 self.hash = 0xcbf29ce484222325;
1147 self.tail.clear();
1148 self.device = false;
1149 }
1150
1151 pub fn on_device(&self) -> bool {
1153 self.device
1154 }
1155
1156 pub fn set_on_device(&mut self, device: bool) {
1158 self.device = device;
1159 }
1160
1161 pub fn len(&self) -> usize {
1163 self.len
1164 }
1165
1166 pub fn is_empty(&self) -> bool {
1167 self.len == 0
1168 }
1169
1170 pub fn tail_len(&self) -> usize {
1172 self.tail.len()
1173 }
1174
1175 pub fn set(&mut self, ids: &[u32]) {
1177 self.clear();
1178 self.extend(ids);
1179 }
1180
1181 pub fn extend(&mut self, more: &[u32]) {
1183 if self.len == 0 && self.hash == 0 {
1184 self.hash = 0xcbf29ce484222325;
1185 }
1186 self.hash = Self::fold(self.hash, more);
1187 self.len += more.len();
1188 if more.len() >= KV_PREFIX_TAIL {
1189 self.tail.clear();
1190 self.tail.extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1191 } else {
1192 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1193 self.tail.drain(..drop);
1194 self.tail.extend_from_slice(more);
1195 }
1196 }
1197
1198 pub fn extension(&self, ids: &[u32]) -> usize {
1202 if self.len == 0 || ids.len() <= self.len {
1203 return 0;
1204 }
1205 let t = self.tail.len();
1206 if ids[self.len - t..self.len] != self.tail[..] {
1207 return 0;
1208 }
1209 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1210 return 0;
1211 }
1212 self.len
1213 }
1214}
1215
1216pub struct GenerateResult {
1218 pub text: String,
1219 pub token_ids: Vec<u32>,
1220 pub prompt_tokens: usize,
1221 pub tokens_generated: usize,
1222 pub finish_reason: String,
1223 pub mtp_drafted: usize,
1225 pub mtp_accepted: usize,
1226 pub token_confidence: Vec<f32>,
1231 pub traces: Vec<TokenTrace>,
1234}
1235
1236#[derive(Clone, Debug)]
1241pub struct TokenTrace {
1242 pub t: usize,
1244 pub token_id: u32,
1246 pub confidence: f32,
1248 pub active_skill: Option<String>,
1250 pub recon: Option<f32>,
1254 pub switched: bool,
1257}
1258
1259#[cfg_attr(not(test), allow(dead_code))]
1264fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1265 let t = if temp > 1e-3 { temp } else { 1.0 };
1266 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1267 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1268 if sum > 0.0 {
1269 (((logits[id as usize] - max) / t).exp()) / sum
1270 } else {
1271 0.0
1272 }
1273}
1274
1275fn prefill_batched() -> bool {
1278 std::env::var("CMF_PREFILL")
1279 .map(|v| v != "seq")
1280 .unwrap_or(true)
1281}
1282
1283#[inline]
1287fn nll_graph_policy(
1288 unmasked: bool,
1289 prefer_graph: bool,
1290 native_metal: bool,
1291) -> (bool, bool) {
1292 let graph_quality = unmasked && prefer_graph;
1293 let fused_head_quality = graph_quality && native_metal;
1294 (graph_quality, fused_head_quality)
1295}
1296
1297#[derive(Clone, Copy)]
1301enum PrefillIn<'a> {
1302 Ids(&'a [u32]),
1303 Hidden(&'a [f32]),
1304}
1305
1306impl Pipeline {
1313 fn can_prefill_batched(&self) -> bool {
1314 #[cfg(test)]
1315 let force_serial = self.nll_test_force_serial;
1316 #[cfg(not(test))]
1317 let force_serial = false;
1318 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1319 }
1320
1321 fn automatic_gpu_prefix(&self) -> Option<usize> {
1324 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1325 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1326 }
1327
1328 pub fn prefill_chunk(&self) -> usize {
1332 let env = env_prefill_chunk();
1333 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1334 return prefill_chunk_rule(env, ChunkHost::here(), false);
1335 }
1336 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1337 }
1338
1339 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1340 let plain_dense = !self.weights.layers.is_empty()
1341 && self.g3n.is_none()
1342 && self.dsv4.is_none()
1343 && self.dsv41.is_none()
1344 && self.qwen4_exp.is_none()
1345 && self.weights.layers.iter().all(|lw| {
1346 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1347 });
1348 let gpu_on = crate::gpu::enabled();
1349 ChunkStackFacts {
1350 plain_dense,
1351 discrete: gpu_on && crate::gpu::discrete(),
1352 gpu_on,
1353 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1356 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1357 multi_gpu: self.gpu_plan.is_some(),
1358 o1: self.o1_active(),
1359 }
1360 }
1361}
1362
1363pub fn prefill_chunk() -> usize {
1372 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1373}
1374
1375fn env_prefill_chunk() -> Option<usize> {
1376 std::env::var("CMF_PREFILL_CHUNK")
1377 .ok()
1378 .and_then(|v| v.parse::<usize>().ok())
1379}
1380
1381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1383enum ChunkHost {
1384 Macos,
1385 Aarch64,
1387 Other,
1389}
1390
1391impl ChunkHost {
1392 fn here() -> Self {
1393 if cfg!(target_os = "macos") {
1394 ChunkHost::Macos
1395 } else if cfg!(target_arch = "aarch64") {
1396 ChunkHost::Aarch64
1397 } else {
1398 ChunkHost::Other
1399 }
1400 }
1401}
1402
1403const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1410
1411fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1417 if let Some(n) = env {
1418 return n.max(1);
1419 }
1420 match host {
1421 ChunkHost::Macos => 512,
1422 ChunkHost::Aarch64 => 256,
1425 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1426 ChunkHost::Other => 48,
1427 }
1428}
1429
1430#[derive(Clone, Copy, Debug, Default)]
1432struct ChunkStackFacts {
1433 plain_dense: bool,
1436 discrete: bool,
1438 gpu_on: bool,
1440 capacity_split: bool,
1442 multi_gpu: bool,
1444 o1: bool,
1446}
1447
1448impl ChunkStackFacts {
1449 fn dense_on_discrete(self) -> bool {
1450 self.plain_dense
1451 && self.discrete
1452 && self.gpu_on
1453 && !self.capacity_split
1454 && !self.multi_gpu
1455 && !self.o1
1456 }
1457}
1458
1459#[inline]
1465fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1466 if end <= start || start >= input_len {
1467 return 0;
1468 }
1469 let rows = (end.min(input_len) - start).min(input_len - start);
1470 if end < input_len {
1471 rows
1472 } else {
1473 rows.saturating_sub(1)
1474 }
1475}
1476
1477pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1479
1480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1482pub(crate) struct ReuseLayer {
1483 pub full: bool,
1486 pub host_rows: usize,
1489 pub device_rows: Option<usize>,
1491 pub device_state: bool,
1493}
1494
1495#[derive(Debug, Clone, PartialEq, Eq)]
1497pub(crate) enum ReusePlan {
1498 Ready,
1500 Pull(Vec<(usize, usize, usize)>),
1503 Fresh,
1505}
1506
1507pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1517 let mut pulls = Vec::new();
1518 for (li, l) in layers.iter().enumerate() {
1519 if !l.full {
1520 if l.device_state {
1521 return ReusePlan::Fresh;
1522 }
1523 continue;
1524 }
1525 if l.host_rows == reuse_from {
1526 continue;
1527 }
1528 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1529 pulls.push((li, l.host_rows, reuse_from));
1530 continue;
1531 }
1532 return ReusePlan::Fresh;
1533 }
1534 if pulls.is_empty() {
1535 ReusePlan::Ready
1536 } else {
1537 ReusePlan::Pull(pulls)
1538 }
1539}
1540
1541impl Pipeline {
1542 fn clear_sequence_state(&mut self) {
1550 #[cfg(target_os = "macos")]
1553 let _ = crate::gpu_metal::wait_replay();
1554 self.kv_cache.clear();
1555 self.clear_history();
1558 self.graph_logits = None;
1559 if let Some(b) = &mut self.dsv41 {
1560 b.3.clear();
1561 }
1562 crate::gpu::graph_kv_reset(self.graph_kv_id);
1563 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1568 }
1569
1570 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1577 if self.graph_prefill_preferred() {
1578 return true;
1579 }
1580 let kv_id = self.graph_kv_id;
1581 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1582 .map(|li| {
1583 let full = matches!(
1584 self.weights.layers[self.phys_layer(li)].attn,
1585 AttnKind::Full { .. }
1586 );
1587 ReuseLayer {
1588 full,
1589 host_rows: self.kv_cache.layers[li].pos_len(),
1592 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1593 device_state: crate::gpu::graph_state_resident(kv_id, li),
1594 }
1595 })
1596 .collect();
1597 if layers
1601 .iter()
1602 .all(|l| l.device_rows.is_none() && !l.device_state)
1603 {
1604 return true;
1605 }
1606 let plan = kv_reuse_plan(reuse_from, &layers);
1607 let (what, rows, n) = match &plan {
1608 ReusePlan::Ready => ("host ready", 0, 0),
1609 ReusePlan::Fresh => ("fresh", 0, 0),
1610 ReusePlan::Pull(p) => (
1611 "pull",
1612 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1613 p.len(),
1614 ),
1615 };
1616 let t0 = std::time::Instant::now();
1617 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1618 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1619 eprintln!(
1620 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1621 if ok { "" } else { " (failed → fresh)" },
1622 t0.elapsed().as_secs_f64() * 1e3
1623 );
1624 }
1625 ok
1626 }
1627
1628 fn apply_kv_reuse_plan(
1629 &mut self,
1630 reuse_from: usize,
1631 plan: ReusePlan,
1632 layers: &[ReuseLayer],
1633 ) -> bool {
1634 let kv_id = self.graph_kv_id;
1635 match plan {
1636 ReusePlan::Fresh => return false,
1637 ReusePlan::Ready => {}
1638 ReusePlan::Pull(pulls) => {
1639 let (nkv, hd) = {
1644 let c = &self.kv_cache.layers[pulls[0].0];
1645 (c.num_kv_heads, c.head_dim)
1646 };
1647 let uniform = pulls.iter().all(|&(li, _, _)| {
1648 let c = &self.kv_cache.layers[li];
1649 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1650 });
1651 let batched = if uniform {
1652 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1653 } else {
1654 None
1655 };
1656 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1657 Some(rows) => rows,
1658 None => {
1659 let mut rows = Vec::with_capacity(pulls.len());
1660 for &(li, from, to) in &pulls {
1661 let (lnkv, lhd) = {
1662 let c = &self.kv_cache.layers[li];
1663 (c.num_kv_heads, c.head_dim)
1664 };
1665 let Some((k, v, first_valid)) =
1666 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1667 else {
1668 return false;
1669 };
1670 let need_from = match self.layer_window(li) {
1674 Some(w) => from.max((to + 1).saturating_sub(w)),
1675 None => from,
1676 };
1677 if first_valid > need_from {
1678 return false;
1679 }
1680 rows.push((k, v));
1681 }
1682 rows
1683 }
1684 };
1685 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1686 let cache = &mut self.kv_cache.layers[li];
1687 let row = cache.num_kv_heads * cache.head_dim;
1688 for p in 0..to - from {
1689 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1690 }
1691 if cache.pos_len() != to {
1692 return false;
1693 }
1694 }
1695 }
1696 }
1697 for (li, l) in layers.iter().enumerate() {
1701 if l.full
1702 && l.device_rows.is_some_and(|d| d > reuse_from)
1703 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1704 {
1705 return false;
1706 }
1707 }
1708 true
1709 }
1710
1711 fn finish_generation(
1717 &mut self,
1718 mtp: &mut Option<MtpModule>,
1719 router: &mut Option<crate::swarm::DynRouter>,
1720 clear_sequence: bool,
1721 ) {
1722 if router.is_some() {
1726 let _ = self.set_active_skill(None);
1727 }
1728 #[cfg(target_os = "macos")]
1735 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1736 if clear_sequence {
1737 self.clear_sequence_state();
1738 if let Some(m) = mtp.as_mut() {
1739 m.kv.clear();
1745 }
1746 if let Some(m) = self.mtp.as_mut() {
1747 m.kv.clear();
1751 }
1752 }
1753 self.graph_want_logits = false;
1754 self.graph_head_required = false;
1755 self.graph_logits = None;
1756 self.graph_failed
1757 .store(false, std::sync::atomic::Ordering::Relaxed);
1758 self.cancel
1759 .store(false, std::sync::atomic::Ordering::Relaxed);
1760 self.dyn_router = router.take().or(self.dyn_router.take());
1761 self.mtp = mtp.take().or(self.mtp.take());
1762 self.mtp_graph_mode = None;
1763 self.spec_forced = None;
1764 }
1765
1766 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1770 if self
1771 .graph_failed
1772 .swap(false, std::sync::atomic::Ordering::Relaxed)
1773 {
1774 self.cancel
1775 .store(false, std::sync::atomic::Ordering::Relaxed);
1776 self.clear_sequence_state();
1777 self.graph_logits = None;
1778 self.graph_want_logits = false;
1779 self.graph_head_required = false;
1780 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1781 }
1782 Ok(())
1783 }
1784
1785 #[cfg(target_os = "macos")]
1786 fn fail_metal_graph(&mut self, reason: &str) {
1787 crate::pipeline::METAL_GRAPH_ERRORS
1788 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1789 self.clear_sequence_state();
1790 self.graph_logits = None;
1791 self.graph_failed
1792 .store(true, std::sync::atomic::Ordering::Relaxed);
1793 self.cancel
1794 .store(true, std::sync::atomic::Ordering::Relaxed);
1795 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1796 }
1797
1798 fn nll_begin(&mut self) -> Result<(), String> {
1803 if self
1804 .graph_failed
1805 .swap(false, std::sync::atomic::Ordering::Relaxed)
1806 {
1807 self.cancel
1808 .store(false, std::sync::atomic::Ordering::Relaxed);
1809 self.clear_sequence_state();
1810 self.graph_logits = None;
1811 self.graph_want_logits = false;
1812 self.graph_head_required = false;
1813 return Err("GPU graph failed before NLL scoring".to_string());
1814 }
1815 self.clear_sequence_state();
1816 self.graph_logits = None;
1817 self.graph_want_logits = false;
1818 self.graph_head_required = false;
1819 Ok(())
1820 }
1821
1822 fn nll_end(&mut self) {
1826 self.clear_sequence_state();
1827 self.graph_logits = None;
1828 self.graph_want_logits = false;
1829 self.graph_head_required = false;
1830 self.graph_failed
1831 .store(false, std::sync::atomic::Ordering::Relaxed);
1832 }
1833
1834 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1837 #[cfg(test)]
1838 if self.nll_test_fail_at == Some(pos) {
1839 self.nll_test_fail_at = None;
1840 self.graph_failed
1841 .store(true, std::sync::atomic::Ordering::Relaxed);
1842 self.cancel
1843 .store(true, std::sync::atomic::Ordering::Relaxed);
1844 }
1845 if self
1846 .graph_failed
1847 .swap(false, std::sync::atomic::Ordering::Relaxed)
1848 {
1849 self.cancel
1850 .store(false, std::sync::atomic::Ordering::Relaxed);
1851 self.clear_sequence_state();
1852 self.graph_logits = None;
1853 self.graph_want_logits = false;
1854 return Err(format!(
1855 "GPU graph failed during NLL {phase} at position {pos}"
1856 ));
1857 }
1858 Ok(())
1859 }
1860
1861 #[inline]
1865 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1866 virtual_idx % self.physical_layers
1867 }
1868
1869 #[inline]
1872 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1873 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1874 }
1875
1876 #[allow(clippy::too_many_arguments)]
1878
1879 #[cfg(target_os = "macos")]
1898 fn graph_prefill_preferred(&self) -> bool {
1899 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1900 if !crate::gpu::enabled_here()
1901 || !graph_force
1902 || std::env::var("CMF_GPU_BLOCK")
1903 .map(|v| v == "0")
1904 .unwrap_or(false)
1905 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1908 {
1909 return false;
1910 }
1911 self.weights
1912 .layers
1913 .iter()
1914 .any(|lw| {
1915 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1916 })
1917 }
1918
1919 #[cfg(not(target_os = "macos"))]
1926 fn batch_prefix_prefill(&self) -> bool {
1927 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1928 Ok("0") => return false,
1929 Ok("1") => true,
1930 _ => false,
1931 };
1932 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1933 && crate::gpu::enabled_here()
1934 && !self.graph_refused()
1935 && (forced || self.graph_attn_decline_reason().is_some())
1936 && self.wgpu_graph_attn_decline().is_none()
1937 && self.attn_softcap == 0.0
1938 && self
1939 .weights
1940 .layers
1941 .iter()
1942 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1943 && self.automatic_gpu_prefix().is_some()
1944 }
1945
1946 fn g4_batch_prefill(&self) -> bool {
1952 #[cfg(target_os = "macos")]
1953 {
1954 false
1955 }
1956 #[cfg(not(target_os = "macos"))]
1957 {
1958 (self.global_attn.is_some() || self.attn_v_norm)
1959 && crate::gpu::g4_batch_on()
1960 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1961 && crate::gpu::enabled_here()
1962 && crate::gpu::discrete()
1963 && !self.graph_refused()
1964 && !self.o1_active()
1965 && self.attn_softcap == 0.0
1966 && self.wgpu_graph_attn_decline().is_none()
1967 && self.weights.layers.iter().all(|lw| {
1968 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1969 })
1970 && self.automatic_gpu_prefix().is_none()
1971 }
1972 }
1973
1974 #[cfg(not(target_os = "macos"))]
1981 fn g4_batch_ingest(
1982 &mut self,
1983 ids: &[u32],
1984 pos0: usize,
1985 keep_all: bool,
1986 ) -> Result<(usize, Vec<f32>), String> {
1987 let hs = self.hidden_size;
1988 let chunk = self.generation_batch_k().max(1);
1989 let end_all = pos0 + ids.len();
1990 let mut pos = pos0;
1991 let mut out = Vec::new();
1992 while pos < end_all && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
1993 let end = (pos + chunk).min(end_all);
1994 let bk = end - pos;
1995 let mut hiddens = vec![0f32; bk * hs];
1996 for (j, &id) in ids[pos - pos0..end - pos0].iter().enumerate() {
1997 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
1998 }
1999 let positions: Vec<usize> = (pos..end).collect();
2000 let t_chunk = std::time::Instant::now();
2001 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
2002 if std::env::var("CMF_GRAPH_PROF").is_ok() {
2003 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
2004 eprintln!(
2005 "batch-chunk: phase=prompt mode=gemma4 k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
2006 bk as f64 / (ms / 1000.0)
2007 );
2008 }
2009 match outcome {
2010 crate::gpu::BatchGraphOutcome::Completed => {
2011 if keep_all {
2012 out.extend_from_slice(&hiddens);
2013 } else {
2014 out = hiddens[(bk - 1) * hs..].to_vec();
2015 }
2016 pos = end;
2017 }
2018 crate::gpu::BatchGraphOutcome::Failed => {
2019 return Err("Gemma-4 batched prefill failed after admission".into());
2020 }
2021 crate::gpu::BatchGraphOutcome::Declined => {
2022 #[cfg(feature = "gpu")]
2023 if pos > pos0 {
2024 self.pull_lagging_host_kv(0, self.num_layers, pos);
2025 }
2026 break;
2027 }
2028 }
2029 }
2030 Ok((pos - pos0, out))
2031 }
2032
2033 pub fn g4_prefill_hidden_rows(&mut self, ids: &[u32]) -> Option<Vec<f32>> {
2038 #[cfg(not(target_os = "macos"))]
2039 {
2040 if ids.is_empty() || !self.g4_batch_prefill() {
2041 return None;
2042 }
2043 self.clear_sequence_state();
2044 match self.g4_batch_ingest(ids, 0, true) {
2045 Ok((n, rows)) if n == ids.len() => Some(rows),
2046 _ => {
2047 self.clear_sequence_state();
2048 None
2049 }
2050 }
2051 }
2052 #[cfg(target_os = "macos")]
2053 {
2054 let _ = ids;
2055 None
2056 }
2057 }
2058
2059 #[cfg(not(target_os = "macos"))]
2060 fn graph_prefill_preferred(&self) -> bool {
2061 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
2069 if !graph_on || !crate::gpu::enabled_here() {
2070 return false;
2071 }
2072 if self.embryo_resident_eligible() {
2076 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
2081 }
2082 if self.o1_active() {
2096 return false;
2097 }
2098 if self.wgpu_graph_attn_decline().is_some() {
2102 return false;
2103 }
2104 if self
2105 .weights
2106 .layers
2107 .iter()
2108 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
2109 {
2110 return true;
2111 }
2112 self.weights
2123 .layers
2124 .iter()
2125 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2126 && self.automatic_gpu_prefix().is_none()
2127 }
2128
2129 #[cfg(target_os = "macos")]
2130 fn q1_graph_gpu(
2131 &mut self,
2132 start: usize,
2133 upto: Option<usize>,
2134 position: usize,
2135 h: &mut [f32],
2136 ) -> usize {
2137 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2139 const MOE_WORKING_SET_REASON: &str =
2140 "model weights exceed Metal working set for native MoE graph";
2141 if self
2145 .graph_declines
2146 .borrow()
2147 .iter()
2148 .any(|&(site, reason)| site == "metal block graph" && reason == MOE_WORKING_SET_REASON)
2149 {
2150 return start;
2151 }
2152 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2153 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2155 || !crate::gpu::enabled_here()
2156 || !graph_force
2157 || std::env::var("CMF_GPU_BLOCK")
2158 .map(|v| v == "0")
2159 .unwrap_or(false)
2160 {
2161 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2162 eprintln!(
2163 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2164 self.attn_softcap > 0.0,
2165 crate::gpu::enabled_here(),
2166 graph_force,
2167 );
2168 }
2169 if self.graph_head_required {
2170 self.fail_metal_graph("native graph front gate refused");
2171 }
2172 return start;
2173 }
2174 let gemma4 = self.metal_gemma4();
2184 let swa_graph = self.metal_graph_swa() || gemma4;
2185 if (self.swa.is_some() && !swa_graph)
2186 || (self.global_attn.is_some() && !gemma4)
2187 || self.attention_heads_per_layer.is_some()
2188 || (self.attn_v_norm && !gemma4)
2189 || (self.graph_attn_decline_reason().is_some() && !swa_graph)
2191 || self.weights.layers.iter().any(|lw| {
2192 ((lw.attn_out_norm.is_some()
2193 || lw.ffn_out_norm.is_some()
2194 || lw.layer_scale.is_some())
2195 && !gemma4)
2196 || matches!(&lw.ffn, FfnKind::Dense(d)
2197 if !(matches!(d.act, Act::Silu | Act::Gelu)
2198 || (gemma4 && d.act == Act::GeluTanh)))
2199 })
2200 {
2201 if let Some(reason) = self.graph_attn_decline_reason().filter(|_| !swa_graph) {
2204 self.note_graph_decline("metal block graph", reason);
2205 }
2206 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2207 eprintln!(
2208 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2209 self.swa.is_some(),
2210 self.global_attn.is_some(),
2211 self.attention_heads_per_layer.is_some(),
2212 self.attn_v_norm,
2213 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2214 );
2215 }
2216 if self.graph_head_required {
2217 self.fail_metal_graph("native graph architecture gate refused");
2218 }
2219 return start;
2220 }
2221 let limit = upto
2224 .map(|u| u + 1)
2225 .unwrap_or(self.num_layers)
2226 .min(self.num_layers);
2227
2228 enum Item<'a> {
2229 Gdn {
2230 run: Vec<GdnGpuLayer<'a>>,
2231 first: usize,
2232 },
2233 Attn {
2234 l: AttnGpuLayer<'a>,
2235 li: usize,
2236 q_norm: Option<&'a [f32]>,
2237 k_norm: Option<&'a [f32]>,
2238 output_gate: bool,
2239 proj_gate: Option<(&'a QTensor, bool)>,
2243 head_gate_w: Option<&'a [f32]>,
2246 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2247 full_gpu: bool,
2250 },
2251 }
2252
2253 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2260 let attend_contract = attend_mode != "0"
2261 && attend_mode != "off"
2262 && self.head_dim % 4 == 0
2263 && self.head_dim <= 256
2264 && self.rotary_dim >= 2
2265 && self.rotary_dim <= self.head_dim
2266 && (self.rotary_dim / 2) % 32 == 0
2267 && self.num_kv_heads > 0
2268 && self.num_heads % self.num_kv_heads == 0;
2269
2270 let mut plan: Vec<Item> = Vec::new();
2271 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2272 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2274 let mut scan = start;
2275 while scan < limit {
2276 let lw = &self.weights.layers[self.phys_layer(scan)];
2277 let ffn = match &lw.ffn {
2278 FfnKind::Dense(d) if d.segs.is_empty() => {
2279 let (Some(g), Some(u), Some(dn)) = (
2280 d.gate_proj.metal_graph_parts(),
2281 d.up_proj.metal_graph_parts(),
2282 d.down_proj.metal_graph_parts(),
2283 ) else {
2284 if block_diag {
2285 eprintln!(
2286 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2287 );
2288 }
2289 break;
2290 };
2291 MetalFfn::Dense {
2292 gate: g,
2293 up: u,
2294 down: dn,
2295 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2298 }
2299 }
2300 FfnKind::Moe(m) => {
2301 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
2302 if block_diag {
2303 eprintln!(
2304 "block-graph: L{scan} MoE outside the graph contract — run ends"
2305 );
2306 }
2307 break;
2308 };
2309 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2310 model_ref.get_or_insert_with(|| model.clone());
2311 }
2312 MetalFfn::Moe(moe)
2313 }
2314 _ => {
2315 if block_diag {
2316 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2317 }
2318 break;
2319 }
2320 };
2321 match &lw.attn {
2322 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2323 let parts = (
2324 w.in_proj_qkv.metal_graph_parts(),
2325 w.in_proj_z.metal_graph_parts(),
2326 w.in_proj_a.f32_parts(),
2327 w.in_proj_b.f32_parts(),
2328 w.out_proj.metal_graph_parts(),
2329 );
2330 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2331 if block_diag {
2332 eprintln!(
2333 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2334 w.in_proj_qkv.metal_graph_parts().is_some(),
2335 w.in_proj_z.metal_graph_parts().is_some(),
2336 w.in_proj_a.f32_parts().is_some(),
2337 w.in_proj_b.f32_parts().is_some(),
2338 w.out_proj.metal_graph_parts().is_some(),
2339 );
2340 }
2341 break;
2342 };
2343 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2344 model_ref.get_or_insert_with(|| model.clone());
2345 }
2346 let gl = GdnGpuLayer {
2347 attn_norm: &lw.input_norm,
2348 post_norm: &lw.post_norm,
2349 qkv,
2350 z,
2351 a,
2352 b,
2353 out,
2354 ffn,
2355 conv1d: &w.conv1d,
2356 a_log: &w.a_log,
2357 dt_bias: &w.dt_bias,
2358 gnorm: &w.norm,
2359 };
2360 match plan.last_mut() {
2361 Some(Item::Gdn { run, .. }) => run.push(gl),
2362 _ => plan.push(Item::Gdn {
2363 run: vec![gl],
2364 first: scan,
2365 }),
2366 }
2367 }
2368 AttnKind::Full {
2369 wq,
2370 wk,
2371 wv,
2372 wo,
2373 q_norm,
2374 k_norm,
2375 output_gate,
2376 softplus_gate,
2377 bias,
2378 } if (!self.kv_cache.layers[scan].o1_sealed()
2379 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2384 && softplus_gate
2388 .as_ref()
2389 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2390 {
2391 let parts = (
2392 wq.metal_graph_parts(),
2393 wk.metal_graph_parts(),
2394 wv.metal_graph_parts(),
2395 wo.metal_graph_parts(),
2396 );
2397 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2398 break;
2399 };
2400 if let QTensor::Mapped { model, .. } = wq {
2401 model_ref.get_or_insert_with(|| model.clone());
2402 }
2403 let cache = &self.kv_cache.layers[scan];
2404 let o1_metal = cache.o1.is_some()
2408 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2409 && cache.o1_views().is_some();
2410 let head_gate_w = softplus_gate
2414 .as_ref()
2415 .and_then(|(g, _)| g.f32_parts())
2416 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2417 .map(|(d, _, _)| d);
2418 let (nkv_g, hd_g) = if gemma4 {
2421 let (a, b, _) = self.layer_geom(scan);
2422 (a, b)
2423 } else {
2424 (self.num_kv_heads, self.head_dim)
2425 };
2426 let full_gpu = attend_contract
2427 && softplus_gate.is_none() == head_gate_w.is_none()
2428 && cache.mode == crate::kv_cache::KvMode::F32
2429 && (cache.o1.is_none() || o1_metal)
2430 && bias.is_none()
2431 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2432 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2433 && pk.1 == nkv_g * hd_g
2434 && pv.1 == nkv_g * hd_g
2435 && po.2 == self.num_heads * hd_g;
2436 plan.push(Item::Attn {
2437 l: AttnGpuLayer {
2438 attn_norm: &lw.input_norm,
2439 post_norm: &lw.post_norm,
2440 wq: pq,
2441 wk: pk,
2442 wv: pv,
2443 wo: po,
2444 ffn,
2445 attn_out_norm: lw.attn_out_norm.as_deref(),
2446 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2447 layer_scale: lw.layer_scale,
2448 },
2449 li: scan,
2450 q_norm: q_norm.as_deref(),
2451 k_norm: k_norm.as_deref(),
2452 output_gate: *output_gate,
2453 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2454 head_gate_w,
2455 bias: bias
2456 .as_ref()
2457 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2458 full_gpu,
2459 });
2460 }
2461 _ => break,
2462 }
2463 scan += 1;
2464 }
2465 let Some(model) = model_ref else {
2466 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2467 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2468 }
2469 if self.graph_head_required {
2470 self.fail_metal_graph("native graph has no mapped model reference");
2471 }
2472 return start;
2473 };
2474 if plan.is_empty() {
2475 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2476 eprintln!("q1-graph: empty plan at layer {start}");
2477 }
2478 if self.graph_head_required {
2479 self.fail_metal_graph("native graph plan is empty");
2480 }
2481 return start;
2482 }
2483 let has_moe = plan.iter().any(|it| match it {
2484 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2485 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2486 });
2487 let metal_working_set = crate::gpu_metal::working_set_bytes();
2496 let model_bytes = model.primary_bytes().len() as u64;
2497 if has_moe && metal_working_set != 0 && model_bytes > metal_working_set {
2498 tracing::info!(
2499 model_bytes,
2500 metal_working_set,
2501 "native Metal MoE graph declined before allocation"
2502 );
2503 self.note_graph_decline("metal block graph", MOE_WORKING_SET_REASON);
2504 if self.graph_head_required {
2505 self.fail_metal_graph(MOE_WORKING_SET_REASON);
2506 }
2507 return start;
2508 }
2509 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2510 let dev_attend = attend_contract
2511 && (self.head_dim <= 128
2512 || has_moe
2513 || (self.head_dim <= 256 && has_gdn)
2519 || (self.head_dim <= 256 && swa_graph)
2523 || attend_mode == "force"
2524 || attend_mode == "256");
2525 if !dev_attend {
2526 for it in &mut plan {
2527 if let Item::Attn { li, full_gpu, .. } = it {
2528 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2531 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2532 if !keep_o1 {
2533 *full_gpu = false;
2534 }
2535 }
2536 }
2537 }
2538 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2539 use std::sync::atomic::{AtomicBool, Ordering};
2540 static SAID: AtomicBool = AtomicBool::new(false);
2541 if !SAID.swap(true, Ordering::Relaxed) {
2542 let fg = plan
2543 .iter()
2544 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2545 .count();
2546 let att = plan
2547 .iter()
2548 .filter(|it| matches!(it, Item::Attn { .. }))
2549 .count();
2550 eprintln!(
2551 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2552 plan.len(),
2553 self.head_dim,
2554 self.rotary_dim,
2555 self.num_kv_heads,
2556 self.num_heads,
2557 );
2558 }
2559 }
2560 let dims = GraphDims {
2561 hidden: self.hidden_size,
2562 eps: self.rms_eps as f32,
2563 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2564 };
2565 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
2566 if self.graph_head_required {
2567 self.fail_metal_graph("native TokenGraph allocation refused");
2568 }
2569 return start;
2570 };
2571 if swa_graph && self.head_dim > 128 {
2572 graph.set_attend_blk_from(64);
2578 }
2579 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2580 nv: cfg.num_v_heads,
2581 nk: cfg.num_k_heads,
2582 dk: cfg.key_head_dim,
2583 dv: cfg.value_head_dim,
2584 kk: cfg.conv_kernel,
2585 hidden: self.hidden_size,
2586 inter: self.intermediate_size,
2587 c_dim: cfg.conv_dim(),
2588 eps: cfg.rms_eps as f32,
2589 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2590 });
2591 let mut valid = 0usize;
2595 let mut end = start;
2596 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2598 static ONCE: std::sync::Once = std::sync::Once::new();
2599 ONCE.call_once(|| {
2600 for it in &plan {
2601 match it {
2602 Item::Gdn { first, run } => {
2603 eprintln!("plan: Gdn first={first} len={}", run.len())
2604 }
2605 Item::Attn { li, full_gpu, .. } => {
2606 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2607 }
2608 }
2609 }
2610 });
2611 }
2612 for item in &plan {
2613 let ok = match item {
2614 Item::Gdn { run, .. } => gcfg
2615 .as_ref()
2616 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2617 .unwrap_or(false),
2618 Item::Attn { l, .. } => graph.attn_ok(l),
2619 };
2620 if !ok {
2621 if block_diag {
2622 eprintln!(
2623 "block-graph: plan item {} ({}) failed graph preflight",
2624 valid,
2625 match item {
2626 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2627 Item::Attn { li, .. } => format!("Attn L{li}"),
2628 }
2629 );
2630 }
2631 break;
2632 }
2633 valid += 1;
2634 end += match item {
2635 Item::Gdn { run, .. } => run.len(),
2636 Item::Attn { .. } => 1,
2637 };
2638 }
2639 plan.truncate(valid);
2640 if plan.is_empty() {
2641 if self.graph_head_required {
2642 self.fail_metal_graph("native graph preflight produced no valid items");
2643 }
2644 return start;
2645 }
2646
2647 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2648 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2649 return start;
2650 }
2651
2652 let one_pass = |t: (usize, usize, usize)| {
2661 use cortiq_core::TensorDtype as D;
2662 matches!(
2663 model.tensors[t.0].dtype,
2664 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2665 )
2666 };
2667 let dense_fast = plan.iter().all(|it| match it {
2668 Item::Attn {
2669 l, li, full_gpu, ..
2670 } => {
2671 *full_gpu
2672 && self.kv_cache.layers[*li].o1.is_none()
2673 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2674 && match l.ffn {
2675 MetalFfn::Dense { gate, up, down, .. } => {
2676 one_pass(gate) && one_pass(up) && one_pass(down)
2677 }
2678 _ => false,
2679 }
2680 }
2681 Item::Gdn { .. } => false,
2682 });
2683 let moe_plan = !dense_fast
2687 && plan.iter().all(|it| match it {
2688 Item::Attn {
2689 l, li, full_gpu, ..
2690 } => {
2691 *full_gpu
2692 && self.kv_cache.layers[*li].o1.is_none()
2693 && matches!(l.ffn, MetalFfn::Moe(_))
2694 }
2695 Item::Gdn { .. } => false,
2696 });
2697 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2698 if block_diag {
2699 use std::sync::atomic::{AtomicBool, Ordering};
2700 static SAID_AB: AtomicBool = AtomicBool::new(false);
2701 if !SAID_AB.swap(true, Ordering::Relaxed) {
2702 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2703 }
2704 }
2705 let _mv_fast = match ab {
2706 Some((bits, _)) => {
2707 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2708 crate::gpu_metal::MvFastGuard::set_raw(bits)
2709 }
2710 None if moe_plan => {
2711 graph.set_dense_concurrent(crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0);
2712 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2713 }
2714 None => {
2715 graph.set_dense_concurrent(dense_fast);
2716 let q8r4 = if swa_graph {
2719 crate::gpu_metal::DENSE_Q8R4
2720 } else {
2721 0
2722 };
2723 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2724 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2725 } else {
2726 0
2727 })
2728 }
2729 };
2730
2731 let pool = self.pool.clone();
2734 let (nh, nkv, hd, hs, eps) = (
2735 self.num_heads,
2736 self.num_kv_heads,
2737 self.head_dim,
2738 self.hidden_size,
2739 self.rms_eps,
2740 );
2741 let norm_style = self.norm_style;
2742 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2743 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2744 let kv_id = self.graph_kv_id;
2745 let mut pending: Vec<(usize, usize)> = Vec::new();
2748 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2751 for item in &plan {
2752 let _xt0 = std::time::Instant::now();
2753 let _xkind: u32 = match item {
2754 Item::Gdn { .. } => 2,
2755 Item::Attn { .. } => 3,
2756 };
2757 if self.loop_final_norm {
2759 let item_start = match item {
2760 Item::Gdn { first, .. } => *first,
2761 Item::Attn { li, .. } => *li,
2762 };
2763 if item_start > start && self.is_loop_end(item_start - 1) {
2764 graph.encode_loop_norm(&self.weights.final_norm);
2765 }
2766 }
2767 match item {
2768 Item::Gdn { run, first } => {
2769 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2770 if l.linear_state.len() != want {
2771 l.linear_state = vec![0f32; want];
2772 }
2773 }
2774 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2775 .iter()
2776 .map(|l| l.linear_state.as_slice())
2777 .collect();
2778 let _ig = std::time::Instant::now();
2779 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2780 tracing::error!("q1 graph: GDN run refused after validation");
2782 return start;
2783 }
2784 graph.commit_kind = 2;
2787 graph.commit();
2788 crate::gpu::stageprof(0, _ig.elapsed());
2789 pending.push((*first, run.len()));
2790 }
2791 Item::Attn {
2792 l,
2793 li,
2794 q_norm,
2795 k_norm,
2796 output_gate,
2797 proj_gate,
2798 head_gate_w,
2799 bias,
2800 full_gpu,
2801 } => {
2802 let _ia = std::time::Instant::now();
2803 let inv_freq_l = self.layer_inv_freq(*li);
2809 let rd_l = self.layer_geom(*li).2;
2810 let window_l = self.layer_window(*li);
2811 let head_gate_w = *head_gate_w;
2812 let (nkv, hd) = if gemma4 {
2815 let (a, b, _) = self.layer_geom(*li);
2816 (a, b)
2817 } else {
2818 (nkv, hd)
2819 };
2820 if *full_gpu {
2822 let cache = &self.kv_cache.layers[*li];
2823 let o1p = if cache.o1.is_some() {
2824 match cache.o1_views() {
2825 Some(views) => Some(crate::gpu::O1AttnParams {
2826 views,
2827 epoch: self.o1_epoch,
2828 }),
2829 None => None,
2831 }
2832 } else {
2833 None
2834 };
2835 let o1_layer = cache.o1.is_some();
2836 if o1_layer && o1p.is_none() {
2837 }
2839 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
2840 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
2841 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
2842 debug_assert!(
2845 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
2846 );
2847 let p = crate::gpu::AttnDeviceParams {
2848 kv_id,
2849 layer: *li,
2850 nh,
2851 nkv,
2852 hd,
2853 rd: rd_l,
2854 position,
2855 scale: self.attn_scale,
2856 eps: eps as f32,
2857 gemma,
2858 late_qk_norm: self.qk_norm_after_rope,
2859 output_gate: *output_gate,
2860 q_norm: *q_norm,
2861 k_norm: *k_norm,
2862 inv_freq: &inv_freq_l,
2863 rope_scale: self.layer_rope_scale(*li),
2864 cpu_k,
2865 cpu_v,
2866 cpu_stored,
2867 cpu_gen: cache.generation(),
2868 o1: o1p,
2869 window: window_l,
2870 head_gate: head_gate_w,
2871 v_norm: self.attn_v_norm,
2872 };
2873 let o1_bad = o1_layer && p.o1.is_none();
2874 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
2875 {
2876 if p.o1.is_none() {
2878 dev_attn.push((*li, nkv, hd));
2879 }
2880 graph.commit_kind = 3;
2881 graph.commit();
2882 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2886 continue;
2887 }
2888 }
2890 graph.encode_attn_prefix(l);
2891 if let Err(err) = graph.sync_checked() {
2892 self.fail_metal_graph(&err);
2893 return start;
2894 }
2895 if !pending.is_empty() {
2896 let idxs: Vec<usize> =
2897 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2898 let mut outs: Vec<&mut [f32]> = self
2899 .kv_cache
2900 .layers
2901 .iter_mut()
2902 .enumerate()
2903 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2904 .map(|(_, s)| s.linear_state.as_mut_slice())
2905 .collect();
2906 graph.read_states(&mut outs);
2907 }
2908 let mut q_raw = attention::take_buf(l.wq.1);
2909 let mut k = attention::take_buf(l.wk.1);
2910 let mut v = attention::take_buf(l.wv.1);
2911 graph.read_qkv(&mut q_raw, &mut k, &mut v);
2912 let mut gate_raw = proj_gate.map(|(gp, _)| {
2915 let mut normed = attention::take_buf(hs);
2916 graph.read_normed(&mut normed);
2917 let mut raw = attention::take_buf(gp.rows());
2918 gp.matvec(&normed, &mut raw, pool.as_deref());
2919 attention::recycle_buf(&mut normed);
2920 raw
2921 });
2922 let cfg = QwenAttnCfg {
2923 num_heads: nh,
2924 num_kv_heads: nkv,
2925 head_dim: hd,
2926 hidden_size: hs,
2927 position,
2928 inv_freq: &inv_freq_l,
2929 rotary_dim: rd_l,
2930 scale: self.attn_scale,
2931 softcap: self.attn_softcap,
2932 window: window_l,
2933 v_norm: self.attn_v_norm,
2936 qk_norm_after_rope: self.qk_norm_after_rope,
2937 gate_sigmoid: self.proj_gate_sigmoid,
2938 q_norm: *q_norm,
2939 k_norm: *k_norm,
2940 output_gate: *output_gate,
2941 softplus_gate: None,
2942 rope_scale: self.layer_rope_scale(*li),
2943 bias: *bias,
2944 rms_eps: eps,
2945 norm_style,
2946 pool: pool.as_deref(),
2947 v_head_dim: hd,
2948 };
2949 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
2952 || std::env::var("CMF_ATTN_DUMP").is_ok();
2953 let _ = full_gpu;
2954 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
2955 let mut ao = attention::qwen_attention_core(
2956 q_raw,
2957 k,
2958 v,
2959 &mut self.kv_cache.layers[*li],
2960 &cfg,
2961 );
2962 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
2966 if let Some((qr0, k0, v0)) = oracle_in.clone() {
2967 let (cq, _cg, _ck, _cv) =
2968 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2969 let cache = &self.kv_cache.layers[*li];
2970 let n = cache.head_keys(0).len() / hd;
2971 let mut bytes: Vec<u8> = Vec::new();
2972 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
2973 bytes.extend_from_slice(&v.to_le_bytes());
2974 }
2975 for v in &cq {
2976 bytes.extend_from_slice(&v.to_le_bytes());
2977 }
2978 for g in 0..nkv {
2979 for v in cache.head_keys(g) {
2980 bytes.extend_from_slice(&v.to_le_bytes());
2981 }
2982 }
2983 for g in 0..nkv {
2984 for v in cache.head_values(g) {
2985 bytes.extend_from_slice(&v.to_le_bytes());
2986 }
2987 }
2988 let _ =
2989 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
2990 }
2991 }
2992 if let Some((qr0, k0, v0)) =
2993 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
2994 {
2995 let (cq, _cg, ck, cv) =
2996 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2997 let mut h_now = vec![0f32; hs];
2998 graph.read_h(&mut h_now);
2999 let cache = &self.kv_cache.layers[*li];
3000 let n_after = cache.head_keys(0).len() / hd;
3001 let stored = n_after.saturating_sub(1);
3005 let cpu_k: Vec<&[f32]> = (0..nkv)
3006 .map(|g| &cache.head_keys(g)[..stored * hd])
3007 .collect();
3008 let cpu_v: Vec<&[f32]> = (0..nkv)
3009 .map(|g| &cache.head_values(g)[..stored * hd])
3010 .collect();
3011 let p = crate::gpu::AttnDeviceParams {
3012 kv_id,
3013 layer: *li,
3014 nh,
3015 nkv,
3016 hd,
3017 rd: rd_l,
3021 position,
3022 scale: self.attn_scale,
3023 eps: eps as f32,
3024 gemma,
3025 late_qk_norm: self.qk_norm_after_rope,
3026 output_gate: *output_gate,
3027 q_norm: *q_norm,
3028 k_norm: *k_norm,
3029 inv_freq: &inv_freq_l,
3030 rope_scale: self.layer_rope_scale(*li),
3031 cpu_k,
3032 cpu_v,
3033 cpu_stored: stored,
3034 cpu_gen: cache.generation(),
3035 o1: None,
3036 window: window_l,
3037 head_gate: None,
3038 v_norm: self.attn_v_norm,
3039 };
3040 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3041 let md = |a: &[f32], b: &[f32]| {
3042 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3043 };
3044 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3045 eprintln!(
3046 "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}",
3047 nn(&cq),
3048 md(&cq, &dq),
3049 nn(&ck),
3050 md(&ck, &dk),
3051 nn(&cv),
3052 md(&cv, &dv),
3053 nn(&ao),
3054 md(&ao, &dao)
3055 );
3056 } else {
3057 eprintln!("attn-oracle L{li}: device probe declined");
3058 }
3059 }
3060 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3061 attention::apply_projected_gate(
3064 &mut ao,
3065 raw,
3066 per_head,
3067 hd,
3068 self.proj_gate_sigmoid,
3069 );
3070 }
3071 if let Some(mut raw) = gate_raw.take() {
3072 attention::recycle_buf(&mut raw);
3073 }
3074 graph.encode_attn_suffix(l, &ao);
3075 graph.commit();
3078 attention::recycle_buf(&mut ao);
3079 }
3080 }
3081
3082 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3083 }
3084 let mut lm_rows = None;
3089 if self.graph_want_logits
3090 && upto.is_none()
3091 && end == self.num_layers
3092 && std::env::var("CMF_GPU_LMHEAD")
3093 .map(|v| v != "0")
3094 .unwrap_or(true)
3095 {
3096 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3097 if graph.lm_head_ok(lm) {
3098 graph.encode_lm_head(&self.weights.final_norm, lm);
3099 lm_rows = Some(lm.1);
3100 }
3101 }
3102 }
3103 if self.graph_head_required && lm_rows.is_none() {
3104 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3105 self.fail_metal_graph("fused graph head was requested but not encodable");
3106 return start;
3107 }
3108 let _sy0 = std::time::Instant::now();
3109 if let Err(err) = graph.sync_checked() {
3110 self.fail_metal_graph(&err);
3111 return start;
3112 }
3113 let _rs0 = std::time::Instant::now();
3114 if !pending.is_empty() {
3115 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3116 let mut outs: Vec<&mut [f32]> = self
3117 .kv_cache
3118 .layers
3119 .iter_mut()
3120 .enumerate()
3121 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3122 .map(|(_, s)| s.linear_state.as_mut_slice())
3123 .collect();
3124 graph.read_states(&mut outs);
3125 }
3126 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3127 use std::sync::atomic::{AtomicU64, Ordering};
3128 static SY: AtomicU64 = AtomicU64::new(0);
3129 static RS: AtomicU64 = AtomicU64::new(0);
3130 static N: AtomicU64 = AtomicU64::new(0);
3131 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3132 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3133 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3134 if n % 100 == 0 {
3135 eprintln!(
3136 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3137 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3138 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3139 );
3140 }
3141 }
3142 if let Some(rows) = lm_rows {
3143 crate::gpu::hostprof_encode_done(_mt0);
3144 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3145 graph.read_logits(&mut lg);
3146 crate::gpu::hostprof_total(_mt0);
3147 lg.resize(self.vocab_size, 0.0);
3148 if let Some(c) = self.final_softcap {
3149 softcap_in_place(&mut lg, c, self.pool.as_deref());
3150 }
3151 self.graph_logits = Some(lg);
3152 }
3153 graph.read_h(h);
3154 if self.graph_head_required && self.graph_logits.is_none() {
3155 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3156 self.fail_metal_graph("fused graph head completed without logits readback");
3157 return start;
3158 }
3159 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3160 METAL_GRAPH_LAYERS.fetch_add(
3161 end.saturating_sub(start) as u64,
3162 std::sync::atomic::Ordering::Relaxed,
3163 );
3164 if self.graph_head_required {
3165 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3166 }
3167 for (li, nkv, hd) in dev_attn {
3171 let mut krow = attention::take_buf(nkv * hd);
3172 let mut vrow = attention::take_buf(nkv * hd);
3173 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3174 let cache = &mut self.kv_cache.layers[li];
3175 cache.append(&krow, &vrow, &[]);
3176 let n = cache.seq_len;
3177 let mut imp = attention::take_buf(n);
3178 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3179 cache.accumulate_imp(&imp);
3180 attention::recycle_buf(&mut imp);
3181 }
3182 attention::recycle_buf(&mut krow);
3183 attention::recycle_buf(&mut vrow);
3184 }
3185 if let Some((_, arm)) = ab {
3186 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3187 }
3188 end
3189 }
3190
3191 pub fn new(
3192 tokenizer: Tokenizer,
3193 weights: PipelineWeights,
3194 hidden_size: usize,
3195 intermediate_size: usize,
3196 num_heads: usize,
3197 num_kv_heads: usize,
3198 head_dim: usize,
3199 num_layers: usize,
3200 physical_layers: usize,
3201 loop_final_norm: bool,
3202 vocab_size: usize,
3203 rms_eps: f64,
3204 rope_base: f32,
3205 norm_style: NormStyle,
3206 max_seq_len: usize,
3207 sampler_config: SamplerConfig,
3208 ) -> Self {
3209 let rng = match sampler_config.seed {
3210 Some(s) => SplitMix64::new(s),
3211 None => SplitMix64::from_entropy(),
3212 };
3213 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3214 let pool = Pool::from_env();
3215 if let Some(p) = &pool {
3216 tracing::info!("worker pool: {} threads", p.n_workers());
3217 if let Some(model) = weights
3219 .lm_head
3220 .model_arc()
3221 .or_else(|| weights.embed_tokens.model_arc())
3222 {
3223 let regions: Vec<&[u8]> =
3224 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3225 p.bind_numa(®ions);
3226 }
3227 }
3228 Self {
3229 gpu_plan: None,
3230 tokenizer: std::sync::Arc::new(tokenizer),
3231 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3232 sampler_config,
3233 weights,
3234 hidden_size,
3235 intermediate_size,
3236 num_heads,
3237 num_kv_heads,
3238 head_dim,
3239 num_layers,
3240 physical_layers,
3241 loop_final_norm,
3242 vocab_size,
3243 rms_eps,
3244 rope_base,
3245 norm_style,
3246 rotary_dim: head_dim,
3247 attention_heads_per_layer: None,
3248 kv_heads_per_layer: None,
3249 v_head_dim: None,
3250 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3251 .filter(|v| !v.is_empty())
3252 .map(std::path::PathBuf::from),
3253 graph_declines: std::cell::RefCell::new(Vec::new()),
3254 mimo_moe: Default::default(),
3255 vmf_cfg: None,
3256 gdn_cfg: None,
3257 kda_cfg: None,
3258 g3n: None,
3259 dsv4: None,
3260 dsv41: None,
3261 dsv41_vision: None,
3262 dsv41_prefill: None,
3263 qwen4_exp: None,
3264 dsv4_mtp: Vec::new(),
3265 dspark: None,
3266 dspark_pending: Vec::new(),
3267 dspark_hist: Vec::new(),
3268 dspark_real: Vec::new(),
3269 dspark_trunk_picks: Vec::new(),
3270 dspark_exp: Vec::new(),
3271 dspark_draft_ns: 0,
3272 logit_multiplier: None,
3273 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3274 graph_failed: std::sync::atomic::AtomicBool::new(false),
3275 kv_history: Vec::new(),
3276 kv_history_device: false,
3277 short_conv_cfg: None,
3278 mtp: None,
3279 mimo_mtp: None,
3280 verify_exact_moe: false,
3281 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3282 ignore_eos: false,
3283 draft_full_streak: 0,
3284 spec_k_adapt: None,
3285 spec_acc_ewma: 0.7,
3286 rng,
3287 sampler_scratch: SamplerScratch::default(),
3288 spec_forced: None,
3289 spec_q: Vec::new(),
3290 spec_p: Vec::new(),
3291 spec_res: Vec::new(),
3292 spec_qs: Vec::new(),
3293 spec_ps: Vec::new(),
3294 spec_ress: Vec::new(),
3295 mtp_graph_mode: None,
3296 #[cfg(target_os = "macos")]
3297 metal_verify: None,
3298 inv_freq,
3299 ws: ForwardScratch::new(hidden_size),
3300 pool,
3301 model: None,
3302 dyn_force_f32: false,
3303 dyn_skill_layers: Vec::new(),
3304 dyn_active: None,
3305 dyn_blend_loaded: false,
3306 dyn_phi_layer: None,
3307 dyn_phi_ema: Vec::new(),
3308 dyn_phi_seen: 0,
3309 dyn_router: None,
3310 o1_cfg: None,
3311 o1_epoch: 0,
3312 o1_flags: Vec::new(),
3313 trace: false,
3314 calib_temp: 1.0,
3315 confidence_on: true,
3316 embed_multiplier: 1.0,
3317 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3318 swa: None,
3319 swa_trim: None,
3320 sliding_layers: None,
3321 anchor_core: None,
3322 bounded_rope: None,
3323 kv_prefix: KvPrefix::default(),
3324 last_prefill_tokens: 0,
3325 inv_freq_local: None,
3326 rotary_dim_local: None,
3327 rope_scale: 1.0,
3328 rope_scale_local: 1.0,
3329 global_attn: None,
3330 inv_freq_global: None,
3331 attn_v_norm: false,
3332 qk_norm_after_rope: false,
3333 proj_gate_sigmoid: false,
3334 final_softcap: None,
3335 head_clusters: None,
3336 attn_softcap: 0.0,
3337 graph_want_logits: false,
3338 graph_head_required: false,
3339 graph_logits: None,
3340 embryo_graph: None,
3341 graph_refused: std::sync::atomic::AtomicBool::new(false),
3342 graph_kv_id: {
3343 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3344 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3345 },
3346 #[cfg(test)]
3347 nll_test_fail_at: None,
3348 #[cfg(test)]
3349 nll_test_force_serial: false,
3350 }
3351 }
3352
3353 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3361 if let Err(e) = self.try_set_o1(cfg) {
3362 tracing::error!("{e}");
3363 }
3364 }
3365
3366 pub fn bounded_native(&self) -> bool {
3370 self.anchor_core.is_some()
3371 }
3372
3373 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3376 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3377 }
3378
3379 pub fn o1_refusal(&self) -> Option<String> {
3381 self.anchor_core.as_ref().map(|ac| {
3382 format!(
3383 "--o1 / CMF_O1 refused: the anchor is native bounded \
3384 (anchor_core kind={} window={} sink={}); the file's operator \
3385 is executed as-is and no post-hoc Nyström overlay applies",
3386 ac.kind, ac.window, ac.sink
3387 )
3388 })
3389 }
3390
3391 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3393 if let Some(c) = &cfg {
3394 if let Some(why) = self.o1_refusal() {
3395 self.o1_flags = Vec::new();
3396 self.o1_cfg = None;
3397 return Err(why);
3398 }
3399 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3400 self.o1_flags.clear();
3401 self.o1_cfg = None;
3402 return Err(format!(
3403 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3404 c.w, c.sink
3405 ));
3406 }
3407 }
3408 self.o1_flags = match &cfg {
3409 Some(c) => {
3410 let mut flags = c.layer_flags(self.num_layers);
3411 for (li, f) in flags.iter_mut().enumerate() {
3412 if *f
3418 && (!matches!(
3419 self.weights.layers[self.phys_layer(li)].attn,
3420 AttnKind::Full { .. }
3421 ) || self.layer_window(li).is_some()
3422 || self.kv_cache.layers[li].sinks.is_some()
3423 || self.layer_v_dim(li) != self.layer_geom(li).1)
3424 {
3425 *f = false;
3426 }
3427 }
3428 flags
3429 }
3430 None => Vec::new(),
3431 };
3432 if let Some(c) = &cfg {
3433 let n = self.o1_flags.iter().filter(|&&f| f).count();
3434 tracing::info!(
3435 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3436 self.num_layers,
3437 c.m,
3438 c.w,
3439 c.sink,
3440 c.rect
3441 );
3442 }
3443 self.o1_cfg = cfg;
3444 Ok(())
3445 }
3446
3447 pub fn install_bounded(
3453 &mut self,
3454 cfg: &cortiq_core::AnchorCoreConfig,
3455 ) -> Result<(), String> {
3456 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3457 return Err(format!(
3458 "anchor_core kind '{}' is not executable by this runtime",
3459 cfg.kind
3460 ));
3461 }
3462 if cfg.window == 0 {
3463 return Err("anchor_core.window must be >= 1".into());
3464 }
3465 let mut n = 0usize;
3466 for li in 0..self.num_layers {
3467 let pl = self.phys_layer(li);
3468 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3469 if w.window != cfg.window || w.sink != cfg.sink {
3470 return Err(format!(
3471 "layer {li}: bounded weights (window {} sink {}) disagree with \
3472 anchor_core (window {} sink {})",
3473 w.window, w.sink, cfg.window, cfg.sink
3474 ));
3475 }
3476 self.kv_cache.layers[li].install_bounded(cfg.window);
3477 n += 1;
3478 }
3479 }
3480 if n == 0 {
3481 return Err("anchor_core is present but no layer executes it".into());
3482 }
3483 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3484 self.bounded_rope = Some(std::sync::Arc::new(rope));
3485 self.anchor_core = Some(cfg.clone());
3486 self.embryo_graph = None;
3487 tracing::info!(
3488 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3489 cfg.kind,
3490 cfg.window,
3491 cfg.sink,
3492 self.kv_cache.layers.iter().map(|l| l.bounded_state_bytes()).max().unwrap_or(0)
3493 );
3494 Ok(())
3495 }
3496
3497 pub fn install_wire_identity(&mut self, identity: u64) {
3501 for li in 0..self.kv_cache.layers.len() {
3502 let pl = self.phys_layer(li);
3503 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3504 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3505 Some(AttnKind::Linear(_))
3506 | Some(AttnKind::LinearGdn(_))
3507 | Some(AttnKind::ShortConv(_))
3508 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3509 _ => crate::kv_cache::WireKind::Full,
3510 };
3511 let l = &mut self.kv_cache.layers[li];
3512 l.wire_kind = kind;
3513 l.wire_identity = identity;
3514 l.wire_layer = li as u32;
3517 }
3518 }
3519
3520 pub fn clear_history(&mut self) {
3522 self.kv_history.clear();
3523 self.kv_history_device = false;
3524 self.kv_prefix.clear();
3525 }
3526
3527 pub fn graph_refused(&self) -> bool {
3531 self.graph_refused
3532 .load(std::sync::atomic::Ordering::Relaxed)
3533 }
3534
3535 pub fn mark_graph_refused(&self) {
3537 if !self
3538 .graph_refused
3539 .swap(true, std::sync::atomic::Ordering::Relaxed)
3540 {
3541 tracing::info!(
3542 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3543 self.graph_kv_id
3544 );
3545 }
3546 }
3547
3548 pub fn device_sequence_position(&self) -> Option<usize> {
3552 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3553 }
3554
3555 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3560 let dev = self.device_sequence_position();
3561 if recorded_on_device {
3562 dev == Some(n) && self.embryo_resident_wanted()
3563 } else {
3564 dev.is_none()
3565 }
3566 }
3567
3568 pub(crate) fn invalidate_for_weight_change(&mut self) {
3576 self.clear_sequence_state();
3577 self.embryo_graph = None;
3578 }
3579
3580 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3586 let (n, on_device) = if self.bounded_native() {
3587 (self.kv_prefix.extension(input_ids), self.kv_prefix.on_device())
3588 } else {
3589 let h = &self.kv_history;
3590 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3591 (h.len(), self.kv_history_device)
3592 } else {
3593 (0, false)
3594 }
3595 };
3596 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3599 tracing::warn!(
3600 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3601 the device now holds {:?} — re-prefilling from zero",
3602 if on_device { "resident device" } else { "host" },
3603 self.device_sequence_position()
3604 );
3605 return 0;
3606 }
3607 n
3608 }
3609
3610 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3613 self.cached_prefix_len(input_ids)
3614 }
3615
3616 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3622 let on_device = self.device_sequence_position().is_some();
3623 if self.bounded_native() {
3624 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3625 let prev_device = self.kv_prefix.on_device();
3626 self.kv_history.clear();
3627 self.kv_history_device = false;
3628 if keep && prev_device == on_device {
3629 self.kv_prefix.extend(&consumed[reused..]);
3630 } else {
3631 self.kv_prefix.set(consumed);
3632 }
3633 self.kv_prefix.set_on_device(on_device);
3634 } else {
3635 self.kv_history = consumed.to_vec();
3636 self.kv_history_device = on_device;
3637 }
3638 }
3639
3640 pub fn o1_active(&self) -> bool {
3642 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3643 }
3644
3645 pub fn generation_batch_k(&self) -> usize {
3663 if let Some(k) = std::env::var("CMF_BATCH_K")
3664 .ok()
3665 .and_then(|v| v.parse::<usize>().ok())
3666 {
3667 return k;
3668 }
3669 if self.g4_batch_prefill() {
3672 return 128;
3673 }
3674 #[cfg(not(target_os = "macos"))]
3675 if self.graph_prefill_preferred() && !self.o1_active() {
3676 let moe = self
3679 .weights
3680 .layers
3681 .iter()
3682 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3683 let q82 = crate::gpu::moe_q82_graph_on()
3689 && self.weights.layers.iter().any(|lw| {
3690 matches!(&lw.ffn, FfnKind::Moe(m)
3691 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3692 });
3693 return if q82 {
3694 128
3695 } else if moe {
3696 32
3697 } else {
3698 128
3699 };
3700 }
3701 0
3702 }
3703
3704 pub fn generation_graph_prefill(&self) -> bool {
3705 let graph = self.graph_prefill_preferred();
3706 #[cfg(not(target_os = "macos"))]
3717 if graph
3718 && self.generation_batch_k() > 0
3719 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3720 {
3721 return false;
3722 }
3723 graph
3724 }
3725
3726 pub fn o1_device_stats(&self) -> (usize, u64) {
3731 crate::gpu::o1_device_stats(self.graph_kv_id)
3732 }
3733
3734 pub fn o1_begin(&mut self) {
3739 self.o1_begin_with_prefix(None);
3740 }
3741
3742 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
3746 if let Some(c) = &self.o1_cfg {
3747 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
3748 let boundary = requested_prefix.map(|p| {
3749 p.max(
3750 crate::nystrom::o1_deferred_boundary(w, sink)
3751 .expect("o1 config boundary validated in set_o1"),
3752 )
3753 });
3754 for (li, &f) in self.o1_flags.iter().enumerate() {
3755 if f {
3756 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
3757 }
3758 }
3759 }
3760 }
3761
3762 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
3764 self.o1_cfg.as_ref().and_then(|c| {
3765 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
3766 .map(|floor| requested_prefix.max(floor))
3767 })
3768 }
3769
3770 fn o1_note_transition(&mut self) {
3771 let mut transitioned = false;
3775 for (li, &flagged) in self.o1_flags.iter().enumerate() {
3776 if flagged {
3777 transitioned |= self.kv_cache.layers[li].take_o1_transition();
3778 }
3779 }
3780 if transitioned {
3781 self.o1_epoch = self.o1_epoch.wrapping_add(1);
3782 }
3783 }
3784
3785 fn o1_pending(&self) -> bool {
3786 self.o1_flags.iter().enumerate().any(|(li, &f)| {
3787 f && self.kv_cache.layers[li].seq_len > 0
3788 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
3789 })
3790 }
3791
3792 fn o1_fail(&mut self, err: String) {
3793 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
3794 self.clear_sequence_state();
3795 self.graph_failed
3796 .store(true, std::sync::atomic::Ordering::Relaxed);
3797 self.cancel
3798 .store(true, std::sync::atomic::Ordering::Relaxed);
3799 }
3800
3801 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
3806 if self.o1_cfg.is_none() {
3807 return Ok(false);
3808 }
3809 let mut participating = false;
3810 for li in 0..self.num_layers {
3811 if !self.o1_flags.get(li).copied().unwrap_or(false) {
3812 continue;
3813 }
3814 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3815 return Err(err);
3816 }
3817 if self.kv_cache.layers[li].seq_len == 0 {
3818 continue;
3819 }
3820 participating = true;
3821 let num_heads = self.layer_num_heads(li);
3822 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
3823 }
3824 self.o1_note_transition();
3825 for li in 0..self.num_layers {
3826 if self.o1_flags.get(li).copied().unwrap_or(false) {
3827 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3828 return Err(err);
3829 }
3830 }
3831 }
3832 Ok(participating
3833 && (0..self.num_layers).all(|li| {
3834 !self.o1_flags.get(li).copied().unwrap_or(false)
3835 || self.kv_cache.layers[li].seq_len == 0
3836 || self.kv_cache.layers[li].o1_sealed()
3837 }))
3838 }
3839
3840 fn o1_progress(&mut self) {
3843 if !self.o1_active() {
3844 return;
3845 }
3846 for li in 0..self.num_layers {
3847 if self.o1_flags.get(li).copied().unwrap_or(false) {
3848 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3849 self.o1_fail(err);
3850 return;
3851 }
3852 }
3853 }
3854 self.o1_note_transition();
3858 if !self.o1_pending() {
3859 return;
3860 }
3861 if let Err(err) = self.o1_seal_checked() {
3862 self.o1_fail(err);
3863 }
3864 }
3865
3866 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
3871 if self
3872 .graph_failed
3873 .swap(false, std::sync::atomic::Ordering::Relaxed)
3874 {
3875 self.cancel
3876 .store(false, std::sync::atomic::Ordering::Relaxed);
3877 self.clear_sequence_state();
3878 return Err(format!("{phase}: deferred O(1) transition failed"));
3879 }
3880 Ok(())
3881 }
3882
3883 pub fn o1_seal(&mut self) {
3887 if let Err(err) = self.o1_seal_checked() {
3888 self.o1_fail(err);
3889 }
3890 }
3891
3892 pub fn set_trace(&mut self, on: bool) {
3894 self.trace = on;
3895 }
3896
3897 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
3900 self.rng = match config.seed {
3901 Some(seed) => SplitMix64::new(seed),
3902 None => SplitMix64::from_entropy(),
3903 };
3904 self.sampler_config = config;
3905 }
3906
3907 pub fn set_confidence(&mut self, on: bool) {
3912 self.confidence_on = on;
3913 }
3914
3915 pub fn set_calib_temp(&mut self, t: f32) {
3918 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
3919 }
3920
3921 pub fn calib_temp(&self) -> f32 {
3923 self.calib_temp
3924 }
3925
3926 pub fn rope_inv_freq(&self) -> &[f32] {
3930 self.inv_freq.as_slice()
3931 }
3932
3933 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
3936 self.rotary_dim = rotary_dim.min(self.head_dim);
3937 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
3938 self.embryo_graph = None;
3942 }
3943
3944 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
3945 QwenAttnCfg {
3946 num_heads: self.num_heads,
3947 num_kv_heads: self.num_kv_heads,
3948 head_dim: self.head_dim,
3949 hidden_size: self.hidden_size,
3950 position,
3951 inv_freq: &self.inv_freq,
3952 rotary_dim: self.rotary_dim,
3953 scale: self.attn_scale,
3954 softcap: self.attn_softcap,
3955 window: None,
3956 v_norm: false,
3957 qk_norm_after_rope: self.qk_norm_after_rope,
3958 gate_sigmoid: self.proj_gate_sigmoid,
3959 q_norm: None,
3960 k_norm: None,
3961 output_gate: false,
3962 softplus_gate: None,
3963 rope_scale: self.rope_scale,
3964 bias: None,
3965 rms_eps: self.rms_eps,
3966 norm_style: self.norm_style,
3967 pool: self.pool.as_deref(),
3968 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
3969 }
3970 }
3971
3972 pub fn generate(
3974 &mut self,
3975 prompt: &str,
3976 max_tokens: usize,
3977 task_mask: Option<&TaskMask>,
3978 on_token: Option<TokenCallback>,
3979 ) -> Result<GenerateResult, String> {
3980 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
3981 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
3982 }
3983
3984 pub fn generate_from_vl(
3987 &mut self,
3988 input: &crate::dsv41_vision::PreparedVlInputs,
3989 max_tokens: usize,
3990 task_mask: Option<&TaskMask>,
3991 on_token: Option<TokenCallback>,
3992 ) -> Result<GenerateResult, String> {
3993 let Some(dsv41) = &self.dsv41 else {
3994 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
3995 };
3996 if input.token_ids.is_empty() {
3997 return Err("empty V4.1 multimodal prompt".into());
3998 }
3999 if input.token_types.len() != input.token_ids.len() {
4000 return Err(format!(
4001 "V4.1 token type count {} != token count {}",
4002 input.token_types.len(),
4003 input.token_ids.len()
4004 ));
4005 }
4006 let dim = dsv41.2.dim;
4007 let mut embeddings = vec![None; input.token_ids.len()];
4008 let mut participates = vec![true; input.token_ids.len()];
4009 if !input.images.is_empty() {
4010 let vision = self
4011 .dsv41_vision
4012 .as_ref()
4013 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
4014 for image in &input.images {
4015 let end = image.start.saturating_add(image.types.len());
4016 if end > input.token_ids.len() {
4017 return Err(format!(
4018 "V4.1 image span {}..{} exceeds prompt length {}",
4019 image.start,
4020 end,
4021 input.token_ids.len()
4022 ));
4023 }
4024 let mut span = vec![0.0f32; image.types.len() * dim];
4025 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
4026 for (offset, &kind) in image.types.iter().enumerate() {
4027 let pos = image.start + offset;
4028 if input.token_types[pos] != kind {
4029 return Err(format!(
4030 "V4.1 image type mismatch at position {pos}: {} != {kind}",
4031 input.token_types[pos]
4032 ));
4033 }
4034 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4035 participates[pos] = false;
4036 }
4037 }
4038 }
4039 for (pos, &kind) in input.token_types.iter().enumerate() {
4040 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4041 return Err(format!("V4.1 text position {pos} has an image embedding"));
4042 }
4043 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4044 return Err(format!("V4.1 image position {pos} has no image embedding"));
4045 }
4046 }
4047 self.dsv41_prefill = Some((embeddings, participates));
4048 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4049 self.dsv41_prefill = None;
4050 result
4051 }
4052
4053 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4055 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4056 }
4057
4058 pub fn generate_from_ids(
4066 &mut self,
4067 input_ids: &[u32],
4068 max_tokens: usize,
4069 task_mask: Option<&TaskMask>,
4070 on_token: Option<TokenCallback>,
4071 ) -> Result<GenerateResult, String> {
4072 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4073 }
4074
4075 pub fn generate_from_embeds(
4081 &mut self,
4082 input_ids: &[u32],
4083 prompt_rows: &[f32],
4084 max_tokens: usize,
4085 task_mask: Option<&TaskMask>,
4086 on_token: Option<TokenCallback>,
4087 ) -> Result<GenerateResult, String> {
4088 if input_ids.is_empty()
4089 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4090 {
4091 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4092 }
4093 if prompt_rows.iter().any(|x| !x.is_finite()) {
4094 return Err("embedded prompt contains non-finite values".into());
4095 }
4096 if !self.can_prefill_batched() || self.dyn_router.is_some()
4097 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
4098 {
4099 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4100 }
4101 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
4102 }
4103
4104 fn generate_with_prompt_rows(
4105 &mut self,
4106 input_ids: &[u32],
4107 prompt_rows: Option<&[f32]>,
4108 max_tokens: usize,
4109 task_mask: Option<&TaskMask>,
4110 mut on_token: Option<TokenCallback>,
4111 ) -> Result<GenerateResult, String> {
4112 #[cfg(target_os = "macos")]
4113 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4114 if std::env::var("CMF_TRACE_H").is_ok() {
4115 eprintln!("input_ids: {input_ids:?}");
4116 }
4117 if input_ids.is_empty() {
4118 return Err("empty prompt: nothing to generate from".to_string());
4119 }
4120 self.graph_failed
4124 .store(false, std::sync::atomic::Ordering::Relaxed);
4125 let task_mask = self.drop_open_mask(task_mask);
4130
4131 let mut reuse_from = {
4139 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4140 if on
4141 && prompt_rows.is_none()
4142 && task_mask.is_none()
4143 && self.mtp.is_none()
4144 && !(self.mimo_mtp.is_some() && self.speculative)
4145 && self.o1_cfg.is_none()
4146 && self.dsv41.is_none()
4147 {
4148 self.cached_prefix_len(input_ids)
4149 } else {
4150 0
4151 }
4152 };
4153 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4156 reuse_from = 0;
4157 }
4158 self.last_prefill_tokens = input_ids.len() - reuse_from;
4159 let bounded_native = self.bounded_native();
4160 if reuse_from == 0 {
4161 self.clear_sequence_state();
4163 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4164 eprintln!(
4165 "kv-reuse: {} of {} prompt positions already cached",
4166 reuse_from,
4167 input_ids.len()
4168 );
4169 }
4170 crate::gpu::graph_race_begin_generation();
4171 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4175 std::env::var("CMF_O1_PREFILL")
4176 .ok()
4177 .and_then(|v| v.parse::<usize>().ok())
4178 .filter(|&p| p > 0)
4179 } else {
4180 None
4181 };
4182 if task_mask.is_none() {
4183 self.o1_begin_with_prefix(o1_prefill);
4184 }
4185
4186 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4192 #[cfg(target_os = "macos")]
4231 let metal_graph = crate::gpu::q1_force()
4232 && crate::gpu::enabled_here()
4233 && std::env::var("CMF_GPU_BLOCK")
4234 .map(|v| v != "0")
4235 .unwrap_or(true);
4236 #[cfg(not(target_os = "macos"))]
4237 let metal_graph = false;
4238 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4239 let spec_cheap_round = self.sampler_config.temperature < 1e-6
4243 || sampler::sparse_ok(&self.sampler_config);
4244 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4245 || match spec_sample_env.as_deref() {
4246 Some("1") => true,
4247 Some(_) => false,
4248 None => metal_graph && spec_cheap_round,
4249 };
4250 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4266 for lw in &self.weights.layers {
4267 if let FfnKind::Dense(d) = &lw.ffn {
4268 dense_n += 1;
4269 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4270 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4271 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4272 {
4273 dense_q4tp += 1;
4274 }
4275 }
4276 }
4277 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4278 let penalized = !metal_graph
4290 && (self.sampler_config.repetition_penalty != 1.0
4291 || self.sampler_config.presence_penalty != 0.0
4292 || !self.sampler_config.suppress_tokens.is_empty());
4293 #[cfg(feature = "gpu")]
4298 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4299 #[cfg(not(feature = "gpu"))]
4300 let metal_wgpu = false;
4301 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4302 let spec_wanted = match spec_env.as_deref() {
4303 Some("0") => false,
4304 Some(_) => {
4305 if metal_wgpu {
4306 tracing::warn!(
4307 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4308 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4309 );
4310 }
4311 true
4312 }
4313 None => spec_default_ok && !penalized && !metal_wgpu,
4314 };
4315 let graph_spec = self.speculative
4319 && (graph_on || metal_graph)
4320 && self.mtp.is_some()
4321 && task_mask.is_none()
4322 && !self.o1_active()
4323 && spec_sampling_ok
4324 && spec_wanted;
4325 #[cfg(target_os = "macos")]
4329 if metal_graph {
4330 static SAID: std::sync::Once = std::sync::Once::new();
4331 SAID.call_once(|| {
4332 let spec = if graph_spec {
4333 let k = std::env::var("CMF_GRAPH_SPEC_K")
4334 .ok()
4335 .and_then(|v| v.parse::<usize>().ok())
4336 .filter(|&v| (1..=8).contains(&v))
4337 .unwrap_or(7);
4338 let arm = if self.sampler_config.temperature < 1e-6 {
4339 "greedy"
4340 } else {
4341 "sampling"
4342 };
4343 format!(
4344 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4345 Self::draft_vocab_rows(usize::MAX)
4346 )
4347 } else if !self.speculative {
4348 "spec off (CMF_MTP=0)".to_string()
4349 } else if self.mtp.is_none() {
4350 "spec off (no MTP head)".to_string()
4351 } else if !spec_sampling_ok {
4352 if spec_cheap_round {
4353 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4354 } else {
4355 "spec off (sampling without a top-k: the dense chain \
4356 costs more than it saves)"
4357 .to_string()
4358 }
4359 } else if !spec_wanted {
4360 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4361 } else if task_mask.is_some() {
4362 "spec off (task mask)".to_string()
4363 } else {
4364 "spec off (O(1) attention)".to_string()
4365 };
4366 let on = |var: &str| {
4367 if std::env::var(var).as_deref() == Ok("0") {
4368 "off"
4369 } else {
4370 "on"
4371 }
4372 };
4373 tracing::info!(
4374 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4375 MTP graph {}, attend {}, probe {}",
4376 if crate::gpu_metal::state4_on() { "on" } else { "off" },
4377 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
4378 on("CMF_METAL_PREFILL"),
4379 on("CMF_MTP_GRAPH"),
4380 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4381 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
4382 );
4383 });
4384 }
4385 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4392 let spec_active = self.speculative
4393 && self.mtp.is_some()
4394 && task_mask.is_none()
4395 && !self.o1_active()
4396 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4397 let mut mtp = if spec_active { self.mtp.take() } else { None };
4400 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4401 eprintln!(
4402 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4403 mtp.is_some(),
4404 self.speculative,
4405 self.sampler_config.temperature < 1e-6,
4406 );
4407 }
4408 if let Some(m) = &mut mtp {
4409 m.kv.clear();
4410 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4412 self.mtp_graph_mode = None;
4413 }
4414 let mimo_spec = self.speculative
4418 && self.mimo_mtp.is_some()
4419 && task_mask.is_none()
4420 && !self.o1_active()
4421 && self.dyn_router.is_none()
4422 && self.sampler_config.temperature < 1e-6
4423 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4424 if let Some(st) = self.mimo_mtp.as_mut() {
4425 st.reset();
4426 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4427 Self::mimo_mtp_hist_cap(st, input_ids.len());
4428 }
4429 }
4430 let mut router = if mtp.is_none() {
4434 self.dyn_router.take()
4435 } else {
4436 None
4437 };
4438 let mut reuse_from = reuse_from;
4439 if let Some(r) = &mut router {
4440 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4443 let _ = self.set_active_skill(None);
4446 reuse_from = 0;
4447 self.last_prefill_tokens = input_ids.len();
4448 }
4449 }
4450
4451 let mut all_ids = input_ids.to_vec();
4452 let mut generated = 0usize;
4453 let mut finish_reason = "max_tokens".to_string();
4454 let mut drafted = 0usize;
4455 let mut accepted = 0usize;
4456 let mut dsv4_spec_bad = 0usize;
4463 let mut dsv4_spec_retry_at = 0usize;
4464 let mut confidence: Vec<f32> = Vec::new();
4465 let trace_on = self.trace;
4466 let calib_temp = self.calib_temp;
4467 let mut traces: Vec<TokenTrace> = Vec::new();
4468
4469 let mut hidden = vec![0.0f32; self.hidden_size];
4475 let mut pos = reuse_from;
4476 let fuse_lm = mtp.is_none()
4485 && router.is_none()
4486 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4487 self.graph_logits = None;
4488 self.graph_want_logits = false;
4489 let _tpf = std::time::Instant::now();
4490 let batch_k = self.generation_batch_k();
4491 if let Some(rows) = prompt_rows {
4492 let hs = self.hidden_size;
4493 let chunk = self.prefill_chunk().max(1);
4494 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4495 let end = (pos + chunk).min(input_ids.len());
4496 let hb = match self.prefill_input_rows(
4497 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
4498 ) {
4499 Ok(hb) => hb,
4500 Err(err) => {
4501 self.finish_generation(&mut mtp, &mut router, true);
4502 return Err(err);
4503 }
4504 };
4505 if mimo_spec { self.mimo_note_rows(&hb, pos); }
4506 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4507 pos = end;
4508 }
4509 }
4510 while self.qwen4_exp.is_some()
4521 && mtp.is_none()
4522 && pos < input_ids.len()
4523 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4524 {
4525 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4528 let want_logits = end == input_ids.len();
4529 let mut lg = Vec::new();
4530 if let Some(b) = &mut self.qwen4_exp {
4531 crate::qwen4_exp::forward_tokens(
4532 &b.0,
4533 &b.1,
4534 &b.2,
4535 &mut b.3,
4536 &input_ids[pos..end],
4537 pos,
4538 &self.inv_freq,
4539 self.pool.as_deref(),
4540 &mut lg,
4541 want_logits,
4542 );
4543 }
4544 if want_logits {
4545 self.graph_logits = Some(lg);
4546 }
4547 pos = end;
4548 hidden.fill(0.0);
4549 }
4550 while self.dsv4.is_some()
4551 && mtp.is_none()
4552 && pos < input_ids.len()
4553 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4554 {
4555 let end = (pos + prefill_chunk()).min(input_ids.len());
4556 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4557 let mut lg = Vec::new();
4558 if let Some(b) = &mut self.dsv4 {
4559 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4560 crate::dsv4::forward_chunk(
4561 g,
4562 layers,
4563 &cfg,
4564 st,
4565 &ids,
4566 pos,
4567 &self.inv_freq,
4568 self.pool.as_deref(),
4569 &mut lg,
4570 end == input_ids.len(),
4571 );
4572 }
4573 if end == input_ids.len() {
4574 self.graph_logits = Some(lg);
4575 }
4576 pos = end;
4577 hidden = vec![0.0; self.hidden_size];
4578 }
4579 let dsv41_prefill = self.dsv41_prefill.take();
4580 while self.dsv41.is_some()
4581 && mtp.is_none()
4582 && pos < input_ids.len()
4583 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4584 {
4585 let end = (pos + prefill_chunk()).min(input_ids.len());
4586 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4587 let mut lg = Vec::new();
4588 if let Some(b) = &mut self.dsv41 {
4589 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4590 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4591 crate::dsv41::forward_chunk_masked_with_embeddings(
4592 g,
4593 layers,
4594 cfg,
4595 st,
4596 &ids,
4597 pos,
4598 &embeddings[pos..end],
4599 &participates[pos..end],
4600 self.pool.as_deref(),
4601 &mut lg,
4602 );
4603 } else {
4604 crate::dsv41::forward_chunk(
4605 g,
4606 layers,
4607 cfg,
4608 st,
4609 &ids,
4610 pos,
4611 self.pool.as_deref(),
4612 &mut lg,
4613 );
4614 }
4615 }
4616 if end == input_ids.len() {
4617 self.graph_logits = Some(lg);
4618 }
4619 pos = end;
4620 hidden = vec![0.0; self.hidden_size];
4621 }
4622 let dyn_prefill = router.is_some();
4627 let o1_prefill_limit = o1_prefill
4635 .and_then(|requested| self.o1_effective_boundary(requested))
4636 .map(|boundary| boundary.min(input_ids.len()));
4637 let mut o1_sealed = false;
4638 if let Some(limit) = o1_prefill_limit {
4639 if self.can_prefill_batched() && limit > 2 {
4642 let chunk = self.prefill_chunk();
4643 let hs = self.hidden_size;
4644 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4645 let end = (pos + chunk).min(limit);
4646 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4647 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4648 pos = end;
4649 }
4650 } else {
4651 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4652 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4653 pos += 1;
4654 }
4655 }
4656 if pos >= limit {
4657 o1_sealed = match self.o1_seal_checked() {
4658 Ok(sealed) => sealed,
4659 Err(err) => {
4660 self.finish_generation(&mut mtp, &mut router, true);
4661 return Err(err);
4662 }
4663 };
4664 tracing::info!(
4665 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4666 o1_prefill.unwrap_or(0),
4667 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4668 .unwrap_or(limit),
4669 limit,
4670 input_ids.len()
4671 );
4672 }
4673 }
4674 let graph_prefill = self.graph_prefill_preferred();
4680 #[cfg(target_os = "macos")]
4688 if task_mask.is_none()
4689 && !dyn_prefill
4690 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4691 && crate::gpu::enabled_here()
4692 && self.gdn_cfg.is_some()
4693 && self.g3n.is_none()
4694 && input_ids.len() > 8
4695 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4696 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4697 {
4698 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4699 .ok()
4700 .and_then(|v| v.parse().ok())
4701 .filter(|&v| (16..=512).contains(&v))
4702 .unwrap_or(256);
4703 let hs = self.hidden_size;
4704 let _tp = std::time::Instant::now();
4705 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4706 let end = (pos + chunk).min(input_ids.len());
4707 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4708 MetalPrefillOutcome::Completed(hb) => hb,
4709 MetalPrefillOutcome::Declined => break,
4710 MetalPrefillOutcome::Failed => {
4711 self.finish_generation(&mut mtp, &mut router, true);
4712 return Err("ordinary Metal prefill failed after admission".into());
4713 }
4714 };
4715 if let Some(m) = &mut mtp {
4716 let n_pairs = if end < input_ids.len() {
4717 end - pos
4718 } else {
4719 end - pos - 1
4720 };
4721 if n_pairs > 0 {
4722 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
4723 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
4724 .collect();
4725 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
4726 for (j, (h, t)) in pairs.iter().enumerate() {
4727 let h = h.to_vec();
4728 let _ = self.mtp_step(m, &h, *t, pos + j);
4729 }
4730 }
4731 }
4732 }
4733 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4734 pos = end;
4735 }
4736 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4737 eprintln!(
4738 "metal-prefill: {} of {} tokens in {:.1} ms",
4739 pos,
4740 input_ids.len(),
4741 _tp.elapsed().as_secs_f64() * 1e3
4742 );
4743 }
4744 }
4745 #[cfg(not(target_os = "macos"))]
4750 if task_mask.is_none()
4751 && !dyn_prefill
4752 && mtp.is_none()
4753 && o1_prefill.is_none()
4754 && batch_k > 0
4755 && pos + 1 < input_ids.len()
4756 && self.g4_batch_prefill()
4757 {
4758 let t_g4 = std::time::Instant::now();
4759 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
4760 Ok((n, last)) => {
4761 if n > 0 {
4762 hidden.copy_from_slice(&last);
4763 }
4764 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4765 eprintln!(
4766 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
4767 input_ids.len() - pos,
4768 t_g4.elapsed().as_secs_f64() * 1e3
4769 );
4770 }
4771 pos += n;
4772 }
4773 Err(e) => {
4774 self.finish_generation(&mut mtp, &mut router, true);
4775 return Err(e);
4776 }
4777 }
4778 }
4779 self.mimo_moe_prepare();
4780 #[cfg(not(target_os = "macos"))]
4786 if task_mask.is_none()
4787 && !dyn_prefill
4788 && !graph_prefill
4789 && mtp.is_none()
4790 && o1_prefill.is_none()
4791 && !self.o1_active()
4792 && input_ids.len() > 2
4793 && self.batch_prefix_prefill()
4794 {
4795 let chunk = self.prefill_chunk().max(1);
4796 let hs = self.hidden_size;
4797 let t_bp = std::time::Instant::now();
4798 let pos0 = pos;
4799 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4800 let end = (pos + chunk).min(input_ids.len());
4801 let bk = end - pos;
4802 let mut hiddens = vec![0f32; bk * hs];
4803 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4804 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4805 }
4806 let positions: Vec<usize> = (pos..end).collect();
4807 let mut run = 0usize;
4808 let outcome = self.try_batch_graph_wgpu_prefix(
4809 &mut hiddens,
4810 &positions,
4811 bk,
4812 None,
4813 Some(&mut run),
4814 );
4815 match outcome {
4816 crate::gpu::BatchGraphOutcome::Completed => {
4817 let hb = if run < self.num_layers {
4818 self.prefill_batch_span(
4819 PrefillIn::Hidden(&hiddens),
4820 pos,
4821 None,
4822 run,
4823 self.num_layers,
4824 )
4825 } else {
4826 hiddens
4827 };
4828 if mimo_spec {
4829 self.mimo_note_rows(&hb, pos);
4830 }
4831 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
4832 pos = end;
4833 }
4834 crate::gpu::BatchGraphOutcome::Failed => {
4835 self.finish_generation(&mut mtp, &mut router, true);
4836 return Err("batched prefix prefill failed after admission".into());
4837 }
4838 crate::gpu::BatchGraphOutcome::Declined => {
4839 #[cfg(feature = "gpu")]
4842 if pos > pos0 {
4843 self.pull_lagging_host_kv(0, self.num_layers, pos);
4844 }
4845 break;
4846 }
4847 }
4848 }
4849 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4850 eprintln!(
4851 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
4852 pos - pos0,
4853 input_ids.len(),
4854 t_bp.elapsed().as_secs_f64() * 1e3
4855 );
4856 }
4857 }
4858 if task_mask.is_none()
4859 && !dyn_prefill
4860 && !graph_prefill
4861 && self.can_prefill_batched()
4862 && self.g3n.is_none()
4863 && o1_prefill.is_none()
4864 && input_ids.len() > 2
4865 {
4866 let chunk = self.prefill_chunk();
4872 let hs = self.hidden_size;
4873 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4874 let end = (pos + chunk).min(input_ids.len());
4875 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4876 if mimo_spec {
4877 self.mimo_note_rows(&hb, pos);
4878 }
4879 if let Some(m) = &mut mtp {
4880 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4881 .ok()
4882 .and_then(|v| v.parse().ok())
4883 .unwrap_or(0);
4884 for p in pos..end {
4885 if p + 1 < input_ids.len() {
4886 if probe >= 1 && p + 2 < input_ids.len() {
4887 let (d1, mut hx) = self.mtp_step_h(
4891 m,
4892 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4893 input_ids[p + 1],
4894 p,
4895 );
4896 let mut ok = d1 == input_ids[p + 2];
4897 Self::chain_probe_note(0, ok);
4898 let mut d_prev = d1;
4899 let mut extra = 0usize;
4900 for j in 1..probe {
4901 if p + 2 + j >= input_ids.len() {
4902 break;
4903 }
4904 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
4905 extra += 1;
4906 ok = ok && dj == input_ids[p + 2 + j];
4907 Self::chain_probe_note(j, ok);
4908 d_prev = dj;
4909 hx = hj;
4910 }
4911 m.kv.truncate_last(extra);
4912 } else {
4913 let _ = self.mtp_step(
4914 m,
4915 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4916 input_ids[p + 1],
4917 p,
4918 );
4919 }
4920 }
4921 }
4922 }
4923 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4924 pos = end;
4925 }
4926 }
4927 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
4928 if task_mask.is_none()
4929 && !dyn_prefill
4930 && !graph_prefill
4931 && !pair_off
4932 && self.pair_supported()
4933 && o1_prefill.is_none()
4934 {
4935 while pos + 1 < input_ids.len()
4936 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4937 {
4938 let e1 = self.embed_single(input_ids[pos]);
4939 let e2 = self.embed_single(input_ids[pos + 1]);
4940 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
4941 if mimo_spec {
4942 self.mimo_note_rows(&h1, pos);
4943 self.mimo_note_rows(&h2, pos + 1);
4944 }
4945 self.commit_linear_scratch();
4947 if let Some(m) = &mut mtp {
4948 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
4949 if pos + 2 < input_ids.len() {
4950 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4951 .ok()
4952 .and_then(|v| v.parse().ok())
4953 .unwrap_or(0);
4954 if probe >= 1 && pos + 3 < input_ids.len() {
4955 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
4959 let mut ok = d1 == input_ids[pos + 3];
4960 Self::chain_probe_note(0, ok);
4961 let mut d_prev = d1;
4962 let mut extra = 0usize;
4963 for j in 1..probe {
4964 if pos + 3 + j >= input_ids.len() {
4965 break;
4966 }
4967 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
4968 extra += 1;
4969 ok = ok && dj == input_ids[pos + 3 + j];
4970 Self::chain_probe_note(j, ok);
4971 d_prev = dj;
4972 hx = hj;
4973 }
4974 m.kv.truncate_last(extra);
4975 } else {
4976 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
4977 }
4978 }
4979 }
4980 hidden = h2;
4981 pos += 2;
4982 }
4983 }
4984 let o1_batch_ready = o1_sealed
4997 && o1_prefill.is_some()
4998 && mtp.is_none()
4999 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
5000 && (0..self.num_layers).all(|li| {
5001 let cache = &self.kv_cache.layers[self.phys_layer(li)];
5002 cache.o1.is_none() || cache.o1_views().is_some()
5003 });
5004 let mtp_batch_prefill = mtp.is_some()
5009 && graph_prefill
5010 && task_mask.is_none()
5011 && !dyn_prefill
5012 && !self.o1_active()
5013 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
5014 if batch_k > 0
5015 && (graph_prefill || o1_batch_ready)
5016 && task_mask.is_none()
5017 && (!self.o1_active() || o1_batch_ready)
5018 && (mtp.is_none() || mtp_batch_prefill)
5019 && !dyn_prefill
5020 && pos + 1 < input_ids.len()
5021 {
5022 let hs = self.hidden_size;
5023 let chunk = batch_k;
5024 while pos < input_ids.len() {
5025 let end = (pos + chunk).min(input_ids.len());
5026 let bk = end - pos;
5027 let mut hiddens = vec![0f32; bk * hs];
5028 for (j, &id) in input_ids[pos..end].iter().enumerate() {
5029 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
5030 }
5031 let positions: Vec<usize> = (pos..end).collect();
5032 let t_chunk = std::time::Instant::now();
5033 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
5034 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5035 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5036 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5037 eprintln!(
5038 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5039 if o1_batch_ready {
5040 "o1"
5041 } else if mtp_batch_prefill {
5042 "ordinary_mtp"
5043 } else {
5044 "ordinary"
5045 },
5046 bk as f64 / (ms / 1000.0)
5047 );
5048 }
5049 {
5050 use std::sync::atomic::{AtomicBool, Ordering};
5051 static SAID: AtomicBool = AtomicBool::new(false);
5052 if !SAID.swap(true, Ordering::Relaxed) {
5053 if ok_b {
5054 tracing::info!(
5055 "batched prefill: ACTIVE mode={} (k={bk})",
5056 if o1_batch_ready {
5057 "o1"
5058 } else if mtp_batch_prefill {
5059 "ordinary_mtp"
5060 } else {
5061 "ordinary"
5062 }
5063 );
5064 } else {
5065 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5066 }
5067 }
5068 }
5069 if ok_b {
5070 if mimo_spec {
5071 self.mimo_note_rows(&hiddens, pos);
5072 }
5073 if mtp_batch_prefill {
5074 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5075 if n_pairs > 0 {
5076 let rows: Vec<Vec<f32>> = (0..n_pairs)
5082 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5083 .collect();
5084 let pairs: Vec<(&[f32], u32)> = rows
5085 .iter()
5086 .enumerate()
5087 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5088 .collect();
5089 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5090 eprintln!(
5091 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5092 pos,
5093 n_pairs,
5094 pos + n_pairs - 1,
5095 );
5096 }
5097 let warm_error = if let Some(m) = mtp.as_mut() {
5098 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5099 } else {
5100 None
5101 };
5102 if let Some(err) = warm_error {
5103 self.finish_generation(&mut mtp, &mut router, true);
5108 return Err(err.to_string());
5109 }
5110 }
5111 }
5112 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5113 pos = end;
5114 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5115 self.finish_generation(&mut mtp, &mut router, true);
5120 return Err(if o1_batch_ready {
5121 "sealed O(1) batch graph failed after admission".to_string()
5122 } else {
5123 "ordinary recurrent batch graph failed after admission".to_string()
5124 });
5125 } else {
5126 break; }
5128 }
5129 }
5130 if graph_prefill
5134 && task_mask.is_none()
5135 && mtp.is_none()
5136 && !dyn_prefill
5137 && pos == 0
5138 && input_ids.len() > 1
5139 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5140 {
5141 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5142 self.graph_logits = Some(lg);
5143 hidden = vec![0.0; self.hidden_size];
5144 pos = input_ids.len();
5145 }
5146 }
5147 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5148 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5149 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5150 if mimo_spec {
5151 self.mimo_note_rows(&hidden, pos);
5152 }
5153 if let Some(m) = &mut mtp {
5154 if pos + 1 < input_ids.len() {
5155 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5161 .ok()
5162 .and_then(|v| v.parse().ok())
5163 .unwrap_or(0);
5164 if probe >= 1 && pos + 2 < input_ids.len() {
5165 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5166 let mut ok = d1 == input_ids[pos + 2];
5167 Self::chain_probe_note(0, ok);
5168 let mut d_prev = d1;
5169 let mut extra = 0usize;
5170 for j in 1..probe {
5171 if pos + 2 + j >= input_ids.len() {
5172 break;
5173 }
5174 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5175 extra += 1;
5176 ok = ok && dj == input_ids[pos + 2 + j];
5177 Self::chain_probe_note(j, ok);
5178 d_prev = dj;
5179 hx = hj;
5180 }
5181 m.kv.truncate_last(extra);
5184 } else {
5185 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5186 }
5187 }
5188 }
5189 pos += 1;
5190 }
5191 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5192 eprintln!(
5193 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5194 input_ids.len(),
5195 _tpf.elapsed().as_secs_f64() * 1000.0
5196 );
5197 }
5198 if self
5199 .graph_failed
5200 .swap(false, std::sync::atomic::Ordering::Relaxed)
5201 {
5202 self.finish_generation(&mut mtp, &mut router, true);
5207 return Err("GPU token graph failed during prefill".to_string());
5208 }
5209 if self
5212 .cancel
5213 .swap(false, std::sync::atomic::Ordering::Relaxed)
5214 {
5215 self.finish_generation(&mut mtp, &mut router, true);
5219 return Ok(GenerateResult {
5220 text: String::new(),
5221 token_ids: Vec::new(),
5222 prompt_tokens: input_ids.len(),
5223 tokens_generated: 0,
5224 finish_reason: "cancelled".to_string(),
5225 mtp_drafted: 0,
5226 mtp_accepted: 0,
5227 token_confidence: Vec::new(),
5228 traces: Vec::new(),
5229 });
5230 }
5231
5232 if !o1_sealed {
5235 match self.o1_seal_checked() {
5236 Ok(_) => {}
5237 Err(err) => {
5238 self.finish_generation(&mut mtp, &mut router, true);
5239 return Err(err);
5240 }
5241 }
5242 }
5243
5244 macro_rules! commit {
5246 ($id:expr) => {{
5247 all_ids.push($id);
5248 generated += 1;
5249 self.note_draft_id($id);
5250 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5251 finish_reason = "stop".to_string();
5252 false
5253 } else {
5254 let token_text = self.tokenizer.decode_token($id);
5255 let mut go = true;
5256 if let Some(ref mut cb) = on_token {
5257 if !cb(&token_text) {
5258 finish_reason = "cancelled".to_string();
5259 go = false;
5260 }
5261 }
5262 go
5263 }
5264 }};
5265 }
5266
5267 let mut spec_trial = SpecTrial::Spec {
5278 t0: std::time::Instant::now(),
5279 gen0: generated,
5280 rounds: 0,
5281 };
5282 let mut spec_mon = SpecMon {
5288 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5289 ..SpecMon::default()
5290 };
5291 let mut spec_watchdog_off = false;
5292 let mut spec_walls: Vec<f32> = Vec::new();
5295 let mut spec_round_end: Option<std::time::Instant> = None;
5298 if mimo_spec {
5299 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5300 if let Some(mut st) = self.mimo_mtp.take() {
5301 self.mimo_mtp_probe(&mut st, input_ids, &path);
5302 self.mimo_mtp = Some(st);
5303 }
5304 }
5305 }
5306 let mut next_pos = input_ids.len();
5308 'decode: while generated < max_tokens {
5309 if self
5310 .graph_failed
5311 .swap(false, std::sync::atomic::Ordering::Relaxed)
5312 {
5313 self.finish_generation(&mut mtp, &mut router, true);
5318 return Err("GPU token graph failed during decode".to_string());
5319 }
5320 if self
5321 .cancel
5322 .swap(false, std::sync::atomic::Ordering::Relaxed)
5323 {
5324 finish_reason = "cancelled".to_string();
5325 break 'decode;
5326 }
5327 if mimo_spec && next_pos > 0 {
5332 self.mimo_note_rows(&hidden, next_pos - 1);
5335 }
5336 let forced = self.spec_forced.take();
5337 let mut logits = match (forced, self.graph_logits.take()) {
5338 (Some(_), _) => Vec::new(),
5339 (None, Some(lg)) => lg,
5340 (None, None) => {
5341 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5342 inference::rms_norm_into(
5343 &hidden,
5344 &self.weights.final_norm,
5345 self.rms_eps,
5346 self.norm_style,
5347 &mut self.ws.n1,
5348 );
5349 self.lm_head_forward(&self.ws.n1)
5350 }
5351 };
5352 if generated
5355 == std::env::var("CMF_LOGIT_DUMP_STEP")
5356 .ok()
5357 .and_then(|v| v.parse().ok())
5358 .unwrap_or(0)
5359 {
5360 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5361 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5362 for v in hidden.iter().chain(logits.iter()) {
5363 bytes.extend_from_slice(&v.to_le_bytes());
5364 }
5365 if let Err(e) = std::fs::write(&path, &bytes) {
5366 eprintln!("logit dump: failed to write {path}: {e}");
5367 self.finish_generation(&mut mtp, &mut router, true);
5368 return Err(format!("logit dump write failed: {e}"));
5369 }
5370 }
5371 }
5372 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5376 if !logits.is_empty() {
5377 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5378 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5379 if let Err(e) =
5380 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5381 {
5382 eprintln!("logit dump: failed to write {}: {e}", path.display());
5383 }
5384 }
5385 }
5386 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5387 Some(c) => c,
5388 None => {
5389 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5390 sampler::sample_with_scratch_pool(
5391 &logits,
5392 &self.sampler_config,
5393 self.sampler_config.penalty_past(&all_ids, bounded_native),
5394 &mut self.rng,
5395 &mut self.sampler_scratch,
5396 self.pool.as_deref(),
5397 )
5398 }
5399 };
5400 if self.confidence_on {
5401 confidence.push(if logits.is_empty() {
5402 0.0
5403 } else {
5404 sampler::top1_prob_pool(
5405 self.pool.as_deref(),
5406 &mut self.sampler_scratch,
5407 &logits,
5408 t_next,
5409 calib_temp,
5410 )
5411 });
5412 }
5413 if !logits.is_empty() {
5414 attention::recycle_buf(&mut logits);
5415 }
5416 if trace_on {
5417 let skill = router.as_ref().and_then(|r| r.active_id());
5421 traces.push(TokenTrace {
5422 t: generated,
5423 token_id: t_next,
5424 confidence: confidence.last().copied().unwrap_or(0.0),
5425 active_skill: skill,
5426 recon: None,
5427 switched: false,
5428 });
5429 }
5430 if !commit!(t_next) {
5431 break 'decode;
5432 }
5433 if generated >= max_tokens {
5434 break 'decode;
5435 }
5436
5437 self.swa_trim_tails();
5440 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5441 static SAID: std::sync::Once = std::sync::Once::new();
5447 SAID.call_once(|| {
5448 tracing::warn!(
5449 "KV cache full at {} positions — evicting half; quality \
5450 will degrade. Raise CMF_MAX_SEQ.",
5451 self.kv_cache.max_seq_len,
5452 );
5453 });
5454 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5455 self.kv_cache.evict(keep);
5456 }
5457
5458 if graph_spec {
5461 match spec_trial {
5462 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5463 spec_mon.plain_ms =
5464 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5465 let keep = spec_mon.pays();
5466 tracing::info!(
5467 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5468 spec_mon.tokens,
5469 spec_mon.round_ms,
5470 spec_mon.plain_ms,
5471 if keep { "speculating" } else { "plain" }
5472 );
5473 spec_mon.fails = 0;
5474 spec_trial = SpecTrial::Decided {
5475 spec: keep,
5476 recheck_at: if keep { usize::MAX } else { generated + 128 },
5477 };
5478 }
5479 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5480 spec_mon.n = 0;
5481 spec_trial = SpecTrial::Spec {
5482 t0: std::time::Instant::now(),
5483 gen0: generated,
5484 rounds: 0,
5485 };
5486 }
5487 _ => {}
5488 }
5489 spec_watchdog_off = matches!(
5490 spec_trial,
5491 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5492 );
5493 }
5494 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5496 let budget = max_tokens - generated - 1;
5497 if let Some(mut st) = self.mimo_mtp.take() {
5498 let k = st.depth.min(budget);
5499 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5500 self.mimo_mtp = Some(st);
5501 let r = match r {
5502 Ok(r) => r,
5503 Err(err) => {
5504 self.finish_generation(&mut mtp, &mut router, true);
5505 return Err(err);
5506 }
5507 };
5508 if let Some(r) = r {
5509 drafted += r.drafted;
5510 accepted += r.accepted.len();
5511 let mut stopped = false;
5512 for &id in &r.accepted {
5513 if self.confidence_on {
5514 confidence.push(0.0);
5515 }
5516 if !commit!(id) {
5517 stopped = true;
5518 break;
5519 }
5520 }
5521 if stopped {
5522 break 'decode;
5523 }
5524 next_pos += r.accepted.len() + 1;
5525 hidden = r.hidden;
5526 self.graph_logits = Some(r.logits);
5529 continue 'decode;
5530 }
5531 }
5532 }
5533 #[cfg(feature = "gpu")]
5536 if self.speculative
5537 && self.qwen4_exp.is_some()
5538 && task_mask.is_none()
5539 && self.sampler_config.temperature < 1e-6
5540 && generated + 1 < max_tokens
5541 && next_pos > 0
5542 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5543 {
5544 let r = match &mut self.qwen4_exp {
5545 Some(b) => crate::qwen4_exp::spec_round(
5546 &b.0,
5547 &b.1,
5548 &b.2,
5549 &mut b.3,
5550 next_pos,
5551 &all_ids,
5552 &self.inv_freq,
5553 self.pool.as_deref(),
5554 ),
5555 None => None,
5556 };
5557 if let Some(r) = r {
5558 drafted += r.drafted;
5559 accepted += r.accepted.len();
5560 let mut stopped = false;
5561 for &id in &r.accepted {
5562 if self.confidence_on {
5563 confidence.push(0.0);
5564 }
5565 if !commit!(id) {
5566 stopped = true;
5567 break;
5568 }
5569 }
5570 if stopped {
5571 break 'decode;
5572 }
5573 next_pos += r.accepted.len() + 1;
5574 hidden.fill(0.0);
5575 self.graph_logits = Some(r.logits);
5576 continue 'decode;
5577 }
5578 }
5579 match &mut mtp {
5580 #[cfg(feature = "gpu")]
5582 Some(m)
5583 if graph_spec
5584 && !spec_watchdog_off
5585 && generated + 1 < max_tokens
5586 && next_pos > 0 =>
5587 {
5588 let t_round = std::time::Instant::now();
5589 if spec_time_level() >= 2 {
5590 if let Some(t) = spec_round_end.take() {
5591 eprintln!(
5592 "spec-gap {:.2} ms (host between rounds)",
5593 t.elapsed().as_secs_f64() * 1e3
5594 );
5595 }
5596 }
5597 spec_stamps_begin();
5598 #[cfg(target_os = "macos")]
5603 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
5604 .load(std::sync::atomic::Ordering::Relaxed);
5605 #[cfg(not(target_os = "macos"))]
5606 let allocs0 = 0u64;
5607 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5608 m,
5609 &hidden,
5610 t_next,
5611 next_pos,
5612 &mut drafted,
5613 &mut accepted,
5614 &mut all_ids,
5615 max_tokens - generated,
5616 ) {
5617 next_pos = n_pos;
5618 hidden = new_h;
5619 let level = spec_time_level();
5620 if level > 0 {
5621 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5622 let stamps = spec_stamps_take();
5623 let median = if spec_walls.len() >= 3 {
5626 let mut s = spec_walls.clone();
5627 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5628 Some(s[s.len() / 2])
5629 } else {
5630 None
5631 };
5632 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5633 #[cfg(target_os = "macos")]
5634 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5635 .load(std::sync::atomic::Ordering::Relaxed)
5636 - allocs0;
5637 #[cfg(not(target_os = "macos"))]
5638 let allocs = allocs0;
5639 eprintln!(
5640 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5641 extra.len() + 1,
5642 if allocs > 0 {
5643 format!(" [{allocs} new device buffers]")
5644 } else {
5645 String::new()
5646 },
5647 match (outlier, median) {
5648 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5649 _ => String::new(),
5650 }
5651 );
5652 if level >= 2 || outlier {
5653 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5654 eprintln!(
5655 "spec-stamps: {}| untracked {:.1}",
5656 spec_stamps_format(&stamps),
5657 wall - sum
5658 );
5659 }
5660 if spec_mon.n >= 1 {
5661 spec_walls.push(wall);
5662 }
5663 }
5664 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5668 spec_trial = Self::spec_trial_round(
5671 spec_trial,
5672 &mut spec_mon,
5673 generated + extra.len() + 1,
5674 );
5675 let mut stopped = false;
5676 for &id in &extra {
5677 if self.confidence_on {
5678 confidence.push(0.0);
5679 }
5680 if !commit!(id) {
5681 stopped = true;
5682 break;
5683 }
5684 }
5685 if stopped {
5686 break 'decode;
5687 }
5688 if spec_time_level() >= 2 {
5689 spec_round_end = Some(std::time::Instant::now());
5690 }
5691 continue 'decode;
5692 }
5693 if self
5694 .graph_failed
5695 .swap(false, std::sync::atomic::Ordering::Relaxed)
5696 {
5697 self.finish_generation(&mut mtp, &mut router, true);
5703 return Err("GPU MTP graph failed during speculative decode".to_string());
5704 }
5705 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
5716 spec_mon.tokens = 0.0;
5717 spec_mon.fails = 3;
5718 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
5719 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
5720 next_pos += 1;
5721 continue 'decode;
5722 }
5723 Some(m) if !graph_spec && generated + 1 < max_tokens => {
5725 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
5726 drafted += 1;
5727 let emb1 = self.embed_single(t_next);
5728 let emb2 = self.embed_single(draft);
5729 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
5730
5731 inference::rms_norm_into(
5732 &h1,
5733 &self.weights.final_norm,
5734 self.rms_eps,
5735 self.norm_style,
5736 &mut self.ws.n1,
5737 );
5738 let mut logits1 = self.lm_head_forward(&self.ws.n1);
5739 let t_after = sampler::sample_with_scratch_pool(
5740 &logits1,
5741 &self.sampler_config,
5742 self.sampler_config.penalty_past(&all_ids, bounded_native),
5743 &mut self.rng,
5744 &mut self.sampler_scratch,
5745 self.pool.as_deref(),
5746 );
5747 if self.confidence_on {
5748 confidence.push(sampler::top1_prob_pool(
5749 self.pool.as_deref(),
5750 &mut self.sampler_scratch,
5751 &logits1,
5752 t_after,
5753 calib_temp,
5754 ));
5755 }
5756 attention::recycle_buf(&mut logits1);
5757 if trace_on {
5758 traces.push(TokenTrace {
5761 t: generated,
5762 token_id: t_after,
5763 confidence: confidence.last().copied().unwrap_or(0.0),
5764 active_skill: None,
5765 recon: None,
5766 switched: false,
5767 });
5768 }
5769 let stop = !commit!(t_after);
5770
5771 if t_after == draft {
5772 accepted += 1;
5773 self.commit_linear_scratch();
5774 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5775 hidden = h2;
5776 next_pos += 2;
5777 } else {
5778 for layer in &mut self.kv_cache.layers {
5780 layer.truncate_last(1);
5781 }
5782 if !stop {
5783 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5784 hidden = self.forward_layers(
5785 &self.embed_single(t_after),
5786 next_pos + 1,
5787 None,
5788 );
5789 }
5790 next_pos += 2;
5791 }
5792 if stop {
5793 break 'decode;
5794 }
5795 }
5796 _ => {
5798 #[cfg(feature = "gpu")]
5803 if Self::dsv4_spec_on() && self.dsv4.is_some() {
5804 static SAID: std::sync::Once = std::sync::Once::new();
5805 SAID.call_once(|| {
5806 eprintln!(
5807 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
5808 !self.dsv4_mtp.is_empty(),
5809 task_mask.is_none(),
5810 router.is_none(),
5811 !trace_on,
5812 self.sampler_config.temperature < 1e-6,
5813 self.sampler_config.repetition_penalty == 1.0,
5814 );
5815 });
5816 }
5817 #[cfg(feature = "gpu")]
5818 if Self::dsv4_spec_on()
5819 && self.dsv4.is_some()
5820 && !self.dsv4_mtp.is_empty()
5821 && task_mask.is_none()
5822 && router.is_none()
5823 && !trace_on
5824 && self.sampler_config.temperature < 1e-6
5825 && self.sampler_config.repetition_penalty == 1.0
5826 && generated + 1 < max_tokens
5827 && all_ids.len() >= 2
5828 && generated >= dsv4_spec_retry_at
5829 {
5830 let tip_token = all_ids[all_ids.len() - 2];
5831 let drafted0 = drafted;
5832 let round = self.dsv4_spec_step(
5833 tip_token,
5834 t_next,
5835 next_pos,
5836 max_tokens.saturating_sub(generated),
5837 &mut drafted,
5838 &mut accepted,
5839 );
5840 if drafted > drafted0 {
5841 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
5842 if useful {
5843 dsv4_spec_bad = 0;
5844 } else {
5845 dsv4_spec_bad += 1;
5846 if dsv4_spec_bad >= 2 {
5847 dsv4_spec_bad = 0;
5848 dsv4_spec_retry_at = generated.saturating_add(32);
5849 tracing::info!(
5850 "dsv4: draft не окупился дважды — точный walk на 32 токена"
5851 );
5852 }
5853 }
5854 }
5855 if let Some((extra, n_pos)) = round {
5856 next_pos = n_pos;
5857 let mut stopped = false;
5858 for &id in &extra {
5859 if self.confidence_on {
5860 confidence.push(0.0);
5861 }
5862 if !commit!(id) {
5863 stopped = true;
5864 break;
5865 }
5866 }
5867 if stopped {
5868 break 'decode;
5869 }
5870 continue 'decode;
5871 }
5872 }
5873 self.graph_want_logits = fuse_lm;
5874 let mut t_fwd = t_next;
5880 let pure_greedy = self.sampler_config.temperature < 1e-6
5881 && self.sampler_config.repetition_penalty == 1.0
5882 && self.sampler_config.suppress_tokens.is_empty();
5883 let burst_k = std::env::var("CMF_MULTISTEP")
5888 .ok()
5889 .and_then(|v| v.parse::<usize>().ok())
5890 .unwrap_or(0);
5891 if pure_greedy
5892 && burst_k >= 1
5893 && fuse_lm
5894 && task_mask.is_none()
5895 && router.is_none()
5896 && !trace_on
5897 && !self.confidence_on
5898 {
5899 let mut stopped = false;
5900 loop {
5901 let room = max_tokens.saturating_sub(generated);
5902 if room <= 2 {
5903 break;
5904 }
5905 let k = burst_k.min(room - 1);
5906 if k < 1 {
5907 break;
5908 }
5909 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
5910 if self
5911 .graph_failed
5912 .swap(false, std::sync::atomic::Ordering::Relaxed)
5913 {
5914 self.finish_generation(&mut mtp, &mut router, true);
5915 return Err(
5916 "GPU token graph failed during greedy burst".to_string()
5917 );
5918 }
5919 break;
5920 };
5921 next_pos += k;
5922 for &id in &ids {
5923 if !commit!(id) {
5924 stopped = true;
5925 break;
5926 }
5927 }
5928 if stopped {
5929 break;
5930 }
5931 t_fwd = *ids.last().unwrap();
5932 }
5933 if stopped {
5934 break 'decode;
5935 }
5936 }
5937 #[cfg(target_os = "macos")]
5947 if graph_spec
5948 && spec_watchdog_off
5949 && next_pos > 0
5950 && self.mtp_graph_mode == Some(true)
5951 && crate::gpu::q1_force()
5952 {
5953 if let Some(m) = mtp.as_mut() {
5954 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
5955 }
5956 }
5957 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
5958 next_pos += 1;
5959 if let Some(r) = &mut router {
5962 let phi = self.dyn_phi_ema.clone();
5963 let decision = r.step(&phi, generated);
5964 if let Some(new_active) = decision {
5965 let _ = self.set_active_skill(new_active);
5966 }
5967 if trace_on {
5970 if let Some(last) = traces.last_mut() {
5971 let e = r.last_best_e();
5972 last.recon = e.is_finite().then_some(e);
5973 last.switched = decision.is_some();
5974 }
5975 }
5976 }
5977 }
5978 }
5979 }
5980
5981 let cancelled = finish_reason == "cancelled";
5982 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
5986 if mimo_spec {
5987 if let Some(st) = self.mimo_mtp.as_ref() {
5988 let line = st.stats.line();
5989 tracing::info!("{line}");
5990 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
5991 eprintln!("{line}");
5992 }
5993 }
5994 }
5995 self.finish_generation(&mut mtp, &mut router, cancelled);
5996
5997 let output_ids = &all_ids[input_ids.len()..];
5998 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
6002 let consumed = std::mem::take(&mut all_ids);
6007 if dyn_switched {
6008 self.clear_sequence_state();
6009 } else if cancelled || mimo_spec || prompt_rows.is_some() {
6010 self.clear_history();
6011 } else {
6012 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
6013 }
6014 all_ids = consumed;
6015 let output_ids = &all_ids[input_ids.len()..];
6016 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
6018 Ok(GenerateResult {
6019 text: self.tokenizer.decode(output_ids),
6020 token_ids: output_ids.to_vec(),
6021 prompt_tokens: input_ids.len(),
6022 tokens_generated: generated,
6023 finish_reason,
6024 mtp_drafted: drafted,
6025 mtp_accepted: accepted,
6026 token_confidence: confidence,
6027 traces,
6028 })
6029 }
6030
6031 fn mtp_step(
6035 &mut self,
6036 m: &mut MtpModule,
6037 hidden: &[f32],
6038 next_token: u32,
6039 position: usize,
6040 ) -> u32 {
6041 self.mtp_step_h(m, hidden, next_token, position).0
6042 }
6043
6044 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6048 use std::sync::Mutex;
6049 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6050 let mut t = T.lock().unwrap();
6051 if t.len() <= depth {
6052 t.resize(depth + 1, (0, 0));
6053 }
6054 t[depth].0 += 1;
6055 t[depth].1 += prefix_ok as u64;
6056 if depth == 0 && t[0].0 % 128 == 0 {
6057 let line: Vec<String> = t
6058 .iter()
6059 .enumerate()
6060 .map(|(d, (n, k))| {
6061 format!(
6062 "d{}={:.0}%({n})",
6063 d + 1,
6064 100.0 * *k as f64 / (*n).max(1) as f64
6065 )
6066 })
6067 .collect();
6068 eprintln!("mtp-chain: {}", line.join(" "));
6069 }
6070 }
6071
6072 fn mtp_step_hl(
6080 &mut self,
6081 m: &mut MtpModule,
6082 hidden: &[f32],
6083 next_token: u32,
6084 position: usize,
6085 ) -> (Vec<f32>, Vec<f32>) {
6086 #[cfg(target_os = "macos")]
6091 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6092 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6093 self.mtp_graph_mode = Some(true);
6094 return r;
6095 }
6096 if self.mtp_graph_mode == Some(true) {
6097 tracing::error!("mtp Metal graph failed after admission");
6098 self.clear_sequence_state();
6099 self.graph_failed
6100 .store(true, std::sync::atomic::Ordering::Relaxed);
6101 self.cancel
6102 .store(true, std::sync::atomic::Ordering::Relaxed);
6103 return (Vec::new(), Vec::new());
6104 }
6105 self.mtp_graph_mode = Some(false);
6106 }
6107 #[cfg(feature = "gpu")]
6108 if self.mtp_graph_mode != Some(false) {
6109 if !self.mtp_graph_ok(m) {
6110 if self.mtp_graph_mode == Some(true) {
6111 tracing::error!("mtp graph became unavailable after admission");
6116 self.clear_sequence_state();
6117 self.graph_failed
6118 .store(true, std::sync::atomic::Ordering::Relaxed);
6119 self.cancel
6120 .store(true, std::sync::atomic::Ordering::Relaxed);
6121 return (Vec::new(), Vec::new());
6122 }
6123 self.mtp_graph_mode = Some(false);
6124 } else {
6125 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6126 self.mtp_graph_mode = Some(true);
6127 return r;
6128 }
6129 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6130 return (Vec::new(), Vec::new());
6137 }
6138 tracing::error!("mtp graph failed or declined after admission");
6142 self.clear_sequence_state();
6143 self.graph_failed
6144 .store(true, std::sync::atomic::Ordering::Relaxed);
6145 self.cancel
6146 .store(true, std::sync::atomic::Ordering::Relaxed);
6147 return (Vec::new(), Vec::new());
6148 }
6149 }
6150 let e = self.embed_single(next_token);
6154 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6155 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6156 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6157 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6158 let mut x = vec![0.0f32; self.hidden_size];
6159 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6160
6161 let lw = &m.layer;
6163 inference::rms_norm_into(
6164 &x,
6165 &lw.input_norm,
6166 self.rms_eps,
6167 self.norm_style,
6168 &mut self.ws.n1,
6169 );
6170 let attn = match &lw.attn {
6171 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6173 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6174 AttnKind::Full {
6175 wq,
6176 wk,
6177 wv,
6178 wo,
6179 q_norm,
6180 k_norm,
6181 output_gate,
6182 softplus_gate,
6183 bias,
6184 } => {
6185 let mut cfg = self.attn_cfg(position);
6186 cfg.q_norm = q_norm.as_deref();
6187 cfg.k_norm = k_norm.as_deref();
6188 cfg.output_gate = *output_gate;
6189 cfg.softplus_gate = softplus_gate
6190 .as_ref()
6191 .map(|(gate, per_head)| (gate, *per_head));
6192 cfg.bias = bias
6193 .as_ref()
6194 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6195 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6196 }
6197 AttnKind::Linear(_)
6198 | AttnKind::LinearGdn(_)
6199 | AttnKind::ShortConv(_)
6200 | AttnKind::Bounded(_) => {
6201 unreachable!("MTP block is full attention")
6202 }
6203 };
6204 for (i, &a) in attn.iter().enumerate() {
6205 x[i] += a;
6206 }
6207 inference::rms_norm_into(
6208 &x,
6209 &lw.post_norm,
6210 self.rms_eps,
6211 self.norm_style,
6212 &mut self.ws.p1,
6213 );
6214 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6215 for (i, &f) in ffn.iter().enumerate() {
6216 x[i] += f;
6217 }
6218
6219 inference::rms_norm_into(
6220 &x,
6221 &m.final_norm,
6222 self.rms_eps,
6223 self.norm_style,
6224 &mut self.ws.n1,
6225 );
6226 let lg = self.lm_head_forward(&self.ws.n1);
6227 (lg, x)
6228 }
6229
6230 fn mtp_step_h(
6232 &mut self,
6233 m: &mut MtpModule,
6234 hidden: &[f32],
6235 next_token: u32,
6236 position: usize,
6237 ) -> (u32, Vec<f32>) {
6238 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6239 let draft = sampler::argmax(&lg);
6240 attention::recycle_buf(&mut lg);
6241 (draft, x)
6242 }
6243
6244 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6250 match trial {
6251 SpecTrial::Spec { t0, gen0, rounds } => {
6252 let rounds = rounds + 1;
6253 if rounds >= 5 {
6254 if mon.plain_ms > 0.0 {
6255 let keep = mon.pays();
6256 mon.fails = 0;
6257 tracing::info!(
6258 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6259 mon.tokens,
6260 mon.round_ms,
6261 mon.plain_ms,
6262 if keep { "speculating" } else { "plain" }
6263 );
6264 SpecTrial::Decided {
6265 spec: keep,
6266 recheck_at: if keep { usize::MAX } else { generated + 128 },
6267 }
6268 } else if mon.pays() {
6269 mon.fails = 0;
6274 tracing::info!(
6275 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6276 mon.tokens,
6277 mon.round_ms,
6278 );
6279 SpecTrial::Decided {
6280 spec: true,
6281 recheck_at: usize::MAX,
6282 }
6283 } else {
6284 SpecTrial::Plain {
6285 t0: std::time::Instant::now(),
6286 gen0: generated,
6287 }
6288 }
6289 } else {
6290 SpecTrial::Spec { t0, gen0, rounds }
6291 }
6292 }
6293 SpecTrial::Decided { spec: true, .. } => {
6294 if mon.pays() {
6295 mon.fails = 0;
6296 trial
6297 } else {
6298 mon.fails += 1;
6299 if mon.fails >= 4 {
6300 if mon.plain_ms <= 0.0 {
6301 tracing::info!(
6305 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6306 mon.tokens,
6307 mon.round_ms,
6308 );
6309 return SpecTrial::Plain {
6310 t0: std::time::Instant::now(),
6311 gen0: generated,
6312 };
6313 }
6314 tracing::info!(
6315 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6316 mon.tokens,
6317 mon.round_ms,
6318 mon.plain_ms
6319 );
6320 SpecTrial::Decided {
6321 spec: false,
6322 recheck_at: generated + 128,
6323 }
6324 } else {
6325 trial
6326 }
6327 }
6328 }
6329 other => other,
6330 }
6331 }
6332
6333 fn mtp_kv_id(&self) -> u64 {
6336 self.graph_kv_id | (1u64 << 40)
6337 }
6338
6339 const MTP_LAYER_BASE: usize = 0;
6344
6345 #[cfg(feature = "gpu")]
6352 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6353 self.mtp_graph_mode != Some(true)
6354 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6355 }
6356
6357 #[cfg(feature = "gpu")]
6363 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6364 let mut ok = true;
6365 let mut expected = false;
6366 for li in 0..self.num_layers {
6367 if matches!(
6368 self.weights.layers[self.phys_layer(li)].attn,
6369 AttnKind::Full { .. }
6370 ) {
6371 expected = true;
6372 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6373 }
6374 }
6375 !expected || ok
6376 }
6377
6378 fn graph_gdn_layer_count(&self) -> usize {
6382 (0..self.num_layers)
6383 .filter(|&li| {
6384 matches!(
6385 &self.weights.layers[self.phys_layer(li)].attn,
6386 AttnKind::LinearGdn(_)
6387 )
6388 })
6389 .count()
6390 }
6391
6392 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6395 let e = self.embed_single(next_token);
6396 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6397 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6398 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6399 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6400 let mut x = vec![0.0f32; self.hidden_size];
6401 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6402 x
6403 }
6404
6405 #[cfg(feature = "gpu")]
6408 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6409 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6410 return false;
6411 }
6412 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6413 || !crate::gpu::enabled_here()
6414 || self.attn_softcap > 0.0
6415 || self.attention_heads_per_layer.is_some()
6416 || self.v_head_dim.is_some()
6419 {
6420 return false;
6421 }
6422 matches!(
6423 &m.layer.attn,
6424 AttnKind::Full {
6425 softplus_gate: None,
6426 ..
6427 }
6428 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6429 }
6430
6431 #[cfg(feature = "gpu")]
6435 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6436 if !self.mtp_block_graph_ok(m) {
6437 return false;
6438 }
6439 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6440 return false;
6441 };
6442 let FfnKind::Dense(d) = &m.layer.ffn else {
6443 return false;
6444 };
6445 d.segs.is_empty()
6446 && wq.graph_weight().is_some()
6447 && wk.graph_weight().is_some()
6448 && wv.graph_weight().is_some()
6449 && wo.graph_weight().is_some()
6450 && d.gate_proj.graph_weight().is_some()
6451 && d.up_proj.graph_weight().is_some()
6452 && d.down_proj.graph_weight().is_some()
6453 && self.weights.lm_head.graph_weight().is_some()
6454 }
6455
6456 #[cfg(feature = "gpu")]
6462 fn mtp_step_graph(
6463 &mut self,
6464 m: &mut MtpModule,
6465 hidden: &[f32],
6466 next_token: u32,
6467 position: usize,
6468 ) -> Option<(Vec<f32>, Vec<f32>)> {
6469 if !self.mtp_graph_ok(m) {
6470 return None;
6471 }
6472 let lw = &m.layer;
6473 let AttnKind::Full {
6474 wq,
6475 wk,
6476 wv,
6477 wo,
6478 q_norm,
6479 k_norm,
6480 output_gate,
6481 softplus_gate,
6482 bias,
6483 } = &lw.attn
6484 else {
6485 return None;
6486 };
6487 if softplus_gate.is_some() {
6488 return None;
6489 }
6490 let FfnKind::Dense(d) = &lw.ffn else {
6491 return None;
6492 };
6493 if !d.segs.is_empty() {
6494 return None; }
6496 let mut x = self.mtp_block_input(m, hidden, next_token);
6499 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6500 let (_, i, kind, rs) = t.graph_weight()?;
6501 Some(crate::gpu::GraphW {
6502 idx: i,
6503 kind,
6504 row_scale: rs,
6505 data: &[],
6506 prism: crate::gpu::GraphPrismOp::None,
6507 affine: false,
6508 })
6509 }
6510 let (model, _, _, _) = wq.graph_weight()?;
6511 let model = model.clone();
6512 let (lm_gw, lm_rows) = {
6513 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6514 let rows = if kind == 6 {
6518 self.draft_head_rows(self.weights.lm_head.rows())
6519 } else {
6520 self.weights.lm_head.rows()
6521 };
6522 (
6523 crate::gpu::GraphW {
6524 idx: i,
6525 kind,
6526 row_scale: rs,
6527 data: &[],
6528 prism: crate::gpu::GraphPrismOp::None,
6529 affine: false,
6530 },
6531 rows,
6532 )
6533 };
6534 let layer = crate::gpu::GraphLayer {
6535 input_norm: &lw.input_norm,
6536 attn: crate::gpu::GraphAttn::Full {
6537 wq: gw(wq)?,
6538 wk: gw(wk)?,
6539 wv: gw(wv)?,
6540 wo: gw(wo)?,
6541 q_norm: q_norm.as_deref(),
6542 k_norm: k_norm.as_deref(),
6543 late_qk_norm: self.qk_norm_after_rope,
6544 bias: bias
6545 .as_ref()
6546 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6547 output_gate: *output_gate,
6548 cpu_k: m.kv.k_heads(),
6549 cpu_v: m.kv.v_heads(),
6550 cpu_base: m.kv.base(),
6551 geom: None,
6552 head_gate: None,
6553 },
6554 post_norm: &lw.post_norm,
6555 attn_out_norm: None,
6556 ffn_out_norm: None,
6557 layer_scale: None,
6558 ffn: crate::gpu::GraphFfn::Dense {
6559 gate: gw(&d.gate_proj)?,
6560 up: gw(&d.up_proj)?,
6561 down: gw(&d.down_proj)?,
6562 act: d.act.graph_act()?,
6563 },
6564 };
6565 let nh = self.num_heads;
6566 let (nkv, hd, rd) = self.layer_geom(0);
6567 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6568 let mut logits = Vec::new();
6569 let ok = crate::gpu::forward_token_graph(
6570 &model,
6571 self.mtp_kv_id(),
6572 std::slice::from_ref(&layer),
6573 &[None],
6574 self.o1_epoch,
6575 &self.inv_freq,
6576 &mut x,
6577 nh,
6578 nkv,
6579 hd,
6580 self.attn_scale,
6581 rd,
6582 self.hidden_size,
6583 self.intermediate_size,
6584 position,
6585 self.kv_cache.max_seq_len,
6586 gemma,
6587 self.rms_eps as f32,
6588 Some((&lm_gw, lm_rows)),
6589 &m.final_norm,
6590 &mut logits,
6591 &[],
6592 1,
6593 None,
6594 None,
6595 None,
6596 Self::MTP_LAYER_BASE,
6597 true,
6598 );
6599 match ok {
6600 crate::gpu::TokenGraphOutcome::Completed => {}
6601 crate::gpu::TokenGraphOutcome::Declined => return None,
6602 crate::gpu::TokenGraphOutcome::Failed => {
6603 self.clear_sequence_state();
6607 self.graph_failed
6608 .store(true, std::sync::atomic::Ordering::Relaxed);
6609 self.cancel
6610 .store(true, std::sync::atomic::Ordering::Relaxed);
6611 return None;
6612 }
6613 }
6614 logits.resize(self.vocab_size, 0.0);
6615 Some((logits, x))
6616 }
6617
6618 #[cfg(feature = "gpu")]
6626 fn mtp_warm_graph(
6627 &mut self,
6628 m: &mut MtpModule,
6629 pairs: &[(&[f32], u32)],
6630 first_pos: usize,
6631 ) -> crate::gpu::BatchGraphOutcome {
6632 if pairs.is_empty() {
6633 return crate::gpu::BatchGraphOutcome::Completed;
6634 }
6635 if !self.mtp_block_graph_ok(m) {
6636 return crate::gpu::BatchGraphOutcome::Declined;
6637 }
6638 let hs = self.hidden_size;
6639 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6642 for (h, t) in pairs {
6643 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6644 }
6645 let lw = &m.layer;
6646 let AttnKind::Full {
6647 wq,
6648 wk,
6649 wv,
6650 wo,
6651 q_norm,
6652 k_norm,
6653 output_gate,
6654 bias,
6655 ..
6656 } = &lw.attn
6657 else {
6658 return crate::gpu::BatchGraphOutcome::Declined;
6659 };
6660 let FfnKind::Dense(d) = &lw.ffn else {
6661 return crate::gpu::BatchGraphOutcome::Declined;
6662 };
6663 if !d.segs.is_empty() {
6664 return crate::gpu::BatchGraphOutcome::Declined; }
6666 let (AttnKind::Full { softplus_gate: None, .. }, Some(gact)) =
6669 (&lw.attn, d.act.graph_act())
6670 else {
6671 return crate::gpu::BatchGraphOutcome::Declined;
6672 };
6673 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6674 let (_, i, kind, rs) = t.graph_weight()?;
6675 Some(crate::gpu::GraphW {
6676 idx: i,
6677 kind,
6678 row_scale: rs,
6679 data: &[],
6680 prism: crate::gpu::GraphPrismOp::None,
6681 affine: false,
6682 })
6683 }
6684 let Some((model, _, _, _)) = wq.graph_weight() else {
6685 return crate::gpu::BatchGraphOutcome::Declined;
6686 };
6687 let model = model.clone();
6688 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6689 gw(wq),
6690 gw(wk),
6691 gw(wv),
6692 gw(wo),
6693 gw(&d.gate_proj),
6694 gw(&d.up_proj),
6695 gw(&d.down_proj),
6696 ) else {
6697 return crate::gpu::BatchGraphOutcome::Declined;
6698 };
6699 let layer = crate::gpu::GraphLayer {
6700 input_norm: &lw.input_norm,
6701 attn: crate::gpu::GraphAttn::Full {
6702 wq: gwq,
6703 wk: gwk,
6704 wv: gwv,
6705 wo: gwo,
6706 q_norm: q_norm.as_deref(),
6707 k_norm: k_norm.as_deref(),
6708 late_qk_norm: self.qk_norm_after_rope,
6709 bias: bias
6710 .as_ref()
6711 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6712 output_gate: *output_gate,
6713 cpu_k: m.kv.k_heads(),
6714 cpu_v: m.kv.v_heads(),
6715 cpu_base: m.kv.base(),
6716 geom: None,
6717 head_gate: None,
6718 },
6719 post_norm: &lw.post_norm,
6720 attn_out_norm: None,
6721 ffn_out_norm: None,
6722 layer_scale: None,
6723 ffn: crate::gpu::GraphFfn::Dense {
6724 gate: gg,
6725 up: gu,
6726 down: gd,
6727 act: gact,
6728 },
6729 };
6730 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
6731 let nh = self.num_heads;
6732 let (nkv, hd, rd) = self.layer_geom(0);
6733 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6734 crate::gpu::forward_batch_graph(
6735 &model,
6736 self.mtp_kv_id(),
6737 std::slice::from_ref(&layer),
6738 &self.inv_freq,
6739 &mut hiddens,
6740 nh,
6741 nkv,
6742 hd,
6743 rd,
6744 hs,
6745 self.intermediate_size,
6746 &positions,
6747 self.kv_cache.max_seq_len,
6748 gemma,
6749 self.rms_eps as f32,
6750 self.attn_scale,
6751 pairs.len(),
6752 &[],
6753 0,
6754 None,
6755 None,
6756 )
6757 }
6758
6759 #[cfg(feature = "gpu")]
6766 fn mtp_warm_graph_fallback(
6767 &mut self,
6768 m: &mut MtpModule,
6769 pairs: &[(&[f32], u32)],
6770 first_pos: usize,
6771 ) -> bool {
6772 if pairs.is_empty() {
6773 return true;
6774 }
6775 let graphable = self.mtp_block_graph_ok(m);
6776 if !graphable {
6777 if self.mtp_graph_mode == Some(true) {
6781 return false;
6782 }
6783 self.mtp_graph_mode = Some(false);
6784 for (j, (h, t)) in pairs.iter().enumerate() {
6785 self.mtp_warm(m, h, *t, first_pos + j);
6786 }
6787 return true;
6788 }
6789
6790 for (j, (h, t)) in pairs.iter().enumerate() {
6795 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
6796 return false;
6797 }
6798 }
6799 self.mtp_graph_mode = Some(true);
6800 true
6801 }
6802
6803 #[cfg(feature = "gpu")]
6808 fn mtp_warm_prefill_pairs(
6809 &mut self,
6810 m: &mut MtpModule,
6811 pairs: &[(&[f32], u32)],
6812 first_pos: usize,
6813 ) -> Result<(), &'static str> {
6814 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
6819 if self.mtp_graph_mode == Some(true) {
6820 return Err("MTP token graph became unavailable after admission");
6821 }
6822 self.mtp_graph_mode = Some(false);
6823 for (j, (h, t)) in pairs.iter().enumerate() {
6824 self.mtp_warm(m, h, *t, first_pos + j);
6825 }
6826 return Ok(());
6827 }
6828 match self.mtp_warm_graph(m, pairs, first_pos) {
6829 crate::gpu::BatchGraphOutcome::Completed => {
6830 if !pairs.is_empty() {
6831 self.mtp_graph_mode = Some(true);
6832 }
6833 Ok(())
6834 }
6835 crate::gpu::BatchGraphOutcome::Declined => {
6836 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
6837 Ok(())
6838 } else {
6839 Err("MTP warm-up fallback failed after device admission")
6840 }
6841 }
6842 crate::gpu::BatchGraphOutcome::Failed => {
6843 Err("MTP warm batch graph failed after admission")
6844 }
6845 }
6846 }
6847
6848 #[cfg(not(feature = "gpu"))]
6849 fn mtp_warm_prefill_pairs(
6850 &mut self,
6851 m: &mut MtpModule,
6852 pairs: &[(&[f32], u32)],
6853 first_pos: usize,
6854 ) -> Result<(), &'static str> {
6855 for (j, (h, t)) in pairs.iter().enumerate() {
6856 self.mtp_warm(m, h, *t, first_pos + j);
6857 }
6858 Ok(())
6859 }
6860
6861 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
6865 let e = self.embed_single(next_token);
6866 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6867 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6868 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6869 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6870 let mut x = vec![0.0f32; self.hidden_size];
6871 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6872 inference::rms_norm_into(
6873 &x,
6874 &m.layer.input_norm,
6875 self.rms_eps,
6876 self.norm_style,
6877 &mut self.ws.n1,
6878 );
6879 let attn = match &m.layer.attn {
6880 AttnKind::Full {
6881 wq,
6882 wk,
6883 wv,
6884 wo,
6885 q_norm,
6886 k_norm,
6887 output_gate,
6888 softplus_gate,
6889 bias,
6890 } => {
6891 let mut cfg = self.attn_cfg(position);
6892 cfg.q_norm = q_norm.as_deref();
6893 cfg.k_norm = k_norm.as_deref();
6894 cfg.output_gate = *output_gate;
6895 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
6896 cfg.bias = bias
6897 .as_ref()
6898 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6899 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6900 }
6901 _ => return,
6902 };
6903 let _ = attn;
6904 }
6905
6906 #[cfg(feature = "gpu")]
6913 #[allow(clippy::too_many_arguments)]
6914 fn graph_spec_step(
6915 &mut self,
6916 m: &mut MtpModule,
6917 hidden: &[f32],
6918 t_next: u32,
6919 next_pos: usize,
6920 drafted: &mut usize,
6921 accepted: &mut usize,
6922 all_ids: &mut Vec<u32>,
6926 room: usize,
6931 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
6932 #[cfg(target_os = "macos")]
6943 let metal_native = crate::gpu::q1_force();
6944 #[cfg(not(target_os = "macos"))]
6945 let metal_native = false;
6946 #[cfg(feature = "gpu")]
6947 let k_default = if metal_native {
6948 7
6951 } else if crate::gpu_wgpu::verify_i8_on() {
6952 5
6953 } else {
6954 4
6955 };
6956 #[cfg(not(feature = "gpu"))]
6957 let k_default = 4;
6958 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
6959 .ok()
6960 .and_then(|v| v.parse().ok())
6961 .filter(|&v| (1..=8).contains(&v));
6962 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
6967 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
6968 let k_spec = k_full.min(room).max(1);
6969 let k_capped = k_spec < k_full;
6972 if next_pos == 0 {
6973 return None;
6974 }
6975 let t_round = std::time::Instant::now();
6976 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
6992 let sub0 = subs();
6993 let cfg = self.sampler_config.clone();
6998 let penalized = !(cfg.repetition_penalty == 1.0
6999 && cfg.presence_penalty == 0.0
7000 && cfg.suppress_tokens.is_empty());
7001 let greedy_pen = cfg.temperature < 1e-6 && penalized;
7006 let sampling = cfg.temperature >= 1e-6;
7007 let sparse = sampling && sampler::sparse_ok(&cfg);
7013 let base_len = all_ids.len();
7014 if sampling && !sparse && self.spec_q.len() < k_spec {
7015 self.spec_q.resize_with(k_spec, Vec::new);
7016 }
7017 if sparse && self.spec_qs.len() < k_spec {
7018 self.spec_qs.resize_with(k_spec, Vec::new);
7019 }
7020 let mut drafts = Vec::with_capacity(k_spec);
7025 let mut hx = hidden.to_vec();
7026 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
7029 spec_stamp("pro");
7030 #[cfg(target_os = "macos")]
7036 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7037 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7038 Ok(ids) => {
7039 self.mtp_graph_mode = Some(true);
7040 drafts = ids;
7041 }
7042 Err(true) => {
7043 tracing::error!("mtp Metal draft chain failed after commit");
7044 self.clear_sequence_state();
7045 self.graph_failed
7046 .store(true, std::sync::atomic::Ordering::Relaxed);
7047 self.cancel
7048 .store(true, std::sync::atomic::Ordering::Relaxed);
7049 return None;
7050 }
7051 Err(false) => {}
7052 }
7053 }
7054 for j in drafts.len()..k_spec {
7055 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7056 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7057 if spec_dbg {
7058 let saved = self.mtp_graph_mode;
7059 self.mtp_graph_mode = Some(false);
7060 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7061 self.mtp_graph_mode = saved;
7062 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7063 return None;
7064 }
7065 m.kv.truncate_last(1);
7066 dbg_ref = Some(r);
7067 }
7068 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7069 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7070 return None;
7071 }
7072 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7073 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7074 let dl = lg
7075 .iter()
7076 .zip(&lg_cpu)
7077 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7078 let dh = hj
7079 .iter()
7080 .zip(&h_cpu)
7081 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7082 eprintln!(
7083 "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 {}",
7084 next_pos - 1 + j,
7085 sampler::argmax(&lg_cpu),
7086 sampler::argmax(&lg),
7087 n(&h_cpu),
7088 n(&hj),
7089 m.kv.seq_len
7090 );
7091 }
7092 let dj = if sparse {
7093 let mut q = std::mem::take(&mut self.spec_qs[j]);
7094 let ok = sampler::sparse_distribution_into(
7095 &lg,
7096 &cfg,
7097 all_ids,
7098 &mut self.sampler_scratch,
7099 self.pool.as_deref(),
7100 &mut q,
7101 );
7102 let d = if ok {
7103 sampler::draw_sparse(&q, &mut self.rng)
7104 } else {
7105 let t = sampler::argmax(&lg);
7107 q.clear();
7108 q.push((t, 1.0));
7109 t
7110 };
7111 self.spec_qs[j] = q;
7112 all_ids.push(d);
7113 d
7114 } else if sampling {
7115 let mut q = std::mem::take(&mut self.spec_q[j]);
7116 sampler::distribution_into(
7117 &lg,
7118 &cfg,
7119 all_ids,
7120 &mut self.sampler_scratch,
7121 self.pool.as_deref(),
7122 &mut q,
7123 );
7124 let d = sampler::draw(&q, &mut self.rng);
7125 self.spec_q[j] = q;
7126 all_ids.push(d); d
7128 } else if greedy_pen {
7129 let d = sampler::argmax_penalized(
7130 &lg,
7131 &cfg,
7132 all_ids,
7133 &mut self.sampler_scratch,
7134 self.pool.as_deref(),
7135 );
7136 all_ids.push(d);
7137 d
7138 } else {
7139 sampler::argmax(&lg)
7140 };
7141 attention::recycle_buf(&mut lg);
7142 drafts.push(dj);
7143 hx = hj;
7144 spec_stamp("d.pick");
7145 }
7146 all_ids.truncate(base_len);
7147 *drafted += k_spec;
7148 let t_draft = t_round.elapsed();
7149 let sub_draft = subs();
7150 let b = k_spec + 1;
7153 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7154 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7155 let e = self.embed_single(t);
7156 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7157 }
7158 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7159 spec_stamp("v.emb");
7160 let (lm_gw, lm_rows) = {
7161 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7162 (
7163 crate::gpu::GraphW {
7164 idx: i,
7165 kind,
7166 row_scale: rs,
7167 data: &[],
7168 prism: crate::gpu::GraphPrismOp::None,
7169 affine: false,
7170 },
7171 self.weights.lm_head.rows(),
7172 )
7173 };
7174 let mut logits = Vec::new();
7175 let final_norm = self.weights.final_norm.clone();
7176 #[cfg(target_os = "macos")]
7185 let greedy_dev = metal_native
7186 && !sampling
7187 && !greedy_pen
7188 && !self.confidence_on
7189 && self.final_softcap.is_none()
7190 && self.vocab_size == lm_rows
7196 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7197 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7198 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7199 #[cfg(not(target_os = "macos"))]
7200 let greedy_dev = false;
7201 let mut dev_ids: Vec<u32> = Vec::new();
7202 #[cfg(target_os = "macos")]
7203 let verify_outcome = if metal_native {
7204 let lm = self.weights.lm_head.q1_parts()?;
7205 let n_score = self.vocab_size.min(lm_rows);
7206 self.try_batch_graph_metal(
7207 &mut hiddens,
7208 &positions,
7209 b,
7210 Some((lm, &final_norm, &mut logits)),
7211 if greedy_dev {
7212 Some((n_score, &mut dev_ids))
7213 } else {
7214 None
7215 },
7216 )
7217 } else {
7218 self.try_batch_graph_wgpu(
7219 &mut hiddens,
7220 &positions,
7221 b,
7222 Some(crate::gpu::SpecTail {
7223 lm: lm_gw,
7224 lm_rows,
7225 final_norm: &final_norm,
7226 logits_out: &mut logits,
7227 }),
7228 )
7229 };
7230 #[cfg(not(target_os = "macos"))]
7231 let verify_outcome = self.try_batch_graph_wgpu(
7232 &mut hiddens,
7233 &positions,
7234 b,
7235 Some(crate::gpu::SpecTail {
7236 lm: lm_gw,
7237 lm_rows,
7238 final_norm: &final_norm,
7239 logits_out: &mut logits,
7240 }),
7241 );
7242 match verify_outcome {
7243 crate::gpu::BatchGraphOutcome::Completed => {}
7244 crate::gpu::BatchGraphOutcome::Declined => {
7245 m.kv.truncate_last(k_spec);
7249 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7250 self.clear_sequence_state();
7251 self.graph_failed
7252 .store(true, std::sync::atomic::Ordering::Relaxed);
7253 self.cancel
7254 .store(true, std::sync::atomic::Ordering::Relaxed);
7255 tracing::error!("MTP graph mirror rewind failed after verify decline");
7256 }
7257 return None;
7258 }
7259 crate::gpu::BatchGraphOutcome::Failed => {
7260 self.clear_sequence_state();
7264 self.graph_failed
7265 .store(true, std::sync::atomic::Ordering::Relaxed);
7266 self.cancel
7267 .store(true, std::sync::atomic::Ordering::Relaxed);
7268 tracing::error!("MTP verify batch graph failed after admission");
7269 return None;
7270 }
7271 }
7272 #[cfg(target_os = "macos")]
7278 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7279 let snap: Vec<Vec<f32>> = self
7280 .kv_cache
7281 .layers
7282 .iter()
7283 .map(|l| l.linear_state.clone())
7284 .collect();
7285 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7286 let toks: Vec<u32> = std::iter::once(t_next)
7287 .chain(drafts.iter().copied())
7288 .collect();
7289 let want_save = self.graph_want_logits;
7290 self.graph_want_logits = false;
7291 for (i, &t) in toks.iter().enumerate() {
7292 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7293 let _ = self.graph_logits.take();
7294 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7298 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7299 }
7300 let ref_lg = self.logits_from_hidden(&hi);
7301 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7302 let ra = sampler::argmax(&ref_lg);
7303 let va = sampler::argmax(row);
7304 let mut md = 0f32;
7305 let mut rms = 0f64;
7306 for j in 0..lm_rows.min(ref_lg.len()) {
7307 let d = (ref_lg[j] - row[j]).abs();
7308 md = md.max(d);
7309 rms += (d as f64) * (d as f64);
7310 }
7311 let mut hd = 0f32;
7312 for j in 0..self.hidden_size {
7313 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7314 }
7315 eprintln!(
7316 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7317 next_pos + i,
7318 if ra == va { "OK" } else { "MISMATCH" },
7319 (rms / lm_rows as f64).sqrt()
7320 );
7321 }
7322 self.graph_want_logits = want_save;
7323 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7326 if l.linear_state.len() == st.len() {
7327 l.linear_state.copy_from_slice(&st);
7328 } else {
7329 l.linear_state = st;
7330 }
7331 }
7332 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7333 let extra = l.seq_len.saturating_sub(n0);
7334 if extra > 0 {
7335 l.truncate_last(extra);
7336 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7337 }
7338 }
7339 }
7340 let t_verify = t_round.elapsed();
7341 let sub_verify = subs();
7342 let mut a = 0usize;
7347 let mut forced: Option<u32> = None;
7348 let ids: Vec<u32> = if sparse {
7349 let mut p = std::mem::take(&mut self.spec_ps);
7350 let mut res = std::mem::take(&mut self.spec_ress);
7351 while a < k_spec {
7352 let ok = sampler::sparse_distribution_into(
7353 &logits[a * lm_rows..(a + 1) * lm_rows],
7354 &cfg,
7355 all_ids,
7356 &mut self.sampler_scratch,
7357 self.pool.as_deref(),
7358 &mut p,
7359 );
7360 if !ok {
7361 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7362 p.clear();
7363 p.push((t, 1.0));
7364 }
7365 match sampler::spec_accept_or_correct_sparse(
7366 &p,
7367 &self.spec_qs[a],
7368 drafts[a],
7369 &mut self.rng,
7370 &mut res,
7371 ) {
7372 None => {
7373 all_ids.push(drafts[a]);
7374 a += 1;
7375 }
7376 Some(c) => {
7377 forced = Some(c);
7378 break;
7379 }
7380 }
7381 }
7382 all_ids.truncate(base_len);
7383 self.spec_ps = p;
7384 self.spec_ress = res;
7385 drafts.clone()
7386 } else if sampling {
7387 let mut p = std::mem::take(&mut self.spec_p);
7388 let mut res = std::mem::take(&mut self.spec_res);
7389 while a < k_spec {
7390 sampler::distribution_into(
7391 &logits[a * lm_rows..(a + 1) * lm_rows],
7392 &cfg,
7393 all_ids,
7394 &mut self.sampler_scratch,
7395 self.pool.as_deref(),
7396 &mut p,
7397 );
7398 match sampler::spec_accept_or_correct(
7399 &p,
7400 &self.spec_q[a],
7401 drafts[a],
7402 &mut self.rng,
7403 &mut res,
7404 self.pool.as_deref(),
7405 ) {
7406 None => {
7407 all_ids.push(drafts[a]);
7408 a += 1;
7409 }
7410 Some(c) => {
7411 forced = Some(c);
7412 break;
7413 }
7414 }
7415 }
7416 all_ids.truncate(base_len);
7417 self.spec_p = p;
7418 self.spec_res = res;
7419 drafts.clone()
7421 } else if greedy_pen {
7422 let mut ids: Vec<u32> = Vec::with_capacity(b);
7426 for i in 0..b {
7427 let t = sampler::argmax_penalized(
7428 &logits[i * lm_rows..(i + 1) * lm_rows],
7429 &cfg,
7430 all_ids,
7431 &mut self.sampler_scratch,
7432 self.pool.as_deref(),
7433 );
7434 ids.push(t);
7435 if i < k_spec && t == drafts[i] {
7436 all_ids.push(t);
7437 } else {
7438 break;
7439 }
7440 }
7441 all_ids.truncate(base_len);
7442 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7443 a += 1;
7444 }
7445 ids
7448 } else if greedy_dev && dev_ids.len() == b {
7449 let ids = std::mem::take(&mut dev_ids);
7450 while a < k_spec && ids[a] == drafts[a] {
7451 a += 1;
7452 }
7453 ids
7454 } else {
7455 if logits.len() < b * lm_rows {
7456 self.clear_sequence_state();
7459 self.graph_failed
7460 .store(true, std::sync::atomic::Ordering::Relaxed);
7461 self.cancel
7462 .store(true, std::sync::atomic::Ordering::Relaxed);
7463 tracing::error!("Metal verify returned neither logits nor argmax ids");
7464 return None;
7465 }
7466 let ids: Vec<u32> = (0..b)
7467 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7468 .collect();
7469 while a < k_spec && ids[a] == drafts[a] {
7470 a += 1;
7471 }
7472 ids
7473 };
7474 spec_stamp("acc");
7475 if spec_dbg {
7476 eprintln!(
7477 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7478 drafts, ids
7479 );
7480 }
7481 #[cfg(target_os = "macos")]
7485 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7486 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7487 {
7488 let snap: Vec<Vec<f32>> = self
7489 .kv_cache
7490 .layers
7491 .iter()
7492 .map(|l| l.linear_state.clone())
7493 .collect();
7494 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7495 let toks: Vec<u32> = std::iter::once(t_next)
7496 .chain(drafts.iter().copied())
7497 .collect();
7498 let want_save = self.graph_want_logits;
7499 self.graph_want_logits = false;
7500 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7501 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7502 let _ = self.graph_logits.take();
7503 }
7504 self.graph_want_logits = want_save;
7505 let plain_states: Vec<Vec<f32>> = self
7506 .kv_cache
7507 .layers
7508 .iter()
7509 .map(|l| l.linear_state.clone())
7510 .collect();
7511 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7512 let mut rows = Vec::new();
7513 for (li, (l, n0)) in self
7514 .kv_cache
7515 .layers
7516 .iter_mut()
7517 .zip(attn_lens.iter())
7518 .enumerate()
7519 {
7520 let extra = l.seq_len.saturating_sub(*n0);
7521 if extra > 0 {
7522 let mut kk = Vec::new();
7523 let mut vv = Vec::new();
7524 for g in 0..nkv {
7525 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7526 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7527 }
7528 rows.push((li, kk, vv));
7529 l.truncate_last(extra);
7530 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7531 }
7532 }
7533 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7534 if l.linear_state.len() == st.len() {
7535 l.linear_state.copy_from_slice(&st);
7536 } else {
7537 l.linear_state = st;
7538 }
7539 }
7540 Some((plain_states, rows))
7541 } else {
7542 None
7543 };
7544 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7545 #[cfg(target_os = "macos")]
7554 let mut warm_pending: Option<MetalWarmPending> = None;
7555 #[cfg(target_os = "macos")]
7556 if metal_native {
7557 m.kv.truncate_last(k_spec.saturating_sub(1));
7558 if self.mtp_graph_mode == Some(true) {
7559 crate::gpu_metal::kv_mirror_set_stored(
7562 self.mtp_kv_id(),
7563 Self::MTP_LAYER_BASE,
7564 m.kv.seq_len,
7565 );
7566 if !warm_off && a > 0 {
7567 let pairs: Vec<(&[f32], u32)> = (0..a)
7568 .map(|j| {
7569 (
7570 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7571 ids[j],
7572 )
7573 })
7574 .collect();
7575 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7576 }
7577 }
7578 spec_stamp("c.wsub");
7579 }
7580 #[cfg(target_os = "macos")]
7582 if metal_native {
7583 if !self.metal_verify_commit(a) {
7586 self.clear_sequence_state();
7587 self.graph_failed
7588 .store(true, std::sync::atomic::Ordering::Relaxed);
7589 self.cancel
7590 .store(true, std::sync::atomic::Ordering::Relaxed);
7591 tracing::error!("Metal verify state/KV handoff failed after admission");
7592 return None;
7593 }
7594 if let Some((plain_states, rows)) = commit_ref {
7595 crate::gpu_metal::queue_fence();
7596 let _ = crate::gpu_metal::wait_replay();
7599 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7600 let mut worst_s = 0f32;
7601 let mut worst_li = 0usize;
7602 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7603 if l.linear_state.len() != ps.len() || ps.is_empty() {
7604 continue;
7605 }
7606 let d = l
7607 .linear_state
7608 .iter()
7609 .zip(ps)
7610 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7611 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7612 let rel = d / n.max(1e-6);
7613 if rel > worst_s {
7614 worst_s = rel;
7615 worst_li = li;
7616 }
7617 }
7618 let mut worst_k = 0f32;
7619 for (li, kk, vv) in &rows {
7620 let l = &self.kv_cache.layers[*li];
7621 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7622 let mut ck = Vec::new();
7623 let mut cv = Vec::new();
7624 for g in 0..nkv {
7625 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7626 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7627 }
7628 if ck.len() == kk.len() {
7629 let dk = ck
7630 .iter()
7631 .zip(kk)
7632 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7633 let dv = cv
7634 .iter()
7635 .zip(vv)
7636 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7637 worst_k = worst_k.max(dk).max(dv);
7638 } else {
7639 eprintln!(
7640 "commit-check L{li}: kv row count mismatch {} vs {}",
7641 ck.len(),
7642 kk.len()
7643 );
7644 }
7645 }
7646 eprintln!(
7647 "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}"
7648 );
7649 }
7650 }
7651 if !metal_native && a + 1 < b {
7652 let expected_gdn_layers = self.graph_gdn_layer_count();
7653 if expected_gdn_layers > 0
7654 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7655 {
7656 self.clear_sequence_state();
7657 self.graph_failed
7658 .store(true, std::sync::atomic::Ordering::Relaxed);
7659 self.cancel
7660 .store(true, std::sync::atomic::Ordering::Relaxed);
7661 tracing::error!("GDN speculative restore failed after verify");
7662 return None;
7663 }
7664 }
7665 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7666 self.clear_sequence_state();
7671 self.graph_failed
7672 .store(true, std::sync::atomic::Ordering::Relaxed);
7673 self.cancel
7674 .store(true, std::sync::atomic::Ordering::Relaxed);
7675 tracing::error!("trunk graph KV rewind failed after speculative verify");
7676 return None;
7677 }
7678 *accepted += a;
7679 if !metal_native {
7690 m.kv.truncate_last(k_spec.saturating_sub(1));
7692 }
7693 spec_stamp("c.trunc");
7694 if !metal_native
7695 && self.mtp_graph_mode == Some(true)
7696 && !self.rewind_mtp_graph_mirror(next_pos)
7697 {
7698 self.clear_sequence_state();
7702 self.graph_failed
7703 .store(true, std::sync::atomic::Ordering::Relaxed);
7704 self.cancel
7705 .store(true, std::sync::atomic::Ordering::Relaxed);
7706 tracing::error!("MTP graph mirror rewind failed after verify commit");
7707 return None;
7708 }
7709 if !warm_off && a > 0 {
7710 let mut warmed = false;
7713 #[cfg(target_os = "macos")]
7714 if metal_native && self.mtp_graph_mode == Some(true) {
7715 warmed = match warm_pending.take() {
7719 Some(p) => self.mtp_warm_batch_finish(m, p),
7720 None => false,
7721 };
7722 if !warmed {
7723 warmed = true;
7724 for j in 0..a {
7725 let row =
7726 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
7727 if self
7728 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
7729 .is_none()
7730 {
7731 warmed = false;
7732 break;
7733 }
7734 }
7735 }
7736 }
7737 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
7738 let rows: Vec<Vec<f32>> = (0..a)
7739 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
7740 .collect();
7741 let pairs: Vec<(&[f32], u32)> = rows
7742 .iter()
7743 .zip(ids.iter())
7744 .map(|(r, &t)| (r.as_slice(), t))
7745 .collect();
7746 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
7747 Ok(()) => warmed = true,
7748 Err(err) => {
7749 tracing::error!("{err}");
7755 self.clear_sequence_state();
7756 self.graph_failed
7757 .store(true, std::sync::atomic::Ordering::Relaxed);
7758 self.cancel
7759 .store(true, std::sync::atomic::Ordering::Relaxed);
7760 return None;
7761 }
7762 }
7763 }
7764 if !warmed {
7765 for j in 0..a {
7766 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
7767 let row = row.to_vec();
7768 self.mtp_warm(m, &row, ids[j], next_pos + j);
7769 }
7770 }
7771 }
7772 spec_stamp("c.warm");
7776 if let Some(c) = forced {
7777 self.spec_forced = Some(c);
7778 self.graph_logits = None;
7779 } else if greedy_dev && logits.is_empty() {
7780 self.spec_forced = Some(ids[a]);
7783 self.graph_logits = None;
7784 } else {
7785 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
7786 row.resize(self.vocab_size, 0.0);
7787 if let Some(c) = self.final_softcap {
7788 for l in row.iter_mut() {
7789 *l = c * (*l / c).tanh();
7790 }
7791 }
7792 self.graph_logits = Some(row);
7793 }
7794 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
7795 spec_stamp("c.row");
7796 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7802 let end = subs();
7803 eprintln!(
7804 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
7805 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
7806 t_draft.as_secs_f64() * 1e3,
7807 sub_draft - sub0,
7808 (t_verify - t_draft).as_secs_f64() * 1e3,
7809 sub_verify - sub_draft,
7810 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
7811 end - sub_verify,
7812 self.draft_full_streak,
7813 );
7814 }
7815 if k_env.is_none() && !metal_native && !k_capped {
7820 let f = a as f32 / k_spec.max(1) as f32;
7824 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
7825 let mut k_next = k_spec;
7826 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
7827 k_next = k_spec + 1;
7828 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
7829 k_next = k_spec - 1;
7830 }
7831 if k_next != k_spec {
7832 self.spec_acc_ewma = 0.6;
7833 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7834 eprintln!("spec-k: {k_spec} → {k_next}");
7835 }
7836 }
7837 self.spec_k_adapt = Some(k_next);
7838 }
7839 spec_stamp("end");
7840 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
7841 }
7842
7843 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
7852 if !self.pair_supported() {
7853 return (0.0, 0.0);
7854 }
7855 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
7862 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7863 let emb1 = self.embed_single(1);
7864 let emb2 = self.embed_single(2);
7865 let pos = self.kv_cache.seq_len();
7866
7867 let t0 = std::time::Instant::now();
7868 for _ in 0..iters {
7869 let _ = self.forward_layers(&emb1, pos, None);
7870 let _ = self.forward_layers(&emb2, pos + 1, None);
7871 for l in &mut self.kv_cache.layers {
7872 l.truncate_last(2);
7873 }
7874 }
7875 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7876
7877 let t1 = std::time::Instant::now();
7878 for _ in 0..iters {
7879 let _ = self.forward_pair(&emb1, &emb2, pos);
7880 for l in &mut self.kv_cache.layers {
7881 l.truncate_last(2);
7882 }
7883 }
7884 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7885 match graph_env {
7886 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
7887 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
7888 }
7889 (singles_ms, pair_ms)
7890 }
7891
7892 fn pair_supported(&self) -> bool {
7900 !self.weights.layers.is_empty()
7907 && self.g3n.is_none()
7908 && !self
7909 .weights
7910 .layers
7911 .iter()
7912 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
7913 }
7914
7915 fn forward_pair(
7916 &mut self,
7917 emb1: &[f32],
7918 emb2: &[f32],
7919 position: usize,
7920 ) -> (Vec<f32>, Vec<f32>) {
7921 self.mimo_moe_prepare();
7924 let mut h1 = emb1.to_vec();
7925 let mut h2 = emb2.to_vec();
7926 let (_nkv, _hd, hs, _rd, eps) = (
7927 self.num_kv_heads,
7928 self.head_dim,
7929 self.hidden_size,
7930 self.rotary_dim,
7931 self.rms_eps,
7932 );
7933 let pool = self.pool.clone();
7934
7935 for li in 0..self.num_layers {
7936 let lw = &self.weights.layers[self.phys_layer(li)];
7937 inference::rms_norm_into(
7940 &h1,
7941 &lw.input_norm,
7942 self.rms_eps,
7943 self.norm_style,
7944 &mut self.ws.n1,
7945 );
7946 inference::rms_norm_into(
7947 &h2,
7948 &lw.input_norm,
7949 self.rms_eps,
7950 self.norm_style,
7951 &mut self.ws.n2,
7952 );
7953
7954 let (a1, a2) = match &lw.attn {
7955 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7956 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7957 AttnKind::Bounded(w) => {
7958 let rope = self
7961 .bounded_rope
7962 .clone()
7963 .expect("bounded layer without an installed rotation table");
7964 let cfg = crate::bounded::BoundedAttnCfg {
7965 num_heads: self.num_heads,
7966 num_kv_heads: self.num_kv_heads,
7967 head_dim: self.head_dim,
7968 hidden_size: hs,
7969 scale: self.attn_scale,
7970 rope: &rope,
7971 pool: pool.as_deref(),
7972 };
7973 let a1 = crate::bounded::bounded_attention(
7974 &self.ws.n1,
7975 w,
7976 &mut self.kv_cache.layers[li],
7977 &cfg,
7978 );
7979 let a2 = crate::bounded::bounded_attention(
7980 &self.ws.n2,
7981 w,
7982 &mut self.kv_cache.layers[li],
7983 &cfg,
7984 );
7985 (a1, a2)
7986 }
7987 AttnKind::Linear(w) => {
7988 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
7989 let layer = &mut self.kv_cache.layers[li];
7990 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7991 vmf_phase_pair(
7992 &self.ws.n1,
7993 &self.ws.n2,
7994 w,
7995 &cfg,
7996 state,
7997 scratch,
7998 self.pool.as_deref(),
7999 )
8000 }
8001 AttnKind::LinearGdn(w) => {
8002 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
8003 let layer = &mut self.kv_cache.layers[li];
8004 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8005 gdn_pair(
8006 &self.ws.n1,
8007 &self.ws.n2,
8008 w,
8009 &cfg,
8010 state,
8011 scratch,
8012 self.pool.as_deref(),
8013 )
8014 }
8015 AttnKind::ShortConv(w) => {
8016 let cfg = self
8017 .short_conv_cfg
8018 .expect("short-conv layer without short_conv_cfg");
8019 let layer = &mut self.kv_cache.layers[li];
8020 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
8021 short_conv_pair(
8022 &self.ws.n1,
8023 &self.ws.n2,
8024 w,
8025 &cfg,
8026 state,
8027 scratch,
8028 self.pool.as_deref(),
8029 )
8030 }
8031 AttnKind::Full {
8032 wq,
8033 wk,
8034 wv,
8035 wo,
8036 q_norm,
8037 k_norm,
8038 output_gate,
8039 softplus_gate,
8040 bias,
8041 } => {
8042 let inv_freq_l = self.layer_inv_freq(li);
8043 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8044 let cfg = QwenAttnCfg {
8045 num_heads: self.layer_num_heads(li),
8046 num_kv_heads: nkv_l,
8047 head_dim: hd_l,
8048 hidden_size: hs,
8049 position,
8050 inv_freq: &inv_freq_l,
8051 rotary_dim: rd_l,
8052 scale: self.attn_scale,
8053 softcap: self.attn_softcap,
8054 window: self.layer_window(li),
8055 v_norm: self.attn_v_norm,
8056 qk_norm_after_rope: self.qk_norm_after_rope,
8057 gate_sigmoid: self.proj_gate_sigmoid,
8058 q_norm: q_norm.as_deref(),
8059 k_norm: k_norm.as_deref(),
8060 output_gate: *output_gate,
8061 softplus_gate: softplus_gate
8062 .as_ref()
8063 .map(|(gate, per_head)| (gate, *per_head)),
8064 rope_scale: self.layer_rope_scale(li),
8065 bias: bias
8066 .as_ref()
8067 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8068 rms_eps: eps,
8069 norm_style: self.norm_style,
8070 pool: pool.as_deref(),
8071 v_head_dim: self.layer_v_dim(li),
8072 };
8073 attention::qwen_attention_pair(
8074 &self.ws.n1,
8075 &self.ws.n2,
8076 wq,
8077 wk,
8078 wv,
8079 wo,
8080 &mut self.kv_cache.layers[li],
8081 &cfg,
8082 )
8083 }
8084 };
8085 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8086 Some(w) => (
8087 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8088 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8089 ),
8090 None => (a1, a2),
8091 };
8092 for i in 0..self.hidden_size {
8093 h1[i] += a1[i];
8094 h2[i] += a2[i];
8095 }
8096 let (mut a1, mut a2) = (a1, a2);
8097 attention::recycle_buf(&mut a1);
8098 attention::recycle_buf(&mut a2);
8099
8100 let lw = &self.weights.layers[self.phys_layer(li)];
8101 inference::rms_norm_into(
8102 &h1,
8103 &lw.post_norm,
8104 self.rms_eps,
8105 self.norm_style,
8106 &mut self.ws.p1,
8107 );
8108 inference::rms_norm_into(
8109 &h2,
8110 &lw.post_norm,
8111 self.rms_eps,
8112 self.norm_style,
8113 &mut self.ws.p2,
8114 );
8115 let (f1, f2) = match &lw.ffn {
8116 FfnKind::DenseMoe(dm) => (
8119 dense_moe_ffn(
8120 dm,
8121 &self.ws.p1,
8122 &h1,
8123 self.rms_eps,
8124 self.norm_style,
8125 self.pool.as_deref(),
8126 ),
8127 dense_moe_ffn(
8128 dm,
8129 &self.ws.p2,
8130 &h2,
8131 self.rms_eps,
8132 self.norm_style,
8133 self.pool.as_deref(),
8134 ),
8135 ),
8136 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8137 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8138 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8139 ),
8140 _ => ffn_forward_pair(
8141 &lw.ffn,
8142 &self.ws.p1,
8143 &self.ws.p2,
8144 self.pool.as_deref(),
8145 None,
8146 ),
8147 };
8148 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8149 Some(w) => (
8150 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8151 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8152 ),
8153 None => (f1, f2),
8154 };
8155 for i in 0..self.hidden_size {
8156 h1[i] += f1[i];
8157 h2[i] += f2[i];
8158 }
8159 let (mut f1, mut f2) = (f1, f2);
8160 attention::recycle_buf(&mut f1);
8161 attention::recycle_buf(&mut f2);
8162 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8163 for i in 0..self.hidden_size {
8164 h1[i] *= sc;
8165 h2[i] *= sc;
8166 }
8167 }
8168 if self.is_loop_end(li) && li + 1 < self.num_layers {
8170 h1 = inference::rms_norm(
8171 &h1,
8172 &self.weights.final_norm,
8173 self.rms_eps,
8174 self.norm_style,
8175 );
8176 h2 = inference::rms_norm(
8177 &h2,
8178 &self.weights.final_norm,
8179 self.rms_eps,
8180 self.norm_style,
8181 );
8182 }
8183 }
8184 if self.o1_active() {
8190 self.commit_linear_scratch();
8191 }
8192 self.o1_progress();
8193 self.swa_trim_tails();
8194 (h1, h2)
8195 }
8196
8197 fn commit_linear_scratch(&mut self) {
8199 for layer in &mut self.kv_cache.layers {
8200 if !layer.linear_scratch.is_empty() {
8201 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8202 layer.linear_scratch.clear();
8203 }
8204 }
8205 }
8206
8207 pub fn forward_ids(
8210 &mut self,
8211 ids: &[u32],
8212 task_mask: Option<&TaskMask>,
8213 ) -> Result<Vec<f32>, String> {
8214 #[cfg(target_os = "macos")]
8215 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8216 if ids.is_empty() {
8217 return Err("empty id sequence".to_string());
8218 }
8219 self.clear_sequence_state();
8220 self.check_forward_graph("forward_ids setup", 0)?;
8221 if task_mask.is_none() {
8222 self.o1_begin();
8223 }
8224 let mut hidden = vec![0.0f32; self.hidden_size];
8225 let mut pos = 0usize;
8226 if let Some(b) = &mut self.dsv41 {
8227 let pool = self.pool.clone();
8228 let mut logits = Vec::new();
8229 crate::dsv41::forward_chunk(
8230 &b.0,
8231 &b.1,
8232 &b.2,
8233 &mut b.3,
8234 ids,
8235 0,
8236 pool.as_deref(),
8237 &mut logits,
8238 );
8239 if let Err(err) = self.o1_seal_checked() {
8240 self.clear_sequence_state();
8241 return Err(err);
8242 }
8243 return Ok(logits);
8244 }
8245 #[cfg(not(target_os = "macos"))]
8256 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8257 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8258 Ok(r) => r,
8259 Err(e) => {
8260 self.clear_sequence_state();
8261 return Err(e);
8262 }
8263 };
8264 if n > 0 {
8265 hidden.copy_from_slice(&last);
8266 pos = n;
8267 }
8268 }
8269 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8270 let chunk = self.prefill_chunk();
8274 let hs = self.hidden_size;
8275 while pos < ids.len() {
8276 let end = (pos + chunk).min(ids.len());
8277 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8278 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8279 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8280 pos = end;
8281 }
8282 }
8283 if task_mask.is_none()
8292 && !self.graph_prefill_preferred()
8293 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8294 && self.pair_supported()
8295 {
8296 while pos + 1 < ids.len() {
8297 let e1 = self.embed_single(ids[pos]);
8298 let e2 = self.embed_single(ids[pos + 1]);
8299 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8300 self.check_forward_graph("forward_ids pair", pos + 1)?;
8301 self.commit_linear_scratch();
8302 hidden = h2;
8303 pos += 2;
8304 }
8305 }
8306 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8309 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8310 self.graph_logits = Some(lg);
8311 hidden = vec![0.0; self.hidden_size];
8312 pos = ids.len();
8313 }
8314 }
8315 while pos < ids.len() {
8316 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8317 self.check_forward_graph("forward_ids", pos)?;
8318 pos += 1;
8319 }
8320 if let Some(logits) = self.graph_logits.take() {
8321 if let Err(err) = self.o1_seal_checked() {
8325 self.clear_sequence_state();
8326 return Err(err);
8327 }
8328 return Ok(logits);
8329 }
8330 if let Err(err) = self.o1_seal_checked() {
8334 self.clear_sequence_state();
8335 return Err(err);
8336 }
8337 let normed = inference::rms_norm(
8338 &hidden,
8339 &self.weights.final_norm,
8340 self.rms_eps,
8341 self.norm_style,
8342 );
8343 Ok(self.lm_head_forward(&normed))
8344 }
8345
8346 #[doc(hidden)]
8350 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8351 #[cfg(target_os = "macos")]
8352 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8353 if ids.is_empty() {
8354 return Err("empty id sequence".to_string());
8355 }
8356 self.clear_sequence_state();
8357 self.dsv41
8358 .as_ref()
8359 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8360 self.o1_begin();
8361 let rows = {
8362 let pool = self.pool.clone();
8363 let b = self
8364 .dsv41
8365 .as_mut()
8366 .expect("dsv41 checked above; state cannot change during forward");
8367 let mut rows = Vec::with_capacity(ids.len());
8368 for (position, &id) in ids.iter().enumerate() {
8369 let mut logits = Vec::new();
8370 crate::dsv41::forward_token(
8371 &b.0,
8372 &b.1,
8373 &b.2,
8374 &mut b.3,
8375 id,
8376 position,
8377 pool.as_deref(),
8378 &mut logits,
8379 );
8380 rows.push(logits);
8381 }
8382 rows
8383 };
8384 self.o1_seal();
8385 Ok(rows)
8386 }
8387
8388 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8395 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8396 Ok((nll / cnt.max(1) as f64).exp())
8397 }
8398
8399 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8404 self.clear_sequence_state();
8405 FFN_PROBE.with(|p| {
8406 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8407 });
8408 crate::gpu::cpu_scope(|| {
8409 for (pos, &id) in ids.iter().enumerate() {
8410 let emb = self.embed_single(id);
8411 let _ = self.forward_layers(&emb, pos, None);
8412 }
8413 });
8414 self.clear_sequence_state();
8415 FFN_PROBE
8416 .with(|p| p.borrow_mut().take())
8417 .unwrap_or_default()
8418 }
8419
8420 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8424 if let Err(err) = self.nll_begin() {
8425 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8429 self.nll_end();
8430 return Err(err);
8431 }
8432 FFN_PROBE.with(|p| {
8433 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8434 });
8435 let result: Result<(), String> = (|| {
8436 for chunk in ids.chunks(256) {
8437 if chunk.len() < 2 {
8438 continue;
8439 }
8440 self.nll_ids_masked(chunk, 0, None)?;
8441 }
8442 Ok(())
8443 })();
8444 self.nll_end();
8445 let probe = FFN_PROBE
8446 .with(|p| p.borrow_mut().take())
8447 .unwrap_or_default();
8448 match result {
8449 Ok(()) => Ok(probe),
8450 Err(err) => {
8451 drop(probe);
8452 Err(err)
8453 }
8454 }
8455 }
8456
8457 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8461 self.nll_begin()?;
8462 let result: Result<f64, String> = (|| {
8463 let mut nll = 0f64;
8464 let mut cnt = 0usize;
8465 let mut hidden = vec![0f32; self.hidden_size];
8466 for (pos, &id) in ids.iter().enumerate() {
8467 if pos > 0 {
8468 inference::rms_norm_into(
8469 &hidden,
8470 &self.weights.final_norm,
8471 self.rms_eps,
8472 self.norm_style,
8473 &mut self.ws.n1,
8474 );
8475 let mut logits = self.lm_head_forward(&self.ws.n1);
8476 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8477 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8478 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8479 nll -= p.max(1e-300).ln();
8480 cnt += 1;
8481 attention::recycle_buf(&mut logits);
8482 }
8483 let emb = self.embed_single(id);
8484 hidden = self.forward_layers(&emb, pos, Some(mask));
8485 self.nll_check_graph("masked serial forward", pos)?;
8486 let _ = self.graph_logits.take();
8490 }
8491 Ok((nll / cnt.max(1) as f64).exp())
8492 })();
8493 self.nll_end();
8494 result
8495 }
8496
8497 pub fn nll_ids_masked(
8516 &mut self,
8517 ids: &[u32],
8518 start: usize,
8519 task_mask: Option<&TaskMask>,
8520 ) -> Result<(f64, usize), String> {
8521 let task_mask = self.drop_open_mask(task_mask);
8522 self.nll_ids_inner(ids, start, task_mask)
8523 }
8524
8525 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8526 self.nll_ids_inner(ids, start, None)
8527 }
8528
8529 fn nll_ids_inner(
8530 &mut self,
8531 ids: &[u32],
8532 start: usize,
8533 task_mask: Option<&TaskMask>,
8534 ) -> Result<(f64, usize), String> {
8535 self.nll_begin()?;
8536 let result: Result<(f64, usize), String> = (|| {
8537 let mut nll = 0f64;
8538 let mut cnt = 0usize;
8539 let (graph_quality, fused_head_quality) = nll_graph_policy(
8552 task_mask.is_none(),
8553 self.graph_prefill_preferred(),
8554 crate::gpu::q1_force(),
8555 );
8556 self.graph_head_required = fused_head_quality;
8557 self.graph_want_logits = fused_head_quality;
8558 #[cfg(target_os = "macos")]
8559 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8560 match self.nll_batch_metal(ids, start) {
8561 MetalBatchNllOutcome::Completed(nll, count) => {
8562 return Ok((nll, count));
8563 }
8564 MetalBatchNllOutcome::Declined => {}
8565 MetalBatchNllOutcome::Failed(err) => return Err(err),
8566 }
8567 }
8568 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8573 if self.can_prefill_batched() && !graph_quality && !force_serial {
8574 const CHUNK: usize = 128;
8580 const LM_SUB: usize = 32;
8581 let n = ids.len().saturating_sub(1);
8582 let hs = self.hidden_size;
8583 let rows = self.weights.lm_head.rows();
8584 let mut pos = 0usize;
8585 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8586 while pos < n {
8587 let end = (pos + CHUNK).min(n);
8588 let bsz = end - pos;
8589 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8590 self.nll_check_graph("batched prefill", pos)?;
8591 if state_trace && end % 256 == 0 {
8592 self.trace_recurrent_state(end);
8593 }
8594 let mut k0 = 0usize;
8595 while k0 < bsz {
8596 let k1 = (k0 + LM_SUB).min(bsz);
8597 let sb = k1 - k0;
8598 if pos + k1 <= start {
8601 k0 = k1;
8602 continue;
8603 }
8604 let mut normed = vec![0.0f32; sb * hs];
8605 for k in 0..sb {
8606 let r = inference::rms_norm(
8607 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8608 &self.weights.final_norm,
8609 self.rms_eps,
8610 self.norm_style,
8611 );
8612 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8613 }
8614 let mut logits = vec![0.0f32; sb * rows];
8615 self.weights
8616 .lm_head
8617 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8618 for k in 0..sb {
8619 if pos + k0 + k < start {
8620 continue;
8621 }
8622 self.nll_check_graph("batched score row", pos + k0 + k)?;
8623 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8624 if let Some(mu) = self.logit_multiplier {
8625 for v in lg.iter_mut() {
8626 *v *= mu;
8627 }
8628 }
8629 if let Some(c) = self.final_softcap {
8633 softcap_in_place(lg, c, self.pool.as_deref());
8634 }
8635 if let Some(cm) = self.head_clusters.clone() {
8638 self.hierarchical_head_logprobs(
8639 &normed[k * hs..(k + 1) * hs],
8640 &cm,
8641 lg,
8642 );
8643 }
8644 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8645 let target = ids[pos + k0 + k + 1] as usize;
8646 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8647 let lse: f64 = lg
8648 .iter()
8649 .map(|&v| ((v - max) as f64).exp())
8650 .sum::<f64>()
8651 .ln()
8652 + max as f64;
8653 nll += lse - lg[target] as f64;
8654 cnt += 1;
8655 if std::env::var("CMF_PPL_TRACE").is_ok() {
8656 let top = lg
8657 .iter()
8658 .enumerate()
8659 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8660 .map(|(i, _)| i)
8661 .unwrap_or(0);
8662 eprintln!(
8663 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
8664 pos + k0 + k,
8665 target,
8666 lse - lg[target] as f64,
8667 top,
8668 lg[target],
8669 lg[top]
8670 );
8671 }
8672 }
8673 k0 = k1;
8674 }
8675 pos = end;
8676 }
8677 return Ok((nll, cnt));
8678 }
8679 for pos in 0..ids.len().saturating_sub(1) {
8680 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8681 self.nll_check_graph("serial forward", pos)?;
8682 let out_of_band = self.graph_logits.take();
8690 if self.graph_head_required && out_of_band.is_none() {
8691 METAL_GRAPH_HEAD_MISS.fetch_add(
8692 1,
8693 std::sync::atomic::Ordering::Relaxed,
8694 );
8695 return Err(format!(
8696 "fused Metal graph head did not complete at NLL position {pos}"
8697 ));
8698 }
8699 if pos < start {
8700 continue;
8701 }
8702 let logits = match out_of_band {
8703 Some(lg) => lg,
8704 None => {
8705 let normed = inference::rms_norm(
8706 &hidden,
8707 &self.weights.final_norm,
8708 self.rms_eps,
8709 self.norm_style,
8710 );
8711 self.lm_head_forward(&normed)
8715 }
8716 };
8717 let target = ids[pos + 1] as usize;
8718 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8719 let lse: f64 = logits
8720 .iter()
8721 .map(|&v| ((v - max) as f64).exp())
8722 .sum::<f64>()
8723 .ln()
8724 + max as f64;
8725 let tok_nll = lse - logits[target] as f64;
8726 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8727 let top = logits
8728 .iter()
8729 .enumerate()
8730 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8731 .map(|(i, _)| i)
8732 .unwrap_or(0);
8733 eprintln!(
8734 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8735 logits[target], logits[top]
8736 );
8737 }
8738 nll += tok_nll;
8739 cnt += 1;
8740 }
8741 Ok((nll, cnt))
8742 })();
8743 self.nll_end();
8744 result
8745 }
8746
8747 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
8752 let normed = inference::rms_norm(
8753 hidden,
8754 &self.weights.final_norm,
8755 self.rms_eps,
8756 self.norm_style,
8757 );
8758 let mut logits = self.lm_head_forward(&normed);
8761 let target = target as usize;
8762 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8763 let lse: f64 = logits
8764 .iter()
8765 .map(|&v| ((v - max) as f64).exp())
8766 .sum::<f64>()
8767 .ln()
8768 + max as f64;
8769 let tok_nll = lse - logits[target] as f64;
8770 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8771 let top = logits
8772 .iter()
8773 .enumerate()
8774 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8775 .map(|(i, _)| i)
8776 .unwrap_or(0);
8777 eprintln!(
8778 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8779 logits[target], logits[top]
8780 );
8781 }
8782 attention::recycle_buf(&mut logits);
8783 tok_nll
8784 }
8785
8786 fn trace_recurrent_state(&self, pos: usize) {
8794 let stats = |v: &[f32]| -> (f64, f64) {
8795 if v.is_empty() {
8796 return (0.0, 0.0);
8797 }
8798 let (mut ss, mut mx) = (0f64, 0f64);
8799 for &x in v {
8800 ss += (x as f64) * (x as f64);
8801 mx = mx.max((x as f64).abs());
8802 }
8803 ((ss / v.len() as f64).sqrt(), mx)
8804 };
8805 for (li, l) in self.kv_cache.layers.iter().enumerate() {
8806 let lw = &self.weights.layers[self.phys_layer(li)];
8807 let (kind, s_len) = match &lw.attn {
8808 AttnKind::Linear(_) => (
8809 "vmf",
8810 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
8811 ),
8812 AttnKind::LinearGdn(_) => (
8813 "gdn",
8814 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
8815 ),
8816 AttnKind::Bounded(_) => ("bounded", 0),
8817 AttnKind::Full { .. } => ("full", 0),
8818 _ => ("other", 0),
8819 };
8820 let (rms, max) = stats(&l.linear_state);
8821 let s_part = if kind == "vmf" {
8822 &l.linear_state[..s_len.min(l.linear_state.len())]
8823 } else if kind == "gdn" {
8824 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
8825 &l.linear_state[ring..]
8826 } else {
8827 &l.linear_state[..0]
8828 };
8829 let (s_rms, s_max) = stats(s_part);
8830 let (ring_rms, ring_len) = match &l.bounded {
8831 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
8832 None => (0.0, 0),
8833 };
8834 eprintln!(
8835 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
8836 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
8837 l.linear_state.len(),
8838 l.seq_len
8839 );
8840 }
8841 }
8842
8843 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
8861 self.nll_begin()?;
8866 let requested_prefix = (prefill > 0).then_some(prefill);
8867 self.o1_begin_with_prefix(requested_prefix);
8868 let n = ids.len().saturating_sub(1);
8869 let requested_start = prefill.min(n);
8870 let exact_end = if self.o1_active() {
8875 match requested_prefix {
8876 Some(requested) => self.o1_effective_boundary(requested),
8877 None => self
8878 .o1_cfg
8879 .as_ref()
8880 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
8881 }
8882 .unwrap_or(requested_start)
8883 .min(n)
8884 } else {
8885 requested_start
8886 };
8887 let mut nll = 0f64;
8888 let mut cnt = 0usize;
8889
8890 let mut pos = 0usize;
8894 if self.can_prefill_batched() {
8895 const CHUNK: usize = 128;
8896 while pos < exact_end {
8897 let end = (pos + CHUNK).min(exact_end);
8898 let hiddens = self.prefill_batch(&ids[pos..end], pos);
8899 if self
8900 .graph_failed
8901 .swap(false, std::sync::atomic::Ordering::Relaxed)
8902 {
8903 self.cancel
8904 .store(false, std::sync::atomic::Ordering::Relaxed);
8905 self.nll_end();
8906 return Err("GPU graph failed during O(1) NLL prefix".into());
8907 }
8908 for row in 0..end - pos {
8909 let score_pos = pos + row;
8910 if score_pos >= requested_start && score_pos < n {
8911 nll += self.nll_from_hidden(
8912 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
8913 ids[score_pos + 1],
8914 score_pos,
8915 );
8916 cnt += 1;
8917 }
8918 }
8919 pos = end;
8920 }
8921 } else {
8922 while pos < exact_end {
8923 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
8924 if self
8925 .graph_failed
8926 .swap(false, std::sync::atomic::Ordering::Relaxed)
8927 {
8928 self.cancel
8929 .store(false, std::sync::atomic::Ordering::Relaxed);
8930 self.nll_end();
8931 return Err("GPU graph failed during O(1) NLL prefix".into());
8932 }
8933 if pos >= requested_start && pos < n {
8934 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
8935 cnt += 1;
8936 }
8937 pos += 1;
8938 }
8939 }
8940 self.o1_seal_checked().map_err(|err| {
8941 self.nll_end();
8942 err
8943 })?;
8944
8945 let batch_k = std::env::var("CMF_BATCH_K")
8954 .ok()
8955 .and_then(|v| v.parse::<usize>().ok())
8956 .unwrap_or(0);
8957 let batch_admitted = batch_k > 0
8958 && self.can_prefill_batched()
8959 && self.o1_active()
8960 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
8961 && (0..self.num_layers).all(|li| {
8962 let cache = &self.kv_cache.layers[self.phys_layer(li)];
8963 cache.o1.is_none() || cache.o1_views().is_some()
8964 });
8965 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8966 eprintln!(
8967 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
8968 batch_admitted,
8969 batch_k,
8970 n.saturating_sub(exact_end),
8971 );
8972 }
8973 let mut batch_completed = false;
8974 if batch_admitted && exact_end < n {
8975 let hs = self.hidden_size;
8976 let mut batch_pos = exact_end;
8977 while batch_pos < n {
8978 let end = (batch_pos + batch_k).min(n);
8979 let bk = end - batch_pos;
8980 let mut hiddens = vec![0.0f32; bk * hs];
8981 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
8982 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
8983 }
8984 let positions: Vec<usize> = (batch_pos..end).collect();
8985 let t_batch = std::time::Instant::now();
8986 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
8987 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8988 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
8989 eprintln!(
8990 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
8991 batch_pos,
8992 end.saturating_sub(1),
8993 bk as f64 / (ms / 1000.0),
8994 );
8995 }
8996 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
8997 self.nll_end();
8998 return Err(err);
8999 }
9000 match outcome {
9001 crate::gpu::BatchGraphOutcome::Completed => {
9002 batch_completed = true;
9003 for row in 0..bk {
9004 nll += self.nll_from_hidden(
9005 &hiddens[row * hs..(row + 1) * hs],
9006 ids[batch_pos + row + 1],
9007 batch_pos + row,
9008 );
9009 cnt += 1;
9010 }
9011 batch_pos = end;
9012 }
9013 crate::gpu::BatchGraphOutcome::Declined => {
9014 if batch_completed {
9015 self.nll_end();
9016 return Err(format!(
9017 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
9018 ));
9019 }
9020 break;
9021 }
9022 crate::gpu::BatchGraphOutcome::Failed => {
9023 self.nll_end();
9024 return Err(format!(
9025 "O(1) NLL batch graph failed after admission at position {batch_pos}"
9026 ));
9027 }
9028 }
9029 }
9030 if batch_completed && cnt == n.saturating_sub(requested_start) {
9031 self.nll_end();
9032 return Ok((nll, cnt));
9033 }
9034 }
9035
9036 for pos in exact_end..n {
9041 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9042 if self
9043 .graph_failed
9044 .swap(false, std::sync::atomic::Ordering::Relaxed)
9045 {
9046 self.cancel
9047 .store(false, std::sync::atomic::Ordering::Relaxed);
9048 self.nll_end();
9049 return Err(format!(
9050 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9051 ));
9052 }
9053 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9054 cnt += 1;
9055 }
9056 self.nll_end();
9057 Ok((nll, cnt))
9058 }
9059
9060 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9068 self.clear_sequence_state();
9069 let n = ids.len().saturating_sub(1);
9070 let mut correct = Vec::with_capacity(n);
9071 let mut pmax = Vec::with_capacity(n);
9072 for pos in 0..n {
9073 let emb = self.embed_single(ids[pos]);
9074 let hidden = self.forward_layers(&emb, pos, None);
9075 let logits = if let Some(logits) = self.graph_logits.take() {
9076 logits
9077 } else {
9078 let normed = inference::rms_norm(
9079 &hidden,
9080 &self.weights.final_norm,
9081 self.rms_eps,
9082 self.norm_style,
9083 );
9084 self.lm_head_forward(&normed)
9088 };
9089 let target = ids[pos + 1] as usize;
9090 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9091 for (i, &v) in logits.iter().enumerate() {
9092 if v > mval {
9093 mval = v;
9094 amax = i;
9095 }
9096 }
9097 correct.push(amax == target);
9098 let row: Vec<f32> = temps
9099 .iter()
9100 .map(|&t| {
9101 let tt = t.max(1e-3);
9102 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9103 1.0 / s.max(1e-12) })
9105 .collect();
9106 pmax.push(row);
9107 }
9108 self.clear_sequence_state();
9109 (correct, pmax)
9110 }
9111
9112 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9119 if self.dyn_router.is_none() {
9120 return Ok((self.ppl_ids(ids)?, 0));
9121 }
9122 self.nll_begin()?;
9123 let saved_active = self.dyn_active;
9124 let mut router = self
9125 .dyn_router
9126 .take()
9127 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9128 router.reset();
9129 self.dyn_phi_seen = 0;
9130 let _ = self.set_active_skill(None);
9131
9132 let result: Result<(f64, usize), String> = (|| {
9133 let mut nll = 0f64;
9134 let mut cnt = 0usize;
9135 for pos in 0..ids.len().saturating_sub(1) {
9136 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9137 self.nll_check_graph("dynamic serial forward", pos)?;
9138 let out_of_band = self.graph_logits.take();
9139 let mut logits = match out_of_band {
9140 Some(lg) => lg,
9141 None => {
9142 let normed = inference::rms_norm(
9143 &hidden,
9144 &self.weights.final_norm,
9145 self.rms_eps,
9146 self.norm_style,
9147 );
9148 self.lm_head_forward(&normed)
9152 }
9153 };
9154 let target = ids[pos + 1] as usize;
9155 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9156 let lse: f64 = logits
9157 .iter()
9158 .map(|&v| ((v - max) as f64).exp())
9159 .sum::<f64>()
9160 .ln()
9161 + max as f64;
9162 let tok_nll = lse - logits[target] as f64;
9163 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9164 let top = logits
9165 .iter()
9166 .enumerate()
9167 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9168 .map(|(i, _)| i)
9169 .unwrap_or(0);
9170 eprintln!(
9171 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9172 logits[target], logits[top]
9173 );
9174 }
9175 nll += tok_nll;
9176 cnt += 1;
9177 attention::recycle_buf(&mut logits);
9178 let phi = self.dyn_phi_ema.clone();
9180 if let Some(new_active) = router.step(&phi, pos) {
9181 let _ = self.set_active_skill(new_active);
9182 }
9183 }
9184 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9185 })();
9186
9187 let _ = self.set_active_skill(saved_active);
9190 self.dyn_router = Some(router);
9191 self.nll_end();
9192 result
9193 }
9194
9195 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9197 self.clear_sequence_state();
9198 let mut acc = vec![0f32; self.hidden_size];
9199 for (pos, &id) in ids.iter().enumerate() {
9200 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9201 for (a, v) in acc.iter_mut().zip(&h) {
9202 *a += v;
9203 }
9204 }
9205 let n = ids.len().max(1) as f32;
9206 for a in acc.iter_mut() {
9207 *a /= n;
9208 }
9209 self.clear_sequence_state();
9210 acc
9211 }
9212
9213 pub fn probe_phi_span(
9227 &mut self,
9228 ids: &[u32],
9229 layer: usize,
9230 span: std::ops::Range<usize>,
9231 ) -> Vec<f32> {
9232 #[cfg(target_os = "macos")]
9233 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9234 let end = span.end.min(ids.len());
9235 let start = span.start.min(end);
9236 let reset = |p: &mut Self| p.clear_sequence_state();
9237 reset(self);
9238 let mut acc = vec![0f32; self.hidden_size];
9239 for (pos, &id) in ids[..end].iter().enumerate() {
9240 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9241 if pos >= start {
9242 for (a, v) in acc.iter_mut().zip(&h) {
9243 *a += v;
9244 }
9245 }
9246 }
9247 let n = end - start;
9248 if n > 0 {
9249 let n = n as f32;
9250 for a in acc.iter_mut() {
9251 *a /= n;
9252 }
9253 }
9254 reset(self);
9255 acc
9256 }
9257
9258 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9265 #[cfg(target_os = "macos")]
9266 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9267 self.graph_logits = None;
9268 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9269 if let Some(logits) = self.graph_logits.take() {
9270 return logits;
9271 }
9272 inference::rms_norm_into(
9273 &hidden,
9274 &self.weights.final_norm,
9275 self.rms_eps,
9276 self.norm_style,
9277 &mut self.ws.n1,
9278 );
9279 self.lm_head_forward(&self.ws.n1)
9280 }
9281
9282 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9288 self.prefill_batch_masked(ids, start_pos, None)
9289 }
9290
9291 fn prefill_batch_masked(
9297 &mut self,
9298 ids: &[u32],
9299 start_pos: usize,
9300 task_mask: Option<&TaskMask>,
9301 ) -> Vec<f32> {
9302 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9303 }
9304
9305 fn prefill_rows(
9313 &mut self,
9314 ids: &[u32],
9315 pos: usize,
9316 task_mask: Option<&TaskMask>,
9317 ) -> Result<Vec<f32>, String> {
9318 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9319 }
9320
9321 fn prefill_input_rows(
9322 &mut self,
9323 input: PrefillIn<'_>,
9324 pos: usize,
9325 task_mask: Option<&TaskMask>,
9326 ) -> Result<Vec<f32>, String> {
9327 self.mimo_moe_prepare();
9328 let hs = self.hidden_size;
9329 let bk = match input {
9330 PrefillIn::Ids(ids) => ids.len(),
9331 PrefillIn::Hidden(rows) => rows.len() / hs,
9332 };
9333 #[cfg(not(target_os = "macos"))]
9334 if task_mask.is_none()
9335 && !self.o1_active()
9336 && bk > 1
9337 && (self.batch_prefix_prefill()
9338 || (self.verify_exact_moe
9339 && crate::gpu::enabled_here()
9340 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9341 {
9342 let mut hiddens = match input {
9343 PrefillIn::Hidden(rows) => rows.to_vec(),
9344 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9345 };
9346 let positions: Vec<usize> = (pos..pos + bk).collect();
9347 let mut run = 0usize;
9348 match self.try_batch_graph_wgpu_prefix(
9349 &mut hiddens,
9350 &positions,
9351 bk,
9352 None,
9353 Some(&mut run),
9354 ) {
9355 crate::gpu::BatchGraphOutcome::Completed => {
9356 let out = if run < self.num_layers {
9357 self.prefill_batch_span(
9358 PrefillIn::Hidden(&hiddens),
9359 pos,
9360 None,
9361 run,
9362 self.num_layers,
9363 )
9364 } else {
9365 hiddens
9366 };
9367 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9368 Err("MiMo attention graph failed after admission".into())
9369 } else { Ok(out) };
9370 }
9371 crate::gpu::BatchGraphOutcome::Failed => {
9372 return Err("batched prefix prefill failed after admission".into());
9373 }
9374 crate::gpu::BatchGraphOutcome::Declined => {
9375 #[cfg(feature = "gpu")]
9377 self.pull_lagging_host_kv(0, self.num_layers, pos);
9378 }
9379 }
9380 }
9381 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9382 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9383 Err("batch tail graph failed after admission".into())
9384 } else { Ok(out) }
9385 }
9386
9387 fn prefill_batch_span(
9393 &mut self,
9394 input: PrefillIn<'_>,
9395 start_pos: usize,
9396 task_mask: Option<&TaskMask>,
9397 from: usize,
9398 upto_excl: usize,
9399 ) -> Vec<f32> {
9400 let hs = self.hidden_size;
9401 let b = match input {
9402 PrefillIn::Ids(ids) => ids.len(),
9403 PrefillIn::Hidden(hb) => hb.len() / hs,
9404 };
9405 let upto_excl = upto_excl.min(self.num_layers);
9406 let mut h: Vec<f32>;
9410 let mut h_ready;
9411 match input {
9412 PrefillIn::Ids(_) => {
9413 h = vec![0.0; b * hs];
9414 h_ready = false;
9415 }
9416 PrefillIn::Hidden(hb) => {
9417 h = hb.to_vec();
9418 h_ready = true;
9419 }
9420 }
9421 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9422 if let PrefillIn::Ids(ids) = input {
9423 for (bi, &id) in ids.iter().enumerate() {
9424 let e = me.embed_single(id);
9425 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9426 }
9427 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9428 if let Ok(t) = tp.parse::<usize>() {
9429 if t >= start_pos && t < start_pos + ids.len() {
9430 let bi = t - start_pos;
9431 let row = &h[bi * hs..(bi + 1) * hs];
9432 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9433 eprintln!(
9434 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9435 ids[bi],
9436 row[0],
9437 row[1],
9438 ids.len(),
9439 &ids[..ids.len().min(8)]
9440 );
9441 }
9442 }
9443 }
9444 }
9445 };
9446 let (_nkv, _hd, _rd, eps) = (
9447 self.num_kv_heads,
9448 self.head_dim,
9449 self.rotary_dim,
9450 self.rms_eps,
9451 );
9452 let pool = self.pool.clone();
9453 let norm_style = self.norm_style;
9454 self.mimo_moe_prepare();
9455 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9456
9457 #[cfg(target_os = "macos")]
9458 let mut chunk_skip_until = 0usize;
9459 for li in from..upto_excl {
9460 let _capacity_tail = automatic_gpu_prefix
9461 .filter(|&prefix| {
9462 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9463 })
9464 .map(|_| crate::gpu::enter_cpu_scope());
9465 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9470 let _fast_gemm = self
9471 .proj_gate_sigmoid
9472 .then(crate::gpu::enter_prefill_fast_gemm);
9473 #[cfg(target_os = "macos")]
9479 if task_mask.is_none() {
9480 if li < chunk_skip_until {
9481 continue;
9482 }
9483 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9489 fill_h(&mut h, self);
9490 h_ready = true;
9491 }
9492 let ids_for_embed = match input {
9493 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9494 PrefillIn::Hidden(_) => None,
9495 };
9496 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9497 if end > li {
9498 h_ready = true;
9499 chunk_skip_until = end;
9500 if self.is_loop_end(end - 1) && end < self.num_layers {
9503 for bi in 0..b {
9504 let normed = inference::rms_norm(
9505 &h[bi * hs..(bi + 1) * hs],
9506 &self.weights.final_norm,
9507 eps,
9508 norm_style,
9509 );
9510 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9511 }
9512 }
9513 continue;
9514 }
9515 }
9516 if !h_ready {
9517 fill_h(&mut h, self);
9518 h_ready = true;
9519 }
9520 if task_mask.is_none() && self.verify_exact_moe {
9521 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9522 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9523 crate::gpu::BatchGraphOutcome::Completed => continue,
9524 crate::gpu::BatchGraphOutcome::Failed => return h,
9525 crate::gpu::BatchGraphOutcome::Declined => {},
9526 }
9527 }
9528 #[cfg(feature = "gpu")]
9529 self.pull_lagging_host_kv(li, li + 1, start_pos);
9530 let lw = &self.weights.layers[self.phys_layer(li)];
9531 let t_attn = std::time::Instant::now();
9532 match &lw.attn {
9534 AttnKind::Kda(w) => {
9535 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9537 let mut normed = vec![0.0f32; b * hs];
9538 for bi in 0..b {
9539 inference::rms_norm_into(
9540 &h[bi * hs..(bi + 1) * hs],
9541 &lw.input_norm,
9542 eps,
9543 norm_style,
9544 &mut normed[bi * hs..(bi + 1) * hs],
9545 );
9546 }
9547 let attn = crate::linear_core::kda_forward_batch(
9548 &normed,
9549 b,
9550 w,
9551 &cfg,
9552 &mut self.kv_cache.layers[li].linear_state,
9553 pool.as_deref(),
9554 );
9555 for (dst, &a) in h.iter_mut().zip(&attn) {
9556 *dst += a;
9557 }
9558 }
9559 AttnKind::LinearGdn(w) => {
9560 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9562 let mut normed = vec![0.0f32; b * hs];
9563 for bi in 0..b {
9564 let r = inference::rms_norm(
9565 &h[bi * hs..(bi + 1) * hs],
9566 &lw.input_norm,
9567 eps,
9568 norm_style,
9569 );
9570 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9571 }
9572 let attn = crate::linear_core::gdn_forward_batch(
9573 &normed,
9574 b,
9575 w,
9576 &cfg,
9577 &mut self.kv_cache.layers[li].linear_state,
9578 pool.as_deref(),
9579 );
9580 for (dst, &a) in h.iter_mut().zip(&attn) {
9581 *dst += a;
9582 }
9583 }
9584 AttnKind::ShortConv(w) => {
9585 let cfg = self
9588 .short_conv_cfg
9589 .expect("short-conv layer without short_conv_cfg");
9590 let mut normed = vec![0.0f32; b * hs];
9591 for bi in 0..b {
9592 inference::rms_norm_into(
9593 &h[bi * hs..(bi + 1) * hs],
9594 &lw.input_norm,
9595 eps,
9596 norm_style,
9597 &mut normed[bi * hs..(bi + 1) * hs],
9598 );
9599 }
9600 let attn = short_conv_forward_batch(
9601 &normed,
9602 b,
9603 w,
9604 &cfg,
9605 &mut self.kv_cache.layers[li].linear_state,
9606 pool.as_deref(),
9607 );
9608 for (dst, &a) in h.iter_mut().zip(&attn) {
9609 *dst += a;
9610 }
9611 }
9612 AttnKind::Mla(w) => {
9613 let inv_freq_l = self.layer_inv_freq(li);
9616 let rs = self.layer_rope_scale(li);
9617 let mut normed = vec![0.0f32; hs];
9618 for bi in 0..b {
9619 inference::rms_norm_into(
9620 &h[bi * hs..(bi + 1) * hs],
9621 &lw.input_norm,
9622 eps,
9623 norm_style,
9624 &mut normed,
9625 );
9626 let ao = mla_attention(
9627 w,
9628 &normed,
9629 &mut self.kv_cache.layers[li],
9630 start_pos + bi,
9631 &inv_freq_l,
9632 rs,
9633 eps,
9634 pool.as_deref(),
9635 );
9636 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9637 *dst += a;
9638 }
9639 }
9640 }
9641 AttnKind::Full {
9642 wq,
9643 wk,
9644 wv,
9645 wo,
9646 q_norm,
9647 k_norm,
9648 output_gate,
9649 softplus_gate,
9650 bias,
9651 } => {
9652 let mut normed = vec![0.0f32; b * hs];
9656 for bi in 0..b {
9657 inference::rms_norm_into(
9658 &h[bi * hs..(bi + 1) * hs],
9659 &lw.input_norm,
9660 eps,
9661 norm_style,
9662 &mut normed[bi * hs..(bi + 1) * hs],
9663 );
9664 }
9665 let inv_freq_l = self.layer_inv_freq(li);
9666 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9667 let cfg = QwenAttnCfg {
9668 num_heads: self.layer_num_heads(li),
9669 num_kv_heads: nkv_l,
9670 head_dim: hd_l,
9671 hidden_size: hs,
9672 position: start_pos,
9673 inv_freq: &inv_freq_l,
9674 rotary_dim: rd_l,
9675 scale: self.attn_scale,
9676 softcap: self.attn_softcap,
9677 window: self.layer_window(li),
9678 v_norm: self.attn_v_norm,
9679 qk_norm_after_rope: self.qk_norm_after_rope,
9680 gate_sigmoid: self.proj_gate_sigmoid,
9681 q_norm: q_norm.as_deref(),
9682 k_norm: k_norm.as_deref(),
9683 output_gate: *output_gate,
9684 softplus_gate: softplus_gate
9685 .as_ref()
9686 .map(|(gate, per_head)| (gate, *per_head)),
9687 rope_scale: self.layer_rope_scale(li),
9688 bias: bias
9689 .as_ref()
9690 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9691 rms_eps: eps,
9692 norm_style,
9693 pool: pool.as_deref(),
9694 v_head_dim: self.layer_v_dim(li),
9695 };
9696 #[cfg(feature = "gpu")]
9697 let _mirror = self
9698 .prefill_mirror_target(li)
9699 .map(crate::gpu::enter_prefill_mirror);
9700 let mut attn = attention::qwen_attention_batch(
9701 &normed,
9702 b,
9703 wq,
9704 wk,
9705 wv,
9706 wo,
9707 &mut self.kv_cache.layers[li],
9708 &cfg,
9709 );
9710 if let Some(w) = &lw.attn_out_norm {
9711 for bi in 0..b {
9712 inference::rms_norm_into(
9713 &attn[bi * hs..(bi + 1) * hs],
9714 w,
9715 eps,
9716 norm_style,
9717 &mut normed[bi * hs..(bi + 1) * hs],
9718 );
9719 }
9720 attn.copy_from_slice(&normed);
9721 }
9722 for (dst, &a) in h.iter_mut().zip(&attn) {
9723 *dst += a;
9724 }
9725 }
9726 AttnKind::Bounded(w) => {
9727 let mut normed = vec![0.0f32; b * hs];
9730 for bi in 0..b {
9731 inference::rms_norm_into(
9732 &h[bi * hs..(bi + 1) * hs],
9733 &lw.input_norm,
9734 eps,
9735 norm_style,
9736 &mut normed[bi * hs..(bi + 1) * hs],
9737 );
9738 }
9739 let rope = self
9740 .bounded_rope
9741 .clone()
9742 .expect("bounded layer without an installed rotation table");
9743 let cfg = crate::bounded::BoundedAttnCfg {
9744 num_heads: self.num_heads,
9745 num_kv_heads: self.num_kv_heads,
9746 head_dim: self.head_dim,
9747 hidden_size: hs,
9748 scale: self.attn_scale,
9749 rope: &rope,
9750 pool: pool.as_deref(),
9751 };
9752 let mut attn = crate::bounded::bounded_attention_batch(
9753 &normed,
9754 b,
9755 w,
9756 &mut self.kv_cache.layers[li],
9757 &cfg,
9758 );
9759 if let Some(wn) = &lw.attn_out_norm {
9760 for bi in 0..b {
9761 inference::rms_norm_into(
9762 &attn[bi * hs..(bi + 1) * hs],
9763 wn,
9764 eps,
9765 norm_style,
9766 &mut normed[bi * hs..(bi + 1) * hs],
9767 );
9768 }
9769 attn.copy_from_slice(&normed);
9770 }
9771 for (dst, &a) in h.iter_mut().zip(&attn) {
9772 *dst += a;
9773 }
9774 attention::recycle_buf(&mut attn);
9775 }
9776 AttnKind::Linear(w) => {
9777 for bi in 0..b {
9778 let normed = inference::rms_norm(
9779 &h[bi * hs..(bi + 1) * hs],
9780 &lw.input_norm,
9781 eps,
9782 norm_style,
9783 );
9784 vmf_phase_forward(
9785 &normed,
9786 w,
9787 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
9788 &mut self.kv_cache.layers[li].linear_state,
9789 pool.as_deref(),
9790 )
9791 .iter()
9792 .enumerate()
9793 .for_each(|(i, &a)| h[bi * hs + i] += a);
9794 }
9795 }
9796 }
9797
9798 let lw = &self.weights.layers[self.phys_layer(li)];
9800 let mut post = vec![0.0f32; b * hs];
9801 for bi in 0..b {
9802 let r =
9803 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
9804 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9805 }
9806 let mask_row = task_mask
9809 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
9810 .and_then(|m| m.ffn_masks.get(li))
9811 .map(|v| v.as_slice());
9812 let attn_ns = t_attn.elapsed().as_nanos() as u64;
9813 let t_ffn = std::time::Instant::now();
9814 let mut ffn = match &lw.ffn {
9815 FfnKind::Dense(d) if !d.segs.is_empty() => {
9816 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
9817 }
9818 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
9819 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
9820 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
9821 }
9822 FfnKind::Moe(m) if self.verify_exact_moe => {
9823 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
9824 }
9825 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
9828 let before = m.stats.borrow().clone();
9829 let out = crate::gpu::cpu_scope(|| {
9830 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
9831 });
9832 self.mimo_moe.prime(li, m, &before);
9833 out
9834 }
9835 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
9836 FfnKind::DenseMoe(dm) => {
9839 let mut out = vec![0.0f32; b * hs];
9840 for bi in 0..b {
9841 let r = dense_moe_ffn(
9842 dm,
9843 &post[bi * hs..(bi + 1) * hs],
9844 &h[bi * hs..(bi + 1) * hs],
9845 eps,
9846 norm_style,
9847 pool.as_deref(),
9848 );
9849 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9850 }
9851 out
9852 }
9853 };
9854 if prefill_prof_on() {
9855 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
9856 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
9857 }
9858 if let Some(w) = &lw.ffn_out_norm {
9859 for bi in 0..b {
9860 inference::rms_norm_into(
9861 &ffn[bi * hs..(bi + 1) * hs],
9862 w,
9863 eps,
9864 norm_style,
9865 &mut post[bi * hs..(bi + 1) * hs],
9866 );
9867 }
9868 ffn.copy_from_slice(&post);
9869 }
9870 for (dst, &f) in h.iter_mut().zip(&ffn) {
9871 *dst += f;
9872 }
9873 if let Some(sc) = lw.layer_scale {
9874 for v in h.iter_mut() {
9875 *v *= sc;
9876 }
9877 }
9878 if self.layer_dump.is_some() {
9880 for bi in 0..b {
9881 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
9882 }
9883 }
9884 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9885 if let Ok(t) = tp.parse::<usize>() {
9886 if t >= start_pos && t < start_pos + b {
9887 let bi = t - start_pos;
9888 let row = &h[bi * hs..(bi + 1) * hs];
9889 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9890 eprintln!(
9891 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
9892 row[0], row[1]
9893 );
9894 }
9895 }
9896 }
9897 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
9901 let row = &h[(b - 1) * hs..b * hs];
9902 let rms =
9903 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
9904 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
9905 eprintln!(
9906 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
9907 match &self.weights.layers[self.phys_layer(li)].attn {
9908 AttnKind::LinearGdn(_) => "gdn",
9909 AttnKind::Linear(_) => "vmf",
9910 AttnKind::ShortConv(_) => "conv",
9911 _ => "attn",
9912 },
9913 match &lw.ffn {
9914 FfnKind::Moe(_) => "moe",
9915 FfnKind::Dense(_) => "dense",
9916 FfnKind::DenseMoe(_) => "dense+moe",
9917 },
9918 );
9919 }
9920 if self.is_loop_end(li) && li + 1 < self.num_layers {
9922 for bi in 0..b {
9923 let normed = inference::rms_norm(
9924 &h[bi * hs..(bi + 1) * hs],
9925 &self.weights.final_norm,
9926 eps,
9927 norm_style,
9928 );
9929 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9930 }
9931 }
9932 if std::env::var("CMF_TRACE_H").is_ok() {
9933 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
9934 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
9935 eprintln!(
9936 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
9937 lw.layer_scale
9938 );
9939 }
9940 }
9941 crate::gpu::set_layer(-1); if prefill_prof_on() {
9943 let ms = |a: &std::sync::atomic::AtomicU64| {
9944 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
9945 };
9946 let sp = &attention::ATTN_SPLIT;
9947 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
9948 eprintln!(
9949 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
9950 host gate {:.1}], host loop {:.1}, attend {:.1} \
9951 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
9952 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
9953 ms(&PREFILL_SPLIT[0]),
9954 ms(&sp[0]),
9955 qkv_enq,
9956 qkv_wait,
9957 ms(&sp[6]),
9958 ms(&sp[1]),
9959 ms(&sp[2]),
9960 ms(&sp[4]),
9961 ms(&sp[5]),
9962 ca_wait,
9963 ms(&sp[3]),
9964 ms(&PREFILL_SPLIT[1]),
9965 );
9966 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
9967 eprintln!(
9968 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
9969 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
9970 fallbacks (cumulative)",
9971 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
9972 );
9973 }
9974 self.o1_progress();
9979 self.swa_trim_tails();
9981 h
9982 }
9983
9984 fn embed_single(&self, id: u32) -> Vec<f32> {
9986 let mut out = vec![0.0f32; self.hidden_size];
9987 if (id as usize) < self.weights.embed_tokens.rows() {
9988 self.weights.embed_tokens.row_f32(id as usize, &mut out);
9989 }
9990 if self.embed_multiplier != 1.0 {
9991 for v in out.iter_mut() {
9992 *v *= self.embed_multiplier;
9993 }
9994 }
9995 if self.dsv4.is_some()
9999 || self.dsv41.is_some()
10000 || self.qwen4_exp.is_some()
10001 {
10002 let mut v = vec![0.0f32; self.hidden_size.max(1)];
10003 v[0] = id as f32;
10004 return v;
10005 }
10006 if let Some(b) = &self.g3n {
10009 return b.0.extend_embedding(id, &out, self.pool.as_deref());
10010 }
10011 out
10012 }
10013
10014 #[cfg(target_os = "macos")]
10020 fn chunk_run_gpu(
10021 &mut self,
10022 li0: usize,
10023 h: &mut [f32],
10024 b: usize,
10025 pos0: usize,
10026 embed_ids: Option<&[u32]>,
10027 cap: usize,
10028 ) -> usize {
10029 let gemma4 = self.metal_gemma4()
10036 && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10037 if !crate::gpu::enabled_here()
10038 || std::env::var("CMF_GPU_CHUNK")
10039 .map(|v| v == "0")
10040 .unwrap_or(false)
10041 || b < 32
10042 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10045 || (self.global_attn.is_some() && !gemma4)
10046 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10048 || self.o1_active()
10051 || (self.attn_v_norm && !gemma4)
10052 || !self.metal_rope_scale_ok()
10053 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10054 {
10055 return li0;
10056 }
10057 let Some(model) = self.model.clone() else {
10058 return li0;
10059 };
10060 let (nh, nkv, hd, hs) = (
10061 self.num_heads,
10062 self.num_kv_heads,
10063 self.head_dim,
10064 self.hidden_size,
10065 );
10066 let loop_end = if self.loop_final_norm {
10070 ((li0 / self.physical_layers) + 1) * self.physical_layers
10071 } else {
10072 self.num_layers
10073 };
10074 let run_end = self.num_layers.min(loop_end).min(cap);
10075 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10080 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10081 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10082 let mut stored_at: Vec<usize> = Vec::new();
10083 for li in li0..run_end {
10084 let lw = &self.weights.layers[self.phys_layer(li)];
10085 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10086 && !gemma4
10087 {
10088 break;
10089 }
10090 let AttnKind::Full {
10091 wq,
10092 wk,
10093 wv,
10094 wo,
10095 q_norm,
10096 k_norm,
10097 output_gate: false,
10098 softplus_gate,
10099 bias,
10100 } = &lw.attn
10101 else {
10102 break;
10103 };
10104 let head_gate = match softplus_gate {
10106 None => None,
10107 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10108 Some((d, r, c)) if r == nh && c == hs => Some(d),
10109 _ => break,
10110 },
10111 Some(_) => break,
10112 };
10113 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10117 FfnKind::Dense(d)
10118 if (matches!(d.act, Act::Silu | Act::Gelu)
10119 || (gemma4 && d.act == Act::GeluTanh))
10120 && d.segs.is_empty() =>
10121 {
10122 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10123 }
10124 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10125 Some((router, experts, q8)) => {
10126 let e0 = &m.experts[0];
10127 let route = Box::new(move |lg: &[f32]| {
10128 let (idx, p, wsum) = moe_route(lg, m, None);
10130 let mut st = m.stats.borrow_mut();
10131 if st.len() < m.experts.len() {
10132 st.resize(m.experts.len(), 0);
10133 }
10134 for &e in &idx {
10135 st[e] += 1;
10136 }
10137 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10138 (idx, w)
10139 });
10140 (
10141 &e0.gate_proj,
10142 &e0.up_proj,
10143 &e0.down_proj,
10144 Act::Silu,
10145 Some(crate::gpu_metal::ChunkMoe {
10146 router,
10147 experts,
10148 q8,
10149 route,
10150 }),
10151 )
10152 }
10153 None => break,
10154 },
10155 _ => break,
10156 };
10157 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10163 t.q8_row_parts()
10164 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10165 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10166 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10167 }
10168 fn cf(t: &QTensor) -> &[f32] {
10169 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10170 }
10171 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10172 if moe.is_none()
10181 && !gemma4
10182 && (col.iter().any(|c| !c.is_empty())
10183 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10184 {
10185 break;
10186 }
10187 let parts = (
10188 cw(wq),
10189 cw(wk),
10190 cw(wv),
10191 cw(wo),
10192 cw(gate_t),
10193 cw(up_t),
10194 cw(down_t),
10195 );
10196 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10197 else {
10198 break;
10199 };
10200 let layer = &self.kv_cache.layers[li];
10201 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10202 break;
10203 }
10204 debug_assert!(
10206 layer.base() == 0
10207 || self
10208 .layer_window(li)
10209 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10210 );
10211 stored_at.push(layer.head_len(0));
10212 let (nkv, hd) = if gemma4 {
10215 let (a, bb, _) = self.layer_geom(li);
10216 (a, bb)
10217 } else {
10218 (nkv, hd)
10219 };
10220 layers.push(crate::gpu_metal::ChunkLayer {
10221 model: &model,
10222 kv_id: self.graph_kv_id,
10223 layer: li,
10224 wq: pq,
10225 wk: pk,
10226 wv: pv,
10227 wo: po,
10228 gate: pg,
10229 up: pu,
10230 down: pd,
10231 input_norm: &lw.input_norm,
10232 post_norm: &lw.post_norm,
10233 bias: bias
10234 .as_ref()
10235 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10236 q_norm: q_norm.as_deref(),
10237 k_norm: k_norm.as_deref(),
10238 inv_freq: &tables[li - li0],
10239 rope_scale: self.layer_rope_scale(li),
10240 rd: self.layer_geom(li).2,
10241 nh,
10242 nkv,
10243 hd,
10244 hs,
10245 inter: gate_t.rows(),
10246 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10247 late_qk_norm: self.qk_norm_after_rope,
10248 eps: self.rms_eps as f32,
10249 window: self.layer_window(li),
10250 head_gate,
10251 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10252 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10253 v_norm: self.attn_v_norm,
10254 scale: self.attn_scale,
10255 attn_out_norm: lw.attn_out_norm.as_deref(),
10256 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10257 layer_scale: lw.layer_scale,
10258 }),
10259 col,
10260 moe,
10261 });
10262 }
10263 if layers.is_empty() {
10264 return li0;
10265 }
10266 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10268 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10269 .iter()
10270 .zip(&rows_kv)
10271 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10272 .collect();
10273 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10274 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10275 let li = layers[i].layer;
10276 let layer = &self.kv_cache.layers[li];
10277 let nkv = rows_kv[i].0;
10278 io.push(crate::gpu_metal::ChunkIo {
10279 cpu_stored: stored_at[i],
10280 cpu_gen: layer.generation(),
10281 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10282 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10283 out_k: ok,
10284 out_v: ov,
10285 imp: oi,
10286 });
10287 }
10288 let n_run = layers.len();
10289 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10290 let ep = embed_ids.and_then(|ids| {
10293 self.weights
10294 .embed_tokens
10295 .q8_row_parts()
10296 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10297 idx,
10298 rows,
10299 row_scale: rs,
10300 ids,
10301 mult: self.embed_multiplier,
10302 })
10303 });
10304 if embed_ids.is_some() && ep.is_none() {
10305 return li0;
10306 }
10307 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10308 return li0;
10309 }
10310 drop(io);
10311 drop(layers);
10312 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10315 let li = li0 + i;
10316 let row = rows_kv[i].1;
10317 let layer = &mut self.kv_cache.layers[li];
10318 for bi in 0..b {
10319 layer.append(
10320 &ok[bi * row..(bi + 1) * row],
10321 &ov[bi * row..(bi + 1) * row],
10322 &[],
10323 );
10324 }
10325 layer.accumulate_imp(oi);
10326 }
10327 last
10328 }
10329
10330 fn layer_is_local(&self, li: usize) -> bool {
10333 if let Some(layers) = &self.sliding_layers {
10334 return layers.get(li).copied().unwrap_or(false);
10335 }
10336 match self.swa {
10337 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10338 None => false,
10339 }
10340 }
10341
10342 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10345 if self.layer_is_local(li) {
10346 if let Some(f) = &self.inv_freq_local {
10347 return f.clone();
10348 }
10349 } else if let Some(f) = &self.inv_freq_global {
10350 return f.clone();
10351 }
10352 self.inv_freq.clone()
10353 }
10354
10355 fn layer_window(&self, li: usize) -> Option<usize> {
10357 self.swa
10358 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10359 }
10360
10361 fn swa_trim_tails(&mut self) {
10370 let Some((slack, align)) = self.swa_trim else {
10371 return;
10372 };
10373 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10374 return;
10375 }
10376 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10377 if let Some(w) = self.layer_window(li) {
10378 self.kv_cache.layers[li].trim_window(w, slack, align);
10379 }
10380 }
10381 }
10382
10383 fn layer_num_heads(&self, li: usize) -> usize {
10384 self.attention_heads_per_layer
10385 .as_ref()
10386 .and_then(|v| v.get(li).copied())
10387 .unwrap_or(self.num_heads)
10388 }
10389
10390 fn layer_rope_scale(&self, li: usize) -> f32 {
10391 if self.layer_is_local(li) {
10392 self.rope_scale_local
10393 } else {
10394 self.rope_scale
10395 }
10396 }
10397
10398 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10401 if !self.layer_is_local(li) {
10402 if let Some((ghd, gkv)) = self.global_attn {
10403 return (gkv, ghd, ghd);
10404 }
10405 }
10406 (
10407 self.layer_num_kv_heads(li),
10408 self.head_dim,
10409 if self.layer_is_local(li) {
10410 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10411 } else {
10412 self.rotary_dim
10413 },
10414 )
10415 }
10416
10417 fn layer_num_kv_heads(&self, li: usize) -> usize {
10420 self.kv_heads_per_layer
10421 .as_ref()
10422 .and_then(|v| v.get(self.phys_layer(li)).copied())
10423 .unwrap_or(self.num_kv_heads)
10424 }
10425
10426 fn layer_v_dim(&self, li: usize) -> usize {
10428 let (_, hd, _) = self.layer_geom(li);
10429 self.v_head_dim.unwrap_or(hd).min(hd)
10430 }
10431
10432 pub fn set_attn_geometry(
10441 &mut self,
10442 kv_heads_per_layer: Option<Vec<usize>>,
10443 v_head_dim: Option<usize>,
10444 ) -> Result<(), String> {
10445 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10446 if self.global_attn.is_some() {
10447 return Err(
10448 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10449 attention geometry"
10450 .into(),
10451 );
10452 }
10453 if self
10454 .weights
10455 .layers
10456 .iter()
10457 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10458 {
10459 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10460 }
10461 }
10462 if let Some(vd) = v_head_dim {
10463 if vd == 0 || vd > self.head_dim {
10464 return Err(format!(
10465 "v_head_dim {vd} must be in 1..={} (head_dim)",
10466 self.head_dim
10467 ));
10468 }
10469 }
10470 if let Some(v) = &kv_heads_per_layer {
10471 if v.len() != self.physical_layers {
10472 return Err(format!(
10473 "kv_heads_per_layer has {} entries, expected {} layers",
10474 v.len(),
10475 self.physical_layers
10476 ));
10477 }
10478 for (li, &nkv) in v.iter().enumerate() {
10479 let is_attn = matches!(
10480 self.weights.layers.get(li).map(|lw| &lw.attn),
10481 Some(AttnKind::Full { .. }) | None
10482 );
10483 if !is_attn {
10484 continue;
10485 }
10486 let nh = self
10487 .attention_heads_per_layer
10488 .as_ref()
10489 .and_then(|h| h.get(li).copied())
10490 .unwrap_or(self.num_heads);
10491 if nkv == 0 || nh % nkv != 0 {
10492 return Err(format!(
10493 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10494 ));
10495 }
10496 }
10497 }
10498 self.kv_heads_per_layer = kv_heads_per_layer;
10499 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10500 if self.kv_heads_per_layer.is_some() {
10501 for li in 0..self.kv_cache.layers.len() {
10502 let full = matches!(
10503 self.weights
10504 .layers
10505 .get(self.phys_layer(li))
10506 .map(|lw| &lw.attn),
10507 Some(AttnKind::Full { .. })
10508 );
10509 let nkv = self.layer_num_kv_heads(li);
10510 let cache = &self.kv_cache.layers[li];
10511 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10512 let sinks = cache.sinks.clone();
10513 self.kv_cache.layers[li] =
10514 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10515 self.kv_cache.layers[li].sinks = sinks;
10516 }
10517 }
10518 }
10519 Ok(())
10520 }
10521
10522 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10526 let Some(lw) = self.weights.layers.get(phys) else {
10527 return Err(format!("sinks for layer {phys}: no such layer"));
10528 };
10529 if !matches!(lw.attn, AttnKind::Full { .. }) {
10530 return Err(format!(
10531 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10532 ));
10533 }
10534 let nh = self
10535 .attention_heads_per_layer
10536 .as_ref()
10537 .and_then(|h| h.get(phys).copied())
10538 .unwrap_or(self.num_heads);
10539 if sinks.len() != nh {
10540 return Err(format!(
10541 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10542 sinks.len()
10543 ));
10544 }
10545 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10546 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10547 }
10548 for li in 0..self.kv_cache.layers.len() {
10549 if self.phys_layer(li) == phys {
10550 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10551 }
10552 }
10553 Ok(())
10554 }
10555
10556 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10566 if self.kv_heads_per_layer.is_some() {
10567 return Some("per-layer KV head counts");
10568 }
10569 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10570 return Some("V heads narrower than Q/K heads");
10571 }
10572 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10573 return Some("learned attention sinks");
10574 }
10575 if self.swa.is_some() || self.sliding_layers.is_some() {
10576 return Some("sliding-window layers");
10577 }
10578 None
10579 }
10580
10581 #[cfg(target_os = "macos")]
10589 fn metal_rope_scale_ok(&self) -> bool {
10590 !(self.qk_norm_after_rope && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
10591 }
10592
10593 #[cfg(target_os = "macos")]
10607 fn metal_graph_swa(&self) -> bool {
10608 (self.swa.is_some() || self.sliding_layers.is_some())
10609 && self.attention_heads_per_layer.is_none()
10610 && !self.attn_v_norm
10611 && self.attn_softcap == 0.0
10612 && self.kv_heads_per_layer.is_none()
10613 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10614 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10615 && self.global_attn.is_none()
10616 && self.inv_freq_global.is_none()
10617 && self.metal_rope_scale_ok()
10618 && !(0..self.num_layers).any(|li| {
10619 self.layer_is_local(li)
10620 && self.inv_freq_local.is_none()
10621 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10622 })
10623 }
10624
10625 #[cfg(target_os = "macos")]
10633 fn metal_gemma4(&self) -> bool {
10634 crate::gpu_metal::gemma4::enabled()
10635 && (self.global_attn.is_some() || self.attn_v_norm)
10636 && self.attention_heads_per_layer.is_none()
10637 && self.kv_heads_per_layer.is_none()
10638 && self.v_head_dim.is_none()
10639 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10640 && self.attn_softcap == 0.0
10641 && !self.qk_norm_after_rope
10642 && self.metal_rope_scale_ok()
10643 && self
10644 .weights
10645 .layers
10646 .iter()
10647 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
10648 && !(0..self.num_layers).any(|li| {
10649 self.layer_is_local(li)
10650 && self.inv_freq_local.is_none()
10651 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10652 })
10653 }
10654
10655 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
10662 let geometry_reason = self.graph_attn_decline_reason();
10666 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
10667 if has_scaled_rope && geometry_reason.is_none() {
10668 return Some("scaled RoPE without per-layer geometry");
10669 }
10670 if has_scaled_rope && self.qk_norm_after_rope {
10675 return Some("scaled RoPE with post-RoPE Q/K norm");
10676 }
10677 geometry_reason?;
10678 if self.attention_heads_per_layer.is_some() {
10684 return Some("per-layer Q head counts with per-layer geometry");
10685 }
10686 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
10690 if !gemma4
10691 && self.weights.layers.iter().any(|lw| {
10692 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10693 })
10694 {
10695 return Some("sandwich norms / layer scale with per-layer geometry");
10696 }
10697 if self.weights.layers.iter().any(|lw| {
10701 matches!(
10702 &lw.attn,
10703 AttnKind::Full {
10704 output_gate: true,
10705 ..
10706 }
10707 )
10708 }) && self.v_head_dim.is_some()
10709 {
10710 return Some("gated attention with V narrower than K");
10711 }
10712 if (0..self.num_layers).any(|li| {
10713 self.layer_is_local(li)
10714 && self.inv_freq_local.is_none()
10715 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10716 }) {
10717 return Some("local rotary width without a local RoPE table");
10718 }
10719 None
10720 }
10721
10722 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
10726 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
10729 return None;
10730 }
10731 if self.global_attn.is_some() {
10732 return Some("per-layer head width (Gemma-4 global layers)");
10733 }
10734 if self.attn_v_norm {
10735 return Some("V norm");
10736 }
10737 if self.weights.layers.iter().any(|lw| {
10738 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10739 }) {
10740 return Some("sandwich norms / layer scale");
10741 }
10742 None
10743 }
10744
10745 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
10750 self.graph_attn_decline_reason()?;
10751 let (nkv, hd, rd) = self.layer_geom(li);
10752 let invf: &[f32] = if self.layer_is_local(li) {
10753 match &self.inv_freq_local {
10754 Some(f) => f.as_slice(),
10755 None => self.inv_freq.as_slice(),
10756 }
10757 } else {
10758 match &self.inv_freq_global {
10759 Some(f) => f.as_slice(),
10760 None => self.inv_freq.as_slice(),
10761 }
10762 };
10763 Some(crate::gpu::GraphAttnGeom {
10764 nkv,
10765 hd,
10766 dv: self.layer_v_dim(li),
10767 rd,
10768 invf,
10769 rope_scale: self.layer_rope_scale(li),
10770 window: self.layer_window(li),
10771 sink: self.kv_cache.layers[li].sinks.as_deref(),
10772 v_norm: self.attn_v_norm,
10773 })
10774 }
10775
10776 #[cfg(feature = "gpu")]
10788 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
10789 if !self.proj_gate_sigmoid
10790 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
10791 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
10792 || self.graph_refused()
10793 || self.o1_active()
10794 || self.attn_softcap > 0.0
10795 || self.wgpu_graph_attn_decline().is_some()
10796 {
10797 return None;
10798 }
10799 let g = self.graph_attn_geom(li)?;
10800 let (nkv, hd, _) = self.layer_geom(li);
10801 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
10803 return None;
10804 }
10805 Some(crate::gpu::PrefillMirror {
10806 kv_id: self.graph_kv_id,
10807 layer: li,
10808 limit: self.kv_cache.max_seq_len,
10809 })
10810 }
10811
10812 #[cfg(feature = "gpu")]
10821 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
10822 let kv_id = self.graph_kv_id;
10823 for li in from..upto.min(self.num_layers) {
10824 if !matches!(
10825 self.weights.layers[self.phys_layer(li)].attn,
10826 AttnKind::Full { .. }
10827 ) {
10828 continue;
10829 }
10830 let host = self.kv_cache.layers[li].pos_len();
10832 if host >= position {
10833 continue;
10834 }
10835 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
10836 continue;
10837 };
10838 let to = dev.min(position);
10839 if to <= host {
10840 continue;
10841 }
10842 let (nkv, hd) = {
10843 let c = &self.kv_cache.layers[li];
10844 (c.num_kv_heads, c.head_dim)
10845 };
10846 let Some((k, v, first_valid)) =
10847 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
10848 else {
10849 continue;
10850 };
10851 let need_from = match self.layer_window(li) {
10854 Some(w) => host.max((position + 1).saturating_sub(w)),
10855 None => host,
10856 };
10857 if first_valid > need_from {
10858 tracing::warn!(
10859 "layer {li}: device KV rows {host}..{to} no longer resident \
10860 (from {first_valid}); host attention will miss them"
10861 );
10862 }
10863 let row = nkv * hd;
10864 let cache = &mut self.kv_cache.layers[li];
10865 for p in 0..to - host {
10866 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
10867 }
10868 }
10869 }
10870
10871 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
10874 let mut seen = self.graph_declines.borrow_mut();
10875 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
10876 tracing::warn!("{site} declined: {reason} (CPU attention path)");
10877 seen.push((site, reason));
10878 }
10879 }
10880
10881 pub fn graph_declines(&self) -> Vec<String> {
10884 self.graph_declines
10885 .borrow()
10886 .iter()
10887 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
10888 .collect()
10889 }
10890
10891 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
10900 let Some(dir) = &self.layer_dump else {
10901 return;
10902 };
10903 let mut bytes = Vec::with_capacity(row.len() * 4);
10904 for v in row {
10905 bytes.extend_from_slice(&v.to_le_bytes());
10906 }
10907 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
10908 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
10909 use std::sync::atomic::{AtomicBool, Ordering};
10910 static SAID: AtomicBool = AtomicBool::new(false);
10911 if !SAID.swap(true, Ordering::Relaxed) {
10912 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
10913 }
10914 }
10915 }
10916
10917 fn mimo_moe_prepare(&mut self) {
10920 if !self.mimo_moe.is_undecided() {
10921 return;
10922 }
10923 let slot = {
10924 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
10925 .filter_map(
10926 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
10927 FfnKind::Moe(m) => Some((li, m)),
10928 _ => None,
10929 },
10930 )
10931 .collect();
10932 if layers.is_empty()
10935 || self.physical_layers != self.num_layers
10936 || self.gpu_plan.is_some()
10937 {
10938 crate::mimo_moe::Slot::Off
10939 } else {
10940 let graph_prefix =
10944 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
10945 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
10946 }
10947 };
10948 self.mimo_moe = slot;
10949 }
10950
10951 #[cfg(test)]
10952 pub(crate) fn test_graph_kv_id(&self) -> u64 {
10953 self.graph_kv_id
10954 }
10955
10956 pub(crate) fn mimo_graph_layer_rows(
10960 &mut self,
10961 li: usize,
10962 h: &mut [f32],
10963 positions: &[usize],
10964 ) -> crate::gpu::BatchGraphOutcome {
10965 use crate::gpu::BatchGraphOutcome as Out;
10966 let b = positions.len();
10967 if !(1..=4).contains(&b)
10968 || h.len() != b * self.hidden_size
10969 || !self.mimo_moe.is_dynamic(li, true)
10970 || !crate::gpu::enabled_here()
10971 || !crate::gpu::wgpu_active()
10972 || self.o1_active()
10973 || self.physical_layers != self.num_layers
10974 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
10978 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
10979 || self.wgpu_graph_attn_decline().is_some()
10980 {
10981 return Out::Declined;
10982 }
10983 let attn_started = std::time::Instant::now();
10984 let outcome = {
10985 let lw = &self.weights.layers[li];
10986 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
10987 return Out::Declined;
10988 }
10989 let FfnKind::Moe(m) = &lw.ffn else {
10990 return Out::Declined;
10991 };
10992 let AttnKind::Full {
10993 wq,
10994 wk,
10995 wv,
10996 wo,
10997 q_norm,
10998 k_norm,
10999 output_gate,
11000 softplus_gate,
11001 bias,
11002 } = &lw.attn
11003 else {
11004 return Out::Declined;
11005 };
11006 if *output_gate || softplus_gate.is_some() {
11007 return Out::Declined;
11008 }
11009 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
11010 m.experts
11011 .first()?
11012 .gate_proj
11013 .mapped_q4tp()
11014 .map(|(m, _)| m.clone())
11015 }) else {
11016 return Out::Declined;
11017 };
11018 fn gw<'a>(
11019 t: &'a QTensor,
11020 owner: &std::sync::Arc<cortiq_core::CmfModel>,
11021 ) -> Option<crate::gpu::GraphW<'a>> {
11022 if let Some((m, idx, kind, rs)) = t.graph_weight() {
11023 if m.uid() != owner.uid() || t.has_prism_contract() {
11024 return None;
11025 }
11026 return Some(crate::gpu::GraphW {
11027 idx,
11028 kind,
11029 row_scale: rs,
11030 data: &[],
11031 prism: crate::gpu::GraphPrismOp::None,
11032 affine: false,
11033 });
11034 }
11035 t.as_f32().map(|data| crate::gpu::GraphW {
11036 idx: 0,
11037 kind: 4,
11038 row_scale: &[],
11039 data,
11040 prism: crate::gpu::GraphPrismOp::None,
11041 affine: false,
11042 })
11043 }
11044 let (Some(q), Some(k), Some(v), Some(o)) = (
11045 gw(wq, &model),
11046 gw(wk, &model),
11047 gw(wv, &model),
11048 gw(wo, &model),
11049 ) else {
11050 return Out::Declined;
11051 };
11052 let layer = crate::gpu::GraphLayer {
11053 input_norm: &lw.input_norm,
11054 post_norm: &lw.post_norm,
11055 attn_out_norm: None,
11056 ffn_out_norm: None,
11057 layer_scale: None,
11058 ffn: crate::gpu::GraphFfn::AttentionOnly,
11059 attn: crate::gpu::GraphAttn::Full {
11060 wq: q,
11061 wk: k,
11062 wv: v,
11063 wo: o,
11064 q_norm: q_norm.as_deref(),
11065 k_norm: k_norm.as_deref(),
11066 late_qk_norm: self.qk_norm_after_rope,
11067 bias: bias
11068 .as_ref()
11069 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11070 output_gate: false,
11071 cpu_k: self.kv_cache.layers[li].k_heads(),
11072 cpu_v: self.kv_cache.layers[li].v_heads(),
11073 cpu_base: self.kv_cache.layers[li].base(),
11074 geom: self.graph_attn_geom(li),
11075 head_gate: None,
11076 },
11077 };
11078 let (nkv, hd, rd) = self.layer_geom(li);
11079 crate::gpu::forward_batch_graph_at(
11080 &model,
11081 self.graph_kv_id,
11082 li,
11083 &[layer],
11084 &self.inv_freq,
11085 h,
11086 self.layer_num_heads(li),
11087 nkv,
11088 hd,
11089 rd,
11090 self.hidden_size,
11091 1,
11092 positions,
11093 self.kv_cache.max_seq_len,
11094 self.norm_style == cortiq_core::NormStyle::Gemma,
11095 self.rms_eps as f32,
11096 self.attn_scale,
11097 b,
11098 &[],
11099 self.o1_epoch,
11100 None,
11101 None,
11102 )
11103 };
11104 match outcome {
11105 Out::Completed => {}
11106 Out::Declined => return Out::Declined,
11107 Out::Failed => {
11108 self.graph_failed
11109 .store(true, std::sync::atomic::Ordering::Relaxed);
11110 return Out::Failed;
11111 }
11112 }
11113 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11114 let hs = self.hidden_size;
11115 let lw = &self.weights.layers[li];
11116 let FfnKind::Moe(m) = &lw.ffn else {
11117 unreachable!()
11118 };
11119 let mut post = vec![0.0; h.len()];
11120 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11121 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11122 }
11123 let mut ffn = if b == 1 {
11124 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11125 } else {
11126 moe_ffn_banked_rows(
11127 &mut self.mimo_moe,
11128 li,
11129 m,
11130 &post,
11131 b,
11132 hs,
11133 self.pool.as_deref(),
11134 )
11135 };
11136 for (x, &f) in h.iter_mut().zip(&ffn) {
11137 *x += f;
11138 }
11139 attention::recycle_buf(&mut ffn);
11140 if self.layer_dump.is_some() {
11141 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11142 self.dump_layer_row(pos, li, row);
11143 }
11144 }
11145 crate::mimo_moe::note_attention_graph(b, attn_ns);
11146 Out::Completed
11147 }
11148
11149 fn layer_attn_plain(&self, li: usize) -> bool {
11150 self.kv_heads_per_layer.is_none()
11151 && self.v_head_dim.is_none()
11152 && self.global_attn.is_none()
11153 && self.layer_window(li).is_none()
11154 && self.kv_cache.layers[li].sinks.is_none()
11155 }
11156
11157 fn forward_layers(
11159 &mut self,
11160 hidden: &[f32],
11161 position: usize,
11162 task_mask: Option<&TaskMask>,
11163 ) -> Vec<f32> {
11164 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11165 self.o1_progress();
11166 self.swa_trim_tails();
11167 out
11168 }
11169
11170 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11178 self.embed_single(id)
11179 }
11180
11181 pub fn split_supported(&self) -> Result<(), String> {
11185 if self.dsv4.is_some() {
11186 return Err(
11187 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11188 );
11189 }
11190 if self.dsv41.is_some() {
11191 return Err(
11192 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11193 .into(),
11194 );
11195 }
11196 if self.qwen4_exp.is_some() {
11197 return Err(
11198 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11199 );
11200 }
11201 if self.g3n.is_some() {
11202 return Err(
11203 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11204 );
11205 }
11206 Ok(())
11207 }
11208
11209 pub fn forward_span(
11214 &mut self,
11215 hidden: &[f32],
11216 position: usize,
11217 from: usize,
11218 upto: usize,
11219 task_mask: Option<&TaskMask>,
11220 ) -> Result<Vec<f32>, String> {
11221 self.split_supported()?;
11222 if from > upto || upto >= self.num_layers {
11223 return Err(format!(
11224 "forward_span: layer range {from}..={upto} outside 0..{}",
11225 self.num_layers
11226 ));
11227 }
11228 if hidden.len() != self.hidden_size {
11229 return Err(format!(
11230 "forward_span: hidden len {} ≠ hidden_size {}",
11231 hidden.len(),
11232 self.hidden_size
11233 ));
11234 }
11235 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11236 self.o1_progress();
11237 self.swa_trim_tails();
11238 if self
11239 .graph_failed
11240 .swap(false, std::sync::atomic::Ordering::Relaxed)
11241 {
11242 self.cancel
11243 .store(false, std::sync::atomic::Ordering::Relaxed);
11244 self.clear_sequence_state();
11245 return Err("forward_span: deferred O(1) transition failed".into());
11246 }
11247 Ok(out)
11248 }
11249
11250 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11253 let normed = inference::rms_norm(
11254 hidden,
11255 &self.weights.final_norm,
11256 self.rms_eps,
11257 self.norm_style,
11258 );
11259 self.lm_head_forward(&normed)
11260 }
11261
11262 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11264 sampler::sample_with_scratch(
11265 logits,
11266 &self.sampler_config,
11267 past_tokens,
11268 &mut self.rng,
11269 &mut self.sampler_scratch,
11270 )
11271 }
11272
11273 pub fn reset_session(&mut self) {
11275 self.clear_sequence_state();
11276 }
11277
11278 pub fn prefill_span_ids(
11284 &mut self,
11285 ids: &[u32],
11286 start_pos: usize,
11287 upto: usize,
11288 task_mask: Option<&TaskMask>,
11289 ) -> Result<Vec<f32>, String> {
11290 self.split_supported()?;
11291 if upto >= self.num_layers {
11292 return Err(format!(
11293 "prefill_span_ids: upto {upto} outside 0..{}",
11294 self.num_layers
11295 ));
11296 }
11297 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11301 let out =
11302 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11303 self.check_o1_progress_failure("prefill_span_ids")?;
11304 Ok(out)
11305 } else {
11306 let hs = self.hidden_size;
11307 let mut out = Vec::with_capacity(ids.len() * hs);
11308 for (i, &id) in ids.iter().enumerate() {
11309 let emb = self.embed_id(id);
11310 out.extend_from_slice(&self.forward_span(
11311 &emb,
11312 start_pos + i,
11313 0,
11314 upto,
11315 task_mask,
11316 )?);
11317 }
11318 Ok(out)
11319 }
11320 }
11321
11322 pub fn prefill_span_hidden(
11325 &mut self,
11326 hidden: &[f32],
11327 start_pos: usize,
11328 from: usize,
11329 upto: usize,
11330 task_mask: Option<&TaskMask>,
11331 ) -> Result<Vec<f32>, String> {
11332 self.split_supported()?;
11333 let hs = self.hidden_size;
11334 if hidden.is_empty() || hidden.len() % hs != 0 {
11335 return Err(format!(
11336 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11337 hidden.len()
11338 ));
11339 }
11340 if from > upto || upto >= self.num_layers {
11341 return Err(format!(
11342 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11343 self.num_layers
11344 ));
11345 }
11346 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11347 let out = self.prefill_batch_span(
11348 PrefillIn::Hidden(hidden),
11349 start_pos,
11350 task_mask,
11351 from,
11352 upto + 1,
11353 );
11354 self.check_o1_progress_failure("prefill_span_hidden")?;
11355 Ok(out)
11356 } else {
11357 let b = hidden.len() / hs;
11358 let mut out = Vec::with_capacity(hidden.len());
11359 for i in 0..b {
11360 let h = self.forward_span(
11361 &hidden[i * hs..(i + 1) * hs],
11362 start_pos + i,
11363 from,
11364 upto,
11365 task_mask,
11366 )?;
11367 out.extend_from_slice(&h);
11368 }
11369 Ok(out)
11370 }
11371 }
11372
11373 fn try_token_graph_wgpu(
11377 &self,
11378 hidden: &[f32],
11379 position: usize,
11380 logits_out: &mut Vec<f32>,
11381 layers_run: &mut usize,
11382 ) -> Option<Result<Vec<f32>, ()>> {
11383 self.try_token_graph_wgpu_steps(
11384 hidden,
11385 position,
11386 logits_out,
11387 1,
11388 None,
11389 Some(layers_run),
11390 0,
11391 self.num_layers,
11392 )
11393 }
11394
11395 fn try_token_graph_wgpu_span(
11399 &self,
11400 hidden: &[f32],
11401 position: usize,
11402 logits_out: &mut Vec<f32>,
11403 from: usize,
11404 upto_excl: usize,
11405 layers_run: &mut usize,
11406 ) -> Option<Result<Vec<f32>, ()>> {
11407 self.try_token_graph_wgpu_steps(
11408 hidden,
11409 position,
11410 logits_out,
11411 1,
11412 None,
11413 Some(layers_run),
11414 from,
11415 upto_excl,
11416 )
11417 }
11418
11419 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11423 if self.o1_active() || self.attn_softcap > 0.0 {
11424 return None;
11425 }
11426 if let Some(reason) = self.wgpu_graph_attn_decline() {
11429 self.note_graph_decline("wgpu multi-burst", reason);
11430 return None;
11431 }
11432 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11433 if !graph_on || self.graph_refused() {
11434 return None;
11441 }
11442 let emb = self.embed_single(t_next);
11443 let mut lg = Vec::new();
11444 let mut ids = Vec::new();
11445 match self.try_token_graph_wgpu_steps(
11446 &emb,
11447 position,
11448 &mut lg,
11449 k,
11450 Some(&mut ids),
11451 None,
11452 0,
11453 self.num_layers,
11454 ) {
11455 Some(Ok(_)) => {}
11456 Some(Err(())) => {
11457 self.graph_failed
11462 .store(true, std::sync::atomic::Ordering::Relaxed);
11463 return None;
11464 }
11465 None => return None,
11466 }
11467 (ids.len() == k).then_some(ids)
11468 }
11469
11470 fn try_token_graph_wgpu_steps(
11474 &self,
11475 hidden: &[f32],
11476 position: usize,
11477 logits_out: &mut Vec<f32>,
11478 steps: usize,
11479 ids_out: Option<&mut Vec<u32>>,
11480 layers_run: Option<&mut usize>,
11481 from: usize,
11482 upto_excl: usize,
11483 ) -> Option<Result<Vec<f32>, ()>> {
11484 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11487 Some(end) if end < upto_excl => {
11488 if steps != 1 || layers_run.is_none() || from >= end {
11489 return None;
11490 }
11491 end
11492 }
11493 _ => upto_excl,
11494 };
11495 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11498 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11499 return None;
11503 }
11504 if let Some(reason) = self.wgpu_graph_attn_decline() {
11511 self.note_graph_decline("wgpu token graph", reason);
11512 return None;
11513 }
11514 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11519 .map(|li| {
11520 if !o1_gpu {
11521 return None;
11522 }
11523 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11524 })
11525 .collect();
11526 if self.o1_active() && o1_gpu {
11527 let want: usize = (from..upto_excl)
11530 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11531 .count();
11532 let have = o1_views.iter().filter(|v| v.is_some()).count();
11533 if want == 0 || have != want {
11534 use std::sync::atomic::{AtomicUsize, Ordering};
11544 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11545 let code = have * 1000 + want;
11546 if LAST.swap(code, Ordering::Relaxed) != code {
11547 tracing::warn!(
11548 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11549 );
11550 }
11551 return None;
11552 }
11553 }
11554 let nh = self.num_heads;
11555 let (nkv, hd, rd) = self.layer_geom(0);
11556 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11557 let mut layers = Vec::with_capacity(upto_excl - from);
11558 let mut model = None;
11559 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11560 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11561 if let Some((m, i, kind, rs)) = t
11562 .graph_weight()
11563 .or_else(|| t.graph_weight_descriptor())
11564 {
11565 let name = &m.tensors[i].name;
11566 let prism = if crate::prism::is_inverse_embedding(m, name) {
11567 crate::gpu::GraphPrismOp::InverseEmbedding
11568 } else if crate::prism::is_forward_weight(m, name) {
11569 crate::gpu::GraphPrismOp::Forward
11570 } else {
11571 crate::gpu::GraphPrismOp::None
11572 };
11573 return Some(crate::gpu::GraphW {
11574 idx: i,
11575 kind,
11576 row_scale: rs,
11577 data: &[],
11578 prism,
11579 affine: crate::prism::is_affine_target(m, name),
11580 });
11581 }
11582 match t.as_f32() {
11584 Some(d) => Some(crate::gpu::GraphW {
11585 idx: 0,
11586 kind: 4,
11587 row_scale: &[],
11588 data: d,
11589 prism: crate::gpu::GraphPrismOp::None,
11590 affine: false,
11591 }),
11592 None => {
11593 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11594 eprintln!("batch graph: weight has no graph/f32 representation");
11595 }
11596 None
11597 }
11598 }
11599 }
11600 for li in from..upto_excl {
11601 let lw = &self.weights.layers[self.phys_layer(li)];
11602 if dbg {
11603 let ak = match &lw.attn {
11604 AttnKind::Mla(_) => "Mla".into(),
11605 AttnKind::Full {
11606 output_gate, bias, ..
11607 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11608 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11609 AttnKind::Kda(_) => "Kda".into(),
11610 AttnKind::Linear(_) => "Linear".into(),
11611 AttnKind::ShortConv(_) => "ShortConv".into(),
11612 AttnKind::Bounded(_) => "Bounded".into(),
11613 };
11614 let fk = match &lw.ffn {
11615 FfnKind::Dense(_) => "Dense",
11616 FfnKind::Moe(_) => "Moe",
11617 FfnKind::DenseMoe(_) => "DenseMoe",
11618 };
11619 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11620 }
11621 let gffn = match &lw.ffn {
11622 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11626 FfnKind::Dense(d) => {
11627 let Some(act) = d.act.graph_act() else {
11631 self.note_graph_decline(
11632 "wgpu token graph",
11633 "dense FFN activation without a graph kernel",
11634 );
11635 return None;
11636 };
11637 crate::gpu::GraphFfn::Dense {
11638 gate: gw(&d.gate_proj)?,
11639 up: gw(&d.up_proj)?,
11640 down: gw(&d.down_proj)?,
11641 act,
11642 }
11643 }
11644 FfnKind::Moe(m) => {
11645 if m.route_tau.is_some() || m.mask.is_some() {
11653 return None;
11654 }
11655 let shared = m.shared.as_ref();
11656 let has_shared = shared.is_some();
11657 let shared_gated = matches!(shared, Some((_, Some(_))));
11658 let sgate = match shared {
11659 Some((_, Some(sg))) => gw(sg)?,
11660 _ => gw(&m.router)?,
11664 };
11665 let router = gw(&m.router)?;
11666 if router.prism != crate::gpu::GraphPrismOp::None
11672 || sgate.prism != crate::gpu::GraphPrismOp::None
11673 || router.affine
11674 || sgate.affine
11675 {
11676 tracing::warn!(
11677 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11678 );
11679 return None;
11680 }
11681 let inter = m.experts.first()?.gate_proj.rows();
11682 let mut experts = Vec::with_capacity(m.experts.len() + 1);
11683 let mut q4tp: Option<bool> = None;
11686 let mut gu_q2: Option<bool> = None;
11689 let mut q82: Option<bool> = None;
11691 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
11692 if !matches!(e.act, Act::Silu)
11693 || e.gate_proj.rows() != inter
11694 || e.up_proj.rows() != inter
11695 {
11696 return None;
11697 }
11698 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
11703 let Some((em, ei, _, _)) = expert_weight
11704 .graph_weight()
11705 .or_else(|| expert_weight.graph_weight_descriptor())
11706 else {
11707 return None;
11708 };
11709 let name = &em.tensors[ei].name;
11710 if crate::prism::is_forward_weight(em, name)
11711 || crate::prism::is_inverse_embedding(em, name)
11712 || crate::prism::is_affine_target(em, name)
11713 {
11714 tracing::warn!(
11715 "resident MoE declined: expert Prism/affine transform is not implemented"
11716 );
11717 return None;
11718 }
11719 }
11720 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
11721 Some((mm, gi)) => (
11722 mm,
11723 gi,
11724 e.up_proj.mapped_q4t()?.1,
11725 e.down_proj.mapped_q4t()?.1,
11726 false,
11727 false,
11728 false,
11729 ),
11730 None => match e.gate_proj.mapped_q2tp() {
11731 Some((mm, gi)) => (
11732 mm,
11733 gi,
11734 e.up_proj.mapped_q2tp()?.1,
11735 e.down_proj.mapped_q4tp()?.1,
11736 true,
11737 true,
11738 false,
11739 ),
11740 None => match e.gate_proj.mapped_q4tp() {
11741 Some((mm, gi)) => (
11742 mm,
11743 gi,
11744 e.up_proj.mapped_q4tp()?.1,
11745 e.down_proj.mapped_q4tp()?.1,
11746 true,
11747 false,
11748 false,
11749 ),
11750 None => {
11753 if !crate::gpu::moe_q82_graph_on() {
11754 return None;
11755 }
11756 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
11757 (
11758 mm,
11759 gi,
11760 e.up_proj.mapped_q8_2f()?.1,
11761 e.down_proj.mapped_q8_2f()?.1,
11762 false,
11763 false,
11764 true,
11765 )
11766 }
11767 },
11768 },
11769 };
11770 if *q4tp.get_or_insert(is_p) != is_p
11771 || *gu_q2.get_or_insert(is_q2) != is_q2
11772 || *q82.get_or_insert(is_8) != is_8
11773 {
11774 tracing::warn!(
11780 "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."
11781 );
11782 return None;
11783 }
11784 model.get_or_insert_with(|| mm.clone());
11785 experts.push((gi, ui, di));
11786 }
11787 crate::gpu::GraphFfn::Moe {
11788 router,
11789 shared_gate: sgate,
11790 experts,
11791 n_exp: m.experts.len(),
11792 top_k: std::env::var("CMF_TOPK_PROBE")
11798 .ok()
11799 .and_then(|v| v.parse::<usize>().ok())
11800 .filter(|k| *k > 0 && *k <= m.top_k)
11801 .unwrap_or(m.top_k),
11802 inter,
11803 norm_topk: m.norm_topk_prob,
11804 q4tp: q4tp?,
11805 gu_q2: gu_q2.unwrap_or(false),
11806 q82: q82.unwrap_or(false),
11807 sigmoid: m.router_sigmoid,
11808 bias: m.expert_bias.as_deref(),
11809 has_shared,
11810 shared_gated,
11811 route_scale: m.routed_scaling,
11812 }
11813 }
11814 };
11815 let attn = match &lw.attn {
11816 AttnKind::Full {
11817 wq,
11818 wk,
11819 wv,
11820 wo,
11821 q_norm,
11822 k_norm,
11823 output_gate,
11824 softplus_gate,
11825 bias,
11826 } => {
11827 if self.attention_heads_per_layer.is_some() {
11828 return None;
11829 }
11830 let head_gate = match softplus_gate {
11834 None => None,
11835 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
11836 Some(gw(g)?)
11837 }
11838 Some(_) => {
11839 self.note_graph_decline(
11840 "wgpu token graph",
11841 "projected softplus / per-element output gate",
11842 );
11843 return None;
11844 }
11845 };
11846 let (m, _, _, _) = wq
11847 .graph_weight()
11848 .or_else(|| wq.graph_weight_descriptor())?;
11849 model = Some(m.clone());
11850 crate::gpu::GraphAttn::Full {
11851 wq: gw(wq)?,
11852 wk: gw(wk)?,
11853 wv: gw(wv)?,
11854 wo: gw(wo)?,
11855 q_norm: q_norm.as_deref(),
11856 k_norm: k_norm.as_deref(),
11857 late_qk_norm: self.qk_norm_after_rope,
11858 bias: bias
11859 .as_ref()
11860 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11861 output_gate: *output_gate,
11862 cpu_k: self.kv_cache.layers[li].k_heads(),
11863 cpu_v: self.kv_cache.layers[li].v_heads(),
11864 cpu_base: self.kv_cache.layers[li].base(),
11865 geom: self.graph_attn_geom(li),
11866 head_gate,
11867 }
11868 }
11869 AttnKind::LinearGdn(w) => {
11870 let cfg = self.gdn_cfg?;
11871 let (m, _, _, _) = w
11872 .in_proj_qkv
11873 .graph_weight()
11874 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
11875 model = Some(m.clone());
11876 crate::gpu::GraphAttn::Gdn {
11877 qkv: gw(&w.in_proj_qkv)?,
11878 z: gw(&w.in_proj_z)?,
11879 a: gw(&w.in_proj_a)?,
11880 b: gw(&w.in_proj_b)?,
11881 out: gw(&w.out_proj)?,
11882 conv1d: &w.conv1d,
11883 a_log: &w.a_log,
11884 dt_bias: &w.dt_bias,
11885 norm: &w.norm,
11886 nv: cfg.num_v_heads,
11887 nk: cfg.num_k_heads,
11888 dk: cfg.key_head_dim,
11889 dv: cfg.value_head_dim,
11890 kk: cfg.conv_kernel,
11891 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11892 }
11893 }
11894 AttnKind::ShortConv(w) => {
11895 let cfg = self.short_conv_cfg?;
11896 let (m, _, _, _) = w
11897 .in_proj
11898 .graph_weight()
11899 .or_else(|| w.in_proj.graph_weight_descriptor())?;
11900 model = Some(m.clone());
11901 crate::gpu::GraphAttn::ShortConv {
11902 inp: gw(&w.in_proj)?,
11903 out: gw(&w.out_proj)?,
11904 taps: &w.conv,
11905 kernel: cfg.kernel,
11906 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11907 }
11908 }
11909 _ => return None,
11910 };
11911 layers.push(crate::gpu::GraphLayer {
11912 input_norm: &lw.input_norm,
11913 attn,
11914 post_norm: &lw.post_norm,
11915 attn_out_norm: lw.attn_out_norm.as_deref(),
11916 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11917 layer_scale: lw.layer_scale,
11918 ffn: gffn,
11919 });
11920 }
11921 let model = model?;
11922 let lm_gw = if upto_excl == self.num_layers
11928 && self.graph_want_logits
11929 && std::env::var("CMF_GPU_LMHEAD")
11930 .map(|v| v != "0")
11931 .unwrap_or(true)
11932 {
11933 self.weights
11934 .lm_head
11935 .graph_weight()
11936 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
11937 .map(|(m, i, kind, rs)| {
11938 let name = &m.tensors[i].name;
11939 let prism = if crate::prism::is_inverse_embedding(m, name) {
11940 crate::gpu::GraphPrismOp::InverseEmbedding
11941 } else if crate::prism::is_forward_weight(m, name) {
11942 crate::gpu::GraphPrismOp::Forward
11943 } else {
11944 crate::gpu::GraphPrismOp::None
11945 };
11946 (
11947 crate::gpu::GraphW {
11948 idx: i,
11949 kind,
11950 row_scale: rs,
11951 data: &[],
11952 prism,
11953 affine: crate::prism::is_affine_target(m, name),
11954 },
11955 self.weights.lm_head.rows(),
11956 )
11957 })
11958 } else {
11959 None
11960 };
11961 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
11962 let emb_gw = if steps > 1 {
11964 self.weights
11965 .embed_tokens
11966 .graph_weight()
11967 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
11968 .map(|(m, i, kind, rs)| {
11969 let name = &m.tensors[i].name;
11970 let prism = if crate::prism::is_inverse_embedding(m, name) {
11971 crate::gpu::GraphPrismOp::InverseEmbedding
11972 } else if crate::prism::is_forward_weight(m, name) {
11973 crate::gpu::GraphPrismOp::Forward
11974 } else {
11975 crate::gpu::GraphPrismOp::None
11976 };
11977 (
11978 crate::gpu::GraphW {
11979 idx: i,
11980 kind,
11981 row_scale: rs,
11982 data: &[],
11983 prism,
11984 affine: crate::prism::is_affine_target(m, name),
11985 },
11986 self.weights.embed_tokens.rows(),
11987 self.embed_multiplier,
11988 )
11989 })
11990 } else {
11991 None
11992 };
11993
11994 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
12000 (from..upto_excl.min(self.num_layers - 1))
12001 .filter(|&li| (li + 1) % self.physical_layers == 0)
12002 .map(|li| li - from)
12003 .collect()
12004 } else {
12005 Vec::new()
12006 };
12007 let mut h = hidden.to_vec();
12008 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
12014 let outcome = crate::gpu::forward_token_graph(
12015 &model,
12016 self.graph_kv_id,
12017 &layers,
12018 &o1_views,
12019 self.o1_epoch,
12020 &self.inv_freq,
12021 &mut h,
12022 nh,
12023 nkv,
12024 hd,
12025 self.attn_scale,
12026 rd,
12027 self.hidden_size,
12028 self.intermediate_size,
12029 position,
12030 self.kv_cache.max_seq_len,
12031 gemma,
12032 self.rms_eps as f32,
12033 lm,
12034 &self.weights.final_norm,
12035 logits_out,
12036 &loop_norm_at,
12037 steps,
12038 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12039 ids_out,
12040 layers_run,
12041 from,
12042 dump_hidden,
12043 );
12044 match outcome {
12045 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12046 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12047 crate::gpu::TokenGraphOutcome::Declined => None,
12048 }
12049 }
12050
12051 #[cfg(target_os = "macos")]
12060 #[allow(clippy::type_complexity)]
12061 fn metal_rows_plan(
12062 &self,
12063 ) -> Option<(
12064 Vec<MetalRowsItem<'_>>,
12065 std::sync::Arc<cortiq_core::CmfModel>,
12066 Option<crate::gpu_metal::GdnGpuCfg>,
12067 )> {
12068 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12069 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12070 if !graph_force
12071 || !crate::gpu::enabled_here()
12072 || std::env::var("CMF_GPU_BLOCK")
12073 .map(|v| v == "0")
12074 .unwrap_or(false)
12075 || self.attn_softcap > 0.0
12076 || self.o1_active()
12077 || self.swa.is_some()
12078 || self.global_attn.is_some()
12079 || self.attention_heads_per_layer.is_some()
12080 || self.graph_attn_decline_reason().is_some()
12082 || self.attn_v_norm
12083 || self.loop_final_norm
12084 || !self.metal_rope_scale_ok()
12085 {
12086 return None;
12087 }
12088 let attend_contract = self.head_dim % 4 == 0
12089 && self.head_dim <= 256
12090 && self.rotary_dim >= 2
12091 && self.rotary_dim <= self.head_dim
12092 && (self.rotary_dim / 2) % 32 == 0
12093 && self.num_kv_heads > 0
12094 && self.num_heads % self.num_kv_heads == 0;
12095 if !attend_contract {
12096 return None;
12097 }
12098 let mut plan: Vec<MetalRowsItem> = Vec::new();
12099 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12100 for li in 0..self.num_layers {
12101 let lw = &self.weights.layers[self.phys_layer(li)];
12102 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12103 return None;
12104 }
12105 let ffn = match &lw.ffn {
12106 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12107 let (Some(g), Some(u), Some(dn)) = (
12108 d.gate_proj.metal_graph_parts(),
12109 d.up_proj.metal_graph_parts(),
12110 d.down_proj.metal_graph_parts(),
12111 ) else {
12112 return None;
12113 };
12114 MetalFfn::Dense {
12115 gate: g,
12116 up: u,
12117 down: dn,
12118 act: 0, }
12120 }
12121 _ => return None,
12122 };
12123 match &lw.attn {
12124 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12125 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12126 w.in_proj_qkv.metal_graph_parts(),
12127 w.in_proj_z.metal_graph_parts(),
12128 w.in_proj_a.f32_parts(),
12129 w.in_proj_b.f32_parts(),
12130 w.out_proj.metal_graph_parts(),
12131 ) else {
12132 return None;
12133 };
12134 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12135 model_ref.get_or_insert_with(|| model.clone());
12136 }
12137 let gl = GdnGpuLayer {
12138 attn_norm: &lw.input_norm,
12139 post_norm: &lw.post_norm,
12140 qkv,
12141 z,
12142 a,
12143 b: bb,
12144 out,
12145 ffn,
12146 conv1d: &w.conv1d,
12147 a_log: &w.a_log,
12148 dt_bias: &w.dt_bias,
12149 gnorm: &w.norm,
12150 };
12151 match plan.last_mut() {
12152 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12153 _ => plan.push(MetalRowsItem::Gdn {
12154 run: vec![gl],
12155 first: li,
12156 }),
12157 }
12158 }
12159 AttnKind::Full {
12160 wq,
12161 wk,
12162 wv,
12163 wo,
12164 q_norm,
12165 k_norm,
12166 output_gate,
12167 softplus_gate: None,
12168 bias: None,
12169 } => {
12170 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12171 (
12172 wq.metal_graph_parts(),
12173 wk.metal_graph_parts(),
12174 wv.metal_graph_parts(),
12175 wo.metal_graph_parts(),
12176 )
12177 else {
12178 return None;
12179 };
12180 if let QTensor::Mapped { model, .. } = wq {
12181 model_ref.get_or_insert_with(|| model.clone());
12182 }
12183 let cache = &self.kv_cache.layers[li];
12184 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12185 return None;
12186 }
12187 plan.push(MetalRowsItem::Attn {
12188 l: AttnGpuLayer {
12189 attn_norm: &lw.input_norm,
12190 post_norm: &lw.post_norm,
12191 wq: pq,
12192 wk: pk,
12193 wv: pv,
12194 wo: po,
12195 ffn,
12196 attn_out_norm: None,
12197 ffn_out_norm: None,
12198 layer_scale: None,
12199 },
12200 li,
12201 q_norm: q_norm.as_deref(),
12202 k_norm: k_norm.as_deref(),
12203 output_gate: *output_gate,
12204 });
12205 }
12206 _ => return None,
12207 }
12208 }
12209 let model = model_ref?;
12210 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12211 nv: cfg.num_v_heads,
12212 nk: cfg.num_k_heads,
12213 dk: cfg.key_head_dim,
12214 dv: cfg.value_head_dim,
12215 kk: cfg.conv_kernel,
12216 hidden: self.hidden_size,
12217 inter: self.intermediate_size,
12218 c_dim: cfg.conv_dim(),
12219 eps: cfg.rms_eps as f32,
12220 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12221 });
12222 Some((plan, model, gcfg))
12223 }
12224
12225 #[cfg(target_os = "macos")]
12227 #[allow(clippy::too_many_arguments)]
12228 fn metal_attn_params<'a>(
12229 li: usize,
12230 cache: &'a crate::kv_cache::LayerKvCache,
12231 q_norm: Option<&'a [f32]>,
12232 k_norm: Option<&'a [f32]>,
12233 output_gate: bool,
12234 inv_freq: &'a [f32],
12235 rope_scale: f32,
12236 geom: (usize, usize, usize, usize),
12237 pos0: usize,
12238 kv_id: u64,
12239 scale: f32,
12240 eps: f32,
12241 gemma: bool,
12242 late_qk_norm: bool,
12243 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12244 let (nh, nkv, hd, rd) = geom;
12245 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12246 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12247 let cpu_stored = cpu_k[0].len() / hd;
12248 (
12249 crate::gpu_metal::AttnDeviceParams {
12250 kv_id,
12251 layer: li,
12252 nh,
12253 nkv,
12254 hd,
12255 rd,
12256 position: pos0,
12257 scale,
12258 eps,
12259 gemma,
12260 late_qk_norm,
12261 output_gate,
12262 q_norm,
12263 k_norm,
12264 inv_freq,
12265 rope_scale,
12266 cpu_k,
12267 cpu_v,
12268 cpu_stored,
12269 cpu_gen: cache.generation(),
12270 o1: None,
12271 window: None,
12272 head_gate: None,
12273 v_norm: false,
12274 },
12275 cpu_stored,
12276 )
12277 }
12278
12279 #[cfg(target_os = "macos")]
12284 #[allow(clippy::type_complexity)]
12285 fn metal_rows_run(
12286 &mut self,
12287 hiddens: &mut [f32],
12288 pos0: usize,
12289 b: usize,
12290 prefill: bool,
12291 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12292 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12296 ) -> MetalRowsRun {
12297 use crate::gpu_metal::{GraphDims, VerifyGraph};
12298 if !crate::gpu_metal::wait_replay() {
12304 tracing::error!("Metal rows graph: the pending async replay failed");
12305 return MetalRowsRun::Failed;
12306 }
12307 spec_stamp("v.wait");
12308 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12314 if want > 0 {
12315 let phys = self.physical_layers.max(1);
12316 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12317 let is_gdn = self
12318 .weights
12319 .layers
12320 .get(li % phys)
12321 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12322 if is_gdn && l.linear_state.len() != want {
12323 l.linear_state = vec![0f32; want];
12324 }
12325 }
12326 }
12327 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12328 return MetalRowsRun::Declined;
12329 };
12330 spec_stamp("v.plan");
12331 let dims = GraphDims {
12332 hidden: self.hidden_size,
12333 eps: self.rms_eps as f32,
12334 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12335 };
12336 let Some(mut graph) = (if prefill {
12337 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12338 } else {
12339 VerifyGraph::new(&model, dims, hiddens, b)
12340 }) else {
12341 return MetalRowsRun::Declined;
12342 };
12343 let geom = (
12344 self.num_heads,
12345 self.num_kv_heads,
12346 self.head_dim,
12347 self.rotary_dim,
12348 );
12349 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12350 let eps = self.rms_eps as f32;
12351 let kv_id = self.graph_kv_id;
12352 let inv_freq = self.inv_freq.clone();
12353 for item in &plan {
12354 let ok = match item {
12355 MetalRowsItem::Gdn { run, .. } => gcfg
12356 .as_ref()
12357 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12358 .unwrap_or(false),
12359 MetalRowsItem::Attn {
12360 l,
12361 li,
12362 q_norm,
12363 k_norm,
12364 output_gate,
12365 } => {
12366 let (p, _) = Self::metal_attn_params(
12367 *li,
12368 &self.kv_cache.layers[*li],
12369 *q_norm,
12370 *k_norm,
12371 *output_gate,
12372 &inv_freq,
12373 self.layer_rope_scale(*li),
12374 geom,
12375 pos0,
12376 kv_id,
12377 self.attn_scale,
12378 eps,
12379 gemma,
12380 self.qk_norm_after_rope,
12381 );
12382 graph.attn_ok(l, &p)
12383 }
12384 };
12385 if !ok {
12386 use std::sync::atomic::{AtomicBool, Ordering};
12387 static SAID: AtomicBool = AtomicBool::new(false);
12388 if !SAID.swap(true, Ordering::Relaxed) {
12389 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12390 }
12391 return MetalRowsRun::Declined;
12392 }
12393 }
12394 let lm = match &spec {
12395 Some((lm, _, _)) => {
12396 if !graph.lm_head_ok(*lm) {
12397 return MetalRowsRun::Declined;
12398 }
12399 Some(*lm)
12400 }
12401 None => None,
12402 };
12403 let mut gdn_layers = Vec::new();
12404 let mut attn_layers = Vec::new();
12405 for item in &plan {
12406 match item {
12407 MetalRowsItem::Gdn { run, first } => {
12408 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12409 .iter()
12410 .map(|l| l.linear_state.as_slice())
12411 .collect();
12412 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12413 return MetalRowsRun::Declined;
12414 }
12415 gdn_layers.extend(*first..*first + run.len());
12416 }
12417 MetalRowsItem::Attn {
12418 l,
12419 li,
12420 q_norm,
12421 k_norm,
12422 output_gate,
12423 } => {
12424 let (p, cpu_stored) = Self::metal_attn_params(
12425 *li,
12426 &self.kv_cache.layers[*li],
12427 *q_norm,
12428 *k_norm,
12429 *output_gate,
12430 &inv_freq,
12431 self.layer_rope_scale(*li),
12432 geom,
12433 pos0,
12434 kv_id,
12435 self.attn_scale,
12436 eps,
12437 gemma,
12438 self.qk_norm_after_rope,
12439 );
12440 if !graph.encode_attn_b(l, &p) {
12441 return MetalRowsRun::Declined;
12442 }
12443 attn_layers.push((*li, cpu_stored));
12444 }
12445 }
12446 }
12447 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12448 if !graph.encode_lm_head_b(final_norm, lm) {
12449 return MetalRowsRun::Declined;
12450 }
12451 if let Some((n, _)) = argmax_out.as_ref() {
12456 if !graph.encode_argmax_b(*n) {
12457 argmax_out = None;
12458 }
12459 }
12460 }
12461 spec_stamp("v.enc");
12462 if !graph.sync() {
12463 return MetalRowsRun::Failed;
12464 }
12465 spec_stamp("v.gpu");
12466 match (spec, argmax_out) {
12467 (Some(_), Some((_, ids))) => {
12468 ids.resize(b, 0);
12469 if !graph.read_argmax(ids) {
12470 return MetalRowsRun::Failed;
12471 }
12472 spec_stamp("v.am");
12473 }
12474 (Some((lm, _, logits)), None) => {
12475 logits.resize(b * lm.1, 0.0);
12476 if !graph.read_logits(logits) {
12477 return MetalRowsRun::Failed;
12478 }
12479 spec_stamp("v.lg");
12480 }
12481 (None, _) => {}
12482 }
12483 if !graph.read_hidden(hiddens) {
12484 return MetalRowsRun::Failed;
12485 }
12486 spec_stamp("v.hid");
12487 MetalRowsRun::Completed(MetalVerifyPending {
12488 graph,
12489 gdn_layers,
12490 attn_layers,
12491 })
12492 }
12493
12494 #[cfg(target_os = "macos")]
12500 fn try_batch_graph_metal(
12501 &mut self,
12502 hiddens: &mut [f32],
12503 positions: &[usize],
12504 b: usize,
12505 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12506 argmax_out: Option<(usize, &mut Vec<u32>)>,
12507 ) -> crate::gpu::BatchGraphOutcome {
12508 let _t0 = std::time::Instant::now();
12509 if positions.len() != b
12510 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12511 || hiddens.len() != b * self.hidden_size
12512 {
12513 return crate::gpu::BatchGraphOutcome::Declined;
12514 }
12515 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12516 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12517 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12518 MetalRowsRun::Completed(pending) => pending,
12519 };
12520 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12521 eprintln!(
12522 "metal-verify: {:.1} ms | b={b}",
12523 _t0.elapsed().as_secs_f64() * 1e3
12524 );
12525 }
12526 self.metal_verify = Some(pending);
12527 crate::gpu::BatchGraphOutcome::Completed
12528 }
12529
12530 #[cfg(target_os = "macos")]
12535 fn prefill_rows_metal(
12536 &mut self,
12537 ids: &[u32],
12538 start_pos: usize,
12539 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12540 ) -> MetalPrefillOutcome {
12541 let b = ids.len();
12542 if b == 0 || b > 512 {
12543 return MetalPrefillOutcome::Declined;
12544 }
12545 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12546 let with_head = spec.is_some();
12547 let hs = self.hidden_size;
12548 let mut hiddens = vec![0f32; b * hs];
12549 for (j, &id) in ids.iter().enumerate() {
12550 let e = self.embed_single(id);
12551 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12552 }
12553 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12554 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12555 MetalRowsRun::Failed => {
12556 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12557 return MetalPrefillOutcome::Failed;
12558 }
12559 MetalRowsRun::Completed(pending) => pending,
12560 };
12561 let idxs = pending.gdn_layers.clone();
12563 let mut outs: Vec<&mut [f32]> = self
12564 .kv_cache
12565 .layers
12566 .iter_mut()
12567 .enumerate()
12568 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12569 .map(|(_, l)| l.linear_state.as_mut_slice())
12570 .collect();
12571 if !pending.graph.finish_states(&mut outs) {
12572 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12573 return MetalPrefillOutcome::Failed;
12574 }
12575 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12576 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12580 for (li, cpu_stored) in &pending.attn_layers {
12581 let mut kbuf = vec![0f32; b * nkv * hd];
12582 let mut vbuf = vec![0f32; b * nkv * hd];
12583 if !crate::gpu_metal::kv_mirror_read_rows(
12584 self.graph_kv_id,
12585 *li,
12586 nkv,
12587 hd,
12588 *cpu_stored,
12589 b,
12590 &mut kbuf,
12591 &mut vbuf,
12592 ) {
12593 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12594 return MetalPrefillOutcome::Failed;
12595 }
12596 rows.push((*li, *cpu_stored, kbuf, vbuf));
12597 }
12598 for (li, cpu_stored, kbuf, vbuf) in rows {
12599 let cache = &mut self.kv_cache.layers[li];
12600 for r in 0..b {
12601 cache.append(
12602 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12603 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12604 &[],
12605 );
12606 }
12607 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12608 }
12609 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12610 if with_head {
12611 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12612 }
12613 MetalPrefillOutcome::Completed(hiddens)
12614 }
12615
12616 #[cfg(target_os = "macos")]
12617 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12618 self.prefill_rows_metal(ids, start_pos, None)
12619 }
12620
12621 #[cfg(target_os = "macos")]
12626 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12627 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12628 return MetalBatchNllOutcome::Declined;
12629 }
12630 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12631 return MetalBatchNllOutcome::Declined;
12632 };
12633 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12634 .ok()
12635 .and_then(|v| v.parse::<usize>().ok())
12636 .filter(|&v| (1..=512).contains(&v))
12637 .unwrap_or(32);
12638 let final_norm = self.weights.final_norm.clone();
12639 let mut nll = 0.0f64;
12640 let mut count = 0usize;
12641 let mut pos = 0usize;
12642 let mut completed = 0usize;
12643 while pos < ids.len() {
12644 let end = (pos + chunk).min(ids.len());
12645 let mut logits = Vec::new();
12646 let outcome = self.prefill_rows_metal(
12647 &ids[pos..end],
12648 pos,
12649 Some((lm, &final_norm, &mut logits)),
12650 );
12651 match outcome {
12652 MetalPrefillOutcome::Declined => {
12653 return if completed == 0 {
12654 MetalBatchNllOutcome::Declined
12655 } else {
12656 MetalBatchNllOutcome::Failed(format!(
12657 "ordinary Metal NLL batch declined after {completed} chunks"
12658 ))
12659 };
12660 }
12661 MetalPrefillOutcome::Failed => {
12662 return MetalBatchNllOutcome::Failed(
12663 "ordinary Metal NLL batch failed after admission".to_string(),
12664 );
12665 }
12666 MetalPrefillOutcome::Completed(_) => {}
12667 }
12668 completed += 1;
12669 let vocab = self.vocab_size.min(lm.1);
12670 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12671 return MetalBatchNllOutcome::Failed(
12672 "ordinary Metal NLL head returned an invalid shape".to_string(),
12673 );
12674 }
12675 for row in 0..(end - pos) {
12676 let absolute = pos + row;
12677 if absolute < start || absolute + 1 >= ids.len() {
12678 continue;
12679 }
12680 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
12681 if let Some(mu) = self.logit_multiplier {
12682 for v in lg.iter_mut() {
12683 *v *= mu;
12684 }
12685 }
12686 if let Some(c) = self.final_softcap {
12687 softcap_in_place(lg, c, self.pool.as_deref());
12688 }
12689 let target = ids[absolute + 1] as usize;
12690 if target >= vocab {
12691 return MetalBatchNllOutcome::Failed(format!(
12692 "target token {target} exceeds Metal head rows {vocab}"
12693 ));
12694 }
12695 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
12696 let lse: f64 = lg
12697 .iter()
12698 .map(|&v| ((v - max) as f64).exp())
12699 .sum::<f64>()
12700 .ln()
12701 + max as f64;
12702 nll += lse - lg[target] as f64;
12703 count += 1;
12704 }
12705 pos = end;
12706 }
12707 MetalBatchNllOutcome::Completed(nll, count)
12708 }
12709
12710 #[cfg(target_os = "macos")]
12714 fn metal_verify_commit(&mut self, a: usize) -> bool {
12715 let Some(mut pending) = self.metal_verify.take() else {
12716 return false;
12717 };
12718 let n = a + 1;
12719 let idxs = pending.gdn_layers.clone();
12721 let mut outs: Vec<&mut [f32]> = self
12722 .kv_cache
12723 .layers
12724 .iter_mut()
12725 .enumerate()
12726 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12727 .map(|(_, l)| l.linear_state.as_mut_slice())
12728 .collect();
12729 if !pending.graph.commit(n, &mut outs) {
12730 return false;
12731 }
12732 spec_stamp("c.replay");
12733 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12734 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12738 for (li, cpu_stored) in &pending.attn_layers {
12739 let mut kbuf = vec![0f32; n * nkv * hd];
12740 let mut vbuf = vec![0f32; n * nkv * hd];
12741 if !crate::gpu_metal::kv_mirror_read_rows(
12742 self.graph_kv_id,
12743 *li,
12744 nkv,
12745 hd,
12746 *cpu_stored,
12747 n,
12748 &mut kbuf,
12749 &mut vbuf,
12750 ) {
12751 return false;
12752 }
12753 rows.push((*li, *cpu_stored, kbuf, vbuf));
12754 }
12755 for (li, cpu_stored, kbuf, vbuf) in rows {
12756 let cache = &mut self.kv_cache.layers[li];
12757 for r in 0..n {
12758 cache.append(
12759 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12760 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12761 &[],
12762 );
12763 }
12764 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
12765 }
12766 spec_stamp("c.kv");
12767 true
12768 }
12769
12770 #[cfg(target_os = "macos")]
12777 fn mtp_warm_batch_submit(
12778 &mut self,
12779 m: &mut MtpModule,
12780 pairs: &[(&[f32], u32)],
12781 first_pos: usize,
12782 ) -> Option<MetalWarmPending> {
12783 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
12784 let b = pairs.len();
12785 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
12786 return None;
12787 }
12788 let AttnKind::Full {
12789 wq,
12790 wk,
12791 wv,
12792 wo,
12793 q_norm,
12794 k_norm,
12795 output_gate,
12796 softplus_gate: None,
12797 bias: None,
12798 } = &m.layer.attn
12799 else {
12800 return None;
12801 };
12802 let FfnKind::Dense(d) = &m.layer.ffn else {
12803 return None;
12804 };
12805 if !d.segs.is_empty() {
12806 return None;
12807 }
12808 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12809 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
12810 else {
12811 return None;
12812 };
12813 let (Some(g), Some(u), Some(dn)) = (
12814 d.gate_proj.q1_parts(),
12815 d.up_proj.q1_parts(),
12816 d.down_proj.q1_parts(),
12817 ) else {
12818 return None;
12819 };
12820 let Some(eh) = m.eh_proj.q1_parts() else {
12821 return None;
12822 };
12823 let QTensor::Mapped { model, .. } = wq else {
12824 return None;
12825 };
12826 let model = model.clone();
12827 let hs = self.hidden_size;
12828 let mut cat = vec![0f32; b * 2 * hs];
12830 for (j, (h, tok)) in pairs.iter().enumerate() {
12831 let e = self.embed_single(*tok);
12832 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
12833 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
12834 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
12835 }
12836 let dims = GraphDims {
12837 hidden: hs,
12838 eps: self.rms_eps as f32,
12839 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12840 };
12841 spec_stamp("w.cat");
12842 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
12843 return None;
12844 };
12845 spec_stamp("w.new");
12846 let l = AttnGpuLayer {
12847 attn_norm: &m.layer.input_norm,
12848 post_norm: &m.layer.post_norm,
12849 wq: pq,
12850 wk: pk,
12851 wv: pv,
12852 wo: po,
12853 ffn: MetalFfn::Dense {
12854 gate: g,
12855 up: u,
12856 down: dn,
12857 act: (d.act == Act::Gelu) as u8,
12858 },
12859 attn_out_norm: None,
12860 ffn_out_norm: None,
12861 layer_scale: None,
12862 };
12863 let (nh, nkv, hd, rd) = (
12864 self.num_heads,
12865 self.num_kv_heads,
12866 self.head_dim,
12867 self.rotary_dim,
12868 );
12869 let inv_freq = self.inv_freq.clone();
12870 let cpu_stored;
12871 {
12872 let cache = &m.kv;
12873 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12874 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12875 cpu_stored = cpu_k[0].len() / hd;
12876 if cpu_stored > first_pos {
12881 spec_stamp("w.decl");
12882 return None;
12883 }
12884 let p = AttnDeviceParams {
12885 kv_id: self.mtp_kv_id(),
12886 layer: Self::MTP_LAYER_BASE,
12887 nh,
12888 nkv,
12889 hd,
12890 rd,
12891 position: first_pos,
12892 scale: self.attn_scale,
12893 eps: self.rms_eps as f32,
12894 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12895 late_qk_norm: self.qk_norm_after_rope,
12896 output_gate: *output_gate,
12897 q_norm: q_norm.as_deref(),
12898 k_norm: k_norm.as_deref(),
12899 inv_freq: &inv_freq,
12900 rope_scale: self.rope_scale,
12901 cpu_k,
12902 cpu_v,
12903 cpu_stored,
12904 cpu_gen: cache.generation(),
12905 o1: None,
12906 window: None,
12907 head_gate: None,
12908 v_norm: false,
12909 };
12910 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
12911 return None;
12912 }
12913 }
12914 spec_stamp("w.enc");
12915 if !graph.submit() {
12916 return None;
12917 }
12918 spec_stamp("w.sub");
12919 Some(MetalWarmPending {
12920 graph,
12921 cpu_stored,
12922 b,
12923 })
12924 }
12925
12926 #[cfg(target_os = "macos")]
12929 fn mtp_warm_batch_metal(
12930 &mut self,
12931 m: &mut MtpModule,
12932 pairs: &[(&[f32], u32)],
12933 first_pos: usize,
12934 ) -> bool {
12935 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
12936 Some(p) => self.mtp_warm_batch_finish(m, p),
12937 None => false,
12938 }
12939 }
12940
12941 #[cfg(target_os = "macos")]
12946 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
12947 let MetalWarmPending {
12948 mut graph,
12949 cpu_stored,
12950 b,
12951 } = pending;
12952 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12953 if !graph.sync() {
12954 return false;
12955 }
12956 spec_stamp("w.gpu");
12957 let mut kbuf = vec![0f32; b * nkv * hd];
12958 let mut vbuf = vec![0f32; b * nkv * hd];
12959 if !crate::gpu_metal::kv_mirror_read_rows(
12960 self.mtp_kv_id(),
12961 Self::MTP_LAYER_BASE,
12962 nkv,
12963 hd,
12964 cpu_stored,
12965 b,
12966 &mut kbuf,
12967 &mut vbuf,
12968 ) {
12969 return false;
12970 }
12971 for r in 0..b {
12972 m.kv.append(
12973 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12974 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12975 &[],
12976 );
12977 }
12978 crate::gpu_metal::kv_mirror_set_stored(
12979 self.mtp_kv_id(),
12980 Self::MTP_LAYER_BASE,
12981 cpu_stored + b,
12982 );
12983 spec_stamp("w.kv");
12984 true
12985 }
12986
12987 pub(crate) fn note_draft_id(&mut self, id: u32) {
12994 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
12995 if (id as usize) >= cut {
12996 self.draft_full_streak = 16;
12997 } else {
12998 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
12999 }
13000 }
13001
13002 fn draft_head_rows(&self, head_rows: usize) -> usize {
13005 if self.draft_full_streak > 0 {
13006 head_rows
13007 } else {
13008 Self::draft_vocab_rows(head_rows)
13009 }
13010 }
13011
13012 fn draft_vocab_rows(head_rows: usize) -> usize {
13015 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
13016 let n = *N.get_or_init(|| {
13017 std::env::var("CMF_DRAFT_VOCAB")
13018 .ok()
13019 .and_then(|v| v.parse().ok())
13020 .unwrap_or(65536)
13021 });
13022 if n == 0 { head_rows } else { n.min(head_rows) }
13023 }
13024
13025 #[cfg(target_os = "macos")]
13030 fn mtp_step_metal(
13031 &mut self,
13032 m: &mut MtpModule,
13033 hidden: &[f32],
13034 next_token: u32,
13035 position: usize,
13036 want_logits: bool,
13037 ) -> Option<(Vec<f32>, Vec<f32>)> {
13038 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13039 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13040 || !crate::gpu::q1_force()
13041 || !crate::gpu::enabled_here()
13042 || self.attn_softcap > 0.0
13043 || self.attention_heads_per_layer.is_some()
13044 || m.kv.mode != crate::kv_cache::KvMode::F32
13045 || m.kv.o1.is_some()
13046 {
13047 return None;
13048 }
13049 let AttnKind::Full {
13050 wq,
13051 wk,
13052 wv,
13053 wo,
13054 q_norm,
13055 k_norm,
13056 output_gate,
13057 softplus_gate: None,
13058 bias: None,
13059 } = &m.layer.attn
13060 else {
13061 return None;
13062 };
13063 let FfnKind::Dense(d) = &m.layer.ffn else {
13064 return None;
13065 };
13066 if d.act != Act::Silu || !d.segs.is_empty() {
13067 return None;
13068 }
13069 let (pq, pk, pv, po) = (
13070 wq.q1_parts()?,
13071 wk.q1_parts()?,
13072 wv.q1_parts()?,
13073 wo.q1_parts()?,
13074 );
13075 let (g, u, dn) = (
13076 d.gate_proj.q1_parts()?,
13077 d.up_proj.q1_parts()?,
13078 d.down_proj.q1_parts()?,
13079 );
13080 let QTensor::Mapped { model, .. } = wq else {
13081 return None;
13082 };
13083 let model = model.clone();
13084 let lm = if want_logits {
13085 Some(self.weights.lm_head.q1_parts()?)
13086 } else {
13087 None
13088 };
13089 let dims = GraphDims {
13090 hidden: self.hidden_size,
13091 eps: self.rms_eps as f32,
13092 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13093 };
13094 let hs = self.hidden_size;
13097 let mut x = vec![0f32; hs];
13098 let mut graph = TokenGraph::new(&model, dims, &x)?;
13099 let mut folded = false;
13100 if let Some(eh) = m.eh_proj.q1_parts() {
13101 let e = self.embed_single(next_token);
13102 let mut cat = vec![0.0f32; 2 * hs];
13103 let (cat_e, cat_h) = cat.split_at_mut(hs);
13104 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13105 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13106 folded = graph.encode_input_proj(eh, &cat);
13107 }
13108 if !folded {
13109 x = self.mtp_block_input(m, hidden, next_token);
13110 graph = TokenGraph::new(&model, dims, &x)?;
13111 }
13112 spec_stamp("d.in");
13113 let l = AttnGpuLayer {
13114 attn_norm: &m.layer.input_norm,
13115 post_norm: &m.layer.post_norm,
13116 wq: pq,
13117 wk: pk,
13118 wv: pv,
13119 wo: po,
13120 ffn: MetalFfn::Dense {
13121 gate: g,
13122 up: u,
13123 down: dn,
13124 act: (d.act == Act::Gelu) as u8,
13125 },
13126 attn_out_norm: None,
13127 ffn_out_norm: None,
13128 layer_scale: None,
13129 };
13130 let (nh, nkv, hd, rd) = (
13131 self.num_heads,
13132 self.num_kv_heads,
13133 self.head_dim,
13134 self.rotary_dim,
13135 );
13136 let inv_freq = self.inv_freq.clone();
13137 {
13138 let cache = &m.kv;
13139 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13140 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13141 let cpu_stored = cpu_k[0].len() / hd;
13142 let p = AttnDeviceParams {
13143 kv_id: self.mtp_kv_id(),
13144 layer: Self::MTP_LAYER_BASE,
13145 nh,
13146 nkv,
13147 hd,
13148 rd,
13149 position,
13150 scale: self.attn_scale,
13151 eps: self.rms_eps as f32,
13152 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13153 late_qk_norm: self.qk_norm_after_rope,
13154 output_gate: *output_gate,
13155 q_norm: q_norm.as_deref(),
13156 k_norm: k_norm.as_deref(),
13157 inv_freq: &inv_freq,
13158 rope_scale: self.rope_scale,
13159 cpu_k,
13160 cpu_v,
13161 cpu_stored,
13162 cpu_gen: cache.generation(),
13163 o1: None,
13164 window: None,
13165 head_gate: None,
13166 v_norm: false,
13167 };
13168 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13169 return None;
13170 }
13171 }
13172 let draft_rows = if let Some(lm) = lm {
13178 self.draft_head_rows(lm.1)
13179 } else {
13180 0
13181 };
13182 if let Some(lm) = lm {
13183 if !graph.lm_head_ok(lm) {
13184 return None;
13185 }
13186 if draft_rows < lm.1 {
13187 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13188 return None;
13189 }
13190 } else {
13191 graph.encode_lm_head(&m.final_norm, lm);
13192 }
13193 }
13194 spec_stamp("d.enc");
13195 if graph.sync_checked().is_err() {
13196 return None;
13197 }
13198 spec_stamp("d.gpu");
13199 let mut logits = Vec::new();
13200 if let Some(lm) = lm {
13201 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13202 logits = attention::take_buf(n_read);
13203 graph.read_logits(&mut logits);
13204 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13206 }
13207 graph.finish(&mut x);
13208 let mut krow = attention::take_buf(nkv * hd);
13209 let mut vrow = attention::take_buf(nkv * hd);
13210 if crate::gpu_metal::kv_mirror_read_last(
13211 self.mtp_kv_id(),
13212 Self::MTP_LAYER_BASE,
13213 nkv,
13214 hd,
13215 &mut krow,
13216 &mut vrow,
13217 ) {
13218 m.kv.append(&krow, &vrow, &[]);
13219 }
13220 attention::recycle_buf(&mut krow);
13221 attention::recycle_buf(&mut vrow);
13222 spec_stamp("d.rd");
13223 Some((logits, x))
13224 }
13225
13226 fn mtp_chain_on() -> bool {
13239 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13240 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13241 }
13242
13243 #[cfg(target_os = "macos")]
13255 fn mtp_draft_chain_metal(
13256 &mut self,
13257 m: &mut MtpModule,
13258 hidden: &[f32],
13259 t_next: u32,
13260 position: usize,
13261 k: usize,
13262 ) -> Result<Vec<u32>, bool> {
13263 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13264 if k == 0
13265 || k > 64
13266 || !Self::mtp_chain_on()
13267 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13268 || !crate::gpu::q1_force()
13269 || !crate::gpu::enabled_here()
13270 || self.attn_softcap > 0.0
13271 || self.attention_heads_per_layer.is_some()
13272 || m.kv.mode != crate::kv_cache::KvMode::F32
13273 || m.kv.o1.is_some()
13274 || self.dsv4.is_some()
13276 || self.dsv41.is_some()
13277 || self.qwen4_exp.is_some()
13278 || self.g3n.is_some()
13279 {
13280 return Err(false);
13281 }
13282 let AttnKind::Full {
13283 wq,
13284 wk,
13285 wv,
13286 wo,
13287 q_norm,
13288 k_norm,
13289 output_gate,
13290 softplus_gate: None,
13291 bias: None,
13292 } = &m.layer.attn
13293 else {
13294 return Err(false);
13295 };
13296 let FfnKind::Dense(d) = &m.layer.ffn else {
13297 return Err(false);
13298 };
13299 if d.act != Act::Silu || !d.segs.is_empty() {
13300 return Err(false);
13301 }
13302 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13303 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13304 else {
13305 return Err(false);
13306 };
13307 let (Some(g), Some(u), Some(dn)) = (
13308 d.gate_proj.q1_parts(),
13309 d.up_proj.q1_parts(),
13310 d.down_proj.q1_parts(),
13311 ) else {
13312 return Err(false);
13313 };
13314 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13315 return Err(false);
13316 };
13317 let QTensor::Mapped { model, .. } = wq else {
13318 return Err(false);
13319 };
13320 let model = model.clone();
13321 let QTensor::Mapped {
13324 model: em,
13325 idx: eidx,
13326 dtype: cortiq_core::TensorDtype::Q4TiledP,
13327 ..
13328 } = &self.weights.embed_tokens
13329 else {
13330 return Err(false);
13331 };
13332 if !std::sync::Arc::ptr_eq(em, &model)
13333 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13334 {
13335 return Err(false);
13336 }
13337 let embed = (
13338 *eidx,
13339 self.weights.embed_tokens.rows(),
13340 self.weights.embed_tokens.cols(),
13341 );
13342 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13343 return Err(false);
13344 }
13345 let dims = GraphDims {
13346 hidden: self.hidden_size,
13347 eps: self.rms_eps as f32,
13348 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13349 };
13350 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13351 return Err(false);
13352 };
13353 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13354 return Err(false);
13355 }
13356 let l = AttnGpuLayer {
13357 attn_norm: &m.layer.input_norm,
13358 post_norm: &m.layer.post_norm,
13359 wq: pq,
13360 wk: pk,
13361 wv: pv,
13362 wo: po,
13363 ffn: MetalFfn::Dense {
13364 gate: g,
13365 up: u,
13366 down: dn,
13367 act: (d.act == Act::Gelu) as u8,
13368 },
13369 attn_out_norm: None,
13370 ffn_out_norm: None,
13371 layer_scale: None,
13372 };
13373 let (nh, nkv, hd, rd) = (
13374 self.num_heads,
13375 self.num_kv_heads,
13376 self.head_dim,
13377 self.rotary_dim,
13378 );
13379 let inv_freq = self.inv_freq.clone();
13380 let draft_rows = self.draft_head_rows(lm.1);
13381 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13382 if n_arg == 0 {
13383 return Err(false);
13384 }
13385 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13392 let t_chain = std::time::Instant::now();
13393 graph.chain_ids_init(t_next, k);
13394 let cpu_stored;
13395 {
13396 let cache = &m.kv;
13397 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13398 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13399 cpu_stored = cpu_k[0].len() / hd;
13400 for j in 0..k {
13401 if !graph.encode_chain_input(
13402 embed,
13403 j as u32,
13404 &m.enorm,
13405 &m.hnorm,
13406 self.embed_multiplier,
13407 eh,
13408 ) {
13409 return Err(false);
13410 }
13411 let p = AttnDeviceParams {
13415 kv_id: self.mtp_kv_id(),
13416 layer: Self::MTP_LAYER_BASE,
13417 nh,
13418 nkv,
13419 hd,
13420 rd,
13421 position: position + j,
13422 scale: self.attn_scale,
13423 eps: self.rms_eps as f32,
13424 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13425 late_qk_norm: self.qk_norm_after_rope,
13426 output_gate: *output_gate,
13427 q_norm: q_norm.as_deref(),
13428 k_norm: k_norm.as_deref(),
13429 inv_freq: &inv_freq,
13430 rope_scale: self.rope_scale,
13431 cpu_k: cpu_k.clone(),
13432 cpu_v: cpu_v.clone(),
13433 cpu_stored: cpu_stored + j,
13434 cpu_gen: cache.generation(),
13435 o1: None,
13436 window: None,
13437 head_gate: None,
13438 v_norm: false,
13439 };
13440 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13441 return Err(false);
13442 }
13443 if draft_rows < lm.1 {
13444 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13445 return Err(false);
13446 }
13447 } else {
13448 graph.encode_lm_head(&m.final_norm, lm);
13449 }
13450 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13451 return Err(false);
13452 }
13453 if split {
13454 graph.commit();
13457 }
13458 }
13459 }
13460 let t_enc = t_chain.elapsed();
13461 if graph.sync_checked().is_err() {
13462 return Err(true);
13463 }
13464 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13465 eprintln!(
13466 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13467 t_enc.as_secs_f64() * 1e3,
13468 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13469 if split { ", split" } else { "" }
13470 );
13471 }
13472 let mut ids = vec![0u32; k];
13473 if !graph.chain_ids_read(&mut ids) {
13474 return Err(true);
13475 }
13476 let mut kbuf = vec![0f32; k * nkv * hd];
13477 let mut vbuf = vec![0f32; k * nkv * hd];
13478 if !crate::gpu_metal::kv_mirror_read_rows(
13479 self.mtp_kv_id(),
13480 Self::MTP_LAYER_BASE,
13481 nkv,
13482 hd,
13483 cpu_stored,
13484 k,
13485 &mut kbuf,
13486 &mut vbuf,
13487 ) {
13488 return Err(true);
13489 }
13490 for r in 0..k {
13491 m.kv.append(
13492 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13493 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13494 &[],
13495 );
13496 }
13497 Ok(ids)
13498 }
13499
13500 fn try_batch_graph_wgpu(
13501 &self,
13502 hiddens: &mut [f32],
13503 positions: &[usize],
13504 k: usize,
13505 spec: Option<crate::gpu::SpecTail<'_>>,
13506 ) -> crate::gpu::BatchGraphOutcome {
13507 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13508 }
13509
13510 fn try_batch_graph_wgpu_prefix(
13515 &self,
13516 hiddens: &mut [f32],
13517 positions: &[usize],
13518 k: usize,
13519 spec: Option<crate::gpu::SpecTail<'_>>,
13520 layers_run: Option<&mut usize>,
13521 ) -> crate::gpu::BatchGraphOutcome {
13522 let graph_end = match self.mimo_moe.graph_prefix_end() {
13523 Some(end) if end < self.num_layers => {
13524 if layers_run.is_none() || spec.is_some() || end == 0 {
13525 return crate::gpu::BatchGraphOutcome::Declined;
13526 }
13527 end
13528 }
13529 _ => self.num_layers,
13530 };
13531 let _tb = std::time::Instant::now();
13532 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13533 if self.attn_softcap > 0.0 {
13534 return crate::gpu::BatchGraphOutcome::Declined; }
13536 if let Some(reason) = self
13539 .wgpu_graph_attn_decline()
13540 .or_else(|| self.batch_graph_gemma_decline())
13541 {
13542 self.note_graph_decline("wgpu batch graph", reason);
13543 return crate::gpu::BatchGraphOutcome::Declined;
13544 }
13545 let nh = self.num_heads;
13546 let (nkv, hd, rd) = self.layer_geom(0);
13547 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13548 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13549 if let Some((m, i, kind, rs)) = t
13550 .graph_weight()
13551 .or_else(|| t.graph_weight_descriptor())
13552 {
13553 let name = &m.tensors[i].name;
13554 let prism = if crate::prism::is_inverse_embedding(m, name) {
13555 crate::gpu::GraphPrismOp::InverseEmbedding
13556 } else if crate::prism::is_forward_weight(m, name) {
13557 crate::gpu::GraphPrismOp::Forward
13558 } else {
13559 crate::gpu::GraphPrismOp::None
13560 };
13561 return Some(crate::gpu::GraphW {
13562 idx: i,
13563 kind,
13564 row_scale: rs,
13565 data: &[],
13566 prism,
13567 affine: crate::prism::is_affine_target(m, name),
13568 });
13569 }
13570 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13571 eprintln!(
13572 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13573 t.rows(),
13574 t.cols()
13575 );
13576 }
13577 t.as_f32().map(|d| crate::gpu::GraphW {
13578 idx: 0,
13579 kind: 4,
13580 row_scale: &[],
13581 data: d,
13582 prism: crate::gpu::GraphPrismOp::None,
13583 affine: false,
13584 })
13585 }
13586 let built: Option<(
13587 Vec<crate::gpu::GraphLayer<'_>>,
13588 std::sync::Arc<cortiq_core::CmfModel>,
13589 )> = (|| {
13590 let mut layers = Vec::with_capacity(graph_end);
13591 let mut model = None;
13592 for li in 0..graph_end {
13593 let lw = &self.weights.layers[self.phys_layer(li)];
13594 let gffn = match &lw.ffn {
13601 FfnKind::Dense(d) if !d.segs.is_empty() => {
13602 if batch_debug {
13603 eprintln!("batch graph: dense segmented FFN at layer {li}");
13604 }
13605 return None;
13606 }
13607 FfnKind::Dense(d) => {
13608 let Some(act) = d.act.graph_act() else {
13609 if batch_debug {
13610 eprintln!(
13611 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13612 d.act
13613 );
13614 }
13615 return None;
13616 };
13617 crate::gpu::GraphFfn::Dense {
13618 gate: gw(&d.gate_proj)?,
13619 up: gw(&d.up_proj)?,
13620 down: gw(&d.down_proj)?,
13621 act,
13622 }
13623 }
13624 FfnKind::Moe(m) => {
13625 if m.route_tau.is_some() || m.mask.is_some() {
13632 return None;
13633 }
13634 let shared = m.shared.as_ref();
13638 let has_shared = shared.is_some();
13639 let shared_gated = matches!(shared, Some((_, Some(_))));
13640 let sgate = match shared {
13641 Some((_, Some(sg))) => gw(sg)?,
13642 _ => gw(&m.router)?,
13646 };
13647 let router = gw(&m.router)?;
13648 if router.prism != crate::gpu::GraphPrismOp::None
13654 || router.affine
13655 || sgate.prism != crate::gpu::GraphPrismOp::None
13656 || sgate.affine
13657 {
13658 return None;
13659 }
13660 let inter = m.experts.first()?.gate_proj.rows();
13661 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13662 let mut q4tp: Option<bool> = None;
13663 let mut gu_q2: Option<bool> = None;
13664 let mut q82: Option<bool> = None;
13665 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13666 if !matches!(e.act, Act::Silu)
13667 || e.gate_proj.rows() != inter
13668 || e.up_proj.rows() != inter
13669 {
13670 return None;
13671 }
13672 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
13676 {
13677 Some((mm, gi)) => (
13678 mm,
13679 gi,
13680 e.up_proj.mapped_q4t()?.1,
13681 e.down_proj.mapped_q4t()?.1,
13682 false,
13683 false,
13684 false,
13685 ),
13686 None => match e.gate_proj.mapped_q2tp() {
13687 Some((mm, gi)) => (
13688 mm,
13689 gi,
13690 e.up_proj.mapped_q2tp()?.1,
13691 e.down_proj.mapped_q4tp()?.1,
13692 true,
13693 true,
13694 false,
13695 ),
13696 None => match e.gate_proj.mapped_q4tp() {
13697 Some((mm, gi)) => (
13698 mm,
13699 gi,
13700 e.up_proj.mapped_q4tp()?.1,
13701 e.down_proj.mapped_q4tp()?.1,
13702 true,
13703 false,
13704 false,
13705 ),
13706 None => {
13707 if !crate::gpu::moe_q82_graph_on() {
13708 return None;
13709 }
13710 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13711 (
13712 mm,
13713 gi,
13714 e.up_proj.mapped_q8_2f()?.1,
13715 e.down_proj.mapped_q8_2f()?.1,
13716 false,
13717 false,
13718 true,
13719 )
13720 }
13721 },
13722 },
13723 };
13724 if *q4tp.get_or_insert(is_p) != is_p
13725 || *gu_q2.get_or_insert(is_q2) != is_q2
13726 || *q82.get_or_insert(is_8) != is_8
13727 {
13728 return None;
13729 }
13730 if [gi, ui, di].into_iter().any(|idx| {
13731 mm.tensors
13732 .get(idx)
13733 .is_some_and(|t| {
13734 crate::prism::is_forward_weight(mm, &t.name)
13735 || crate::prism::is_affine_target(mm, &t.name)
13736 })
13737 }) {
13738 return None;
13739 }
13740 model.get_or_insert_with(|| mm.clone());
13741 experts.push((gi, ui, di));
13742 }
13743 crate::gpu::GraphFfn::Moe {
13744 router,
13745 shared_gate: sgate,
13746 experts,
13747 n_exp: m.experts.len(),
13748 top_k: m.top_k,
13749 inter,
13750 norm_topk: m.norm_topk_prob,
13751 q4tp: q4tp?,
13752 gu_q2: gu_q2.unwrap_or(false),
13753 q82: q82.unwrap_or(false),
13754 sigmoid: m.router_sigmoid,
13755 bias: m.expert_bias.as_deref(),
13756 has_shared,
13757 shared_gated,
13758 route_scale: m.routed_scaling,
13759 }
13760 }
13761 _ => return None,
13762 };
13763 let attn = match &lw.attn {
13764 AttnKind::Full {
13765 wq,
13766 wk,
13767 wv,
13768 wo,
13769 q_norm,
13770 k_norm,
13771 output_gate,
13772 softplus_gate,
13773 bias,
13774 } => {
13775 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
13776 if batch_debug {
13777 eprintln!(
13778 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
13779 softplus_gate.is_some(),
13780 self.attention_heads_per_layer.is_some()
13781 );
13782 }
13783 return None;
13784 }
13785 let (m, _, _, _) = wq
13786 .graph_weight()
13787 .or_else(|| wq.graph_weight_descriptor())?;
13788 model = Some(m.clone());
13789 crate::gpu::GraphAttn::Full {
13790 wq: gw(wq)?,
13791 wk: gw(wk)?,
13792 wv: gw(wv)?,
13793 wo: gw(wo)?,
13794 q_norm: q_norm.as_deref(),
13795 k_norm: k_norm.as_deref(),
13796 late_qk_norm: self.qk_norm_after_rope,
13797 bias: bias
13798 .as_ref()
13799 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13800 output_gate: *output_gate,
13801 cpu_k: self.kv_cache.layers[li].k_heads(),
13802 cpu_v: self.kv_cache.layers[li].v_heads(),
13803 cpu_base: self.kv_cache.layers[li].base(),
13804 geom: self.graph_attn_geom(li),
13805 head_gate: None,
13808 }
13809 }
13810 AttnKind::LinearGdn(w) => {
13811 let Some(cfg) = self.gdn_cfg else {
13812 if batch_debug {
13813 eprintln!("batch graph: no GDN config at layer {li}");
13814 }
13815 return None;
13816 };
13817 let (m, _, _, _) = w
13818 .in_proj_qkv
13819 .graph_weight()
13820 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
13821 model = Some(m.clone());
13822 crate::gpu::GraphAttn::Gdn {
13823 qkv: gw(&w.in_proj_qkv)?,
13824 z: gw(&w.in_proj_z)?,
13825 a: gw(&w.in_proj_a)?,
13826 b: gw(&w.in_proj_b)?,
13827 out: gw(&w.out_proj)?,
13828 conv1d: &w.conv1d,
13829 a_log: &w.a_log,
13830 dt_bias: &w.dt_bias,
13831 norm: &w.norm,
13832 nv: cfg.num_v_heads,
13833 nk: cfg.num_k_heads,
13834 dk: cfg.key_head_dim,
13835 dv: cfg.value_head_dim,
13836 kk: cfg.conv_kernel,
13837 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13838 }
13839 }
13840 _ => return None,
13841 };
13842 let g4 = self.global_attn.is_some() || self.attn_v_norm;
13845 layers.push(crate::gpu::GraphLayer {
13846 input_norm: &lw.input_norm,
13847 attn,
13848 post_norm: &lw.post_norm,
13849 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
13850 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
13851 layer_scale: if g4 { lw.layer_scale } else { None },
13852 ffn: gffn,
13853 });
13854 }
13855 Some((layers, model?))
13856 })();
13857 let Some((layers, model)) = built else {
13858 {
13859 use std::sync::atomic::{AtomicBool, Ordering};
13860 static SAID: AtomicBool = AtomicBool::new(false);
13861 if !SAID.swap(true, Ordering::Relaxed) {
13862 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
13863 }
13864 }
13865 return crate::gpu::BatchGraphOutcome::Declined;
13866 };
13867 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13868 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
13869 }
13870 crate::gpu::forward_batch_graph(
13871 &model,
13872 self.graph_kv_id,
13873 &layers,
13874 &self.inv_freq,
13875 hiddens,
13876 nh,
13877 nkv,
13878 hd,
13879 rd,
13880 self.hidden_size,
13881 self.intermediate_size,
13882 positions,
13883 self.kv_cache.max_seq_len,
13884 gemma,
13885 self.rms_eps as f32,
13886 self.attn_scale,
13887 k,
13888 &(0..graph_end)
13889 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
13890 .collect::<Vec<_>>(),
13891 self.o1_epoch,
13892 spec,
13893 layers_run,
13894 )
13895 }
13896
13897 fn draft_probe() -> bool {
13901 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13902 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
13903 }
13904
13905 #[cfg(feature = "gpu")]
13917 fn dsv4_spec_on() -> bool {
13918 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13919 *ON.get_or_init(|| {
13920 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
13924 return v != "0";
13925 }
13926 std::env::var("CMF_DSV4_SPEC")
13933 .map(|v| v != "0")
13934 .unwrap_or_else(|_| {
13935 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
13936 })
13937 })
13938 }
13939
13940 #[cfg(feature = "gpu")]
13947 fn dsv4_spec_step(
13948 &mut self,
13949 tip_token: u32,
13950 t_next: u32,
13951 next_pos: usize,
13952 max_extra: usize,
13953 drafted: &mut usize,
13954 accepted_ctr: &mut usize,
13955 ) -> Option<(Vec<u32>, usize)> {
13956 let t_all = std::time::Instant::now();
13957 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
13958 thread_local! {
13959 static LAST: std::cell::Cell<Option<std::time::Instant>> =
13960 const { std::cell::Cell::new(None) };
13961 }
13962 LAST.with(|l| {
13963 if let Some(prev) = l.get() {
13964 eprintln!(
13965 "между раундами {:.1} мс",
13966 prev.elapsed().as_secs_f64() * 1e3
13967 );
13968 }
13969 l.set(Some(std::time::Instant::now()));
13970 });
13971 }
13972 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13973 eprintln!("spec_step: вход pos={next_pos}");
13974 }
13975 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
13976 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
13977 if self.dspark.is_none() {
13979 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13980 if t.is_empty() {
13981 return None;
13982 }
13983 crate::dsv4::dspark_arm(&t, cfg.dim);
13984 self.dspark = Some(crate::dsv4::DsparkState::new(
13985 self.dsv4_mtp.len(),
13986 &cfg,
13987 t.len(),
13988 ));
13989 }
13990 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13991 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
13992 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13993 eprintln!("spec_step: пак не построился (targets {targets:?})");
13994 }
13995 let pack = pack?;
13996 let block = crate::dsv4::dspark_block();
13997 let b_box = self.dsv4.as_mut()?;
13998 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
13999 let ds = self.dspark.as_mut()?;
14000 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
14003 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
14004 if dbg {
14005 eprintln!("spec_step: нет захвата");
14006 }
14007 return None;
14008 }
14009 ds.have_hidden = true;
14010 let tip_pos = next_pos.checked_sub(1)?;
14011 let draft_started = std::time::Instant::now();
14012 let mut conf = Vec::new();
14013 let props = crate::dsv4::dspark_draft_gpu(
14014 g,
14015 &self.dsv4_mtp,
14016 &cfg,
14017 ds,
14018 pack,
14019 st.kv_id,
14020 tip_token,
14021 tip_pos,
14022 self.pool.as_deref(),
14023 &mut conf,
14024 );
14025 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14026 *drafted += block;
14027 if props.is_empty() || props[0] != t_next {
14028 if dbg {
14029 eprintln!(
14030 "spec_step: черновик {} (props0={:?} t_next={t_next})",
14031 if props.is_empty() {
14032 "пуст"
14033 } else {
14034 "мимо"
14035 },
14036 props.first()
14037 );
14038 }
14039 return None;
14040 }
14041 let mut k_verify = crate::dsv4::dspark_verify_k()
14048 .min(props.len())
14049 .min(max_extra.saturating_add(1));
14050 let conf_min = {
14056 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14057 *M.get_or_init(|| {
14058 std::env::var("CMF_DSPARK_CONF_MIN")
14059 .ok()
14060 .and_then(|v| v.parse().ok())
14061 .unwrap_or(0.0)
14062 })
14063 };
14064 if conf_min > 0.0 && conf.len() >= props.len() {
14065 let mut keep = 1usize;
14066 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14067 keep += 1;
14068 }
14069 k_verify = k_verify.min(keep.max(2));
14070 }
14071 if k_verify < 2 {
14072 return None;
14073 }
14074 let mut fed = Vec::with_capacity(k_verify);
14075 fed.push(t_next);
14076 fed.extend_from_slice(&props[1..k_verify]);
14077 let mut argmax = Vec::new();
14078 let mut logits_all = Vec::new();
14079 let mut walked = Vec::new();
14080 let txn = crate::dsv4::dsv4_verify_chunk(
14081 g,
14082 layers,
14083 &cfg,
14084 st,
14085 &fed,
14086 next_pos,
14087 &self.inv_freq,
14088 self.pool.as_deref(),
14089 &targets,
14090 &mut argmax,
14091 &mut logits_all,
14092 &mut walked,
14093 );
14094 if txn.is_none() && dbg {
14095 eprintln!("spec_step: verify отказал");
14096 }
14097 let txn = txn?;
14098 let spec_gpu_end = txn.gpu_end;
14099 let b = fed.len();
14100 let mut accepted = 1usize;
14101 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14102 accepted += 1;
14103 }
14104 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14109 accepted = 1;
14110 }
14111 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14112 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14113 }
14114 let t_fin = std::time::Instant::now();
14115 if !crate::dsv4::dsv4_spec_finish(
14116 g,
14117 layers,
14118 &cfg,
14119 st,
14120 txn,
14121 accepted,
14122 &fed,
14123 &self.inv_freq,
14124 self.pool.as_deref(),
14125 ) {
14126 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14127 return None;
14128 }
14129 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14130 eprintln!(
14131 "finish(k={accepted}): {:.1} мс",
14132 t_fin.elapsed().as_secs_f64() * 1e3
14133 );
14134 }
14135 *accepted_ctr += accepted - 1;
14136 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14141 let dev_caps: Vec<usize> = targets
14146 .iter()
14147 .copied()
14148 .filter(|&t| t < spec_gpu_end)
14149 .collect();
14150 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14151 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14152 return None;
14153 }
14154 for t in 0..accepted {
14155 let tip = t + 1 == accepted;
14156 for (slot, &tl) in targets.iter().enumerate() {
14157 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14158 let lo = (di * b + t) * hc * dim;
14159 crate::dsv4::dspark_capture(
14160 &caps_all[lo..lo + hc * dim],
14161 &cfg,
14162 slot,
14163 &mut ds.main_hidden,
14164 );
14165 } else if tip
14166 && crate::dsv4::dspark_peek_slot(slot, dim, {
14167 let lo = slot * dim;
14168 &mut ds.main_hidden[lo..lo + dim]
14169 })
14170 {
14171 } else {
14176 crate::dsv4::dspark_capture(
14180 &walked[t * hc * dim..(t + 1) * hc * dim],
14181 &cfg,
14182 slot,
14183 &mut ds.main_hidden,
14184 );
14185 }
14186 }
14187 crate::dsv4::dspark_ring_append(
14188 g,
14189 &self.dsv4_mtp,
14190 &cfg,
14191 ds,
14192 next_pos + t,
14193 self.pool.as_deref(),
14194 );
14195 }
14196 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14197 self.graph_logits = Some(row);
14198 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14203 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14204 crate::dsv4::pick_tally_arm();
14205 }
14206 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14207 eprintln!(
14208 "spec_step total {:.1} мс (k={accepted})",
14209 t_all.elapsed().as_secs_f64() * 1e3
14210 );
14211 }
14212 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14213 }
14214
14215 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14216 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14217 return;
14218 }
14219 let trunk_now = crate::dsv4::pick_tally_take();
14221 crate::dsv4::trunk_freq_note(&trunk_now);
14222 if !trunk_now.is_empty() {
14223 self.dspark_trunk_picks.push(trunk_now);
14224 let keep = crate::dsv4::dspark_block();
14225 if self.dspark_trunk_picks.len() > keep {
14226 self.dspark_trunk_picks.remove(0);
14227 }
14228 }
14229 for p in std::mem::take(&mut self.dspark_pending) {
14232 let Some(i) = position.checked_sub(p.0 + 1) else {
14233 continue;
14234 };
14235 let mut p = p;
14236 if i < p.1.len() {
14237 if p.2 && p.1[i] == token_id {
14238 p.3 = i + 1;
14239 } else {
14240 p.2 = false;
14241 }
14242 if i + 1 < p.1.len() {
14243 self.dspark_pending.push(p);
14244 continue;
14245 }
14246 }
14247 self.dspark_hist.push(p.3);
14248 self.dspark_real.push(token_id);
14249 }
14250 let Some(b) = &mut self.dsv4 else { return };
14251 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14252 let n_layers = layers.len();
14253 if self.dspark.is_none() {
14254 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14255 if t.is_empty() {
14256 return;
14257 }
14258 eprintln!(
14259 "DSpark: захват со слоёв {t:?}, блок {}",
14260 crate::dsv4::dspark_block()
14261 );
14262 crate::dsv4::dspark_arm(&t, cfg.dim);
14263 self.dspark = Some(crate::dsv4::DsparkState::new(
14264 self.dsv4_mtp.len(),
14265 &cfg,
14266 t.len(),
14267 ));
14268 }
14269 let ds = self.dspark.as_mut().unwrap();
14270 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14271 return; }
14273 let mut conf = Vec::new();
14274 crate::dsv4::pick_tally_arm();
14275 let draft_started = std::time::Instant::now();
14280 #[cfg(feature = "gpu")]
14281 let gpu_draft = crate::dsv4::dspark_gpu_on();
14282 #[cfg(not(feature = "gpu"))]
14283 let gpu_draft = false;
14284 let props = if gpu_draft {
14285 #[cfg(feature = "gpu")]
14286 {
14287 let kv_id = b.3.kv_id;
14288 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14289 Some(pk) => crate::dsv4::dspark_draft_gpu(
14290 g,
14291 &self.dsv4_mtp,
14292 &cfg,
14293 ds,
14294 pk,
14295 kv_id,
14296 token_id,
14297 position,
14298 self.pool.as_deref(),
14299 &mut conf,
14300 ),
14301 None => Vec::new(),
14302 }
14303 }
14304 #[cfg(not(feature = "gpu"))]
14305 Vec::new()
14306 } else {
14307 crate::gpu::cpu_scope(|| {
14308 crate::dsv4::dspark_draft(
14309 g,
14310 &self.dsv4_mtp,
14311 &cfg,
14312 ds,
14313 token_id,
14314 position,
14315 self.pool.as_deref(),
14316 &mut conf,
14317 )
14318 })
14319 };
14320 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14321 let draft_picks = crate::dsv4::pick_tally_take();
14322 crate::dsv4::dspark_freq_note(&draft_picks);
14323 crate::dsv4::pick_tally_arm();
14326 if !props.is_empty() {
14327 let (tu, tt) = {
14331 let flat: Vec<(usize, Vec<usize>)> = self
14332 .dspark_trunk_picks
14333 .iter()
14334 .flat_map(|v| v.iter().cloned())
14335 .collect();
14336 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14338 std::collections::HashMap::new();
14339 for (li, picks) in flat {
14340 per.entry(li).or_default().extend(picks);
14341 }
14342 let n = per.len().max(1);
14343 let mut u = 0usize;
14344 let mut t = 0usize;
14345 for (_, v) in per {
14346 t += v.len();
14347 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14348 }
14349 (u / n, t / n)
14350 };
14351 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14352 self.dspark_exp.push((tu, tt, du, dt));
14353 self.dspark_pending.push((position, props, true, 0));
14354 }
14355 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14356 let n = self.dspark_hist.len() as f32;
14357 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14358 let block = crate::dsv4::dspark_block();
14359 let mut at = vec![0usize; block + 1];
14360 for &k in &self.dspark_hist {
14361 at[k] += 1;
14362 }
14363 let mut surv = Vec::with_capacity(block);
14365 for i in 1..=block {
14366 let k = at[i..].iter().sum::<usize>() as f32 / n;
14367 surv.push(format!("{k:.2}"));
14368 }
14369 let distinct = self
14370 .dspark_real
14371 .iter()
14372 .collect::<std::collections::HashSet<_>>()
14373 .len();
14374 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14375 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14376 });
14377 let m = self.dspark_exp.len().max(1);
14378 eprintln!(
14379 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14380 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14381 self.dspark_hist.len(),
14382 mean + 1.0,
14383 surv.join(" ")
14384 );
14385 eprintln!(
14386 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14387 эксперты ствол {}/{} на слой за {block} токенов, \
14388 черновик {}/{} за блок, draft {:.2} мс/блок",
14389 self.dspark_real.len(),
14390 tu / m,
14391 tt / m,
14392 du / m,
14393 dt / m,
14394 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14395 );
14396 }
14397 }
14398
14399 fn forward_layers_upto(
14400 &mut self,
14401 hidden: &[f32],
14402 position: usize,
14403 task_mask: Option<&TaskMask>,
14404 upto: Option<usize>,
14405 ) -> Vec<f32> {
14406 if let Some(plan) = self.gpu_plan.clone() {
14412 if upto.is_none() && plan.len() > 1 {
14413 let mut h = hidden.to_vec();
14414 for &(dev, from, upto_incl) in plan.iter() {
14415 h = crate::gpu::with_device(dev, || {
14416 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14417 });
14418 }
14419 return h;
14420 }
14421 }
14422 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14423 }
14424
14425 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14430 self.set_gpu_plan_at(devices, None)
14431 }
14432
14433 pub fn set_gpu_plan_at(
14437 &mut self,
14438 devices: Option<&[usize]>,
14439 at: Option<usize>,
14440 ) -> Result<(), String> {
14441 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14442 self.gpu_plan = None;
14443 return Ok(());
14444 };
14445 self.split_supported()?;
14446 let n = self.num_layers;
14447 if devs.len() > n {
14448 return Err(format!("{} devices for {n} layers", devs.len()));
14449 }
14450 if let Some(k) = at {
14451 if k == 0 || k >= n {
14452 return Err(format!("split at {k}: the model has {n} layers"));
14453 }
14454 if devs.len() == 2 {
14455 self.gpu_plan = Some(std::sync::Arc::new(vec![
14456 (devs[0], 0, k - 1),
14457 (devs[1], k, n - 1),
14458 ]));
14459 return Ok(());
14460 }
14461 return Err(format!(
14462 "an explicit split point takes exactly 2 devices, got {}",
14463 devs.len()
14464 ));
14465 }
14466 let per = n.div_ceil(devs.len());
14467 let mut plan = Vec::with_capacity(devs.len());
14468 let mut from = 0usize;
14469 for &d in devs {
14470 if from >= n {
14471 break;
14472 }
14473 let upto = (from + per - 1).min(n - 1);
14474 plan.push((d, from, upto));
14475 from = upto + 1;
14476 }
14477 self.gpu_plan = Some(std::sync::Arc::new(plan));
14478 Ok(())
14479 }
14480
14481 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14483 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14484 }
14485
14486 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14492 if let Some(x) = t.as_f32() {
14493 return x.to_vec();
14494 }
14495 let mut out = vec![0.0; t.rows() * t.cols()];
14496 for r in 0..t.rows() {
14497 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14498 }
14499 out
14500 }
14501
14502 fn embryo_resident_eligible(&self) -> bool {
14503 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14506 || self.num_layers != self.physical_layers
14507 || self.loop_final_norm
14508 || self.weights.layers.len() != self.num_layers
14509 || self.head_clusters.is_none()
14510 || self.final_softcap.is_some()
14511 || self.logit_multiplier.is_some()
14512 || self.attn_softcap != 0.0
14513 || self.mtp.is_some()
14514 || self.g3n.is_some()
14515 || self.dsv4.is_some()
14516 || self.dsv41.is_some()
14517 || self.qwen4_exp.is_some()
14518 || self.dyn_router.is_some()
14525 || self.dyn_phi_layer.is_some()
14526 || self.dyn_blend_loaded
14527 || self.o1_cfg.is_some()
14528 || self.swa.is_some()
14529 || self.sliding_layers.is_some()
14530 || self.global_attn.is_some()
14531 || self.attention_heads_per_layer.is_some()
14532 || self.attn_v_norm
14533 || self
14534 .kv_cache
14535 .layers
14536 .iter()
14537 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14538 || self.rope_scale != 1.0
14539 || self.rope_scale_local != 1.0
14540 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14541 || self.hidden_size == 0
14542 || self.hidden_size > 1024
14543 || self.intermediate_size > 1024
14544 || self.num_heads == 0
14545 || self.num_kv_heads == 0
14546 || self.num_heads % self.num_kv_heads != 0
14547 || self.num_heads.saturating_mul(self.head_dim) > 1024
14548 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14549 || self.vocab_size == 0
14550 || self.kv_cache.max_seq_len == 0
14551 || self.rotary_dim == 0
14552 || self.rotary_dim > self.head_dim
14553 || self.rotary_dim % 2 != 0
14554 || self.inv_freq.len() < self.rotary_dim / 2
14555 {
14556 return false;
14557 }
14558 if self.weights.lm_head.as_f32().is_none()
14569 || self.weights.embed_tokens.as_f32().is_none()
14570 || self.weights.lm_head.rows() < self.vocab_size
14571 || self.weights.lm_head.cols() != self.hidden_size
14572 || self.weights.embed_tokens.rows() < self.vocab_size
14573 || self.weights.embed_tokens.cols() != self.hidden_size
14574 || self.weights.final_norm.len() != self.hidden_size
14575 {
14576 return false;
14577 }
14578 if let Some(cfg) = self.vmf_cfg {
14579 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14580 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14581 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14582 || cfg.state_len() == 0
14583 {
14584 return false;
14585 }
14586 }
14587 if let Some(g) = self.gdn_cfg {
14588 if g.num_v_heads == 0
14593 || g.num_k_heads == 0
14594 || g.num_v_heads % g.num_k_heads != 0
14595 || g.key_head_dim == 0
14596 || g.key_head_dim > 128
14597 || g.value_head_dim == 0
14598 || g.value_head_dim > 256
14599 || g.value_head_dim % 4 != 0
14600 || g.conv_kernel == 0
14601 || g.num_v_heads > 512
14602 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14603 || g.conv_dim() > 2048
14604 || g.conv_dim() % 4 != 0
14605 || g.hidden_size != self.hidden_size
14606 || g.output_gate_sigmoid
14607 || g.rms_eps != self.rms_eps
14608 || g.state_len() == 0
14609 {
14610 return false;
14611 }
14612 }
14613 let mut full_seen = false;
14614 for lw in &self.weights.layers {
14615 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14616 return false;
14617 }
14618 match &lw.attn {
14619 AttnKind::LinearGdn(w) => {
14620 let Some(g) = self.gdn_cfg else {
14621 return false;
14622 };
14623 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14624 if w.in_proj_qkv.rows() != g.conv_dim()
14625 || w.in_proj_qkv.cols() != self.hidden_size
14626 || w.in_proj_qkv.as_f32().is_none()
14627 || w.in_proj_z.rows() != nv * dv
14628 || w.in_proj_z.cols() != self.hidden_size
14629 || w.in_proj_z.as_f32().is_none()
14630 || w.in_proj_a.rows() != nv
14631 || w.in_proj_a.cols() != self.hidden_size
14632 || w.in_proj_a.as_f32().is_none()
14633 || w.in_proj_b.rows() != nv
14634 || w.in_proj_b.cols() != self.hidden_size
14635 || w.in_proj_b.as_f32().is_none()
14636 || w.conv1d.len() != g.conv_dim() * kk
14637 || w.a_log.len() != nv
14638 || w.dt_bias.len() != nv
14639 || w.norm.len() != dv
14640 || w.out_proj.rows() != self.hidden_size
14641 || w.out_proj.cols() != nv * dv
14642 || w.out_proj.as_f32().is_none()
14643 {
14644 return false;
14645 }
14646 }
14647 AttnKind::Linear(w) => {
14648 let Some(cfg) = self.vmf_cfg else {
14649 return false;
14650 };
14651 if w.thq.rows() != cfg.num_heads * cfg.nphase
14652 || w.thq.cols() != self.hidden_size
14653 || w.thq.as_f32().is_none()
14654 || w.thk.rows() != cfg.num_heads * cfg.nphase
14655 || w.thk.cols() != self.hidden_size
14656 || w.thk.as_f32().is_none()
14657 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14658 || w.v_proj.cols() != self.hidden_size
14659 || w.v_proj.as_f32().is_none()
14660 || w.out_proj.rows() != self.hidden_size
14661 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14662 || w.out_proj.as_f32().is_none()
14663 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14664 {
14665 return false;
14666 }
14667 if let Some((kg, kb)) = &w.k_gate {
14668 if kg.rows() != cfg.num_heads
14669 || kg.cols() != self.hidden_size
14670 || kg.as_f32().is_none()
14671 || kb.len() != cfg.num_heads
14672 {
14673 return false;
14674 }
14675 }
14676 if let Some(conv) = &w.conv {
14677 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14678 return false;
14679 }
14680 }
14681 }
14682 AttnKind::Full {
14683 wq,
14684 wk,
14685 wv,
14686 wo,
14687 q_norm,
14688 k_norm,
14689 output_gate,
14690 softplus_gate,
14691 bias,
14692 } => {
14693 if full_seen
14694 || q_norm.is_some()
14695 || k_norm.is_some()
14696 || *output_gate
14697 || softplus_gate.is_some()
14698 || bias.is_some()
14699 || wq.as_f32().is_none()
14700 || wk.as_f32().is_none()
14701 || wv.as_f32().is_none()
14702 || wo.as_f32().is_none()
14703 || wq.rows() != self.num_heads * self.head_dim
14704 || wk.rows() != self.num_kv_heads * self.head_dim
14705 || wv.rows() != self.num_kv_heads * self.head_dim
14706 || wq.cols() != self.hidden_size
14707 || wk.cols() != self.hidden_size
14708 || wv.cols() != self.hidden_size
14709 || wo.rows() != self.hidden_size
14710 || wo.cols() != self.num_heads * self.head_dim
14711 {
14712 return false;
14713 }
14714 full_seen = true;
14715 }
14716 AttnKind::Bounded(w) => {
14717 let Some(ac) = self.anchor_core.as_ref() else {
14720 return false;
14721 };
14722 if self.bounded_rope.is_none()
14723 || w.window != ac.window
14724 || w.sink != ac.sink
14725 || w.window == 0
14726 || w.window + w.sink > 256
14727 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
14728 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
14729 || w.wq.as_f32().is_none()
14730 || w.wk.as_f32().is_none()
14731 || w.wv.as_f32().is_none()
14732 || w.wo.as_f32().is_none()
14733 || w.wq.rows() != self.num_heads * self.head_dim
14734 || w.wk.rows() != self.num_kv_heads * self.head_dim
14735 || w.wv.rows() != self.num_kv_heads * self.head_dim
14736 || w.wq.cols() != self.hidden_size
14737 || w.wk.cols() != self.hidden_size
14738 || w.wv.cols() != self.hidden_size
14739 || w.wo.rows() != self.hidden_size
14740 || w.wo.cols() != self.num_heads * self.head_dim
14741 {
14742 return false;
14743 }
14744 }
14745 _ => return false,
14746 }
14747 match &lw.ffn {
14748 FfnKind::Dense(d) => {
14749 if d.act != Act::Silu
14750 || !d.segs.is_empty()
14751 || d.gate_proj.as_f32().is_none()
14752 || d.up_proj.as_f32().is_none()
14753 || d.down_proj.as_f32().is_none()
14754 || d.gate_proj.rows() != self.intermediate_size
14755 || d.gate_proj.cols() != self.hidden_size
14756 || d.up_proj.rows() != self.intermediate_size
14757 || d.up_proj.cols() != self.hidden_size
14758 || d.down_proj.rows() != self.hidden_size
14759 || d.down_proj.cols() != self.intermediate_size
14760 {
14761 return false;
14762 }
14763 }
14764 FfnKind::Moe(m) => {
14765 if m.resonance.is_none()
14766 || m.top_k != 1
14767 || m.router_sigmoid
14768 || !m.norm_topk_prob
14769 || m.expert_bias.is_some()
14770 || m.routed_scaling != 1.0
14771 || m.route_tau.is_some()
14772 || m.shared.is_none()
14773 || m.mask.is_some()
14774 || m.per_expert_scale.is_some()
14775 || m.router_input_norm
14776 || m.experts.is_empty()
14777 || m.experts.len() > 8
14778 {
14779 return false;
14780 }
14781 let r = m.resonance.as_ref().unwrap();
14782 if r.mu.len() != m.experts.len() * self.hidden_size
14783 || r.bias.len() != m.experts.len()
14784 || r.u.len() != m.experts.len() * r.k * self.hidden_size
14785 || r.k > 128
14786 {
14787 return false;
14788 }
14789 let Some((shared, gate)) = &m.shared else {
14790 return false;
14791 };
14792 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
14793 return false;
14794 }
14795 if shared.gate_proj.as_f32().is_none()
14796 || shared.up_proj.as_f32().is_none()
14797 || shared.down_proj.as_f32().is_none()
14798 || shared.gate_proj.rows() != self.intermediate_size
14799 || shared.gate_proj.cols() != self.hidden_size
14800 || shared.up_proj.rows() != self.intermediate_size
14801 || shared.up_proj.cols() != self.hidden_size
14802 || shared.down_proj.rows() != self.hidden_size
14803 || shared.down_proj.cols() != self.intermediate_size
14804 {
14805 return false;
14806 }
14807 for e in &m.experts {
14808 if e.act != Act::Silu
14809 || !e.segs.is_empty()
14810 || e.gate_proj.as_f32().is_none()
14811 || e.up_proj.as_f32().is_none()
14812 || e.down_proj.as_f32().is_none()
14813 || e.gate_proj.rows() != self.intermediate_size
14814 || e.gate_proj.cols() != self.hidden_size
14815 || e.up_proj.rows() != self.intermediate_size
14816 || e.up_proj.cols() != self.hidden_size
14817 || e.down_proj.rows() != self.hidden_size
14818 || e.down_proj.cols() != self.intermediate_size
14819 {
14820 return false;
14821 }
14822 }
14823 }
14824 FfnKind::DenseMoe(_) => return false,
14825 }
14826 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
14827 return false;
14828 }
14829 }
14830 if full_seen && self.anchor_core.is_some() {
14831 return false;
14832 }
14833 full_seen || self.num_layers > 0
14834 }
14835
14836 fn embryo_resident_wanted(&self) -> bool {
14841 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
14842 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
14843 && matches!(
14844 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
14845 Ok("1") | Ok("parallel")
14846 )
14847 && crate::gpu::enabled_here()
14848 && !self.graph_refused()
14849 && self.embryo_resident_eligible()
14850 }
14851
14852 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
14860 if ids.len() < 2
14861 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
14862 || !self.embryo_resident_wanted()
14863 {
14864 return None;
14865 }
14866 let model = self.ensure_embryo_graph()?;
14867 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
14868 .ok()
14869 .and_then(|v| v.parse::<usize>().ok())
14870 .filter(|&v| v >= 1)
14871 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
14872 .min(crate::gpu::EMBRYO_CHUNK_MAX);
14873 let hs = self.hidden_size;
14874 let n = ids.len();
14875 let mut pos = start;
14876 let mut last = None;
14877 let mut rows = Vec::with_capacity(cmax * hs);
14878 while pos < n {
14879 let end = (pos + cmax).min(n);
14880 rows.clear();
14881 for &id in &ids[pos..end] {
14882 rows.extend_from_slice(&self.embed_single(id));
14883 }
14884 let mut lg = Vec::new();
14885 if !crate::gpu::forward_embryo_graph_chunk(
14886 &model,
14887 self.graph_kv_id,
14888 &rows,
14889 pos,
14890 end - pos,
14891 &mut lg,
14892 ) {
14893 if pos == start {
14894 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14895 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
14896 }
14897 return None;
14898 }
14899 self.kv_cache.clear();
14903 self.clear_history();
14904 crate::gpu::graph_kv_reset(self.graph_kv_id);
14905 panic!(
14906 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
14907 );
14908 }
14909 last = Some(lg);
14910 pos = end;
14911 }
14912 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14913 eprintln!(
14914 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
14915 n - start,
14916 (n - start).div_ceil(cmax)
14917 );
14918 }
14919 last
14920 }
14921
14922 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
14923 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
14924 const UMAX: u32 = u32::MAX;
14925 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
14926 const REC: usize = 64;
14927 struct Pack {
14928 data: Vec<f32>,
14929 }
14930 impl Pack {
14931 fn put(&mut self, x: &[f32]) -> u32 {
14932 if x.is_empty() {
14933 return u32::MAX;
14934 }
14935 let off = self.data.len();
14936 self.data.extend_from_slice(x);
14937 off as u32
14938 }
14939 }
14940 let vmf = self.vmf_cfg;
14944 let gdn = self.gdn_cfg;
14945 let mut pack = Pack { data: Vec::new() };
14946 let mut meta = vec![0u32; HEADER];
14947 meta[0] = self.hidden_size as u32;
14948 meta[1] = self.intermediate_size as u32;
14949 meta[2] = self.vocab_size as u32;
14950 meta[3] = self.num_layers as u32;
14951 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
14952 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
14953 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
14954 if let Some(g) = gdn {
14955 meta[24] = g.num_v_heads as u32;
14956 meta[25] = g.num_k_heads as u32;
14957 meta[26] = g.key_head_dim as u32;
14958 meta[27] = g.value_head_dim as u32;
14959 meta[28] = g.conv_kernel as u32;
14960 meta[29] = g.conv_dim() as u32;
14961 }
14962 meta[7] = self.num_heads as u32;
14963 meta[8] = self.num_kv_heads as u32;
14964 meta[9] = self.head_dim as u32;
14965 meta[10] = self.kv_cache.max_seq_len as u32;
14966 let clusters = self.head_clusters.as_ref().unwrap();
14967 let cluster_count = clusters.len() / self.hidden_size;
14968 if clusters.len() % self.hidden_size != 0
14969 || cluster_count == 0
14970 || cluster_count > 1024
14971 || self.vocab_size % cluster_count != 0
14972 || self.weights.lm_head.rows() < self.vocab_size
14973 || self.weights.final_norm.len() != self.hidden_size
14974 {
14975 return None;
14976 }
14977 meta[11] = cluster_count as u32;
14978 meta[12] = (self.vocab_size / cluster_count) as u32;
14979 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
14980 meta[16] = self.rotary_dim as u32;
14981 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
14982 meta[19] = (self.rms_eps as f32).to_bits();
14983 let max_conv = self
14984 .weights
14985 .layers
14986 .iter()
14987 .filter_map(|lw| match &lw.attn {
14988 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
14989 _ => None,
14990 })
14991 .max()
14992 .unwrap_or(1);
14993 let phase_stride = vmf
14999 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
15000 .unwrap_or(0);
15001 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
15002 let state_stride = phase_stride.max(gdn_stride);
15003 let bounded = self.anchor_core.clone();
15007 let (anchor_window, anchor_sink) = bounded
15008 .as_ref()
15009 .map(|ac| (ac.window, ac.sink))
15010 .unwrap_or((0, 0));
15011 let kv_stride = if bounded.is_some() {
15012 2usize
15013 .saturating_mul(self.num_kv_heads)
15014 .saturating_mul(anchor_window)
15015 .saturating_mul(self.head_dim)
15016 } else {
15017 2usize
15018 .saturating_mul(self.num_kv_heads)
15019 .saturating_mul(self.kv_cache.max_seq_len)
15020 .saturating_mul(self.head_dim)
15021 };
15022 meta[14] = state_stride as u32;
15023 meta[15] = kv_stride as u32;
15024 meta[18] = anchor_window as u32;
15025 meta[20] = anchor_sink as u32;
15026 meta[21] = match &self.bounded_rope {
15027 Some(rope) => {
15028 let off = pack.put(&rope.cos);
15030 let _ = pack.put(&rope.sin);
15031 off
15032 }
15033 None => UMAX,
15034 };
15035 let mut full_seen = false;
15036 let mut bounded_seen = 0usize;
15037 let mut phase_seen = 0usize;
15041 let mut gdn_seen = 0usize;
15042 for (li, lw) in self.weights.layers.iter().enumerate() {
15043 let base = meta.len();
15044 meta.resize(base + REC, UMAX);
15045 meta[base] = match &lw.attn {
15046 AttnKind::Linear(w) if w.phase_delta => 1,
15047 AttnKind::Linear(_) => 0,
15048 AttnKind::Full { .. } => 2,
15049 AttnKind::Bounded(_) => 3,
15050 AttnKind::LinearGdn(_) => 4,
15051 _ => UMAX,
15052 };
15053 meta[base + 1] = pack.put(&lw.input_norm);
15054 meta[base + 2] = pack.put(&lw.post_norm);
15055 meta[base + 25] = match &lw.attn {
15056 AttnKind::Linear(_) => {
15057 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15058 phase_seen += 1;
15059 off
15060 }
15061 AttnKind::LinearGdn(_) => {
15062 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15063 gdn_seen += 1;
15064 off
15065 }
15066 _ => UMAX,
15067 };
15068 match &lw.attn {
15069 AttnKind::LinearGdn(w) => {
15070 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15073 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15074 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15075 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15076 meta[base + 60] = pack.put(&w.conv1d);
15077 meta[base + 61] = pack.put(&w.a_log);
15078 meta[base + 62] = pack.put(&w.dt_bias);
15079 meta[base + 63] = pack.put(&w.norm);
15080 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15081 meta[base + 24] = 0;
15082 }
15083 AttnKind::Linear(w) => {
15084 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15085 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15086 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15087 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15088 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15089 meta[base + 7] = pack.put(&decay);
15090 if let Some((kg, kb)) = &w.k_gate {
15091 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15092 meta[base + 9] = pack.put(kb);
15093 }
15094 if let Some(conv) = &w.conv {
15095 meta[base + 10] = pack.put(conv);
15096 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15097 } else {
15098 meta[base + 24] = 0;
15099 }
15100 }
15101 AttnKind::Full { wq, wk, wv, wo, .. } => {
15102 full_seen = true;
15103 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15104 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15105 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15106 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15107 meta[base + 26] = (li * kv_stride) as u32;
15108 }
15109 AttnKind::Bounded(w) => {
15110 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15111 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15112 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15113 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15114 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15116 meta[base + 27] = pack.put(&w.sink_k);
15117 meta[base + 28] = pack.put(&w.sink_v);
15118 bounded_seen += 1;
15119 }
15120 _ => return None,
15121 }
15122 match &lw.ffn {
15123 FfnKind::Dense(d) => {
15124 meta[base + 15] = 0;
15125 meta[base + 16] = 0;
15126 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15127 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15128 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15129 }
15130 FfnKind::Moe(m) => {
15131 let r = m.resonance.as_ref().unwrap();
15132 let (shared, _) = m.shared.as_ref().unwrap();
15133 meta[base + 15] = 1;
15134 meta[base + 16] = m.experts.len() as u32;
15135 meta[base + 17] = pack.put(&r.mu);
15136 meta[base + 18] = pack.put(&r.u);
15137 meta[base + 19] = pack.put(&r.bias);
15138 meta[base + 20] = r.k as u32;
15139 let mut shell = r.effective_shell(m.experts.len());
15148 shell.push(f32::NEG_INFINITY);
15149 meta[base + 30] = pack.put(&shell);
15150 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15151 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15152 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15153 for (e, ex) in m.experts.iter().enumerate() {
15154 meta[base + 32 + e * 3] =
15155 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15156 meta[base + 33 + e * 3] =
15157 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15158 meta[base + 34 + e * 3] =
15159 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15160 }
15161 }
15162 FfnKind::DenseMoe(_) => return None,
15163 }
15164 }
15165 if !full_seen && self.num_layers == 0 {
15166 return None;
15167 }
15168 let id = {
15169 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15170 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15171 };
15172 let model = crate::gpu::EmbryoGraphModel {
15173 id,
15174 hidden: self.hidden_size,
15175 intermediate: self.intermediate_size,
15176 vocab: self.vocab_size,
15177 layers: self.num_layers,
15178 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15179 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15180 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15181 anchor_q_heads: self.num_heads,
15182 anchor_kv_heads: self.num_kv_heads,
15183 anchor_head_dim: self.head_dim,
15184 rotary_dim: self.rotary_dim,
15185 max_seq: self.kv_cache.max_seq_len,
15186 cluster_count,
15187 cluster_size: self.vocab_size / cluster_count,
15188 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15189 state_stride,
15190 kv_stride,
15191 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15192 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15193 weights: pack.data,
15194 meta,
15195 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15196 clusters: clusters.as_ref().clone(),
15197 final_norm: self.weights.final_norm.clone(),
15198 inv_freq: self.inv_freq.as_ref().clone(),
15199 bounded: bounded.is_some(),
15200 kv_layers: if bounded.is_some() {
15201 bounded_seen
15202 } else {
15203 self.num_layers
15204 },
15205 state_layers: phase_seen + gdn_seen,
15206 anchor_window,
15207 anchor_sink,
15208 phase_layers: phase_seen,
15209 gdn_layers: gdn_seen,
15210 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15211 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15212 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15213 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15214 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15215 };
15216 self.embryo_graph = Some(std::sync::Arc::new(model));
15217 }
15218 self.embryo_graph.clone()
15219 }
15220
15221 fn forward_layers_span(
15222 &mut self,
15223 hidden: &[f32],
15224 position: usize,
15225 task_mask: Option<&TaskMask>,
15226 from: usize,
15227 upto: Option<usize>,
15228 ) -> Vec<f32> {
15229 debug_assert!(
15230 from == 0
15231 || (self.dsv4.is_none()
15232 && self.dsv41.is_none()
15233 && self.qwen4_exp.is_none()
15234 && self.g3n.is_none())
15235 );
15236 #[cfg(target_os = "macos")]
15242 if !crate::gpu_metal::wait_replay() {
15243 self.fail_metal_graph("the pending async replay failed before a plain forward");
15244 return vec![0.0; self.hidden_size];
15245 }
15246 if let Some(b) = &mut self.qwen4_exp {
15247 let _ = (task_mask, upto);
15248 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15249 let mut logits = Vec::new();
15250 crate::qwen4_exp::forward_token(
15251 &b.0,
15252 &b.1,
15253 &b.2,
15254 &mut b.3,
15255 token_id,
15256 position,
15257 &self.inv_freq,
15258 self.pool.as_deref(),
15259 &mut logits,
15260 true,
15261 );
15262 self.graph_logits = Some(logits);
15263 return vec![0.0; self.hidden_size];
15264 }
15265 if let Some(b) = &mut self.dsv4 {
15271 let _ = (task_mask, upto);
15272 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15273 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15274 st.pos = position;
15275 let mut logits = Vec::new();
15276 crate::dsv4::forward_token(
15277 g,
15278 layers,
15279 &cfg,
15280 st,
15281 token_id,
15282 &self.inv_freq,
15283 self.pool.as_deref(),
15284 &mut logits,
15285 );
15286 self.graph_logits = Some(logits);
15287 self.dspark_probe(position, token_id);
15288 return vec![0.0; self.hidden_size];
15291 }
15292 if let Some(b) = &mut self.dsv41 {
15294 let _ = (task_mask, upto);
15295 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15296 let mut logits = Vec::new();
15297 crate::dsv41::forward_token(
15298 &b.0,
15299 &b.1,
15300 &b.2,
15301 &mut b.3,
15302 token_id,
15303 position,
15304 self.pool.as_deref(),
15305 &mut logits,
15306 );
15307 self.graph_logits = Some(logits);
15308 return vec![0.0; self.hidden_size];
15309 }
15310 if let Some(b) = &self.g3n {
15313 let _ = (task_mask, upto);
15314 return crate::g3n::g3n_forward(
15315 &b.0,
15316 &b.1,
15317 hidden,
15318 position,
15319 &mut self.kv_cache.layers,
15320 self.num_heads,
15321 self.num_kv_heads,
15322 self.head_dim,
15323 self.pool.as_deref(),
15324 );
15325 }
15326 if from == 0
15333 && upto.is_none()
15334 && task_mask.is_none()
15335 && self.anchor_core.is_some()
15336 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15337 {
15338 static ONCE: std::sync::Once = std::sync::Once::new();
15339 ONCE.call_once(|| {
15340 eprintln!(
15341 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15342 unsupported={} eligible={}",
15343 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15344 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15345 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15346 crate::gpu::enabled_here(),
15347 self.graph_refused(),
15348 self.embryo_resident_eligible(),
15349 );
15350 });
15351 }
15352 if from == 0
15353 && upto.is_none()
15354 && task_mask.is_none()
15355 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15359 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15360 && matches!(
15364 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15365 Ok("1") | Ok("parallel")
15366 )
15367 && crate::gpu::enabled_here()
15368 && !self.graph_refused()
15369 && (position == 0 || self.device_sequence_position().is_some())
15375 && self.embryo_resident_eligible()
15376 && let Some(model) = self.ensure_embryo_graph()
15377 {
15378 let mut lg = Vec::new();
15379 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15380 {
15381 self.graph_logits = Some(lg);
15382 return vec![0.0; self.hidden_size];
15383 }
15384 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15385 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15386 }
15387 self.mark_graph_refused();
15393 if position != 0 {
15394 self.kv_cache.clear();
15398 self.clear_history();
15399 crate::gpu::graph_kv_reset(self.graph_kv_id);
15400 panic!(
15401 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15402 );
15403 }
15404 }
15405 let mut h = hidden.to_vec();
15406 self.mimo_moe_prepare();
15409 let _mimo_q8 = self.mimo_moe.is_on()
15410 .then(crate::qtensor::enter_full_gpu_q8_scope);
15411 let (nh, _nkv, _hd, hs, _rd, eps) = (
15414 self.num_heads,
15415 self.num_kv_heads,
15416 self.head_dim,
15417 self.hidden_size,
15418 self.rotary_dim,
15419 self.rms_eps,
15420 );
15421 let pool = self.pool.clone();
15422 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15434 let graph_on = match graph_env.as_deref() {
15435 Some("0") => false,
15436 Some("prefill") => false, Some(_) => true,
15438 None => crate::gpu::wgpu_graph_default(),
15444 };
15445 let graph_trusted =
15446 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15447 let race_eligible = graph_on
15448 && upto.is_none()
15449 && task_mask.is_none()
15450 && from == 0
15451 && !self.graph_refused();
15452 let mut tail_start = 0usize;
15453 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15454 let t_graph = std::time::Instant::now();
15455 let mut lg = Vec::new();
15456 let mut gl = 0usize;
15457 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15458 let declined = built.is_none();
15459 let built = match built {
15460 Some(Ok(hh)) => Some(hh),
15461 Some(Err(())) => {
15462 self.clear_sequence_state();
15466 self.graph_failed
15467 .store(true, std::sync::atomic::Ordering::Relaxed);
15468 self.cancel
15469 .store(true, std::sync::atomic::Ordering::Relaxed);
15470 tracing::error!("token graph failed after admission; sequence state cleared");
15471 return vec![0.0; self.hidden_size];
15472 }
15473 None => None,
15474 };
15475 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15480 self.mark_graph_refused();
15481 }
15482 graph_note(built.is_some(), gl, self.num_layers);
15483 if let Some(hh) = built {
15484 let dur = t_graph.elapsed();
15485 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15486 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15487 }
15488 if gl > 0 && gl < self.num_layers {
15489 h = hh;
15495 tail_start = gl;
15496 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15497 if !graph_trusted {
15498 crate::gpu::graph_race_record(true, dur);
15499 }
15500 if !lg.is_empty() {
15501 lg.resize(self.vocab_size, 0.0);
15504 if let Some(c) = self.final_softcap {
15505 softcap_in_place(&mut lg, c, self.pool.as_deref());
15506 }
15507 self.graph_logits = Some(lg);
15508 }
15509 return hh;
15510 }
15511 }
15517 }
15518 let span = from > 0 || upto.is_some();
15542 if span && graph_on && task_mask.is_none() && graph_trusted {
15543 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15544 let mut lg = Vec::new();
15545 let mut gl = 0usize;
15546 let span_res =
15547 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15548 let span_res = match span_res {
15549 Some(Ok(hh)) => Some(hh),
15550 Some(Err(())) => {
15551 self.clear_sequence_state();
15552 self.graph_failed
15553 .store(true, std::sync::atomic::Ordering::Relaxed);
15554 self.cancel
15555 .store(true, std::sync::atomic::Ordering::Relaxed);
15556 tracing::error!(
15557 "span token graph failed after admission; sequence state cleared"
15558 );
15559 return vec![0.0; self.hidden_size];
15560 }
15561 None => None,
15562 };
15563 graph_note(span_res.is_some(), gl, upto_excl - from);
15564 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15565 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15569 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15570 eprintln!(
15571 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15572 upto_excl - from,
15573 span_res.is_some()
15574 );
15575 }
15576 }
15577 if let Some(hh) = span_res {
15578 if gl == upto_excl - from {
15579 if !lg.is_empty() {
15580 lg.resize(self.vocab_size, 0.0);
15581 if let Some(c) = self.final_softcap {
15582 softcap_in_place(&mut lg, c, self.pool.as_deref());
15583 }
15584 self.graph_logits = Some(lg);
15585 }
15586 crate::gpu::set_layer(-1);
15587 return hh;
15588 }
15589 h = hh;
15591 tail_start = from + gl;
15592 }
15593 }
15594 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15599
15600 let host_tail = tail_start > from;
15610 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15611 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15612
15613 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15614 #[cfg(target_os = "macos")]
15615 let mut gpu_skip_until = 0usize;
15616 for li in tail_start.max(from)..self.num_layers {
15617 let _capacity_tail = automatic_gpu_prefix
15618 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15619 .map(|_| crate::gpu::enter_cpu_scope());
15620 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15622 if li > u {
15623 break;
15624 }
15625 }
15626 if let Some(mask) = task_mask {
15627 if !mask.layer_alive(li) {
15628 continue; }
15630 }
15631 #[cfg(target_os = "macos")]
15635 {
15636 if li < gpu_skip_until {
15637 continue;
15638 }
15639 if task_mask.is_none() {
15640 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15641 if self
15642 .graph_failed
15643 .load(std::sync::atomic::Ordering::Relaxed)
15644 {
15645 return vec![0.0; self.hidden_size];
15649 }
15650 if end > li {
15651 gpu_skip_until = end;
15652 if self.is_loop_end(end - 1) && end < self.num_layers {
15655 h = inference::rms_norm(
15656 &h,
15657 &self.weights.final_norm,
15658 self.rms_eps,
15659 self.norm_style,
15660 );
15661 }
15662 continue;
15663 }
15664 }
15665 }
15666
15667 if task_mask.is_none() {
15668 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15669 crate::gpu::BatchGraphOutcome::Completed => continue,
15670 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15671 crate::gpu::BatchGraphOutcome::Declined => {},
15672 }
15673 }
15674 #[cfg(feature = "gpu")]
15675 self.pull_lagging_host_kv(li, li + 1, position);
15676 let lw = &self.weights.layers[self.phys_layer(li)];
15677 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15678 if tp.parse::<usize>().ok() == Some(position) {
15679 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
15680 eprintln!(
15681 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15682 h[0], h[1]
15683 );
15684 }
15685 }
15686 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15689 inference::rms_norm_into(
15690 &h,
15691 &lw.input_norm,
15692 self.rms_eps,
15693 self.norm_style,
15694 &mut self.ws.n1,
15695 );
15696 drop(prof);
15697
15698 let attn_out = match &lw.attn {
15699 AttnKind::Mla(w) => {
15700 let inv_freq_l = self.layer_inv_freq(li);
15701 let rs = self.layer_rope_scale(li);
15702 let eps = self.rms_eps;
15703 let pool = self.pool.clone();
15704 mla_attention(
15705 w,
15706 &self.ws.n1,
15707 &mut self.kv_cache.layers[li],
15708 position,
15709 &inv_freq_l,
15710 rs,
15711 eps,
15712 pool.as_deref(),
15713 )
15714 }
15715 AttnKind::Linear(w) => {
15716 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
15717 vmf_phase_forward(
15718 &self.ws.n1,
15719 w,
15720 &cfg,
15721 &mut self.kv_cache.layers[li].linear_state,
15722 self.pool.as_deref(),
15723 )
15724 }
15725 AttnKind::Kda(w) => {
15726 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15727 crate::linear_core::kda_forward(
15728 &self.ws.n1,
15729 w,
15730 &cfg,
15731 &mut self.kv_cache.layers[li].linear_state,
15732 self.pool.as_deref(),
15733 )
15734 }
15735 AttnKind::LinearGdn(w) => {
15736 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15737 gdn_forward(
15738 &self.ws.n1,
15739 w,
15740 &cfg,
15741 &mut self.kv_cache.layers[li].linear_state,
15742 self.pool.as_deref(),
15743 )
15744 }
15745 AttnKind::ShortConv(w) => {
15746 let cfg = self
15747 .short_conv_cfg
15748 .expect("short-conv layer without short_conv_cfg");
15749 short_conv_forward(
15750 &self.ws.n1,
15751 w,
15752 &cfg,
15753 &mut self.kv_cache.layers[li].linear_state,
15754 self.pool.as_deref(),
15755 )
15756 }
15757 AttnKind::Bounded(w) => {
15758 let rope = self
15761 .bounded_rope
15762 .clone()
15763 .expect("bounded layer without an installed rotation table");
15764 let cfg = crate::bounded::BoundedAttnCfg {
15765 num_heads: self.num_heads,
15766 num_kv_heads: self.num_kv_heads,
15767 head_dim: self.head_dim,
15768 hidden_size: hs,
15769 scale: self.attn_scale,
15770 rope: &rope,
15771 pool: pool.as_deref(),
15772 };
15773 crate::bounded::bounded_attention(
15774 &self.ws.n1,
15775 w,
15776 &mut self.kv_cache.layers[li],
15777 &cfg,
15778 )
15779 }
15780 AttnKind::Full {
15781 wq,
15782 wk,
15783 wv,
15784 wo,
15785 q_norm,
15786 k_norm,
15787 output_gate,
15788 softplus_gate,
15789 bias,
15790 } if self.kv_cache.layers[li].o1_sealed() => {
15791 let inv_freq_l = self.layer_inv_freq(li);
15794 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15795 let cfg = QwenAttnCfg {
15796 num_heads: self.layer_num_heads(li),
15797 num_kv_heads: nkv_l,
15798 head_dim: hd_l,
15799 hidden_size: hs,
15800 position,
15801 inv_freq: &inv_freq_l,
15802 rotary_dim: rd_l,
15803 scale: self.attn_scale,
15804 softcap: self.attn_softcap,
15805 window: None,
15806 v_norm: self.attn_v_norm,
15807 qk_norm_after_rope: self.qk_norm_after_rope,
15808 gate_sigmoid: self.proj_gate_sigmoid,
15809 q_norm: q_norm.as_deref(),
15810 k_norm: k_norm.as_deref(),
15811 output_gate: *output_gate,
15812 softplus_gate: softplus_gate
15813 .as_ref()
15814 .map(|(gate, per_head)| (gate, *per_head)),
15815 rope_scale: self.layer_rope_scale(li),
15816 bias: bias
15817 .as_ref()
15818 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15819 rms_eps: eps,
15820 norm_style: self.norm_style,
15821 pool: pool.as_deref(),
15822 v_head_dim: self.layer_v_dim(li),
15823 };
15824 attention::qwen_attention_nystrom(
15825 &self.ws.n1,
15826 wq,
15827 wk,
15828 wv,
15829 wo,
15830 &mut self.kv_cache.layers[li],
15831 &cfg,
15832 )
15833 }
15834 AttnKind::Full {
15835 wq,
15836 wk,
15837 wv,
15838 wo,
15839 q_norm,
15840 k_norm,
15841 output_gate,
15842 softplus_gate,
15843 bias,
15844 } => 'attn: {
15845 let dropin_reason =
15850 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
15851 if let Some(reason) = dropin_reason {
15852 self.note_graph_decline("wgpu attn dropin", reason);
15853 }
15854 if graph_on
15855 && dropin_reason.is_none()
15856 && !*output_gate
15857 && softplus_gate.is_none()
15858 && self.attention_heads_per_layer.is_none()
15859 && bias.is_none()
15860 && task_mask.is_none()
15861 {
15862 let inv_freq_l = self.layer_inv_freq(li);
15863 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15864 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15865 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
15866 wq.mapped_q1(),
15867 wk.mapped_q1(),
15868 wv.mapped_q1(),
15869 wo.mapped_q1(),
15870 ) {
15871 let gm = gm.clone();
15872 let mut out = vec![0f32; hs];
15873 let cache = &self.kv_cache.layers[li];
15874 if crate::gpu::attn_dropin(
15875 &gm,
15876 self.graph_kv_id,
15877 li,
15878 &self.ws.n1,
15879 qi,
15880 ki,
15881 vi,
15882 oi,
15883 q_norm.as_deref(),
15884 k_norm.as_deref(),
15885 self.qk_norm_after_rope,
15886 &inv_freq_l,
15887 nh,
15888 nkv_l,
15889 hd_l,
15890 rd_l,
15891 hs,
15892 position,
15893 self.kv_cache.max_seq_len,
15894 gemma,
15895 eps as f32,
15896 cache.k_heads(),
15897 cache.v_heads(),
15898 &mut out,
15899 ) {
15900 break 'attn out;
15901 }
15902 }
15903 }
15904 let masked = task_mask
15905 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
15906 .unwrap_or(false);
15907 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
15908 let plain = self.layer_attn_plain(li);
15911 match (masked, f32_view) {
15912 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
15915 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
15916 attention::multi_head_attention(
15917 &self.ws.n1,
15918 q,
15919 k,
15920 v,
15921 o,
15922 &mut self.kv_cache.layers[li],
15923 self.num_heads,
15924 self.num_kv_heads,
15925 self.head_dim,
15926 self.hidden_size,
15927 position,
15928 &active_heads,
15929 &self.inv_freq,
15930 )
15931 }
15932 (masked, _) => {
15933 if masked {
15934 tracing::warn!(
15935 "layer {li}: head mask on quantized weights or on a \
15936 window/sink/per-layer-geometry layer not supported \
15937 yet — executing dense"
15938 );
15939 }
15940 let inv_freq_l = self.layer_inv_freq(li);
15941 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15942 let cfg = QwenAttnCfg {
15943 num_heads: self.layer_num_heads(li),
15944 num_kv_heads: nkv_l,
15945 head_dim: hd_l,
15946 hidden_size: hs,
15947 position,
15948 inv_freq: &inv_freq_l,
15949 rotary_dim: rd_l,
15950 scale: self.attn_scale,
15951 softcap: self.attn_softcap,
15952 window: self.layer_window(li),
15953 v_norm: self.attn_v_norm,
15954 qk_norm_after_rope: self.qk_norm_after_rope,
15955 gate_sigmoid: self.proj_gate_sigmoid,
15956 q_norm: q_norm.as_deref(),
15957 k_norm: k_norm.as_deref(),
15958 output_gate: *output_gate,
15959 softplus_gate: softplus_gate
15960 .as_ref()
15961 .map(|(gate, per_head)| (gate, *per_head)),
15962 rope_scale: self.layer_rope_scale(li),
15963 bias: bias
15964 .as_ref()
15965 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15966 rms_eps: eps,
15967 norm_style: self.norm_style,
15968 pool: pool.as_deref(),
15969 v_head_dim: self.layer_v_dim(li),
15970 };
15971 attention::qwen_attention(
15972 &self.ws.n1,
15973 wq,
15974 wk,
15975 wv,
15976 wo,
15977 &mut self.kv_cache.layers[li],
15978 &cfg,
15979 )
15980 }
15981 }
15982 }
15983 };
15984 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
15987 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
15988 None => attn_out,
15989 };
15990 let lw = &self.weights.layers[self.phys_layer(li)];
15991 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15992 inference::add_rmsnorm_fused_into(
15993 &mut h,
15994 &attn_out,
15995 &lw.post_norm,
15996 self.rms_eps,
15997 self.norm_style,
15998 &mut self.ws.p1,
15999 );
16000 drop(prof);
16001 let mut attn_out = attn_out;
16002 attention::recycle_buf(&mut attn_out);
16003 let post_normed = &self.ws.p1;
16004
16005 let ffn_masked = task_mask
16006 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
16007 .unwrap_or(false);
16008 let ffn_out = match (ffn_masked, &lw.ffn) {
16020 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
16024 let row = task_mask
16025 .and_then(|tm| tm.ffn_masks.get(li))
16026 .map(|v| v.as_slice());
16027 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
16028 }
16029 (true, FfnKind::Dense(d)) => {
16030 let tm = task_mask.unwrap();
16031 let alive = tm.ffn_active_count(li);
16032 let deep = alive * 2 <= self.intermediate_size;
16033 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
16034 let active = tm.ffn_active_indices(li);
16035 sparse_ffn_quant(
16036 d,
16037 post_normed,
16038 &active,
16039 self.hidden_size,
16040 self.pool.as_deref(),
16041 )
16042 } else if deep
16043 && let (Some(g), Some(u), Some(dn)) = (
16044 d.gate_proj.as_f32(),
16045 d.up_proj.as_f32(),
16046 d.down_proj.as_f32(),
16047 )
16048 {
16049 let active = tm.ffn_active_indices(li);
16050 inference::sparse_ffn_forward(
16051 post_normed,
16052 g,
16053 u,
16054 dn,
16055 self.hidden_size,
16056 self.intermediate_size,
16057 &active,
16058 self.pool.as_deref(),
16059 )
16060 } else {
16061 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16062 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16063 }
16064 }
16065 (true, FfnKind::Moe(m)) => {
16066 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16070 ffn_forward(
16071 &lw.ffn,
16072 post_normed,
16073 self.pool.as_deref(),
16074 allowed.as_deref(),
16075 )
16076 }
16077 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16078 dm,
16079 post_normed,
16080 &h,
16081 self.rms_eps,
16082 self.norm_style,
16083 self.pool.as_deref(),
16084 ),
16085 (false, _) => match &lw.ffn {
16086 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16087 dm,
16088 post_normed,
16089 &h,
16090 self.rms_eps,
16091 self.norm_style,
16092 self.pool.as_deref(),
16093 ),
16094 FfnKind::Moe(m)
16095 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16096 {
16097 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16098 }
16099 _ => {
16100 let allowed = match (&lw.ffn, task_mask) {
16101 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16102 _ => None,
16103 };
16104 ffn_forward(
16105 &lw.ffn,
16106 post_normed,
16107 self.pool.as_deref(),
16108 allowed.as_deref(),
16109 )
16110 }
16111 },
16112 };
16113 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16114 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16115 None => ffn_out,
16116 };
16117 for (i, &f) in ffn_out.iter().enumerate() {
16118 h[i] += f;
16119 }
16120 let mut ffn_out = ffn_out;
16121 attention::recycle_buf(&mut ffn_out);
16122
16123 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16125 for v in h.iter_mut() {
16126 *v *= sc;
16127 }
16128 }
16129 if self.layer_dump.is_some() {
16131 self.dump_layer_row(position, li, &h);
16132 }
16133
16134 if self.is_loop_end(li) && li + 1 < self.num_layers {
16137 h = inference::rms_norm(
16138 &h,
16139 &self.weights.final_norm,
16140 self.rms_eps,
16141 self.norm_style,
16142 );
16143 }
16144
16145 if self.dyn_phi_layer == Some(li) {
16149 self.update_dyn_phi(&h);
16150 }
16151 }
16152 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16154 crate::gpu::graph_race_record(false, t.elapsed());
16155 }
16156
16157 h
16158 }
16159
16160 fn update_dyn_phi(&mut self, h: &[f32]) {
16163 const A: f32 = 0.2;
16164 if self.dyn_phi_ema.len() != h.len() {
16165 self.dyn_phi_ema = vec![0.0; h.len()];
16166 self.dyn_phi_seen = 0;
16167 }
16168 if self.dyn_phi_seen == 0 {
16169 self.dyn_phi_ema.copy_from_slice(h);
16170 } else {
16171 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16172 *e = (1.0 - A) * *e + A * v;
16173 }
16174 }
16175 self.dyn_phi_seen += 1;
16176 }
16177
16178 pub fn dyn_phi(&self) -> &[f32] {
16180 &self.dyn_phi_ema
16181 }
16182
16183 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16185 self.dyn_phi_layer = layer;
16186 self.dyn_phi_ema.clear();
16187 self.dyn_phi_seen = 0;
16188 }
16189
16190 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16192 let Some(model) = &self.model else {
16193 return Vec::new();
16194 };
16195 model
16196 .header
16197 .skills
16198 .iter()
16199 .enumerate()
16200 .filter_map(|(i, sk)| {
16201 if sk.is_v2() {
16205 return None;
16206 }
16207 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16208 let sel = sk.selection.as_ref()?;
16209 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16210 })
16211 .collect()
16212 }
16213
16214 pub fn active_skill(&self) -> Option<usize> {
16216 self.dyn_active
16217 }
16218
16219 pub fn enable_dynamic_routing(&mut self) -> usize {
16224 use crate::swarm::{DynRouter, RoutableSkill};
16225 let Some(model) = self.model.clone() else {
16226 return 0;
16227 };
16228 if let Some(r) = &model.header.router {
16235 tracing::warn!(
16236 "dynamic routing disabled: this file declares router policy '{}' with \
16237 granularity \"{}\" — the request-level decision applies instead",
16238 r.policy,
16239 r.granularity
16240 );
16241 return 0;
16242 }
16243 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16249 || model.header.skills.iter().any(|s| s.is_v2())
16250 {
16251 tracing::warn!(
16252 "dynamic routing disabled: this file carries format-v2 skill records \
16253 (SKILLS_V2) — they route per request through a router policy only"
16254 );
16255 return 0;
16256 }
16257 if self.dyn_blend_loaded {
16260 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16261 return 0;
16262 }
16263 if let Some(a) = self.dyn_active {
16267 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16268 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16269 return 0;
16270 }
16271 }
16272 let hidden = self.hidden_size;
16273 let mut skills = Vec::new();
16274 for (idx, id, _phi) in self.dynamic_skills() {
16275 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16276 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16277 skills.push(rs);
16278 }
16279 }
16280 }
16281 if skills.is_empty() {
16282 return 0;
16283 }
16284 let phi = skills[0].phi_layer;
16286 if skills.iter().any(|s| s.phi_layer != phi) {
16287 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16288 }
16289 let n = skills.len();
16290 self.set_dyn_phi_layer(Some(phi));
16291 self.dyn_router = Some(DynRouter::new(skills));
16292 n
16293 }
16294
16295 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16297 self.dyn_router
16298 .as_ref()
16299 .map(|r| r.switches.clone())
16300 .unwrap_or_default()
16301 }
16302
16303 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16306 let _mimo_q8 = self.mimo_moe.is_on()
16307 .then(crate::qtensor::enter_full_gpu_q8_scope);
16308 let rows = self.weights.lm_head.rows();
16309 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16310 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16314 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16315 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16316 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16317 rows, self.hidden_size, &mut logits)
16318 });
16319 if !served {
16320 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16321 }
16322 logits.resize(self.vocab_size, 0.0);
16323 if let Some(m) = self.logit_multiplier {
16324 for l in logits.iter_mut() {
16325 *l *= m;
16326 }
16327 }
16328 if let Some(c) = self.final_softcap {
16329 softcap_in_place(&mut logits, c, self.pool.as_deref());
16330 }
16331 if let Some(cm) = self.head_clusters.as_ref() {
16332 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16333 }
16334 logits
16335 }
16336
16337 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16340 let h = hidden.len();
16341 let ncl = cm.len() / h.max(1);
16342 if ncl == 0 || logits.len() % ncl != 0 {
16343 return;
16344 }
16345 let cs = logits.len() / ncl;
16346 let mut lc = vec![0.0f32; ncl];
16348 for c in 0..ncl {
16349 let row = &cm[c * h..(c + 1) * h];
16350 let mut s = 0.0f32;
16351 for j in 0..h {
16352 s += row[j] * hidden[j];
16353 }
16354 lc[c] = s;
16355 }
16356 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16357 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16358 for c in 0..ncl {
16359 let blk = &mut logits[c * cs..(c + 1) * cs];
16360 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16361 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16362 let add = lc[c] - lse - bl;
16363 for v in blk.iter_mut() {
16364 *v += add;
16365 }
16366 }
16367 }
16368
16369 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16374 #[cfg(target_os = "macos")]
16375 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16376 self.clear_sequence_state();
16377 crate::gpu::graph_race_begin_generation();
16381 if task_mask.is_none() {
16382 self.o1_begin();
16383 }
16384 let mut hidden = vec![0.0f32; self.hidden_size];
16385 for (pos, &id) in ids.iter().enumerate() {
16386 let emb = self.embed_single(id);
16387 hidden = self.forward_layers(&emb, pos, task_mask);
16388 }
16389 if let Err(err) = self.o1_seal_checked() {
16390 self.o1_fail(err);
16391 }
16392 if let Some(logits) = self.graph_logits.take() {
16395 return logits;
16396 }
16397 inference::rms_norm_into(
16398 &hidden,
16399 &self.weights.final_norm,
16400 self.rms_eps,
16401 self.norm_style,
16402 &mut self.ws.n1,
16403 );
16404 self.lm_head_forward(&self.ws.n1)
16405 }
16406}
16407
16408pub fn create_test_pipeline(
16410 hidden_size: usize,
16411 intermediate_size: usize,
16412 num_heads: usize,
16413 num_kv_heads: usize,
16414 head_dim: usize,
16415 num_layers: usize,
16416 vocab_size: usize,
16417) -> Pipeline {
16418 let synth = |n: usize, salt: usize| -> Vec<f32> {
16421 (0..n)
16422 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16423 .collect()
16424 };
16425 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16426 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16427 };
16428 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16429 .map(|li| LayerWeights {
16430 input_norm: vec![1.0; hidden_size],
16431 post_norm: vec![1.0; hidden_size],
16432 attn_out_norm: None,
16433 ffn_out_norm: None,
16434 layer_scale: None,
16435 ffn: FfnKind::Dense(DenseFfn {
16436 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16437 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16438 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16439 act: Act::Silu,
16440 down_t: None,
16441 segs: Vec::new(),
16442 }),
16443 attn: AttnKind::Full {
16444 bias: None,
16445 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16446 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16447 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16448 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16449 q_norm: None,
16450 k_norm: None,
16451 output_gate: false,
16452 softplus_gate: None,
16453 },
16454 })
16455 .collect();
16456
16457 Pipeline::new(
16458 Tokenizer::byte_level(),
16459 PipelineWeights {
16460 embed_tokens: qt(vocab_size, hidden_size, 100),
16461 layers: layer_weights,
16462 lm_head: qt(vocab_size, hidden_size, 200),
16463 final_norm: vec![1.0; hidden_size],
16464 },
16465 hidden_size,
16466 intermediate_size,
16467 num_heads,
16468 num_kv_heads,
16469 head_dim,
16470 num_layers,
16471 num_layers, false, vocab_size,
16474 1e-6,
16475 10_000.0,
16476 NormStyle::Qwen,
16477 4096,
16478 SamplerConfig {
16479 seed: Some(42),
16480 ..Default::default()
16481 },
16482 )
16483}
16484
16485#[inline]
16490fn mask_bit(row: &[u8], j: usize) -> bool {
16491 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16492}
16493
16494fn mask_gain() -> f32 {
16505 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16506 *G.get_or_init(|| {
16507 std::env::var("CMF_FFN_MASK_GAIN")
16508 .ok()
16509 .and_then(|v| v.parse().ok())
16510 .unwrap_or(1.0)
16511 })
16512}
16513
16514fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16515 let fill = meanfill().and_then(|(i, v)| {
16518 let li = crate::gpu::cur_layer();
16519 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16520 });
16521 for r in 0..rows {
16522 let base = r * inter;
16523 for (bi, &byte) in row.iter().enumerate() {
16524 if byte == 0xFF {
16525 continue;
16526 }
16527 let j0 = bi * 8;
16528 for bit in 0..8 {
16529 let j = j0 + bit;
16530 if j < inter && byte & (1 << bit) == 0 {
16531 g[base + j] = fill.map_or(0.0, |f| f[j]);
16532 }
16533 }
16534 }
16535 }
16536 let gain = mask_gain();
16537 if gain != 1.0 {
16538 for v in g[..rows * inter].iter_mut() {
16539 *v *= gain;
16540 }
16541 }
16542}
16543
16544#[inline]
16546fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16547 row.is_none_or(|r| mask_bit(r, i))
16548}
16549
16550fn all_bits_on(row: &[u8], n: usize) -> bool {
16553 (0..n).all(|i| mask_bit(row, i))
16554}
16555
16556fn tube_topk() -> usize {
16564 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16565 *K.get_or_init(|| {
16566 std::env::var("CMF_TUBE_TOPK")
16567 .ok()
16568 .and_then(|v| v.parse().ok())
16569 .unwrap_or(0)
16570 })
16571}
16572
16573fn tube_score_oracle() -> bool {
16574 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16575 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16576}
16577
16578fn tube_ffn_routed(
16585 d: &DenseFfn,
16586 xs: &[f32],
16587 b: usize,
16588 pool: Option<&Pool>,
16589 mask_row: Option<&[u8]>,
16590 k: usize,
16591) -> Vec<f32> {
16592 let hidden = d.down_proj.rows();
16593 let core = d.gate_proj.rows();
16594 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16595 let mut out = match (b, core_full, mask_row) {
16596 (1, true, _) => dense_ffn(d, xs, pool),
16597 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16598 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16599 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16600 };
16601 let cand: Vec<usize> = (0..d.segs.len())
16602 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16603 .collect();
16604 if cand.is_empty() {
16605 return out;
16606 }
16607 let oracle = tube_score_oracle();
16611 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16612 let mut scores = vec![0f32; b * cand.len()];
16613 for (ci, &i) in cand.iter().enumerate() {
16614 let seg = &d.segs[i];
16615 let w = seg.width;
16616 let mut g = vec![0.0f32; b * w];
16617 if b == 1 {
16618 seg.gate.matvec(xs, &mut g, pool);
16619 } else {
16620 seg.gate.matmat(xs, b, &mut g, pool);
16621 }
16622 for v in g.iter_mut() {
16623 *v = Act::Silu.combine(*v, 1.0);
16624 }
16625 if !oracle {
16626 for t in 0..b {
16627 scores[t * cand.len() + ci] =
16628 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16629 }
16630 }
16631 if oracle || b > 1 {
16632 let mut u = vec![0.0f32; b * w];
16633 if b == 1 {
16634 seg.up.matvec(xs, &mut u, pool);
16635 } else {
16636 seg.up.matmat(xs, b, &mut u, pool);
16637 }
16638 for (a, &v) in g.iter_mut().zip(u.iter()) {
16639 *a *= v;
16640 }
16641 if oracle {
16642 for t in 0..b {
16643 scores[t * cand.len() + ci] =
16644 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16645 }
16646 }
16647 }
16648 acts.push(g);
16649 }
16650 let keep = k.min(cand.len());
16652 let mut scratch: Vec<f32> = Vec::new();
16653 for t in 0..b {
16654 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16655 .map(|ci| (scores[t * cand.len() + ci], ci))
16656 .collect();
16657 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
16658 let mut alive = vec![false; cand.len()];
16659 for &(_, ci) in sc.iter().take(keep) {
16660 alive[ci] = true;
16661 }
16662 if b > 1 {
16663 for (ci, a) in acts.iter_mut().enumerate() {
16664 if !alive[ci] {
16665 let w = d.segs[cand[ci]].width;
16666 a[t * w..(t + 1) * w].fill(0.0);
16667 }
16668 }
16669 } else {
16670 for (ci, &i) in cand.iter().enumerate() {
16674 if !alive[ci] {
16675 continue;
16676 }
16677 let seg = &d.segs[i];
16678 let w = seg.width;
16679 let g = &mut acts[ci];
16680 if !tube_score_oracle() {
16681 scratch.clear();
16682 scratch.resize(w, 0.0);
16683 seg.up.matvec(xs, &mut scratch, pool);
16684 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
16685 *a *= v;
16686 }
16687 }
16688 let mut acc = vec![0.0f32; hidden];
16689 seg.down.matvec(g, &mut acc, pool);
16690 for (o, a) in out.iter_mut().zip(&acc) {
16691 *o += *a;
16692 }
16693 }
16694 }
16695 }
16696 if b > 1 {
16697 for (ci, &i) in cand.iter().enumerate() {
16698 let seg = &d.segs[i];
16699 let mut acc = vec![0.0f32; b * hidden];
16700 seg.down.matmat(&acts[ci], b, &mut acc, pool);
16701 for (o, a) in out.iter_mut().zip(&acc) {
16702 *o += *a;
16703 }
16704 }
16705 }
16706 out
16707}
16708
16709fn tube_ffn(
16715 d: &DenseFfn,
16716 xs: &[f32],
16717 b: usize,
16718 pool: Option<&Pool>,
16719 mask_row: Option<&[u8]>,
16720) -> Vec<f32> {
16721 if tube_topk() > 0 {
16722 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
16723 }
16724 let hidden = d.down_proj.rows();
16725 let core = d.gate_proj.rows();
16726 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16727 let mut out = match (b, core_full, mask_row) {
16728 (1, true, _) => dense_ffn(d, xs, pool),
16729 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16730 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16731 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16732 };
16733 TUBE_SCRATCH.with(|sc| {
16734 let mut sc = sc.borrow_mut();
16735 let [g, u, acc] = &mut *sc;
16736 for seg in &d.segs {
16737 if !tube_bit(mask_row, seg.start) {
16738 continue;
16739 }
16740 let w = seg.width;
16741 g.resize(b * w, 0.0);
16742 if b == 1
16743 && d.act == Act::Silu
16744 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
16745 {
16746 } else {
16748 u.resize(b * w, 0.0);
16749 if b == 1 {
16750 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
16751 } else {
16752 seg.gate.matmat(xs, b, g, pool);
16753 seg.up.matmat(xs, b, u, pool);
16754 }
16755 for i in 0..b * w {
16756 g[i] = d.act.combine(g[i], u[i]);
16757 }
16758 }
16759 acc.resize(b * hidden, 0.0);
16760 acc.fill(0.0);
16761 if b == 1 {
16762 seg.down.matvec(g, acc, pool);
16763 } else {
16764 seg.down.matmat(g, b, acc, pool);
16765 }
16766 for (o, a) in out.iter_mut().zip(acc.iter()) {
16767 *o += *a;
16768 }
16769 }
16770 out
16771 })
16772}
16773
16774thread_local! {
16775 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
16779 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
16780}
16781
16782static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
16785 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
16786
16787fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
16791 #[cfg(feature = "gpu")]
16792 {
16793 use std::sync::atomic::Ordering::Relaxed;
16794 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
16795 (
16796 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
16797 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
16798 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
16799 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
16800 )
16801 }
16802 #[cfg(not(feature = "gpu"))]
16803 (0.0, 0.0, 0.0, 0)
16804}
16805
16806fn wgpu_mirror_counters() -> [u64; 6] {
16809 #[cfg(feature = "gpu")]
16810 {
16811 crate::gpu_wgpu::MIRROR_EVENTS
16812 .each_ref()
16813 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
16814 }
16815 #[cfg(not(feature = "gpu"))]
16816 [0; 6]
16817}
16818
16819fn prefill_prof_on() -> bool {
16820 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16821 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
16822}
16823
16824fn dense_ffn_batch(
16825 d: &DenseFfn,
16826 xs: &[f32],
16827 b: usize,
16828 pool: Option<&Pool>,
16829 mask_row: Option<&[u8]>,
16830) -> Vec<f32> {
16831 let inter = d.gate_proj.rows();
16832 let hidden = d.down_proj.rows();
16833 let fused_act = d.act.graph_act();
16843 if mask_row.is_none()
16844 && fused_act.is_some()
16845 && b >= 32
16846 && crate::gpu::enabled_here()
16847 && !crate::gpu::mm_killed()
16848 && refit_dir().is_none()
16853 && !ffn_probe_active()
16858 {
16859 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16860 d.gate_proj.mapped_q4t(),
16861 d.up_proj.mapped_q4t(),
16862 d.down_proj.mapped_q4t(),
16863 ) {
16864 let mut out = vec![0.0f32; b * hidden];
16865 let act = fused_act.expect("checked above");
16866 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
16867 {
16868 return out;
16869 }
16870 }
16871 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16876 d.gate_proj.mapped_q4tp(),
16877 d.up_proj.mapped_q4tp(),
16878 d.down_proj.mapped_q4tp(),
16879 ) {
16880 let mut out = vec![0.0f32; b * hidden];
16881 let act = fused_act.expect("checked above");
16882 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
16883 {
16884 return out;
16885 }
16886 }
16887 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16893 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
16894 d.gate_proj.mapped_device_gemm(),
16895 d.up_proj.mapped_device_gemm(),
16896 d.down_proj.mapped_device_gemm(),
16897 ) {
16898 let mut out = vec![0.0f32; b * hidden];
16899 let act = fused_act.expect("checked above");
16900 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
16901 return out;
16902 }
16903 }
16904 }
16905 let mut g = vec![0.0f32; b * inter];
16906 d.gate_proj.matmat(xs, b, &mut g, pool);
16907 let mut u = vec![0.0f32; b * inter];
16908 d.up_proj.matmat(xs, b, &mut u, pool);
16909 if gate_topk() > 0 && d.act == Act::Silu {
16910 for t in 0..b {
16911 let row = &mut g[t * inter..(t + 1) * inter];
16912 for v in row.iter_mut() {
16913 *v = Act::Silu.combine(*v, 1.0);
16914 }
16915 keep_top_k(row, gate_topk());
16916 }
16917 for i in 0..b * inter {
16918 g[i] *= u[i];
16919 }
16920 } else {
16921 for i in 0..b * inter {
16922 g[i] = d.act.combine(g[i], u[i]);
16923 }
16924 }
16925 if let Some(row) = mask_row {
16926 zero_masked_cols(&mut g, b, inter, row);
16927 }
16928 if oracle_topk() > 0 {
16929 for t in 0..b {
16930 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
16931 }
16932 }
16933 let mut out = vec![0.0f32; b * hidden];
16934 d.down_proj.matmat(&g, b, &mut out, pool);
16935 if refit_dir().is_some() {
16936 let li = crate::gpu::cur_layer();
16937 if li >= 0 {
16938 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
16939 }
16940 }
16941 FFN_PROBE.with(|pr| {
16945 if let Some(acc) = pr.borrow_mut().as_mut() {
16946 let li = crate::gpu::cur_layer();
16947 if li < 0 {
16948 return;
16949 }
16950 let Some(row) = acc.get_mut(li as usize) else {
16951 return;
16952 };
16953 let sq = probe_sq();
16954 for t in 0..b {
16955 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
16956 *a += if sq {
16957 (v as f64) * (v as f64)
16958 } else {
16959 (v as f64).abs()
16960 };
16961 }
16962 }
16963 }
16964 });
16965 out
16966}
16967
16968fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
16973 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16974 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16975 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
16976 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
16977 if (!on && !dump) || b == 0 {
16978 return;
16979 }
16980 let hidden = xs.len() / b;
16981 if on {
16982 let mut acc = m.act_sq.borrow_mut();
16983 if acc.len() < hidden {
16984 acc.resize(hidden, 0.0);
16985 }
16986 for t in 0..b {
16987 let row = &xs[t * hidden..(t + 1) * hidden];
16988 for (a, &v) in acc.iter_mut().zip(row) {
16989 *a += (v as f64) * (v as f64);
16990 }
16991 }
16992 }
16993 if dump {
16994 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
16997 .ok()
16998 .and_then(|v| v.parse().ok())
16999 .unwrap_or(4096);
17000 let mut rows = m.act_rows.borrow_mut();
17001 if rows.len() < cap * hidden {
17002 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
17003 rows.extend_from_slice(&xs[..take * hidden]);
17004 }
17005 }
17006}
17007
17008#[derive(Clone, Copy)]
17011struct SendVecs(*mut Vec<f32>);
17012unsafe impl Send for SendVecs {}
17013unsafe impl Sync for SendVecs {}
17014impl SendVecs {
17015 #[inline]
17016 fn at(self, i: usize) -> *mut Vec<f32> {
17017 unsafe { self.0.add(i) }
17018 }
17019}
17020
17021fn moe_ffn_batch(
17022 m: &MoeFfn,
17023 xs: &[f32],
17024 b: usize,
17025 hidden: usize,
17026 pool: Option<&Pool>,
17027 allowed: Option<&[bool]>,
17028) -> Vec<f32> {
17029 accumulate_act(m, xs, b);
17030 let ne = m.experts.len();
17031 let mut logits = vec![0.0f32; b * ne];
17032 match &m.resonance {
17033 Some(r) => {
17034 let hdim = xs.len() / b.max(1);
17035 for bi in 0..b {
17036 r.scores(
17037 &xs[bi * hdim..(bi + 1) * hdim],
17038 &mut logits[bi * ne..(bi + 1) * ne],
17039 );
17040 }
17041 }
17042 None => m.router.matmat(xs, b, &mut logits, pool),
17043 }
17044
17045 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17048 {
17049 let mut st = m.stats.borrow_mut();
17050 if st.len() < ne {
17051 st.resize(ne, 0);
17052 }
17053 for bi in 0..b {
17054 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17055 for &e in &idx {
17056 st[e] += 1;
17057 assign[e].push((bi, p[e] / wsum));
17058 }
17059 }
17060 }
17061
17062 let mut out = vec![0.0f32; b * hidden];
17063 let cols = m.experts[0].gate_proj.cols();
17064 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17065 let sb = list.len();
17066 let mut sub = vec![0.0f32; sb * cols];
17067 for (k, &(bi, _)) in list.iter().enumerate() {
17068 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17069 }
17070 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17071 for (k, &(bi, w)) in list.iter().enumerate() {
17072 for i in 0..hidden {
17073 out[bi * hidden + i] += w * eo[k * hidden + i];
17074 }
17075 }
17076 };
17077 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17083 if pool.is_some() && active.len() >= 8 {
17084 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17085 {
17086 let panel_ptr = SendVecs(panels.as_mut_ptr());
17087 let experts = &m.experts;
17090 let (active_r, assign_r) = (&active, &assign);
17091 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17092 let run = |start: usize, end: usize| {
17093 let _cpu_scope = inherit_cpu();
17094 #[cfg(target_os = "macos")]
17102 let _metal_cpu = crate::gpu::enter_cpu_scope();
17103 for ai in start..end {
17104 let e = active_r[ai];
17105 let list = &assign_r[e];
17106 let sb = list.len();
17107 let mut sub = vec![0.0f32; sb * cols];
17108 for (k, &(bi, _)) in list.iter().enumerate() {
17109 sub[k * cols..(k + 1) * cols]
17110 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17111 }
17112 unsafe {
17114 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17115 }
17116 }
17117 };
17118 match pool {
17119 Some(p) => p.run_rows(active.len(), &run),
17120 None => run(0, active.len()),
17121 }
17122 }
17123 for (ai, &e) in active.iter().enumerate() {
17124 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17125 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17126 for i in 0..hidden {
17127 out[bi * hidden + i] += w * eo[i];
17128 }
17129 }
17130 }
17131 } else {
17132 for &e in &active {
17133 run_expert(&m.experts[e], &assign[e], &mut out);
17134 }
17135 }
17136 if let Some((se, gate)) = &m.shared {
17137 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17138 let mut gl = vec![0.0f32; b];
17139 gate.matmat(xs, b, &mut gl, pool);
17140 (0..b)
17141 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17142 .collect()
17143 } else {
17144 (0..b).map(|bi| (bi, 1.0)).collect()
17145 };
17146 run_expert(se, &all, &mut out);
17147 }
17148 out
17149}
17150
17151fn moe_ffn_rows_exact(
17163 m: &MoeFfn,
17164 xs: &[f32],
17165 b: usize,
17166 hidden: usize,
17167 pool: Option<&Pool>,
17168) -> Vec<f32> {
17169 let mut out = vec![0.0f32; b * hidden];
17170 let per_row = |out: &mut [f32]| {
17171 for r in 0..b {
17172 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17173 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17174 }
17175 };
17176 let covered = !crate::gpu::enabled_here()
17177 && moe_batch_enabled()
17178 && m.shared.is_none()
17179 && m.resonance.is_none()
17180 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17181 && m.experts.iter().all(|d| d.act == Act::Silu);
17182 if !covered {
17183 per_row(&mut out);
17184 return out;
17185 }
17186 let ne = m.experts.len();
17187 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17189 for r in 0..b {
17190 let x = &xs[r * hidden..(r + 1) * hidden];
17191 accumulate_act(m, x, 1);
17192 let mut logits = vec![0.0f32; ne];
17193 m.router.matvec(x, &mut logits, pool);
17194 let (idx, p, wsum) = moe_route(&logits, m, None);
17195 {
17196 let mut st = m.stats.borrow_mut();
17197 if st.len() < ne {
17198 st.resize(ne, 0);
17199 }
17200 for &e in &idx {
17201 st[e] += 1;
17202 }
17203 }
17204 let w: Vec<f32> = idx
17205 .iter()
17206 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17207 .collect();
17208 routes.push((idx, w));
17209 }
17210 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17211 per_row(&mut out);
17212 return out;
17213 }
17214 let mut experts: Vec<usize> = Vec::new();
17216 let mut groups: Vec<Vec<usize>> = Vec::new();
17217 for (r, (idx, _)) in routes.iter().enumerate() {
17218 for &e in idx {
17219 match experts.iter().position(|&x| x == e) {
17220 Some(g) => groups[g].push(r),
17221 None => {
17222 experts.push(e);
17223 groups.push(vec![r]);
17224 }
17225 }
17226 }
17227 }
17228 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17229 let inter = m.experts[experts[0]].gate_proj.rows();
17230 let pairs: Vec<(&QTensor, &QTensor)> = experts
17231 .iter()
17232 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17233 .collect();
17234 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17235 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17236 per_row(&mut out);
17237 return out;
17238 }
17239 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17240 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17241 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17242 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17243 per_row(&mut out);
17244 return out;
17245 }
17246 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17248 let mut p = 0usize;
17249 for (g, &e) in experts.iter().enumerate() {
17250 for &r in &groups[g] {
17251 slot.insert((r, e), p);
17252 p += 1;
17253 }
17254 }
17255 for (r, (idx, w)) in routes.iter().enumerate() {
17256 let terms: Vec<(&[f32], f32)> = idx
17257 .iter()
17258 .zip(w)
17259 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17260 .collect();
17261 let row = &mut out[r * hidden..(r + 1) * hidden];
17262 for (i, dst) in row.iter_mut().enumerate() {
17263 let mut acc = 0f32;
17265 for (d, we) in &terms {
17266 acc += we * d[i];
17267 }
17268 *dst = acc;
17269 }
17270 }
17271 out
17272}
17273
17274pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17278 match pool {
17279 Some(p) if v.len() >= 1 << 15 => {
17280 let base = v.as_mut_ptr() as usize;
17281 p.run_rows(v.len(), &|s, e| {
17282 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17285 for x in sl {
17286 *x = c * (*x / c).tanh();
17287 }
17288 });
17289 }
17290 _ => {
17291 for x in v.iter_mut() {
17292 *x = c * (*x / c).tanh();
17293 }
17294 }
17295 }
17296}
17297
17298thread_local! {
17299 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17303 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17304}
17305
17306fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17308 if gate_topk() > 0
17311 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17312 {
17313 return out;
17314 }
17315 let prism_body = d.gate_proj.has_prism_contract()
17331 || d.up_proj.has_prism_contract()
17332 || d.down_proj.has_prism_contract();
17333 if !prism_body
17334 && crate::gpu::enabled_here()
17335 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17336 {
17337 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17338 crate::gpu::ProbeArm::Gpu
17339 } else {
17340 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17341 };
17342 match arm {
17343 crate::gpu::ProbeArm::Gpu => {
17344 let t0 = std::time::Instant::now();
17345 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17346 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17347 return out;
17348 }
17349 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17353 }
17354 crate::gpu::ProbeArm::CpuTimed => {
17355 let t0 = std::time::Instant::now();
17356 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17357 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17358 return out;
17359 }
17360 crate::gpu::ProbeArm::Cpu => {
17361 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17362 }
17363 }
17364 }
17365 dense_ffn_cpu(d, x, pool)
17366}
17367
17368fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17370 let inter = d.gate_proj.rows();
17371 FFN_SCRATCH.with(|s| {
17372 let mut s = s.borrow_mut();
17373 let [g, u, ..] = &mut *s;
17374 g.resize(inter, 0.0);
17375 if gate_topk() > 0 {
17378 u.resize(inter, 0.0);
17382 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17383 for i in 0..inter {
17384 g[i] = Act::Silu.combine(g[i], 1.0);
17385 }
17386 keep_top_k(g, gate_topk());
17387 for i in 0..inter {
17388 g[i] *= u[i];
17389 }
17390 } else if d.act == Act::Silu && {
17391 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17392 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17393 } {
17394 } else {
17396 u.resize(inter, 0.0);
17397 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17399 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17400 for i in 0..inter {
17401 g[i] = d.act.combine(g[i], u[i]);
17402 }
17403 }
17404 FFN_PROBE.with(|pr| {
17412 if let Some(acc) = pr.borrow_mut().as_mut() {
17413 let li = crate::gpu::cur_layer();
17414 if li >= 0 {
17415 if let Some(row) = acc.get_mut(li as usize) {
17416 match probe_topk() {
17417 0 if probe_sq() => {
17418 for (a, &v) in row.iter_mut().zip(g.iter()) {
17419 *a += (v as f64) * (v as f64);
17420 }
17421 }
17422 0 if probe_signed() => {
17423 for (a, &v) in row.iter_mut().zip(g.iter()) {
17424 *a += v as f64;
17425 }
17426 }
17427 0 => {
17428 for (a, &v) in row.iter_mut().zip(g.iter()) {
17429 *a += (v as f64).abs();
17430 }
17431 }
17432 k => {
17433 let n = g.len();
17434 let k = k.min(n);
17435 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17436 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17437 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17438 });
17439 let thr = *kth;
17440 for (a, &v) in row.iter_mut().zip(g.iter()) {
17441 if v.abs() >= thr {
17442 *a += 1.0;
17443 }
17444 }
17445 }
17446 }
17447 }
17448 }
17449 }
17450 });
17451 if oracle_topk() > 0 {
17452 keep_top_k(g, oracle_topk());
17453 }
17454 {
17455 let li = crate::gpu::cur_layer();
17456 if li >= 0 {
17457 adump_row(li as usize, g);
17458 }
17459 }
17460 let mut out = attention::take_buf(d.down_proj.rows());
17461 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17462 d.down_proj.matvec(g, &mut out, pool);
17463 out
17464 })
17465}
17466
17467pub struct RefitAcc {
17480 pub support: Vec<u32>,
17481 pub gss: Vec<f32>,
17482 pub ya: Vec<f32>,
17483 pub hidden: usize,
17484 pub tokens: u64,
17485 pub buf_g: Vec<f32>,
17491 pub buf_o: Vec<f32>,
17492 pub buf_t: usize,
17493}
17494
17495type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17499
17500static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17501 std::sync::OnceLock::new();
17502
17503fn ffn_probe_active() -> bool {
17506 FFN_PROBE.with(|p| p.borrow().is_some())
17507}
17508
17509fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17510 REFIT
17511 .get_or_init(|| {
17512 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17513 (
17514 d,
17515 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17516 )
17517 })
17518 })
17519 .as_ref()
17520}
17521
17522fn refit_accumulate(
17524 li: usize,
17525 g: &[f32],
17526 b: usize,
17527 inter: usize,
17528 out: &[f32],
17529 hidden: usize,
17530 pool: Option<&Pool>,
17531) {
17532 let Some((dir, map)) = refit_dir() else {
17533 return;
17534 };
17535 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17536 let (from, to) = *SPAN.get_or_init(|| {
17537 let g = |k: &str, d: usize| {
17538 std::env::var(k)
17539 .ok()
17540 .and_then(|v| v.parse().ok())
17541 .unwrap_or(d)
17542 };
17543 (
17544 g("CMF_FFN_REFIT_FROM", 0),
17545 g("CMF_FFN_REFIT_TO", usize::MAX),
17546 )
17547 });
17548 if li < from || li > to {
17549 return;
17550 }
17551 let mut guard = map.lock().unwrap();
17552 let (map, shared) = &mut *guard;
17553 let acc = match map.entry(li) {
17554 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17555 std::collections::hash_map::Entry::Vacant(e) => {
17556 let path = format!("{dir}/support.{li}.u32");
17557 let Ok(bytes) = std::fs::read(&path) else {
17558 eprintln!("refit: no {path} — layer {li} skipped");
17559 return;
17560 };
17561 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17562 let support: Vec<u32> = bytes[4..4 + n * 4]
17563 .chunks_exact(4)
17564 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17565 .collect();
17566 eprintln!(
17567 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17568 (n * n + hidden * n) as f64 * 4.0 / 1e6
17569 );
17570 e.insert(RefitAcc {
17571 gss: vec![0.0; n * n],
17572 ya: vec![0.0; hidden * n],
17573 buf_g: Vec::new(),
17574 buf_o: Vec::new(),
17575 buf_t: 0,
17576 support,
17577 hidden,
17578 tokens: 0,
17579 })
17580 }
17581 };
17582 let ns = acc.support.len();
17583 let cap = refit_batch();
17585 if acc.buf_g.is_empty() {
17586 acc.buf_g = vec![0.0; ns * cap];
17587 acc.buf_o = vec![0.0; hidden * cap];
17588 }
17589 let take = b.min(cap - acc.buf_t);
17590 for t in 0..take {
17591 let col = acc.buf_t + t;
17592 for (j, &n) in acc.support.iter().enumerate() {
17593 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17594 }
17595 for h in 0..hidden {
17596 acc.buf_o[h * cap + col] = out[t * hidden + h];
17597 }
17598 }
17599 acc.buf_t += take;
17600 acc.tokens += take as u64;
17601 if acc.buf_t < cap {
17602 return;
17603 }
17604 let bt = acc.buf_t;
17605 acc.buf_t = 0;
17606 let RefitAcc {
17616 gss,
17617 ya,
17618 buf_g,
17619 buf_o,
17620 ..
17621 } = acc;
17622 let need = (ns * ns).max(hidden * ns);
17623 if shared.len() < need {
17624 shared.resize(need, 0.0);
17625 }
17626 let scratch = &mut shared[..];
17627 let _ = bt;
17628 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17629 add_into(gss, &scratch[..ns * ns], pool);
17630 if crate::gpu::gemm_nt_f32_transient(
17631 buf_o,
17632 buf_g,
17633 &mut scratch[..hidden * ns],
17634 hidden,
17635 cap,
17636 ns,
17637 ) {
17638 add_into(ya, &scratch[..hidden * ns], pool);
17639 } else {
17640 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17641 }
17642 } else {
17643 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17644 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17645 }
17646 }
17650
17651fn refit_batch() -> usize {
17653 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17654 *B.get_or_init(|| {
17655 std::env::var("CMF_FFN_REFIT_BATCH")
17656 .ok()
17657 .and_then(|v| v.parse().ok())
17658 .unwrap_or(4096)
17659 })
17660}
17661
17662fn accum_outer_t(
17665 c: &mut [f32],
17666 m: usize,
17667 n: usize,
17668 b: usize,
17669 left: &[f32],
17670 right: &[f32],
17671 pool: Option<&Pool>,
17672) {
17673 let ptr = SendMut(c.as_mut_ptr());
17674 let body = |i: usize| {
17675 let ptr = &ptr;
17676 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
17677 for t in 0..b {
17678 let a = left[i * b + t];
17679 if a == 0.0 {
17680 continue;
17681 }
17682 for (j, o) in row.iter_mut().enumerate() {
17683 *o += a * right[j * b + t];
17684 }
17685 }
17686 };
17687 match pool {
17688 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
17689 for i in s..e {
17690 body(i);
17691 }
17692 }),
17693 _ => {
17694 for i in 0..m {
17695 body(i);
17696 }
17697 }
17698 }
17699}
17700
17701fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
17704 let n = dst.len().min(src.len());
17705 match pool {
17706 Some(p) if n >= 1 << 16 => {
17707 let ptr = SendMut(dst.as_mut_ptr());
17708 let f = |s: usize, e: usize| {
17709 let ptr = &ptr;
17710 for blk in s..e {
17711 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
17712 for i in a..b {
17713 unsafe { *ptr.0.add(i) += src[i] };
17714 }
17715 }
17716 };
17717 p.run_rows(n.div_ceil(4096), &f);
17718 }
17719 _ => {
17720 for (d, v) in dst.iter_mut().zip(&src[..n]) {
17721 *d += *v;
17722 }
17723 }
17724 }
17725}
17726
17727fn accum_outer(
17732 c: &mut [f32],
17733 m: usize,
17734 n: usize,
17735 b: usize,
17736 left: &[f32],
17737 right: &[f32],
17738 pool: Option<&Pool>,
17739) {
17740 const TILE: usize = 32;
17741 let tiles = m.div_ceil(TILE);
17742 let cp = SendMut(c.as_mut_ptr());
17743 let body = |ti: usize| {
17744 let cp = &cp;
17745 let i0 = ti * TILE;
17746 let i1 = (i0 + TILE).min(m);
17747 for t in 0..b {
17748 let r = &right[t * n..t * n + n];
17749 for i in i0..i1 {
17750 let a = left[i * b + t];
17751 if a == 0.0 {
17752 continue;
17753 }
17754 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
17756 for (o, v) in row.iter_mut().zip(r) {
17757 *o += a * *v;
17758 }
17759 }
17760 }
17761 };
17762 match pool {
17763 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
17764 for ti in s..e {
17765 body(ti);
17766 }
17767 }),
17768 _ => {
17769 for ti in 0..tiles {
17770 body(ti);
17771 }
17772 }
17773 }
17774}
17775
17776pub fn refit_flush() -> usize {
17778 let Some((dir, map)) = refit_dir() else {
17779 return 0;
17780 };
17781 let guard = map.lock().unwrap();
17782 let mut n = 0;
17783 for (li, acc) in guard.0.iter() {
17784 let w = |name: &str, v: &[f32]| {
17787 let path = format!("{dir}/{name}.{li}.f32");
17788 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
17789 match std::fs::write(&path, &bytes) {
17790 Ok(()) => {}
17791 Err(e) => eprintln!(
17792 "refit: FAILED to write {path} ({} MB): {e}",
17793 bytes.len() / 1_000_000
17794 ),
17795 }
17796 };
17797 w("gss", &acc.gss);
17798 w("ya", &acc.ya);
17799 println!(
17800 "refit L{li}: {} support, {} tokens, hidden {}",
17801 acc.support.len(),
17802 acc.tokens,
17803 acc.hidden
17804 );
17805 n += 1;
17806 }
17807 n
17808}
17809
17810fn adump_row(li: usize, g: &[f32]) {
17815 use std::io::Write as _;
17816 static FILES: std::sync::OnceLock<
17817 Option<(
17818 String,
17819 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
17820 )>,
17821 > = std::sync::OnceLock::new();
17822 let Some((prefix, map)) = FILES
17823 .get_or_init(|| {
17824 std::env::var("CMF_FFN_ADUMP")
17825 .ok()
17826 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
17827 })
17828 .as_ref()
17829 else {
17830 return;
17831 };
17832 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17835 let (from, to) = *SPAN.get_or_init(|| {
17836 let g = |k: &str, d: usize| {
17837 std::env::var(k)
17838 .ok()
17839 .and_then(|v| v.parse().ok())
17840 .unwrap_or(d)
17841 };
17842 (
17843 g("CMF_FFN_ADUMP_FROM", 0),
17844 g("CMF_FFN_ADUMP_TO", usize::MAX),
17845 )
17846 });
17847 if li < from || li > to {
17848 return;
17849 }
17850 let mut map = map.lock().unwrap();
17851 let f = map.entry(li).or_insert_with(|| {
17852 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
17853 });
17854 let mut bytes = Vec::with_capacity(g.len() * 2);
17855 for v in g {
17856 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
17857 }
17858 let _ = f.write_all(&bytes);
17859}
17860
17861fn oracle_topk() -> usize {
17867 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17868 *K.get_or_init(|| {
17869 std::env::var("CMF_FFN_ORACLE_TOPK")
17870 .ok()
17871 .and_then(|v| v.parse().ok())
17872 .unwrap_or(0)
17873 })
17874}
17875
17876fn gate_topk() -> usize {
17882 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17883 *K.get_or_init(|| {
17884 std::env::var("CMF_FFN_GATE_TOPK")
17885 .ok()
17886 .and_then(|v| v.parse().ok())
17887 .unwrap_or(0)
17888 })
17889}
17890
17891fn gate_block() -> usize {
17898 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17899 *B.get_or_init(|| {
17900 std::env::var("CMF_FFN_GATE_BLOCK")
17901 .ok()
17902 .and_then(|v| v.parse().ok())
17903 .unwrap_or(1)
17904 })
17905}
17906
17907fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
17909 let n = g.len();
17910 let nb = n.div_ceil(block);
17911 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
17912 if kb >= nb {
17913 return;
17914 }
17915 let mut score: Vec<f32> = (0..nb)
17916 .map(|b| {
17917 g[b * block..((b + 1) * block).min(n)]
17918 .iter()
17919 .map(|v| v * v)
17920 .sum::<f32>()
17921 })
17922 .collect();
17923 let mut ord = score.clone();
17924 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
17925 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17926 });
17927 let thr = *kth;
17928 for b in 0..nb {
17929 if score[b] < thr {
17930 g[b * block..((b + 1) * block).min(n)].fill(0.0);
17931 }
17932 }
17933 score.clear();
17934}
17935
17936fn keep_top_k(g: &mut [f32], k: usize) {
17938 if gate_block() > 1 {
17939 return keep_top_blocks(g, k, gate_block());
17940 }
17941 let n = g.len();
17942 if k == 0 || k >= n {
17943 return;
17944 }
17945 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17946 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17947 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17948 });
17949 let thr = *kth;
17950 for v in g.iter_mut() {
17951 if v.abs() < thr {
17952 *v = 0.0;
17953 }
17954 }
17955}
17956
17957fn probe_sq() -> bool {
17961 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17962 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
17963}
17964
17965fn probe_signed() -> bool {
17969 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17970 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
17971}
17972
17973fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
17981 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
17982 M.get_or_init(|| {
17983 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
17984 let b = std::fs::read(&p).ok()?;
17985 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
17986 let vals: Vec<f32> = b[8..]
17987 .chunks_exact(4)
17988 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17989 .collect();
17990 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
17991 Some((inter, vals))
17992 })
17993 .as_ref()
17994}
17995
17996fn probe_topk() -> usize {
17999 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
18000 *K.get_or_init(|| {
18001 std::env::var("CMF_FFN_PROBE_TOPK")
18002 .ok()
18003 .and_then(|v| v.parse().ok())
18004 .unwrap_or(0)
18005 })
18006}
18007
18008thread_local! {
18009 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
18012 const { std::cell::RefCell::new(None) };
18013}
18014
18015fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
18028 if d.gate_proj.has_prism_contract()
18033 || d.up_proj.has_prism_contract()
18034 || d.down_proj.has_prism_contract()
18035 {
18036 return None;
18037 }
18038 let dt = d.down_t.as_ref()?;
18039 let inter = d.gate_proj.rows();
18040 let hidden = dt.cols();
18041 if k == 0 || k >= inter || d.act != Act::Silu {
18042 return None;
18043 }
18044 DYN_SCRATCH.with(|sc| {
18045 let mut sc = sc.borrow_mut();
18046 let DynScratch {
18047 g,
18048 mag,
18049 live,
18050 parts,
18051 } = &mut *sc;
18052 g.resize(inter, 0.0);
18053 d.gate_proj.matvec(x, g, pool);
18054 for v in g.iter_mut() {
18055 *v = inference::silu(*v);
18056 }
18057 mag.clear();
18060 mag.extend(g.iter().map(|v| v.abs()));
18061 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18062 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18063 });
18064 let thr = *kth;
18065 live.clear();
18066 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18067 let mut out = vec![0.0f32; hidden];
18068 match pool {
18069 Some(p) if live.len() >= 64 => {
18070 let nw = p.n_workers() + 1;
18071 parts.clear();
18072 parts.resize(nw * hidden, 0.0);
18073 let ptr = SendMut(parts.as_mut_ptr());
18074 let n = live.len();
18075 let live_ref: &[u32] = live;
18076 let g_ref: &[f32] = g;
18077 p.run(&|w, workers| {
18078 let chunk = n.div_ceil(workers);
18079 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18080 if s >= e {
18081 return;
18082 }
18083 WORKER_SCRATCH.with(|ws| {
18084 let mut ws = ws.borrow_mut();
18085 let [scratch, acc] = &mut *ws;
18086 scratch.resize(hidden.max(x.len()), 0.0);
18087 acc.clear();
18088 acc.resize(hidden, 0.0);
18089 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18090 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18093 d.up_proj.prefetch_row(nx as usize);
18094 dt.prefetch_row(nx as usize);
18095 }
18096 let idx = nrm as usize;
18097 let up = d.up_proj.row_dot(idx, x, scratch);
18098 let a = g_ref[idx] * up;
18099 if a != 0.0 {
18100 dt.add_row_scaled(idx, a, acc, scratch);
18101 }
18102 }
18103 for (j, v) in acc.iter().enumerate() {
18104 unsafe { *ptr.at(w * hidden + j) = *v };
18105 }
18106 });
18107 });
18108 for w in 0..nw {
18109 for (j, o) in out.iter_mut().enumerate() {
18110 *o += parts[w * hidden + j];
18111 }
18112 }
18113 }
18114 _ => {
18115 WORKER_SCRATCH.with(|ws| {
18116 let mut ws = ws.borrow_mut();
18117 let [scratch, _acc] = &mut *ws;
18118 scratch.resize(hidden.max(x.len()), 0.0);
18119 for &nrm in live.iter() {
18120 let idx = nrm as usize;
18121 let up = d.up_proj.row_dot(idx, x, scratch);
18122 let a = g[idx] * up;
18123 if a != 0.0 {
18124 dt.add_row_scaled(idx, a, &mut out, scratch);
18125 }
18126 }
18127 });
18128 }
18129 }
18130 Some(out)
18131 })
18132}
18133
18134struct DynScratch {
18137 g: Vec<f32>,
18138 mag: Vec<f32>,
18139 live: Vec<u32>,
18140 parts: Vec<f32>,
18141}
18142
18143thread_local! {
18144 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18145 std::cell::RefCell::new(DynScratch {
18146 g: Vec::new(),
18147 mag: Vec::new(),
18148 live: Vec::new(),
18149 parts: Vec::new(),
18150 })
18151 };
18152 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18154 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18155}
18156
18157fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18162 let inter = d.gate_proj.rows();
18163 FFN_SCRATCH.with(|s| {
18164 let mut s = s.borrow_mut();
18165 let [g, u, ..] = &mut *s;
18166 g.resize(inter, 0.0);
18167 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18168 } else {
18170 u.resize(inter, 0.0);
18171 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18172 for i in 0..inter {
18173 g[i] = d.act.combine(g[i], u[i]);
18174 }
18175 }
18176 zero_masked_cols(g, 1, inter, mask_row);
18177 let mut out = attention::take_buf(d.down_proj.rows());
18178 d.down_proj.matvec(g, &mut out, pool);
18179 out
18180 })
18181}
18182
18183fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18189 if d.gate_proj.has_prism_contract()
18190 || d.up_proj.has_prism_contract()
18191 || d.down_proj.has_prism_contract()
18192 {
18193 return None;
18194 }
18195 d.act.graph_act()?;
18198 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18201 return None;
18202 }
18203 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18204 let mut model_ref = None;
18205 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18206 let model = model_ref?;
18207 let hidden = jobs[0].down.1;
18208 let mut out = attention::take_buf(hidden);
18209 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18210 Some(out)
18211 } else {
18212 let mut out = out;
18213 attention::recycle_buf(&mut out);
18214 None
18215 }
18216}
18217
18218#[allow(clippy::type_complexity)]
18223#[allow(clippy::type_complexity)]
18224pub(crate) fn moe_parts(
18225 t: &QTensor,
18226) -> Option<(
18227 &std::sync::Arc<cortiq_core::CmfModel>,
18228 usize,
18229 usize,
18230 usize,
18231 &[f32],
18232 &[f32],
18233 bool,
18234 bool,
18235 bool,
18236)> {
18237 match t {
18238 QTensor::Mapped {
18239 model,
18240 idx,
18241 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18242 rows,
18243 cols,
18244 row_scale,
18245 col_field,
18246 ..
18247 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18248 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18249 )),
18250 QTensor::Mapped {
18252 model,
18253 idx,
18254 dtype: cortiq_core::TensorDtype::Q1,
18255 rows,
18256 cols,
18257 ..
18258 } => Some((
18259 model,
18260 *idx,
18261 *rows,
18262 *cols,
18263 &[][..],
18264 &[][..],
18265 true,
18266 false,
18267 false,
18268 )),
18269 QTensor::Mapped {
18271 model,
18272 idx,
18273 dtype: cortiq_core::TensorDtype::Q4Tiled,
18274 rows,
18275 cols,
18276 ..
18277 } => Some((
18278 model,
18279 *idx,
18280 *rows,
18281 *cols,
18282 &[][..],
18283 &[][..],
18284 false,
18285 true,
18286 false,
18287 )),
18288 QTensor::Mapped {
18290 model,
18291 idx,
18292 dtype: cortiq_core::TensorDtype::Q4TiledP,
18293 rows,
18294 cols,
18295 ..
18296 } => Some((
18297 model,
18298 *idx,
18299 *rows,
18300 *cols,
18301 &[][..],
18302 &[][..],
18303 false,
18304 true,
18305 false,
18306 )),
18307 QTensor::Mapped {
18311 model,
18312 idx,
18313 dtype: cortiq_core::TensorDtype::Q2TiledP,
18314 rows,
18315 cols,
18316 ..
18317 } => Some((
18318 model,
18319 *idx,
18320 *rows,
18321 *cols,
18322 &[][..],
18323 &[][..],
18324 false,
18325 true,
18326 true,
18327 )),
18328 _ => None,
18329 }
18330}
18331
18332#[cfg(target_os = "macos")]
18339#[allow(clippy::type_complexity)]
18340fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18341 if m.shared.is_some()
18342 || m.per_expert_scale.is_some()
18343 || m.resonance.is_some()
18344 || m.route_tau.is_some()
18345 || m.router_input_norm
18346 || m.experts.is_empty()
18347 || m.top_k == 0
18348 || std::env::var("CMF_RMS_TRACE").is_ok()
18349 || std::env::var("CMF_ACT_DUMP").is_ok()
18350 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18351 {
18352 return None;
18353 }
18354 let (rf, rr, rc) = m.router.f32_parts()?;
18355 if rr != m.experts.len() || rc != hidden {
18356 return None;
18357 }
18358 let inter = m.experts[0].gate_proj.rows();
18359 let q8 = crate::gpu_metal::moe_q8::enabled()
18362 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18363 let experts = m
18364 .experts
18365 .iter()
18366 .map(|e| {
18367 if e.act != Act::Silu
18368 || !e.segs.is_empty()
18369 || e.gate_proj.rows() != inter
18370 || e.up_proj.rows() != inter
18371 || e.down_proj.cols() != inter
18372 || e.gate_proj.cols() != hidden
18373 || e.down_proj.rows() != hidden
18374 {
18375 return None;
18376 }
18377 if q8 {
18378 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18379 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18380 }
18381 Some((
18382 e.gate_proj.mapped_q4tp()?.1,
18383 e.up_proj.mapped_q4tp()?.1,
18384 e.down_proj.mapped_q4tp()?.1,
18385 ))
18386 })
18387 .collect::<Option<Vec<_>>>()?;
18388 Some((rf, experts, q8))
18389}
18390
18391#[cfg(target_os = "macos")]
18399fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18400 if m.router_input_norm
18401 || m.route_tau.is_some()
18402 || m.mask.is_some()
18403 || m.per_expert_scale.is_some()
18404 || m.experts.is_empty()
18405 || m.top_k == 0
18406 || m.resonance.is_some()
18407 {
18408 return None;
18409 }
18410 let (sh, sg) = match &m.shared {
18415 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18416 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18417 None => return None,
18418 };
18419 let (rf, rr, rc) = m.router.f32_parts()?;
18420 if rr != m.experts.len() || rc != hidden {
18421 return None;
18422 }
18423 let shared_gated = sg.is_some();
18424 let sf = match sg {
18425 Some(sg) => {
18426 let (sf, sr, sc) = sg.f32_parts()?;
18427 if sr * sc != hidden {
18428 return None;
18429 }
18430 sf
18431 }
18432 None => &rf[..hidden],
18435 };
18436 if let Some(b) = &m.expert_bias {
18437 if b.len() != m.experts.len() {
18438 return None;
18439 }
18440 }
18441 let inter = m.experts[0].gate_proj.rows();
18442 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18445 let q8 = crate::gpu_metal::moe_q8::enabled()
18449 && m.shared.is_none()
18450 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18451 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18452 if e.act != Act::Silu
18453 || e.gate_proj.rows() != inter
18454 || e.gate_proj.cols() != hidden
18455 || e.up_proj.rows() != inter
18456 || e.up_proj.cols() != hidden
18457 || e.down_proj.rows() != hidden
18458 || e.down_proj.cols() != inter
18459 {
18460 return None;
18461 }
18462 if q8 {
18463 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18464 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18465 }
18466 let pick = |t: &QTensor| -> Option<usize> {
18467 if gu_q2 {
18468 t.mapped_q2tp().map(|(_, i)| i)
18469 } else {
18470 t.mapped_q4tp().map(|(_, i)| i)
18471 }
18472 };
18473 Some((
18474 pick(&e.gate_proj)?,
18475 pick(&e.up_proj)?,
18476 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18477 ))
18478 };
18479 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18480 let shared = match sh {
18481 Some(sh) => Some(trio(sh)?),
18482 None => None,
18483 };
18484 Some(crate::gpu::GpuMoe {
18485 router: rf,
18486 sgate: sf,
18487 experts,
18488 shared,
18489 n_exp: m.experts.len(),
18490 top_k: m.top_k,
18491 inter,
18492 norm_topk: m.norm_topk_prob,
18493 route_scale: m.routed_scaling,
18494 gu_q2,
18495 q8,
18496 sigmoid: m.router_sigmoid,
18497 bias: m.expert_bias.as_deref(),
18498 shared_gated,
18499 })
18500}
18501
18502fn forced_ids_at(step: usize) -> Option<u32> {
18508 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18509 IDS.get_or_init(|| {
18510 std::env::var("CMF_FORCE_IDS")
18511 .ok()
18512 .and_then(|p| std::fs::read_to_string(p).ok())
18513 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18514 .unwrap_or_default()
18515 })
18516 .get(step)
18517 .copied()
18518}
18519
18520pub(crate) fn moe_push_job_parts<'a>(
18524 gate: &'a QTensor,
18525 up: &'a QTensor,
18526 down: &'a QTensor,
18527 x: &[f32],
18528 w: f32,
18529 swiglu_limit: f32,
18530 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18531 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18532) -> Option<()> {
18533 use crate::qtensor::prescale;
18534 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18535 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18536 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18537 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18538 return None; }
18540 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18543 return None;
18544 }
18545 if !gq2 && dq2 {
18546 return None;
18547 }
18548 model_ref.get_or_insert_with(|| gm.clone());
18549 let dt = |cf: &[f32]| {
18550 if cf.is_empty() {
18551 cortiq_core::TensorDtype::Q8Row
18552 } else {
18553 cortiq_core::TensorDtype::Q8_2f
18554 }
18555 };
18556 jobs.push(crate::gpu::MoeJob {
18557 gate: (gi, gr, gc, grs),
18558 up: (ui, ur, uc, urs),
18559 down: (di, dr, dc, drs),
18560 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18561 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18562 down_col: dcf,
18563 w,
18564 q1: gq1,
18565 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18566 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18567 gu_q2: gq2,
18568 swiglu_limit,
18569 act: 0,
18570 });
18571 Some(())
18572}
18573
18574fn moe_push_job<'a>(
18576 d: &'a DenseFfn,
18577 x: &[f32],
18578 w: f32,
18579 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18580 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18581) -> Option<()> {
18582 use crate::qtensor::prescale;
18583 let act = d.act.graph_act()?.code();
18586 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18587 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18588 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18589 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18590 return None; }
18592 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18593 return None;
18594 }
18595 if !gq2 && dq2 {
18596 return None;
18597 }
18598 model_ref.get_or_insert_with(|| gm.clone());
18599 let gdt = if gcf.is_empty() {
18600 cortiq_core::TensorDtype::Q8Row
18601 } else {
18602 cortiq_core::TensorDtype::Q8_2f
18603 };
18604 let udt = if ucf.is_empty() {
18605 cortiq_core::TensorDtype::Q8Row
18606 } else {
18607 cortiq_core::TensorDtype::Q8_2f
18608 };
18609 jobs.push(crate::gpu::MoeJob {
18610 gate: (gi, gr, gc, grs),
18611 up: (ui, ur, uc, urs),
18612 down: (di, dr, dc, drs),
18613 xs_gate: prescale(x, gcf, gdt).into_owned(),
18614 xs_up: prescale(x, ucf, udt).into_owned(),
18615 down_col: dcf,
18616 w,
18617 q1: gq1,
18618 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18619 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18620 gu_q2: gq2,
18621 swiglu_limit: 0.0,
18622 act,
18623 });
18624 Some(())
18625}
18626
18627fn sparse_ffn_quant(
18634 d: &DenseFfn,
18635 x: &[f32],
18636 active: &[u16],
18637 hidden: usize,
18638 pool: Option<&Pool>,
18639) -> Vec<f32> {
18640 let n = active.len();
18641 let inter = d.gate_proj.rows();
18642 let mut act = vec![0.0f32; n];
18643 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
18646 let compute = |ai: usize| -> f32 {
18647 let idx = active[ai] as usize;
18648 if idx >= inter {
18649 return 0.0; }
18651 let mut s = if need_scratch {
18652 vec![0.0f32; hidden]
18653 } else {
18654 Vec::new()
18655 };
18656 let gate = d.gate_proj.row_dot(idx, x, &mut s);
18657 let up = d.up_proj.row_dot(idx, x, &mut s);
18658 d.act.combine(gate, up)
18659 };
18660 match pool {
18661 Some(p) if n >= 256 => {
18662 let ptr = SendMut(act.as_mut_ptr());
18663 p.run(&|widx, nw| {
18664 let chunk = n.div_ceil(nw);
18665 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
18666 for ai in s..e {
18667 unsafe { *ptr.at(ai) = compute(ai) };
18668 }
18669 });
18670 }
18671 _ => {
18672 for (ai, a) in act.iter_mut().enumerate() {
18673 *a = compute(ai);
18674 }
18675 }
18676 }
18677 let mut out = vec![0.0f32; hidden];
18679 for (ai, &idx) in active.iter().enumerate() {
18680 let w = act[ai];
18681 if w.abs() >= 1e-12 && (idx as usize) < inter {
18682 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
18683 }
18684 }
18685 out
18686}
18687
18688#[doc(hidden)]
18690pub fn sparse_ffn_quant_for_test(
18691 d: &DenseFfn,
18692 x: &[f32],
18693 active: &[u16],
18694 hidden: usize,
18695) -> Vec<f32> {
18696 sparse_ffn_quant(d, x, active, hidden, None)
18697}
18698
18699fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
18703 let deq = |t: &QTensor| -> Vec<f32> {
18704 let (rows, cols) = (t.rows(), t.cols());
18705 let mut out = vec![0.0f32; rows * cols];
18706 for r in 0..rows {
18707 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
18708 }
18709 out
18710 };
18711 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
18712}
18713
18714struct SendMut(*mut f32);
18716unsafe impl Send for SendMut {}
18717unsafe impl Sync for SendMut {}
18718impl SendMut {
18719 #[inline]
18720 #[allow(clippy::mut_from_ref)]
18723 unsafe fn at(&self, i: usize) -> &mut f32 {
18724 unsafe { &mut *self.0.add(i) }
18725 }
18726}
18727
18728pub(crate) fn moe_route(
18741 logits: &[f32],
18742 m: &MoeFfn,
18743 allowed: Option<&[bool]>,
18744) -> (Vec<usize>, Vec<f32>, f32) {
18745 moe_route_with_eps(logits, m, allowed, 1e-6)
18746}
18747
18748pub(crate) fn moe_route_with_eps(
18757 logits: &[f32],
18758 m: &MoeFfn,
18759 allowed: Option<&[bool]>,
18760 sigmoid_denom_eps: f32,
18761) -> (Vec<usize>, Vec<f32>, f32) {
18762 let ne = logits.len();
18763 let admit = |e: usize| {
18769 m.mask.as_ref().is_none_or(|mk| mk[e])
18770 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
18771 };
18772 if m.resonance.is_some() && m.top_k == 1 {
18785 let mut best: Option<usize> = None;
18786 for e in (0..ne).filter(|&e| admit(e)) {
18787 let l = logits[e];
18788 if l == f32::NEG_INFINITY || l.is_nan() {
18789 continue;
18790 }
18791 if best.is_none_or(|b| l > logits[b]) {
18792 best = Some(e);
18793 }
18794 }
18795 if let Some(b) = best {
18796 let mut p = vec![0.0f32; ne];
18797 p[b] = 1.0;
18798 return (vec![b], p, 1.0 / m.routed_scaling);
18799 }
18800 }
18801 let p: Vec<f32> = if m.router_sigmoid {
18807 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
18808 } else {
18809 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18810 if mx == f32::NEG_INFINITY {
18811 vec![1.0 / ne.max(1) as f32; ne]
18812 } else {
18813 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
18814 let s: f32 = e.iter().sum();
18815 for v in &mut e {
18816 *v /= s;
18817 }
18818 e
18819 }
18820 };
18821 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
18822 match &m.expert_bias {
18824 Some(b) => idx.sort_unstable_by(|&x, &y| {
18825 (p[y] + b[y])
18826 .partial_cmp(&(p[x] + b[x]))
18827 .unwrap()
18828 .then(x.cmp(&y))
18829 }),
18830 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
18831 }
18832 idx.truncate(m.top_k);
18833 if let Some(tau) = m.route_tau {
18837 let total: f32 = idx.iter().map(|&e| p[e]).sum();
18838 if total > 0.0 {
18839 let mut acc = 0.0f32;
18840 let mut keep = idx.len();
18841 for (i, &e) in idx.iter().enumerate() {
18842 acc += p[e];
18843 if acc >= tau * total {
18844 keep = i + 1;
18845 break;
18846 }
18847 }
18848 idx.truncate(keep);
18849 }
18850 }
18851 let wsum: f32 = if m.norm_topk_prob {
18852 let s: f32 = idx.iter().map(|&e| p[e]).sum();
18853 (if m.router_sigmoid {
18857 s + sigmoid_denom_eps
18858 } else {
18859 s
18860 }) / m.routed_scaling
18861 } else {
18862 1.0 / m.routed_scaling
18863 };
18864 (idx, p, wsum)
18865}
18866
18867fn moe_trace(idx: &[usize]) {
18869 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
18870}
18871
18872pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
18875 use std::io::Write;
18876 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
18877 std::sync::OnceLock::new();
18878 let Some(f) = F.get_or_init(|| {
18879 let p = std::env::var("CMF_MOE_TRACE").ok()?;
18880 Some(std::sync::Mutex::new(
18881 std::fs::OpenOptions::new()
18882 .create(true)
18883 .append(true)
18884 .open(p)
18885 .ok()?,
18886 ))
18887 }) else {
18888 return;
18889 };
18890 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
18891 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
18892}
18893
18894pub(crate) fn moe_ffn(
18897 m: &MoeFfn,
18898 x: &[f32],
18899 pool: Option<&Pool>,
18900 allowed: Option<&[bool]>,
18901) -> Vec<f32> {
18902 let r = moe_ffn_route(m, x, pool, allowed);
18903 moe_ffn_experts(m, x, &r, pool)
18904}
18905
18906pub(crate) struct MoeRoute {
18910 pub idx: Vec<usize>,
18911 pub p: Vec<f32>,
18912 pub wsum: f32,
18913 pub logits: Vec<f32>,
18914}
18915
18916pub(crate) fn moe_ffn_route(
18922 m: &MoeFfn,
18923 x: &[f32],
18924 pool: Option<&Pool>,
18925 allowed: Option<&[bool]>,
18926) -> MoeRoute {
18927 accumulate_act(m, x, 1);
18928 let ne = m.experts.len();
18929 let mut logits = vec![0.0f32; ne];
18930 match &m.resonance {
18931 Some(r) => r.scores(x, &mut logits),
18932 None => m.router.matvec(x, &mut logits, pool),
18933 }
18934 let (idx, p, wsum) = moe_route(&logits, m, allowed);
18935 {
18936 let mut st = m.stats.borrow_mut();
18937 if st.len() < ne {
18938 st.resize(ne, 0);
18939 }
18940 for &e in &idx {
18941 st[e] += 1;
18942 }
18943 }
18944 moe_trace(&idx);
18950 MoeRoute {
18951 idx,
18952 p,
18953 wsum,
18954 logits,
18955 }
18956}
18957
18958pub(crate) fn moe_ffn_experts(
18961 m: &MoeFfn,
18962 x: &[f32],
18963 r: &MoeRoute,
18964 pool: Option<&Pool>,
18965) -> Vec<f32> {
18966 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
18967 if crate::gpu::enabled_here() {
18972 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
18973 crate::gpu::ProbeArm::Gpu => {
18974 let t0 = std::time::Instant::now();
18975 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
18976 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18977 return out;
18978 }
18979 }
18980 crate::gpu::ProbeArm::CpuTimed => {
18981 let t0 = std::time::Instant::now();
18982 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18983 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18984 return out;
18985 }
18986 crate::gpu::ProbeArm::Cpu => {
18987 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18988 }
18989 }
18990 }
18991 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
18992}
18993
18994fn moe_ffn_banked(
18998 slot: &mut crate::mimo_moe::Slot,
18999 li: usize,
19000 m: &MoeFfn,
19001 x: &[f32],
19002 pool: Option<&Pool>,
19003) -> Vec<f32> {
19004 let t0 = std::time::Instant::now();
19005 let r = moe_ffn_route(m, x, pool, None);
19006 slot.note_route(t0.elapsed().as_nanos() as u64);
19007 match slot.forward(li, m, x, &r, pool) {
19008 Some(out) => out,
19009 None => crate::qtensor::float_activations_scope(|| {
19010 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
19011 }),
19012 }
19013}
19014
19015fn moe_ffn_banked_rows(
19017 slot: &mut crate::mimo_moe::Slot,
19018 li: usize,
19019 m: &MoeFfn,
19020 xs: &[f32],
19021 b: usize,
19022 hidden: usize,
19023 pool: Option<&Pool>,
19024) -> Vec<f32> {
19025 let t0 = std::time::Instant::now();
19026 let routes: Vec<_> = xs
19027 .chunks_exact(hidden)
19028 .map(|x| moe_ffn_route(m, x, pool, None))
19029 .collect();
19030 slot.note_route(t0.elapsed().as_nanos() as u64);
19031 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
19032 return out;
19033 }
19034 let mut out = Vec::with_capacity(b * hidden);
19035 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19036 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19037 crate::qtensor::float_activations_scope(|| {
19039 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19040 })
19041 });
19042 out.extend(row);
19043 }
19044 out
19045}
19046
19047fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19052 use std::sync::atomic::{AtomicBool, Ordering};
19053 if built {
19054 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19055 if total_layers > 0 && layers_run < total_layers {
19056 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19057 } else {
19058 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19059 }
19060 } else {
19061 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19062 }
19063 static SAID: AtomicBool = AtomicBool::new(false);
19064 if !SAID.swap(true, Ordering::Relaxed) {
19065 if built {
19066 tracing::info!("wgpu whole-token graph: ACTIVE");
19067 } else {
19068 tracing::warn!("wgpu whole-token graph refused — per-op path");
19069 }
19070 }
19071}
19072
19073pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19077pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19078pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19082pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19084
19085pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19089 std::sync::atomic::AtomicU64::new(0);
19090pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19091 std::sync::atomic::AtomicU64::new(0);
19092pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19093 std::sync::atomic::AtomicU64::new(0);
19094pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19095 std::sync::atomic::AtomicU64::new(0);
19096pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19097 std::sync::atomic::AtomicU64::new(0);
19098pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19102 std::sync::atomic::AtomicU64::new(0);
19103pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19104 std::sync::atomic::AtomicU64::new(0);
19105pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19106 std::sync::atomic::AtomicU64::new(0);
19107pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19108 std::sync::atomic::AtomicU64::new(0);
19109
19110fn moe_batch_enabled() -> bool {
19113 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19114 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19115}
19116
19117fn moe_ffn_cpu_batched(
19123 m: &MoeFfn,
19124 x: &[f32],
19125 idx: &[usize],
19126 p: &[f32],
19127 wsum: f32,
19128 pool: Option<&Pool>,
19129) -> Option<Vec<f32>> {
19130 if idx.is_empty() || !moe_batch_enabled() {
19131 return None;
19132 }
19133 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19137 return None;
19138 }
19139 let n = idx.len() + usize::from(m.shared.is_some());
19140 let mut pairs = Vec::with_capacity(n);
19141 let mut downs = Vec::with_capacity(n);
19142 let mut ws = Vec::with_capacity(n);
19143 for &e in idx {
19144 let d = &m.experts[e];
19145 if d.act != Act::Silu {
19146 return None;
19147 }
19148 pairs.push((&d.gate_proj, &d.up_proj));
19149 downs.push(&d.down_proj);
19150 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19151 }
19152 if let Some((se, gate)) = &m.shared {
19155 if se.act != Act::Silu {
19156 return None;
19157 }
19158 let g = gate.as_ref().map_or(1.0, |gate| {
19159 let mut gl = [0.0f32; 1];
19160 gate.matvec(x, &mut gl, pool);
19161 1.0 / (1.0 + (-gl[0]).exp())
19162 });
19163 pairs.push((&se.gate_proj, &se.up_proj));
19164 downs.push(&se.down_proj);
19165 ws.push(g);
19166 }
19167 let inter = pairs[0].0.rows();
19168 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19169 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19170 return None;
19171 }
19172 let mut out = attention::take_buf(x.len());
19173 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19174 attention::recycle_buf(&mut out);
19175 return None;
19176 }
19177 Some(out)
19178}
19179
19180pub(crate) fn moe_cold_experts_cpu(
19186 experts: &[(&DenseFfn, f32)],
19187 x: &[f32],
19188 pool: Option<&Pool>,
19189) -> Vec<f32> {
19190 let mut out = attention::take_buf(x.len());
19191 if experts.is_empty() {
19192 return out;
19193 }
19194 let pairs: Vec<_> = experts
19195 .iter()
19196 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19197 .collect();
19198 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19199 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19200 let inter = experts[0].0.gate_proj.rows();
19201 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19202 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19203 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19204 {
19205 return out;
19206 }
19207 out.fill(0.0);
19208 for &(expert, weight) in experts {
19209 let mut one = dense_ffn(expert, x, pool);
19210 for (o, v) in out.iter_mut().zip(&one) {
19211 *o += weight * v;
19212 }
19213 attention::recycle_buf(&mut one);
19214 }
19215 out
19216}
19217
19218pub(crate) fn moe_cold_experts_rows_cpu(
19222 jobs: &[Vec<(&DenseFfn, f32)>],
19223 xs: &[f32],
19224 hidden: usize,
19225 pool: Option<&Pool>,
19226) -> Vec<f32> {
19227 let mut out = vec![0.0; xs.len()];
19228 let mut experts: Vec<&DenseFfn> = Vec::new();
19229 let mut groups: Vec<Vec<usize>> = Vec::new();
19230 let mut terms = vec![Vec::new(); jobs.len()];
19231 for (r, row) in jobs.iter().enumerate() {
19232 for &(e, w) in row {
19233 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19234 Some(g) => g,
19235 None => {
19236 experts.push(e);
19237 groups.push(Vec::new());
19238 groups.len() - 1
19239 }
19240 };
19241 terms[r].push((g, groups[g].len(), w));
19242 groups[g].push(r);
19243 }
19244 }
19245 if experts.is_empty() {
19246 return out;
19247 }
19248 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19249 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19250 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19251 let count: usize = lens.iter().sum();
19252 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19253 let mut ds = vec![vec![0.0; hidden]; count];
19254 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19255 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19256 {
19257 let mut offset = 0;
19258 let offsets: Vec<_> = lens
19259 .iter()
19260 .map(|&n| {
19261 let start = offset;
19262 offset += n;
19263 start
19264 })
19265 .collect();
19266 for (r, terms) in terms.iter().enumerate() {
19267 for &(g, slot, w) in terms {
19268 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19269 .iter_mut()
19270 .zip(&ds[offsets[g] + slot])
19271 {
19272 *o += w * v;
19273 }
19274 }
19275 }
19276 } else {
19277 for (r, jobs) in jobs.iter().enumerate() {
19278 if !jobs.is_empty() {
19279 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19280 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19281 attention::recycle_buf(&mut row);
19282 }
19283 }
19284 }
19285 out
19286}
19287
19288fn moe_ffn_cpu(
19290 m: &MoeFfn,
19291 x: &[f32],
19292 idx: &[usize],
19293 p: &[f32],
19294 wsum: f32,
19295 pool: Option<&Pool>,
19296) -> Vec<f32> {
19297 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19298 return out;
19299 }
19300 let mut out = attention::take_buf(x.len());
19301 for &e in idx {
19302 let mut eo = dense_ffn(&m.experts[e], x, pool);
19303 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19304 for i in 0..out.len() {
19305 out[i] += w * eo[i];
19306 }
19307 attention::recycle_buf(&mut eo);
19308 }
19309 if let Some((se, gate)) = &m.shared {
19310 let mut so = dense_ffn(se, x, pool);
19311 let g = gate.as_ref().map_or(1.0, |gate| {
19312 let mut gl = [0.0f32; 1];
19313 gate.matvec(x, &mut gl, pool);
19314 1.0 / (1.0 + (-gl[0]).exp())
19315 });
19316 for i in 0..out.len() {
19317 out[i] += g * so[i];
19318 }
19319 attention::recycle_buf(&mut so);
19320 }
19321 out
19322}
19323
19324#[allow(clippy::too_many_arguments)]
19332pub(crate) fn mla_attention(
19333 w: &MlaWeights,
19334 normed: &[f32],
19335 cache: &mut crate::kv_cache::LayerKvCache,
19336 position: usize,
19337 inv_freq: &[f32],
19338 rope_scale: f32,
19339 eps: f64,
19340 pool: Option<&Pool>,
19341) -> Vec<f32> {
19342 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19343 let hd = dr + dn;
19344 let mut q = vec![0.0f32; nh * hd];
19345 match (&w.q_a, &w.q_a_norm) {
19346 (Some(qa), Some(qn)) => {
19347 let mut t = vec![0.0f32; qa.rows()];
19348 qa.matvec(normed, &mut t, pool);
19349 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19350 w.q_proj.matvec(&tn, &mut q, pool);
19351 }
19352 _ => w.q_proj.matvec(normed, &mut q, pool),
19353 }
19354 let mut ca = vec![0.0f32; lora + dr];
19355 w.kv_a.matvec(normed, &mut ca, pool);
19356 let (c_lat, k_rope) = ca.split_at_mut(lora);
19357 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19358 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19359 w.kv_b.matvec(&latn, &mut kvb, pool);
19360 if !w.nope {
19361 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19362 }
19363 for h in 0..nh {
19364 if !w.nope {
19365 attention::rope_rotate_scaled(
19366 &mut q[h * hd..h * hd + dr],
19367 position,
19368 inv_freq,
19369 rope_scale,
19370 );
19371 }
19372 }
19373 let mut k = vec![0.0f32; nh * hd];
19374 let mut v = vec![0.0f32; nh * hd];
19375 for h in 0..nh {
19376 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19377 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19378 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19379 }
19380 cache.append(&k, &v, &vec![true; nh]);
19381 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19382 attention::recycle_buf(&mut imp);
19383 let mut ov = vec![0.0f32; nh * dv];
19384 for h in 0..nh {
19385 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19386 }
19387 let mut out = vec![0.0f32; w.o_proj.rows()];
19388 w.o_proj.matvec(&ov, &mut out, pool);
19389 out
19390}
19391
19392fn dense_moe_ffn(
19399 dm: &DenseMoeFfn,
19400 x_normed: &[f32],
19401 h_raw: &[f32],
19402 eps: f64,
19403 norm_style: NormStyle,
19404 pool: Option<&Pool>,
19405) -> Vec<f32> {
19406 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19407 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19408 let m = &dm.moe;
19409 let ne = m.experts.len();
19410 let mut logits = vec![0.0f32; ne];
19411 if m.router_input_norm {
19412 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19413 let inv = 1.0 / (ss + eps as f32).sqrt();
19414 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19415 m.router.matvec(&xr, &mut logits, pool);
19416 } else {
19417 m.router.matvec(h_raw, &mut logits, pool);
19418 }
19419 let (idx, p, wsum) = moe_route(&logits, m, None);
19420 {
19421 let mut st = m.stats.borrow_mut();
19422 if st.len() < ne {
19423 st.resize(ne, 0);
19424 }
19425 for &e in &idx {
19426 st[e] += 1;
19427 }
19428 }
19429 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19430 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19431 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19432 for (di, mi) in d.iter_mut().zip(&mo) {
19433 *di += mi;
19434 }
19435 d
19436}
19437
19438fn moe_gpu_refused(why: &'static str) {
19445 use std::sync::atomic::{AtomicBool, Ordering};
19446 static SAID: AtomicBool = AtomicBool::new(false);
19447 if !SAID.swap(true, Ordering::Relaxed) {
19448 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19449 }
19450}
19451
19452fn moe_ffn_gpu(
19453 m: &MoeFfn,
19454 x: &[f32],
19455 idx: &[usize],
19456 p: &[f32],
19457 wsum: f32,
19458 pool: Option<&Pool>,
19459) -> Option<Vec<f32>> {
19460 use crate::gpu::MoeJob;
19461
19462 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19463 let mut model_ref = None;
19464 for &e in idx {
19465 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19466 moe_gpu_refused("push_job(expert)");
19467 return None;
19468 }
19469 }
19470 if let Some((se, gate)) = &m.shared {
19471 let g = gate.as_ref().map_or(1.0, |gate| {
19472 let mut gl = [0.0f32; 1];
19473 gate.matvec(x, &mut gl, pool);
19474 1.0 / (1.0 + (-gl[0]).exp())
19475 });
19476 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19477 moe_gpu_refused("push_job(shared)");
19478 return None;
19479 }
19480 }
19481 let Some(model) = model_ref else {
19482 moe_gpu_refused("no model_ref");
19483 return None;
19484 };
19485 let hidden = jobs[0].down.1;
19486 let mut out = vec![0.0f32; hidden];
19487 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19488 Some(out)
19489 } else {
19490 moe_gpu_refused("gpu::moe_block");
19491 None
19492 }
19493}
19494
19495fn ffn_forward(
19497 ffn: &FfnKind,
19498 x: &[f32],
19499 pool: Option<&Pool>,
19500 experts_allowed: Option<&[bool]>,
19501) -> Vec<f32> {
19502 match ffn {
19503 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19504 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19505 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19506 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19510 }
19511}
19512
19513fn ffn_forward_pair(
19517 ffn: &FfnKind,
19518 x1: &[f32],
19519 x2: &[f32],
19520 pool: Option<&Pool>,
19521 experts_allowed: Option<&[bool]>,
19522) -> (Vec<f32>, Vec<f32>) {
19523 let d = match ffn {
19524 FfnKind::Dense(d) if !d.segs.is_empty() => {
19527 return (
19528 tube_ffn(d, x1, 1, pool, None),
19529 tube_ffn(d, x2, 1, pool, None),
19530 );
19531 }
19532 FfnKind::Dense(d) => d,
19533 FfnKind::Moe(m) => {
19534 return (
19535 moe_ffn(m, x1, pool, experts_allowed),
19536 moe_ffn(m, x2, pool, experts_allowed),
19537 );
19538 }
19539 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19540 };
19541 let inter = d.gate_proj.rows();
19542 FFN_SCRATCH.with(|s| {
19543 let mut s = s.borrow_mut();
19544 let [g1, g2, u1, u2] = &mut *s;
19545 g1.resize(inter, 0.0);
19546 g2.resize(inter, 0.0);
19547 u1.resize(inter, 0.0);
19548 u2.resize(inter, 0.0);
19549 QTensor::matvec2_many(
19552 [&d.gate_proj, &d.up_proj],
19553 x1,
19554 x2,
19555 [g1.as_mut_slice(), u1.as_mut_slice()],
19556 [g2.as_mut_slice(), u2.as_mut_slice()],
19557 pool,
19558 );
19559 for i in 0..inter {
19560 g1[i] = d.act.combine(g1[i], u1[i]);
19561 g2[i] = d.act.combine(g2[i], u2[i]);
19562 }
19563 let mut o1 = attention::take_buf(d.down_proj.rows());
19564 let mut o2 = attention::take_buf(d.down_proj.rows());
19565 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19566 (o1, o2)
19567 })
19568}
19569
19570#[cfg(test)]
19571mod tests {
19572
19573 #[test]
19578 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19579 use super::{
19580 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19581 };
19582 let dense_card = ChunkStackFacts {
19583 plain_dense: true,
19584 discrete: true,
19585 gpu_on: true,
19586 ..Default::default()
19587 };
19588 assert!(dense_card.dense_on_discrete());
19589 assert_eq!(
19591 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19592 DISCRETE_DENSE_PREFILL_CHUNK
19593 );
19594 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19595 for (label, facts) in [
19596 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19597 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19598 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19599 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19600 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19601 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19602 ] {
19603 assert!(!facts.dense_on_discrete(), "{label}");
19604 assert_eq!(
19605 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19606 48,
19607 "{label} keeps the historical x86 chunk"
19608 );
19609 }
19610 for dense in [false, true] {
19612 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19613 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19614 }
19615 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19617 for dense in [false, true] {
19618 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
19619 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
19620 }
19621 }
19622 }
19623
19624 #[test]
19625 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
19626 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
19627 let full = |host_rows, device_rows| ReuseLayer {
19628 full: true,
19629 host_rows,
19630 device_rows,
19631 device_state: false,
19632 };
19633 assert_eq!(
19636 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
19637 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
19638 );
19639 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
19641 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
19643 assert_eq!(
19645 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
19646 ReusePlan::Pull(vec![(0, 300, 339)])
19647 );
19648 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
19650 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
19651 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
19652 let conv = |device_state| ReuseLayer {
19655 full: false,
19656 host_rows: 0,
19657 device_rows: None,
19658 device_state,
19659 };
19660 assert_eq!(
19661 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
19662 ReusePlan::Fresh
19663 );
19664 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
19665 }
19666
19667 #[test]
19668 fn nll_graph_policy_scopes_only_the_fused_head() {
19669 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
19670 ("vulkan graph", true, true, false, true, false),
19672 ("native Metal graph", true, true, true, true, true),
19674 ("masked", false, true, false, false, false),
19676 ("graph disabled", true, false, true, false, false),
19677 ] {
19678 let (graph_quality, graph_head_required) =
19679 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
19680 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
19681 assert_eq!(graph_head_required, want_head, "{label}: fused head");
19682 }
19683 }
19684
19685 #[test]
19686 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
19687 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
19688 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
19689 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
19690 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
19691 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
19692 }
19693
19694 #[test]
19695 fn cancel_flag_stops_generation() {
19696 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
19697 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
19700 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
19701 assert_eq!(r.finish_reason, "cancelled");
19702 assert!(
19703 r.token_ids.is_empty(),
19704 "no tokens after cancel: {:?}",
19705 r.token_ids
19706 );
19707 assert_eq!(p.kv_cache.seq_len(), 0);
19708 assert!(p.kv_history.is_empty());
19709 assert!(!p.graph_want_logits);
19710 assert!(p.graph_logits.is_none());
19711 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
19713 assert_ne!(r2.finish_reason, "cancelled");
19714 }
19715 use super::*;
19716
19717 #[test]
19718 fn explicit_sliding_schedule_selects_local_rope_profile() {
19719 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
19724 p.swa = Some((1024, usize::MAX));
19725 p.sliding_layers = Some(vec![true, true, true, false]);
19726 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
19727 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
19728 p.rope_scale = 1.277_258_9;
19729 p.rope_scale_local = 1.0;
19730
19731 for li in 0..3 {
19732 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
19733 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
19734 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
19735 }
19736 assert_eq!(p.layer_window(3), None);
19737 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
19738 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
19739 }
19740
19741 #[test]
19749 fn dynamic_ffn_equals_the_zeroing_arm() {
19750 let (hidden, inter) = (8usize, 32usize);
19751 let synth = |n: usize, salt: usize| -> Vec<f32> {
19752 (0..n)
19753 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
19754 .collect()
19755 };
19756 let down = synth(hidden * inter, 3);
19757 let mut down_t = vec![0.0f32; inter * hidden];
19758 for r in 0..hidden {
19759 for c in 0..inter {
19760 down_t[c * hidden + r] = down[r * inter + c];
19761 }
19762 }
19763 let d = DenseFfn {
19764 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19765 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19766 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
19767 act: Act::Silu,
19768 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
19769 segs: Vec::new(),
19770 };
19771 let x = synth(hidden, 11);
19772 let k = 12usize;
19773 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
19774 let mut g = vec![0.0f32; inter];
19776 d.gate_proj.matvec(&x, &mut g, None);
19777 let mut u = vec![0.0f32; inter];
19778 d.up_proj.matvec(&x, &mut u, None);
19779 for v in g.iter_mut() {
19780 *v = inference::silu(*v);
19781 }
19782 keep_top_k(&mut g, k);
19783 for i in 0..inter {
19784 g[i] *= u[i];
19785 }
19786 let mut want = vec![0.0f32; hidden];
19787 d.down_proj.matvec(&g, &mut want, None);
19788 for (a, b) in want.iter().zip(&got) {
19789 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
19790 }
19791 }
19792
19793 #[test]
19799 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
19800 let (hidden, core, tube) = (8usize, 12usize, 8usize);
19801 let inter = core + tube;
19802 let synth = |n: usize, salt: usize| -> Vec<f32> {
19803 (0..n)
19804 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
19805 .collect()
19806 };
19807 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
19808 let d_all = synth(hidden * inter, 3);
19809 let dense = DenseFfn {
19811 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
19812 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
19813 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
19814 act: Act::Silu,
19815 down_t: None,
19816 segs: Vec::new(),
19817 };
19818 let rows =
19819 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
19820 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
19821 let mut o = Vec::with_capacity(hidden * (b - a));
19822 for r in 0..hidden {
19823 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
19824 }
19825 o
19826 };
19827 let tubed = DenseFfn {
19828 down_t: None,
19829 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
19830 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
19831 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
19832 act: Act::Silu,
19833 segs: vec![FfnSeg {
19834 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
19835 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
19836 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
19837 start: core,
19838 width: tube,
19839 }],
19840 };
19841 let x = synth(hidden, 7);
19842 let want = dense_ffn(&dense, &x, None);
19843 let got = tube_ffn(&tubed, &x, 1, None, None);
19844 for (a, b) in want.iter().zip(&got) {
19845 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
19846 }
19847 let mut bits = vec![0u8; inter.div_ceil(8)];
19849 for n in 0..core {
19850 bits[n / 8] |= 1 << (n % 8);
19851 }
19852 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19853 let masked = dense_ffn_masked(&dense, &x, None, &bits);
19854 for (a, b) in masked.iter().zip(&closed) {
19855 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
19856 }
19857 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19859 for (a, b) in closed.iter().zip(&batch) {
19860 assert_eq!(a, b, "batch arm disagrees with decode arm");
19861 }
19862 }
19863
19864 #[test]
19866 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
19867 let (hidden, inter) = (16usize, 40usize);
19868 let synth = |n: usize, salt: usize| -> Vec<f32> {
19869 (0..n)
19870 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
19871 .collect()
19872 };
19873 let d = DenseFfn {
19874 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19875 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19876 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
19877 act: Act::Silu,
19878 down_t: None,
19879 segs: Vec::new(),
19880 };
19881 let x = synth(hidden, 9);
19882 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
19884
19885 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
19886
19887 let mut g = vec![0.0f32; inter];
19889 d.gate_proj.matvec(&x, &mut g, None);
19890 let mut u = vec![0.0f32; inter];
19891 d.up_proj.matvec(&x, &mut u, None);
19892 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
19893 for i in 0..inter {
19894 g[i] = if act_set.contains(&(i as u16)) {
19895 inference::silu(g[i]) * u[i]
19896 } else {
19897 0.0
19898 };
19899 }
19900 let mut reference = vec![0.0f32; hidden];
19901 d.down_proj.matvec(&g, &mut reference, None);
19902
19903 let max_d = sparse
19904 .iter()
19905 .zip(&reference)
19906 .map(|(a, b)| (a - b).abs())
19907 .fold(0.0f32, f32::max);
19908 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
19909 }
19910
19911 fn attach_test_mtp(p: &mut Pipeline) {
19913 let (h, inter, heads, kv, hd) = (
19914 p.hidden_size,
19915 p.intermediate_size,
19916 p.num_heads,
19917 p.num_kv_heads,
19918 p.head_dim,
19919 );
19920 let synth = |n: usize, salt: usize| -> Vec<f32> {
19921 (0..n)
19922 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
19923 .collect()
19924 };
19925 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19926 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19927 };
19928 p.mtp = Some(MtpModule {
19929 enorm: vec![1.0; h],
19930 hnorm: vec![1.0; h],
19931 eh_proj: qt(h, 2 * h, 301),
19932 layer: LayerWeights {
19933 input_norm: vec![1.0; h],
19934 post_norm: vec![1.0; h],
19935 attn_out_norm: None,
19936 ffn_out_norm: None,
19937 layer_scale: None,
19938 ffn: FfnKind::Dense(DenseFfn {
19939 gate_proj: qt(inter, h, 315),
19940 up_proj: qt(inter, h, 316),
19941 down_proj: qt(h, inter, 317),
19942 act: Act::Silu,
19943 down_t: None,
19944 segs: Vec::new(),
19945 }),
19946 attn: AttnKind::Full {
19947 bias: None,
19948 wq: qt(heads * hd, h, 311),
19949 wk: qt(kv * hd, h, 312),
19950 wv: qt(kv * hd, h, 313),
19951 wo: qt(h, heads * hd, 314),
19952 q_norm: None,
19953 k_norm: None,
19954 output_gate: false,
19955 softplus_gate: None,
19956 },
19957 },
19958 final_norm: vec![1.0; h],
19959 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
19960 });
19961 }
19962
19963 #[test]
19964 fn speculative_equals_vanilla_greedy() {
19965 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19969 let run = |spec: bool| {
19970 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19971 p.sampler_config.temperature = 0.0;
19972 attach_test_mtp(&mut p);
19973 p.speculative = spec;
19974 let r = p.generate("abcdef", 12, None, None).unwrap();
19975 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
19976 };
19977 let (vanilla, d0, _) = run(false);
19978 let (spec, d1, a1) = run(true);
19979 assert_eq!(d0, 0, "vanilla path must not draft");
19980 assert!(d1 > 0, "speculative path must draft");
19981 assert_eq!(
19982 vanilla, spec,
19983 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
19984 );
19985 }
19986
19987 #[test]
19988 fn speculative_accepts_constant_oracle() {
19989 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19991 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19992 p.sampler_config.temperature = 0.0;
19993 p.sampler_config.repetition_penalty = 1.0;
19994 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
19997 attach_test_mtp(&mut p);
19998 p.speculative = true;
19999 let r = p.generate("abcd", 10, None, None).unwrap();
20000 assert!(r.mtp_drafted > 0);
20001 assert_eq!(
20002 r.mtp_accepted, r.mtp_drafted,
20003 "constant logits → every draft accepted"
20004 );
20005 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
20008 }
20009
20010 #[test]
20011 fn empty_prompt_is_an_error_not_a_panic() {
20012 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20013 let r = p.generate("", 4, None, None);
20014 assert!(r.is_err(), "empty prompt must be a clean error");
20015 }
20016
20017 #[test]
20018 fn every_token_enters_kv_exactly_once() {
20019 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20020 p.sampler_config.temperature = 0.0;
20022 let r = p.generate("abc", 2, None, None).unwrap();
20023 assert_eq!(r.prompt_tokens, 3);
20024 assert_eq!(
20028 p.kv_cache.seq_len(),
20029 3 + r.tokens_generated - 1,
20030 "each token must be cached exactly once (v1 cached the last prompt token twice)"
20031 );
20032 }
20033
20034 #[test]
20035 fn generation_is_reproducible_with_seed() {
20036 let run = || {
20037 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20038 p.generate("hello", 8, None, None).unwrap().token_ids
20039 };
20040 assert_eq!(run(), run());
20041 }
20042
20043 #[test]
20044 fn resetting_sampler_restarts_the_seeded_stream() {
20045 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20046 let config = SamplerConfig {
20047 seed: Some(1234),
20048 ..SamplerConfig::default()
20049 };
20050 p.set_sampler_config(config.clone());
20051 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20052 p.set_sampler_config(config);
20053 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20054 assert_eq!(first, second);
20055 }
20056
20057 #[test]
20058 fn eviction_bounds_the_cache() {
20059 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20060 p.kv_cache.max_seq_len = 6;
20061 p.sampler_config.temperature = 0.0;
20062 let _ = p.generate("abcd", 12, None, None).unwrap();
20063 assert!(
20064 p.kv_cache.seq_len() <= 6 + 1,
20065 "cache must stay bounded by max_seq_len (got {})",
20066 p.kv_cache.seq_len()
20067 );
20068 }
20069
20070 #[test]
20071 fn confidence_matches_tokens_and_is_a_probability() {
20072 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20073 p.sampler_config.temperature = 0.0;
20074 p.sampler_config.repetition_penalty = 1.0;
20075 let r = p.generate("abcd", 10, None, None).unwrap();
20076 assert_eq!(
20077 r.token_confidence.len(),
20078 r.token_ids.len(),
20079 "one confidence per emitted token"
20080 );
20081 for &c in &r.token_confidence {
20082 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20083 }
20084 let logits = [1.0f32, 3.0, 0.5, 3.0];
20086 let p0 = top1_prob_t(&logits, 1, 1.0);
20087 let p1 = top1_prob_t(&logits, 3, 1.0);
20088 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20089 assert!(p0 > 0.0 && p0 < 1.0);
20090 let sharp = top1_prob_t(&logits, 1, 1.0);
20092 let soft = top1_prob_t(&logits, 1, 2.0);
20093 assert!(soft < sharp, "higher temperature lowers peak confidence");
20094 }
20095
20096 #[test]
20097 fn trace_is_opt_in_and_parallels_the_output() {
20098 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20100 p.sampler_config.temperature = 0.0;
20101 p.sampler_config.repetition_penalty = 1.0;
20102 let r = p.generate("abcd", 10, None, None).unwrap();
20103 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20104
20105 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20107 p.sampler_config.temperature = 0.0;
20108 p.sampler_config.repetition_penalty = 1.0;
20109 p.set_trace(true);
20110 let r = p.generate("abcd", 10, None, None).unwrap();
20111 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20112 for (i, tr) in r.traces.iter().enumerate() {
20113 assert_eq!(tr.t, i, "trace index is sequential");
20114 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20115 assert_eq!(
20116 tr.confidence, r.token_confidence[i],
20117 "trace confidence matches the confidence channel"
20118 );
20119 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20121 }
20122 }
20123
20124 #[test]
20125 fn explain_prefill_logits_match_greedy_first_token() {
20126 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20130 p.sampler_config.temperature = 0.0;
20131 p.sampler_config.repetition_penalty = 1.0;
20132 let ids = p.tokenizer.encode("abcd");
20133 let logits = p.prefill_next_logits(&ids, None);
20134 let argmax = logits
20135 .iter()
20136 .enumerate()
20137 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20138 .unwrap()
20139 .0 as u32;
20140 let r = p.generate("abcd", 1, None, None).unwrap();
20141 assert_eq!(
20142 argmax, r.token_ids[0],
20143 "explain preview must match greedy emit"
20144 );
20145 }
20146
20147 #[test]
20148 fn laguna_shared_expert_is_unconditionally_added() {
20149 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20150 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20151 let zero_dense = || DenseFfn {
20152 gate_proj: matrix(vec![0.0; 4]),
20153 up_proj: matrix(vec![0.0; 4]),
20154 down_proj: matrix(vec![0.0; 4]),
20155 act: Act::Silu,
20156 down_t: None,
20157 segs: Vec::new(),
20158 };
20159 let shared = DenseFfn {
20160 gate_proj: identity(),
20161 up_proj: identity(),
20162 down_proj: identity(),
20163 act: Act::Silu,
20164 down_t: None,
20165 segs: Vec::new(),
20166 };
20167 let x = [1.0, 2.0];
20168 let expected = dense_ffn(&shared, &x, None);
20169 let moe = MoeFfn {
20170 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20171 experts: vec![zero_dense()],
20172 top_k: 1,
20173 norm_topk_prob: true,
20174 router_sigmoid: true,
20175 expert_bias: None,
20176 routed_scaling: 1.0,
20177 route_tau: None,
20178 shared: Some((shared, None)),
20179 stats: std::cell::RefCell::new(Vec::new()),
20180 act_sq: std::cell::RefCell::new(Vec::new()),
20181 act_rows: std::cell::RefCell::new(Vec::new()),
20182 mask: None,
20183 per_expert_scale: None,
20184 router_input_norm: false,
20185 resonance: None,
20186 grown: Vec::new(),
20187 };
20188 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20189 for (actual, expected) in actual.iter().zip(expected) {
20190 assert!((actual - expected).abs() < 1e-6);
20191 }
20192 }
20193
20194 fn mimo_test_pipeline() -> Pipeline {
20203 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20204 let kvh = [1usize, 2, 2, 1];
20205 let synth = |n: usize, salt: usize| -> Vec<f32> {
20206 (0..n)
20207 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20208 .collect()
20209 };
20210 let qt = |rows: usize, cols: usize, salt: usize| {
20211 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20212 };
20213 let dense = |inter: usize, salt: usize| DenseFfn {
20214 gate_proj: qt(inter, hs, salt),
20215 up_proj: qt(inter, hs, salt + 1),
20216 down_proj: qt(hs, inter, salt + 2),
20217 act: Act::Silu,
20218 down_t: None,
20219 segs: Vec::new(),
20220 };
20221 let layers: Vec<LayerWeights> = (0..4)
20222 .map(|li| LayerWeights {
20223 input_norm: vec![1.0; hs],
20224 post_norm: vec![1.0; hs],
20225 attn_out_norm: None,
20226 ffn_out_norm: None,
20227 layer_scale: None,
20228 ffn: if li == 0 {
20229 FfnKind::Dense(dense(inter, 50))
20230 } else {
20231 FfnKind::Moe(MoeFfn {
20232 router: qt(4, hs, 60 + li),
20233 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20234 top_k: 2,
20235 norm_topk_prob: true,
20236 router_sigmoid: true,
20237 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20238 routed_scaling: 1.0,
20239 route_tau: None,
20240 shared: None,
20241 stats: std::cell::RefCell::new(Vec::new()),
20242 act_sq: std::cell::RefCell::new(Vec::new()),
20243 act_rows: std::cell::RefCell::new(Vec::new()),
20244 mask: None,
20245 per_expert_scale: None,
20246 router_input_norm: false,
20247 resonance: None,
20248 grown: Vec::new(),
20249 })
20250 },
20251 attn: AttnKind::Full {
20252 wq: qt(nh * hd, hs, li * 10 + 1),
20253 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20254 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20255 wo: qt(hs, nh * vd, li * 10 + 4),
20256 q_norm: None,
20257 k_norm: None,
20258 output_gate: false,
20259 softplus_gate: None,
20260 bias: None,
20261 },
20262 })
20263 .collect();
20264 let mut p = Pipeline::new(
20265 Tokenizer::byte_level(),
20266 PipelineWeights {
20267 embed_tokens: qt(vocab, hs, 100),
20268 layers,
20269 lm_head: qt(vocab, hs, 200),
20270 final_norm: vec![1.0; hs],
20271 },
20272 hs,
20273 inter,
20274 nh,
20275 1, hd,
20277 4,
20278 4,
20279 false,
20280 vocab,
20281 1e-6,
20282 1e7,
20283 NormStyle::Qwen,
20284 4096,
20285 SamplerConfig {
20286 seed: Some(7),
20287 ..Default::default()
20288 },
20289 );
20290 p.layer_dump = None;
20292 p.set_rotary(4, 1e7);
20293 p.sliding_layers = Some(vec![false, true, true, false]);
20294 p.swa = Some((3, usize::MAX));
20295 p.rotary_dim_local = Some(4);
20296 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20297 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20298 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20299 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20300 p
20301 }
20302
20303 #[test]
20304 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20305 let mut p = mimo_test_pipeline();
20306 p.speculative = false;
20307 p.ignore_eos = true;
20308 p.sampler_config.temperature = 0.0;
20309 p.sampler_config.repetition_penalty = 1.0;
20310 let a = vec![3, 5, 7, 9, 11, 13];
20311 let b = vec![4, 8, 12, 16, 20, 24];
20312 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20313 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20314 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20318 assert_eq!(actual, expected);
20319 assert!(p.kv_history.is_empty());
20320 let mut extended = a.clone();
20321 extended.push(17);
20322 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20323 p.reset_session();
20324 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20325 assert_eq!(after_media, fresh);
20326 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20327 p.reset_session();
20331 p.generate_from_ids(&a, 1, None, None).unwrap();
20332 let mut media_ids = p.kv_history.clone();
20333 assert!(!media_ids.is_empty());
20334 media_ids.extend_from_slice(&[19, 21, 23]);
20335 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20336 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20337 let mut oracle = mimo_test_pipeline();
20338 oracle.speculative = false;
20339 oracle.ignore_eos = true;
20340 oracle.sampler_config.temperature = 0.0;
20341 oracle.sampler_config.repetition_penalty = 1.0;
20342 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20343 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20344 assert!(p.kv_history.is_empty());
20345 let mut bad = rows;
20346 bad[0] = f32::NAN;
20347 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20348 }
20349
20350 fn f32_bits(v: &[f32]) -> Vec<u32> {
20351 v.iter().map(|x| x.to_bits()).collect()
20352 }
20353
20354 #[test]
20361 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20362 let mut p = mimo_test_pipeline();
20363 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20364 assert_eq!(kv, vec![1, 2, 2, 1]);
20365 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20366 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20367 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20368 let hs = p.hidden_size;
20369 let mut decode = Vec::new();
20370 for (pos, &id) in ids.iter().enumerate() {
20371 let e = p.embed_single(id);
20372 let h = p.forward_layers(&e, pos, None);
20373 decode.push(p.logits_from_hidden(&h));
20374 }
20375 for l in &p.kv_cache.layers {
20376 assert_eq!(l.seq_len, 12);
20377 assert_eq!(l.head_values(0).len(), 12 * 8);
20379 }
20380 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20381
20382 p.clear_sequence_state();
20383 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20384 for pos in 0..ids.len() {
20385 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20386 assert_eq!(
20387 f32_bits(&decode[pos]),
20388 f32_bits(&lg),
20389 "whole prompt, pos {pos}"
20390 );
20391 }
20392
20393 p.clear_sequence_state();
20394 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20395 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20396 for pos in 0..ids.len() {
20397 let row = if pos < 5 {
20398 &a[pos * hs..(pos + 1) * hs]
20399 } else {
20400 &b[(pos - 5) * hs..(pos - 4) * hs]
20401 };
20402 let lg = p.logits_from_hidden(row);
20403 assert_eq!(
20404 f32_bits(&decode[pos]),
20405 f32_bits(&lg),
20406 "two chunks, pos {pos}"
20407 );
20408 }
20409
20410 let last = |p: &mut Pipeline| {
20413 p.clear_sequence_state();
20414 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20415 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20416 };
20417 let base = last(&mut p);
20418 let mut no_sinks = mimo_test_pipeline();
20419 for l in &mut no_sinks.kv_cache.layers {
20420 l.sinks = None;
20421 }
20422 assert_ne!(
20423 f32_bits(&last(&mut no_sinks)),
20424 f32_bits(&base),
20425 "sinks are live"
20426 );
20427 let mut wide = mimo_test_pipeline();
20428 wide.swa = Some((64, usize::MAX));
20429 assert_ne!(
20430 f32_bits(&last(&mut wide)),
20431 f32_bits(&base),
20432 "window is live"
20433 );
20434
20435 p.clear_sequence_state();
20437 p.ignore_eos = true;
20438 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20439 assert_eq!(r.token_ids.len(), 4);
20440 }
20441
20442 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20445 let (hs, nh) = (16usize, 4usize);
20446 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20447 p.layer_dump = None;
20448 p.swa = Some((6, 4));
20449 p.proj_gate_sigmoid = true;
20450 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20451 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20452 let g: Vec<f32> = (0..nh * hs)
20453 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20454 .collect();
20455 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20456 }
20457 }
20458 p.swa_trim = trim;
20459 p
20460 }
20461
20462 #[test]
20468 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20469 let mut a = spark_test_pipeline(None);
20470 let mut b = spark_test_pipeline(Some((2, 4)));
20471 assert_eq!(
20472 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20473 vec![Some(6), Some(6), Some(6), None]
20474 );
20475 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20476 let mut pos = 0usize;
20477 for &n in [5usize, 13, 7, 16].iter().cycle() {
20478 if pos >= ids.len() {
20479 break;
20480 }
20481 let end = (pos + n).min(ids.len());
20482 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20483 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20484 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20485 pos = end;
20486 }
20487 for _ in 0..23 {
20488 let e = a.embed_single(((pos * 7) % 64) as u32);
20489 let ha = a.forward_layers(&e, pos, None);
20490 let hb = b.forward_layers(&e, pos, None);
20491 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20492 for li in 0..3 {
20493 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20494 }
20495 pos += 1;
20496 }
20497 for round in 0..9 {
20498 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20499 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20500 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20501 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20502 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20503 if round % 2 == 1 {
20504 for p in [&mut a, &mut b] {
20506 for l in &mut p.kv_cache.layers {
20507 l.truncate_last(2);
20508 }
20509 }
20510 } else {
20511 pos += 2;
20512 }
20513 }
20514 let e = a.embed_single(9);
20515 let (ha, hb) = (
20516 a.forward_layers(&e, pos, None),
20517 b.forward_layers(&e, pos, None),
20518 );
20519 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20520 for li in 0..3 {
20521 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20522 assert!(lb.base() > 0, "layer {li} trimmed");
20523 assert_eq!(la.base(), 0);
20524 assert_eq!(lb.pos_len(), la.seq_len);
20525 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20526 }
20527 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20528 assert_eq!(
20529 (gb.base(), gb.seq_len),
20530 (0, ga.seq_len),
20531 "the global layer keeps all"
20532 );
20533 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20534 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20535 }
20536
20537 #[test]
20542 fn swa_trim_wire_handoff_continues_bitwise() {
20543 let mut a = spark_test_pipeline(None);
20544 let mut b = spark_test_pipeline(Some((2, 4)));
20545 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20546 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20547 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20548 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20549 let mut pos = ids.len();
20550 for _ in 0..5 {
20551 let e = a.embed_single(((pos * 5) % 64) as u32);
20552 assert_eq!(
20553 f32_bits(&a.forward_layers(&e, pos, None)),
20554 f32_bits(&b.forward_layers(&e, pos, None))
20555 );
20556 pos += 1;
20557 }
20558 let mut c = spark_test_pipeline(Some((2, 4)));
20559 for li in 0..4 {
20560 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20561 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20562 }
20563 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20564 assert_eq!(c.kv_cache.seq_len(), pos);
20565 for step in 0..17 {
20566 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20567 let ha = a.forward_layers(&e, pos, None);
20568 let hc = c.forward_layers(&e, pos, None);
20569 assert_eq!(
20570 f32_bits(&ha),
20571 f32_bits(&hc),
20572 "after the hand-off, step {step}"
20573 );
20574 pos += 1;
20575 }
20576 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20577 }
20578
20579 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20582 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20583 let synth = |len: usize, salt: usize| -> Vec<f32> {
20584 (0..len)
20585 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20586 .collect()
20587 };
20588 let qt = |rows: usize, cols: usize, salt: usize| {
20589 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20590 };
20591 let layers = (0..n)
20592 .map(|k| {
20593 let s = 500 + k * 40;
20594 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20595 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20596 MtpModule {
20597 enorm: vec![1.0; hs],
20598 hnorm: vec![1.0; hs],
20599 eh_proj: qt(hs, 2 * hs, s),
20600 layer: LayerWeights {
20601 input_norm: vec![1.0; hs],
20602 post_norm: vec![1.0; hs],
20603 attn_out_norm: None,
20604 ffn_out_norm: None,
20605 layer_scale: None,
20606 attn: AttnKind::Full {
20607 wq: qt(nh * hd, hs, s + 1),
20608 wk: qt(nkv * hd, hs, s + 2),
20609 wv: qt(nkv * vd, hs, s + 3),
20610 wo: qt(hs, nh * vd, s + 4),
20611 q_norm: None,
20612 k_norm: None,
20613 output_gate: false,
20614 softplus_gate: None,
20615 bias: None,
20616 },
20617 ffn: FfnKind::Dense(DenseFfn {
20618 gate_proj: qt(inter, hs, s + 5),
20619 up_proj: qt(inter, hs, s + 6),
20620 down_proj: qt(hs, inter, s + 7),
20621 act: Act::Silu,
20622 down_t: None,
20623 segs: Vec::new(),
20624 }),
20625 },
20626 final_norm: vec![1.0; hs],
20627 kv,
20628 }
20629 })
20630 .collect();
20631 mimo_mtp::MimoMtp::from_layers(layers)
20632 }
20633
20634 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
20635 p.clear_sequence_state();
20636 p.speculative = spec;
20637 p.ignore_eos = true;
20638 p.sampler_config.temperature = 0.0;
20639 p.generate_from_ids(ids, n, None, None).unwrap()
20640 }
20641
20642 #[test]
20648 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
20649 for post in [false, true] {
20652 let mut p = mimo_test_pipeline();
20653 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
20655 let mut st0 = mimo_test_mtp(3, 1.0);
20656 st0.post_norm_hidden = post;
20657 p.mimo_mtp = Some(st0);
20658 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
20659 let hs = p.hidden_size;
20660 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20661 p.mimo_note_rows(&hb, 0);
20662 let mut st = p.mimo_mtp.take().unwrap();
20663 let k = 3;
20666 let mut inc = Vec::new();
20667 for t in 0..ids.len() - k - 1 {
20668 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
20669 }
20670 let s = ids.len();
20673 let mut reference = vec![vec![0u32; k]; s - k - 1];
20674 let mut fresh = mimo_test_mtp(3, 1.0);
20675 for (layer, m) in fresh.layers.iter_mut().enumerate() {
20676 let n = s - layer - 1;
20677 let mut cats = vec![0.0f32; n * 2 * hs];
20678 for j in 0..n {
20679 let e = p.embed_single(ids[j + layer + 1]);
20680 let raw = &hb[j * hs..(j + 1) * hs];
20681 let g = if post {
20682 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
20683 } else {
20684 raw.to_vec()
20685 };
20686 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
20687 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
20688 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
20689 }
20690 let mut x = vec![0.0f32; n * hs];
20691 m.eh_proj.matmat(&cats, n, &mut x, None);
20692 p.mimo_mtp_block(m, &mut x, n, 0);
20693 for (t, row) in reference.iter_mut().enumerate() {
20694 let y = inference::rms_norm(
20695 &x[t * hs..(t + 1) * hs],
20696 &m.final_norm,
20697 p.rms_eps,
20698 p.norm_style,
20699 );
20700 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
20701 }
20702 }
20703 assert_eq!(inc, reference, "post_norm_hidden = {post}");
20704 let distinct: std::collections::HashSet<u32> =
20706 inc.iter().flatten().copied().collect();
20707 assert!(distinct.len() > 3, "{inc:?}");
20708 let last_t = ids.len() - k - 2;
20710 for m in &st.layers {
20711 assert_eq!(m.kv.seq_len, last_t + 1);
20712 }
20713 }
20714 }
20715
20716 #[test]
20723 fn mimo_speculative_greedy_equals_plain_greedy() {
20724 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20725 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
20726 let n = 24;
20727 let mut p = mimo_test_pipeline();
20728 let plain = mimo_greedy(&mut p, &ids, n, false);
20729 assert_eq!(plain.mtp_drafted, 0);
20730 assert_eq!(plain.token_ids.len(), n);
20731 let plain_kv = p.kv_cache.layers[0].seq_len;
20732
20733 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
20735 let spec = mimo_greedy(&mut p, &ids, n, true);
20736 assert!(spec.mtp_drafted > 0, "the round must draft");
20737 assert_eq!(spec.token_ids, plain.token_ids);
20738 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20739
20740 let mut truth: Vec<u32> = ids.clone();
20743 truth.extend(&plain.token_ids);
20744 let mut noisy = truth.clone();
20745 for (i, t) in noisy.iter_mut().enumerate() {
20746 if i % 5 == 0 {
20747 *t = (*t + 1) % 64;
20748 }
20749 }
20750 let mut st = mimo_test_mtp(3, 1.0);
20751 st.draft_override = Some(noisy);
20752 p.mimo_mtp = Some(st);
20753 let spec = mimo_greedy(&mut p, &ids, n, true);
20754 assert_eq!(spec.token_ids, plain.token_ids);
20755 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20756 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
20757 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
20758 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
20759 assert!(
20760 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
20761 "{:?}",
20762 stats.accept_hist
20763 );
20764 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
20765
20766 let mut st = mimo_test_mtp(3, 1.0);
20769 st.draft_override = Some(truth);
20770 p.mimo_mtp = Some(st);
20771 let spec = mimo_greedy(&mut p, &ids, n, true);
20772 assert_eq!(spec.token_ids, plain.token_ids);
20773 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
20774 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20775
20776 let off = mimo_greedy(&mut p, &ids, n, false);
20778 assert_eq!(off.token_ids, plain.token_ids);
20779 assert_eq!(off.mtp_drafted, 0);
20780 }
20781
20782 #[test]
20791 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
20792 let p = mimo_test_pipeline();
20793 assert_eq!(
20794 p.graph_attn_decline_reason(),
20795 Some("per-layer KV head counts")
20796 );
20797 assert_eq!(p.wgpu_graph_attn_decline(), None);
20798 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
20799 assert_eq!(
20800 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
20801 (1, 4, 4, None, false)
20802 );
20803 assert_eq!(g0.invf, p.inv_freq.as_slice());
20804 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
20805 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
20806 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
20807 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
20808 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
20809 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
20810 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
20811
20812 let emb = p.embed_single(3);
20815 let mut lg = Vec::new();
20816 assert!(
20817 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
20818 .is_none()
20819 );
20820 let mut hid = emb.clone();
20821 assert_eq!(
20822 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
20823 crate::gpu::BatchGraphOutcome::Declined
20824 );
20825 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
20826 assert!(p.try_multi_burst(3, 0, 4).is_none());
20827 assert!(
20828 p.graph_declines().is_empty(),
20829 "no attention decline logged: {:?}",
20830 p.graph_declines()
20831 );
20832 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
20837 assert_eq!(plain().graph_attn_decline_reason(), None);
20838 assert_eq!(plain().wgpu_graph_attn_decline(), None);
20839 assert!(
20840 plain().graph_attn_geom(0).is_none(),
20841 "uniform models keep the historical arms"
20842 );
20843 let mut q = plain();
20844 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
20845 assert_eq!(
20846 q.graph_attn_decline_reason(),
20847 Some("learned attention sinks")
20848 );
20849 assert_eq!(
20850 q.graph_attn_geom(1).unwrap().sink,
20851 Some(&[0.25f32, -0.25][..])
20852 );
20853 let mut q = plain();
20854 q.set_attn_geometry(None, Some(2)).unwrap();
20855 assert_eq!(
20856 q.graph_attn_decline_reason(),
20857 Some("V heads narrower than Q/K heads")
20858 );
20859 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
20860 let mut q = plain();
20861 q.sliding_layers = Some(vec![true, false]);
20862 q.swa = Some((4, usize::MAX));
20863 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
20864 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
20865 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
20866
20867 let mut q = mimo_test_pipeline();
20871 q.rope_scale = 2.0;
20872 q.rope_scale_local = 1.25;
20873 assert_eq!(q.wgpu_graph_attn_decline(), None);
20874 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
20875 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
20876
20877 let mut q = plain();
20880 q.rope_scale = 2.0;
20881 assert_eq!(
20882 q.wgpu_graph_attn_decline(),
20883 Some("scaled RoPE without per-layer geometry")
20884 );
20885 }
20886
20887 #[test]
20888 fn mimo_verify_rewind_preserves_lagging_host_caches() {
20889 let mut p = mimo_test_pipeline();
20890 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
20891 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
20892 for _ in 0..if li == 0 { 2 } else { 12 } {
20893 layer.append(&row, &row, &[]);
20894 }
20895 }
20896 p.mimo_verify_rewind(9).unwrap();
20897 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
20898 for layer in &p.kv_cache.layers[1..] {
20899 assert_eq!(layer.seq_len, 9);
20900 }
20901 }
20902
20903 #[test]
20907 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
20908 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
20909 let _ = std::fs::remove_dir_all(&dir);
20910 let mut p = mimo_test_pipeline();
20911 let hs = p.hidden_size;
20912 let ids = [5u32, 9, 11, 2, 40];
20913 p.layer_dump = Some(dir.join("decode"));
20914 for (pos, &id) in ids.iter().enumerate() {
20915 let e = p.embed_single(id);
20916 let _ = p.forward_layers(&e, pos, None);
20917 }
20918 p.clear_sequence_state();
20919 p.layer_dump = Some(dir.join("prefill"));
20920 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20921 for pos in 0..ids.len() {
20922 for li in 0..p.num_layers {
20923 let name = format!("p{pos:06}_l{li:02}.f32");
20924 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
20925 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
20926 assert_eq!(a.len(), hs * 4, "{name}");
20927 assert_eq!(a, b, "{name}");
20928 }
20929 }
20930 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
20931 let vals: Vec<f32> = last
20932 .chunks(4)
20933 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
20934 .collect();
20935 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
20936 let _ = std::fs::remove_dir_all(&dir);
20937 }
20938
20939 #[test]
20940 fn attn_geometry_and_sinks_are_validated() {
20941 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
20942 assert!(
20943 p.set_attn_geometry(Some(vec![2]), None).is_err(),
20944 "one entry per layer"
20945 );
20946 assert!(
20947 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
20948 "3 does not divide 4"
20949 );
20950 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
20951 assert!(p.set_attn_geometry(None, Some(0)).is_err());
20952 assert!(
20953 p.set_attn_geometry(None, Some(5)).is_err(),
20954 "V wider than the head"
20955 );
20956 p.set_attn_geometry(None, Some(4)).unwrap();
20957 assert_eq!(
20958 p.v_head_dim, None,
20959 "v_head_dim == head_dim is the uniform case"
20960 );
20961 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
20962 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
20963 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
20964 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
20965 assert!(
20966 p.kv_cache.layers[1].sinks.is_some(),
20967 "a reshape keeps the layer's sinks"
20968 );
20969 assert_eq!(p.layer_geom(1).0, 4);
20970 assert!(
20971 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
20972 "one sink per Q head"
20973 );
20974 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
20975 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
20976 }
20977
20978 #[test]
20981 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
20982 let cfg = || {
20983 Some(crate::nystrom::O1Cfg {
20984 layers: crate::nystrom::O1Layers::All,
20985 m: 4,
20986 w: 8,
20987 sink: 2,
20988 rect: crate::nystrom::O1Rect::Aggregate,
20989 })
20990 };
20991 let mut p = mimo_test_pipeline();
20992 p.set_o1(cfg());
20993 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
20994 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
20995 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
20996 q.sliding_layers = Some(vec![false, false, true]);
20997 q.swa = Some((4, usize::MAX));
20998 q.set_o1(cfg());
20999 assert_eq!(q.o1_flags, vec![true, false, false]);
21000 }
21001
21002 #[test]
21003 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
21004 const B: usize = 19;
21005 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21006 p.set_o1(Some(crate::nystrom::O1Cfg {
21007 layers: crate::nystrom::O1Layers::All,
21008 m: 4,
21009 w: 8,
21010 sink: 2,
21011 rect: crate::nystrom::O1Rect::Aggregate,
21012 }));
21013 p.o1_begin_with_prefix(Some(B));
21014 let ids: Vec<u32> = (0..B as u32).collect();
21015 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
21016
21017 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
21018 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
21019 let next = p.embed_single(B as u32);
21020 let _ = p.forward_layers(&next, B, None);
21021 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
21022 }
21023
21024 #[test]
21025 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
21026 const B: usize = 19;
21027 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
21028 let gdn_cfg = crate::linear_core::GdnCfg {
21032 num_v_heads: 2,
21033 num_k_heads: 1,
21034 key_head_dim: 2,
21035 value_head_dim: 4,
21036 conv_kernel: 3,
21037 hidden_size: 8,
21038 rms_eps: 1e-6,
21039 output_gate_sigmoid: false,
21040 };
21041 let synth = |n: usize, salt: usize| -> Vec<f32> {
21042 (0..n)
21043 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21044 .collect()
21045 };
21046 let qt = |rows: usize, cols: usize, salt: usize| {
21047 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21048 };
21049 let c_dim = gdn_cfg.conv_dim();
21050 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21051 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21052 in_proj_qkv: qt(c_dim, 8, 1),
21053 in_proj_z: qt(vd, 8, 2),
21054 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21055 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21056 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21057 a_log: vec![0.2, 0.5],
21058 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21059 norm: vec![1.0; gdn_cfg.value_head_dim],
21060 out_proj: qt(8, vd, 7),
21061 });
21062 p.gdn_cfg = Some(gdn_cfg);
21063 p.set_o1(Some(crate::nystrom::O1Cfg {
21064 layers: crate::nystrom::O1Layers::All,
21065 m: 4,
21066 w: 8,
21067 sink: 2,
21068 rect: crate::nystrom::O1Rect::Aggregate,
21069 }));
21070 p.o1_begin_with_prefix(Some(B));
21071 for pos in 0..B - 2 {
21072 let emb = p.embed_single(pos as u32);
21073 let _ = p.forward_layers(&emb, pos, None);
21074 }
21075 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21076
21077 let e1 = p.embed_single((B - 2) as u32);
21078 let e2 = p.embed_single((B - 1) as u32);
21079 let _ = p.forward_pair(&e1, &e2, B - 2);
21080
21081 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21082 assert!(
21083 p.kv_cache
21084 .layers
21085 .iter()
21086 .enumerate()
21087 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21088 );
21089 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21090 assert_ne!(
21091 p.kv_cache.layers[0].linear_state, lane1_state,
21092 "real pair must commit GDN lane 2 before returning"
21093 );
21094 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21095 let next = p.embed_single(B as u32);
21096 let _ = p.forward_layers(&next, B, None);
21097 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21098 }
21099
21100 #[test]
21101 fn o1_error_observation_stays_terminal_until_reset() {
21102 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21103 p.set_o1(Some(crate::nystrom::O1Cfg {
21104 layers: crate::nystrom::O1Layers::All,
21105 m: 4,
21106 w: 8,
21107 sink: 2,
21108 rect: crate::nystrom::O1Rect::Aggregate,
21109 }));
21110 p.o1_begin();
21111 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21112
21113 assert!(p.o1_seal_checked().is_err());
21114 assert!(
21115 p.o1_seal_checked().is_err(),
21116 "retry must see the sticky error"
21117 );
21118 let k = vec![0.2f32; 4];
21119 let v = vec![0.3f32; 4];
21120 p.kv_cache.layers[0].append(&k, &v, &[]);
21121 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21122
21123 p.reset_session();
21124 p.o1_begin();
21125 p.kv_cache.layers[0].append(&k, &v, &[]);
21126 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21127 }
21128
21129 #[test]
21130 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21131 let ids = vec![1u32, 2, 3, 4, 5, 6];
21132 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21133 p.graph_logits = Some(vec![123.0]);
21134 p.graph_want_logits = true;
21135 p.graph_failed
21136 .store(true, std::sync::atomic::Ordering::Relaxed);
21137 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21138 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21139 assert!(err.contains("before NLL"));
21140 assert!(p.graph_logits.is_none());
21141 assert!(!p.graph_want_logits);
21142 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21143 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21144
21145 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21146 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21147 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21148 assert_eq!(actual.1, expected.1);
21149 assert!((actual.0 - expected.0).abs() < 1e-9);
21150 }
21151
21152 #[test]
21153 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21154 let ids = vec![1u32, 2, 3, 4, 5, 6];
21155 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21156 p.nll_test_fail_at = Some(1);
21157 let err = p
21158 .nll_ids_from(&ids, 0)
21159 .expect_err("one-shot forward failure");
21160 assert!(err.contains("forward") || err.contains("score row"));
21161 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21162 assert!(!p.graph_want_logits);
21163 assert!(p.graph_logits.is_none());
21164 assert!(p.kv_history.is_empty());
21165
21166 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21167 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21168 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21169 assert_eq!(actual.1, expected.1);
21170 assert!((actual.0 - expected.0).abs() < 1e-9);
21171 }
21172
21173 #[test]
21174 fn nll_serial_failure_before_first_row_is_reported() {
21175 let ids = vec![1u32, 2, 3, 4];
21176 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21177 p.nll_test_force_serial = true;
21178 p.nll_test_fail_at = Some(0);
21179 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21180 assert!(err.contains("serial forward"));
21181 assert!(p.kv_history.is_empty());
21182 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21183 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21184 }
21185
21186 #[test]
21187 fn ffn_probe_failure_discards_recorder_and_state() {
21188 let ids = vec![1u32, 2, 3, 4];
21189 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21190 p.nll_test_fail_at = Some(0);
21191 let err = p
21192 .probe_ffn_mass_batch(&ids)
21193 .expect_err("probe forward failure");
21194 assert!(err.contains("NLL"));
21195 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21196 assert!(p.kv_history.is_empty());
21197 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21198 }
21199
21200 #[test]
21201 fn nll_test_controls_are_pipeline_scoped() {
21202 let ids = vec![1u32, 2, 3, 4];
21203 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21204 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21205 failing.nll_test_force_serial = true;
21206 failing.nll_test_fail_at = Some(0);
21207
21208 assert!(!failing.can_prefill_batched());
21209 assert!(unaffected.can_prefill_batched());
21210 let expected = unaffected
21211 .nll_ids_from(&ids, 0)
21212 .expect("unaffected pipeline remains usable");
21213 let err = failing
21214 .nll_ids_from(&ids, 0)
21215 .expect_err("failure injection belongs to failing pipeline");
21216 assert!(err.contains("serial forward"));
21217 assert!(failing.nll_test_fail_at.is_none());
21218 assert!(unaffected.can_prefill_batched());
21219 let actual = unaffected
21220 .nll_ids_from(&ids, 0)
21221 .expect("unaffected pipeline remains reusable");
21222 assert_eq!(actual.1, expected.1);
21223 assert!((actual.0 - expected.0).abs() < 1e-9);
21224 }
21225
21226 #[test]
21227 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21228 let ids = vec![1u32, 2, 3, 4, 5, 6];
21229 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21230 p.graph_logits = Some(vec![123.0]);
21231 p.graph_want_logits = true;
21232 p.graph_failed
21233 .store(true, std::sync::atomic::Ordering::Relaxed);
21234 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21235
21236 let err = p
21237 .forward_ids(&ids, None)
21238 .expect_err("a failed forward must not become a valid head result");
21239 assert!(err.contains("forward_ids setup"));
21240 assert!(p.graph_logits.is_none());
21241 assert!(!p.graph_want_logits);
21242 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21243 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21244 assert_eq!(p.kv_cache.seq_len(), 0);
21245
21246 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21247 .forward_ids(&ids, None)
21248 .expect("fresh forward_ids");
21249 let actual = p
21250 .forward_ids(&ids, None)
21251 .expect("pipeline remains reusable after a failed forward");
21252 assert_eq!(actual.len(), expected.len());
21253 assert!(
21254 actual
21255 .iter()
21256 .zip(expected)
21257 .all(|(a, b)| (a - b).abs() < 1e-9)
21258 );
21259 assert_eq!(p.kv_cache.seq_len(), ids.len());
21260 }
21261
21262 #[test]
21263 fn sigmoid_router_floor_is_explicit_per_architecture() {
21264 let zero = || DenseFfn {
21270 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21271 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21272 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21273 act: Act::Silu,
21274 down_t: None,
21275 segs: Vec::new(),
21276 };
21277 let m = MoeFfn {
21278 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21279 experts: vec![zero(), zero()],
21280 top_k: 1,
21281 norm_topk_prob: true,
21282 router_sigmoid: true,
21283 expert_bias: None,
21284 routed_scaling: 2.5,
21285 route_tau: None,
21286 shared: None,
21287 stats: std::cell::RefCell::new(Vec::new()),
21288 act_sq: std::cell::RefCell::new(Vec::new()),
21289 act_rows: std::cell::RefCell::new(Vec::new()),
21290 mask: None,
21291 per_expert_scale: None,
21292 router_input_norm: false,
21293 resonance: None,
21294 grown: Vec::new(),
21295 };
21296 let logits = [-20.0f32, -20.0];
21297 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21298 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21299 let expected = (p[0] + 1e-20) / m.routed_scaling;
21300 assert!((glm_wsum - expected).abs() < 1e-15);
21301 assert!(generic_wsum > glm_wsum * 100.0);
21302 }
21303
21304 #[test]
21305 fn resonance_scores_match_formula_and_stable_tie() {
21306 let r = Resonance {
21307 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21309 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21310 k: 1,
21311 bias: vec![1.5, 0.5, 0.0],
21312 shell: Vec::new(),
21313 };
21314 let x = [1.0f32, 1.0];
21315 let mut got = vec![0.0; 3];
21316 r.scores(&x, &mut got);
21317 assert!((got[0] - 0.5).abs() < 1e-6);
21321 assert!((got[1] - 0.5).abs() < 1e-6);
21322 assert!(got[2].abs() < 1e-6);
21323 let best = got
21324 .iter()
21325 .enumerate()
21326 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21327 .map(|(i, _)| i);
21328 assert_eq!(best, Some(0));
21329 assert!(got.iter().all(|v| v.is_finite()));
21330 }
21331
21332 #[test]
21337 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21338 let plain = Resonance {
21344 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21345 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21346 k: 1,
21347 bias: vec![1.5, 0.5, 0.0, 0.0],
21348 shell: Vec::new(),
21349 };
21350 let shelled = Resonance {
21351 mu: plain.mu.clone(),
21352 u: plain.u.clone(),
21353 k: 1,
21354 bias: plain.bias.clone(),
21355 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21356 };
21357 assert!(!plain.has_shell());
21358 assert!(shelled.has_shell());
21359 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21360 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21361 set_growth_shell(Some(true));
21362 assert!(growth_shell_enabled());
21363 let x = [1.0f32, 1.0];
21366 plain.scores(&x, &mut a);
21367 shelled.scores(&x, &mut b);
21368 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21369 assert!(a[2] == 0.0 && a[3] == 0.0);
21370 let xo = [3.0f32, 0.0];
21373 plain.scores(&xo, &mut a);
21374 shelled.scores(&xo, &mut b);
21375 assert_eq!(a[2], -6.0);
21376 assert_eq!(a[3], -6.0);
21377 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21378 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21379 assert_eq!(shelled.effective_shell(4), shelled.shell);
21380 set_growth_shell(Some(false));
21383 assert!(!growth_shell_enabled());
21384 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21385 shelled.scores(&xo, &mut b);
21386 assert_eq!(bits(&a), bits(&b));
21387 set_growth_shell(None);
21388 let short = Resonance {
21391 shell: vec![f32::INFINITY, f32::INFINITY],
21392 ..shelled
21393 };
21394 set_growth_shell(Some(true));
21395 short.scores(&xo, &mut b);
21396 assert_eq!(bits(&a), bits(&b));
21397 set_growth_shell(None);
21398 }
21399
21400 #[test]
21404 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21405 let zero = || DenseFfn {
21406 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21407 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21408 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21409 act: Act::Silu,
21410 down_t: None,
21411 segs: Vec::new(),
21412 };
21413 let moe = |sigmoid: bool| MoeFfn {
21414 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21415 experts: vec![zero(), zero(), zero(), zero()],
21416 top_k: 1,
21417 norm_topk_prob: true,
21418 router_sigmoid: sigmoid,
21419 expert_bias: None,
21420 routed_scaling: 1.0,
21421 route_tau: None,
21422 shared: None,
21423 stats: std::cell::RefCell::new(Vec::new()),
21424 act_sq: std::cell::RefCell::new(Vec::new()),
21425 act_rows: std::cell::RefCell::new(Vec::new()),
21426 mask: None,
21427 per_expert_scale: None,
21428 router_input_norm: false,
21429 resonance: None,
21430 grown: Vec::new(),
21431 };
21432 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21433 for sigmoid in [false, true] {
21434 let m = moe(sigmoid);
21435 let (idx, p, wsum) = moe_route(&logits, &m, None);
21436 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21437 assert_eq!(p[1], 0.0);
21438 assert_eq!(p[3], 0.0);
21439 assert!(p[2] > p[0] && p[0] > 0.0);
21440 assert!(p.iter().all(|v| v.is_finite()));
21441 let w = p[2] / wsum;
21442 if sigmoid {
21443 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21445 } else {
21446 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21447 }
21448 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21451 assert_eq!(idx, vec![2]);
21452 }
21453 let m = moe(false);
21455 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21456 assert_eq!(idx, vec![3]);
21457 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21460 assert_eq!(idx, vec![0]);
21461 assert!(p.iter().all(|&v| v == 0.25));
21462 assert!(wsum.is_finite() && wsum > 0.0);
21463 }
21464
21465 #[test]
21470 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21471 let zero = || DenseFfn {
21472 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21473 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21474 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21475 act: Act::Silu,
21476 down_t: None,
21477 segs: Vec::new(),
21478 };
21479 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21480 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21481 experts: vec![zero(), zero(), zero()],
21482 top_k: 1,
21483 norm_topk_prob: norm_topk,
21484 router_sigmoid: false,
21485 expert_bias: None,
21486 routed_scaling: 1.0,
21487 route_tau: None,
21488 shared: None,
21489 stats: std::cell::RefCell::new(Vec::new()),
21490 act_sq: std::cell::RefCell::new(Vec::new()),
21491 act_rows: std::cell::RefCell::new(Vec::new()),
21492 mask: None,
21493 per_expert_scale: None,
21494 router_input_norm: false,
21495 resonance: resonant.then(|| Resonance {
21496 mu: vec![0.0; 6],
21497 u: Vec::new(),
21498 k: 0,
21499 bias: vec![0.0; 3],
21500 shell: Vec::new(),
21501 }),
21502 grown: Vec::new(),
21503 };
21504 let lo = -0.1f32;
21507 let hi = f32::from_bits(lo.to_bits() - 1);
21508 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21509 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21510 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21512 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21513 for norm in [true, false] {
21516 let m = moe(true, norm);
21517 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21518 assert_eq!(idx, vec![1], "norm_topk {norm}");
21519 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21520 assert_eq!(p[1] / wsum, 1.0);
21521 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21524 assert_eq!(idx, vec![0]);
21525 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21527 assert_eq!(idx, vec![2]);
21528 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21529 assert_eq!(idx, vec![0]);
21530 assert_eq!(p[0] / wsum, 1.0);
21531 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21534 assert_eq!(idx, vec![0]);
21535 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21536 }
21537 }
21538}