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 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2140 if self.attn_softcap > 0.0 || !self.metal_rope_scale_ok()
2142 || !crate::gpu::enabled_here()
2143 || !graph_force
2144 || std::env::var("CMF_GPU_BLOCK")
2145 .map(|v| v == "0")
2146 .unwrap_or(false)
2147 {
2148 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2149 eprintln!(
2150 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2151 self.attn_softcap > 0.0,
2152 crate::gpu::enabled_here(),
2153 graph_force,
2154 );
2155 }
2156 if self.graph_head_required {
2157 self.fail_metal_graph("native graph front gate refused");
2158 }
2159 return start;
2160 }
2161 let gemma4 = self.metal_gemma4();
2171 let swa_graph = self.metal_graph_swa() || gemma4;
2172 if (self.swa.is_some() && !swa_graph)
2173 || (self.global_attn.is_some() && !gemma4)
2174 || self.attention_heads_per_layer.is_some()
2175 || (self.attn_v_norm && !gemma4)
2176 || (self.graph_attn_decline_reason().is_some() && !swa_graph)
2178 || self.weights.layers.iter().any(|lw| {
2179 ((lw.attn_out_norm.is_some()
2180 || lw.ffn_out_norm.is_some()
2181 || lw.layer_scale.is_some())
2182 && !gemma4)
2183 || matches!(&lw.ffn, FfnKind::Dense(d)
2184 if !(matches!(d.act, Act::Silu | Act::Gelu)
2185 || (gemma4 && d.act == Act::GeluTanh)))
2186 })
2187 {
2188 if let Some(reason) = self.graph_attn_decline_reason().filter(|_| !swa_graph) {
2191 self.note_graph_decline("metal block graph", reason);
2192 }
2193 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2194 eprintln!(
2195 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2196 self.swa.is_some(),
2197 self.global_attn.is_some(),
2198 self.attention_heads_per_layer.is_some(),
2199 self.attn_v_norm,
2200 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2201 );
2202 }
2203 if self.graph_head_required {
2204 self.fail_metal_graph("native graph architecture gate refused");
2205 }
2206 return start;
2207 }
2208 let limit = upto
2211 .map(|u| u + 1)
2212 .unwrap_or(self.num_layers)
2213 .min(self.num_layers);
2214
2215 enum Item<'a> {
2216 Gdn {
2217 run: Vec<GdnGpuLayer<'a>>,
2218 first: usize,
2219 },
2220 Attn {
2221 l: AttnGpuLayer<'a>,
2222 li: usize,
2223 q_norm: Option<&'a [f32]>,
2224 k_norm: Option<&'a [f32]>,
2225 output_gate: bool,
2226 proj_gate: Option<(&'a QTensor, bool)>,
2230 head_gate_w: Option<&'a [f32]>,
2233 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2234 full_gpu: bool,
2237 },
2238 }
2239
2240 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2247 let attend_contract = attend_mode != "0"
2248 && attend_mode != "off"
2249 && self.head_dim % 4 == 0
2250 && self.head_dim <= 256
2251 && self.rotary_dim >= 2
2252 && self.rotary_dim <= self.head_dim
2253 && (self.rotary_dim / 2) % 32 == 0
2254 && self.num_kv_heads > 0
2255 && self.num_heads % self.num_kv_heads == 0;
2256
2257 let mut plan: Vec<Item> = Vec::new();
2258 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2259 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2261 let mut scan = start;
2262 while scan < limit {
2263 let lw = &self.weights.layers[self.phys_layer(scan)];
2264 let ffn = match &lw.ffn {
2265 FfnKind::Dense(d) if d.segs.is_empty() => {
2266 let (Some(g), Some(u), Some(dn)) = (
2267 d.gate_proj.metal_graph_parts(),
2268 d.up_proj.metal_graph_parts(),
2269 d.down_proj.metal_graph_parts(),
2270 ) else {
2271 if block_diag {
2272 eprintln!(
2273 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2274 );
2275 }
2276 break;
2277 };
2278 MetalFfn::Dense {
2279 gate: g,
2280 up: u,
2281 down: dn,
2282 act: d.act.graph_act().map_or(0, |a| a.code() as u8),
2285 }
2286 }
2287 FfnKind::Moe(m) => {
2288 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
2289 if block_diag {
2290 eprintln!(
2291 "block-graph: L{scan} MoE outside the graph contract — run ends"
2292 );
2293 }
2294 break;
2295 };
2296 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2297 model_ref.get_or_insert_with(|| model.clone());
2298 }
2299 MetalFfn::Moe(moe)
2300 }
2301 _ => {
2302 if block_diag {
2303 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2304 }
2305 break;
2306 }
2307 };
2308 match &lw.attn {
2309 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2310 let parts = (
2311 w.in_proj_qkv.metal_graph_parts(),
2312 w.in_proj_z.metal_graph_parts(),
2313 w.in_proj_a.f32_parts(),
2314 w.in_proj_b.f32_parts(),
2315 w.out_proj.metal_graph_parts(),
2316 );
2317 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2318 if block_diag {
2319 eprintln!(
2320 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2321 w.in_proj_qkv.metal_graph_parts().is_some(),
2322 w.in_proj_z.metal_graph_parts().is_some(),
2323 w.in_proj_a.f32_parts().is_some(),
2324 w.in_proj_b.f32_parts().is_some(),
2325 w.out_proj.metal_graph_parts().is_some(),
2326 );
2327 }
2328 break;
2329 };
2330 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2331 model_ref.get_or_insert_with(|| model.clone());
2332 }
2333 let gl = GdnGpuLayer {
2334 attn_norm: &lw.input_norm,
2335 post_norm: &lw.post_norm,
2336 qkv,
2337 z,
2338 a,
2339 b,
2340 out,
2341 ffn,
2342 conv1d: &w.conv1d,
2343 a_log: &w.a_log,
2344 dt_bias: &w.dt_bias,
2345 gnorm: &w.norm,
2346 };
2347 match plan.last_mut() {
2348 Some(Item::Gdn { run, .. }) => run.push(gl),
2349 _ => plan.push(Item::Gdn {
2350 run: vec![gl],
2351 first: scan,
2352 }),
2353 }
2354 }
2355 AttnKind::Full {
2356 wq,
2357 wk,
2358 wv,
2359 wo,
2360 q_norm,
2361 k_norm,
2362 output_gate,
2363 softplus_gate,
2364 bias,
2365 } if (!self.kv_cache.layers[scan].o1_sealed()
2366 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2371 && softplus_gate
2375 .as_ref()
2376 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2377 {
2378 let parts = (
2379 wq.metal_graph_parts(),
2380 wk.metal_graph_parts(),
2381 wv.metal_graph_parts(),
2382 wo.metal_graph_parts(),
2383 );
2384 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2385 break;
2386 };
2387 if let QTensor::Mapped { model, .. } = wq {
2388 model_ref.get_or_insert_with(|| model.clone());
2389 }
2390 let cache = &self.kv_cache.layers[scan];
2391 let o1_metal = cache.o1.is_some()
2395 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2396 && cache.o1_views().is_some();
2397 let head_gate_w = softplus_gate
2401 .as_ref()
2402 .and_then(|(g, _)| g.f32_parts())
2403 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2404 .map(|(d, _, _)| d);
2405 let (nkv_g, hd_g) = if gemma4 {
2408 let (a, b, _) = self.layer_geom(scan);
2409 (a, b)
2410 } else {
2411 (self.num_kv_heads, self.head_dim)
2412 };
2413 let full_gpu = attend_contract
2414 && softplus_gate.is_none() == head_gate_w.is_none()
2415 && cache.mode == crate::kv_cache::KvMode::F32
2416 && (cache.o1.is_none() || o1_metal)
2417 && bias.is_none()
2418 && (hd_g <= 256 || crate::gpu_metal::gemma4::w16_on())
2419 && pq.1 == self.num_heads * hd_g * (1 + *output_gate as usize)
2420 && pk.1 == nkv_g * hd_g
2421 && pv.1 == nkv_g * hd_g
2422 && po.2 == self.num_heads * hd_g;
2423 plan.push(Item::Attn {
2424 l: AttnGpuLayer {
2425 attn_norm: &lw.input_norm,
2426 post_norm: &lw.post_norm,
2427 wq: pq,
2428 wk: pk,
2429 wv: pv,
2430 wo: po,
2431 ffn,
2432 attn_out_norm: lw.attn_out_norm.as_deref(),
2433 ffn_out_norm: lw.ffn_out_norm.as_deref(),
2434 layer_scale: lw.layer_scale,
2435 },
2436 li: scan,
2437 q_norm: q_norm.as_deref(),
2438 k_norm: k_norm.as_deref(),
2439 output_gate: *output_gate,
2440 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2441 head_gate_w,
2442 bias: bias
2443 .as_ref()
2444 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2445 full_gpu,
2446 });
2447 }
2448 _ => break,
2449 }
2450 scan += 1;
2451 }
2452 let Some(model) = model_ref else {
2453 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2454 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2455 }
2456 if self.graph_head_required {
2457 self.fail_metal_graph("native graph has no mapped model reference");
2458 }
2459 return start;
2460 };
2461 if plan.is_empty() {
2462 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2463 eprintln!("q1-graph: empty plan at layer {start}");
2464 }
2465 if self.graph_head_required {
2466 self.fail_metal_graph("native graph plan is empty");
2467 }
2468 return start;
2469 }
2470 let has_moe = plan.iter().any(|it| match it {
2471 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2472 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2473 });
2474 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2475 let dev_attend = attend_contract
2476 && (self.head_dim <= 128
2477 || has_moe
2478 || (self.head_dim <= 256 && has_gdn)
2484 || (self.head_dim <= 256 && swa_graph)
2488 || attend_mode == "force"
2489 || attend_mode == "256");
2490 if !dev_attend {
2491 for it in &mut plan {
2492 if let Item::Attn { li, full_gpu, .. } = it {
2493 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2496 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2497 if !keep_o1 {
2498 *full_gpu = false;
2499 }
2500 }
2501 }
2502 }
2503 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2504 use std::sync::atomic::{AtomicBool, Ordering};
2505 static SAID: AtomicBool = AtomicBool::new(false);
2506 if !SAID.swap(true, Ordering::Relaxed) {
2507 let fg = plan
2508 .iter()
2509 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2510 .count();
2511 let att = plan
2512 .iter()
2513 .filter(|it| matches!(it, Item::Attn { .. }))
2514 .count();
2515 eprintln!(
2516 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2517 plan.len(),
2518 self.head_dim,
2519 self.rotary_dim,
2520 self.num_kv_heads,
2521 self.num_heads,
2522 );
2523 }
2524 }
2525 let dims = GraphDims {
2526 hidden: self.hidden_size,
2527 eps: self.rms_eps as f32,
2528 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2529 };
2530 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
2531 if self.graph_head_required {
2532 self.fail_metal_graph("native TokenGraph allocation refused");
2533 }
2534 return start;
2535 };
2536 if swa_graph && self.head_dim > 128 {
2537 graph.set_attend_blk_from(64);
2543 }
2544 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2545 nv: cfg.num_v_heads,
2546 nk: cfg.num_k_heads,
2547 dk: cfg.key_head_dim,
2548 dv: cfg.value_head_dim,
2549 kk: cfg.conv_kernel,
2550 hidden: self.hidden_size,
2551 inter: self.intermediate_size,
2552 c_dim: cfg.conv_dim(),
2553 eps: cfg.rms_eps as f32,
2554 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2555 });
2556 let mut valid = 0usize;
2560 let mut end = start;
2561 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2563 static ONCE: std::sync::Once = std::sync::Once::new();
2564 ONCE.call_once(|| {
2565 for it in &plan {
2566 match it {
2567 Item::Gdn { first, run } => {
2568 eprintln!("plan: Gdn first={first} len={}", run.len())
2569 }
2570 Item::Attn { li, full_gpu, .. } => {
2571 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2572 }
2573 }
2574 }
2575 });
2576 }
2577 for item in &plan {
2578 let ok = match item {
2579 Item::Gdn { run, .. } => gcfg
2580 .as_ref()
2581 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2582 .unwrap_or(false),
2583 Item::Attn { l, .. } => graph.attn_ok(l),
2584 };
2585 if !ok {
2586 if block_diag {
2587 eprintln!(
2588 "block-graph: plan item {} ({}) failed graph preflight",
2589 valid,
2590 match item {
2591 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2592 Item::Attn { li, .. } => format!("Attn L{li}"),
2593 }
2594 );
2595 }
2596 break;
2597 }
2598 valid += 1;
2599 end += match item {
2600 Item::Gdn { run, .. } => run.len(),
2601 Item::Attn { .. } => 1,
2602 };
2603 }
2604 plan.truncate(valid);
2605 if plan.is_empty() {
2606 if self.graph_head_required {
2607 self.fail_metal_graph("native graph preflight produced no valid items");
2608 }
2609 return start;
2610 }
2611
2612 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2613 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2614 return start;
2615 }
2616
2617 let one_pass = |t: (usize, usize, usize)| {
2626 use cortiq_core::TensorDtype as D;
2627 matches!(
2628 model.tensors[t.0].dtype,
2629 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2630 )
2631 };
2632 let dense_fast = plan.iter().all(|it| match it {
2633 Item::Attn {
2634 l, li, full_gpu, ..
2635 } => {
2636 *full_gpu
2637 && self.kv_cache.layers[*li].o1.is_none()
2638 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2639 && match l.ffn {
2640 MetalFfn::Dense { gate, up, down, .. } => {
2641 one_pass(gate) && one_pass(up) && one_pass(down)
2642 }
2643 _ => false,
2644 }
2645 }
2646 Item::Gdn { .. } => false,
2647 });
2648 let moe_plan = !dense_fast
2652 && plan.iter().all(|it| match it {
2653 Item::Attn {
2654 l, li, full_gpu, ..
2655 } => {
2656 *full_gpu
2657 && self.kv_cache.layers[*li].o1.is_none()
2658 && matches!(l.ffn, MetalFfn::Moe(_))
2659 }
2660 Item::Gdn { .. } => false,
2661 });
2662 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast || moe_plan);
2663 if block_diag {
2664 use std::sync::atomic::{AtomicBool, Ordering};
2665 static SAID_AB: AtomicBool = AtomicBool::new(false);
2666 if !SAID_AB.swap(true, Ordering::Relaxed) {
2667 eprintln!("q1-graph: dense_fast={dense_fast} moe_plan={moe_plan} ab={ab:?}");
2668 }
2669 }
2670 let _mv_fast = match ab {
2671 Some((bits, _)) => {
2672 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2673 crate::gpu_metal::MvFastGuard::set_raw(bits)
2674 }
2675 None if moe_plan => {
2676 graph.set_dense_concurrent(crate::gpu_metal::MOE_PLAN_BITS & crate::gpu_metal::DENSE_CONC != 0);
2677 crate::gpu_metal::MvFastGuard::set_bits(crate::gpu_metal::MOE_PLAN_BITS)
2678 }
2679 None => {
2680 graph.set_dense_concurrent(dense_fast);
2681 let q8r4 = if swa_graph {
2684 crate::gpu_metal::DENSE_Q8R4
2685 } else {
2686 0
2687 };
2688 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2689 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2690 } else {
2691 0
2692 })
2693 }
2694 };
2695
2696 let pool = self.pool.clone();
2699 let (nh, nkv, hd, hs, eps) = (
2700 self.num_heads,
2701 self.num_kv_heads,
2702 self.head_dim,
2703 self.hidden_size,
2704 self.rms_eps,
2705 );
2706 let norm_style = self.norm_style;
2707 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2708 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2709 let kv_id = self.graph_kv_id;
2710 let mut pending: Vec<(usize, usize)> = Vec::new();
2713 let mut dev_attn: Vec<(usize, usize, usize)> = Vec::new();
2716 for item in &plan {
2717 let _xt0 = std::time::Instant::now();
2718 let _xkind: u32 = match item {
2719 Item::Gdn { .. } => 2,
2720 Item::Attn { .. } => 3,
2721 };
2722 if self.loop_final_norm {
2724 let item_start = match item {
2725 Item::Gdn { first, .. } => *first,
2726 Item::Attn { li, .. } => *li,
2727 };
2728 if item_start > start && self.is_loop_end(item_start - 1) {
2729 graph.encode_loop_norm(&self.weights.final_norm);
2730 }
2731 }
2732 match item {
2733 Item::Gdn { run, first } => {
2734 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2735 if l.linear_state.len() != want {
2736 l.linear_state = vec![0f32; want];
2737 }
2738 }
2739 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2740 .iter()
2741 .map(|l| l.linear_state.as_slice())
2742 .collect();
2743 let _ig = std::time::Instant::now();
2744 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2745 tracing::error!("q1 graph: GDN run refused after validation");
2747 return start;
2748 }
2749 graph.commit_kind = 2;
2752 graph.commit();
2753 crate::gpu::stageprof(0, _ig.elapsed());
2754 pending.push((*first, run.len()));
2755 }
2756 Item::Attn {
2757 l,
2758 li,
2759 q_norm,
2760 k_norm,
2761 output_gate,
2762 proj_gate,
2763 head_gate_w,
2764 bias,
2765 full_gpu,
2766 } => {
2767 let _ia = std::time::Instant::now();
2768 let inv_freq_l = self.layer_inv_freq(*li);
2774 let rd_l = self.layer_geom(*li).2;
2775 let window_l = self.layer_window(*li);
2776 let head_gate_w = *head_gate_w;
2777 let (nkv, hd) = if gemma4 {
2780 let (a, b, _) = self.layer_geom(*li);
2781 (a, b)
2782 } else {
2783 (nkv, hd)
2784 };
2785 if *full_gpu {
2787 let cache = &self.kv_cache.layers[*li];
2788 let o1p = if cache.o1.is_some() {
2789 match cache.o1_views() {
2790 Some(views) => Some(crate::gpu::O1AttnParams {
2791 views,
2792 epoch: self.o1_epoch,
2793 }),
2794 None => None,
2796 }
2797 } else {
2798 None
2799 };
2800 let o1_layer = cache.o1.is_some();
2801 if o1_layer && o1p.is_none() {
2802 }
2804 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
2805 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
2806 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
2807 debug_assert!(
2810 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
2811 );
2812 let p = crate::gpu::AttnDeviceParams {
2813 kv_id,
2814 layer: *li,
2815 nh,
2816 nkv,
2817 hd,
2818 rd: rd_l,
2819 position,
2820 scale: self.attn_scale,
2821 eps: eps as f32,
2822 gemma,
2823 late_qk_norm: self.qk_norm_after_rope,
2824 output_gate: *output_gate,
2825 q_norm: *q_norm,
2826 k_norm: *k_norm,
2827 inv_freq: &inv_freq_l,
2828 rope_scale: self.layer_rope_scale(*li),
2829 cpu_k,
2830 cpu_v,
2831 cpu_stored,
2832 cpu_gen: cache.generation(),
2833 o1: o1p,
2834 window: window_l,
2835 head_gate: head_gate_w,
2836 v_norm: self.attn_v_norm,
2837 };
2838 let o1_bad = o1_layer && p.o1.is_none();
2839 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
2840 {
2841 if p.o1.is_none() {
2843 dev_attn.push((*li, nkv, hd));
2844 }
2845 graph.commit_kind = 3;
2846 graph.commit();
2847 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2851 continue;
2852 }
2853 }
2855 graph.encode_attn_prefix(l);
2856 if let Err(err) = graph.sync_checked() {
2857 self.fail_metal_graph(&err);
2858 return start;
2859 }
2860 if !pending.is_empty() {
2861 let idxs: Vec<usize> =
2862 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2863 let mut outs: Vec<&mut [f32]> = self
2864 .kv_cache
2865 .layers
2866 .iter_mut()
2867 .enumerate()
2868 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2869 .map(|(_, s)| s.linear_state.as_mut_slice())
2870 .collect();
2871 graph.read_states(&mut outs);
2872 }
2873 let mut q_raw = attention::take_buf(l.wq.1);
2874 let mut k = attention::take_buf(l.wk.1);
2875 let mut v = attention::take_buf(l.wv.1);
2876 graph.read_qkv(&mut q_raw, &mut k, &mut v);
2877 let mut gate_raw = proj_gate.map(|(gp, _)| {
2880 let mut normed = attention::take_buf(hs);
2881 graph.read_normed(&mut normed);
2882 let mut raw = attention::take_buf(gp.rows());
2883 gp.matvec(&normed, &mut raw, pool.as_deref());
2884 attention::recycle_buf(&mut normed);
2885 raw
2886 });
2887 let cfg = QwenAttnCfg {
2888 num_heads: nh,
2889 num_kv_heads: nkv,
2890 head_dim: hd,
2891 hidden_size: hs,
2892 position,
2893 inv_freq: &inv_freq_l,
2894 rotary_dim: rd_l,
2895 scale: self.attn_scale,
2896 softcap: self.attn_softcap,
2897 window: window_l,
2898 v_norm: self.attn_v_norm,
2901 qk_norm_after_rope: self.qk_norm_after_rope,
2902 gate_sigmoid: self.proj_gate_sigmoid,
2903 q_norm: *q_norm,
2904 k_norm: *k_norm,
2905 output_gate: *output_gate,
2906 softplus_gate: None,
2907 rope_scale: self.layer_rope_scale(*li),
2908 bias: *bias,
2909 rms_eps: eps,
2910 norm_style,
2911 pool: pool.as_deref(),
2912 v_head_dim: hd,
2913 };
2914 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
2917 || std::env::var("CMF_ATTN_DUMP").is_ok();
2918 let _ = full_gpu;
2919 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
2920 let mut ao = attention::qwen_attention_core(
2921 q_raw,
2922 k,
2923 v,
2924 &mut self.kv_cache.layers[*li],
2925 &cfg,
2926 );
2927 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
2931 if let Some((qr0, k0, v0)) = oracle_in.clone() {
2932 let (cq, _cg, _ck, _cv) =
2933 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2934 let cache = &self.kv_cache.layers[*li];
2935 let n = cache.head_keys(0).len() / hd;
2936 let mut bytes: Vec<u8> = Vec::new();
2937 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
2938 bytes.extend_from_slice(&v.to_le_bytes());
2939 }
2940 for v in &cq {
2941 bytes.extend_from_slice(&v.to_le_bytes());
2942 }
2943 for g in 0..nkv {
2944 for v in cache.head_keys(g) {
2945 bytes.extend_from_slice(&v.to_le_bytes());
2946 }
2947 }
2948 for g in 0..nkv {
2949 for v in cache.head_values(g) {
2950 bytes.extend_from_slice(&v.to_le_bytes());
2951 }
2952 }
2953 let _ =
2954 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
2955 }
2956 }
2957 if let Some((qr0, k0, v0)) =
2958 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
2959 {
2960 let (cq, _cg, ck, cv) =
2961 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2962 let mut h_now = vec![0f32; hs];
2963 graph.read_h(&mut h_now);
2964 let cache = &self.kv_cache.layers[*li];
2965 let n_after = cache.head_keys(0).len() / hd;
2966 let stored = n_after.saturating_sub(1);
2970 let cpu_k: Vec<&[f32]> = (0..nkv)
2971 .map(|g| &cache.head_keys(g)[..stored * hd])
2972 .collect();
2973 let cpu_v: Vec<&[f32]> = (0..nkv)
2974 .map(|g| &cache.head_values(g)[..stored * hd])
2975 .collect();
2976 let p = crate::gpu::AttnDeviceParams {
2977 kv_id,
2978 layer: *li,
2979 nh,
2980 nkv,
2981 hd,
2982 rd: rd_l,
2986 position,
2987 scale: self.attn_scale,
2988 eps: eps as f32,
2989 gemma,
2990 late_qk_norm: self.qk_norm_after_rope,
2991 output_gate: *output_gate,
2992 q_norm: *q_norm,
2993 k_norm: *k_norm,
2994 inv_freq: &inv_freq_l,
2995 rope_scale: self.layer_rope_scale(*li),
2996 cpu_k,
2997 cpu_v,
2998 cpu_stored: stored,
2999 cpu_gen: cache.generation(),
3000 o1: None,
3001 window: window_l,
3002 head_gate: None,
3003 v_norm: self.attn_v_norm,
3004 };
3005 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
3006 let md = |a: &[f32], b: &[f32]| {
3007 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
3008 };
3009 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
3010 eprintln!(
3011 "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}",
3012 nn(&cq),
3013 md(&cq, &dq),
3014 nn(&ck),
3015 md(&ck, &dk),
3016 nn(&cv),
3017 md(&cv, &dv),
3018 nn(&ao),
3019 md(&ao, &dao)
3020 );
3021 } else {
3022 eprintln!("attn-oracle L{li}: device probe declined");
3023 }
3024 }
3025 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
3026 attention::apply_projected_gate(
3029 &mut ao,
3030 raw,
3031 per_head,
3032 hd,
3033 self.proj_gate_sigmoid,
3034 );
3035 }
3036 if let Some(mut raw) = gate_raw.take() {
3037 attention::recycle_buf(&mut raw);
3038 }
3039 graph.encode_attn_suffix(l, &ao);
3040 graph.commit();
3043 attention::recycle_buf(&mut ao);
3044 }
3045 }
3046
3047 crate::gpu::stageprof(_xkind, _xt0.elapsed());
3048 }
3049 let mut lm_rows = None;
3054 if self.graph_want_logits
3055 && upto.is_none()
3056 && end == self.num_layers
3057 && std::env::var("CMF_GPU_LMHEAD")
3058 .map(|v| v != "0")
3059 .unwrap_or(true)
3060 {
3061 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
3062 if graph.lm_head_ok(lm) {
3063 graph.encode_lm_head(&self.weights.final_norm, lm);
3064 lm_rows = Some(lm.1);
3065 }
3066 }
3067 }
3068 if self.graph_head_required && lm_rows.is_none() {
3069 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3070 self.fail_metal_graph("fused graph head was requested but not encodable");
3071 return start;
3072 }
3073 let _sy0 = std::time::Instant::now();
3074 if let Err(err) = graph.sync_checked() {
3075 self.fail_metal_graph(&err);
3076 return start;
3077 }
3078 let _rs0 = std::time::Instant::now();
3079 if !pending.is_empty() {
3080 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
3081 let mut outs: Vec<&mut [f32]> = self
3082 .kv_cache
3083 .layers
3084 .iter_mut()
3085 .enumerate()
3086 .filter(|(i, _)| idxs.binary_search(i).is_ok())
3087 .map(|(_, s)| s.linear_state.as_mut_slice())
3088 .collect();
3089 graph.read_states(&mut outs);
3090 }
3091 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
3092 use std::sync::atomic::{AtomicU64, Ordering};
3093 static SY: AtomicU64 = AtomicU64::new(0);
3094 static RS: AtomicU64 = AtomicU64::new(0);
3095 static N: AtomicU64 = AtomicU64::new(0);
3096 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
3097 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3098 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3099 if n % 100 == 0 {
3100 eprintln!(
3101 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
3102 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3103 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3104 );
3105 }
3106 }
3107 if let Some(rows) = lm_rows {
3108 crate::gpu::hostprof_encode_done(_mt0);
3109 let mut lg = attention::take_buf(rows.min(self.vocab_size));
3110 graph.read_logits(&mut lg);
3111 crate::gpu::hostprof_total(_mt0);
3112 lg.resize(self.vocab_size, 0.0);
3113 if let Some(c) = self.final_softcap {
3114 softcap_in_place(&mut lg, c, self.pool.as_deref());
3115 }
3116 self.graph_logits = Some(lg);
3117 }
3118 graph.read_h(h);
3119 if self.graph_head_required && self.graph_logits.is_none() {
3120 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3121 self.fail_metal_graph("fused graph head completed without logits readback");
3122 return start;
3123 }
3124 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3125 METAL_GRAPH_LAYERS.fetch_add(
3126 end.saturating_sub(start) as u64,
3127 std::sync::atomic::Ordering::Relaxed,
3128 );
3129 if self.graph_head_required {
3130 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3131 }
3132 for (li, nkv, hd) in dev_attn {
3136 let mut krow = attention::take_buf(nkv * hd);
3137 let mut vrow = attention::take_buf(nkv * hd);
3138 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
3139 let cache = &mut self.kv_cache.layers[li];
3140 cache.append(&krow, &vrow, &[]);
3141 let n = cache.seq_len;
3142 let mut imp = attention::take_buf(n);
3143 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
3144 cache.accumulate_imp(&imp);
3145 attention::recycle_buf(&mut imp);
3146 }
3147 attention::recycle_buf(&mut krow);
3148 attention::recycle_buf(&mut vrow);
3149 }
3150 if let Some((_, arm)) = ab {
3151 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
3152 }
3153 end
3154 }
3155
3156 pub fn new(
3157 tokenizer: Tokenizer,
3158 weights: PipelineWeights,
3159 hidden_size: usize,
3160 intermediate_size: usize,
3161 num_heads: usize,
3162 num_kv_heads: usize,
3163 head_dim: usize,
3164 num_layers: usize,
3165 physical_layers: usize,
3166 loop_final_norm: bool,
3167 vocab_size: usize,
3168 rms_eps: f64,
3169 rope_base: f32,
3170 norm_style: NormStyle,
3171 max_seq_len: usize,
3172 sampler_config: SamplerConfig,
3173 ) -> Self {
3174 let rng = match sampler_config.seed {
3175 Some(s) => SplitMix64::new(s),
3176 None => SplitMix64::from_entropy(),
3177 };
3178 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3179 let pool = Pool::from_env();
3180 if let Some(p) = &pool {
3181 tracing::info!("worker pool: {} threads", p.n_workers());
3182 if let Some(model) = weights
3184 .lm_head
3185 .model_arc()
3186 .or_else(|| weights.embed_tokens.model_arc())
3187 {
3188 let regions: Vec<&[u8]> =
3189 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3190 p.bind_numa(®ions);
3191 }
3192 }
3193 Self {
3194 gpu_plan: None,
3195 tokenizer: std::sync::Arc::new(tokenizer),
3196 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3197 sampler_config,
3198 weights,
3199 hidden_size,
3200 intermediate_size,
3201 num_heads,
3202 num_kv_heads,
3203 head_dim,
3204 num_layers,
3205 physical_layers,
3206 loop_final_norm,
3207 vocab_size,
3208 rms_eps,
3209 rope_base,
3210 norm_style,
3211 rotary_dim: head_dim,
3212 attention_heads_per_layer: None,
3213 kv_heads_per_layer: None,
3214 v_head_dim: None,
3215 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3216 .filter(|v| !v.is_empty())
3217 .map(std::path::PathBuf::from),
3218 graph_declines: std::cell::RefCell::new(Vec::new()),
3219 mimo_moe: Default::default(),
3220 vmf_cfg: None,
3221 gdn_cfg: None,
3222 kda_cfg: None,
3223 g3n: None,
3224 dsv4: None,
3225 dsv41: None,
3226 dsv41_vision: None,
3227 dsv41_prefill: None,
3228 qwen4_exp: None,
3229 dsv4_mtp: Vec::new(),
3230 dspark: None,
3231 dspark_pending: Vec::new(),
3232 dspark_hist: Vec::new(),
3233 dspark_real: Vec::new(),
3234 dspark_trunk_picks: Vec::new(),
3235 dspark_exp: Vec::new(),
3236 dspark_draft_ns: 0,
3237 logit_multiplier: None,
3238 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3239 graph_failed: std::sync::atomic::AtomicBool::new(false),
3240 kv_history: Vec::new(),
3241 kv_history_device: false,
3242 short_conv_cfg: None,
3243 mtp: None,
3244 mimo_mtp: None,
3245 verify_exact_moe: false,
3246 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3247 ignore_eos: false,
3248 draft_full_streak: 0,
3249 spec_k_adapt: None,
3250 spec_acc_ewma: 0.7,
3251 rng,
3252 sampler_scratch: SamplerScratch::default(),
3253 spec_forced: None,
3254 spec_q: Vec::new(),
3255 spec_p: Vec::new(),
3256 spec_res: Vec::new(),
3257 spec_qs: Vec::new(),
3258 spec_ps: Vec::new(),
3259 spec_ress: Vec::new(),
3260 mtp_graph_mode: None,
3261 #[cfg(target_os = "macos")]
3262 metal_verify: None,
3263 inv_freq,
3264 ws: ForwardScratch::new(hidden_size),
3265 pool,
3266 model: None,
3267 dyn_force_f32: false,
3268 dyn_skill_layers: Vec::new(),
3269 dyn_active: None,
3270 dyn_blend_loaded: false,
3271 dyn_phi_layer: None,
3272 dyn_phi_ema: Vec::new(),
3273 dyn_phi_seen: 0,
3274 dyn_router: None,
3275 o1_cfg: None,
3276 o1_epoch: 0,
3277 o1_flags: Vec::new(),
3278 trace: false,
3279 calib_temp: 1.0,
3280 confidence_on: true,
3281 embed_multiplier: 1.0,
3282 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3283 swa: None,
3284 swa_trim: None,
3285 sliding_layers: None,
3286 anchor_core: None,
3287 bounded_rope: None,
3288 kv_prefix: KvPrefix::default(),
3289 last_prefill_tokens: 0,
3290 inv_freq_local: None,
3291 rotary_dim_local: None,
3292 rope_scale: 1.0,
3293 rope_scale_local: 1.0,
3294 global_attn: None,
3295 inv_freq_global: None,
3296 attn_v_norm: false,
3297 qk_norm_after_rope: false,
3298 proj_gate_sigmoid: false,
3299 final_softcap: None,
3300 head_clusters: None,
3301 attn_softcap: 0.0,
3302 graph_want_logits: false,
3303 graph_head_required: false,
3304 graph_logits: None,
3305 embryo_graph: None,
3306 graph_refused: std::sync::atomic::AtomicBool::new(false),
3307 graph_kv_id: {
3308 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3309 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3310 },
3311 #[cfg(test)]
3312 nll_test_fail_at: None,
3313 #[cfg(test)]
3314 nll_test_force_serial: false,
3315 }
3316 }
3317
3318 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3326 if let Err(e) = self.try_set_o1(cfg) {
3327 tracing::error!("{e}");
3328 }
3329 }
3330
3331 pub fn bounded_native(&self) -> bool {
3335 self.anchor_core.is_some()
3336 }
3337
3338 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3341 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3342 }
3343
3344 pub fn o1_refusal(&self) -> Option<String> {
3346 self.anchor_core.as_ref().map(|ac| {
3347 format!(
3348 "--o1 / CMF_O1 refused: the anchor is native bounded \
3349 (anchor_core kind={} window={} sink={}); the file's operator \
3350 is executed as-is and no post-hoc Nyström overlay applies",
3351 ac.kind, ac.window, ac.sink
3352 )
3353 })
3354 }
3355
3356 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3358 if let Some(c) = &cfg {
3359 if let Some(why) = self.o1_refusal() {
3360 self.o1_flags = Vec::new();
3361 self.o1_cfg = None;
3362 return Err(why);
3363 }
3364 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3365 self.o1_flags.clear();
3366 self.o1_cfg = None;
3367 return Err(format!(
3368 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3369 c.w, c.sink
3370 ));
3371 }
3372 }
3373 self.o1_flags = match &cfg {
3374 Some(c) => {
3375 let mut flags = c.layer_flags(self.num_layers);
3376 for (li, f) in flags.iter_mut().enumerate() {
3377 if *f
3383 && (!matches!(
3384 self.weights.layers[self.phys_layer(li)].attn,
3385 AttnKind::Full { .. }
3386 ) || self.layer_window(li).is_some()
3387 || self.kv_cache.layers[li].sinks.is_some()
3388 || self.layer_v_dim(li) != self.layer_geom(li).1)
3389 {
3390 *f = false;
3391 }
3392 }
3393 flags
3394 }
3395 None => Vec::new(),
3396 };
3397 if let Some(c) = &cfg {
3398 let n = self.o1_flags.iter().filter(|&&f| f).count();
3399 tracing::info!(
3400 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3401 self.num_layers,
3402 c.m,
3403 c.w,
3404 c.sink,
3405 c.rect
3406 );
3407 }
3408 self.o1_cfg = cfg;
3409 Ok(())
3410 }
3411
3412 pub fn install_bounded(
3418 &mut self,
3419 cfg: &cortiq_core::AnchorCoreConfig,
3420 ) -> Result<(), String> {
3421 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3422 return Err(format!(
3423 "anchor_core kind '{}' is not executable by this runtime",
3424 cfg.kind
3425 ));
3426 }
3427 if cfg.window == 0 {
3428 return Err("anchor_core.window must be >= 1".into());
3429 }
3430 let mut n = 0usize;
3431 for li in 0..self.num_layers {
3432 let pl = self.phys_layer(li);
3433 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3434 if w.window != cfg.window || w.sink != cfg.sink {
3435 return Err(format!(
3436 "layer {li}: bounded weights (window {} sink {}) disagree with \
3437 anchor_core (window {} sink {})",
3438 w.window, w.sink, cfg.window, cfg.sink
3439 ));
3440 }
3441 self.kv_cache.layers[li].install_bounded(cfg.window);
3442 n += 1;
3443 }
3444 }
3445 if n == 0 {
3446 return Err("anchor_core is present but no layer executes it".into());
3447 }
3448 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3449 self.bounded_rope = Some(std::sync::Arc::new(rope));
3450 self.anchor_core = Some(cfg.clone());
3451 self.embryo_graph = None;
3452 tracing::info!(
3453 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3454 cfg.kind,
3455 cfg.window,
3456 cfg.sink,
3457 self.kv_cache.layers.iter().map(|l| l.bounded_state_bytes()).max().unwrap_or(0)
3458 );
3459 Ok(())
3460 }
3461
3462 pub fn install_wire_identity(&mut self, identity: u64) {
3466 for li in 0..self.kv_cache.layers.len() {
3467 let pl = self.phys_layer(li);
3468 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3469 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3470 Some(AttnKind::Linear(_))
3471 | Some(AttnKind::LinearGdn(_))
3472 | Some(AttnKind::ShortConv(_))
3473 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3474 _ => crate::kv_cache::WireKind::Full,
3475 };
3476 let l = &mut self.kv_cache.layers[li];
3477 l.wire_kind = kind;
3478 l.wire_identity = identity;
3479 l.wire_layer = li as u32;
3482 }
3483 }
3484
3485 pub fn clear_history(&mut self) {
3487 self.kv_history.clear();
3488 self.kv_history_device = false;
3489 self.kv_prefix.clear();
3490 }
3491
3492 pub fn graph_refused(&self) -> bool {
3496 self.graph_refused
3497 .load(std::sync::atomic::Ordering::Relaxed)
3498 }
3499
3500 pub fn mark_graph_refused(&self) {
3502 if !self
3503 .graph_refused
3504 .swap(true, std::sync::atomic::Ordering::Relaxed)
3505 {
3506 tracing::info!(
3507 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3508 self.graph_kv_id
3509 );
3510 }
3511 }
3512
3513 pub fn device_sequence_position(&self) -> Option<usize> {
3517 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3518 }
3519
3520 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3525 let dev = self.device_sequence_position();
3526 if recorded_on_device {
3527 dev == Some(n) && self.embryo_resident_wanted()
3528 } else {
3529 dev.is_none()
3530 }
3531 }
3532
3533 pub(crate) fn invalidate_for_weight_change(&mut self) {
3541 self.clear_sequence_state();
3542 self.embryo_graph = None;
3543 }
3544
3545 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3551 let (n, on_device) = if self.bounded_native() {
3552 (self.kv_prefix.extension(input_ids), self.kv_prefix.on_device())
3553 } else {
3554 let h = &self.kv_history;
3555 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3556 (h.len(), self.kv_history_device)
3557 } else {
3558 (0, false)
3559 }
3560 };
3561 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3564 tracing::warn!(
3565 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3566 the device now holds {:?} — re-prefilling from zero",
3567 if on_device { "resident device" } else { "host" },
3568 self.device_sequence_position()
3569 );
3570 return 0;
3571 }
3572 n
3573 }
3574
3575 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3578 self.cached_prefix_len(input_ids)
3579 }
3580
3581 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3587 let on_device = self.device_sequence_position().is_some();
3588 if self.bounded_native() {
3589 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3590 let prev_device = self.kv_prefix.on_device();
3591 self.kv_history.clear();
3592 self.kv_history_device = false;
3593 if keep && prev_device == on_device {
3594 self.kv_prefix.extend(&consumed[reused..]);
3595 } else {
3596 self.kv_prefix.set(consumed);
3597 }
3598 self.kv_prefix.set_on_device(on_device);
3599 } else {
3600 self.kv_history = consumed.to_vec();
3601 self.kv_history_device = on_device;
3602 }
3603 }
3604
3605 pub fn o1_active(&self) -> bool {
3607 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3608 }
3609
3610 pub fn generation_batch_k(&self) -> usize {
3628 if let Some(k) = std::env::var("CMF_BATCH_K")
3629 .ok()
3630 .and_then(|v| v.parse::<usize>().ok())
3631 {
3632 return k;
3633 }
3634 if self.g4_batch_prefill() {
3637 return 128;
3638 }
3639 #[cfg(not(target_os = "macos"))]
3640 if self.graph_prefill_preferred() && !self.o1_active() {
3641 let moe = self
3644 .weights
3645 .layers
3646 .iter()
3647 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)));
3648 let q82 = crate::gpu::moe_q82_graph_on()
3654 && self.weights.layers.iter().any(|lw| {
3655 matches!(&lw.ffn, FfnKind::Moe(m)
3656 if m.experts.first().is_some_and(|e| e.gate_proj.mapped_q8_2f().is_some()))
3657 });
3658 return if q82 {
3659 128
3660 } else if moe {
3661 32
3662 } else {
3663 128
3664 };
3665 }
3666 0
3667 }
3668
3669 pub fn generation_graph_prefill(&self) -> bool {
3670 let graph = self.graph_prefill_preferred();
3671 #[cfg(not(target_os = "macos"))]
3682 if graph
3683 && self.generation_batch_k() > 0
3684 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3685 {
3686 return false;
3687 }
3688 graph
3689 }
3690
3691 pub fn o1_device_stats(&self) -> (usize, u64) {
3696 crate::gpu::o1_device_stats(self.graph_kv_id)
3697 }
3698
3699 pub fn o1_begin(&mut self) {
3704 self.o1_begin_with_prefix(None);
3705 }
3706
3707 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
3711 if let Some(c) = &self.o1_cfg {
3712 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
3713 let boundary = requested_prefix.map(|p| {
3714 p.max(
3715 crate::nystrom::o1_deferred_boundary(w, sink)
3716 .expect("o1 config boundary validated in set_o1"),
3717 )
3718 });
3719 for (li, &f) in self.o1_flags.iter().enumerate() {
3720 if f {
3721 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
3722 }
3723 }
3724 }
3725 }
3726
3727 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
3729 self.o1_cfg.as_ref().and_then(|c| {
3730 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
3731 .map(|floor| requested_prefix.max(floor))
3732 })
3733 }
3734
3735 fn o1_note_transition(&mut self) {
3736 let mut transitioned = false;
3740 for (li, &flagged) in self.o1_flags.iter().enumerate() {
3741 if flagged {
3742 transitioned |= self.kv_cache.layers[li].take_o1_transition();
3743 }
3744 }
3745 if transitioned {
3746 self.o1_epoch = self.o1_epoch.wrapping_add(1);
3747 }
3748 }
3749
3750 fn o1_pending(&self) -> bool {
3751 self.o1_flags.iter().enumerate().any(|(li, &f)| {
3752 f && self.kv_cache.layers[li].seq_len > 0
3753 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
3754 })
3755 }
3756
3757 fn o1_fail(&mut self, err: String) {
3758 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
3759 self.clear_sequence_state();
3760 self.graph_failed
3761 .store(true, std::sync::atomic::Ordering::Relaxed);
3762 self.cancel
3763 .store(true, std::sync::atomic::Ordering::Relaxed);
3764 }
3765
3766 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
3771 if self.o1_cfg.is_none() {
3772 return Ok(false);
3773 }
3774 let mut participating = false;
3775 for li in 0..self.num_layers {
3776 if !self.o1_flags.get(li).copied().unwrap_or(false) {
3777 continue;
3778 }
3779 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3780 return Err(err);
3781 }
3782 if self.kv_cache.layers[li].seq_len == 0 {
3783 continue;
3784 }
3785 participating = true;
3786 let num_heads = self.layer_num_heads(li);
3787 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
3788 }
3789 self.o1_note_transition();
3790 for li in 0..self.num_layers {
3791 if self.o1_flags.get(li).copied().unwrap_or(false) {
3792 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3793 return Err(err);
3794 }
3795 }
3796 }
3797 Ok(participating
3798 && (0..self.num_layers).all(|li| {
3799 !self.o1_flags.get(li).copied().unwrap_or(false)
3800 || self.kv_cache.layers[li].seq_len == 0
3801 || self.kv_cache.layers[li].o1_sealed()
3802 }))
3803 }
3804
3805 fn o1_progress(&mut self) {
3808 if !self.o1_active() {
3809 return;
3810 }
3811 for li in 0..self.num_layers {
3812 if self.o1_flags.get(li).copied().unwrap_or(false) {
3813 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3814 self.o1_fail(err);
3815 return;
3816 }
3817 }
3818 }
3819 self.o1_note_transition();
3823 if !self.o1_pending() {
3824 return;
3825 }
3826 if let Err(err) = self.o1_seal_checked() {
3827 self.o1_fail(err);
3828 }
3829 }
3830
3831 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
3836 if self
3837 .graph_failed
3838 .swap(false, std::sync::atomic::Ordering::Relaxed)
3839 {
3840 self.cancel
3841 .store(false, std::sync::atomic::Ordering::Relaxed);
3842 self.clear_sequence_state();
3843 return Err(format!("{phase}: deferred O(1) transition failed"));
3844 }
3845 Ok(())
3846 }
3847
3848 pub fn o1_seal(&mut self) {
3852 if let Err(err) = self.o1_seal_checked() {
3853 self.o1_fail(err);
3854 }
3855 }
3856
3857 pub fn set_trace(&mut self, on: bool) {
3859 self.trace = on;
3860 }
3861
3862 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
3865 self.rng = match config.seed {
3866 Some(seed) => SplitMix64::new(seed),
3867 None => SplitMix64::from_entropy(),
3868 };
3869 self.sampler_config = config;
3870 }
3871
3872 pub fn set_confidence(&mut self, on: bool) {
3877 self.confidence_on = on;
3878 }
3879
3880 pub fn set_calib_temp(&mut self, t: f32) {
3883 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
3884 }
3885
3886 pub fn calib_temp(&self) -> f32 {
3888 self.calib_temp
3889 }
3890
3891 pub fn rope_inv_freq(&self) -> &[f32] {
3895 self.inv_freq.as_slice()
3896 }
3897
3898 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
3901 self.rotary_dim = rotary_dim.min(self.head_dim);
3902 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
3903 self.embryo_graph = None;
3907 }
3908
3909 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
3910 QwenAttnCfg {
3911 num_heads: self.num_heads,
3912 num_kv_heads: self.num_kv_heads,
3913 head_dim: self.head_dim,
3914 hidden_size: self.hidden_size,
3915 position,
3916 inv_freq: &self.inv_freq,
3917 rotary_dim: self.rotary_dim,
3918 scale: self.attn_scale,
3919 softcap: self.attn_softcap,
3920 window: None,
3921 v_norm: false,
3922 qk_norm_after_rope: self.qk_norm_after_rope,
3923 gate_sigmoid: self.proj_gate_sigmoid,
3924 q_norm: None,
3925 k_norm: None,
3926 output_gate: false,
3927 softplus_gate: None,
3928 rope_scale: self.rope_scale,
3929 bias: None,
3930 rms_eps: self.rms_eps,
3931 norm_style: self.norm_style,
3932 pool: self.pool.as_deref(),
3933 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
3934 }
3935 }
3936
3937 pub fn generate(
3939 &mut self,
3940 prompt: &str,
3941 max_tokens: usize,
3942 task_mask: Option<&TaskMask>,
3943 on_token: Option<TokenCallback>,
3944 ) -> Result<GenerateResult, String> {
3945 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
3946 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
3947 }
3948
3949 pub fn generate_from_vl(
3952 &mut self,
3953 input: &crate::dsv41_vision::PreparedVlInputs,
3954 max_tokens: usize,
3955 task_mask: Option<&TaskMask>,
3956 on_token: Option<TokenCallback>,
3957 ) -> Result<GenerateResult, String> {
3958 let Some(dsv41) = &self.dsv41 else {
3959 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
3960 };
3961 if input.token_ids.is_empty() {
3962 return Err("empty V4.1 multimodal prompt".into());
3963 }
3964 if input.token_types.len() != input.token_ids.len() {
3965 return Err(format!(
3966 "V4.1 token type count {} != token count {}",
3967 input.token_types.len(),
3968 input.token_ids.len()
3969 ));
3970 }
3971 let dim = dsv41.2.dim;
3972 let mut embeddings = vec![None; input.token_ids.len()];
3973 let mut participates = vec![true; input.token_ids.len()];
3974 if !input.images.is_empty() {
3975 let vision = self
3976 .dsv41_vision
3977 .as_ref()
3978 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
3979 for image in &input.images {
3980 let end = image.start.saturating_add(image.types.len());
3981 if end > input.token_ids.len() {
3982 return Err(format!(
3983 "V4.1 image span {}..{} exceeds prompt length {}",
3984 image.start,
3985 end,
3986 input.token_ids.len()
3987 ));
3988 }
3989 let mut span = vec![0.0f32; image.types.len() * dim];
3990 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
3991 for (offset, &kind) in image.types.iter().enumerate() {
3992 let pos = image.start + offset;
3993 if input.token_types[pos] != kind {
3994 return Err(format!(
3995 "V4.1 image type mismatch at position {pos}: {} != {kind}",
3996 input.token_types[pos]
3997 ));
3998 }
3999 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
4000 participates[pos] = false;
4001 }
4002 }
4003 }
4004 for (pos, &kind) in input.token_types.iter().enumerate() {
4005 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
4006 return Err(format!("V4.1 text position {pos} has an image embedding"));
4007 }
4008 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
4009 return Err(format!("V4.1 image position {pos} has no image embedding"));
4010 }
4011 }
4012 self.dsv41_prefill = Some((embeddings, participates));
4013 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
4014 self.dsv41_prefill = None;
4015 result
4016 }
4017
4018 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
4020 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
4021 }
4022
4023 pub fn generate_from_ids(
4031 &mut self,
4032 input_ids: &[u32],
4033 max_tokens: usize,
4034 task_mask: Option<&TaskMask>,
4035 on_token: Option<TokenCallback>,
4036 ) -> Result<GenerateResult, String> {
4037 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
4038 }
4039
4040 pub fn generate_from_embeds(
4046 &mut self,
4047 input_ids: &[u32],
4048 prompt_rows: &[f32],
4049 max_tokens: usize,
4050 task_mask: Option<&TaskMask>,
4051 on_token: Option<TokenCallback>,
4052 ) -> Result<GenerateResult, String> {
4053 if input_ids.is_empty()
4054 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
4055 {
4056 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
4057 }
4058 if prompt_rows.iter().any(|x| !x.is_finite()) {
4059 return Err("embedded prompt contains non-finite values".into());
4060 }
4061 if !self.can_prefill_batched() || self.dyn_router.is_some()
4062 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
4063 {
4064 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
4065 }
4066 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
4067 }
4068
4069 fn generate_with_prompt_rows(
4070 &mut self,
4071 input_ids: &[u32],
4072 prompt_rows: Option<&[f32]>,
4073 max_tokens: usize,
4074 task_mask: Option<&TaskMask>,
4075 mut on_token: Option<TokenCallback>,
4076 ) -> Result<GenerateResult, String> {
4077 #[cfg(target_os = "macos")]
4078 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
4079 if std::env::var("CMF_TRACE_H").is_ok() {
4080 eprintln!("input_ids: {input_ids:?}");
4081 }
4082 if input_ids.is_empty() {
4083 return Err("empty prompt: nothing to generate from".to_string());
4084 }
4085 self.graph_failed
4089 .store(false, std::sync::atomic::Ordering::Relaxed);
4090 let task_mask = self.drop_open_mask(task_mask);
4095
4096 let mut reuse_from = {
4104 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
4105 if on
4106 && prompt_rows.is_none()
4107 && task_mask.is_none()
4108 && self.mtp.is_none()
4109 && !(self.mimo_mtp.is_some() && self.speculative)
4110 && self.o1_cfg.is_none()
4111 && self.dsv41.is_none()
4112 {
4113 self.cached_prefix_len(input_ids)
4114 } else {
4115 0
4116 }
4117 };
4118 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
4121 reuse_from = 0;
4122 }
4123 self.last_prefill_tokens = input_ids.len() - reuse_from;
4124 let bounded_native = self.bounded_native();
4125 if reuse_from == 0 {
4126 self.clear_sequence_state();
4128 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
4129 eprintln!(
4130 "kv-reuse: {} of {} prompt positions already cached",
4131 reuse_from,
4132 input_ids.len()
4133 );
4134 }
4135 crate::gpu::graph_race_begin_generation();
4136 let o1_prefill = if self.o1_active() && task_mask.is_none() {
4140 std::env::var("CMF_O1_PREFILL")
4141 .ok()
4142 .and_then(|v| v.parse::<usize>().ok())
4143 .filter(|&p| p > 0)
4144 } else {
4145 None
4146 };
4147 if task_mask.is_none() {
4148 self.o1_begin_with_prefix(o1_prefill);
4149 }
4150
4151 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
4157 #[cfg(target_os = "macos")]
4196 let metal_graph = crate::gpu::q1_force()
4197 && crate::gpu::enabled_here()
4198 && std::env::var("CMF_GPU_BLOCK")
4199 .map(|v| v != "0")
4200 .unwrap_or(true);
4201 #[cfg(not(target_os = "macos"))]
4202 let metal_graph = false;
4203 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
4204 let spec_cheap_round = self.sampler_config.temperature < 1e-6
4208 || sampler::sparse_ok(&self.sampler_config);
4209 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4210 || match spec_sample_env.as_deref() {
4211 Some("1") => true,
4212 Some(_) => false,
4213 None => metal_graph && spec_cheap_round,
4214 };
4215 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4231 for lw in &self.weights.layers {
4232 if let FfnKind::Dense(d) = &lw.ffn {
4233 dense_n += 1;
4234 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4235 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4236 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4237 {
4238 dense_q4tp += 1;
4239 }
4240 }
4241 }
4242 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4243 let penalized = !metal_graph
4255 && (self.sampler_config.repetition_penalty != 1.0
4256 || self.sampler_config.presence_penalty != 0.0
4257 || !self.sampler_config.suppress_tokens.is_empty());
4258 #[cfg(feature = "gpu")]
4263 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4264 #[cfg(not(feature = "gpu"))]
4265 let metal_wgpu = false;
4266 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4267 let spec_wanted = match spec_env.as_deref() {
4268 Some("0") => false,
4269 Some(_) => {
4270 if metal_wgpu {
4271 tracing::warn!(
4272 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4273 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4274 );
4275 }
4276 true
4277 }
4278 None => spec_default_ok && !penalized && !metal_wgpu,
4279 };
4280 let graph_spec = self.speculative
4284 && (graph_on || metal_graph)
4285 && self.mtp.is_some()
4286 && task_mask.is_none()
4287 && !self.o1_active()
4288 && spec_sampling_ok
4289 && spec_wanted;
4290 #[cfg(target_os = "macos")]
4294 if metal_graph {
4295 static SAID: std::sync::Once = std::sync::Once::new();
4296 SAID.call_once(|| {
4297 let spec = if graph_spec {
4298 let k = std::env::var("CMF_GRAPH_SPEC_K")
4299 .ok()
4300 .and_then(|v| v.parse::<usize>().ok())
4301 .filter(|&v| (1..=8).contains(&v))
4302 .unwrap_or(7);
4303 let arm = if self.sampler_config.temperature < 1e-6 {
4304 "greedy"
4305 } else {
4306 "sampling"
4307 };
4308 format!(
4309 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4310 Self::draft_vocab_rows(usize::MAX)
4311 )
4312 } else if !self.speculative {
4313 "spec off (CMF_MTP=0)".to_string()
4314 } else if self.mtp.is_none() {
4315 "spec off (no MTP head)".to_string()
4316 } else if !spec_sampling_ok {
4317 if spec_cheap_round {
4318 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4319 } else {
4320 "spec off (sampling without a top-k: the dense chain \
4321 costs more than it saves)"
4322 .to_string()
4323 }
4324 } else if !spec_wanted {
4325 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4326 } else if task_mask.is_some() {
4327 "spec off (task mask)".to_string()
4328 } else {
4329 "spec off (O(1) attention)".to_string()
4330 };
4331 let on = |var: &str| {
4332 if std::env::var(var).as_deref() == Ok("0") {
4333 "off"
4334 } else {
4335 "on"
4336 }
4337 };
4338 tracing::info!(
4339 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4340 MTP graph {}, attend {}, probe {}",
4341 if crate::gpu_metal::state4_on() { "on" } else { "off" },
4342 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
4343 on("CMF_METAL_PREFILL"),
4344 on("CMF_MTP_GRAPH"),
4345 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4346 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
4347 );
4348 });
4349 }
4350 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4357 let spec_active = self.speculative
4358 && self.mtp.is_some()
4359 && task_mask.is_none()
4360 && !self.o1_active()
4361 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4362 let mut mtp = if spec_active { self.mtp.take() } else { None };
4365 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4366 eprintln!(
4367 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4368 mtp.is_some(),
4369 self.speculative,
4370 self.sampler_config.temperature < 1e-6,
4371 );
4372 }
4373 if let Some(m) = &mut mtp {
4374 m.kv.clear();
4375 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4377 self.mtp_graph_mode = None;
4378 }
4379 let mimo_spec = self.speculative
4383 && self.mimo_mtp.is_some()
4384 && task_mask.is_none()
4385 && !self.o1_active()
4386 && self.dyn_router.is_none()
4387 && self.sampler_config.temperature < 1e-6
4388 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4389 if let Some(st) = self.mimo_mtp.as_mut() {
4390 st.reset();
4391 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4392 Self::mimo_mtp_hist_cap(st, input_ids.len());
4393 }
4394 }
4395 let mut router = if mtp.is_none() {
4399 self.dyn_router.take()
4400 } else {
4401 None
4402 };
4403 let mut reuse_from = reuse_from;
4404 if let Some(r) = &mut router {
4405 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4408 let _ = self.set_active_skill(None);
4411 reuse_from = 0;
4412 self.last_prefill_tokens = input_ids.len();
4413 }
4414 }
4415
4416 let mut all_ids = input_ids.to_vec();
4417 let mut generated = 0usize;
4418 let mut finish_reason = "max_tokens".to_string();
4419 let mut drafted = 0usize;
4420 let mut accepted = 0usize;
4421 let mut dsv4_spec_bad = 0usize;
4428 let mut dsv4_spec_retry_at = 0usize;
4429 let mut confidence: Vec<f32> = Vec::new();
4430 let trace_on = self.trace;
4431 let calib_temp = self.calib_temp;
4432 let mut traces: Vec<TokenTrace> = Vec::new();
4433
4434 let mut hidden = vec![0.0f32; self.hidden_size];
4440 let mut pos = reuse_from;
4441 let fuse_lm = mtp.is_none()
4450 && router.is_none()
4451 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4452 self.graph_logits = None;
4453 self.graph_want_logits = false;
4454 let _tpf = std::time::Instant::now();
4455 let batch_k = self.generation_batch_k();
4456 if let Some(rows) = prompt_rows {
4457 let hs = self.hidden_size;
4458 let chunk = self.prefill_chunk().max(1);
4459 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4460 let end = (pos + chunk).min(input_ids.len());
4461 let hb = match self.prefill_input_rows(
4462 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
4463 ) {
4464 Ok(hb) => hb,
4465 Err(err) => {
4466 self.finish_generation(&mut mtp, &mut router, true);
4467 return Err(err);
4468 }
4469 };
4470 if mimo_spec { self.mimo_note_rows(&hb, pos); }
4471 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4472 pos = end;
4473 }
4474 }
4475 while self.qwen4_exp.is_some()
4486 && mtp.is_none()
4487 && pos < input_ids.len()
4488 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4489 {
4490 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4493 let want_logits = end == input_ids.len();
4494 let mut lg = Vec::new();
4495 if let Some(b) = &mut self.qwen4_exp {
4496 crate::qwen4_exp::forward_tokens(
4497 &b.0,
4498 &b.1,
4499 &b.2,
4500 &mut b.3,
4501 &input_ids[pos..end],
4502 pos,
4503 &self.inv_freq,
4504 self.pool.as_deref(),
4505 &mut lg,
4506 want_logits,
4507 );
4508 }
4509 if want_logits {
4510 self.graph_logits = Some(lg);
4511 }
4512 pos = end;
4513 hidden.fill(0.0);
4514 }
4515 while self.dsv4.is_some()
4516 && mtp.is_none()
4517 && pos < input_ids.len()
4518 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4519 {
4520 let end = (pos + prefill_chunk()).min(input_ids.len());
4521 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4522 let mut lg = Vec::new();
4523 if let Some(b) = &mut self.dsv4 {
4524 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4525 crate::dsv4::forward_chunk(
4526 g,
4527 layers,
4528 &cfg,
4529 st,
4530 &ids,
4531 pos,
4532 &self.inv_freq,
4533 self.pool.as_deref(),
4534 &mut lg,
4535 end == input_ids.len(),
4536 );
4537 }
4538 if end == input_ids.len() {
4539 self.graph_logits = Some(lg);
4540 }
4541 pos = end;
4542 hidden = vec![0.0; self.hidden_size];
4543 }
4544 let dsv41_prefill = self.dsv41_prefill.take();
4545 while self.dsv41.is_some()
4546 && mtp.is_none()
4547 && pos < input_ids.len()
4548 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4549 {
4550 let end = (pos + prefill_chunk()).min(input_ids.len());
4551 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4552 let mut lg = Vec::new();
4553 if let Some(b) = &mut self.dsv41 {
4554 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4555 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4556 crate::dsv41::forward_chunk_masked_with_embeddings(
4557 g,
4558 layers,
4559 cfg,
4560 st,
4561 &ids,
4562 pos,
4563 &embeddings[pos..end],
4564 &participates[pos..end],
4565 self.pool.as_deref(),
4566 &mut lg,
4567 );
4568 } else {
4569 crate::dsv41::forward_chunk(
4570 g,
4571 layers,
4572 cfg,
4573 st,
4574 &ids,
4575 pos,
4576 self.pool.as_deref(),
4577 &mut lg,
4578 );
4579 }
4580 }
4581 if end == input_ids.len() {
4582 self.graph_logits = Some(lg);
4583 }
4584 pos = end;
4585 hidden = vec![0.0; self.hidden_size];
4586 }
4587 let dyn_prefill = router.is_some();
4592 let o1_prefill_limit = o1_prefill
4600 .and_then(|requested| self.o1_effective_boundary(requested))
4601 .map(|boundary| boundary.min(input_ids.len()));
4602 let mut o1_sealed = false;
4603 if let Some(limit) = o1_prefill_limit {
4604 if self.can_prefill_batched() && limit > 2 {
4607 let chunk = self.prefill_chunk();
4608 let hs = self.hidden_size;
4609 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4610 let end = (pos + chunk).min(limit);
4611 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4612 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4613 pos = end;
4614 }
4615 } else {
4616 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4617 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4618 pos += 1;
4619 }
4620 }
4621 if pos >= limit {
4622 o1_sealed = match self.o1_seal_checked() {
4623 Ok(sealed) => sealed,
4624 Err(err) => {
4625 self.finish_generation(&mut mtp, &mut router, true);
4626 return Err(err);
4627 }
4628 };
4629 tracing::info!(
4630 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4631 o1_prefill.unwrap_or(0),
4632 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4633 .unwrap_or(limit),
4634 limit,
4635 input_ids.len()
4636 );
4637 }
4638 }
4639 let graph_prefill = self.graph_prefill_preferred();
4645 #[cfg(target_os = "macos")]
4653 if task_mask.is_none()
4654 && !dyn_prefill
4655 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4656 && crate::gpu::enabled_here()
4657 && self.gdn_cfg.is_some()
4658 && self.g3n.is_none()
4659 && input_ids.len() > 8
4660 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4661 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4662 {
4663 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4664 .ok()
4665 .and_then(|v| v.parse().ok())
4666 .filter(|&v| (16..=512).contains(&v))
4667 .unwrap_or(256);
4668 let hs = self.hidden_size;
4669 let _tp = std::time::Instant::now();
4670 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4671 let end = (pos + chunk).min(input_ids.len());
4672 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4673 MetalPrefillOutcome::Completed(hb) => hb,
4674 MetalPrefillOutcome::Declined => break,
4675 MetalPrefillOutcome::Failed => {
4676 self.finish_generation(&mut mtp, &mut router, true);
4677 return Err("ordinary Metal prefill failed after admission".into());
4678 }
4679 };
4680 if let Some(m) = &mut mtp {
4681 let n_pairs = if end < input_ids.len() {
4682 end - pos
4683 } else {
4684 end - pos - 1
4685 };
4686 if n_pairs > 0 {
4687 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
4688 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
4689 .collect();
4690 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
4691 for (j, (h, t)) in pairs.iter().enumerate() {
4692 let h = h.to_vec();
4693 let _ = self.mtp_step(m, &h, *t, pos + j);
4694 }
4695 }
4696 }
4697 }
4698 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4699 pos = end;
4700 }
4701 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4702 eprintln!(
4703 "metal-prefill: {} of {} tokens in {:.1} ms",
4704 pos,
4705 input_ids.len(),
4706 _tp.elapsed().as_secs_f64() * 1e3
4707 );
4708 }
4709 }
4710 #[cfg(not(target_os = "macos"))]
4715 if task_mask.is_none()
4716 && !dyn_prefill
4717 && mtp.is_none()
4718 && o1_prefill.is_none()
4719 && batch_k > 0
4720 && pos + 1 < input_ids.len()
4721 && self.g4_batch_prefill()
4722 {
4723 let t_g4 = std::time::Instant::now();
4724 match self.g4_batch_ingest(&input_ids[pos..], pos, false) {
4725 Ok((n, last)) => {
4726 if n > 0 {
4727 hidden.copy_from_slice(&last);
4728 }
4729 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4730 eprintln!(
4731 "gemma4 batch prefill: {n} of {} tokens in {:.1} ms",
4732 input_ids.len() - pos,
4733 t_g4.elapsed().as_secs_f64() * 1e3
4734 );
4735 }
4736 pos += n;
4737 }
4738 Err(e) => {
4739 self.finish_generation(&mut mtp, &mut router, true);
4740 return Err(e);
4741 }
4742 }
4743 }
4744 self.mimo_moe_prepare();
4745 #[cfg(not(target_os = "macos"))]
4751 if task_mask.is_none()
4752 && !dyn_prefill
4753 && !graph_prefill
4754 && mtp.is_none()
4755 && o1_prefill.is_none()
4756 && !self.o1_active()
4757 && input_ids.len() > 2
4758 && self.batch_prefix_prefill()
4759 {
4760 let chunk = self.prefill_chunk().max(1);
4761 let hs = self.hidden_size;
4762 let t_bp = std::time::Instant::now();
4763 let pos0 = pos;
4764 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4765 let end = (pos + chunk).min(input_ids.len());
4766 let bk = end - pos;
4767 let mut hiddens = vec![0f32; bk * hs];
4768 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4769 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4770 }
4771 let positions: Vec<usize> = (pos..end).collect();
4772 let mut run = 0usize;
4773 let outcome = self.try_batch_graph_wgpu_prefix(
4774 &mut hiddens,
4775 &positions,
4776 bk,
4777 None,
4778 Some(&mut run),
4779 );
4780 match outcome {
4781 crate::gpu::BatchGraphOutcome::Completed => {
4782 let hb = if run < self.num_layers {
4783 self.prefill_batch_span(
4784 PrefillIn::Hidden(&hiddens),
4785 pos,
4786 None,
4787 run,
4788 self.num_layers,
4789 )
4790 } else {
4791 hiddens
4792 };
4793 if mimo_spec {
4794 self.mimo_note_rows(&hb, pos);
4795 }
4796 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
4797 pos = end;
4798 }
4799 crate::gpu::BatchGraphOutcome::Failed => {
4800 self.finish_generation(&mut mtp, &mut router, true);
4801 return Err("batched prefix prefill failed after admission".into());
4802 }
4803 crate::gpu::BatchGraphOutcome::Declined => {
4804 #[cfg(feature = "gpu")]
4807 if pos > pos0 {
4808 self.pull_lagging_host_kv(0, self.num_layers, pos);
4809 }
4810 break;
4811 }
4812 }
4813 }
4814 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4815 eprintln!(
4816 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
4817 pos - pos0,
4818 input_ids.len(),
4819 t_bp.elapsed().as_secs_f64() * 1e3
4820 );
4821 }
4822 }
4823 if task_mask.is_none()
4824 && !dyn_prefill
4825 && !graph_prefill
4826 && self.can_prefill_batched()
4827 && self.g3n.is_none()
4828 && o1_prefill.is_none()
4829 && input_ids.len() > 2
4830 {
4831 let chunk = self.prefill_chunk();
4837 let hs = self.hidden_size;
4838 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4839 let end = (pos + chunk).min(input_ids.len());
4840 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4841 if mimo_spec {
4842 self.mimo_note_rows(&hb, pos);
4843 }
4844 if let Some(m) = &mut mtp {
4845 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4846 .ok()
4847 .and_then(|v| v.parse().ok())
4848 .unwrap_or(0);
4849 for p in pos..end {
4850 if p + 1 < input_ids.len() {
4851 if probe >= 1 && p + 2 < input_ids.len() {
4852 let (d1, mut hx) = self.mtp_step_h(
4856 m,
4857 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4858 input_ids[p + 1],
4859 p,
4860 );
4861 let mut ok = d1 == input_ids[p + 2];
4862 Self::chain_probe_note(0, ok);
4863 let mut d_prev = d1;
4864 let mut extra = 0usize;
4865 for j in 1..probe {
4866 if p + 2 + j >= input_ids.len() {
4867 break;
4868 }
4869 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
4870 extra += 1;
4871 ok = ok && dj == input_ids[p + 2 + j];
4872 Self::chain_probe_note(j, ok);
4873 d_prev = dj;
4874 hx = hj;
4875 }
4876 m.kv.truncate_last(extra);
4877 } else {
4878 let _ = self.mtp_step(
4879 m,
4880 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4881 input_ids[p + 1],
4882 p,
4883 );
4884 }
4885 }
4886 }
4887 }
4888 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4889 pos = end;
4890 }
4891 }
4892 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
4893 if task_mask.is_none()
4894 && !dyn_prefill
4895 && !graph_prefill
4896 && !pair_off
4897 && self.pair_supported()
4898 && o1_prefill.is_none()
4899 {
4900 while pos + 1 < input_ids.len()
4901 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4902 {
4903 let e1 = self.embed_single(input_ids[pos]);
4904 let e2 = self.embed_single(input_ids[pos + 1]);
4905 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
4906 if mimo_spec {
4907 self.mimo_note_rows(&h1, pos);
4908 self.mimo_note_rows(&h2, pos + 1);
4909 }
4910 self.commit_linear_scratch();
4912 if let Some(m) = &mut mtp {
4913 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
4914 if pos + 2 < input_ids.len() {
4915 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4916 .ok()
4917 .and_then(|v| v.parse().ok())
4918 .unwrap_or(0);
4919 if probe >= 1 && pos + 3 < input_ids.len() {
4920 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
4924 let mut ok = d1 == input_ids[pos + 3];
4925 Self::chain_probe_note(0, ok);
4926 let mut d_prev = d1;
4927 let mut extra = 0usize;
4928 for j in 1..probe {
4929 if pos + 3 + j >= input_ids.len() {
4930 break;
4931 }
4932 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
4933 extra += 1;
4934 ok = ok && dj == input_ids[pos + 3 + j];
4935 Self::chain_probe_note(j, ok);
4936 d_prev = dj;
4937 hx = hj;
4938 }
4939 m.kv.truncate_last(extra);
4940 } else {
4941 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
4942 }
4943 }
4944 }
4945 hidden = h2;
4946 pos += 2;
4947 }
4948 }
4949 let o1_batch_ready = o1_sealed
4962 && o1_prefill.is_some()
4963 && mtp.is_none()
4964 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4965 && (0..self.num_layers).all(|li| {
4966 let cache = &self.kv_cache.layers[self.phys_layer(li)];
4967 cache.o1.is_none() || cache.o1_views().is_some()
4968 });
4969 let mtp_batch_prefill = mtp.is_some()
4974 && graph_prefill
4975 && task_mask.is_none()
4976 && !dyn_prefill
4977 && !self.o1_active()
4978 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
4979 if batch_k > 0
4980 && (graph_prefill || o1_batch_ready)
4981 && task_mask.is_none()
4982 && (!self.o1_active() || o1_batch_ready)
4983 && (mtp.is_none() || mtp_batch_prefill)
4984 && !dyn_prefill
4985 && pos + 1 < input_ids.len()
4986 {
4987 let hs = self.hidden_size;
4988 let chunk = batch_k;
4989 while pos < input_ids.len() {
4990 let end = (pos + chunk).min(input_ids.len());
4991 let bk = end - pos;
4992 let mut hiddens = vec![0f32; bk * hs];
4993 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4994 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4995 }
4996 let positions: Vec<usize> = (pos..end).collect();
4997 let t_chunk = std::time::Instant::now();
4998 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
4999 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
5000 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5001 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
5002 eprintln!(
5003 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
5004 if o1_batch_ready {
5005 "o1"
5006 } else if mtp_batch_prefill {
5007 "ordinary_mtp"
5008 } else {
5009 "ordinary"
5010 },
5011 bk as f64 / (ms / 1000.0)
5012 );
5013 }
5014 {
5015 use std::sync::atomic::{AtomicBool, Ordering};
5016 static SAID: AtomicBool = AtomicBool::new(false);
5017 if !SAID.swap(true, Ordering::Relaxed) {
5018 if ok_b {
5019 tracing::info!(
5020 "batched prefill: ACTIVE mode={} (k={bk})",
5021 if o1_batch_ready {
5022 "o1"
5023 } else if mtp_batch_prefill {
5024 "ordinary_mtp"
5025 } else {
5026 "ordinary"
5027 }
5028 );
5029 } else {
5030 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
5031 }
5032 }
5033 }
5034 if ok_b {
5035 if mimo_spec {
5036 self.mimo_note_rows(&hiddens, pos);
5037 }
5038 if mtp_batch_prefill {
5039 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
5040 if n_pairs > 0 {
5041 let rows: Vec<Vec<f32>> = (0..n_pairs)
5047 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
5048 .collect();
5049 let pairs: Vec<(&[f32], u32)> = rows
5050 .iter()
5051 .enumerate()
5052 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
5053 .collect();
5054 if std::env::var("CMF_GRAPH_PROF").is_ok() {
5055 eprintln!(
5056 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
5057 pos,
5058 n_pairs,
5059 pos + n_pairs - 1,
5060 );
5061 }
5062 let warm_error = if let Some(m) = mtp.as_mut() {
5063 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
5064 } else {
5065 None
5066 };
5067 if let Some(err) = warm_error {
5068 self.finish_generation(&mut mtp, &mut router, true);
5073 return Err(err.to_string());
5074 }
5075 }
5076 }
5077 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
5078 pos = end;
5079 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
5080 self.finish_generation(&mut mtp, &mut router, true);
5085 return Err(if o1_batch_ready {
5086 "sealed O(1) batch graph failed after admission".to_string()
5087 } else {
5088 "ordinary recurrent batch graph failed after admission".to_string()
5089 });
5090 } else {
5091 break; }
5093 }
5094 }
5095 if graph_prefill
5099 && task_mask.is_none()
5100 && mtp.is_none()
5101 && !dyn_prefill
5102 && pos == 0
5103 && input_ids.len() > 1
5104 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
5105 {
5106 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
5107 self.graph_logits = Some(lg);
5108 hidden = vec![0.0; self.hidden_size];
5109 pos = input_ids.len();
5110 }
5111 }
5112 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
5113 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
5114 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
5115 if mimo_spec {
5116 self.mimo_note_rows(&hidden, pos);
5117 }
5118 if let Some(m) = &mut mtp {
5119 if pos + 1 < input_ids.len() {
5120 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
5126 .ok()
5127 .and_then(|v| v.parse().ok())
5128 .unwrap_or(0);
5129 if probe >= 1 && pos + 2 < input_ids.len() {
5130 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
5131 let mut ok = d1 == input_ids[pos + 2];
5132 Self::chain_probe_note(0, ok);
5133 let mut d_prev = d1;
5134 let mut extra = 0usize;
5135 for j in 1..probe {
5136 if pos + 2 + j >= input_ids.len() {
5137 break;
5138 }
5139 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
5140 extra += 1;
5141 ok = ok && dj == input_ids[pos + 2 + j];
5142 Self::chain_probe_note(j, ok);
5143 d_prev = dj;
5144 hx = hj;
5145 }
5146 m.kv.truncate_last(extra);
5149 } else {
5150 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
5151 }
5152 }
5153 }
5154 pos += 1;
5155 }
5156 if std::env::var("CMF_PREFILL_PROF").is_ok() {
5157 eprintln!(
5158 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
5159 input_ids.len(),
5160 _tpf.elapsed().as_secs_f64() * 1000.0
5161 );
5162 }
5163 if self
5164 .graph_failed
5165 .swap(false, std::sync::atomic::Ordering::Relaxed)
5166 {
5167 self.finish_generation(&mut mtp, &mut router, true);
5172 return Err("GPU token graph failed during prefill".to_string());
5173 }
5174 if self
5177 .cancel
5178 .swap(false, std::sync::atomic::Ordering::Relaxed)
5179 {
5180 self.finish_generation(&mut mtp, &mut router, true);
5184 return Ok(GenerateResult {
5185 text: String::new(),
5186 token_ids: Vec::new(),
5187 prompt_tokens: input_ids.len(),
5188 tokens_generated: 0,
5189 finish_reason: "cancelled".to_string(),
5190 mtp_drafted: 0,
5191 mtp_accepted: 0,
5192 token_confidence: Vec::new(),
5193 traces: Vec::new(),
5194 });
5195 }
5196
5197 if !o1_sealed {
5200 match self.o1_seal_checked() {
5201 Ok(_) => {}
5202 Err(err) => {
5203 self.finish_generation(&mut mtp, &mut router, true);
5204 return Err(err);
5205 }
5206 }
5207 }
5208
5209 macro_rules! commit {
5211 ($id:expr) => {{
5212 all_ids.push($id);
5213 generated += 1;
5214 self.note_draft_id($id);
5215 if self.tokenizer.is_eos($id) && !self.ignore_eos {
5216 finish_reason = "stop".to_string();
5217 false
5218 } else {
5219 let token_text = self.tokenizer.decode_token($id);
5220 let mut go = true;
5221 if let Some(ref mut cb) = on_token {
5222 if !cb(&token_text) {
5223 finish_reason = "cancelled".to_string();
5224 go = false;
5225 }
5226 }
5227 go
5228 }
5229 }};
5230 }
5231
5232 let mut spec_trial = SpecTrial::Spec {
5243 t0: std::time::Instant::now(),
5244 gen0: generated,
5245 rounds: 0,
5246 };
5247 let mut spec_mon = SpecMon {
5253 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5254 ..SpecMon::default()
5255 };
5256 let mut spec_watchdog_off = false;
5257 let mut spec_walls: Vec<f32> = Vec::new();
5260 let mut spec_round_end: Option<std::time::Instant> = None;
5263 if mimo_spec {
5264 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5265 if let Some(mut st) = self.mimo_mtp.take() {
5266 self.mimo_mtp_probe(&mut st, input_ids, &path);
5267 self.mimo_mtp = Some(st);
5268 }
5269 }
5270 }
5271 let mut next_pos = input_ids.len();
5273 'decode: while generated < max_tokens {
5274 if self
5275 .graph_failed
5276 .swap(false, std::sync::atomic::Ordering::Relaxed)
5277 {
5278 self.finish_generation(&mut mtp, &mut router, true);
5283 return Err("GPU token graph failed during decode".to_string());
5284 }
5285 if self
5286 .cancel
5287 .swap(false, std::sync::atomic::Ordering::Relaxed)
5288 {
5289 finish_reason = "cancelled".to_string();
5290 break 'decode;
5291 }
5292 if mimo_spec && next_pos > 0 {
5297 self.mimo_note_rows(&hidden, next_pos - 1);
5300 }
5301 let forced = self.spec_forced.take();
5302 let mut logits = match (forced, self.graph_logits.take()) {
5303 (Some(_), _) => Vec::new(),
5304 (None, Some(lg)) => lg,
5305 (None, None) => {
5306 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5307 inference::rms_norm_into(
5308 &hidden,
5309 &self.weights.final_norm,
5310 self.rms_eps,
5311 self.norm_style,
5312 &mut self.ws.n1,
5313 );
5314 self.lm_head_forward(&self.ws.n1)
5315 }
5316 };
5317 if generated
5320 == std::env::var("CMF_LOGIT_DUMP_STEP")
5321 .ok()
5322 .and_then(|v| v.parse().ok())
5323 .unwrap_or(0)
5324 {
5325 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5326 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5327 for v in hidden.iter().chain(logits.iter()) {
5328 bytes.extend_from_slice(&v.to_le_bytes());
5329 }
5330 if let Err(e) = std::fs::write(&path, &bytes) {
5331 eprintln!("logit dump: failed to write {path}: {e}");
5332 self.finish_generation(&mut mtp, &mut router, true);
5333 return Err(format!("logit dump write failed: {e}"));
5334 }
5335 }
5336 }
5337 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5341 if !logits.is_empty() {
5342 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5343 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5344 if let Err(e) =
5345 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5346 {
5347 eprintln!("logit dump: failed to write {}: {e}", path.display());
5348 }
5349 }
5350 }
5351 let t_next = match forced.or_else(|| forced_ids_at(generated)) {
5352 Some(c) => c,
5353 None => {
5354 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5355 sampler::sample_with_scratch_pool(
5356 &logits,
5357 &self.sampler_config,
5358 self.sampler_config.penalty_past(&all_ids, bounded_native),
5359 &mut self.rng,
5360 &mut self.sampler_scratch,
5361 self.pool.as_deref(),
5362 )
5363 }
5364 };
5365 if self.confidence_on {
5366 confidence.push(if logits.is_empty() {
5367 0.0
5368 } else {
5369 sampler::top1_prob_pool(
5370 self.pool.as_deref(),
5371 &mut self.sampler_scratch,
5372 &logits,
5373 t_next,
5374 calib_temp,
5375 )
5376 });
5377 }
5378 if !logits.is_empty() {
5379 attention::recycle_buf(&mut logits);
5380 }
5381 if trace_on {
5382 let skill = router.as_ref().and_then(|r| r.active_id());
5386 traces.push(TokenTrace {
5387 t: generated,
5388 token_id: t_next,
5389 confidence: confidence.last().copied().unwrap_or(0.0),
5390 active_skill: skill,
5391 recon: None,
5392 switched: false,
5393 });
5394 }
5395 if !commit!(t_next) {
5396 break 'decode;
5397 }
5398 if generated >= max_tokens {
5399 break 'decode;
5400 }
5401
5402 self.swa_trim_tails();
5405 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5406 static SAID: std::sync::Once = std::sync::Once::new();
5412 SAID.call_once(|| {
5413 tracing::warn!(
5414 "KV cache full at {} positions — evicting half; quality \
5415 will degrade. Raise CMF_MAX_SEQ.",
5416 self.kv_cache.max_seq_len,
5417 );
5418 });
5419 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5420 self.kv_cache.evict(keep);
5421 }
5422
5423 if graph_spec {
5426 match spec_trial {
5427 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5428 spec_mon.plain_ms =
5429 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5430 let keep = spec_mon.pays();
5431 tracing::info!(
5432 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5433 spec_mon.tokens,
5434 spec_mon.round_ms,
5435 spec_mon.plain_ms,
5436 if keep { "speculating" } else { "plain" }
5437 );
5438 spec_mon.fails = 0;
5439 spec_trial = SpecTrial::Decided {
5440 spec: keep,
5441 recheck_at: if keep { usize::MAX } else { generated + 128 },
5442 };
5443 }
5444 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5445 spec_mon.n = 0;
5446 spec_trial = SpecTrial::Spec {
5447 t0: std::time::Instant::now(),
5448 gen0: generated,
5449 rounds: 0,
5450 };
5451 }
5452 _ => {}
5453 }
5454 spec_watchdog_off = matches!(
5455 spec_trial,
5456 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5457 );
5458 }
5459 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5461 let budget = max_tokens - generated - 1;
5462 if let Some(mut st) = self.mimo_mtp.take() {
5463 let k = st.depth.min(budget);
5464 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5465 self.mimo_mtp = Some(st);
5466 let r = match r {
5467 Ok(r) => r,
5468 Err(err) => {
5469 self.finish_generation(&mut mtp, &mut router, true);
5470 return Err(err);
5471 }
5472 };
5473 if let Some(r) = r {
5474 drafted += r.drafted;
5475 accepted += r.accepted.len();
5476 let mut stopped = false;
5477 for &id in &r.accepted {
5478 if self.confidence_on {
5479 confidence.push(0.0);
5480 }
5481 if !commit!(id) {
5482 stopped = true;
5483 break;
5484 }
5485 }
5486 if stopped {
5487 break 'decode;
5488 }
5489 next_pos += r.accepted.len() + 1;
5490 hidden = r.hidden;
5491 self.graph_logits = Some(r.logits);
5494 continue 'decode;
5495 }
5496 }
5497 }
5498 #[cfg(feature = "gpu")]
5501 if self.speculative
5502 && self.qwen4_exp.is_some()
5503 && task_mask.is_none()
5504 && self.sampler_config.temperature < 1e-6
5505 && generated + 1 < max_tokens
5506 && next_pos > 0
5507 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5508 {
5509 let r = match &mut self.qwen4_exp {
5510 Some(b) => crate::qwen4_exp::spec_round(
5511 &b.0,
5512 &b.1,
5513 &b.2,
5514 &mut b.3,
5515 next_pos,
5516 &all_ids,
5517 &self.inv_freq,
5518 self.pool.as_deref(),
5519 ),
5520 None => None,
5521 };
5522 if let Some(r) = r {
5523 drafted += r.drafted;
5524 accepted += r.accepted.len();
5525 let mut stopped = false;
5526 for &id in &r.accepted {
5527 if self.confidence_on {
5528 confidence.push(0.0);
5529 }
5530 if !commit!(id) {
5531 stopped = true;
5532 break;
5533 }
5534 }
5535 if stopped {
5536 break 'decode;
5537 }
5538 next_pos += r.accepted.len() + 1;
5539 hidden.fill(0.0);
5540 self.graph_logits = Some(r.logits);
5541 continue 'decode;
5542 }
5543 }
5544 match &mut mtp {
5545 #[cfg(feature = "gpu")]
5547 Some(m)
5548 if graph_spec
5549 && !spec_watchdog_off
5550 && generated + 1 < max_tokens
5551 && next_pos > 0 =>
5552 {
5553 let t_round = std::time::Instant::now();
5554 if spec_time_level() >= 2 {
5555 if let Some(t) = spec_round_end.take() {
5556 eprintln!(
5557 "spec-gap {:.2} ms (host between rounds)",
5558 t.elapsed().as_secs_f64() * 1e3
5559 );
5560 }
5561 }
5562 spec_stamps_begin();
5563 #[cfg(target_os = "macos")]
5568 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
5569 .load(std::sync::atomic::Ordering::Relaxed);
5570 #[cfg(not(target_os = "macos"))]
5571 let allocs0 = 0u64;
5572 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5573 m,
5574 &hidden,
5575 t_next,
5576 next_pos,
5577 &mut drafted,
5578 &mut accepted,
5579 &mut all_ids,
5580 max_tokens - generated,
5581 ) {
5582 next_pos = n_pos;
5583 hidden = new_h;
5584 let level = spec_time_level();
5585 if level > 0 {
5586 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5587 let stamps = spec_stamps_take();
5588 let median = if spec_walls.len() >= 3 {
5591 let mut s = spec_walls.clone();
5592 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5593 Some(s[s.len() / 2])
5594 } else {
5595 None
5596 };
5597 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5598 #[cfg(target_os = "macos")]
5599 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5600 .load(std::sync::atomic::Ordering::Relaxed)
5601 - allocs0;
5602 #[cfg(not(target_os = "macos"))]
5603 let allocs = allocs0;
5604 eprintln!(
5605 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5606 extra.len() + 1,
5607 if allocs > 0 {
5608 format!(" [{allocs} new device buffers]")
5609 } else {
5610 String::new()
5611 },
5612 match (outlier, median) {
5613 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5614 _ => String::new(),
5615 }
5616 );
5617 if level >= 2 || outlier {
5618 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5619 eprintln!(
5620 "spec-stamps: {}| untracked {:.1}",
5621 spec_stamps_format(&stamps),
5622 wall - sum
5623 );
5624 }
5625 if spec_mon.n >= 1 {
5626 spec_walls.push(wall);
5627 }
5628 }
5629 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5633 spec_trial = Self::spec_trial_round(
5636 spec_trial,
5637 &mut spec_mon,
5638 generated + extra.len() + 1,
5639 );
5640 let mut stopped = false;
5641 for &id in &extra {
5642 if self.confidence_on {
5643 confidence.push(0.0);
5644 }
5645 if !commit!(id) {
5646 stopped = true;
5647 break;
5648 }
5649 }
5650 if stopped {
5651 break 'decode;
5652 }
5653 if spec_time_level() >= 2 {
5654 spec_round_end = Some(std::time::Instant::now());
5655 }
5656 continue 'decode;
5657 }
5658 if self
5659 .graph_failed
5660 .swap(false, std::sync::atomic::Ordering::Relaxed)
5661 {
5662 self.finish_generation(&mut mtp, &mut router, true);
5668 return Err("GPU MTP graph failed during speculative decode".to_string());
5669 }
5670 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
5681 spec_mon.tokens = 0.0;
5682 spec_mon.fails = 3;
5683 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
5684 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
5685 next_pos += 1;
5686 continue 'decode;
5687 }
5688 Some(m) if !graph_spec && generated + 1 < max_tokens => {
5690 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
5691 drafted += 1;
5692 let emb1 = self.embed_single(t_next);
5693 let emb2 = self.embed_single(draft);
5694 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
5695
5696 inference::rms_norm_into(
5697 &h1,
5698 &self.weights.final_norm,
5699 self.rms_eps,
5700 self.norm_style,
5701 &mut self.ws.n1,
5702 );
5703 let mut logits1 = self.lm_head_forward(&self.ws.n1);
5704 let t_after = sampler::sample_with_scratch_pool(
5705 &logits1,
5706 &self.sampler_config,
5707 self.sampler_config.penalty_past(&all_ids, bounded_native),
5708 &mut self.rng,
5709 &mut self.sampler_scratch,
5710 self.pool.as_deref(),
5711 );
5712 if self.confidence_on {
5713 confidence.push(sampler::top1_prob_pool(
5714 self.pool.as_deref(),
5715 &mut self.sampler_scratch,
5716 &logits1,
5717 t_after,
5718 calib_temp,
5719 ));
5720 }
5721 attention::recycle_buf(&mut logits1);
5722 if trace_on {
5723 traces.push(TokenTrace {
5726 t: generated,
5727 token_id: t_after,
5728 confidence: confidence.last().copied().unwrap_or(0.0),
5729 active_skill: None,
5730 recon: None,
5731 switched: false,
5732 });
5733 }
5734 let stop = !commit!(t_after);
5735
5736 if t_after == draft {
5737 accepted += 1;
5738 self.commit_linear_scratch();
5739 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5740 hidden = h2;
5741 next_pos += 2;
5742 } else {
5743 for layer in &mut self.kv_cache.layers {
5745 layer.truncate_last(1);
5746 }
5747 if !stop {
5748 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5749 hidden = self.forward_layers(
5750 &self.embed_single(t_after),
5751 next_pos + 1,
5752 None,
5753 );
5754 }
5755 next_pos += 2;
5756 }
5757 if stop {
5758 break 'decode;
5759 }
5760 }
5761 _ => {
5763 #[cfg(feature = "gpu")]
5768 if Self::dsv4_spec_on() && self.dsv4.is_some() {
5769 static SAID: std::sync::Once = std::sync::Once::new();
5770 SAID.call_once(|| {
5771 eprintln!(
5772 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
5773 !self.dsv4_mtp.is_empty(),
5774 task_mask.is_none(),
5775 router.is_none(),
5776 !trace_on,
5777 self.sampler_config.temperature < 1e-6,
5778 self.sampler_config.repetition_penalty == 1.0,
5779 );
5780 });
5781 }
5782 #[cfg(feature = "gpu")]
5783 if Self::dsv4_spec_on()
5784 && self.dsv4.is_some()
5785 && !self.dsv4_mtp.is_empty()
5786 && task_mask.is_none()
5787 && router.is_none()
5788 && !trace_on
5789 && self.sampler_config.temperature < 1e-6
5790 && self.sampler_config.repetition_penalty == 1.0
5791 && generated + 1 < max_tokens
5792 && all_ids.len() >= 2
5793 && generated >= dsv4_spec_retry_at
5794 {
5795 let tip_token = all_ids[all_ids.len() - 2];
5796 let drafted0 = drafted;
5797 let round = self.dsv4_spec_step(
5798 tip_token,
5799 t_next,
5800 next_pos,
5801 max_tokens.saturating_sub(generated),
5802 &mut drafted,
5803 &mut accepted,
5804 );
5805 if drafted > drafted0 {
5806 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
5807 if useful {
5808 dsv4_spec_bad = 0;
5809 } else {
5810 dsv4_spec_bad += 1;
5811 if dsv4_spec_bad >= 2 {
5812 dsv4_spec_bad = 0;
5813 dsv4_spec_retry_at = generated.saturating_add(32);
5814 tracing::info!(
5815 "dsv4: draft не окупился дважды — точный walk на 32 токена"
5816 );
5817 }
5818 }
5819 }
5820 if let Some((extra, n_pos)) = round {
5821 next_pos = n_pos;
5822 let mut stopped = false;
5823 for &id in &extra {
5824 if self.confidence_on {
5825 confidence.push(0.0);
5826 }
5827 if !commit!(id) {
5828 stopped = true;
5829 break;
5830 }
5831 }
5832 if stopped {
5833 break 'decode;
5834 }
5835 continue 'decode;
5836 }
5837 }
5838 self.graph_want_logits = fuse_lm;
5839 let mut t_fwd = t_next;
5845 let pure_greedy = self.sampler_config.temperature < 1e-6
5846 && self.sampler_config.repetition_penalty == 1.0
5847 && self.sampler_config.suppress_tokens.is_empty();
5848 let burst_k = std::env::var("CMF_MULTISTEP")
5853 .ok()
5854 .and_then(|v| v.parse::<usize>().ok())
5855 .unwrap_or(0);
5856 if pure_greedy
5857 && burst_k >= 1
5858 && fuse_lm
5859 && task_mask.is_none()
5860 && router.is_none()
5861 && !trace_on
5862 && !self.confidence_on
5863 {
5864 let mut stopped = false;
5865 loop {
5866 let room = max_tokens.saturating_sub(generated);
5867 if room <= 2 {
5868 break;
5869 }
5870 let k = burst_k.min(room - 1);
5871 if k < 1 {
5872 break;
5873 }
5874 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
5875 if self
5876 .graph_failed
5877 .swap(false, std::sync::atomic::Ordering::Relaxed)
5878 {
5879 self.finish_generation(&mut mtp, &mut router, true);
5880 return Err(
5881 "GPU token graph failed during greedy burst".to_string()
5882 );
5883 }
5884 break;
5885 };
5886 next_pos += k;
5887 for &id in &ids {
5888 if !commit!(id) {
5889 stopped = true;
5890 break;
5891 }
5892 }
5893 if stopped {
5894 break;
5895 }
5896 t_fwd = *ids.last().unwrap();
5897 }
5898 if stopped {
5899 break 'decode;
5900 }
5901 }
5902 #[cfg(target_os = "macos")]
5912 if graph_spec
5913 && spec_watchdog_off
5914 && next_pos > 0
5915 && self.mtp_graph_mode == Some(true)
5916 && crate::gpu::q1_force()
5917 {
5918 if let Some(m) = mtp.as_mut() {
5919 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
5920 }
5921 }
5922 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
5923 next_pos += 1;
5924 if let Some(r) = &mut router {
5927 let phi = self.dyn_phi_ema.clone();
5928 let decision = r.step(&phi, generated);
5929 if let Some(new_active) = decision {
5930 let _ = self.set_active_skill(new_active);
5931 }
5932 if trace_on {
5935 if let Some(last) = traces.last_mut() {
5936 let e = r.last_best_e();
5937 last.recon = e.is_finite().then_some(e);
5938 last.switched = decision.is_some();
5939 }
5940 }
5941 }
5942 }
5943 }
5944 }
5945
5946 let cancelled = finish_reason == "cancelled";
5947 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
5951 if mimo_spec {
5952 if let Some(st) = self.mimo_mtp.as_ref() {
5953 let line = st.stats.line();
5954 tracing::info!("{line}");
5955 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
5956 eprintln!("{line}");
5957 }
5958 }
5959 }
5960 self.finish_generation(&mut mtp, &mut router, cancelled);
5961
5962 let output_ids = &all_ids[input_ids.len()..];
5963 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
5967 let consumed = std::mem::take(&mut all_ids);
5972 if dyn_switched {
5973 self.clear_sequence_state();
5974 } else if cancelled || mimo_spec || prompt_rows.is_some() {
5975 self.clear_history();
5976 } else {
5977 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
5978 }
5979 all_ids = consumed;
5980 let output_ids = &all_ids[input_ids.len()..];
5981 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
5983 Ok(GenerateResult {
5984 text: self.tokenizer.decode(output_ids),
5985 token_ids: output_ids.to_vec(),
5986 prompt_tokens: input_ids.len(),
5987 tokens_generated: generated,
5988 finish_reason,
5989 mtp_drafted: drafted,
5990 mtp_accepted: accepted,
5991 token_confidence: confidence,
5992 traces,
5993 })
5994 }
5995
5996 fn mtp_step(
6000 &mut self,
6001 m: &mut MtpModule,
6002 hidden: &[f32],
6003 next_token: u32,
6004 position: usize,
6005 ) -> u32 {
6006 self.mtp_step_h(m, hidden, next_token, position).0
6007 }
6008
6009 fn chain_probe_note(depth: usize, prefix_ok: bool) {
6013 use std::sync::Mutex;
6014 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
6015 let mut t = T.lock().unwrap();
6016 if t.len() <= depth {
6017 t.resize(depth + 1, (0, 0));
6018 }
6019 t[depth].0 += 1;
6020 t[depth].1 += prefix_ok as u64;
6021 if depth == 0 && t[0].0 % 128 == 0 {
6022 let line: Vec<String> = t
6023 .iter()
6024 .enumerate()
6025 .map(|(d, (n, k))| {
6026 format!(
6027 "d{}={:.0}%({n})",
6028 d + 1,
6029 100.0 * *k as f64 / (*n).max(1) as f64
6030 )
6031 })
6032 .collect();
6033 eprintln!("mtp-chain: {}", line.join(" "));
6034 }
6035 }
6036
6037 fn mtp_step_hl(
6045 &mut self,
6046 m: &mut MtpModule,
6047 hidden: &[f32],
6048 next_token: u32,
6049 position: usize,
6050 ) -> (Vec<f32>, Vec<f32>) {
6051 #[cfg(target_os = "macos")]
6056 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
6057 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
6058 self.mtp_graph_mode = Some(true);
6059 return r;
6060 }
6061 if self.mtp_graph_mode == Some(true) {
6062 tracing::error!("mtp Metal graph failed after admission");
6063 self.clear_sequence_state();
6064 self.graph_failed
6065 .store(true, std::sync::atomic::Ordering::Relaxed);
6066 self.cancel
6067 .store(true, std::sync::atomic::Ordering::Relaxed);
6068 return (Vec::new(), Vec::new());
6069 }
6070 self.mtp_graph_mode = Some(false);
6071 }
6072 #[cfg(feature = "gpu")]
6073 if self.mtp_graph_mode != Some(false) {
6074 if !self.mtp_graph_ok(m) {
6075 if self.mtp_graph_mode == Some(true) {
6076 tracing::error!("mtp graph became unavailable after admission");
6081 self.clear_sequence_state();
6082 self.graph_failed
6083 .store(true, std::sync::atomic::Ordering::Relaxed);
6084 self.cancel
6085 .store(true, std::sync::atomic::Ordering::Relaxed);
6086 return (Vec::new(), Vec::new());
6087 }
6088 self.mtp_graph_mode = Some(false);
6089 } else {
6090 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
6091 self.mtp_graph_mode = Some(true);
6092 return r;
6093 }
6094 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6095 return (Vec::new(), Vec::new());
6102 }
6103 tracing::error!("mtp graph failed or declined after admission");
6107 self.clear_sequence_state();
6108 self.graph_failed
6109 .store(true, std::sync::atomic::Ordering::Relaxed);
6110 self.cancel
6111 .store(true, std::sync::atomic::Ordering::Relaxed);
6112 return (Vec::new(), Vec::new());
6113 }
6114 }
6115 let e = self.embed_single(next_token);
6119 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6120 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6121 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6122 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6123 let mut x = vec![0.0f32; self.hidden_size];
6124 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6125
6126 let lw = &m.layer;
6128 inference::rms_norm_into(
6129 &x,
6130 &lw.input_norm,
6131 self.rms_eps,
6132 self.norm_style,
6133 &mut self.ws.n1,
6134 );
6135 let attn = match &lw.attn {
6136 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
6138 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
6139 AttnKind::Full {
6140 wq,
6141 wk,
6142 wv,
6143 wo,
6144 q_norm,
6145 k_norm,
6146 output_gate,
6147 softplus_gate,
6148 bias,
6149 } => {
6150 let mut cfg = self.attn_cfg(position);
6151 cfg.q_norm = q_norm.as_deref();
6152 cfg.k_norm = k_norm.as_deref();
6153 cfg.output_gate = *output_gate;
6154 cfg.softplus_gate = softplus_gate
6155 .as_ref()
6156 .map(|(gate, per_head)| (gate, *per_head));
6157 cfg.bias = bias
6158 .as_ref()
6159 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6160 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6161 }
6162 AttnKind::Linear(_)
6163 | AttnKind::LinearGdn(_)
6164 | AttnKind::ShortConv(_)
6165 | AttnKind::Bounded(_) => {
6166 unreachable!("MTP block is full attention")
6167 }
6168 };
6169 for (i, &a) in attn.iter().enumerate() {
6170 x[i] += a;
6171 }
6172 inference::rms_norm_into(
6173 &x,
6174 &lw.post_norm,
6175 self.rms_eps,
6176 self.norm_style,
6177 &mut self.ws.p1,
6178 );
6179 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
6180 for (i, &f) in ffn.iter().enumerate() {
6181 x[i] += f;
6182 }
6183
6184 inference::rms_norm_into(
6185 &x,
6186 &m.final_norm,
6187 self.rms_eps,
6188 self.norm_style,
6189 &mut self.ws.n1,
6190 );
6191 let lg = self.lm_head_forward(&self.ws.n1);
6192 (lg, x)
6193 }
6194
6195 fn mtp_step_h(
6197 &mut self,
6198 m: &mut MtpModule,
6199 hidden: &[f32],
6200 next_token: u32,
6201 position: usize,
6202 ) -> (u32, Vec<f32>) {
6203 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
6204 let draft = sampler::argmax(&lg);
6205 attention::recycle_buf(&mut lg);
6206 (draft, x)
6207 }
6208
6209 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
6215 match trial {
6216 SpecTrial::Spec { t0, gen0, rounds } => {
6217 let rounds = rounds + 1;
6218 if rounds >= 5 {
6219 if mon.plain_ms > 0.0 {
6220 let keep = mon.pays();
6221 mon.fails = 0;
6222 tracing::info!(
6223 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
6224 mon.tokens,
6225 mon.round_ms,
6226 mon.plain_ms,
6227 if keep { "speculating" } else { "plain" }
6228 );
6229 SpecTrial::Decided {
6230 spec: keep,
6231 recheck_at: if keep { usize::MAX } else { generated + 128 },
6232 }
6233 } else if mon.pays() {
6234 mon.fails = 0;
6239 tracing::info!(
6240 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6241 mon.tokens,
6242 mon.round_ms,
6243 );
6244 SpecTrial::Decided {
6245 spec: true,
6246 recheck_at: usize::MAX,
6247 }
6248 } else {
6249 SpecTrial::Plain {
6250 t0: std::time::Instant::now(),
6251 gen0: generated,
6252 }
6253 }
6254 } else {
6255 SpecTrial::Spec { t0, gen0, rounds }
6256 }
6257 }
6258 SpecTrial::Decided { spec: true, .. } => {
6259 if mon.pays() {
6260 mon.fails = 0;
6261 trial
6262 } else {
6263 mon.fails += 1;
6264 if mon.fails >= 4 {
6265 if mon.plain_ms <= 0.0 {
6266 tracing::info!(
6270 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6271 mon.tokens,
6272 mon.round_ms,
6273 );
6274 return SpecTrial::Plain {
6275 t0: std::time::Instant::now(),
6276 gen0: generated,
6277 };
6278 }
6279 tracing::info!(
6280 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6281 mon.tokens,
6282 mon.round_ms,
6283 mon.plain_ms
6284 );
6285 SpecTrial::Decided {
6286 spec: false,
6287 recheck_at: generated + 128,
6288 }
6289 } else {
6290 trial
6291 }
6292 }
6293 }
6294 other => other,
6295 }
6296 }
6297
6298 fn mtp_kv_id(&self) -> u64 {
6301 self.graph_kv_id | (1u64 << 40)
6302 }
6303
6304 const MTP_LAYER_BASE: usize = 0;
6309
6310 #[cfg(feature = "gpu")]
6317 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6318 self.mtp_graph_mode != Some(true)
6319 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6320 }
6321
6322 #[cfg(feature = "gpu")]
6328 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6329 let mut ok = true;
6330 let mut expected = false;
6331 for li in 0..self.num_layers {
6332 if matches!(
6333 self.weights.layers[self.phys_layer(li)].attn,
6334 AttnKind::Full { .. }
6335 ) {
6336 expected = true;
6337 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6338 }
6339 }
6340 !expected || ok
6341 }
6342
6343 fn graph_gdn_layer_count(&self) -> usize {
6347 (0..self.num_layers)
6348 .filter(|&li| {
6349 matches!(
6350 &self.weights.layers[self.phys_layer(li)].attn,
6351 AttnKind::LinearGdn(_)
6352 )
6353 })
6354 .count()
6355 }
6356
6357 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6360 let e = self.embed_single(next_token);
6361 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6362 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6363 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6364 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6365 let mut x = vec![0.0f32; self.hidden_size];
6366 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6367 x
6368 }
6369
6370 #[cfg(feature = "gpu")]
6373 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6374 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6375 return false;
6376 }
6377 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6378 || !crate::gpu::enabled_here()
6379 || self.attn_softcap > 0.0
6380 || self.attention_heads_per_layer.is_some()
6381 || self.v_head_dim.is_some()
6384 {
6385 return false;
6386 }
6387 matches!(
6388 &m.layer.attn,
6389 AttnKind::Full {
6390 softplus_gate: None,
6391 ..
6392 }
6393 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6394 }
6395
6396 #[cfg(feature = "gpu")]
6400 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6401 if !self.mtp_block_graph_ok(m) {
6402 return false;
6403 }
6404 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6405 return false;
6406 };
6407 let FfnKind::Dense(d) = &m.layer.ffn else {
6408 return false;
6409 };
6410 d.segs.is_empty()
6411 && wq.graph_weight().is_some()
6412 && wk.graph_weight().is_some()
6413 && wv.graph_weight().is_some()
6414 && wo.graph_weight().is_some()
6415 && d.gate_proj.graph_weight().is_some()
6416 && d.up_proj.graph_weight().is_some()
6417 && d.down_proj.graph_weight().is_some()
6418 && self.weights.lm_head.graph_weight().is_some()
6419 }
6420
6421 #[cfg(feature = "gpu")]
6427 fn mtp_step_graph(
6428 &mut self,
6429 m: &mut MtpModule,
6430 hidden: &[f32],
6431 next_token: u32,
6432 position: usize,
6433 ) -> Option<(Vec<f32>, Vec<f32>)> {
6434 if !self.mtp_graph_ok(m) {
6435 return None;
6436 }
6437 let lw = &m.layer;
6438 let AttnKind::Full {
6439 wq,
6440 wk,
6441 wv,
6442 wo,
6443 q_norm,
6444 k_norm,
6445 output_gate,
6446 softplus_gate,
6447 bias,
6448 } = &lw.attn
6449 else {
6450 return None;
6451 };
6452 if softplus_gate.is_some() {
6453 return None;
6454 }
6455 let FfnKind::Dense(d) = &lw.ffn else {
6456 return None;
6457 };
6458 if !d.segs.is_empty() {
6459 return None; }
6461 let mut x = self.mtp_block_input(m, hidden, next_token);
6464 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6465 let (_, i, kind, rs) = t.graph_weight()?;
6466 Some(crate::gpu::GraphW {
6467 idx: i,
6468 kind,
6469 row_scale: rs,
6470 data: &[],
6471 prism: crate::gpu::GraphPrismOp::None,
6472 affine: false,
6473 })
6474 }
6475 let (model, _, _, _) = wq.graph_weight()?;
6476 let model = model.clone();
6477 let (lm_gw, lm_rows) = {
6478 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6479 let rows = if kind == 6 {
6483 self.draft_head_rows(self.weights.lm_head.rows())
6484 } else {
6485 self.weights.lm_head.rows()
6486 };
6487 (
6488 crate::gpu::GraphW {
6489 idx: i,
6490 kind,
6491 row_scale: rs,
6492 data: &[],
6493 prism: crate::gpu::GraphPrismOp::None,
6494 affine: false,
6495 },
6496 rows,
6497 )
6498 };
6499 let layer = crate::gpu::GraphLayer {
6500 input_norm: &lw.input_norm,
6501 attn: crate::gpu::GraphAttn::Full {
6502 wq: gw(wq)?,
6503 wk: gw(wk)?,
6504 wv: gw(wv)?,
6505 wo: gw(wo)?,
6506 q_norm: q_norm.as_deref(),
6507 k_norm: k_norm.as_deref(),
6508 late_qk_norm: self.qk_norm_after_rope,
6509 bias: bias
6510 .as_ref()
6511 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6512 output_gate: *output_gate,
6513 cpu_k: m.kv.k_heads(),
6514 cpu_v: m.kv.v_heads(),
6515 cpu_base: m.kv.base(),
6516 geom: None,
6517 head_gate: None,
6518 },
6519 post_norm: &lw.post_norm,
6520 attn_out_norm: None,
6521 ffn_out_norm: None,
6522 layer_scale: None,
6523 ffn: crate::gpu::GraphFfn::Dense {
6524 gate: gw(&d.gate_proj)?,
6525 up: gw(&d.up_proj)?,
6526 down: gw(&d.down_proj)?,
6527 act: d.act.graph_act()?,
6528 },
6529 };
6530 let nh = self.num_heads;
6531 let (nkv, hd, rd) = self.layer_geom(0);
6532 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6533 let mut logits = Vec::new();
6534 let ok = crate::gpu::forward_token_graph(
6535 &model,
6536 self.mtp_kv_id(),
6537 std::slice::from_ref(&layer),
6538 &[None],
6539 self.o1_epoch,
6540 &self.inv_freq,
6541 &mut x,
6542 nh,
6543 nkv,
6544 hd,
6545 self.attn_scale,
6546 rd,
6547 self.hidden_size,
6548 self.intermediate_size,
6549 position,
6550 self.kv_cache.max_seq_len,
6551 gemma,
6552 self.rms_eps as f32,
6553 Some((&lm_gw, lm_rows)),
6554 &m.final_norm,
6555 &mut logits,
6556 &[],
6557 1,
6558 None,
6559 None,
6560 None,
6561 Self::MTP_LAYER_BASE,
6562 true,
6563 );
6564 match ok {
6565 crate::gpu::TokenGraphOutcome::Completed => {}
6566 crate::gpu::TokenGraphOutcome::Declined => return None,
6567 crate::gpu::TokenGraphOutcome::Failed => {
6568 self.clear_sequence_state();
6572 self.graph_failed
6573 .store(true, std::sync::atomic::Ordering::Relaxed);
6574 self.cancel
6575 .store(true, std::sync::atomic::Ordering::Relaxed);
6576 return None;
6577 }
6578 }
6579 logits.resize(self.vocab_size, 0.0);
6580 Some((logits, x))
6581 }
6582
6583 #[cfg(feature = "gpu")]
6591 fn mtp_warm_graph(
6592 &mut self,
6593 m: &mut MtpModule,
6594 pairs: &[(&[f32], u32)],
6595 first_pos: usize,
6596 ) -> crate::gpu::BatchGraphOutcome {
6597 if pairs.is_empty() {
6598 return crate::gpu::BatchGraphOutcome::Completed;
6599 }
6600 if !self.mtp_block_graph_ok(m) {
6601 return crate::gpu::BatchGraphOutcome::Declined;
6602 }
6603 let hs = self.hidden_size;
6604 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6607 for (h, t) in pairs {
6608 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6609 }
6610 let lw = &m.layer;
6611 let AttnKind::Full {
6612 wq,
6613 wk,
6614 wv,
6615 wo,
6616 q_norm,
6617 k_norm,
6618 output_gate,
6619 bias,
6620 ..
6621 } = &lw.attn
6622 else {
6623 return crate::gpu::BatchGraphOutcome::Declined;
6624 };
6625 let FfnKind::Dense(d) = &lw.ffn else {
6626 return crate::gpu::BatchGraphOutcome::Declined;
6627 };
6628 if !d.segs.is_empty() {
6629 return crate::gpu::BatchGraphOutcome::Declined; }
6631 let (AttnKind::Full { softplus_gate: None, .. }, Some(gact)) =
6634 (&lw.attn, d.act.graph_act())
6635 else {
6636 return crate::gpu::BatchGraphOutcome::Declined;
6637 };
6638 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6639 let (_, i, kind, rs) = t.graph_weight()?;
6640 Some(crate::gpu::GraphW {
6641 idx: i,
6642 kind,
6643 row_scale: rs,
6644 data: &[],
6645 prism: crate::gpu::GraphPrismOp::None,
6646 affine: false,
6647 })
6648 }
6649 let Some((model, _, _, _)) = wq.graph_weight() else {
6650 return crate::gpu::BatchGraphOutcome::Declined;
6651 };
6652 let model = model.clone();
6653 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6654 gw(wq),
6655 gw(wk),
6656 gw(wv),
6657 gw(wo),
6658 gw(&d.gate_proj),
6659 gw(&d.up_proj),
6660 gw(&d.down_proj),
6661 ) else {
6662 return crate::gpu::BatchGraphOutcome::Declined;
6663 };
6664 let layer = crate::gpu::GraphLayer {
6665 input_norm: &lw.input_norm,
6666 attn: crate::gpu::GraphAttn::Full {
6667 wq: gwq,
6668 wk: gwk,
6669 wv: gwv,
6670 wo: gwo,
6671 q_norm: q_norm.as_deref(),
6672 k_norm: k_norm.as_deref(),
6673 late_qk_norm: self.qk_norm_after_rope,
6674 bias: bias
6675 .as_ref()
6676 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6677 output_gate: *output_gate,
6678 cpu_k: m.kv.k_heads(),
6679 cpu_v: m.kv.v_heads(),
6680 cpu_base: m.kv.base(),
6681 geom: None,
6682 head_gate: None,
6683 },
6684 post_norm: &lw.post_norm,
6685 attn_out_norm: None,
6686 ffn_out_norm: None,
6687 layer_scale: None,
6688 ffn: crate::gpu::GraphFfn::Dense {
6689 gate: gg,
6690 up: gu,
6691 down: gd,
6692 act: gact,
6693 },
6694 };
6695 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
6696 let nh = self.num_heads;
6697 let (nkv, hd, rd) = self.layer_geom(0);
6698 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6699 crate::gpu::forward_batch_graph(
6700 &model,
6701 self.mtp_kv_id(),
6702 std::slice::from_ref(&layer),
6703 &self.inv_freq,
6704 &mut hiddens,
6705 nh,
6706 nkv,
6707 hd,
6708 rd,
6709 hs,
6710 self.intermediate_size,
6711 &positions,
6712 self.kv_cache.max_seq_len,
6713 gemma,
6714 self.rms_eps as f32,
6715 self.attn_scale,
6716 pairs.len(),
6717 &[],
6718 0,
6719 None,
6720 None,
6721 )
6722 }
6723
6724 #[cfg(feature = "gpu")]
6731 fn mtp_warm_graph_fallback(
6732 &mut self,
6733 m: &mut MtpModule,
6734 pairs: &[(&[f32], u32)],
6735 first_pos: usize,
6736 ) -> bool {
6737 if pairs.is_empty() {
6738 return true;
6739 }
6740 let graphable = self.mtp_block_graph_ok(m);
6741 if !graphable {
6742 if self.mtp_graph_mode == Some(true) {
6746 return false;
6747 }
6748 self.mtp_graph_mode = Some(false);
6749 for (j, (h, t)) in pairs.iter().enumerate() {
6750 self.mtp_warm(m, h, *t, first_pos + j);
6751 }
6752 return true;
6753 }
6754
6755 for (j, (h, t)) in pairs.iter().enumerate() {
6760 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
6761 return false;
6762 }
6763 }
6764 self.mtp_graph_mode = Some(true);
6765 true
6766 }
6767
6768 #[cfg(feature = "gpu")]
6773 fn mtp_warm_prefill_pairs(
6774 &mut self,
6775 m: &mut MtpModule,
6776 pairs: &[(&[f32], u32)],
6777 first_pos: usize,
6778 ) -> Result<(), &'static str> {
6779 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
6784 if self.mtp_graph_mode == Some(true) {
6785 return Err("MTP token graph became unavailable after admission");
6786 }
6787 self.mtp_graph_mode = Some(false);
6788 for (j, (h, t)) in pairs.iter().enumerate() {
6789 self.mtp_warm(m, h, *t, first_pos + j);
6790 }
6791 return Ok(());
6792 }
6793 match self.mtp_warm_graph(m, pairs, first_pos) {
6794 crate::gpu::BatchGraphOutcome::Completed => {
6795 if !pairs.is_empty() {
6796 self.mtp_graph_mode = Some(true);
6797 }
6798 Ok(())
6799 }
6800 crate::gpu::BatchGraphOutcome::Declined => {
6801 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
6802 Ok(())
6803 } else {
6804 Err("MTP warm-up fallback failed after device admission")
6805 }
6806 }
6807 crate::gpu::BatchGraphOutcome::Failed => {
6808 Err("MTP warm batch graph failed after admission")
6809 }
6810 }
6811 }
6812
6813 #[cfg(not(feature = "gpu"))]
6814 fn mtp_warm_prefill_pairs(
6815 &mut self,
6816 m: &mut MtpModule,
6817 pairs: &[(&[f32], u32)],
6818 first_pos: usize,
6819 ) -> Result<(), &'static str> {
6820 for (j, (h, t)) in pairs.iter().enumerate() {
6821 self.mtp_warm(m, h, *t, first_pos + j);
6822 }
6823 Ok(())
6824 }
6825
6826 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
6830 let e = self.embed_single(next_token);
6831 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6832 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6833 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6834 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6835 let mut x = vec![0.0f32; self.hidden_size];
6836 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6837 inference::rms_norm_into(
6838 &x,
6839 &m.layer.input_norm,
6840 self.rms_eps,
6841 self.norm_style,
6842 &mut self.ws.n1,
6843 );
6844 let attn = match &m.layer.attn {
6845 AttnKind::Full {
6846 wq,
6847 wk,
6848 wv,
6849 wo,
6850 q_norm,
6851 k_norm,
6852 output_gate,
6853 softplus_gate,
6854 bias,
6855 } => {
6856 let mut cfg = self.attn_cfg(position);
6857 cfg.q_norm = q_norm.as_deref();
6858 cfg.k_norm = k_norm.as_deref();
6859 cfg.output_gate = *output_gate;
6860 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
6861 cfg.bias = bias
6862 .as_ref()
6863 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6864 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6865 }
6866 _ => return,
6867 };
6868 let _ = attn;
6869 }
6870
6871 #[cfg(feature = "gpu")]
6878 #[allow(clippy::too_many_arguments)]
6879 fn graph_spec_step(
6880 &mut self,
6881 m: &mut MtpModule,
6882 hidden: &[f32],
6883 t_next: u32,
6884 next_pos: usize,
6885 drafted: &mut usize,
6886 accepted: &mut usize,
6887 all_ids: &mut Vec<u32>,
6891 room: usize,
6896 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
6897 #[cfg(target_os = "macos")]
6908 let metal_native = crate::gpu::q1_force();
6909 #[cfg(not(target_os = "macos"))]
6910 let metal_native = false;
6911 #[cfg(feature = "gpu")]
6912 let k_default = if metal_native {
6913 7
6916 } else if crate::gpu_wgpu::verify_i8_on() {
6917 5
6918 } else {
6919 4
6920 };
6921 #[cfg(not(feature = "gpu"))]
6922 let k_default = 4;
6923 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
6924 .ok()
6925 .and_then(|v| v.parse().ok())
6926 .filter(|&v| (1..=8).contains(&v));
6927 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
6932 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
6933 let k_spec = k_full.min(room).max(1);
6934 let k_capped = k_spec < k_full;
6937 if next_pos == 0 {
6938 return None;
6939 }
6940 let t_round = std::time::Instant::now();
6941 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
6957 let sub0 = subs();
6958 let cfg = self.sampler_config.clone();
6963 let penalized = !(cfg.repetition_penalty == 1.0
6964 && cfg.presence_penalty == 0.0
6965 && cfg.suppress_tokens.is_empty());
6966 let greedy_pen = cfg.temperature < 1e-6 && penalized;
6971 let sampling = cfg.temperature >= 1e-6;
6972 let sparse = sampling && sampler::sparse_ok(&cfg);
6978 let base_len = all_ids.len();
6979 if sampling && !sparse && self.spec_q.len() < k_spec {
6980 self.spec_q.resize_with(k_spec, Vec::new);
6981 }
6982 if sparse && self.spec_qs.len() < k_spec {
6983 self.spec_qs.resize_with(k_spec, Vec::new);
6984 }
6985 let mut drafts = Vec::with_capacity(k_spec);
6990 let mut hx = hidden.to_vec();
6991 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
6994 spec_stamp("pro");
6995 #[cfg(target_os = "macos")]
7001 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
7002 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
7003 Ok(ids) => {
7004 self.mtp_graph_mode = Some(true);
7005 drafts = ids;
7006 }
7007 Err(true) => {
7008 tracing::error!("mtp Metal draft chain failed after commit");
7009 self.clear_sequence_state();
7010 self.graph_failed
7011 .store(true, std::sync::atomic::Ordering::Relaxed);
7012 self.cancel
7013 .store(true, std::sync::atomic::Ordering::Relaxed);
7014 return None;
7015 }
7016 Err(false) => {}
7017 }
7018 }
7019 for j in drafts.len()..k_spec {
7020 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
7021 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
7022 if spec_dbg {
7023 let saved = self.mtp_graph_mode;
7024 self.mtp_graph_mode = Some(false);
7025 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7026 self.mtp_graph_mode = saved;
7027 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7028 return None;
7029 }
7030 m.kv.truncate_last(1);
7031 dbg_ref = Some(r);
7032 }
7033 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
7034 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
7035 return None;
7036 }
7037 if let Some((lg_cpu, h_cpu)) = dbg_ref {
7038 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
7039 let dl = lg
7040 .iter()
7041 .zip(&lg_cpu)
7042 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7043 let dh = hj
7044 .iter()
7045 .zip(&h_cpu)
7046 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
7047 eprintln!(
7048 "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 {}",
7049 next_pos - 1 + j,
7050 sampler::argmax(&lg_cpu),
7051 sampler::argmax(&lg),
7052 n(&h_cpu),
7053 n(&hj),
7054 m.kv.seq_len
7055 );
7056 }
7057 let dj = if sparse {
7058 let mut q = std::mem::take(&mut self.spec_qs[j]);
7059 let ok = sampler::sparse_distribution_into(
7060 &lg,
7061 &cfg,
7062 all_ids,
7063 &mut self.sampler_scratch,
7064 self.pool.as_deref(),
7065 &mut q,
7066 );
7067 let d = if ok {
7068 sampler::draw_sparse(&q, &mut self.rng)
7069 } else {
7070 let t = sampler::argmax(&lg);
7072 q.clear();
7073 q.push((t, 1.0));
7074 t
7075 };
7076 self.spec_qs[j] = q;
7077 all_ids.push(d);
7078 d
7079 } else if sampling {
7080 let mut q = std::mem::take(&mut self.spec_q[j]);
7081 sampler::distribution_into(
7082 &lg,
7083 &cfg,
7084 all_ids,
7085 &mut self.sampler_scratch,
7086 self.pool.as_deref(),
7087 &mut q,
7088 );
7089 let d = sampler::draw(&q, &mut self.rng);
7090 self.spec_q[j] = q;
7091 all_ids.push(d); d
7093 } else if greedy_pen {
7094 let d = sampler::argmax_penalized(
7095 &lg,
7096 &cfg,
7097 all_ids,
7098 &mut self.sampler_scratch,
7099 self.pool.as_deref(),
7100 );
7101 all_ids.push(d);
7102 d
7103 } else {
7104 sampler::argmax(&lg)
7105 };
7106 attention::recycle_buf(&mut lg);
7107 drafts.push(dj);
7108 hx = hj;
7109 spec_stamp("d.pick");
7110 }
7111 all_ids.truncate(base_len);
7112 *drafted += k_spec;
7113 let t_draft = t_round.elapsed();
7114 let sub_draft = subs();
7115 let b = k_spec + 1;
7118 let mut hiddens = vec![0.0f32; b * self.hidden_size];
7119 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
7120 let e = self.embed_single(t);
7121 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
7122 }
7123 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
7124 spec_stamp("v.emb");
7125 let (lm_gw, lm_rows) = {
7126 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
7127 (
7128 crate::gpu::GraphW {
7129 idx: i,
7130 kind,
7131 row_scale: rs,
7132 data: &[],
7133 prism: crate::gpu::GraphPrismOp::None,
7134 affine: false,
7135 },
7136 self.weights.lm_head.rows(),
7137 )
7138 };
7139 let mut logits = Vec::new();
7140 let final_norm = self.weights.final_norm.clone();
7141 #[cfg(target_os = "macos")]
7150 let greedy_dev = metal_native
7151 && !sampling
7152 && !greedy_pen
7153 && !self.confidence_on
7154 && self.final_softcap.is_none()
7155 && self.vocab_size == lm_rows
7161 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
7162 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
7163 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
7164 #[cfg(not(target_os = "macos"))]
7165 let greedy_dev = false;
7166 let mut dev_ids: Vec<u32> = Vec::new();
7167 #[cfg(target_os = "macos")]
7168 let verify_outcome = if metal_native {
7169 let lm = self.weights.lm_head.q1_parts()?;
7170 let n_score = self.vocab_size.min(lm_rows);
7171 self.try_batch_graph_metal(
7172 &mut hiddens,
7173 &positions,
7174 b,
7175 Some((lm, &final_norm, &mut logits)),
7176 if greedy_dev {
7177 Some((n_score, &mut dev_ids))
7178 } else {
7179 None
7180 },
7181 )
7182 } else {
7183 self.try_batch_graph_wgpu(
7184 &mut hiddens,
7185 &positions,
7186 b,
7187 Some(crate::gpu::SpecTail {
7188 lm: lm_gw,
7189 lm_rows,
7190 final_norm: &final_norm,
7191 logits_out: &mut logits,
7192 }),
7193 )
7194 };
7195 #[cfg(not(target_os = "macos"))]
7196 let verify_outcome = self.try_batch_graph_wgpu(
7197 &mut hiddens,
7198 &positions,
7199 b,
7200 Some(crate::gpu::SpecTail {
7201 lm: lm_gw,
7202 lm_rows,
7203 final_norm: &final_norm,
7204 logits_out: &mut logits,
7205 }),
7206 );
7207 match verify_outcome {
7208 crate::gpu::BatchGraphOutcome::Completed => {}
7209 crate::gpu::BatchGraphOutcome::Declined => {
7210 m.kv.truncate_last(k_spec);
7214 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
7215 self.clear_sequence_state();
7216 self.graph_failed
7217 .store(true, std::sync::atomic::Ordering::Relaxed);
7218 self.cancel
7219 .store(true, std::sync::atomic::Ordering::Relaxed);
7220 tracing::error!("MTP graph mirror rewind failed after verify decline");
7221 }
7222 return None;
7223 }
7224 crate::gpu::BatchGraphOutcome::Failed => {
7225 self.clear_sequence_state();
7229 self.graph_failed
7230 .store(true, std::sync::atomic::Ordering::Relaxed);
7231 self.cancel
7232 .store(true, std::sync::atomic::Ordering::Relaxed);
7233 tracing::error!("MTP verify batch graph failed after admission");
7234 return None;
7235 }
7236 }
7237 #[cfg(target_os = "macos")]
7243 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
7244 let snap: Vec<Vec<f32>> = self
7245 .kv_cache
7246 .layers
7247 .iter()
7248 .map(|l| l.linear_state.clone())
7249 .collect();
7250 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7251 let toks: Vec<u32> = std::iter::once(t_next)
7252 .chain(drafts.iter().copied())
7253 .collect();
7254 let want_save = self.graph_want_logits;
7255 self.graph_want_logits = false;
7256 for (i, &t) in toks.iter().enumerate() {
7257 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7258 let _ = self.graph_logits.take();
7259 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7263 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7264 }
7265 let ref_lg = self.logits_from_hidden(&hi);
7266 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7267 let ra = sampler::argmax(&ref_lg);
7268 let va = sampler::argmax(row);
7269 let mut md = 0f32;
7270 let mut rms = 0f64;
7271 for j in 0..lm_rows.min(ref_lg.len()) {
7272 let d = (ref_lg[j] - row[j]).abs();
7273 md = md.max(d);
7274 rms += (d as f64) * (d as f64);
7275 }
7276 let mut hd = 0f32;
7277 for j in 0..self.hidden_size {
7278 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7279 }
7280 eprintln!(
7281 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7282 next_pos + i,
7283 if ra == va { "OK" } else { "MISMATCH" },
7284 (rms / lm_rows as f64).sqrt()
7285 );
7286 }
7287 self.graph_want_logits = want_save;
7288 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7291 if l.linear_state.len() == st.len() {
7292 l.linear_state.copy_from_slice(&st);
7293 } else {
7294 l.linear_state = st;
7295 }
7296 }
7297 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7298 let extra = l.seq_len.saturating_sub(n0);
7299 if extra > 0 {
7300 l.truncate_last(extra);
7301 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7302 }
7303 }
7304 }
7305 let t_verify = t_round.elapsed();
7306 let sub_verify = subs();
7307 let mut a = 0usize;
7312 let mut forced: Option<u32> = None;
7313 let ids: Vec<u32> = if sparse {
7314 let mut p = std::mem::take(&mut self.spec_ps);
7315 let mut res = std::mem::take(&mut self.spec_ress);
7316 while a < k_spec {
7317 let ok = sampler::sparse_distribution_into(
7318 &logits[a * lm_rows..(a + 1) * lm_rows],
7319 &cfg,
7320 all_ids,
7321 &mut self.sampler_scratch,
7322 self.pool.as_deref(),
7323 &mut p,
7324 );
7325 if !ok {
7326 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7327 p.clear();
7328 p.push((t, 1.0));
7329 }
7330 match sampler::spec_accept_or_correct_sparse(
7331 &p,
7332 &self.spec_qs[a],
7333 drafts[a],
7334 &mut self.rng,
7335 &mut res,
7336 ) {
7337 None => {
7338 all_ids.push(drafts[a]);
7339 a += 1;
7340 }
7341 Some(c) => {
7342 forced = Some(c);
7343 break;
7344 }
7345 }
7346 }
7347 all_ids.truncate(base_len);
7348 self.spec_ps = p;
7349 self.spec_ress = res;
7350 drafts.clone()
7351 } else if sampling {
7352 let mut p = std::mem::take(&mut self.spec_p);
7353 let mut res = std::mem::take(&mut self.spec_res);
7354 while a < k_spec {
7355 sampler::distribution_into(
7356 &logits[a * lm_rows..(a + 1) * lm_rows],
7357 &cfg,
7358 all_ids,
7359 &mut self.sampler_scratch,
7360 self.pool.as_deref(),
7361 &mut p,
7362 );
7363 match sampler::spec_accept_or_correct(
7364 &p,
7365 &self.spec_q[a],
7366 drafts[a],
7367 &mut self.rng,
7368 &mut res,
7369 self.pool.as_deref(),
7370 ) {
7371 None => {
7372 all_ids.push(drafts[a]);
7373 a += 1;
7374 }
7375 Some(c) => {
7376 forced = Some(c);
7377 break;
7378 }
7379 }
7380 }
7381 all_ids.truncate(base_len);
7382 self.spec_p = p;
7383 self.spec_res = res;
7384 drafts.clone()
7386 } else if greedy_pen {
7387 let mut ids: Vec<u32> = Vec::with_capacity(b);
7391 for i in 0..b {
7392 let t = sampler::argmax_penalized(
7393 &logits[i * lm_rows..(i + 1) * lm_rows],
7394 &cfg,
7395 all_ids,
7396 &mut self.sampler_scratch,
7397 self.pool.as_deref(),
7398 );
7399 ids.push(t);
7400 if i < k_spec && t == drafts[i] {
7401 all_ids.push(t);
7402 } else {
7403 break;
7404 }
7405 }
7406 all_ids.truncate(base_len);
7407 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7408 a += 1;
7409 }
7410 ids
7413 } else if greedy_dev && dev_ids.len() == b {
7414 let ids = std::mem::take(&mut dev_ids);
7415 while a < k_spec && ids[a] == drafts[a] {
7416 a += 1;
7417 }
7418 ids
7419 } else {
7420 if logits.len() < b * lm_rows {
7421 self.clear_sequence_state();
7424 self.graph_failed
7425 .store(true, std::sync::atomic::Ordering::Relaxed);
7426 self.cancel
7427 .store(true, std::sync::atomic::Ordering::Relaxed);
7428 tracing::error!("Metal verify returned neither logits nor argmax ids");
7429 return None;
7430 }
7431 let ids: Vec<u32> = (0..b)
7432 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7433 .collect();
7434 while a < k_spec && ids[a] == drafts[a] {
7435 a += 1;
7436 }
7437 ids
7438 };
7439 spec_stamp("acc");
7440 if spec_dbg {
7441 eprintln!(
7442 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7443 drafts, ids
7444 );
7445 }
7446 #[cfg(target_os = "macos")]
7450 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7451 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7452 {
7453 let snap: Vec<Vec<f32>> = self
7454 .kv_cache
7455 .layers
7456 .iter()
7457 .map(|l| l.linear_state.clone())
7458 .collect();
7459 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7460 let toks: Vec<u32> = std::iter::once(t_next)
7461 .chain(drafts.iter().copied())
7462 .collect();
7463 let want_save = self.graph_want_logits;
7464 self.graph_want_logits = false;
7465 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7466 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7467 let _ = self.graph_logits.take();
7468 }
7469 self.graph_want_logits = want_save;
7470 let plain_states: Vec<Vec<f32>> = self
7471 .kv_cache
7472 .layers
7473 .iter()
7474 .map(|l| l.linear_state.clone())
7475 .collect();
7476 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7477 let mut rows = Vec::new();
7478 for (li, (l, n0)) in self
7479 .kv_cache
7480 .layers
7481 .iter_mut()
7482 .zip(attn_lens.iter())
7483 .enumerate()
7484 {
7485 let extra = l.seq_len.saturating_sub(*n0);
7486 if extra > 0 {
7487 let mut kk = Vec::new();
7488 let mut vv = Vec::new();
7489 for g in 0..nkv {
7490 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7491 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7492 }
7493 rows.push((li, kk, vv));
7494 l.truncate_last(extra);
7495 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7496 }
7497 }
7498 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7499 if l.linear_state.len() == st.len() {
7500 l.linear_state.copy_from_slice(&st);
7501 } else {
7502 l.linear_state = st;
7503 }
7504 }
7505 Some((plain_states, rows))
7506 } else {
7507 None
7508 };
7509 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7510 #[cfg(target_os = "macos")]
7519 let mut warm_pending: Option<MetalWarmPending> = None;
7520 #[cfg(target_os = "macos")]
7521 if metal_native {
7522 m.kv.truncate_last(k_spec.saturating_sub(1));
7523 if self.mtp_graph_mode == Some(true) {
7524 crate::gpu_metal::kv_mirror_set_stored(
7527 self.mtp_kv_id(),
7528 Self::MTP_LAYER_BASE,
7529 m.kv.seq_len,
7530 );
7531 if !warm_off && a > 0 {
7532 let pairs: Vec<(&[f32], u32)> = (0..a)
7533 .map(|j| {
7534 (
7535 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7536 ids[j],
7537 )
7538 })
7539 .collect();
7540 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7541 }
7542 }
7543 spec_stamp("c.wsub");
7544 }
7545 #[cfg(target_os = "macos")]
7547 if metal_native {
7548 if !self.metal_verify_commit(a) {
7551 self.clear_sequence_state();
7552 self.graph_failed
7553 .store(true, std::sync::atomic::Ordering::Relaxed);
7554 self.cancel
7555 .store(true, std::sync::atomic::Ordering::Relaxed);
7556 tracing::error!("Metal verify state/KV handoff failed after admission");
7557 return None;
7558 }
7559 if let Some((plain_states, rows)) = commit_ref {
7560 crate::gpu_metal::queue_fence();
7561 let _ = crate::gpu_metal::wait_replay();
7564 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7565 let mut worst_s = 0f32;
7566 let mut worst_li = 0usize;
7567 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7568 if l.linear_state.len() != ps.len() || ps.is_empty() {
7569 continue;
7570 }
7571 let d = l
7572 .linear_state
7573 .iter()
7574 .zip(ps)
7575 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7576 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7577 let rel = d / n.max(1e-6);
7578 if rel > worst_s {
7579 worst_s = rel;
7580 worst_li = li;
7581 }
7582 }
7583 let mut worst_k = 0f32;
7584 for (li, kk, vv) in &rows {
7585 let l = &self.kv_cache.layers[*li];
7586 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7587 let mut ck = Vec::new();
7588 let mut cv = Vec::new();
7589 for g in 0..nkv {
7590 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7591 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7592 }
7593 if ck.len() == kk.len() {
7594 let dk = ck
7595 .iter()
7596 .zip(kk)
7597 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7598 let dv = cv
7599 .iter()
7600 .zip(vv)
7601 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7602 worst_k = worst_k.max(dk).max(dv);
7603 } else {
7604 eprintln!(
7605 "commit-check L{li}: kv row count mismatch {} vs {}",
7606 ck.len(),
7607 kk.len()
7608 );
7609 }
7610 }
7611 eprintln!(
7612 "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}"
7613 );
7614 }
7615 }
7616 if !metal_native && a + 1 < b {
7617 let expected_gdn_layers = self.graph_gdn_layer_count();
7618 if expected_gdn_layers > 0
7619 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7620 {
7621 self.clear_sequence_state();
7622 self.graph_failed
7623 .store(true, std::sync::atomic::Ordering::Relaxed);
7624 self.cancel
7625 .store(true, std::sync::atomic::Ordering::Relaxed);
7626 tracing::error!("GDN speculative restore failed after verify");
7627 return None;
7628 }
7629 }
7630 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7631 self.clear_sequence_state();
7636 self.graph_failed
7637 .store(true, std::sync::atomic::Ordering::Relaxed);
7638 self.cancel
7639 .store(true, std::sync::atomic::Ordering::Relaxed);
7640 tracing::error!("trunk graph KV rewind failed after speculative verify");
7641 return None;
7642 }
7643 *accepted += a;
7644 if !metal_native {
7655 m.kv.truncate_last(k_spec.saturating_sub(1));
7657 }
7658 spec_stamp("c.trunc");
7659 if !metal_native
7660 && self.mtp_graph_mode == Some(true)
7661 && !self.rewind_mtp_graph_mirror(next_pos)
7662 {
7663 self.clear_sequence_state();
7667 self.graph_failed
7668 .store(true, std::sync::atomic::Ordering::Relaxed);
7669 self.cancel
7670 .store(true, std::sync::atomic::Ordering::Relaxed);
7671 tracing::error!("MTP graph mirror rewind failed after verify commit");
7672 return None;
7673 }
7674 if !warm_off && a > 0 {
7675 let mut warmed = false;
7678 #[cfg(target_os = "macos")]
7679 if metal_native && self.mtp_graph_mode == Some(true) {
7680 warmed = match warm_pending.take() {
7684 Some(p) => self.mtp_warm_batch_finish(m, p),
7685 None => false,
7686 };
7687 if !warmed {
7688 warmed = true;
7689 for j in 0..a {
7690 let row =
7691 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
7692 if self
7693 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
7694 .is_none()
7695 {
7696 warmed = false;
7697 break;
7698 }
7699 }
7700 }
7701 }
7702 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
7703 let rows: Vec<Vec<f32>> = (0..a)
7704 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
7705 .collect();
7706 let pairs: Vec<(&[f32], u32)> = rows
7707 .iter()
7708 .zip(ids.iter())
7709 .map(|(r, &t)| (r.as_slice(), t))
7710 .collect();
7711 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
7712 Ok(()) => warmed = true,
7713 Err(err) => {
7714 tracing::error!("{err}");
7720 self.clear_sequence_state();
7721 self.graph_failed
7722 .store(true, std::sync::atomic::Ordering::Relaxed);
7723 self.cancel
7724 .store(true, std::sync::atomic::Ordering::Relaxed);
7725 return None;
7726 }
7727 }
7728 }
7729 if !warmed {
7730 for j in 0..a {
7731 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
7732 let row = row.to_vec();
7733 self.mtp_warm(m, &row, ids[j], next_pos + j);
7734 }
7735 }
7736 }
7737 spec_stamp("c.warm");
7741 if let Some(c) = forced {
7742 self.spec_forced = Some(c);
7743 self.graph_logits = None;
7744 } else if greedy_dev && logits.is_empty() {
7745 self.spec_forced = Some(ids[a]);
7748 self.graph_logits = None;
7749 } else {
7750 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
7751 row.resize(self.vocab_size, 0.0);
7752 if let Some(c) = self.final_softcap {
7753 for l in row.iter_mut() {
7754 *l = c * (*l / c).tanh();
7755 }
7756 }
7757 self.graph_logits = Some(row);
7758 }
7759 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
7760 spec_stamp("c.row");
7761 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7767 let end = subs();
7768 eprintln!(
7769 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
7770 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
7771 t_draft.as_secs_f64() * 1e3,
7772 sub_draft - sub0,
7773 (t_verify - t_draft).as_secs_f64() * 1e3,
7774 sub_verify - sub_draft,
7775 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
7776 end - sub_verify,
7777 self.draft_full_streak,
7778 );
7779 }
7780 if k_env.is_none() && !metal_native && !k_capped {
7785 let f = a as f32 / k_spec.max(1) as f32;
7789 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
7790 let mut k_next = k_spec;
7791 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
7792 k_next = k_spec + 1;
7793 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
7794 k_next = k_spec - 1;
7795 }
7796 if k_next != k_spec {
7797 self.spec_acc_ewma = 0.6;
7798 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7799 eprintln!("spec-k: {k_spec} → {k_next}");
7800 }
7801 }
7802 self.spec_k_adapt = Some(k_next);
7803 }
7804 spec_stamp("end");
7805 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
7806 }
7807
7808 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
7817 if !self.pair_supported() {
7818 return (0.0, 0.0);
7819 }
7820 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
7827 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7828 let emb1 = self.embed_single(1);
7829 let emb2 = self.embed_single(2);
7830 let pos = self.kv_cache.seq_len();
7831
7832 let t0 = std::time::Instant::now();
7833 for _ in 0..iters {
7834 let _ = self.forward_layers(&emb1, pos, None);
7835 let _ = self.forward_layers(&emb2, pos + 1, None);
7836 for l in &mut self.kv_cache.layers {
7837 l.truncate_last(2);
7838 }
7839 }
7840 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7841
7842 let t1 = std::time::Instant::now();
7843 for _ in 0..iters {
7844 let _ = self.forward_pair(&emb1, &emb2, pos);
7845 for l in &mut self.kv_cache.layers {
7846 l.truncate_last(2);
7847 }
7848 }
7849 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7850 match graph_env {
7851 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
7852 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
7853 }
7854 (singles_ms, pair_ms)
7855 }
7856
7857 fn pair_supported(&self) -> bool {
7865 !self.weights.layers.is_empty()
7872 && self.g3n.is_none()
7873 && !self
7874 .weights
7875 .layers
7876 .iter()
7877 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
7878 }
7879
7880 fn forward_pair(
7881 &mut self,
7882 emb1: &[f32],
7883 emb2: &[f32],
7884 position: usize,
7885 ) -> (Vec<f32>, Vec<f32>) {
7886 self.mimo_moe_prepare();
7889 let mut h1 = emb1.to_vec();
7890 let mut h2 = emb2.to_vec();
7891 let (_nkv, _hd, hs, _rd, eps) = (
7892 self.num_kv_heads,
7893 self.head_dim,
7894 self.hidden_size,
7895 self.rotary_dim,
7896 self.rms_eps,
7897 );
7898 let pool = self.pool.clone();
7899
7900 for li in 0..self.num_layers {
7901 let lw = &self.weights.layers[self.phys_layer(li)];
7902 inference::rms_norm_into(
7905 &h1,
7906 &lw.input_norm,
7907 self.rms_eps,
7908 self.norm_style,
7909 &mut self.ws.n1,
7910 );
7911 inference::rms_norm_into(
7912 &h2,
7913 &lw.input_norm,
7914 self.rms_eps,
7915 self.norm_style,
7916 &mut self.ws.n2,
7917 );
7918
7919 let (a1, a2) = match &lw.attn {
7920 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7921 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7922 AttnKind::Bounded(w) => {
7923 let rope = self
7926 .bounded_rope
7927 .clone()
7928 .expect("bounded layer without an installed rotation table");
7929 let cfg = crate::bounded::BoundedAttnCfg {
7930 num_heads: self.num_heads,
7931 num_kv_heads: self.num_kv_heads,
7932 head_dim: self.head_dim,
7933 hidden_size: hs,
7934 scale: self.attn_scale,
7935 rope: &rope,
7936 pool: pool.as_deref(),
7937 };
7938 let a1 = crate::bounded::bounded_attention(
7939 &self.ws.n1,
7940 w,
7941 &mut self.kv_cache.layers[li],
7942 &cfg,
7943 );
7944 let a2 = crate::bounded::bounded_attention(
7945 &self.ws.n2,
7946 w,
7947 &mut self.kv_cache.layers[li],
7948 &cfg,
7949 );
7950 (a1, a2)
7951 }
7952 AttnKind::Linear(w) => {
7953 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
7954 let layer = &mut self.kv_cache.layers[li];
7955 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7956 vmf_phase_pair(
7957 &self.ws.n1,
7958 &self.ws.n2,
7959 w,
7960 &cfg,
7961 state,
7962 scratch,
7963 self.pool.as_deref(),
7964 )
7965 }
7966 AttnKind::LinearGdn(w) => {
7967 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7968 let layer = &mut self.kv_cache.layers[li];
7969 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7970 gdn_pair(
7971 &self.ws.n1,
7972 &self.ws.n2,
7973 w,
7974 &cfg,
7975 state,
7976 scratch,
7977 self.pool.as_deref(),
7978 )
7979 }
7980 AttnKind::ShortConv(w) => {
7981 let cfg = self
7982 .short_conv_cfg
7983 .expect("short-conv layer without short_conv_cfg");
7984 let layer = &mut self.kv_cache.layers[li];
7985 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7986 short_conv_pair(
7987 &self.ws.n1,
7988 &self.ws.n2,
7989 w,
7990 &cfg,
7991 state,
7992 scratch,
7993 self.pool.as_deref(),
7994 )
7995 }
7996 AttnKind::Full {
7997 wq,
7998 wk,
7999 wv,
8000 wo,
8001 q_norm,
8002 k_norm,
8003 output_gate,
8004 softplus_gate,
8005 bias,
8006 } => {
8007 let inv_freq_l = self.layer_inv_freq(li);
8008 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
8009 let cfg = QwenAttnCfg {
8010 num_heads: self.layer_num_heads(li),
8011 num_kv_heads: nkv_l,
8012 head_dim: hd_l,
8013 hidden_size: hs,
8014 position,
8015 inv_freq: &inv_freq_l,
8016 rotary_dim: rd_l,
8017 scale: self.attn_scale,
8018 softcap: self.attn_softcap,
8019 window: self.layer_window(li),
8020 v_norm: self.attn_v_norm,
8021 qk_norm_after_rope: self.qk_norm_after_rope,
8022 gate_sigmoid: self.proj_gate_sigmoid,
8023 q_norm: q_norm.as_deref(),
8024 k_norm: k_norm.as_deref(),
8025 output_gate: *output_gate,
8026 softplus_gate: softplus_gate
8027 .as_ref()
8028 .map(|(gate, per_head)| (gate, *per_head)),
8029 rope_scale: self.layer_rope_scale(li),
8030 bias: bias
8031 .as_ref()
8032 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
8033 rms_eps: eps,
8034 norm_style: self.norm_style,
8035 pool: pool.as_deref(),
8036 v_head_dim: self.layer_v_dim(li),
8037 };
8038 attention::qwen_attention_pair(
8039 &self.ws.n1,
8040 &self.ws.n2,
8041 wq,
8042 wk,
8043 wv,
8044 wo,
8045 &mut self.kv_cache.layers[li],
8046 &cfg,
8047 )
8048 }
8049 };
8050 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
8051 Some(w) => (
8052 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
8053 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
8054 ),
8055 None => (a1, a2),
8056 };
8057 for i in 0..self.hidden_size {
8058 h1[i] += a1[i];
8059 h2[i] += a2[i];
8060 }
8061 let (mut a1, mut a2) = (a1, a2);
8062 attention::recycle_buf(&mut a1);
8063 attention::recycle_buf(&mut a2);
8064
8065 let lw = &self.weights.layers[self.phys_layer(li)];
8066 inference::rms_norm_into(
8067 &h1,
8068 &lw.post_norm,
8069 self.rms_eps,
8070 self.norm_style,
8071 &mut self.ws.p1,
8072 );
8073 inference::rms_norm_into(
8074 &h2,
8075 &lw.post_norm,
8076 self.rms_eps,
8077 self.norm_style,
8078 &mut self.ws.p2,
8079 );
8080 let (f1, f2) = match &lw.ffn {
8081 FfnKind::DenseMoe(dm) => (
8084 dense_moe_ffn(
8085 dm,
8086 &self.ws.p1,
8087 &h1,
8088 self.rms_eps,
8089 self.norm_style,
8090 self.pool.as_deref(),
8091 ),
8092 dense_moe_ffn(
8093 dm,
8094 &self.ws.p2,
8095 &h2,
8096 self.rms_eps,
8097 self.norm_style,
8098 self.pool.as_deref(),
8099 ),
8100 ),
8101 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
8102 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
8103 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
8104 ),
8105 _ => ffn_forward_pair(
8106 &lw.ffn,
8107 &self.ws.p1,
8108 &self.ws.p2,
8109 self.pool.as_deref(),
8110 None,
8111 ),
8112 };
8113 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
8114 Some(w) => (
8115 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
8116 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
8117 ),
8118 None => (f1, f2),
8119 };
8120 for i in 0..self.hidden_size {
8121 h1[i] += f1[i];
8122 h2[i] += f2[i];
8123 }
8124 let (mut f1, mut f2) = (f1, f2);
8125 attention::recycle_buf(&mut f1);
8126 attention::recycle_buf(&mut f2);
8127 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
8128 for i in 0..self.hidden_size {
8129 h1[i] *= sc;
8130 h2[i] *= sc;
8131 }
8132 }
8133 if self.is_loop_end(li) && li + 1 < self.num_layers {
8135 h1 = inference::rms_norm(
8136 &h1,
8137 &self.weights.final_norm,
8138 self.rms_eps,
8139 self.norm_style,
8140 );
8141 h2 = inference::rms_norm(
8142 &h2,
8143 &self.weights.final_norm,
8144 self.rms_eps,
8145 self.norm_style,
8146 );
8147 }
8148 }
8149 if self.o1_active() {
8155 self.commit_linear_scratch();
8156 }
8157 self.o1_progress();
8158 self.swa_trim_tails();
8159 (h1, h2)
8160 }
8161
8162 fn commit_linear_scratch(&mut self) {
8164 for layer in &mut self.kv_cache.layers {
8165 if !layer.linear_scratch.is_empty() {
8166 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
8167 layer.linear_scratch.clear();
8168 }
8169 }
8170 }
8171
8172 pub fn forward_ids(
8175 &mut self,
8176 ids: &[u32],
8177 task_mask: Option<&TaskMask>,
8178 ) -> Result<Vec<f32>, String> {
8179 #[cfg(target_os = "macos")]
8180 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8181 if ids.is_empty() {
8182 return Err("empty id sequence".to_string());
8183 }
8184 self.clear_sequence_state();
8185 self.check_forward_graph("forward_ids setup", 0)?;
8186 if task_mask.is_none() {
8187 self.o1_begin();
8188 }
8189 let mut hidden = vec![0.0f32; self.hidden_size];
8190 let mut pos = 0usize;
8191 if let Some(b) = &mut self.dsv41 {
8192 let pool = self.pool.clone();
8193 let mut logits = Vec::new();
8194 crate::dsv41::forward_chunk(
8195 &b.0,
8196 &b.1,
8197 &b.2,
8198 &mut b.3,
8199 ids,
8200 0,
8201 pool.as_deref(),
8202 &mut logits,
8203 );
8204 if let Err(err) = self.o1_seal_checked() {
8205 self.clear_sequence_state();
8206 return Err(err);
8207 }
8208 return Ok(logits);
8209 }
8210 #[cfg(not(target_os = "macos"))]
8221 if task_mask.is_none() && ids.len() > 2 && self.g4_batch_prefill() {
8222 let (n, last) = match self.g4_batch_ingest(ids, 0, false) {
8223 Ok(r) => r,
8224 Err(e) => {
8225 self.clear_sequence_state();
8226 return Err(e);
8227 }
8228 };
8229 if n > 0 {
8230 hidden.copy_from_slice(&last);
8231 pos = n;
8232 }
8233 }
8234 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
8235 let chunk = self.prefill_chunk();
8239 let hs = self.hidden_size;
8240 while pos < ids.len() {
8241 let end = (pos + chunk).min(ids.len());
8242 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8243 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
8244 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
8245 pos = end;
8246 }
8247 }
8248 if task_mask.is_none()
8257 && !self.graph_prefill_preferred()
8258 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
8259 && self.pair_supported()
8260 {
8261 while pos + 1 < ids.len() {
8262 let e1 = self.embed_single(ids[pos]);
8263 let e2 = self.embed_single(ids[pos + 1]);
8264 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8265 self.check_forward_graph("forward_ids pair", pos + 1)?;
8266 self.commit_linear_scratch();
8267 hidden = h2;
8268 pos += 2;
8269 }
8270 }
8271 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8274 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8275 self.graph_logits = Some(lg);
8276 hidden = vec![0.0; self.hidden_size];
8277 pos = ids.len();
8278 }
8279 }
8280 while pos < ids.len() {
8281 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8282 self.check_forward_graph("forward_ids", pos)?;
8283 pos += 1;
8284 }
8285 if let Some(logits) = self.graph_logits.take() {
8286 if let Err(err) = self.o1_seal_checked() {
8290 self.clear_sequence_state();
8291 return Err(err);
8292 }
8293 return Ok(logits);
8294 }
8295 if let Err(err) = self.o1_seal_checked() {
8299 self.clear_sequence_state();
8300 return Err(err);
8301 }
8302 let normed = inference::rms_norm(
8303 &hidden,
8304 &self.weights.final_norm,
8305 self.rms_eps,
8306 self.norm_style,
8307 );
8308 Ok(self.lm_head_forward(&normed))
8309 }
8310
8311 #[doc(hidden)]
8315 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8316 #[cfg(target_os = "macos")]
8317 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8318 if ids.is_empty() {
8319 return Err("empty id sequence".to_string());
8320 }
8321 self.clear_sequence_state();
8322 self.dsv41
8323 .as_ref()
8324 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8325 self.o1_begin();
8326 let rows = {
8327 let pool = self.pool.clone();
8328 let b = self
8329 .dsv41
8330 .as_mut()
8331 .expect("dsv41 checked above; state cannot change during forward");
8332 let mut rows = Vec::with_capacity(ids.len());
8333 for (position, &id) in ids.iter().enumerate() {
8334 let mut logits = Vec::new();
8335 crate::dsv41::forward_token(
8336 &b.0,
8337 &b.1,
8338 &b.2,
8339 &mut b.3,
8340 id,
8341 position,
8342 pool.as_deref(),
8343 &mut logits,
8344 );
8345 rows.push(logits);
8346 }
8347 rows
8348 };
8349 self.o1_seal();
8350 Ok(rows)
8351 }
8352
8353 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8360 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8361 Ok((nll / cnt.max(1) as f64).exp())
8362 }
8363
8364 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8369 self.clear_sequence_state();
8370 FFN_PROBE.with(|p| {
8371 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8372 });
8373 crate::gpu::cpu_scope(|| {
8374 for (pos, &id) in ids.iter().enumerate() {
8375 let emb = self.embed_single(id);
8376 let _ = self.forward_layers(&emb, pos, None);
8377 }
8378 });
8379 self.clear_sequence_state();
8380 FFN_PROBE
8381 .with(|p| p.borrow_mut().take())
8382 .unwrap_or_default()
8383 }
8384
8385 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8389 if let Err(err) = self.nll_begin() {
8390 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8394 self.nll_end();
8395 return Err(err);
8396 }
8397 FFN_PROBE.with(|p| {
8398 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8399 });
8400 let result: Result<(), String> = (|| {
8401 for chunk in ids.chunks(256) {
8402 if chunk.len() < 2 {
8403 continue;
8404 }
8405 self.nll_ids_masked(chunk, 0, None)?;
8406 }
8407 Ok(())
8408 })();
8409 self.nll_end();
8410 let probe = FFN_PROBE
8411 .with(|p| p.borrow_mut().take())
8412 .unwrap_or_default();
8413 match result {
8414 Ok(()) => Ok(probe),
8415 Err(err) => {
8416 drop(probe);
8417 Err(err)
8418 }
8419 }
8420 }
8421
8422 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8426 self.nll_begin()?;
8427 let result: Result<f64, String> = (|| {
8428 let mut nll = 0f64;
8429 let mut cnt = 0usize;
8430 let mut hidden = vec![0f32; self.hidden_size];
8431 for (pos, &id) in ids.iter().enumerate() {
8432 if pos > 0 {
8433 inference::rms_norm_into(
8434 &hidden,
8435 &self.weights.final_norm,
8436 self.rms_eps,
8437 self.norm_style,
8438 &mut self.ws.n1,
8439 );
8440 let mut logits = self.lm_head_forward(&self.ws.n1);
8441 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8442 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8443 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8444 nll -= p.max(1e-300).ln();
8445 cnt += 1;
8446 attention::recycle_buf(&mut logits);
8447 }
8448 let emb = self.embed_single(id);
8449 hidden = self.forward_layers(&emb, pos, Some(mask));
8450 self.nll_check_graph("masked serial forward", pos)?;
8451 let _ = self.graph_logits.take();
8455 }
8456 Ok((nll / cnt.max(1) as f64).exp())
8457 })();
8458 self.nll_end();
8459 result
8460 }
8461
8462 pub fn nll_ids_masked(
8481 &mut self,
8482 ids: &[u32],
8483 start: usize,
8484 task_mask: Option<&TaskMask>,
8485 ) -> Result<(f64, usize), String> {
8486 let task_mask = self.drop_open_mask(task_mask);
8487 self.nll_ids_inner(ids, start, task_mask)
8488 }
8489
8490 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8491 self.nll_ids_inner(ids, start, None)
8492 }
8493
8494 fn nll_ids_inner(
8495 &mut self,
8496 ids: &[u32],
8497 start: usize,
8498 task_mask: Option<&TaskMask>,
8499 ) -> Result<(f64, usize), String> {
8500 self.nll_begin()?;
8501 let result: Result<(f64, usize), String> = (|| {
8502 let mut nll = 0f64;
8503 let mut cnt = 0usize;
8504 let (graph_quality, fused_head_quality) = nll_graph_policy(
8517 task_mask.is_none(),
8518 self.graph_prefill_preferred(),
8519 crate::gpu::q1_force(),
8520 );
8521 self.graph_head_required = fused_head_quality;
8522 self.graph_want_logits = fused_head_quality;
8523 #[cfg(target_os = "macos")]
8524 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8525 match self.nll_batch_metal(ids, start) {
8526 MetalBatchNllOutcome::Completed(nll, count) => {
8527 return Ok((nll, count));
8528 }
8529 MetalBatchNllOutcome::Declined => {}
8530 MetalBatchNllOutcome::Failed(err) => return Err(err),
8531 }
8532 }
8533 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8538 if self.can_prefill_batched() && !graph_quality && !force_serial {
8539 const CHUNK: usize = 128;
8545 const LM_SUB: usize = 32;
8546 let n = ids.len().saturating_sub(1);
8547 let hs = self.hidden_size;
8548 let rows = self.weights.lm_head.rows();
8549 let mut pos = 0usize;
8550 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8551 while pos < n {
8552 let end = (pos + CHUNK).min(n);
8553 let bsz = end - pos;
8554 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8555 self.nll_check_graph("batched prefill", pos)?;
8556 if state_trace && end % 256 == 0 {
8557 self.trace_recurrent_state(end);
8558 }
8559 let mut k0 = 0usize;
8560 while k0 < bsz {
8561 let k1 = (k0 + LM_SUB).min(bsz);
8562 let sb = k1 - k0;
8563 if pos + k1 <= start {
8566 k0 = k1;
8567 continue;
8568 }
8569 let mut normed = vec![0.0f32; sb * hs];
8570 for k in 0..sb {
8571 let r = inference::rms_norm(
8572 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8573 &self.weights.final_norm,
8574 self.rms_eps,
8575 self.norm_style,
8576 );
8577 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8578 }
8579 let mut logits = vec![0.0f32; sb * rows];
8580 self.weights
8581 .lm_head
8582 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8583 for k in 0..sb {
8584 if pos + k0 + k < start {
8585 continue;
8586 }
8587 self.nll_check_graph("batched score row", pos + k0 + k)?;
8588 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8589 if let Some(mu) = self.logit_multiplier {
8590 for v in lg.iter_mut() {
8591 *v *= mu;
8592 }
8593 }
8594 if let Some(c) = self.final_softcap {
8598 softcap_in_place(lg, c, self.pool.as_deref());
8599 }
8600 if let Some(cm) = self.head_clusters.clone() {
8603 self.hierarchical_head_logprobs(
8604 &normed[k * hs..(k + 1) * hs],
8605 &cm,
8606 lg,
8607 );
8608 }
8609 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8610 let target = ids[pos + k0 + k + 1] as usize;
8611 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8612 let lse: f64 = lg
8613 .iter()
8614 .map(|&v| ((v - max) as f64).exp())
8615 .sum::<f64>()
8616 .ln()
8617 + max as f64;
8618 nll += lse - lg[target] as f64;
8619 cnt += 1;
8620 if std::env::var("CMF_PPL_TRACE").is_ok() {
8621 let top = lg
8622 .iter()
8623 .enumerate()
8624 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8625 .map(|(i, _)| i)
8626 .unwrap_or(0);
8627 eprintln!(
8628 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
8629 pos + k0 + k,
8630 target,
8631 lse - lg[target] as f64,
8632 top,
8633 lg[target],
8634 lg[top]
8635 );
8636 }
8637 }
8638 k0 = k1;
8639 }
8640 pos = end;
8641 }
8642 return Ok((nll, cnt));
8643 }
8644 for pos in 0..ids.len().saturating_sub(1) {
8645 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8646 self.nll_check_graph("serial forward", pos)?;
8647 let out_of_band = self.graph_logits.take();
8655 if self.graph_head_required && out_of_band.is_none() {
8656 METAL_GRAPH_HEAD_MISS.fetch_add(
8657 1,
8658 std::sync::atomic::Ordering::Relaxed,
8659 );
8660 return Err(format!(
8661 "fused Metal graph head did not complete at NLL position {pos}"
8662 ));
8663 }
8664 if pos < start {
8665 continue;
8666 }
8667 let logits = match out_of_band {
8668 Some(lg) => lg,
8669 None => {
8670 let normed = inference::rms_norm(
8671 &hidden,
8672 &self.weights.final_norm,
8673 self.rms_eps,
8674 self.norm_style,
8675 );
8676 self.lm_head_forward(&normed)
8680 }
8681 };
8682 let target = ids[pos + 1] as usize;
8683 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8684 let lse: f64 = logits
8685 .iter()
8686 .map(|&v| ((v - max) as f64).exp())
8687 .sum::<f64>()
8688 .ln()
8689 + max as f64;
8690 let tok_nll = lse - logits[target] as f64;
8691 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8692 let top = logits
8693 .iter()
8694 .enumerate()
8695 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8696 .map(|(i, _)| i)
8697 .unwrap_or(0);
8698 eprintln!(
8699 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8700 logits[target], logits[top]
8701 );
8702 }
8703 nll += tok_nll;
8704 cnt += 1;
8705 }
8706 Ok((nll, cnt))
8707 })();
8708 self.nll_end();
8709 result
8710 }
8711
8712 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
8717 let normed = inference::rms_norm(
8718 hidden,
8719 &self.weights.final_norm,
8720 self.rms_eps,
8721 self.norm_style,
8722 );
8723 let mut logits = self.lm_head_forward(&normed);
8726 let target = target as usize;
8727 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8728 let lse: f64 = logits
8729 .iter()
8730 .map(|&v| ((v - max) as f64).exp())
8731 .sum::<f64>()
8732 .ln()
8733 + max as f64;
8734 let tok_nll = lse - logits[target] as f64;
8735 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8736 let top = logits
8737 .iter()
8738 .enumerate()
8739 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8740 .map(|(i, _)| i)
8741 .unwrap_or(0);
8742 eprintln!(
8743 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8744 logits[target], logits[top]
8745 );
8746 }
8747 attention::recycle_buf(&mut logits);
8748 tok_nll
8749 }
8750
8751 fn trace_recurrent_state(&self, pos: usize) {
8759 let stats = |v: &[f32]| -> (f64, f64) {
8760 if v.is_empty() {
8761 return (0.0, 0.0);
8762 }
8763 let (mut ss, mut mx) = (0f64, 0f64);
8764 for &x in v {
8765 ss += (x as f64) * (x as f64);
8766 mx = mx.max((x as f64).abs());
8767 }
8768 ((ss / v.len() as f64).sqrt(), mx)
8769 };
8770 for (li, l) in self.kv_cache.layers.iter().enumerate() {
8771 let lw = &self.weights.layers[self.phys_layer(li)];
8772 let (kind, s_len) = match &lw.attn {
8773 AttnKind::Linear(_) => (
8774 "vmf",
8775 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
8776 ),
8777 AttnKind::LinearGdn(_) => (
8778 "gdn",
8779 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
8780 ),
8781 AttnKind::Bounded(_) => ("bounded", 0),
8782 AttnKind::Full { .. } => ("full", 0),
8783 _ => ("other", 0),
8784 };
8785 let (rms, max) = stats(&l.linear_state);
8786 let s_part = if kind == "vmf" {
8787 &l.linear_state[..s_len.min(l.linear_state.len())]
8788 } else if kind == "gdn" {
8789 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
8790 &l.linear_state[ring..]
8791 } else {
8792 &l.linear_state[..0]
8793 };
8794 let (s_rms, s_max) = stats(s_part);
8795 let (ring_rms, ring_len) = match &l.bounded {
8796 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
8797 None => (0.0, 0),
8798 };
8799 eprintln!(
8800 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
8801 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
8802 l.linear_state.len(),
8803 l.seq_len
8804 );
8805 }
8806 }
8807
8808 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
8826 self.nll_begin()?;
8831 let requested_prefix = (prefill > 0).then_some(prefill);
8832 self.o1_begin_with_prefix(requested_prefix);
8833 let n = ids.len().saturating_sub(1);
8834 let requested_start = prefill.min(n);
8835 let exact_end = if self.o1_active() {
8840 match requested_prefix {
8841 Some(requested) => self.o1_effective_boundary(requested),
8842 None => self
8843 .o1_cfg
8844 .as_ref()
8845 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
8846 }
8847 .unwrap_or(requested_start)
8848 .min(n)
8849 } else {
8850 requested_start
8851 };
8852 let mut nll = 0f64;
8853 let mut cnt = 0usize;
8854
8855 let mut pos = 0usize;
8859 if self.can_prefill_batched() {
8860 const CHUNK: usize = 128;
8861 while pos < exact_end {
8862 let end = (pos + CHUNK).min(exact_end);
8863 let hiddens = self.prefill_batch(&ids[pos..end], pos);
8864 if self
8865 .graph_failed
8866 .swap(false, std::sync::atomic::Ordering::Relaxed)
8867 {
8868 self.cancel
8869 .store(false, std::sync::atomic::Ordering::Relaxed);
8870 self.nll_end();
8871 return Err("GPU graph failed during O(1) NLL prefix".into());
8872 }
8873 for row in 0..end - pos {
8874 let score_pos = pos + row;
8875 if score_pos >= requested_start && score_pos < n {
8876 nll += self.nll_from_hidden(
8877 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
8878 ids[score_pos + 1],
8879 score_pos,
8880 );
8881 cnt += 1;
8882 }
8883 }
8884 pos = end;
8885 }
8886 } else {
8887 while pos < exact_end {
8888 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
8889 if self
8890 .graph_failed
8891 .swap(false, std::sync::atomic::Ordering::Relaxed)
8892 {
8893 self.cancel
8894 .store(false, std::sync::atomic::Ordering::Relaxed);
8895 self.nll_end();
8896 return Err("GPU graph failed during O(1) NLL prefix".into());
8897 }
8898 if pos >= requested_start && pos < n {
8899 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
8900 cnt += 1;
8901 }
8902 pos += 1;
8903 }
8904 }
8905 self.o1_seal_checked().map_err(|err| {
8906 self.nll_end();
8907 err
8908 })?;
8909
8910 let batch_k = std::env::var("CMF_BATCH_K")
8919 .ok()
8920 .and_then(|v| v.parse::<usize>().ok())
8921 .unwrap_or(0);
8922 let batch_admitted = batch_k > 0
8923 && self.can_prefill_batched()
8924 && self.o1_active()
8925 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
8926 && (0..self.num_layers).all(|li| {
8927 let cache = &self.kv_cache.layers[self.phys_layer(li)];
8928 cache.o1.is_none() || cache.o1_views().is_some()
8929 });
8930 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8931 eprintln!(
8932 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
8933 batch_admitted,
8934 batch_k,
8935 n.saturating_sub(exact_end),
8936 );
8937 }
8938 let mut batch_completed = false;
8939 if batch_admitted && exact_end < n {
8940 let hs = self.hidden_size;
8941 let mut batch_pos = exact_end;
8942 while batch_pos < n {
8943 let end = (batch_pos + batch_k).min(n);
8944 let bk = end - batch_pos;
8945 let mut hiddens = vec![0.0f32; bk * hs];
8946 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
8947 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
8948 }
8949 let positions: Vec<usize> = (batch_pos..end).collect();
8950 let t_batch = std::time::Instant::now();
8951 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
8952 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8953 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
8954 eprintln!(
8955 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
8956 batch_pos,
8957 end.saturating_sub(1),
8958 bk as f64 / (ms / 1000.0),
8959 );
8960 }
8961 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
8962 self.nll_end();
8963 return Err(err);
8964 }
8965 match outcome {
8966 crate::gpu::BatchGraphOutcome::Completed => {
8967 batch_completed = true;
8968 for row in 0..bk {
8969 nll += self.nll_from_hidden(
8970 &hiddens[row * hs..(row + 1) * hs],
8971 ids[batch_pos + row + 1],
8972 batch_pos + row,
8973 );
8974 cnt += 1;
8975 }
8976 batch_pos = end;
8977 }
8978 crate::gpu::BatchGraphOutcome::Declined => {
8979 if batch_completed {
8980 self.nll_end();
8981 return Err(format!(
8982 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
8983 ));
8984 }
8985 break;
8986 }
8987 crate::gpu::BatchGraphOutcome::Failed => {
8988 self.nll_end();
8989 return Err(format!(
8990 "O(1) NLL batch graph failed after admission at position {batch_pos}"
8991 ));
8992 }
8993 }
8994 }
8995 if batch_completed && cnt == n.saturating_sub(requested_start) {
8996 self.nll_end();
8997 return Ok((nll, cnt));
8998 }
8999 }
9000
9001 for pos in exact_end..n {
9006 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9007 if self
9008 .graph_failed
9009 .swap(false, std::sync::atomic::Ordering::Relaxed)
9010 {
9011 self.cancel
9012 .store(false, std::sync::atomic::Ordering::Relaxed);
9013 self.nll_end();
9014 return Err(format!(
9015 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
9016 ));
9017 }
9018 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
9019 cnt += 1;
9020 }
9021 self.nll_end();
9022 Ok((nll, cnt))
9023 }
9024
9025 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
9033 self.clear_sequence_state();
9034 let n = ids.len().saturating_sub(1);
9035 let mut correct = Vec::with_capacity(n);
9036 let mut pmax = Vec::with_capacity(n);
9037 for pos in 0..n {
9038 let emb = self.embed_single(ids[pos]);
9039 let hidden = self.forward_layers(&emb, pos, None);
9040 let logits = if let Some(logits) = self.graph_logits.take() {
9041 logits
9042 } else {
9043 let normed = inference::rms_norm(
9044 &hidden,
9045 &self.weights.final_norm,
9046 self.rms_eps,
9047 self.norm_style,
9048 );
9049 self.lm_head_forward(&normed)
9053 };
9054 let target = ids[pos + 1] as usize;
9055 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
9056 for (i, &v) in logits.iter().enumerate() {
9057 if v > mval {
9058 mval = v;
9059 amax = i;
9060 }
9061 }
9062 correct.push(amax == target);
9063 let row: Vec<f32> = temps
9064 .iter()
9065 .map(|&t| {
9066 let tt = t.max(1e-3);
9067 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
9068 1.0 / s.max(1e-12) })
9070 .collect();
9071 pmax.push(row);
9072 }
9073 self.clear_sequence_state();
9074 (correct, pmax)
9075 }
9076
9077 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
9084 if self.dyn_router.is_none() {
9085 return Ok((self.ppl_ids(ids)?, 0));
9086 }
9087 self.nll_begin()?;
9088 let saved_active = self.dyn_active;
9089 let mut router = self
9090 .dyn_router
9091 .take()
9092 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
9093 router.reset();
9094 self.dyn_phi_seen = 0;
9095 let _ = self.set_active_skill(None);
9096
9097 let result: Result<(f64, usize), String> = (|| {
9098 let mut nll = 0f64;
9099 let mut cnt = 0usize;
9100 for pos in 0..ids.len().saturating_sub(1) {
9101 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
9102 self.nll_check_graph("dynamic serial forward", pos)?;
9103 let out_of_band = self.graph_logits.take();
9104 let mut logits = match out_of_band {
9105 Some(lg) => lg,
9106 None => {
9107 let normed = inference::rms_norm(
9108 &hidden,
9109 &self.weights.final_norm,
9110 self.rms_eps,
9111 self.norm_style,
9112 );
9113 self.lm_head_forward(&normed)
9117 }
9118 };
9119 let target = ids[pos + 1] as usize;
9120 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
9121 let lse: f64 = logits
9122 .iter()
9123 .map(|&v| ((v - max) as f64).exp())
9124 .sum::<f64>()
9125 .ln()
9126 + max as f64;
9127 let tok_nll = lse - logits[target] as f64;
9128 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
9129 let top = logits
9130 .iter()
9131 .enumerate()
9132 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
9133 .map(|(i, _)| i)
9134 .unwrap_or(0);
9135 eprintln!(
9136 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
9137 logits[target], logits[top]
9138 );
9139 }
9140 nll += tok_nll;
9141 cnt += 1;
9142 attention::recycle_buf(&mut logits);
9143 let phi = self.dyn_phi_ema.clone();
9145 if let Some(new_active) = router.step(&phi, pos) {
9146 let _ = self.set_active_skill(new_active);
9147 }
9148 }
9149 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
9150 })();
9151
9152 let _ = self.set_active_skill(saved_active);
9155 self.dyn_router = Some(router);
9156 self.nll_end();
9157 result
9158 }
9159
9160 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
9162 self.clear_sequence_state();
9163 let mut acc = vec![0f32; self.hidden_size];
9164 for (pos, &id) in ids.iter().enumerate() {
9165 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9166 for (a, v) in acc.iter_mut().zip(&h) {
9167 *a += v;
9168 }
9169 }
9170 let n = ids.len().max(1) as f32;
9171 for a in acc.iter_mut() {
9172 *a /= n;
9173 }
9174 self.clear_sequence_state();
9175 acc
9176 }
9177
9178 pub fn probe_phi_span(
9192 &mut self,
9193 ids: &[u32],
9194 layer: usize,
9195 span: std::ops::Range<usize>,
9196 ) -> Vec<f32> {
9197 #[cfg(target_os = "macos")]
9198 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9199 let end = span.end.min(ids.len());
9200 let start = span.start.min(end);
9201 let reset = |p: &mut Self| p.clear_sequence_state();
9202 reset(self);
9203 let mut acc = vec![0f32; self.hidden_size];
9204 for (pos, &id) in ids[..end].iter().enumerate() {
9205 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
9206 if pos >= start {
9207 for (a, v) in acc.iter_mut().zip(&h) {
9208 *a += v;
9209 }
9210 }
9211 }
9212 let n = end - start;
9213 if n > 0 {
9214 let n = n as f32;
9215 for a in acc.iter_mut() {
9216 *a /= n;
9217 }
9218 }
9219 reset(self);
9220 acc
9221 }
9222
9223 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
9230 #[cfg(target_os = "macos")]
9231 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
9232 self.graph_logits = None;
9233 let hidden = self.forward_layers(&self.embed_single(token), position, None);
9234 if let Some(logits) = self.graph_logits.take() {
9235 return logits;
9236 }
9237 inference::rms_norm_into(
9238 &hidden,
9239 &self.weights.final_norm,
9240 self.rms_eps,
9241 self.norm_style,
9242 &mut self.ws.n1,
9243 );
9244 self.lm_head_forward(&self.ws.n1)
9245 }
9246
9247 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
9253 self.prefill_batch_masked(ids, start_pos, None)
9254 }
9255
9256 fn prefill_batch_masked(
9262 &mut self,
9263 ids: &[u32],
9264 start_pos: usize,
9265 task_mask: Option<&TaskMask>,
9266 ) -> Vec<f32> {
9267 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9268 }
9269
9270 fn prefill_rows(
9278 &mut self,
9279 ids: &[u32],
9280 pos: usize,
9281 task_mask: Option<&TaskMask>,
9282 ) -> Result<Vec<f32>, String> {
9283 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9284 }
9285
9286 fn prefill_input_rows(
9287 &mut self,
9288 input: PrefillIn<'_>,
9289 pos: usize,
9290 task_mask: Option<&TaskMask>,
9291 ) -> Result<Vec<f32>, String> {
9292 self.mimo_moe_prepare();
9293 let hs = self.hidden_size;
9294 let bk = match input {
9295 PrefillIn::Ids(ids) => ids.len(),
9296 PrefillIn::Hidden(rows) => rows.len() / hs,
9297 };
9298 #[cfg(not(target_os = "macos"))]
9299 if task_mask.is_none()
9300 && !self.o1_active()
9301 && bk > 1
9302 && (self.batch_prefix_prefill()
9303 || (self.verify_exact_moe
9304 && crate::gpu::enabled_here()
9305 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9306 {
9307 let mut hiddens = match input {
9308 PrefillIn::Hidden(rows) => rows.to_vec(),
9309 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9310 };
9311 let positions: Vec<usize> = (pos..pos + bk).collect();
9312 let mut run = 0usize;
9313 match self.try_batch_graph_wgpu_prefix(
9314 &mut hiddens,
9315 &positions,
9316 bk,
9317 None,
9318 Some(&mut run),
9319 ) {
9320 crate::gpu::BatchGraphOutcome::Completed => {
9321 let out = if run < self.num_layers {
9322 self.prefill_batch_span(
9323 PrefillIn::Hidden(&hiddens),
9324 pos,
9325 None,
9326 run,
9327 self.num_layers,
9328 )
9329 } else {
9330 hiddens
9331 };
9332 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9333 Err("MiMo attention graph failed after admission".into())
9334 } else { Ok(out) };
9335 }
9336 crate::gpu::BatchGraphOutcome::Failed => {
9337 return Err("batched prefix prefill failed after admission".into());
9338 }
9339 crate::gpu::BatchGraphOutcome::Declined => {
9340 #[cfg(feature = "gpu")]
9342 self.pull_lagging_host_kv(0, self.num_layers, pos);
9343 }
9344 }
9345 }
9346 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9347 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9348 Err("batch tail graph failed after admission".into())
9349 } else { Ok(out) }
9350 }
9351
9352 fn prefill_batch_span(
9358 &mut self,
9359 input: PrefillIn<'_>,
9360 start_pos: usize,
9361 task_mask: Option<&TaskMask>,
9362 from: usize,
9363 upto_excl: usize,
9364 ) -> Vec<f32> {
9365 let hs = self.hidden_size;
9366 let b = match input {
9367 PrefillIn::Ids(ids) => ids.len(),
9368 PrefillIn::Hidden(hb) => hb.len() / hs,
9369 };
9370 let upto_excl = upto_excl.min(self.num_layers);
9371 let mut h: Vec<f32>;
9375 let mut h_ready;
9376 match input {
9377 PrefillIn::Ids(_) => {
9378 h = vec![0.0; b * hs];
9379 h_ready = false;
9380 }
9381 PrefillIn::Hidden(hb) => {
9382 h = hb.to_vec();
9383 h_ready = true;
9384 }
9385 }
9386 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9387 if let PrefillIn::Ids(ids) = input {
9388 for (bi, &id) in ids.iter().enumerate() {
9389 let e = me.embed_single(id);
9390 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9391 }
9392 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9393 if let Ok(t) = tp.parse::<usize>() {
9394 if t >= start_pos && t < start_pos + ids.len() {
9395 let bi = t - start_pos;
9396 let row = &h[bi * hs..(bi + 1) * hs];
9397 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9398 eprintln!(
9399 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9400 ids[bi],
9401 row[0],
9402 row[1],
9403 ids.len(),
9404 &ids[..ids.len().min(8)]
9405 );
9406 }
9407 }
9408 }
9409 }
9410 };
9411 let (_nkv, _hd, _rd, eps) = (
9412 self.num_kv_heads,
9413 self.head_dim,
9414 self.rotary_dim,
9415 self.rms_eps,
9416 );
9417 let pool = self.pool.clone();
9418 let norm_style = self.norm_style;
9419 self.mimo_moe_prepare();
9420 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9421
9422 #[cfg(target_os = "macos")]
9423 let mut chunk_skip_until = 0usize;
9424 for li in from..upto_excl {
9425 let _capacity_tail = automatic_gpu_prefix
9426 .filter(|&prefix| {
9427 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9428 })
9429 .map(|_| crate::gpu::enter_cpu_scope());
9430 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9435 let _fast_gemm = self
9436 .proj_gate_sigmoid
9437 .then(crate::gpu::enter_prefill_fast_gemm);
9438 #[cfg(target_os = "macos")]
9444 if task_mask.is_none() {
9445 if li < chunk_skip_until {
9446 continue;
9447 }
9448 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9454 fill_h(&mut h, self);
9455 h_ready = true;
9456 }
9457 let ids_for_embed = match input {
9458 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9459 PrefillIn::Hidden(_) => None,
9460 };
9461 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9462 if end > li {
9463 h_ready = true;
9464 chunk_skip_until = end;
9465 if self.is_loop_end(end - 1) && end < self.num_layers {
9468 for bi in 0..b {
9469 let normed = inference::rms_norm(
9470 &h[bi * hs..(bi + 1) * hs],
9471 &self.weights.final_norm,
9472 eps,
9473 norm_style,
9474 );
9475 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9476 }
9477 }
9478 continue;
9479 }
9480 }
9481 if !h_ready {
9482 fill_h(&mut h, self);
9483 h_ready = true;
9484 }
9485 if task_mask.is_none() && self.verify_exact_moe {
9486 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9487 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9488 crate::gpu::BatchGraphOutcome::Completed => continue,
9489 crate::gpu::BatchGraphOutcome::Failed => return h,
9490 crate::gpu::BatchGraphOutcome::Declined => {},
9491 }
9492 }
9493 #[cfg(feature = "gpu")]
9494 self.pull_lagging_host_kv(li, li + 1, start_pos);
9495 let lw = &self.weights.layers[self.phys_layer(li)];
9496 let t_attn = std::time::Instant::now();
9497 match &lw.attn {
9499 AttnKind::Kda(w) => {
9500 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9502 let mut normed = vec![0.0f32; b * hs];
9503 for bi in 0..b {
9504 inference::rms_norm_into(
9505 &h[bi * hs..(bi + 1) * hs],
9506 &lw.input_norm,
9507 eps,
9508 norm_style,
9509 &mut normed[bi * hs..(bi + 1) * hs],
9510 );
9511 }
9512 let attn = crate::linear_core::kda_forward_batch(
9513 &normed,
9514 b,
9515 w,
9516 &cfg,
9517 &mut self.kv_cache.layers[li].linear_state,
9518 pool.as_deref(),
9519 );
9520 for (dst, &a) in h.iter_mut().zip(&attn) {
9521 *dst += a;
9522 }
9523 }
9524 AttnKind::LinearGdn(w) => {
9525 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9527 let mut normed = vec![0.0f32; b * hs];
9528 for bi in 0..b {
9529 let r = inference::rms_norm(
9530 &h[bi * hs..(bi + 1) * hs],
9531 &lw.input_norm,
9532 eps,
9533 norm_style,
9534 );
9535 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9536 }
9537 let attn = crate::linear_core::gdn_forward_batch(
9538 &normed,
9539 b,
9540 w,
9541 &cfg,
9542 &mut self.kv_cache.layers[li].linear_state,
9543 pool.as_deref(),
9544 );
9545 for (dst, &a) in h.iter_mut().zip(&attn) {
9546 *dst += a;
9547 }
9548 }
9549 AttnKind::ShortConv(w) => {
9550 let cfg = self
9553 .short_conv_cfg
9554 .expect("short-conv layer without short_conv_cfg");
9555 let mut normed = vec![0.0f32; b * hs];
9556 for bi in 0..b {
9557 inference::rms_norm_into(
9558 &h[bi * hs..(bi + 1) * hs],
9559 &lw.input_norm,
9560 eps,
9561 norm_style,
9562 &mut normed[bi * hs..(bi + 1) * hs],
9563 );
9564 }
9565 let attn = short_conv_forward_batch(
9566 &normed,
9567 b,
9568 w,
9569 &cfg,
9570 &mut self.kv_cache.layers[li].linear_state,
9571 pool.as_deref(),
9572 );
9573 for (dst, &a) in h.iter_mut().zip(&attn) {
9574 *dst += a;
9575 }
9576 }
9577 AttnKind::Mla(w) => {
9578 let inv_freq_l = self.layer_inv_freq(li);
9581 let rs = self.layer_rope_scale(li);
9582 let mut normed = vec![0.0f32; hs];
9583 for bi in 0..b {
9584 inference::rms_norm_into(
9585 &h[bi * hs..(bi + 1) * hs],
9586 &lw.input_norm,
9587 eps,
9588 norm_style,
9589 &mut normed,
9590 );
9591 let ao = mla_attention(
9592 w,
9593 &normed,
9594 &mut self.kv_cache.layers[li],
9595 start_pos + bi,
9596 &inv_freq_l,
9597 rs,
9598 eps,
9599 pool.as_deref(),
9600 );
9601 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9602 *dst += a;
9603 }
9604 }
9605 }
9606 AttnKind::Full {
9607 wq,
9608 wk,
9609 wv,
9610 wo,
9611 q_norm,
9612 k_norm,
9613 output_gate,
9614 softplus_gate,
9615 bias,
9616 } => {
9617 let mut normed = vec![0.0f32; b * hs];
9621 for bi in 0..b {
9622 inference::rms_norm_into(
9623 &h[bi * hs..(bi + 1) * hs],
9624 &lw.input_norm,
9625 eps,
9626 norm_style,
9627 &mut normed[bi * hs..(bi + 1) * hs],
9628 );
9629 }
9630 let inv_freq_l = self.layer_inv_freq(li);
9631 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9632 let cfg = QwenAttnCfg {
9633 num_heads: self.layer_num_heads(li),
9634 num_kv_heads: nkv_l,
9635 head_dim: hd_l,
9636 hidden_size: hs,
9637 position: start_pos,
9638 inv_freq: &inv_freq_l,
9639 rotary_dim: rd_l,
9640 scale: self.attn_scale,
9641 softcap: self.attn_softcap,
9642 window: self.layer_window(li),
9643 v_norm: self.attn_v_norm,
9644 qk_norm_after_rope: self.qk_norm_after_rope,
9645 gate_sigmoid: self.proj_gate_sigmoid,
9646 q_norm: q_norm.as_deref(),
9647 k_norm: k_norm.as_deref(),
9648 output_gate: *output_gate,
9649 softplus_gate: softplus_gate
9650 .as_ref()
9651 .map(|(gate, per_head)| (gate, *per_head)),
9652 rope_scale: self.layer_rope_scale(li),
9653 bias: bias
9654 .as_ref()
9655 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9656 rms_eps: eps,
9657 norm_style,
9658 pool: pool.as_deref(),
9659 v_head_dim: self.layer_v_dim(li),
9660 };
9661 #[cfg(feature = "gpu")]
9662 let _mirror = self
9663 .prefill_mirror_target(li)
9664 .map(crate::gpu::enter_prefill_mirror);
9665 let mut attn = attention::qwen_attention_batch(
9666 &normed,
9667 b,
9668 wq,
9669 wk,
9670 wv,
9671 wo,
9672 &mut self.kv_cache.layers[li],
9673 &cfg,
9674 );
9675 if let Some(w) = &lw.attn_out_norm {
9676 for bi in 0..b {
9677 inference::rms_norm_into(
9678 &attn[bi * hs..(bi + 1) * hs],
9679 w,
9680 eps,
9681 norm_style,
9682 &mut normed[bi * hs..(bi + 1) * hs],
9683 );
9684 }
9685 attn.copy_from_slice(&normed);
9686 }
9687 for (dst, &a) in h.iter_mut().zip(&attn) {
9688 *dst += a;
9689 }
9690 }
9691 AttnKind::Bounded(w) => {
9692 let mut normed = vec![0.0f32; b * hs];
9695 for bi in 0..b {
9696 inference::rms_norm_into(
9697 &h[bi * hs..(bi + 1) * hs],
9698 &lw.input_norm,
9699 eps,
9700 norm_style,
9701 &mut normed[bi * hs..(bi + 1) * hs],
9702 );
9703 }
9704 let rope = self
9705 .bounded_rope
9706 .clone()
9707 .expect("bounded layer without an installed rotation table");
9708 let cfg = crate::bounded::BoundedAttnCfg {
9709 num_heads: self.num_heads,
9710 num_kv_heads: self.num_kv_heads,
9711 head_dim: self.head_dim,
9712 hidden_size: hs,
9713 scale: self.attn_scale,
9714 rope: &rope,
9715 pool: pool.as_deref(),
9716 };
9717 let mut attn = crate::bounded::bounded_attention_batch(
9718 &normed,
9719 b,
9720 w,
9721 &mut self.kv_cache.layers[li],
9722 &cfg,
9723 );
9724 if let Some(wn) = &lw.attn_out_norm {
9725 for bi in 0..b {
9726 inference::rms_norm_into(
9727 &attn[bi * hs..(bi + 1) * hs],
9728 wn,
9729 eps,
9730 norm_style,
9731 &mut normed[bi * hs..(bi + 1) * hs],
9732 );
9733 }
9734 attn.copy_from_slice(&normed);
9735 }
9736 for (dst, &a) in h.iter_mut().zip(&attn) {
9737 *dst += a;
9738 }
9739 attention::recycle_buf(&mut attn);
9740 }
9741 AttnKind::Linear(w) => {
9742 for bi in 0..b {
9743 let normed = inference::rms_norm(
9744 &h[bi * hs..(bi + 1) * hs],
9745 &lw.input_norm,
9746 eps,
9747 norm_style,
9748 );
9749 vmf_phase_forward(
9750 &normed,
9751 w,
9752 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
9753 &mut self.kv_cache.layers[li].linear_state,
9754 pool.as_deref(),
9755 )
9756 .iter()
9757 .enumerate()
9758 .for_each(|(i, &a)| h[bi * hs + i] += a);
9759 }
9760 }
9761 }
9762
9763 let lw = &self.weights.layers[self.phys_layer(li)];
9765 let mut post = vec![0.0f32; b * hs];
9766 for bi in 0..b {
9767 let r =
9768 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
9769 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9770 }
9771 let mask_row = task_mask
9774 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
9775 .and_then(|m| m.ffn_masks.get(li))
9776 .map(|v| v.as_slice());
9777 let attn_ns = t_attn.elapsed().as_nanos() as u64;
9778 let t_ffn = std::time::Instant::now();
9779 let mut ffn = match &lw.ffn {
9780 FfnKind::Dense(d) if !d.segs.is_empty() => {
9781 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
9782 }
9783 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
9784 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
9785 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
9786 }
9787 FfnKind::Moe(m) if self.verify_exact_moe => {
9788 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
9789 }
9790 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
9793 let before = m.stats.borrow().clone();
9794 let out = crate::gpu::cpu_scope(|| {
9795 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
9796 });
9797 self.mimo_moe.prime(li, m, &before);
9798 out
9799 }
9800 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
9801 FfnKind::DenseMoe(dm) => {
9804 let mut out = vec![0.0f32; b * hs];
9805 for bi in 0..b {
9806 let r = dense_moe_ffn(
9807 dm,
9808 &post[bi * hs..(bi + 1) * hs],
9809 &h[bi * hs..(bi + 1) * hs],
9810 eps,
9811 norm_style,
9812 pool.as_deref(),
9813 );
9814 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9815 }
9816 out
9817 }
9818 };
9819 if prefill_prof_on() {
9820 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
9821 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
9822 }
9823 if let Some(w) = &lw.ffn_out_norm {
9824 for bi in 0..b {
9825 inference::rms_norm_into(
9826 &ffn[bi * hs..(bi + 1) * hs],
9827 w,
9828 eps,
9829 norm_style,
9830 &mut post[bi * hs..(bi + 1) * hs],
9831 );
9832 }
9833 ffn.copy_from_slice(&post);
9834 }
9835 for (dst, &f) in h.iter_mut().zip(&ffn) {
9836 *dst += f;
9837 }
9838 if let Some(sc) = lw.layer_scale {
9839 for v in h.iter_mut() {
9840 *v *= sc;
9841 }
9842 }
9843 if self.layer_dump.is_some() {
9845 for bi in 0..b {
9846 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
9847 }
9848 }
9849 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9850 if let Ok(t) = tp.parse::<usize>() {
9851 if t >= start_pos && t < start_pos + b {
9852 let bi = t - start_pos;
9853 let row = &h[bi * hs..(bi + 1) * hs];
9854 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9855 eprintln!(
9856 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
9857 row[0], row[1]
9858 );
9859 }
9860 }
9861 }
9862 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
9866 let row = &h[(b - 1) * hs..b * hs];
9867 let rms =
9868 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
9869 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
9870 eprintln!(
9871 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
9872 match &self.weights.layers[self.phys_layer(li)].attn {
9873 AttnKind::LinearGdn(_) => "gdn",
9874 AttnKind::Linear(_) => "vmf",
9875 AttnKind::ShortConv(_) => "conv",
9876 _ => "attn",
9877 },
9878 match &lw.ffn {
9879 FfnKind::Moe(_) => "moe",
9880 FfnKind::Dense(_) => "dense",
9881 FfnKind::DenseMoe(_) => "dense+moe",
9882 },
9883 );
9884 }
9885 if self.is_loop_end(li) && li + 1 < self.num_layers {
9887 for bi in 0..b {
9888 let normed = inference::rms_norm(
9889 &h[bi * hs..(bi + 1) * hs],
9890 &self.weights.final_norm,
9891 eps,
9892 norm_style,
9893 );
9894 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9895 }
9896 }
9897 if std::env::var("CMF_TRACE_H").is_ok() {
9898 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
9899 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
9900 eprintln!(
9901 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
9902 lw.layer_scale
9903 );
9904 }
9905 }
9906 crate::gpu::set_layer(-1); if prefill_prof_on() {
9908 let ms = |a: &std::sync::atomic::AtomicU64| {
9909 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
9910 };
9911 let sp = &attention::ATTN_SPLIT;
9912 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
9913 eprintln!(
9914 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
9915 host gate {:.1}], host loop {:.1}, attend {:.1} \
9916 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
9917 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
9918 ms(&PREFILL_SPLIT[0]),
9919 ms(&sp[0]),
9920 qkv_enq,
9921 qkv_wait,
9922 ms(&sp[6]),
9923 ms(&sp[1]),
9924 ms(&sp[2]),
9925 ms(&sp[4]),
9926 ms(&sp[5]),
9927 ca_wait,
9928 ms(&sp[3]),
9929 ms(&PREFILL_SPLIT[1]),
9930 );
9931 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
9932 eprintln!(
9933 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
9934 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
9935 fallbacks (cumulative)",
9936 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
9937 );
9938 }
9939 self.o1_progress();
9944 self.swa_trim_tails();
9946 h
9947 }
9948
9949 fn embed_single(&self, id: u32) -> Vec<f32> {
9951 let mut out = vec![0.0f32; self.hidden_size];
9952 if (id as usize) < self.weights.embed_tokens.rows() {
9953 self.weights.embed_tokens.row_f32(id as usize, &mut out);
9954 }
9955 if self.embed_multiplier != 1.0 {
9956 for v in out.iter_mut() {
9957 *v *= self.embed_multiplier;
9958 }
9959 }
9960 if self.dsv4.is_some()
9964 || self.dsv41.is_some()
9965 || self.qwen4_exp.is_some()
9966 {
9967 let mut v = vec![0.0f32; self.hidden_size.max(1)];
9968 v[0] = id as f32;
9969 return v;
9970 }
9971 if let Some(b) = &self.g3n {
9974 return b.0.extend_embedding(id, &out, self.pool.as_deref());
9975 }
9976 out
9977 }
9978
9979 #[cfg(target_os = "macos")]
9985 fn chunk_run_gpu(
9986 &mut self,
9987 li0: usize,
9988 h: &mut [f32],
9989 b: usize,
9990 pos0: usize,
9991 embed_ids: Option<&[u32]>,
9992 cap: usize,
9993 ) -> usize {
9994 let gemma4 = self.metal_gemma4()
10001 && std::env::var("CMF_METAL_GEMMA4_CHUNK").as_deref() != Ok("0");
10002 if !crate::gpu::enabled_here()
10003 || std::env::var("CMF_GPU_CHUNK")
10004 .map(|v| v == "0")
10005 .unwrap_or(false)
10006 || b < 32
10007 || (self.swa.is_some() && !self.metal_graph_swa() && !gemma4)
10010 || (self.global_attn.is_some() && !gemma4)
10011 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa() && !gemma4)
10013 || self.o1_active()
10016 || (self.attn_v_norm && !gemma4)
10017 || !self.metal_rope_scale_ok()
10018 || ((self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9 && !gemma4)
10019 {
10020 return li0;
10021 }
10022 let Some(model) = self.model.clone() else {
10023 return li0;
10024 };
10025 let (nh, nkv, hd, hs) = (
10026 self.num_heads,
10027 self.num_kv_heads,
10028 self.head_dim,
10029 self.hidden_size,
10030 );
10031 let loop_end = if self.loop_final_norm {
10035 ((li0 / self.physical_layers) + 1) * self.physical_layers
10036 } else {
10037 self.num_layers
10038 };
10039 let run_end = self.num_layers.min(loop_end).min(cap);
10040 let tables: Vec<std::sync::Arc<Vec<f32>>> =
10045 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
10046 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
10047 let mut stored_at: Vec<usize> = Vec::new();
10048 for li in li0..run_end {
10049 let lw = &self.weights.layers[self.phys_layer(li)];
10050 if (lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some())
10051 && !gemma4
10052 {
10053 break;
10054 }
10055 let AttnKind::Full {
10056 wq,
10057 wk,
10058 wv,
10059 wo,
10060 q_norm,
10061 k_norm,
10062 output_gate: false,
10063 softplus_gate,
10064 bias,
10065 } = &lw.attn
10066 else {
10067 break;
10068 };
10069 let head_gate = match softplus_gate {
10071 None => None,
10072 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
10073 Some((d, r, c)) if r == nh && c == hs => Some(d),
10074 _ => break,
10075 },
10076 Some(_) => break,
10077 };
10078 let (gate_t, up_t, down_t, ffn_act, moe) = match &lw.ffn {
10082 FfnKind::Dense(d)
10083 if (matches!(d.act, Act::Silu | Act::Gelu)
10084 || (gemma4 && d.act == Act::GeluTanh))
10085 && d.segs.is_empty() =>
10086 {
10087 (&d.gate_proj, &d.up_proj, &d.down_proj, d.act, None)
10088 }
10089 FfnKind::Moe(m) => match chunk_moe_parts(m, hs) {
10090 Some((router, experts, q8)) => {
10091 let e0 = &m.experts[0];
10092 let route = Box::new(move |lg: &[f32]| {
10093 let (idx, p, wsum) = moe_route(lg, m, None);
10095 let mut st = m.stats.borrow_mut();
10096 if st.len() < m.experts.len() {
10097 st.resize(m.experts.len(), 0);
10098 }
10099 for &e in &idx {
10100 st[e] += 1;
10101 }
10102 let w = idx.iter().map(|&e| p[e] / wsum).collect();
10103 (idx, w)
10104 });
10105 (
10106 &e0.gate_proj,
10107 &e0.up_proj,
10108 &e0.down_proj,
10109 Act::Silu,
10110 Some(crate::gpu_metal::ChunkMoe {
10111 router,
10112 experts,
10113 q8,
10114 route,
10115 }),
10116 )
10117 }
10118 None => break,
10119 },
10120 _ => break,
10121 };
10122 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
10128 t.q8_row_parts()
10129 .or_else(|| t.q8_2f_parts().map(|(i, r, c, rs, _)| (i, r, c, rs)))
10130 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10131 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
10132 }
10133 fn cf(t: &QTensor) -> &[f32] {
10134 t.q8_2f_parts().map_or(&[][..], |(_, _, _, _, cf)| cf)
10135 }
10136 let col = [cf(wq), cf(wk), cf(wv), cf(wo)];
10137 if moe.is_none()
10146 && !gemma4
10147 && (col.iter().any(|c| !c.is_empty())
10148 || [gate_t, up_t, down_t].iter().any(|t| !cf(t).is_empty()))
10149 {
10150 break;
10151 }
10152 let parts = (
10153 cw(wq),
10154 cw(wk),
10155 cw(wv),
10156 cw(wo),
10157 cw(gate_t),
10158 cw(up_t),
10159 cw(down_t),
10160 );
10161 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
10162 else {
10163 break;
10164 };
10165 let layer = &self.kv_cache.layers[li];
10166 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
10167 break;
10168 }
10169 debug_assert!(
10171 layer.base() == 0
10172 || self
10173 .layer_window(li)
10174 .is_some_and(|w| layer.head_len(0) + 1 >= w)
10175 );
10176 stored_at.push(layer.head_len(0));
10177 let (nkv, hd) = if gemma4 {
10180 let (a, bb, _) = self.layer_geom(li);
10181 (a, bb)
10182 } else {
10183 (nkv, hd)
10184 };
10185 layers.push(crate::gpu_metal::ChunkLayer {
10186 model: &model,
10187 kv_id: self.graph_kv_id,
10188 layer: li,
10189 wq: pq,
10190 wk: pk,
10191 wv: pv,
10192 wo: po,
10193 gate: pg,
10194 up: pu,
10195 down: pd,
10196 input_norm: &lw.input_norm,
10197 post_norm: &lw.post_norm,
10198 bias: bias
10199 .as_ref()
10200 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
10201 q_norm: q_norm.as_deref(),
10202 k_norm: k_norm.as_deref(),
10203 inv_freq: &tables[li - li0],
10204 rope_scale: self.layer_rope_scale(li),
10205 rd: self.layer_geom(li).2,
10206 nh,
10207 nkv,
10208 hd,
10209 hs,
10210 inter: gate_t.rows(),
10211 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
10212 late_qk_norm: self.qk_norm_after_rope,
10213 eps: self.rms_eps as f32,
10214 window: self.layer_window(li),
10215 head_gate,
10216 act: ffn_act.graph_act().map_or(0, |a| a.code() as u8),
10217 g4: gemma4.then(|| crate::gpu_metal::ChunkGemma4 {
10218 v_norm: self.attn_v_norm,
10219 scale: self.attn_scale,
10220 attn_out_norm: lw.attn_out_norm.as_deref(),
10221 ffn_out_norm: lw.ffn_out_norm.as_deref(),
10222 layer_scale: lw.layer_scale,
10223 }),
10224 col,
10225 moe,
10226 });
10227 }
10228 if layers.is_empty() {
10229 return li0;
10230 }
10231 let rows_kv: Vec<(usize, usize)> = layers.iter().map(|l| (l.nkv, l.nkv * l.hd)).collect();
10233 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
10234 .iter()
10235 .zip(&rows_kv)
10236 .map(|(&st, &(_, row))| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
10237 .collect();
10238 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
10239 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
10240 let li = layers[i].layer;
10241 let layer = &self.kv_cache.layers[li];
10242 let nkv = rows_kv[i].0;
10243 io.push(crate::gpu_metal::ChunkIo {
10244 cpu_stored: stored_at[i],
10245 cpu_gen: layer.generation(),
10246 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
10247 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
10248 out_k: ok,
10249 out_v: ov,
10250 imp: oi,
10251 });
10252 }
10253 let n_run = layers.len();
10254 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
10255 let ep = embed_ids.and_then(|ids| {
10258 self.weights
10259 .embed_tokens
10260 .q8_row_parts()
10261 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
10262 idx,
10263 rows,
10264 row_scale: rs,
10265 ids,
10266 mult: self.embed_multiplier,
10267 })
10268 });
10269 if embed_ids.is_some() && ep.is_none() {
10270 return li0;
10271 }
10272 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
10273 return li0;
10274 }
10275 drop(io);
10276 drop(layers);
10277 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
10280 let li = li0 + i;
10281 let row = rows_kv[i].1;
10282 let layer = &mut self.kv_cache.layers[li];
10283 for bi in 0..b {
10284 layer.append(
10285 &ok[bi * row..(bi + 1) * row],
10286 &ov[bi * row..(bi + 1) * row],
10287 &[],
10288 );
10289 }
10290 layer.accumulate_imp(oi);
10291 }
10292 last
10293 }
10294
10295 fn layer_is_local(&self, li: usize) -> bool {
10298 if let Some(layers) = &self.sliding_layers {
10299 return layers.get(li).copied().unwrap_or(false);
10300 }
10301 match self.swa {
10302 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
10303 None => false,
10304 }
10305 }
10306
10307 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
10310 if self.layer_is_local(li) {
10311 if let Some(f) = &self.inv_freq_local {
10312 return f.clone();
10313 }
10314 } else if let Some(f) = &self.inv_freq_global {
10315 return f.clone();
10316 }
10317 self.inv_freq.clone()
10318 }
10319
10320 fn layer_window(&self, li: usize) -> Option<usize> {
10322 self.swa
10323 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
10324 }
10325
10326 fn swa_trim_tails(&mut self) {
10335 let Some((slack, align)) = self.swa_trim else {
10336 return;
10337 };
10338 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
10339 return;
10340 }
10341 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
10342 if let Some(w) = self.layer_window(li) {
10343 self.kv_cache.layers[li].trim_window(w, slack, align);
10344 }
10345 }
10346 }
10347
10348 fn layer_num_heads(&self, li: usize) -> usize {
10349 self.attention_heads_per_layer
10350 .as_ref()
10351 .and_then(|v| v.get(li).copied())
10352 .unwrap_or(self.num_heads)
10353 }
10354
10355 fn layer_rope_scale(&self, li: usize) -> f32 {
10356 if self.layer_is_local(li) {
10357 self.rope_scale_local
10358 } else {
10359 self.rope_scale
10360 }
10361 }
10362
10363 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10366 if !self.layer_is_local(li) {
10367 if let Some((ghd, gkv)) = self.global_attn {
10368 return (gkv, ghd, ghd);
10369 }
10370 }
10371 (
10372 self.layer_num_kv_heads(li),
10373 self.head_dim,
10374 if self.layer_is_local(li) {
10375 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10376 } else {
10377 self.rotary_dim
10378 },
10379 )
10380 }
10381
10382 fn layer_num_kv_heads(&self, li: usize) -> usize {
10385 self.kv_heads_per_layer
10386 .as_ref()
10387 .and_then(|v| v.get(self.phys_layer(li)).copied())
10388 .unwrap_or(self.num_kv_heads)
10389 }
10390
10391 fn layer_v_dim(&self, li: usize) -> usize {
10393 let (_, hd, _) = self.layer_geom(li);
10394 self.v_head_dim.unwrap_or(hd).min(hd)
10395 }
10396
10397 pub fn set_attn_geometry(
10406 &mut self,
10407 kv_heads_per_layer: Option<Vec<usize>>,
10408 v_head_dim: Option<usize>,
10409 ) -> Result<(), String> {
10410 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10411 if self.global_attn.is_some() {
10412 return Err(
10413 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10414 attention geometry"
10415 .into(),
10416 );
10417 }
10418 if self
10419 .weights
10420 .layers
10421 .iter()
10422 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10423 {
10424 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10425 }
10426 }
10427 if let Some(vd) = v_head_dim {
10428 if vd == 0 || vd > self.head_dim {
10429 return Err(format!(
10430 "v_head_dim {vd} must be in 1..={} (head_dim)",
10431 self.head_dim
10432 ));
10433 }
10434 }
10435 if let Some(v) = &kv_heads_per_layer {
10436 if v.len() != self.physical_layers {
10437 return Err(format!(
10438 "kv_heads_per_layer has {} entries, expected {} layers",
10439 v.len(),
10440 self.physical_layers
10441 ));
10442 }
10443 for (li, &nkv) in v.iter().enumerate() {
10444 let is_attn = matches!(
10445 self.weights.layers.get(li).map(|lw| &lw.attn),
10446 Some(AttnKind::Full { .. }) | None
10447 );
10448 if !is_attn {
10449 continue;
10450 }
10451 let nh = self
10452 .attention_heads_per_layer
10453 .as_ref()
10454 .and_then(|h| h.get(li).copied())
10455 .unwrap_or(self.num_heads);
10456 if nkv == 0 || nh % nkv != 0 {
10457 return Err(format!(
10458 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10459 ));
10460 }
10461 }
10462 }
10463 self.kv_heads_per_layer = kv_heads_per_layer;
10464 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10465 if self.kv_heads_per_layer.is_some() {
10466 for li in 0..self.kv_cache.layers.len() {
10467 let full = matches!(
10468 self.weights
10469 .layers
10470 .get(self.phys_layer(li))
10471 .map(|lw| &lw.attn),
10472 Some(AttnKind::Full { .. })
10473 );
10474 let nkv = self.layer_num_kv_heads(li);
10475 let cache = &self.kv_cache.layers[li];
10476 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10477 let sinks = cache.sinks.clone();
10478 self.kv_cache.layers[li] =
10479 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10480 self.kv_cache.layers[li].sinks = sinks;
10481 }
10482 }
10483 }
10484 Ok(())
10485 }
10486
10487 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10491 let Some(lw) = self.weights.layers.get(phys) else {
10492 return Err(format!("sinks for layer {phys}: no such layer"));
10493 };
10494 if !matches!(lw.attn, AttnKind::Full { .. }) {
10495 return Err(format!(
10496 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10497 ));
10498 }
10499 let nh = self
10500 .attention_heads_per_layer
10501 .as_ref()
10502 .and_then(|h| h.get(phys).copied())
10503 .unwrap_or(self.num_heads);
10504 if sinks.len() != nh {
10505 return Err(format!(
10506 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10507 sinks.len()
10508 ));
10509 }
10510 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10511 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10512 }
10513 for li in 0..self.kv_cache.layers.len() {
10514 if self.phys_layer(li) == phys {
10515 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10516 }
10517 }
10518 Ok(())
10519 }
10520
10521 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10531 if self.kv_heads_per_layer.is_some() {
10532 return Some("per-layer KV head counts");
10533 }
10534 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10535 return Some("V heads narrower than Q/K heads");
10536 }
10537 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10538 return Some("learned attention sinks");
10539 }
10540 if self.swa.is_some() || self.sliding_layers.is_some() {
10541 return Some("sliding-window layers");
10542 }
10543 None
10544 }
10545
10546 #[cfg(target_os = "macos")]
10554 fn metal_rope_scale_ok(&self) -> bool {
10555 !(self.qk_norm_after_rope && (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0))
10556 }
10557
10558 #[cfg(target_os = "macos")]
10572 fn metal_graph_swa(&self) -> bool {
10573 (self.swa.is_some() || self.sliding_layers.is_some())
10574 && self.attention_heads_per_layer.is_none()
10575 && !self.attn_v_norm
10576 && self.attn_softcap == 0.0
10577 && self.kv_heads_per_layer.is_none()
10578 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10579 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10580 && self.global_attn.is_none()
10581 && self.inv_freq_global.is_none()
10582 && self.metal_rope_scale_ok()
10583 && !(0..self.num_layers).any(|li| {
10584 self.layer_is_local(li)
10585 && self.inv_freq_local.is_none()
10586 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10587 })
10588 }
10589
10590 #[cfg(target_os = "macos")]
10598 fn metal_gemma4(&self) -> bool {
10599 crate::gpu_metal::gemma4::enabled()
10600 && (self.global_attn.is_some() || self.attn_v_norm)
10601 && self.attention_heads_per_layer.is_none()
10602 && self.kv_heads_per_layer.is_none()
10603 && self.v_head_dim.is_none()
10604 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10605 && self.attn_softcap == 0.0
10606 && !self.qk_norm_after_rope
10607 && self.metal_rope_scale_ok()
10608 && self
10609 .weights
10610 .layers
10611 .iter()
10612 .all(|lw| matches!(&lw.ffn, FfnKind::Dense(d) if d.segs.is_empty()))
10613 && !(0..self.num_layers).any(|li| {
10614 self.layer_is_local(li)
10615 && self.inv_freq_local.is_none()
10616 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10617 })
10618 }
10619
10620 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
10627 let geometry_reason = self.graph_attn_decline_reason();
10631 let has_scaled_rope = (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0);
10632 if has_scaled_rope && geometry_reason.is_none() {
10633 return Some("scaled RoPE without per-layer geometry");
10634 }
10635 if has_scaled_rope && self.qk_norm_after_rope {
10640 return Some("scaled RoPE with post-RoPE Q/K norm");
10641 }
10642 geometry_reason?;
10643 if self.attention_heads_per_layer.is_some() {
10649 return Some("per-layer Q head counts with per-layer geometry");
10650 }
10651 let gemma4 = self.global_attn.is_some() || self.attn_v_norm;
10655 if !gemma4
10656 && self.weights.layers.iter().any(|lw| {
10657 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10658 })
10659 {
10660 return Some("sandwich norms / layer scale with per-layer geometry");
10661 }
10662 if self.weights.layers.iter().any(|lw| {
10666 matches!(
10667 &lw.attn,
10668 AttnKind::Full {
10669 output_gate: true,
10670 ..
10671 }
10672 )
10673 }) && self.v_head_dim.is_some()
10674 {
10675 return Some("gated attention with V narrower than K");
10676 }
10677 if (0..self.num_layers).any(|li| {
10678 self.layer_is_local(li)
10679 && self.inv_freq_local.is_none()
10680 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10681 }) {
10682 return Some("local rotary width without a local RoPE table");
10683 }
10684 None
10685 }
10686
10687 fn batch_graph_gemma_decline(&self) -> Option<&'static str> {
10691 if (self.global_attn.is_some() || self.attn_v_norm) && crate::gpu::g4_batch_on() {
10694 return None;
10695 }
10696 if self.global_attn.is_some() {
10697 return Some("per-layer head width (Gemma-4 global layers)");
10698 }
10699 if self.attn_v_norm {
10700 return Some("V norm");
10701 }
10702 if self.weights.layers.iter().any(|lw| {
10703 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10704 }) {
10705 return Some("sandwich norms / layer scale");
10706 }
10707 None
10708 }
10709
10710 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
10715 self.graph_attn_decline_reason()?;
10716 let (nkv, hd, rd) = self.layer_geom(li);
10717 let invf: &[f32] = if self.layer_is_local(li) {
10718 match &self.inv_freq_local {
10719 Some(f) => f.as_slice(),
10720 None => self.inv_freq.as_slice(),
10721 }
10722 } else {
10723 match &self.inv_freq_global {
10724 Some(f) => f.as_slice(),
10725 None => self.inv_freq.as_slice(),
10726 }
10727 };
10728 Some(crate::gpu::GraphAttnGeom {
10729 nkv,
10730 hd,
10731 dv: self.layer_v_dim(li),
10732 rd,
10733 invf,
10734 rope_scale: self.layer_rope_scale(li),
10735 window: self.layer_window(li),
10736 sink: self.kv_cache.layers[li].sinks.as_deref(),
10737 v_norm: self.attn_v_norm,
10738 })
10739 }
10740
10741 #[cfg(feature = "gpu")]
10753 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
10754 if !self.proj_gate_sigmoid
10755 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
10756 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
10757 || self.graph_refused()
10758 || self.o1_active()
10759 || self.attn_softcap > 0.0
10760 || self.wgpu_graph_attn_decline().is_some()
10761 {
10762 return None;
10763 }
10764 let g = self.graph_attn_geom(li)?;
10765 let (nkv, hd, _) = self.layer_geom(li);
10766 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
10768 return None;
10769 }
10770 Some(crate::gpu::PrefillMirror {
10771 kv_id: self.graph_kv_id,
10772 layer: li,
10773 limit: self.kv_cache.max_seq_len,
10774 })
10775 }
10776
10777 #[cfg(feature = "gpu")]
10786 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
10787 let kv_id = self.graph_kv_id;
10788 for li in from..upto.min(self.num_layers) {
10789 if !matches!(
10790 self.weights.layers[self.phys_layer(li)].attn,
10791 AttnKind::Full { .. }
10792 ) {
10793 continue;
10794 }
10795 let host = self.kv_cache.layers[li].pos_len();
10797 if host >= position {
10798 continue;
10799 }
10800 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
10801 continue;
10802 };
10803 let to = dev.min(position);
10804 if to <= host {
10805 continue;
10806 }
10807 let (nkv, hd) = {
10808 let c = &self.kv_cache.layers[li];
10809 (c.num_kv_heads, c.head_dim)
10810 };
10811 let Some((k, v, first_valid)) =
10812 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
10813 else {
10814 continue;
10815 };
10816 let need_from = match self.layer_window(li) {
10819 Some(w) => host.max((position + 1).saturating_sub(w)),
10820 None => host,
10821 };
10822 if first_valid > need_from {
10823 tracing::warn!(
10824 "layer {li}: device KV rows {host}..{to} no longer resident \
10825 (from {first_valid}); host attention will miss them"
10826 );
10827 }
10828 let row = nkv * hd;
10829 let cache = &mut self.kv_cache.layers[li];
10830 for p in 0..to - host {
10831 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
10832 }
10833 }
10834 }
10835
10836 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
10839 let mut seen = self.graph_declines.borrow_mut();
10840 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
10841 tracing::warn!("{site} declined: {reason} (CPU attention path)");
10842 seen.push((site, reason));
10843 }
10844 }
10845
10846 pub fn graph_declines(&self) -> Vec<String> {
10849 self.graph_declines
10850 .borrow()
10851 .iter()
10852 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
10853 .collect()
10854 }
10855
10856 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
10865 let Some(dir) = &self.layer_dump else {
10866 return;
10867 };
10868 let mut bytes = Vec::with_capacity(row.len() * 4);
10869 for v in row {
10870 bytes.extend_from_slice(&v.to_le_bytes());
10871 }
10872 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
10873 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
10874 use std::sync::atomic::{AtomicBool, Ordering};
10875 static SAID: AtomicBool = AtomicBool::new(false);
10876 if !SAID.swap(true, Ordering::Relaxed) {
10877 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
10878 }
10879 }
10880 }
10881
10882 fn mimo_moe_prepare(&mut self) {
10885 if !self.mimo_moe.is_undecided() {
10886 return;
10887 }
10888 let slot = {
10889 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
10890 .filter_map(
10891 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
10892 FfnKind::Moe(m) => Some((li, m)),
10893 _ => None,
10894 },
10895 )
10896 .collect();
10897 if layers.is_empty()
10900 || self.physical_layers != self.num_layers
10901 || self.gpu_plan.is_some()
10902 {
10903 crate::mimo_moe::Slot::Off
10904 } else {
10905 let graph_prefix =
10909 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
10910 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
10911 }
10912 };
10913 self.mimo_moe = slot;
10914 }
10915
10916 #[cfg(test)]
10917 pub(crate) fn test_graph_kv_id(&self) -> u64 {
10918 self.graph_kv_id
10919 }
10920
10921 pub(crate) fn mimo_graph_layer_rows(
10925 &mut self,
10926 li: usize,
10927 h: &mut [f32],
10928 positions: &[usize],
10929 ) -> crate::gpu::BatchGraphOutcome {
10930 use crate::gpu::BatchGraphOutcome as Out;
10931 let b = positions.len();
10932 if !(1..=4).contains(&b)
10933 || h.len() != b * self.hidden_size
10934 || !self.mimo_moe.is_dynamic(li, true)
10935 || !crate::gpu::enabled_here()
10936 || !crate::gpu::wgpu_active()
10937 || self.o1_active()
10938 || self.physical_layers != self.num_layers
10939 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
10943 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
10944 || self.wgpu_graph_attn_decline().is_some()
10945 {
10946 return Out::Declined;
10947 }
10948 let attn_started = std::time::Instant::now();
10949 let outcome = {
10950 let lw = &self.weights.layers[li];
10951 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
10952 return Out::Declined;
10953 }
10954 let FfnKind::Moe(m) = &lw.ffn else {
10955 return Out::Declined;
10956 };
10957 let AttnKind::Full {
10958 wq,
10959 wk,
10960 wv,
10961 wo,
10962 q_norm,
10963 k_norm,
10964 output_gate,
10965 softplus_gate,
10966 bias,
10967 } = &lw.attn
10968 else {
10969 return Out::Declined;
10970 };
10971 if *output_gate || softplus_gate.is_some() {
10972 return Out::Declined;
10973 }
10974 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
10975 m.experts
10976 .first()?
10977 .gate_proj
10978 .mapped_q4tp()
10979 .map(|(m, _)| m.clone())
10980 }) else {
10981 return Out::Declined;
10982 };
10983 fn gw<'a>(
10984 t: &'a QTensor,
10985 owner: &std::sync::Arc<cortiq_core::CmfModel>,
10986 ) -> Option<crate::gpu::GraphW<'a>> {
10987 if let Some((m, idx, kind, rs)) = t.graph_weight() {
10988 if m.uid() != owner.uid() || t.has_prism_contract() {
10989 return None;
10990 }
10991 return Some(crate::gpu::GraphW {
10992 idx,
10993 kind,
10994 row_scale: rs,
10995 data: &[],
10996 prism: crate::gpu::GraphPrismOp::None,
10997 affine: false,
10998 });
10999 }
11000 t.as_f32().map(|data| crate::gpu::GraphW {
11001 idx: 0,
11002 kind: 4,
11003 row_scale: &[],
11004 data,
11005 prism: crate::gpu::GraphPrismOp::None,
11006 affine: false,
11007 })
11008 }
11009 let (Some(q), Some(k), Some(v), Some(o)) = (
11010 gw(wq, &model),
11011 gw(wk, &model),
11012 gw(wv, &model),
11013 gw(wo, &model),
11014 ) else {
11015 return Out::Declined;
11016 };
11017 let layer = crate::gpu::GraphLayer {
11018 input_norm: &lw.input_norm,
11019 post_norm: &lw.post_norm,
11020 attn_out_norm: None,
11021 ffn_out_norm: None,
11022 layer_scale: None,
11023 ffn: crate::gpu::GraphFfn::AttentionOnly,
11024 attn: crate::gpu::GraphAttn::Full {
11025 wq: q,
11026 wk: k,
11027 wv: v,
11028 wo: o,
11029 q_norm: q_norm.as_deref(),
11030 k_norm: k_norm.as_deref(),
11031 late_qk_norm: self.qk_norm_after_rope,
11032 bias: bias
11033 .as_ref()
11034 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
11035 output_gate: false,
11036 cpu_k: self.kv_cache.layers[li].k_heads(),
11037 cpu_v: self.kv_cache.layers[li].v_heads(),
11038 cpu_base: self.kv_cache.layers[li].base(),
11039 geom: self.graph_attn_geom(li),
11040 head_gate: None,
11041 },
11042 };
11043 let (nkv, hd, rd) = self.layer_geom(li);
11044 crate::gpu::forward_batch_graph_at(
11045 &model,
11046 self.graph_kv_id,
11047 li,
11048 &[layer],
11049 &self.inv_freq,
11050 h,
11051 self.layer_num_heads(li),
11052 nkv,
11053 hd,
11054 rd,
11055 self.hidden_size,
11056 1,
11057 positions,
11058 self.kv_cache.max_seq_len,
11059 self.norm_style == cortiq_core::NormStyle::Gemma,
11060 self.rms_eps as f32,
11061 self.attn_scale,
11062 b,
11063 &[],
11064 self.o1_epoch,
11065 None,
11066 None,
11067 )
11068 };
11069 match outcome {
11070 Out::Completed => {}
11071 Out::Declined => return Out::Declined,
11072 Out::Failed => {
11073 self.graph_failed
11074 .store(true, std::sync::atomic::Ordering::Relaxed);
11075 return Out::Failed;
11076 }
11077 }
11078 let attn_ns = attn_started.elapsed().as_nanos() as u64;
11079 let hs = self.hidden_size;
11080 let lw = &self.weights.layers[li];
11081 let FfnKind::Moe(m) = &lw.ffn else {
11082 unreachable!()
11083 };
11084 let mut post = vec![0.0; h.len()];
11085 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
11086 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
11087 }
11088 let mut ffn = if b == 1 {
11089 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
11090 } else {
11091 moe_ffn_banked_rows(
11092 &mut self.mimo_moe,
11093 li,
11094 m,
11095 &post,
11096 b,
11097 hs,
11098 self.pool.as_deref(),
11099 )
11100 };
11101 for (x, &f) in h.iter_mut().zip(&ffn) {
11102 *x += f;
11103 }
11104 attention::recycle_buf(&mut ffn);
11105 if self.layer_dump.is_some() {
11106 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
11107 self.dump_layer_row(pos, li, row);
11108 }
11109 }
11110 crate::mimo_moe::note_attention_graph(b, attn_ns);
11111 Out::Completed
11112 }
11113
11114 fn layer_attn_plain(&self, li: usize) -> bool {
11115 self.kv_heads_per_layer.is_none()
11116 && self.v_head_dim.is_none()
11117 && self.global_attn.is_none()
11118 && self.layer_window(li).is_none()
11119 && self.kv_cache.layers[li].sinks.is_none()
11120 }
11121
11122 fn forward_layers(
11124 &mut self,
11125 hidden: &[f32],
11126 position: usize,
11127 task_mask: Option<&TaskMask>,
11128 ) -> Vec<f32> {
11129 let out = self.forward_layers_upto(hidden, position, task_mask, None);
11130 self.o1_progress();
11131 self.swa_trim_tails();
11132 out
11133 }
11134
11135 pub fn embed_id(&self, id: u32) -> Vec<f32> {
11143 self.embed_single(id)
11144 }
11145
11146 pub fn split_supported(&self) -> Result<(), String> {
11150 if self.dsv4.is_some() {
11151 return Err(
11152 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
11153 );
11154 }
11155 if self.dsv41.is_some() {
11156 return Err(
11157 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
11158 .into(),
11159 );
11160 }
11161 if self.qwen4_exp.is_some() {
11162 return Err(
11163 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
11164 );
11165 }
11166 if self.g3n.is_some() {
11167 return Err(
11168 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
11169 );
11170 }
11171 Ok(())
11172 }
11173
11174 pub fn forward_span(
11179 &mut self,
11180 hidden: &[f32],
11181 position: usize,
11182 from: usize,
11183 upto: usize,
11184 task_mask: Option<&TaskMask>,
11185 ) -> Result<Vec<f32>, String> {
11186 self.split_supported()?;
11187 if from > upto || upto >= self.num_layers {
11188 return Err(format!(
11189 "forward_span: layer range {from}..={upto} outside 0..{}",
11190 self.num_layers
11191 ));
11192 }
11193 if hidden.len() != self.hidden_size {
11194 return Err(format!(
11195 "forward_span: hidden len {} ≠ hidden_size {}",
11196 hidden.len(),
11197 self.hidden_size
11198 ));
11199 }
11200 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
11201 self.o1_progress();
11202 self.swa_trim_tails();
11203 if self
11204 .graph_failed
11205 .swap(false, std::sync::atomic::Ordering::Relaxed)
11206 {
11207 self.cancel
11208 .store(false, std::sync::atomic::Ordering::Relaxed);
11209 self.clear_sequence_state();
11210 return Err("forward_span: deferred O(1) transition failed".into());
11211 }
11212 Ok(out)
11213 }
11214
11215 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
11218 let normed = inference::rms_norm(
11219 hidden,
11220 &self.weights.final_norm,
11221 self.rms_eps,
11222 self.norm_style,
11223 );
11224 self.lm_head_forward(&normed)
11225 }
11226
11227 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
11229 sampler::sample_with_scratch(
11230 logits,
11231 &self.sampler_config,
11232 past_tokens,
11233 &mut self.rng,
11234 &mut self.sampler_scratch,
11235 )
11236 }
11237
11238 pub fn reset_session(&mut self) {
11240 self.clear_sequence_state();
11241 }
11242
11243 pub fn prefill_span_ids(
11249 &mut self,
11250 ids: &[u32],
11251 start_pos: usize,
11252 upto: usize,
11253 task_mask: Option<&TaskMask>,
11254 ) -> Result<Vec<f32>, String> {
11255 self.split_supported()?;
11256 if upto >= self.num_layers {
11257 return Err(format!(
11258 "prefill_span_ids: upto {upto} outside 0..{}",
11259 self.num_layers
11260 ));
11261 }
11262 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11266 let out =
11267 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
11268 self.check_o1_progress_failure("prefill_span_ids")?;
11269 Ok(out)
11270 } else {
11271 let hs = self.hidden_size;
11272 let mut out = Vec::with_capacity(ids.len() * hs);
11273 for (i, &id) in ids.iter().enumerate() {
11274 let emb = self.embed_id(id);
11275 out.extend_from_slice(&self.forward_span(
11276 &emb,
11277 start_pos + i,
11278 0,
11279 upto,
11280 task_mask,
11281 )?);
11282 }
11283 Ok(out)
11284 }
11285 }
11286
11287 pub fn prefill_span_hidden(
11290 &mut self,
11291 hidden: &[f32],
11292 start_pos: usize,
11293 from: usize,
11294 upto: usize,
11295 task_mask: Option<&TaskMask>,
11296 ) -> Result<Vec<f32>, String> {
11297 self.split_supported()?;
11298 let hs = self.hidden_size;
11299 if hidden.is_empty() || hidden.len() % hs != 0 {
11300 return Err(format!(
11301 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
11302 hidden.len()
11303 ));
11304 }
11305 if from > upto || upto >= self.num_layers {
11306 return Err(format!(
11307 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
11308 self.num_layers
11309 ));
11310 }
11311 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
11312 let out = self.prefill_batch_span(
11313 PrefillIn::Hidden(hidden),
11314 start_pos,
11315 task_mask,
11316 from,
11317 upto + 1,
11318 );
11319 self.check_o1_progress_failure("prefill_span_hidden")?;
11320 Ok(out)
11321 } else {
11322 let b = hidden.len() / hs;
11323 let mut out = Vec::with_capacity(hidden.len());
11324 for i in 0..b {
11325 let h = self.forward_span(
11326 &hidden[i * hs..(i + 1) * hs],
11327 start_pos + i,
11328 from,
11329 upto,
11330 task_mask,
11331 )?;
11332 out.extend_from_slice(&h);
11333 }
11334 Ok(out)
11335 }
11336 }
11337
11338 fn try_token_graph_wgpu(
11342 &self,
11343 hidden: &[f32],
11344 position: usize,
11345 logits_out: &mut Vec<f32>,
11346 layers_run: &mut usize,
11347 ) -> Option<Result<Vec<f32>, ()>> {
11348 self.try_token_graph_wgpu_steps(
11349 hidden,
11350 position,
11351 logits_out,
11352 1,
11353 None,
11354 Some(layers_run),
11355 0,
11356 self.num_layers,
11357 )
11358 }
11359
11360 fn try_token_graph_wgpu_span(
11364 &self,
11365 hidden: &[f32],
11366 position: usize,
11367 logits_out: &mut Vec<f32>,
11368 from: usize,
11369 upto_excl: usize,
11370 layers_run: &mut usize,
11371 ) -> Option<Result<Vec<f32>, ()>> {
11372 self.try_token_graph_wgpu_steps(
11373 hidden,
11374 position,
11375 logits_out,
11376 1,
11377 None,
11378 Some(layers_run),
11379 from,
11380 upto_excl,
11381 )
11382 }
11383
11384 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
11388 if self.o1_active() || self.attn_softcap > 0.0 {
11389 return None;
11390 }
11391 if let Some(reason) = self.wgpu_graph_attn_decline() {
11394 self.note_graph_decline("wgpu multi-burst", reason);
11395 return None;
11396 }
11397 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
11398 if !graph_on || self.graph_refused() {
11399 return None;
11406 }
11407 let emb = self.embed_single(t_next);
11408 let mut lg = Vec::new();
11409 let mut ids = Vec::new();
11410 match self.try_token_graph_wgpu_steps(
11411 &emb,
11412 position,
11413 &mut lg,
11414 k,
11415 Some(&mut ids),
11416 None,
11417 0,
11418 self.num_layers,
11419 ) {
11420 Some(Ok(_)) => {}
11421 Some(Err(())) => {
11422 self.graph_failed
11427 .store(true, std::sync::atomic::Ordering::Relaxed);
11428 return None;
11429 }
11430 None => return None,
11431 }
11432 (ids.len() == k).then_some(ids)
11433 }
11434
11435 fn try_token_graph_wgpu_steps(
11439 &self,
11440 hidden: &[f32],
11441 position: usize,
11442 logits_out: &mut Vec<f32>,
11443 steps: usize,
11444 ids_out: Option<&mut Vec<u32>>,
11445 layers_run: Option<&mut usize>,
11446 from: usize,
11447 upto_excl: usize,
11448 ) -> Option<Result<Vec<f32>, ()>> {
11449 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11452 Some(end) if end < upto_excl => {
11453 if steps != 1 || layers_run.is_none() || from >= end {
11454 return None;
11455 }
11456 end
11457 }
11458 _ => upto_excl,
11459 };
11460 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11463 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11464 return None;
11468 }
11469 if let Some(reason) = self.wgpu_graph_attn_decline() {
11476 self.note_graph_decline("wgpu token graph", reason);
11477 return None;
11478 }
11479 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11484 .map(|li| {
11485 if !o1_gpu {
11486 return None;
11487 }
11488 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11489 })
11490 .collect();
11491 if self.o1_active() && o1_gpu {
11492 let want: usize = (from..upto_excl)
11495 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11496 .count();
11497 let have = o1_views.iter().filter(|v| v.is_some()).count();
11498 if want == 0 || have != want {
11499 use std::sync::atomic::{AtomicUsize, Ordering};
11509 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11510 let code = have * 1000 + want;
11511 if LAST.swap(code, Ordering::Relaxed) != code {
11512 tracing::warn!(
11513 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11514 );
11515 }
11516 return None;
11517 }
11518 }
11519 let nh = self.num_heads;
11520 let (nkv, hd, rd) = self.layer_geom(0);
11521 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11522 let mut layers = Vec::with_capacity(upto_excl - from);
11523 let mut model = None;
11524 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11525 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11526 if let Some((m, i, kind, rs)) = t
11527 .graph_weight()
11528 .or_else(|| t.graph_weight_descriptor())
11529 {
11530 let name = &m.tensors[i].name;
11531 let prism = if crate::prism::is_inverse_embedding(m, name) {
11532 crate::gpu::GraphPrismOp::InverseEmbedding
11533 } else if crate::prism::is_forward_weight(m, name) {
11534 crate::gpu::GraphPrismOp::Forward
11535 } else {
11536 crate::gpu::GraphPrismOp::None
11537 };
11538 return Some(crate::gpu::GraphW {
11539 idx: i,
11540 kind,
11541 row_scale: rs,
11542 data: &[],
11543 prism,
11544 affine: crate::prism::is_affine_target(m, name),
11545 });
11546 }
11547 match t.as_f32() {
11549 Some(d) => Some(crate::gpu::GraphW {
11550 idx: 0,
11551 kind: 4,
11552 row_scale: &[],
11553 data: d,
11554 prism: crate::gpu::GraphPrismOp::None,
11555 affine: false,
11556 }),
11557 None => {
11558 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11559 eprintln!("batch graph: weight has no graph/f32 representation");
11560 }
11561 None
11562 }
11563 }
11564 }
11565 for li in from..upto_excl {
11566 let lw = &self.weights.layers[self.phys_layer(li)];
11567 if dbg {
11568 let ak = match &lw.attn {
11569 AttnKind::Mla(_) => "Mla".into(),
11570 AttnKind::Full {
11571 output_gate, bias, ..
11572 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11573 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11574 AttnKind::Kda(_) => "Kda".into(),
11575 AttnKind::Linear(_) => "Linear".into(),
11576 AttnKind::ShortConv(_) => "ShortConv".into(),
11577 AttnKind::Bounded(_) => "Bounded".into(),
11578 };
11579 let fk = match &lw.ffn {
11580 FfnKind::Dense(_) => "Dense",
11581 FfnKind::Moe(_) => "Moe",
11582 FfnKind::DenseMoe(_) => "DenseMoe",
11583 };
11584 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11585 }
11586 let gffn = match &lw.ffn {
11587 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11591 FfnKind::Dense(d) => {
11592 let Some(act) = d.act.graph_act() else {
11596 self.note_graph_decline(
11597 "wgpu token graph",
11598 "dense FFN activation without a graph kernel",
11599 );
11600 return None;
11601 };
11602 crate::gpu::GraphFfn::Dense {
11603 gate: gw(&d.gate_proj)?,
11604 up: gw(&d.up_proj)?,
11605 down: gw(&d.down_proj)?,
11606 act,
11607 }
11608 }
11609 FfnKind::Moe(m) => {
11610 if m.route_tau.is_some() || m.mask.is_some() {
11618 return None;
11619 }
11620 let shared = m.shared.as_ref();
11621 let has_shared = shared.is_some();
11622 let shared_gated = matches!(shared, Some((_, Some(_))));
11623 let sgate = match shared {
11624 Some((_, Some(sg))) => gw(sg)?,
11625 _ => gw(&m.router)?,
11629 };
11630 let router = gw(&m.router)?;
11631 if router.prism != crate::gpu::GraphPrismOp::None
11637 || sgate.prism != crate::gpu::GraphPrismOp::None
11638 || router.affine
11639 || sgate.affine
11640 {
11641 tracing::warn!(
11642 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11643 );
11644 return None;
11645 }
11646 let inter = m.experts.first()?.gate_proj.rows();
11647 let mut experts = Vec::with_capacity(m.experts.len() + 1);
11648 let mut q4tp: Option<bool> = None;
11651 let mut gu_q2: Option<bool> = None;
11654 let mut q82: Option<bool> = None;
11656 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
11657 if !matches!(e.act, Act::Silu)
11658 || e.gate_proj.rows() != inter
11659 || e.up_proj.rows() != inter
11660 {
11661 return None;
11662 }
11663 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
11668 let Some((em, ei, _, _)) = expert_weight
11669 .graph_weight()
11670 .or_else(|| expert_weight.graph_weight_descriptor())
11671 else {
11672 return None;
11673 };
11674 let name = &em.tensors[ei].name;
11675 if crate::prism::is_forward_weight(em, name)
11676 || crate::prism::is_inverse_embedding(em, name)
11677 || crate::prism::is_affine_target(em, name)
11678 {
11679 tracing::warn!(
11680 "resident MoE declined: expert Prism/affine transform is not implemented"
11681 );
11682 return None;
11683 }
11684 }
11685 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t() {
11686 Some((mm, gi)) => (
11687 mm,
11688 gi,
11689 e.up_proj.mapped_q4t()?.1,
11690 e.down_proj.mapped_q4t()?.1,
11691 false,
11692 false,
11693 false,
11694 ),
11695 None => match e.gate_proj.mapped_q2tp() {
11696 Some((mm, gi)) => (
11697 mm,
11698 gi,
11699 e.up_proj.mapped_q2tp()?.1,
11700 e.down_proj.mapped_q4tp()?.1,
11701 true,
11702 true,
11703 false,
11704 ),
11705 None => match e.gate_proj.mapped_q4tp() {
11706 Some((mm, gi)) => (
11707 mm,
11708 gi,
11709 e.up_proj.mapped_q4tp()?.1,
11710 e.down_proj.mapped_q4tp()?.1,
11711 true,
11712 false,
11713 false,
11714 ),
11715 None => {
11718 if !crate::gpu::moe_q82_graph_on() {
11719 return None;
11720 }
11721 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
11722 (
11723 mm,
11724 gi,
11725 e.up_proj.mapped_q8_2f()?.1,
11726 e.down_proj.mapped_q8_2f()?.1,
11727 false,
11728 false,
11729 true,
11730 )
11731 }
11732 },
11733 },
11734 };
11735 if *q4tp.get_or_insert(is_p) != is_p
11736 || *gu_q2.get_or_insert(is_q2) != is_q2
11737 || *q82.get_or_insert(is_8) != is_8
11738 {
11739 tracing::warn!(
11745 "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."
11746 );
11747 return None;
11748 }
11749 model.get_or_insert_with(|| mm.clone());
11750 experts.push((gi, ui, di));
11751 }
11752 crate::gpu::GraphFfn::Moe {
11753 router,
11754 shared_gate: sgate,
11755 experts,
11756 n_exp: m.experts.len(),
11757 top_k: std::env::var("CMF_TOPK_PROBE")
11763 .ok()
11764 .and_then(|v| v.parse::<usize>().ok())
11765 .filter(|k| *k > 0 && *k <= m.top_k)
11766 .unwrap_or(m.top_k),
11767 inter,
11768 norm_topk: m.norm_topk_prob,
11769 q4tp: q4tp?,
11770 gu_q2: gu_q2.unwrap_or(false),
11771 q82: q82.unwrap_or(false),
11772 sigmoid: m.router_sigmoid,
11773 bias: m.expert_bias.as_deref(),
11774 has_shared,
11775 shared_gated,
11776 route_scale: m.routed_scaling,
11777 }
11778 }
11779 };
11780 let attn = match &lw.attn {
11781 AttnKind::Full {
11782 wq,
11783 wk,
11784 wv,
11785 wo,
11786 q_norm,
11787 k_norm,
11788 output_gate,
11789 softplus_gate,
11790 bias,
11791 } => {
11792 if self.attention_heads_per_layer.is_some() {
11793 return None;
11794 }
11795 let head_gate = match softplus_gate {
11799 None => None,
11800 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
11801 Some(gw(g)?)
11802 }
11803 Some(_) => {
11804 self.note_graph_decline(
11805 "wgpu token graph",
11806 "projected softplus / per-element output gate",
11807 );
11808 return None;
11809 }
11810 };
11811 let (m, _, _, _) = wq
11812 .graph_weight()
11813 .or_else(|| wq.graph_weight_descriptor())?;
11814 model = Some(m.clone());
11815 crate::gpu::GraphAttn::Full {
11816 wq: gw(wq)?,
11817 wk: gw(wk)?,
11818 wv: gw(wv)?,
11819 wo: gw(wo)?,
11820 q_norm: q_norm.as_deref(),
11821 k_norm: k_norm.as_deref(),
11822 late_qk_norm: self.qk_norm_after_rope,
11823 bias: bias
11824 .as_ref()
11825 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11826 output_gate: *output_gate,
11827 cpu_k: self.kv_cache.layers[li].k_heads(),
11828 cpu_v: self.kv_cache.layers[li].v_heads(),
11829 cpu_base: self.kv_cache.layers[li].base(),
11830 geom: self.graph_attn_geom(li),
11831 head_gate,
11832 }
11833 }
11834 AttnKind::LinearGdn(w) => {
11835 let cfg = self.gdn_cfg?;
11836 let (m, _, _, _) = w
11837 .in_proj_qkv
11838 .graph_weight()
11839 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
11840 model = Some(m.clone());
11841 crate::gpu::GraphAttn::Gdn {
11842 qkv: gw(&w.in_proj_qkv)?,
11843 z: gw(&w.in_proj_z)?,
11844 a: gw(&w.in_proj_a)?,
11845 b: gw(&w.in_proj_b)?,
11846 out: gw(&w.out_proj)?,
11847 conv1d: &w.conv1d,
11848 a_log: &w.a_log,
11849 dt_bias: &w.dt_bias,
11850 norm: &w.norm,
11851 nv: cfg.num_v_heads,
11852 nk: cfg.num_k_heads,
11853 dk: cfg.key_head_dim,
11854 dv: cfg.value_head_dim,
11855 kk: cfg.conv_kernel,
11856 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11857 }
11858 }
11859 AttnKind::ShortConv(w) => {
11860 let cfg = self.short_conv_cfg?;
11861 let (m, _, _, _) = w
11862 .in_proj
11863 .graph_weight()
11864 .or_else(|| w.in_proj.graph_weight_descriptor())?;
11865 model = Some(m.clone());
11866 crate::gpu::GraphAttn::ShortConv {
11867 inp: gw(&w.in_proj)?,
11868 out: gw(&w.out_proj)?,
11869 taps: &w.conv,
11870 kernel: cfg.kernel,
11871 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11872 }
11873 }
11874 _ => return None,
11875 };
11876 layers.push(crate::gpu::GraphLayer {
11877 input_norm: &lw.input_norm,
11878 attn,
11879 post_norm: &lw.post_norm,
11880 attn_out_norm: lw.attn_out_norm.as_deref(),
11881 ffn_out_norm: lw.ffn_out_norm.as_deref(),
11882 layer_scale: lw.layer_scale,
11883 ffn: gffn,
11884 });
11885 }
11886 let model = model?;
11887 let lm_gw = if upto_excl == self.num_layers
11893 && self.graph_want_logits
11894 && std::env::var("CMF_GPU_LMHEAD")
11895 .map(|v| v != "0")
11896 .unwrap_or(true)
11897 {
11898 self.weights
11899 .lm_head
11900 .graph_weight()
11901 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
11902 .map(|(m, i, kind, rs)| {
11903 let name = &m.tensors[i].name;
11904 let prism = if crate::prism::is_inverse_embedding(m, name) {
11905 crate::gpu::GraphPrismOp::InverseEmbedding
11906 } else if crate::prism::is_forward_weight(m, name) {
11907 crate::gpu::GraphPrismOp::Forward
11908 } else {
11909 crate::gpu::GraphPrismOp::None
11910 };
11911 (
11912 crate::gpu::GraphW {
11913 idx: i,
11914 kind,
11915 row_scale: rs,
11916 data: &[],
11917 prism,
11918 affine: crate::prism::is_affine_target(m, name),
11919 },
11920 self.weights.lm_head.rows(),
11921 )
11922 })
11923 } else {
11924 None
11925 };
11926 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
11927 let emb_gw = if steps > 1 {
11929 self.weights
11930 .embed_tokens
11931 .graph_weight()
11932 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
11933 .map(|(m, i, kind, rs)| {
11934 let name = &m.tensors[i].name;
11935 let prism = if crate::prism::is_inverse_embedding(m, name) {
11936 crate::gpu::GraphPrismOp::InverseEmbedding
11937 } else if crate::prism::is_forward_weight(m, name) {
11938 crate::gpu::GraphPrismOp::Forward
11939 } else {
11940 crate::gpu::GraphPrismOp::None
11941 };
11942 (
11943 crate::gpu::GraphW {
11944 idx: i,
11945 kind,
11946 row_scale: rs,
11947 data: &[],
11948 prism,
11949 affine: crate::prism::is_affine_target(m, name),
11950 },
11951 self.weights.embed_tokens.rows(),
11952 self.embed_multiplier,
11953 )
11954 })
11955 } else {
11956 None
11957 };
11958
11959 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
11965 (from..upto_excl.min(self.num_layers - 1))
11966 .filter(|&li| (li + 1) % self.physical_layers == 0)
11967 .map(|li| li - from)
11968 .collect()
11969 } else {
11970 Vec::new()
11971 };
11972 let mut h = hidden.to_vec();
11973 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
11979 let outcome = crate::gpu::forward_token_graph(
11980 &model,
11981 self.graph_kv_id,
11982 &layers,
11983 &o1_views,
11984 self.o1_epoch,
11985 &self.inv_freq,
11986 &mut h,
11987 nh,
11988 nkv,
11989 hd,
11990 self.attn_scale,
11991 rd,
11992 self.hidden_size,
11993 self.intermediate_size,
11994 position,
11995 self.kv_cache.max_seq_len,
11996 gemma,
11997 self.rms_eps as f32,
11998 lm,
11999 &self.weights.final_norm,
12000 logits_out,
12001 &loop_norm_at,
12002 steps,
12003 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
12004 ids_out,
12005 layers_run,
12006 from,
12007 dump_hidden,
12008 );
12009 match outcome {
12010 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
12011 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
12012 crate::gpu::TokenGraphOutcome::Declined => None,
12013 }
12014 }
12015
12016 #[cfg(target_os = "macos")]
12025 #[allow(clippy::type_complexity)]
12026 fn metal_rows_plan(
12027 &self,
12028 ) -> Option<(
12029 Vec<MetalRowsItem<'_>>,
12030 std::sync::Arc<cortiq_core::CmfModel>,
12031 Option<crate::gpu_metal::GdnGpuCfg>,
12032 )> {
12033 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
12034 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
12035 if !graph_force
12036 || !crate::gpu::enabled_here()
12037 || std::env::var("CMF_GPU_BLOCK")
12038 .map(|v| v == "0")
12039 .unwrap_or(false)
12040 || self.attn_softcap > 0.0
12041 || self.o1_active()
12042 || self.swa.is_some()
12043 || self.global_attn.is_some()
12044 || self.attention_heads_per_layer.is_some()
12045 || self.graph_attn_decline_reason().is_some()
12047 || self.attn_v_norm
12048 || self.loop_final_norm
12049 || !self.metal_rope_scale_ok()
12050 {
12051 return None;
12052 }
12053 let attend_contract = self.head_dim % 4 == 0
12054 && self.head_dim <= 256
12055 && self.rotary_dim >= 2
12056 && self.rotary_dim <= self.head_dim
12057 && (self.rotary_dim / 2) % 32 == 0
12058 && self.num_kv_heads > 0
12059 && self.num_heads % self.num_kv_heads == 0;
12060 if !attend_contract {
12061 return None;
12062 }
12063 let mut plan: Vec<MetalRowsItem> = Vec::new();
12064 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
12065 for li in 0..self.num_layers {
12066 let lw = &self.weights.layers[self.phys_layer(li)];
12067 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
12068 return None;
12069 }
12070 let ffn = match &lw.ffn {
12071 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
12072 let (Some(g), Some(u), Some(dn)) = (
12073 d.gate_proj.metal_graph_parts(),
12074 d.up_proj.metal_graph_parts(),
12075 d.down_proj.metal_graph_parts(),
12076 ) else {
12077 return None;
12078 };
12079 MetalFfn::Dense {
12080 gate: g,
12081 up: u,
12082 down: dn,
12083 act: 0, }
12085 }
12086 _ => return None,
12087 };
12088 match &lw.attn {
12089 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
12090 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
12091 w.in_proj_qkv.metal_graph_parts(),
12092 w.in_proj_z.metal_graph_parts(),
12093 w.in_proj_a.f32_parts(),
12094 w.in_proj_b.f32_parts(),
12095 w.out_proj.metal_graph_parts(),
12096 ) else {
12097 return None;
12098 };
12099 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
12100 model_ref.get_or_insert_with(|| model.clone());
12101 }
12102 let gl = GdnGpuLayer {
12103 attn_norm: &lw.input_norm,
12104 post_norm: &lw.post_norm,
12105 qkv,
12106 z,
12107 a,
12108 b: bb,
12109 out,
12110 ffn,
12111 conv1d: &w.conv1d,
12112 a_log: &w.a_log,
12113 dt_bias: &w.dt_bias,
12114 gnorm: &w.norm,
12115 };
12116 match plan.last_mut() {
12117 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
12118 _ => plan.push(MetalRowsItem::Gdn {
12119 run: vec![gl],
12120 first: li,
12121 }),
12122 }
12123 }
12124 AttnKind::Full {
12125 wq,
12126 wk,
12127 wv,
12128 wo,
12129 q_norm,
12130 k_norm,
12131 output_gate,
12132 softplus_gate: None,
12133 bias: None,
12134 } => {
12135 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12136 (
12137 wq.metal_graph_parts(),
12138 wk.metal_graph_parts(),
12139 wv.metal_graph_parts(),
12140 wo.metal_graph_parts(),
12141 )
12142 else {
12143 return None;
12144 };
12145 if let QTensor::Mapped { model, .. } = wq {
12146 model_ref.get_or_insert_with(|| model.clone());
12147 }
12148 let cache = &self.kv_cache.layers[li];
12149 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
12150 return None;
12151 }
12152 plan.push(MetalRowsItem::Attn {
12153 l: AttnGpuLayer {
12154 attn_norm: &lw.input_norm,
12155 post_norm: &lw.post_norm,
12156 wq: pq,
12157 wk: pk,
12158 wv: pv,
12159 wo: po,
12160 ffn,
12161 attn_out_norm: None,
12162 ffn_out_norm: None,
12163 layer_scale: None,
12164 },
12165 li,
12166 q_norm: q_norm.as_deref(),
12167 k_norm: k_norm.as_deref(),
12168 output_gate: *output_gate,
12169 });
12170 }
12171 _ => return None,
12172 }
12173 }
12174 let model = model_ref?;
12175 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
12176 nv: cfg.num_v_heads,
12177 nk: cfg.num_k_heads,
12178 dk: cfg.key_head_dim,
12179 dv: cfg.value_head_dim,
12180 kk: cfg.conv_kernel,
12181 hidden: self.hidden_size,
12182 inter: self.intermediate_size,
12183 c_dim: cfg.conv_dim(),
12184 eps: cfg.rms_eps as f32,
12185 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12186 });
12187 Some((plan, model, gcfg))
12188 }
12189
12190 #[cfg(target_os = "macos")]
12192 #[allow(clippy::too_many_arguments)]
12193 fn metal_attn_params<'a>(
12194 li: usize,
12195 cache: &'a crate::kv_cache::LayerKvCache,
12196 q_norm: Option<&'a [f32]>,
12197 k_norm: Option<&'a [f32]>,
12198 output_gate: bool,
12199 inv_freq: &'a [f32],
12200 rope_scale: f32,
12201 geom: (usize, usize, usize, usize),
12202 pos0: usize,
12203 kv_id: u64,
12204 scale: f32,
12205 eps: f32,
12206 gemma: bool,
12207 late_qk_norm: bool,
12208 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
12209 let (nh, nkv, hd, rd) = geom;
12210 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12211 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12212 let cpu_stored = cpu_k[0].len() / hd;
12213 (
12214 crate::gpu_metal::AttnDeviceParams {
12215 kv_id,
12216 layer: li,
12217 nh,
12218 nkv,
12219 hd,
12220 rd,
12221 position: pos0,
12222 scale,
12223 eps,
12224 gemma,
12225 late_qk_norm,
12226 output_gate,
12227 q_norm,
12228 k_norm,
12229 inv_freq,
12230 rope_scale,
12231 cpu_k,
12232 cpu_v,
12233 cpu_stored,
12234 cpu_gen: cache.generation(),
12235 o1: None,
12236 window: None,
12237 head_gate: None,
12238 v_norm: false,
12239 },
12240 cpu_stored,
12241 )
12242 }
12243
12244 #[cfg(target_os = "macos")]
12249 #[allow(clippy::type_complexity)]
12250 fn metal_rows_run(
12251 &mut self,
12252 hiddens: &mut [f32],
12253 pos0: usize,
12254 b: usize,
12255 prefill: bool,
12256 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12257 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
12261 ) -> MetalRowsRun {
12262 use crate::gpu_metal::{GraphDims, VerifyGraph};
12263 if !crate::gpu_metal::wait_replay() {
12269 tracing::error!("Metal rows graph: the pending async replay failed");
12270 return MetalRowsRun::Failed;
12271 }
12272 spec_stamp("v.wait");
12273 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
12279 if want > 0 {
12280 let phys = self.physical_layers.max(1);
12281 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
12282 let is_gdn = self
12283 .weights
12284 .layers
12285 .get(li % phys)
12286 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
12287 if is_gdn && l.linear_state.len() != want {
12288 l.linear_state = vec![0f32; want];
12289 }
12290 }
12291 }
12292 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
12293 return MetalRowsRun::Declined;
12294 };
12295 spec_stamp("v.plan");
12296 let dims = GraphDims {
12297 hidden: self.hidden_size,
12298 eps: self.rms_eps as f32,
12299 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12300 };
12301 let Some(mut graph) = (if prefill {
12302 VerifyGraph::new_prefill(&model, dims, hiddens, b)
12303 } else {
12304 VerifyGraph::new(&model, dims, hiddens, b)
12305 }) else {
12306 return MetalRowsRun::Declined;
12307 };
12308 let geom = (
12309 self.num_heads,
12310 self.num_kv_heads,
12311 self.head_dim,
12312 self.rotary_dim,
12313 );
12314 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
12315 let eps = self.rms_eps as f32;
12316 let kv_id = self.graph_kv_id;
12317 let inv_freq = self.inv_freq.clone();
12318 for item in &plan {
12319 let ok = match item {
12320 MetalRowsItem::Gdn { run, .. } => gcfg
12321 .as_ref()
12322 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
12323 .unwrap_or(false),
12324 MetalRowsItem::Attn {
12325 l,
12326 li,
12327 q_norm,
12328 k_norm,
12329 output_gate,
12330 } => {
12331 let (p, _) = Self::metal_attn_params(
12332 *li,
12333 &self.kv_cache.layers[*li],
12334 *q_norm,
12335 *k_norm,
12336 *output_gate,
12337 &inv_freq,
12338 self.layer_rope_scale(*li),
12339 geom,
12340 pos0,
12341 kv_id,
12342 self.attn_scale,
12343 eps,
12344 gemma,
12345 self.qk_norm_after_rope,
12346 );
12347 graph.attn_ok(l, &p)
12348 }
12349 };
12350 if !ok {
12351 use std::sync::atomic::{AtomicBool, Ordering};
12352 static SAID: AtomicBool = AtomicBool::new(false);
12353 if !SAID.swap(true, Ordering::Relaxed) {
12354 tracing::warn!("metal rows graph: a layer failed preflight — declining");
12355 }
12356 return MetalRowsRun::Declined;
12357 }
12358 }
12359 let lm = match &spec {
12360 Some((lm, _, _)) => {
12361 if !graph.lm_head_ok(*lm) {
12362 return MetalRowsRun::Declined;
12363 }
12364 Some(*lm)
12365 }
12366 None => None,
12367 };
12368 let mut gdn_layers = Vec::new();
12369 let mut attn_layers = Vec::new();
12370 for item in &plan {
12371 match item {
12372 MetalRowsItem::Gdn { run, first } => {
12373 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
12374 .iter()
12375 .map(|l| l.linear_state.as_slice())
12376 .collect();
12377 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
12378 return MetalRowsRun::Declined;
12379 }
12380 gdn_layers.extend(*first..*first + run.len());
12381 }
12382 MetalRowsItem::Attn {
12383 l,
12384 li,
12385 q_norm,
12386 k_norm,
12387 output_gate,
12388 } => {
12389 let (p, cpu_stored) = Self::metal_attn_params(
12390 *li,
12391 &self.kv_cache.layers[*li],
12392 *q_norm,
12393 *k_norm,
12394 *output_gate,
12395 &inv_freq,
12396 self.layer_rope_scale(*li),
12397 geom,
12398 pos0,
12399 kv_id,
12400 self.attn_scale,
12401 eps,
12402 gemma,
12403 self.qk_norm_after_rope,
12404 );
12405 if !graph.encode_attn_b(l, &p) {
12406 return MetalRowsRun::Declined;
12407 }
12408 attn_layers.push((*li, cpu_stored));
12409 }
12410 }
12411 }
12412 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
12413 if !graph.encode_lm_head_b(final_norm, lm) {
12414 return MetalRowsRun::Declined;
12415 }
12416 if let Some((n, _)) = argmax_out.as_ref() {
12421 if !graph.encode_argmax_b(*n) {
12422 argmax_out = None;
12423 }
12424 }
12425 }
12426 spec_stamp("v.enc");
12427 if !graph.sync() {
12428 return MetalRowsRun::Failed;
12429 }
12430 spec_stamp("v.gpu");
12431 match (spec, argmax_out) {
12432 (Some(_), Some((_, ids))) => {
12433 ids.resize(b, 0);
12434 if !graph.read_argmax(ids) {
12435 return MetalRowsRun::Failed;
12436 }
12437 spec_stamp("v.am");
12438 }
12439 (Some((lm, _, logits)), None) => {
12440 logits.resize(b * lm.1, 0.0);
12441 if !graph.read_logits(logits) {
12442 return MetalRowsRun::Failed;
12443 }
12444 spec_stamp("v.lg");
12445 }
12446 (None, _) => {}
12447 }
12448 if !graph.read_hidden(hiddens) {
12449 return MetalRowsRun::Failed;
12450 }
12451 spec_stamp("v.hid");
12452 MetalRowsRun::Completed(MetalVerifyPending {
12453 graph,
12454 gdn_layers,
12455 attn_layers,
12456 })
12457 }
12458
12459 #[cfg(target_os = "macos")]
12465 fn try_batch_graph_metal(
12466 &mut self,
12467 hiddens: &mut [f32],
12468 positions: &[usize],
12469 b: usize,
12470 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12471 argmax_out: Option<(usize, &mut Vec<u32>)>,
12472 ) -> crate::gpu::BatchGraphOutcome {
12473 let _t0 = std::time::Instant::now();
12474 if positions.len() != b
12475 || positions.windows(2).any(|w| w[1] != w[0] + 1)
12476 || hiddens.len() != b * self.hidden_size
12477 {
12478 return crate::gpu::BatchGraphOutcome::Declined;
12479 }
12480 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12481 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12482 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12483 MetalRowsRun::Completed(pending) => pending,
12484 };
12485 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12486 eprintln!(
12487 "metal-verify: {:.1} ms | b={b}",
12488 _t0.elapsed().as_secs_f64() * 1e3
12489 );
12490 }
12491 self.metal_verify = Some(pending);
12492 crate::gpu::BatchGraphOutcome::Completed
12493 }
12494
12495 #[cfg(target_os = "macos")]
12500 fn prefill_rows_metal(
12501 &mut self,
12502 ids: &[u32],
12503 start_pos: usize,
12504 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12505 ) -> MetalPrefillOutcome {
12506 let b = ids.len();
12507 if b == 0 || b > 512 {
12508 return MetalPrefillOutcome::Declined;
12509 }
12510 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12511 let with_head = spec.is_some();
12512 let hs = self.hidden_size;
12513 let mut hiddens = vec![0f32; b * hs];
12514 for (j, &id) in ids.iter().enumerate() {
12515 let e = self.embed_single(id);
12516 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12517 }
12518 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12519 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12520 MetalRowsRun::Failed => {
12521 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12522 return MetalPrefillOutcome::Failed;
12523 }
12524 MetalRowsRun::Completed(pending) => pending,
12525 };
12526 let idxs = pending.gdn_layers.clone();
12528 let mut outs: Vec<&mut [f32]> = self
12529 .kv_cache
12530 .layers
12531 .iter_mut()
12532 .enumerate()
12533 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12534 .map(|(_, l)| l.linear_state.as_mut_slice())
12535 .collect();
12536 if !pending.graph.finish_states(&mut outs) {
12537 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12538 return MetalPrefillOutcome::Failed;
12539 }
12540 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12541 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12545 for (li, cpu_stored) in &pending.attn_layers {
12546 let mut kbuf = vec![0f32; b * nkv * hd];
12547 let mut vbuf = vec![0f32; b * nkv * hd];
12548 if !crate::gpu_metal::kv_mirror_read_rows(
12549 self.graph_kv_id,
12550 *li,
12551 nkv,
12552 hd,
12553 *cpu_stored,
12554 b,
12555 &mut kbuf,
12556 &mut vbuf,
12557 ) {
12558 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12559 return MetalPrefillOutcome::Failed;
12560 }
12561 rows.push((*li, *cpu_stored, kbuf, vbuf));
12562 }
12563 for (li, cpu_stored, kbuf, vbuf) in rows {
12564 let cache = &mut self.kv_cache.layers[li];
12565 for r in 0..b {
12566 cache.append(
12567 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12568 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12569 &[],
12570 );
12571 }
12572 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12573 }
12574 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12575 if with_head {
12576 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12577 }
12578 MetalPrefillOutcome::Completed(hiddens)
12579 }
12580
12581 #[cfg(target_os = "macos")]
12582 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12583 self.prefill_rows_metal(ids, start_pos, None)
12584 }
12585
12586 #[cfg(target_os = "macos")]
12591 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12592 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12593 return MetalBatchNllOutcome::Declined;
12594 }
12595 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12596 return MetalBatchNllOutcome::Declined;
12597 };
12598 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12599 .ok()
12600 .and_then(|v| v.parse::<usize>().ok())
12601 .filter(|&v| (1..=512).contains(&v))
12602 .unwrap_or(32);
12603 let final_norm = self.weights.final_norm.clone();
12604 let mut nll = 0.0f64;
12605 let mut count = 0usize;
12606 let mut pos = 0usize;
12607 let mut completed = 0usize;
12608 while pos < ids.len() {
12609 let end = (pos + chunk).min(ids.len());
12610 let mut logits = Vec::new();
12611 let outcome = self.prefill_rows_metal(
12612 &ids[pos..end],
12613 pos,
12614 Some((lm, &final_norm, &mut logits)),
12615 );
12616 match outcome {
12617 MetalPrefillOutcome::Declined => {
12618 return if completed == 0 {
12619 MetalBatchNllOutcome::Declined
12620 } else {
12621 MetalBatchNllOutcome::Failed(format!(
12622 "ordinary Metal NLL batch declined after {completed} chunks"
12623 ))
12624 };
12625 }
12626 MetalPrefillOutcome::Failed => {
12627 return MetalBatchNllOutcome::Failed(
12628 "ordinary Metal NLL batch failed after admission".to_string(),
12629 );
12630 }
12631 MetalPrefillOutcome::Completed(_) => {}
12632 }
12633 completed += 1;
12634 let vocab = self.vocab_size.min(lm.1);
12635 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12636 return MetalBatchNllOutcome::Failed(
12637 "ordinary Metal NLL head returned an invalid shape".to_string(),
12638 );
12639 }
12640 for row in 0..(end - pos) {
12641 let absolute = pos + row;
12642 if absolute < start || absolute + 1 >= ids.len() {
12643 continue;
12644 }
12645 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
12646 if let Some(mu) = self.logit_multiplier {
12647 for v in lg.iter_mut() {
12648 *v *= mu;
12649 }
12650 }
12651 if let Some(c) = self.final_softcap {
12652 softcap_in_place(lg, c, self.pool.as_deref());
12653 }
12654 let target = ids[absolute + 1] as usize;
12655 if target >= vocab {
12656 return MetalBatchNllOutcome::Failed(format!(
12657 "target token {target} exceeds Metal head rows {vocab}"
12658 ));
12659 }
12660 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
12661 let lse: f64 = lg
12662 .iter()
12663 .map(|&v| ((v - max) as f64).exp())
12664 .sum::<f64>()
12665 .ln()
12666 + max as f64;
12667 nll += lse - lg[target] as f64;
12668 count += 1;
12669 }
12670 pos = end;
12671 }
12672 MetalBatchNllOutcome::Completed(nll, count)
12673 }
12674
12675 #[cfg(target_os = "macos")]
12679 fn metal_verify_commit(&mut self, a: usize) -> bool {
12680 let Some(mut pending) = self.metal_verify.take() else {
12681 return false;
12682 };
12683 let n = a + 1;
12684 let idxs = pending.gdn_layers.clone();
12686 let mut outs: Vec<&mut [f32]> = self
12687 .kv_cache
12688 .layers
12689 .iter_mut()
12690 .enumerate()
12691 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12692 .map(|(_, l)| l.linear_state.as_mut_slice())
12693 .collect();
12694 if !pending.graph.commit(n, &mut outs) {
12695 return false;
12696 }
12697 spec_stamp("c.replay");
12698 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12699 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12703 for (li, cpu_stored) in &pending.attn_layers {
12704 let mut kbuf = vec![0f32; n * nkv * hd];
12705 let mut vbuf = vec![0f32; n * nkv * hd];
12706 if !crate::gpu_metal::kv_mirror_read_rows(
12707 self.graph_kv_id,
12708 *li,
12709 nkv,
12710 hd,
12711 *cpu_stored,
12712 n,
12713 &mut kbuf,
12714 &mut vbuf,
12715 ) {
12716 return false;
12717 }
12718 rows.push((*li, *cpu_stored, kbuf, vbuf));
12719 }
12720 for (li, cpu_stored, kbuf, vbuf) in rows {
12721 let cache = &mut self.kv_cache.layers[li];
12722 for r in 0..n {
12723 cache.append(
12724 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12725 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12726 &[],
12727 );
12728 }
12729 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
12730 }
12731 spec_stamp("c.kv");
12732 true
12733 }
12734
12735 #[cfg(target_os = "macos")]
12742 fn mtp_warm_batch_submit(
12743 &mut self,
12744 m: &mut MtpModule,
12745 pairs: &[(&[f32], u32)],
12746 first_pos: usize,
12747 ) -> Option<MetalWarmPending> {
12748 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
12749 let b = pairs.len();
12750 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
12751 return None;
12752 }
12753 let AttnKind::Full {
12754 wq,
12755 wk,
12756 wv,
12757 wo,
12758 q_norm,
12759 k_norm,
12760 output_gate,
12761 softplus_gate: None,
12762 bias: None,
12763 } = &m.layer.attn
12764 else {
12765 return None;
12766 };
12767 let FfnKind::Dense(d) = &m.layer.ffn else {
12768 return None;
12769 };
12770 if !d.segs.is_empty() {
12771 return None;
12772 }
12773 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12774 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
12775 else {
12776 return None;
12777 };
12778 let (Some(g), Some(u), Some(dn)) = (
12779 d.gate_proj.q1_parts(),
12780 d.up_proj.q1_parts(),
12781 d.down_proj.q1_parts(),
12782 ) else {
12783 return None;
12784 };
12785 let Some(eh) = m.eh_proj.q1_parts() else {
12786 return None;
12787 };
12788 let QTensor::Mapped { model, .. } = wq else {
12789 return None;
12790 };
12791 let model = model.clone();
12792 let hs = self.hidden_size;
12793 let mut cat = vec![0f32; b * 2 * hs];
12795 for (j, (h, tok)) in pairs.iter().enumerate() {
12796 let e = self.embed_single(*tok);
12797 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
12798 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
12799 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
12800 }
12801 let dims = GraphDims {
12802 hidden: hs,
12803 eps: self.rms_eps as f32,
12804 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12805 };
12806 spec_stamp("w.cat");
12807 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
12808 return None;
12809 };
12810 spec_stamp("w.new");
12811 let l = AttnGpuLayer {
12812 attn_norm: &m.layer.input_norm,
12813 post_norm: &m.layer.post_norm,
12814 wq: pq,
12815 wk: pk,
12816 wv: pv,
12817 wo: po,
12818 ffn: MetalFfn::Dense {
12819 gate: g,
12820 up: u,
12821 down: dn,
12822 act: (d.act == Act::Gelu) as u8,
12823 },
12824 attn_out_norm: None,
12825 ffn_out_norm: None,
12826 layer_scale: None,
12827 };
12828 let (nh, nkv, hd, rd) = (
12829 self.num_heads,
12830 self.num_kv_heads,
12831 self.head_dim,
12832 self.rotary_dim,
12833 );
12834 let inv_freq = self.inv_freq.clone();
12835 let cpu_stored;
12836 {
12837 let cache = &m.kv;
12838 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12839 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12840 cpu_stored = cpu_k[0].len() / hd;
12841 if cpu_stored > first_pos {
12846 spec_stamp("w.decl");
12847 return None;
12848 }
12849 let p = AttnDeviceParams {
12850 kv_id: self.mtp_kv_id(),
12851 layer: Self::MTP_LAYER_BASE,
12852 nh,
12853 nkv,
12854 hd,
12855 rd,
12856 position: first_pos,
12857 scale: self.attn_scale,
12858 eps: self.rms_eps as f32,
12859 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12860 late_qk_norm: self.qk_norm_after_rope,
12861 output_gate: *output_gate,
12862 q_norm: q_norm.as_deref(),
12863 k_norm: k_norm.as_deref(),
12864 inv_freq: &inv_freq,
12865 rope_scale: self.rope_scale,
12866 cpu_k,
12867 cpu_v,
12868 cpu_stored,
12869 cpu_gen: cache.generation(),
12870 o1: None,
12871 window: None,
12872 head_gate: None,
12873 v_norm: false,
12874 };
12875 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
12876 return None;
12877 }
12878 }
12879 spec_stamp("w.enc");
12880 if !graph.submit() {
12881 return None;
12882 }
12883 spec_stamp("w.sub");
12884 Some(MetalWarmPending {
12885 graph,
12886 cpu_stored,
12887 b,
12888 })
12889 }
12890
12891 #[cfg(target_os = "macos")]
12894 fn mtp_warm_batch_metal(
12895 &mut self,
12896 m: &mut MtpModule,
12897 pairs: &[(&[f32], u32)],
12898 first_pos: usize,
12899 ) -> bool {
12900 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
12901 Some(p) => self.mtp_warm_batch_finish(m, p),
12902 None => false,
12903 }
12904 }
12905
12906 #[cfg(target_os = "macos")]
12911 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
12912 let MetalWarmPending {
12913 mut graph,
12914 cpu_stored,
12915 b,
12916 } = pending;
12917 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12918 if !graph.sync() {
12919 return false;
12920 }
12921 spec_stamp("w.gpu");
12922 let mut kbuf = vec![0f32; b * nkv * hd];
12923 let mut vbuf = vec![0f32; b * nkv * hd];
12924 if !crate::gpu_metal::kv_mirror_read_rows(
12925 self.mtp_kv_id(),
12926 Self::MTP_LAYER_BASE,
12927 nkv,
12928 hd,
12929 cpu_stored,
12930 b,
12931 &mut kbuf,
12932 &mut vbuf,
12933 ) {
12934 return false;
12935 }
12936 for r in 0..b {
12937 m.kv.append(
12938 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12939 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12940 &[],
12941 );
12942 }
12943 crate::gpu_metal::kv_mirror_set_stored(
12944 self.mtp_kv_id(),
12945 Self::MTP_LAYER_BASE,
12946 cpu_stored + b,
12947 );
12948 spec_stamp("w.kv");
12949 true
12950 }
12951
12952 pub(crate) fn note_draft_id(&mut self, id: u32) {
12959 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
12960 if (id as usize) >= cut {
12961 self.draft_full_streak = 16;
12962 } else {
12963 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
12964 }
12965 }
12966
12967 fn draft_head_rows(&self, head_rows: usize) -> usize {
12970 if self.draft_full_streak > 0 {
12971 head_rows
12972 } else {
12973 Self::draft_vocab_rows(head_rows)
12974 }
12975 }
12976
12977 fn draft_vocab_rows(head_rows: usize) -> usize {
12980 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12981 let n = *N.get_or_init(|| {
12982 std::env::var("CMF_DRAFT_VOCAB")
12983 .ok()
12984 .and_then(|v| v.parse().ok())
12985 .unwrap_or(65536)
12986 });
12987 if n == 0 { head_rows } else { n.min(head_rows) }
12988 }
12989
12990 #[cfg(target_os = "macos")]
12995 fn mtp_step_metal(
12996 &mut self,
12997 m: &mut MtpModule,
12998 hidden: &[f32],
12999 next_token: u32,
13000 position: usize,
13001 want_logits: bool,
13002 ) -> Option<(Vec<f32>, Vec<f32>)> {
13003 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13004 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13005 || !crate::gpu::q1_force()
13006 || !crate::gpu::enabled_here()
13007 || self.attn_softcap > 0.0
13008 || self.attention_heads_per_layer.is_some()
13009 || m.kv.mode != crate::kv_cache::KvMode::F32
13010 || m.kv.o1.is_some()
13011 {
13012 return None;
13013 }
13014 let AttnKind::Full {
13015 wq,
13016 wk,
13017 wv,
13018 wo,
13019 q_norm,
13020 k_norm,
13021 output_gate,
13022 softplus_gate: None,
13023 bias: None,
13024 } = &m.layer.attn
13025 else {
13026 return None;
13027 };
13028 let FfnKind::Dense(d) = &m.layer.ffn else {
13029 return None;
13030 };
13031 if d.act != Act::Silu || !d.segs.is_empty() {
13032 return None;
13033 }
13034 let (pq, pk, pv, po) = (
13035 wq.q1_parts()?,
13036 wk.q1_parts()?,
13037 wv.q1_parts()?,
13038 wo.q1_parts()?,
13039 );
13040 let (g, u, dn) = (
13041 d.gate_proj.q1_parts()?,
13042 d.up_proj.q1_parts()?,
13043 d.down_proj.q1_parts()?,
13044 );
13045 let QTensor::Mapped { model, .. } = wq else {
13046 return None;
13047 };
13048 let model = model.clone();
13049 let lm = if want_logits {
13050 Some(self.weights.lm_head.q1_parts()?)
13051 } else {
13052 None
13053 };
13054 let dims = GraphDims {
13055 hidden: self.hidden_size,
13056 eps: self.rms_eps as f32,
13057 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13058 };
13059 let hs = self.hidden_size;
13062 let mut x = vec![0f32; hs];
13063 let mut graph = TokenGraph::new(&model, dims, &x)?;
13064 let mut folded = false;
13065 if let Some(eh) = m.eh_proj.q1_parts() {
13066 let e = self.embed_single(next_token);
13067 let mut cat = vec![0.0f32; 2 * hs];
13068 let (cat_e, cat_h) = cat.split_at_mut(hs);
13069 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
13070 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
13071 folded = graph.encode_input_proj(eh, &cat);
13072 }
13073 if !folded {
13074 x = self.mtp_block_input(m, hidden, next_token);
13075 graph = TokenGraph::new(&model, dims, &x)?;
13076 }
13077 spec_stamp("d.in");
13078 let l = AttnGpuLayer {
13079 attn_norm: &m.layer.input_norm,
13080 post_norm: &m.layer.post_norm,
13081 wq: pq,
13082 wk: pk,
13083 wv: pv,
13084 wo: po,
13085 ffn: MetalFfn::Dense {
13086 gate: g,
13087 up: u,
13088 down: dn,
13089 act: (d.act == Act::Gelu) as u8,
13090 },
13091 attn_out_norm: None,
13092 ffn_out_norm: None,
13093 layer_scale: None,
13094 };
13095 let (nh, nkv, hd, rd) = (
13096 self.num_heads,
13097 self.num_kv_heads,
13098 self.head_dim,
13099 self.rotary_dim,
13100 );
13101 let inv_freq = self.inv_freq.clone();
13102 {
13103 let cache = &m.kv;
13104 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13105 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13106 let cpu_stored = cpu_k[0].len() / hd;
13107 let p = AttnDeviceParams {
13108 kv_id: self.mtp_kv_id(),
13109 layer: Self::MTP_LAYER_BASE,
13110 nh,
13111 nkv,
13112 hd,
13113 rd,
13114 position,
13115 scale: self.attn_scale,
13116 eps: self.rms_eps as f32,
13117 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13118 late_qk_norm: self.qk_norm_after_rope,
13119 output_gate: *output_gate,
13120 q_norm: q_norm.as_deref(),
13121 k_norm: k_norm.as_deref(),
13122 inv_freq: &inv_freq,
13123 rope_scale: self.rope_scale,
13124 cpu_k,
13125 cpu_v,
13126 cpu_stored,
13127 cpu_gen: cache.generation(),
13128 o1: None,
13129 window: None,
13130 head_gate: None,
13131 v_norm: false,
13132 };
13133 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13134 return None;
13135 }
13136 }
13137 let draft_rows = if let Some(lm) = lm {
13143 self.draft_head_rows(lm.1)
13144 } else {
13145 0
13146 };
13147 if let Some(lm) = lm {
13148 if !graph.lm_head_ok(lm) {
13149 return None;
13150 }
13151 if draft_rows < lm.1 {
13152 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13153 return None;
13154 }
13155 } else {
13156 graph.encode_lm_head(&m.final_norm, lm);
13157 }
13158 }
13159 spec_stamp("d.enc");
13160 if graph.sync_checked().is_err() {
13161 return None;
13162 }
13163 spec_stamp("d.gpu");
13164 let mut logits = Vec::new();
13165 if let Some(lm) = lm {
13166 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
13167 logits = attention::take_buf(n_read);
13168 graph.read_logits(&mut logits);
13169 logits.resize(self.vocab_size, f32::NEG_INFINITY);
13171 }
13172 graph.finish(&mut x);
13173 let mut krow = attention::take_buf(nkv * hd);
13174 let mut vrow = attention::take_buf(nkv * hd);
13175 if crate::gpu_metal::kv_mirror_read_last(
13176 self.mtp_kv_id(),
13177 Self::MTP_LAYER_BASE,
13178 nkv,
13179 hd,
13180 &mut krow,
13181 &mut vrow,
13182 ) {
13183 m.kv.append(&krow, &vrow, &[]);
13184 }
13185 attention::recycle_buf(&mut krow);
13186 attention::recycle_buf(&mut vrow);
13187 spec_stamp("d.rd");
13188 Some((logits, x))
13189 }
13190
13191 fn mtp_chain_on() -> bool {
13204 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13205 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
13206 }
13207
13208 #[cfg(target_os = "macos")]
13220 fn mtp_draft_chain_metal(
13221 &mut self,
13222 m: &mut MtpModule,
13223 hidden: &[f32],
13224 t_next: u32,
13225 position: usize,
13226 k: usize,
13227 ) -> Result<Vec<u32>, bool> {
13228 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
13229 if k == 0
13230 || k > 64
13231 || !Self::mtp_chain_on()
13232 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
13233 || !crate::gpu::q1_force()
13234 || !crate::gpu::enabled_here()
13235 || self.attn_softcap > 0.0
13236 || self.attention_heads_per_layer.is_some()
13237 || m.kv.mode != crate::kv_cache::KvMode::F32
13238 || m.kv.o1.is_some()
13239 || self.dsv4.is_some()
13241 || self.dsv41.is_some()
13242 || self.qwen4_exp.is_some()
13243 || self.g3n.is_some()
13244 {
13245 return Err(false);
13246 }
13247 let AttnKind::Full {
13248 wq,
13249 wk,
13250 wv,
13251 wo,
13252 q_norm,
13253 k_norm,
13254 output_gate,
13255 softplus_gate: None,
13256 bias: None,
13257 } = &m.layer.attn
13258 else {
13259 return Err(false);
13260 };
13261 let FfnKind::Dense(d) = &m.layer.ffn else {
13262 return Err(false);
13263 };
13264 if d.act != Act::Silu || !d.segs.is_empty() {
13265 return Err(false);
13266 }
13267 let (Some(pq), Some(pk), Some(pv), Some(po)) =
13268 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
13269 else {
13270 return Err(false);
13271 };
13272 let (Some(g), Some(u), Some(dn)) = (
13273 d.gate_proj.q1_parts(),
13274 d.up_proj.q1_parts(),
13275 d.down_proj.q1_parts(),
13276 ) else {
13277 return Err(false);
13278 };
13279 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
13280 return Err(false);
13281 };
13282 let QTensor::Mapped { model, .. } = wq else {
13283 return Err(false);
13284 };
13285 let model = model.clone();
13286 let QTensor::Mapped {
13289 model: em,
13290 idx: eidx,
13291 dtype: cortiq_core::TensorDtype::Q4TiledP,
13292 ..
13293 } = &self.weights.embed_tokens
13294 else {
13295 return Err(false);
13296 };
13297 if !std::sync::Arc::ptr_eq(em, &model)
13298 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
13299 {
13300 return Err(false);
13301 }
13302 let embed = (
13303 *eidx,
13304 self.weights.embed_tokens.rows(),
13305 self.weights.embed_tokens.cols(),
13306 );
13307 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
13308 return Err(false);
13309 }
13310 let dims = GraphDims {
13311 hidden: self.hidden_size,
13312 eps: self.rms_eps as f32,
13313 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13314 };
13315 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
13316 return Err(false);
13317 };
13318 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
13319 return Err(false);
13320 }
13321 let l = AttnGpuLayer {
13322 attn_norm: &m.layer.input_norm,
13323 post_norm: &m.layer.post_norm,
13324 wq: pq,
13325 wk: pk,
13326 wv: pv,
13327 wo: po,
13328 ffn: MetalFfn::Dense {
13329 gate: g,
13330 up: u,
13331 down: dn,
13332 act: (d.act == Act::Gelu) as u8,
13333 },
13334 attn_out_norm: None,
13335 ffn_out_norm: None,
13336 layer_scale: None,
13337 };
13338 let (nh, nkv, hd, rd) = (
13339 self.num_heads,
13340 self.num_kv_heads,
13341 self.head_dim,
13342 self.rotary_dim,
13343 );
13344 let inv_freq = self.inv_freq.clone();
13345 let draft_rows = self.draft_head_rows(lm.1);
13346 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
13347 if n_arg == 0 {
13348 return Err(false);
13349 }
13350 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
13357 let t_chain = std::time::Instant::now();
13358 graph.chain_ids_init(t_next, k);
13359 let cpu_stored;
13360 {
13361 let cache = &m.kv;
13362 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
13363 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
13364 cpu_stored = cpu_k[0].len() / hd;
13365 for j in 0..k {
13366 if !graph.encode_chain_input(
13367 embed,
13368 j as u32,
13369 &m.enorm,
13370 &m.hnorm,
13371 self.embed_multiplier,
13372 eh,
13373 ) {
13374 return Err(false);
13375 }
13376 let p = AttnDeviceParams {
13380 kv_id: self.mtp_kv_id(),
13381 layer: Self::MTP_LAYER_BASE,
13382 nh,
13383 nkv,
13384 hd,
13385 rd,
13386 position: position + j,
13387 scale: self.attn_scale,
13388 eps: self.rms_eps as f32,
13389 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
13390 late_qk_norm: self.qk_norm_after_rope,
13391 output_gate: *output_gate,
13392 q_norm: q_norm.as_deref(),
13393 k_norm: k_norm.as_deref(),
13394 inv_freq: &inv_freq,
13395 rope_scale: self.rope_scale,
13396 cpu_k: cpu_k.clone(),
13397 cpu_v: cpu_v.clone(),
13398 cpu_stored: cpu_stored + j,
13399 cpu_gen: cache.generation(),
13400 o1: None,
13401 window: None,
13402 head_gate: None,
13403 v_norm: false,
13404 };
13405 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
13406 return Err(false);
13407 }
13408 if draft_rows < lm.1 {
13409 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
13410 return Err(false);
13411 }
13412 } else {
13413 graph.encode_lm_head(&m.final_norm, lm);
13414 }
13415 if !graph.encode_argmax(n_arg, j as u32 + 1) {
13416 return Err(false);
13417 }
13418 if split {
13419 graph.commit();
13422 }
13423 }
13424 }
13425 let t_enc = t_chain.elapsed();
13426 if graph.sync_checked().is_err() {
13427 return Err(true);
13428 }
13429 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
13430 eprintln!(
13431 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
13432 t_enc.as_secs_f64() * 1e3,
13433 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
13434 if split { ", split" } else { "" }
13435 );
13436 }
13437 let mut ids = vec![0u32; k];
13438 if !graph.chain_ids_read(&mut ids) {
13439 return Err(true);
13440 }
13441 let mut kbuf = vec![0f32; k * nkv * hd];
13442 let mut vbuf = vec![0f32; k * nkv * hd];
13443 if !crate::gpu_metal::kv_mirror_read_rows(
13444 self.mtp_kv_id(),
13445 Self::MTP_LAYER_BASE,
13446 nkv,
13447 hd,
13448 cpu_stored,
13449 k,
13450 &mut kbuf,
13451 &mut vbuf,
13452 ) {
13453 return Err(true);
13454 }
13455 for r in 0..k {
13456 m.kv.append(
13457 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
13458 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
13459 &[],
13460 );
13461 }
13462 Ok(ids)
13463 }
13464
13465 fn try_batch_graph_wgpu(
13466 &self,
13467 hiddens: &mut [f32],
13468 positions: &[usize],
13469 k: usize,
13470 spec: Option<crate::gpu::SpecTail<'_>>,
13471 ) -> crate::gpu::BatchGraphOutcome {
13472 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
13473 }
13474
13475 fn try_batch_graph_wgpu_prefix(
13480 &self,
13481 hiddens: &mut [f32],
13482 positions: &[usize],
13483 k: usize,
13484 spec: Option<crate::gpu::SpecTail<'_>>,
13485 layers_run: Option<&mut usize>,
13486 ) -> crate::gpu::BatchGraphOutcome {
13487 let graph_end = match self.mimo_moe.graph_prefix_end() {
13488 Some(end) if end < self.num_layers => {
13489 if layers_run.is_none() || spec.is_some() || end == 0 {
13490 return crate::gpu::BatchGraphOutcome::Declined;
13491 }
13492 end
13493 }
13494 _ => self.num_layers,
13495 };
13496 let _tb = std::time::Instant::now();
13497 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13498 if self.attn_softcap > 0.0 {
13499 return crate::gpu::BatchGraphOutcome::Declined; }
13501 if let Some(reason) = self
13504 .wgpu_graph_attn_decline()
13505 .or_else(|| self.batch_graph_gemma_decline())
13506 {
13507 self.note_graph_decline("wgpu batch graph", reason);
13508 return crate::gpu::BatchGraphOutcome::Declined;
13509 }
13510 let nh = self.num_heads;
13511 let (nkv, hd, rd) = self.layer_geom(0);
13512 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13513 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13514 if let Some((m, i, kind, rs)) = t
13515 .graph_weight()
13516 .or_else(|| t.graph_weight_descriptor())
13517 {
13518 let name = &m.tensors[i].name;
13519 let prism = if crate::prism::is_inverse_embedding(m, name) {
13520 crate::gpu::GraphPrismOp::InverseEmbedding
13521 } else if crate::prism::is_forward_weight(m, name) {
13522 crate::gpu::GraphPrismOp::Forward
13523 } else {
13524 crate::gpu::GraphPrismOp::None
13525 };
13526 return Some(crate::gpu::GraphW {
13527 idx: i,
13528 kind,
13529 row_scale: rs,
13530 data: &[],
13531 prism,
13532 affine: crate::prism::is_affine_target(m, name),
13533 });
13534 }
13535 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13536 eprintln!(
13537 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13538 t.rows(),
13539 t.cols()
13540 );
13541 }
13542 t.as_f32().map(|d| crate::gpu::GraphW {
13543 idx: 0,
13544 kind: 4,
13545 row_scale: &[],
13546 data: d,
13547 prism: crate::gpu::GraphPrismOp::None,
13548 affine: false,
13549 })
13550 }
13551 let built: Option<(
13552 Vec<crate::gpu::GraphLayer<'_>>,
13553 std::sync::Arc<cortiq_core::CmfModel>,
13554 )> = (|| {
13555 let mut layers = Vec::with_capacity(graph_end);
13556 let mut model = None;
13557 for li in 0..graph_end {
13558 let lw = &self.weights.layers[self.phys_layer(li)];
13559 let gffn = match &lw.ffn {
13566 FfnKind::Dense(d) if !d.segs.is_empty() => {
13567 if batch_debug {
13568 eprintln!("batch graph: dense segmented FFN at layer {li}");
13569 }
13570 return None;
13571 }
13572 FfnKind::Dense(d) => {
13573 let Some(act) = d.act.graph_act() else {
13574 if batch_debug {
13575 eprintln!(
13576 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13577 d.act
13578 );
13579 }
13580 return None;
13581 };
13582 crate::gpu::GraphFfn::Dense {
13583 gate: gw(&d.gate_proj)?,
13584 up: gw(&d.up_proj)?,
13585 down: gw(&d.down_proj)?,
13586 act,
13587 }
13588 }
13589 FfnKind::Moe(m) => {
13590 if m.route_tau.is_some() || m.mask.is_some() {
13597 return None;
13598 }
13599 let shared = m.shared.as_ref();
13603 let has_shared = shared.is_some();
13604 let shared_gated = matches!(shared, Some((_, Some(_))));
13605 let sgate = match shared {
13606 Some((_, Some(sg))) => gw(sg)?,
13607 _ => gw(&m.router)?,
13611 };
13612 let router = gw(&m.router)?;
13613 if router.prism != crate::gpu::GraphPrismOp::None
13619 || router.affine
13620 || sgate.prism != crate::gpu::GraphPrismOp::None
13621 || sgate.affine
13622 {
13623 return None;
13624 }
13625 let inter = m.experts.first()?.gate_proj.rows();
13626 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13627 let mut q4tp: Option<bool> = None;
13628 let mut gu_q2: Option<bool> = None;
13629 let mut q82: Option<bool> = None;
13630 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13631 if !matches!(e.act, Act::Silu)
13632 || e.gate_proj.rows() != inter
13633 || e.up_proj.rows() != inter
13634 {
13635 return None;
13636 }
13637 let (mm, gi, ui, di, is_p, is_q2, is_8) = match e.gate_proj.mapped_q4t()
13641 {
13642 Some((mm, gi)) => (
13643 mm,
13644 gi,
13645 e.up_proj.mapped_q4t()?.1,
13646 e.down_proj.mapped_q4t()?.1,
13647 false,
13648 false,
13649 false,
13650 ),
13651 None => match e.gate_proj.mapped_q2tp() {
13652 Some((mm, gi)) => (
13653 mm,
13654 gi,
13655 e.up_proj.mapped_q2tp()?.1,
13656 e.down_proj.mapped_q4tp()?.1,
13657 true,
13658 true,
13659 false,
13660 ),
13661 None => match e.gate_proj.mapped_q4tp() {
13662 Some((mm, gi)) => (
13663 mm,
13664 gi,
13665 e.up_proj.mapped_q4tp()?.1,
13666 e.down_proj.mapped_q4tp()?.1,
13667 true,
13668 false,
13669 false,
13670 ),
13671 None => {
13672 if !crate::gpu::moe_q82_graph_on() {
13673 return None;
13674 }
13675 let (mm, gi) = e.gate_proj.mapped_q8_2f()?;
13676 (
13677 mm,
13678 gi,
13679 e.up_proj.mapped_q8_2f()?.1,
13680 e.down_proj.mapped_q8_2f()?.1,
13681 false,
13682 false,
13683 true,
13684 )
13685 }
13686 },
13687 },
13688 };
13689 if *q4tp.get_or_insert(is_p) != is_p
13690 || *gu_q2.get_or_insert(is_q2) != is_q2
13691 || *q82.get_or_insert(is_8) != is_8
13692 {
13693 return None;
13694 }
13695 if [gi, ui, di].into_iter().any(|idx| {
13696 mm.tensors
13697 .get(idx)
13698 .is_some_and(|t| {
13699 crate::prism::is_forward_weight(mm, &t.name)
13700 || crate::prism::is_affine_target(mm, &t.name)
13701 })
13702 }) {
13703 return None;
13704 }
13705 model.get_or_insert_with(|| mm.clone());
13706 experts.push((gi, ui, di));
13707 }
13708 crate::gpu::GraphFfn::Moe {
13709 router,
13710 shared_gate: sgate,
13711 experts,
13712 n_exp: m.experts.len(),
13713 top_k: m.top_k,
13714 inter,
13715 norm_topk: m.norm_topk_prob,
13716 q4tp: q4tp?,
13717 gu_q2: gu_q2.unwrap_or(false),
13718 q82: q82.unwrap_or(false),
13719 sigmoid: m.router_sigmoid,
13720 bias: m.expert_bias.as_deref(),
13721 has_shared,
13722 shared_gated,
13723 route_scale: m.routed_scaling,
13724 }
13725 }
13726 _ => return None,
13727 };
13728 let attn = match &lw.attn {
13729 AttnKind::Full {
13730 wq,
13731 wk,
13732 wv,
13733 wo,
13734 q_norm,
13735 k_norm,
13736 output_gate,
13737 softplus_gate,
13738 bias,
13739 } => {
13740 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
13741 if batch_debug {
13742 eprintln!(
13743 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
13744 softplus_gate.is_some(),
13745 self.attention_heads_per_layer.is_some()
13746 );
13747 }
13748 return None;
13749 }
13750 let (m, _, _, _) = wq
13751 .graph_weight()
13752 .or_else(|| wq.graph_weight_descriptor())?;
13753 model = Some(m.clone());
13754 crate::gpu::GraphAttn::Full {
13755 wq: gw(wq)?,
13756 wk: gw(wk)?,
13757 wv: gw(wv)?,
13758 wo: gw(wo)?,
13759 q_norm: q_norm.as_deref(),
13760 k_norm: k_norm.as_deref(),
13761 late_qk_norm: self.qk_norm_after_rope,
13762 bias: bias
13763 .as_ref()
13764 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13765 output_gate: *output_gate,
13766 cpu_k: self.kv_cache.layers[li].k_heads(),
13767 cpu_v: self.kv_cache.layers[li].v_heads(),
13768 cpu_base: self.kv_cache.layers[li].base(),
13769 geom: self.graph_attn_geom(li),
13770 head_gate: None,
13773 }
13774 }
13775 AttnKind::LinearGdn(w) => {
13776 let Some(cfg) = self.gdn_cfg else {
13777 if batch_debug {
13778 eprintln!("batch graph: no GDN config at layer {li}");
13779 }
13780 return None;
13781 };
13782 let (m, _, _, _) = w
13783 .in_proj_qkv
13784 .graph_weight()
13785 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
13786 model = Some(m.clone());
13787 crate::gpu::GraphAttn::Gdn {
13788 qkv: gw(&w.in_proj_qkv)?,
13789 z: gw(&w.in_proj_z)?,
13790 a: gw(&w.in_proj_a)?,
13791 b: gw(&w.in_proj_b)?,
13792 out: gw(&w.out_proj)?,
13793 conv1d: &w.conv1d,
13794 a_log: &w.a_log,
13795 dt_bias: &w.dt_bias,
13796 norm: &w.norm,
13797 nv: cfg.num_v_heads,
13798 nk: cfg.num_k_heads,
13799 dk: cfg.key_head_dim,
13800 dv: cfg.value_head_dim,
13801 kk: cfg.conv_kernel,
13802 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13803 }
13804 }
13805 _ => return None,
13806 };
13807 let g4 = self.global_attn.is_some() || self.attn_v_norm;
13810 layers.push(crate::gpu::GraphLayer {
13811 input_norm: &lw.input_norm,
13812 attn,
13813 post_norm: &lw.post_norm,
13814 attn_out_norm: if g4 { lw.attn_out_norm.as_deref() } else { None },
13815 ffn_out_norm: if g4 { lw.ffn_out_norm.as_deref() } else { None },
13816 layer_scale: if g4 { lw.layer_scale } else { None },
13817 ffn: gffn,
13818 });
13819 }
13820 Some((layers, model?))
13821 })();
13822 let Some((layers, model)) = built else {
13823 {
13824 use std::sync::atomic::{AtomicBool, Ordering};
13825 static SAID: AtomicBool = AtomicBool::new(false);
13826 if !SAID.swap(true, Ordering::Relaxed) {
13827 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
13828 }
13829 }
13830 return crate::gpu::BatchGraphOutcome::Declined;
13831 };
13832 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13833 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
13834 }
13835 crate::gpu::forward_batch_graph(
13836 &model,
13837 self.graph_kv_id,
13838 &layers,
13839 &self.inv_freq,
13840 hiddens,
13841 nh,
13842 nkv,
13843 hd,
13844 rd,
13845 self.hidden_size,
13846 self.intermediate_size,
13847 positions,
13848 self.kv_cache.max_seq_len,
13849 gemma,
13850 self.rms_eps as f32,
13851 self.attn_scale,
13852 k,
13853 &(0..graph_end)
13854 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
13855 .collect::<Vec<_>>(),
13856 self.o1_epoch,
13857 spec,
13858 layers_run,
13859 )
13860 }
13861
13862 fn draft_probe() -> bool {
13866 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13867 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
13868 }
13869
13870 #[cfg(feature = "gpu")]
13882 fn dsv4_spec_on() -> bool {
13883 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13884 *ON.get_or_init(|| {
13885 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
13889 return v != "0";
13890 }
13891 std::env::var("CMF_DSV4_SPEC")
13898 .map(|v| v != "0")
13899 .unwrap_or_else(|_| {
13900 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
13901 })
13902 })
13903 }
13904
13905 #[cfg(feature = "gpu")]
13912 fn dsv4_spec_step(
13913 &mut self,
13914 tip_token: u32,
13915 t_next: u32,
13916 next_pos: usize,
13917 max_extra: usize,
13918 drafted: &mut usize,
13919 accepted_ctr: &mut usize,
13920 ) -> Option<(Vec<u32>, usize)> {
13921 let t_all = std::time::Instant::now();
13922 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
13923 thread_local! {
13924 static LAST: std::cell::Cell<Option<std::time::Instant>> =
13925 const { std::cell::Cell::new(None) };
13926 }
13927 LAST.with(|l| {
13928 if let Some(prev) = l.get() {
13929 eprintln!(
13930 "между раундами {:.1} мс",
13931 prev.elapsed().as_secs_f64() * 1e3
13932 );
13933 }
13934 l.set(Some(std::time::Instant::now()));
13935 });
13936 }
13937 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13938 eprintln!("spec_step: вход pos={next_pos}");
13939 }
13940 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
13941 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
13942 if self.dspark.is_none() {
13944 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13945 if t.is_empty() {
13946 return None;
13947 }
13948 crate::dsv4::dspark_arm(&t, cfg.dim);
13949 self.dspark = Some(crate::dsv4::DsparkState::new(
13950 self.dsv4_mtp.len(),
13951 &cfg,
13952 t.len(),
13953 ));
13954 }
13955 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13956 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
13957 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13958 eprintln!("spec_step: пак не построился (targets {targets:?})");
13959 }
13960 let pack = pack?;
13961 let block = crate::dsv4::dspark_block();
13962 let b_box = self.dsv4.as_mut()?;
13963 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
13964 let ds = self.dspark.as_mut()?;
13965 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
13968 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
13969 if dbg {
13970 eprintln!("spec_step: нет захвата");
13971 }
13972 return None;
13973 }
13974 ds.have_hidden = true;
13975 let tip_pos = next_pos.checked_sub(1)?;
13976 let draft_started = std::time::Instant::now();
13977 let mut conf = Vec::new();
13978 let props = crate::dsv4::dspark_draft_gpu(
13979 g,
13980 &self.dsv4_mtp,
13981 &cfg,
13982 ds,
13983 pack,
13984 st.kv_id,
13985 tip_token,
13986 tip_pos,
13987 self.pool.as_deref(),
13988 &mut conf,
13989 );
13990 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
13991 *drafted += block;
13992 if props.is_empty() || props[0] != t_next {
13993 if dbg {
13994 eprintln!(
13995 "spec_step: черновик {} (props0={:?} t_next={t_next})",
13996 if props.is_empty() {
13997 "пуст"
13998 } else {
13999 "мимо"
14000 },
14001 props.first()
14002 );
14003 }
14004 return None;
14005 }
14006 let mut k_verify = crate::dsv4::dspark_verify_k()
14013 .min(props.len())
14014 .min(max_extra.saturating_add(1));
14015 let conf_min = {
14021 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
14022 *M.get_or_init(|| {
14023 std::env::var("CMF_DSPARK_CONF_MIN")
14024 .ok()
14025 .and_then(|v| v.parse().ok())
14026 .unwrap_or(0.0)
14027 })
14028 };
14029 if conf_min > 0.0 && conf.len() >= props.len() {
14030 let mut keep = 1usize;
14031 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
14032 keep += 1;
14033 }
14034 k_verify = k_verify.min(keep.max(2));
14035 }
14036 if k_verify < 2 {
14037 return None;
14038 }
14039 let mut fed = Vec::with_capacity(k_verify);
14040 fed.push(t_next);
14041 fed.extend_from_slice(&props[1..k_verify]);
14042 let mut argmax = Vec::new();
14043 let mut logits_all = Vec::new();
14044 let mut walked = Vec::new();
14045 let txn = crate::dsv4::dsv4_verify_chunk(
14046 g,
14047 layers,
14048 &cfg,
14049 st,
14050 &fed,
14051 next_pos,
14052 &self.inv_freq,
14053 self.pool.as_deref(),
14054 &targets,
14055 &mut argmax,
14056 &mut logits_all,
14057 &mut walked,
14058 );
14059 if txn.is_none() && dbg {
14060 eprintln!("spec_step: verify отказал");
14061 }
14062 let txn = txn?;
14063 let spec_gpu_end = txn.gpu_end;
14064 let b = fed.len();
14065 let mut accepted = 1usize;
14066 while accepted < b && fed[accepted] == argmax[accepted - 1] {
14067 accepted += 1;
14068 }
14069 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
14074 accepted = 1;
14075 }
14076 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
14077 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
14078 }
14079 let t_fin = std::time::Instant::now();
14080 if !crate::dsv4::dsv4_spec_finish(
14081 g,
14082 layers,
14083 &cfg,
14084 st,
14085 txn,
14086 accepted,
14087 &fed,
14088 &self.inv_freq,
14089 self.pool.as_deref(),
14090 ) {
14091 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
14092 return None;
14093 }
14094 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14095 eprintln!(
14096 "finish(k={accepted}): {:.1} мс",
14097 t_fin.elapsed().as_secs_f64() * 1e3
14098 );
14099 }
14100 *accepted_ctr += accepted - 1;
14101 let (hc, dim) = (cfg.hc_mult, cfg.dim);
14106 let dev_caps: Vec<usize> = targets
14111 .iter()
14112 .copied()
14113 .filter(|&t| t < spec_gpu_end)
14114 .collect();
14115 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
14116 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
14117 return None;
14118 }
14119 for t in 0..accepted {
14120 let tip = t + 1 == accepted;
14121 for (slot, &tl) in targets.iter().enumerate() {
14122 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
14123 let lo = (di * b + t) * hc * dim;
14124 crate::dsv4::dspark_capture(
14125 &caps_all[lo..lo + hc * dim],
14126 &cfg,
14127 slot,
14128 &mut ds.main_hidden,
14129 );
14130 } else if tip
14131 && crate::dsv4::dspark_peek_slot(slot, dim, {
14132 let lo = slot * dim;
14133 &mut ds.main_hidden[lo..lo + dim]
14134 })
14135 {
14136 } else {
14141 crate::dsv4::dspark_capture(
14145 &walked[t * hc * dim..(t + 1) * hc * dim],
14146 &cfg,
14147 slot,
14148 &mut ds.main_hidden,
14149 );
14150 }
14151 }
14152 crate::dsv4::dspark_ring_append(
14153 g,
14154 &self.dsv4_mtp,
14155 &cfg,
14156 ds,
14157 next_pos + t,
14158 self.pool.as_deref(),
14159 );
14160 }
14161 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
14162 self.graph_logits = Some(row);
14163 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
14168 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
14169 crate::dsv4::pick_tally_arm();
14170 }
14171 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
14172 eprintln!(
14173 "spec_step total {:.1} мс (k={accepted})",
14174 t_all.elapsed().as_secs_f64() * 1e3
14175 );
14176 }
14177 Some((fed[1..accepted].to_vec(), next_pos + accepted))
14178 }
14179
14180 fn dspark_probe(&mut self, position: usize, token_id: u32) {
14181 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
14182 return;
14183 }
14184 let trunk_now = crate::dsv4::pick_tally_take();
14186 crate::dsv4::trunk_freq_note(&trunk_now);
14187 if !trunk_now.is_empty() {
14188 self.dspark_trunk_picks.push(trunk_now);
14189 let keep = crate::dsv4::dspark_block();
14190 if self.dspark_trunk_picks.len() > keep {
14191 self.dspark_trunk_picks.remove(0);
14192 }
14193 }
14194 for p in std::mem::take(&mut self.dspark_pending) {
14197 let Some(i) = position.checked_sub(p.0 + 1) else {
14198 continue;
14199 };
14200 let mut p = p;
14201 if i < p.1.len() {
14202 if p.2 && p.1[i] == token_id {
14203 p.3 = i + 1;
14204 } else {
14205 p.2 = false;
14206 }
14207 if i + 1 < p.1.len() {
14208 self.dspark_pending.push(p);
14209 continue;
14210 }
14211 }
14212 self.dspark_hist.push(p.3);
14213 self.dspark_real.push(token_id);
14214 }
14215 let Some(b) = &mut self.dsv4 else { return };
14216 let (g, layers, cfg) = (&b.0, &b.1, b.2);
14217 let n_layers = layers.len();
14218 if self.dspark.is_none() {
14219 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
14220 if t.is_empty() {
14221 return;
14222 }
14223 eprintln!(
14224 "DSpark: захват со слоёв {t:?}, блок {}",
14225 crate::dsv4::dspark_block()
14226 );
14227 crate::dsv4::dspark_arm(&t, cfg.dim);
14228 self.dspark = Some(crate::dsv4::DsparkState::new(
14229 self.dsv4_mtp.len(),
14230 &cfg,
14231 t.len(),
14232 ));
14233 }
14234 let ds = self.dspark.as_mut().unwrap();
14235 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
14236 return; }
14238 let mut conf = Vec::new();
14239 crate::dsv4::pick_tally_arm();
14240 let draft_started = std::time::Instant::now();
14245 #[cfg(feature = "gpu")]
14246 let gpu_draft = crate::dsv4::dspark_gpu_on();
14247 #[cfg(not(feature = "gpu"))]
14248 let gpu_draft = false;
14249 let props = if gpu_draft {
14250 #[cfg(feature = "gpu")]
14251 {
14252 let kv_id = b.3.kv_id;
14253 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
14254 Some(pk) => crate::dsv4::dspark_draft_gpu(
14255 g,
14256 &self.dsv4_mtp,
14257 &cfg,
14258 ds,
14259 pk,
14260 kv_id,
14261 token_id,
14262 position,
14263 self.pool.as_deref(),
14264 &mut conf,
14265 ),
14266 None => Vec::new(),
14267 }
14268 }
14269 #[cfg(not(feature = "gpu"))]
14270 Vec::new()
14271 } else {
14272 crate::gpu::cpu_scope(|| {
14273 crate::dsv4::dspark_draft(
14274 g,
14275 &self.dsv4_mtp,
14276 &cfg,
14277 ds,
14278 token_id,
14279 position,
14280 self.pool.as_deref(),
14281 &mut conf,
14282 )
14283 })
14284 };
14285 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
14286 let draft_picks = crate::dsv4::pick_tally_take();
14287 crate::dsv4::dspark_freq_note(&draft_picks);
14288 crate::dsv4::pick_tally_arm();
14291 if !props.is_empty() {
14292 let (tu, tt) = {
14296 let flat: Vec<(usize, Vec<usize>)> = self
14297 .dspark_trunk_picks
14298 .iter()
14299 .flat_map(|v| v.iter().cloned())
14300 .collect();
14301 let mut per: std::collections::HashMap<usize, Vec<usize>> =
14303 std::collections::HashMap::new();
14304 for (li, picks) in flat {
14305 per.entry(li).or_default().extend(picks);
14306 }
14307 let n = per.len().max(1);
14308 let mut u = 0usize;
14309 let mut t = 0usize;
14310 for (_, v) in per {
14311 t += v.len();
14312 u += v.iter().collect::<std::collections::HashSet<_>>().len();
14313 }
14314 (u / n, t / n)
14315 };
14316 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
14317 self.dspark_exp.push((tu, tt, du, dt));
14318 self.dspark_pending.push((position, props, true, 0));
14319 }
14320 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
14321 let n = self.dspark_hist.len() as f32;
14322 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
14323 let block = crate::dsv4::dspark_block();
14324 let mut at = vec![0usize; block + 1];
14325 for &k in &self.dspark_hist {
14326 at[k] += 1;
14327 }
14328 let mut surv = Vec::with_capacity(block);
14330 for i in 1..=block {
14331 let k = at[i..].iter().sum::<usize>() as f32 / n;
14332 surv.push(format!("{k:.2}"));
14333 }
14334 let distinct = self
14335 .dspark_real
14336 .iter()
14337 .collect::<std::collections::HashSet<_>>()
14338 .len();
14339 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
14340 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
14341 });
14342 let m = self.dspark_exp.len().max(1);
14343 eprintln!(
14344 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
14345 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
14346 self.dspark_hist.len(),
14347 mean + 1.0,
14348 surv.join(" ")
14349 );
14350 eprintln!(
14351 "DSpark: разных токенов {distinct} из {} (вырожденность), \
14352 эксперты ствол {}/{} на слой за {block} токенов, \
14353 черновик {}/{} за блок, draft {:.2} мс/блок",
14354 self.dspark_real.len(),
14355 tu / m,
14356 tt / m,
14357 du / m,
14358 dt / m,
14359 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
14360 );
14361 }
14362 }
14363
14364 fn forward_layers_upto(
14365 &mut self,
14366 hidden: &[f32],
14367 position: usize,
14368 task_mask: Option<&TaskMask>,
14369 upto: Option<usize>,
14370 ) -> Vec<f32> {
14371 if let Some(plan) = self.gpu_plan.clone() {
14377 if upto.is_none() && plan.len() > 1 {
14378 let mut h = hidden.to_vec();
14379 for &(dev, from, upto_incl) in plan.iter() {
14380 h = crate::gpu::with_device(dev, || {
14381 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
14382 });
14383 }
14384 return h;
14385 }
14386 }
14387 self.forward_layers_span(hidden, position, task_mask, 0, upto)
14388 }
14389
14390 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
14395 self.set_gpu_plan_at(devices, None)
14396 }
14397
14398 pub fn set_gpu_plan_at(
14402 &mut self,
14403 devices: Option<&[usize]>,
14404 at: Option<usize>,
14405 ) -> Result<(), String> {
14406 let Some(devs) = devices.filter(|d| d.len() > 1) else {
14407 self.gpu_plan = None;
14408 return Ok(());
14409 };
14410 self.split_supported()?;
14411 let n = self.num_layers;
14412 if devs.len() > n {
14413 return Err(format!("{} devices for {n} layers", devs.len()));
14414 }
14415 if let Some(k) = at {
14416 if k == 0 || k >= n {
14417 return Err(format!("split at {k}: the model has {n} layers"));
14418 }
14419 if devs.len() == 2 {
14420 self.gpu_plan = Some(std::sync::Arc::new(vec![
14421 (devs[0], 0, k - 1),
14422 (devs[1], k, n - 1),
14423 ]));
14424 return Ok(());
14425 }
14426 return Err(format!(
14427 "an explicit split point takes exactly 2 devices, got {}",
14428 devs.len()
14429 ));
14430 }
14431 let per = n.div_ceil(devs.len());
14432 let mut plan = Vec::with_capacity(devs.len());
14433 let mut from = 0usize;
14434 for &d in devs {
14435 if from >= n {
14436 break;
14437 }
14438 let upto = (from + per - 1).min(n - 1);
14439 plan.push((d, from, upto));
14440 from = upto + 1;
14441 }
14442 self.gpu_plan = Some(std::sync::Arc::new(plan));
14443 Ok(())
14444 }
14445
14446 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
14448 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
14449 }
14450
14451 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
14457 if let Some(x) = t.as_f32() {
14458 return x.to_vec();
14459 }
14460 let mut out = vec![0.0; t.rows() * t.cols()];
14461 for r in 0..t.rows() {
14462 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
14463 }
14464 out
14465 }
14466
14467 fn embryo_resident_eligible(&self) -> bool {
14468 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
14471 || self.num_layers != self.physical_layers
14472 || self.loop_final_norm
14473 || self.weights.layers.len() != self.num_layers
14474 || self.head_clusters.is_none()
14475 || self.final_softcap.is_some()
14476 || self.logit_multiplier.is_some()
14477 || self.attn_softcap != 0.0
14478 || self.mtp.is_some()
14479 || self.g3n.is_some()
14480 || self.dsv4.is_some()
14481 || self.dsv41.is_some()
14482 || self.qwen4_exp.is_some()
14483 || self.dyn_router.is_some()
14490 || self.dyn_phi_layer.is_some()
14491 || self.dyn_blend_loaded
14492 || self.o1_cfg.is_some()
14493 || self.swa.is_some()
14494 || self.sliding_layers.is_some()
14495 || self.global_attn.is_some()
14496 || self.attention_heads_per_layer.is_some()
14497 || self.attn_v_norm
14498 || self
14499 .kv_cache
14500 .layers
14501 .iter()
14502 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
14503 || self.rope_scale != 1.0
14504 || self.rope_scale_local != 1.0
14505 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
14506 || self.hidden_size == 0
14507 || self.hidden_size > 1024
14508 || self.intermediate_size > 1024
14509 || self.num_heads == 0
14510 || self.num_kv_heads == 0
14511 || self.num_heads % self.num_kv_heads != 0
14512 || self.num_heads.saturating_mul(self.head_dim) > 1024
14513 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
14514 || self.vocab_size == 0
14515 || self.kv_cache.max_seq_len == 0
14516 || self.rotary_dim == 0
14517 || self.rotary_dim > self.head_dim
14518 || self.rotary_dim % 2 != 0
14519 || self.inv_freq.len() < self.rotary_dim / 2
14520 {
14521 return false;
14522 }
14523 if self.weights.lm_head.as_f32().is_none()
14534 || self.weights.embed_tokens.as_f32().is_none()
14535 || self.weights.lm_head.rows() < self.vocab_size
14536 || self.weights.lm_head.cols() != self.hidden_size
14537 || self.weights.embed_tokens.rows() < self.vocab_size
14538 || self.weights.embed_tokens.cols() != self.hidden_size
14539 || self.weights.final_norm.len() != self.hidden_size
14540 {
14541 return false;
14542 }
14543 if let Some(cfg) = self.vmf_cfg {
14544 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14545 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14546 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14547 || cfg.state_len() == 0
14548 {
14549 return false;
14550 }
14551 }
14552 if let Some(g) = self.gdn_cfg {
14553 if g.num_v_heads == 0
14558 || g.num_k_heads == 0
14559 || g.num_v_heads % g.num_k_heads != 0
14560 || g.key_head_dim == 0
14561 || g.key_head_dim > 128
14562 || g.value_head_dim == 0
14563 || g.value_head_dim > 256
14564 || g.value_head_dim % 4 != 0
14565 || g.conv_kernel == 0
14566 || g.num_v_heads > 512
14567 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14568 || g.conv_dim() > 2048
14569 || g.conv_dim() % 4 != 0
14570 || g.hidden_size != self.hidden_size
14571 || g.output_gate_sigmoid
14572 || g.rms_eps != self.rms_eps
14573 || g.state_len() == 0
14574 {
14575 return false;
14576 }
14577 }
14578 let mut full_seen = false;
14579 for lw in &self.weights.layers {
14580 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14581 return false;
14582 }
14583 match &lw.attn {
14584 AttnKind::LinearGdn(w) => {
14585 let Some(g) = self.gdn_cfg else {
14586 return false;
14587 };
14588 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14589 if w.in_proj_qkv.rows() != g.conv_dim()
14590 || w.in_proj_qkv.cols() != self.hidden_size
14591 || w.in_proj_qkv.as_f32().is_none()
14592 || w.in_proj_z.rows() != nv * dv
14593 || w.in_proj_z.cols() != self.hidden_size
14594 || w.in_proj_z.as_f32().is_none()
14595 || w.in_proj_a.rows() != nv
14596 || w.in_proj_a.cols() != self.hidden_size
14597 || w.in_proj_a.as_f32().is_none()
14598 || w.in_proj_b.rows() != nv
14599 || w.in_proj_b.cols() != self.hidden_size
14600 || w.in_proj_b.as_f32().is_none()
14601 || w.conv1d.len() != g.conv_dim() * kk
14602 || w.a_log.len() != nv
14603 || w.dt_bias.len() != nv
14604 || w.norm.len() != dv
14605 || w.out_proj.rows() != self.hidden_size
14606 || w.out_proj.cols() != nv * dv
14607 || w.out_proj.as_f32().is_none()
14608 {
14609 return false;
14610 }
14611 }
14612 AttnKind::Linear(w) => {
14613 let Some(cfg) = self.vmf_cfg else {
14614 return false;
14615 };
14616 if w.thq.rows() != cfg.num_heads * cfg.nphase
14617 || w.thq.cols() != self.hidden_size
14618 || w.thq.as_f32().is_none()
14619 || w.thk.rows() != cfg.num_heads * cfg.nphase
14620 || w.thk.cols() != self.hidden_size
14621 || w.thk.as_f32().is_none()
14622 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14623 || w.v_proj.cols() != self.hidden_size
14624 || w.v_proj.as_f32().is_none()
14625 || w.out_proj.rows() != self.hidden_size
14626 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14627 || w.out_proj.as_f32().is_none()
14628 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14629 {
14630 return false;
14631 }
14632 if let Some((kg, kb)) = &w.k_gate {
14633 if kg.rows() != cfg.num_heads
14634 || kg.cols() != self.hidden_size
14635 || kg.as_f32().is_none()
14636 || kb.len() != cfg.num_heads
14637 {
14638 return false;
14639 }
14640 }
14641 if let Some(conv) = &w.conv {
14642 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14643 return false;
14644 }
14645 }
14646 }
14647 AttnKind::Full {
14648 wq,
14649 wk,
14650 wv,
14651 wo,
14652 q_norm,
14653 k_norm,
14654 output_gate,
14655 softplus_gate,
14656 bias,
14657 } => {
14658 if full_seen
14659 || q_norm.is_some()
14660 || k_norm.is_some()
14661 || *output_gate
14662 || softplus_gate.is_some()
14663 || bias.is_some()
14664 || wq.as_f32().is_none()
14665 || wk.as_f32().is_none()
14666 || wv.as_f32().is_none()
14667 || wo.as_f32().is_none()
14668 || wq.rows() != self.num_heads * self.head_dim
14669 || wk.rows() != self.num_kv_heads * self.head_dim
14670 || wv.rows() != self.num_kv_heads * self.head_dim
14671 || wq.cols() != self.hidden_size
14672 || wk.cols() != self.hidden_size
14673 || wv.cols() != self.hidden_size
14674 || wo.rows() != self.hidden_size
14675 || wo.cols() != self.num_heads * self.head_dim
14676 {
14677 return false;
14678 }
14679 full_seen = true;
14680 }
14681 AttnKind::Bounded(w) => {
14682 let Some(ac) = self.anchor_core.as_ref() else {
14685 return false;
14686 };
14687 if self.bounded_rope.is_none()
14688 || w.window != ac.window
14689 || w.sink != ac.sink
14690 || w.window == 0
14691 || w.window + w.sink > 256
14692 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
14693 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
14694 || w.wq.as_f32().is_none()
14695 || w.wk.as_f32().is_none()
14696 || w.wv.as_f32().is_none()
14697 || w.wo.as_f32().is_none()
14698 || w.wq.rows() != self.num_heads * self.head_dim
14699 || w.wk.rows() != self.num_kv_heads * self.head_dim
14700 || w.wv.rows() != self.num_kv_heads * self.head_dim
14701 || w.wq.cols() != self.hidden_size
14702 || w.wk.cols() != self.hidden_size
14703 || w.wv.cols() != self.hidden_size
14704 || w.wo.rows() != self.hidden_size
14705 || w.wo.cols() != self.num_heads * self.head_dim
14706 {
14707 return false;
14708 }
14709 }
14710 _ => return false,
14711 }
14712 match &lw.ffn {
14713 FfnKind::Dense(d) => {
14714 if d.act != Act::Silu
14715 || !d.segs.is_empty()
14716 || d.gate_proj.as_f32().is_none()
14717 || d.up_proj.as_f32().is_none()
14718 || d.down_proj.as_f32().is_none()
14719 || d.gate_proj.rows() != self.intermediate_size
14720 || d.gate_proj.cols() != self.hidden_size
14721 || d.up_proj.rows() != self.intermediate_size
14722 || d.up_proj.cols() != self.hidden_size
14723 || d.down_proj.rows() != self.hidden_size
14724 || d.down_proj.cols() != self.intermediate_size
14725 {
14726 return false;
14727 }
14728 }
14729 FfnKind::Moe(m) => {
14730 if m.resonance.is_none()
14731 || m.top_k != 1
14732 || m.router_sigmoid
14733 || !m.norm_topk_prob
14734 || m.expert_bias.is_some()
14735 || m.routed_scaling != 1.0
14736 || m.route_tau.is_some()
14737 || m.shared.is_none()
14738 || m.mask.is_some()
14739 || m.per_expert_scale.is_some()
14740 || m.router_input_norm
14741 || m.experts.is_empty()
14742 || m.experts.len() > 8
14743 {
14744 return false;
14745 }
14746 let r = m.resonance.as_ref().unwrap();
14747 if r.mu.len() != m.experts.len() * self.hidden_size
14748 || r.bias.len() != m.experts.len()
14749 || r.u.len() != m.experts.len() * r.k * self.hidden_size
14750 || r.k > 128
14751 {
14752 return false;
14753 }
14754 let Some((shared, gate)) = &m.shared else {
14755 return false;
14756 };
14757 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
14758 return false;
14759 }
14760 if shared.gate_proj.as_f32().is_none()
14761 || shared.up_proj.as_f32().is_none()
14762 || shared.down_proj.as_f32().is_none()
14763 || shared.gate_proj.rows() != self.intermediate_size
14764 || shared.gate_proj.cols() != self.hidden_size
14765 || shared.up_proj.rows() != self.intermediate_size
14766 || shared.up_proj.cols() != self.hidden_size
14767 || shared.down_proj.rows() != self.hidden_size
14768 || shared.down_proj.cols() != self.intermediate_size
14769 {
14770 return false;
14771 }
14772 for e in &m.experts {
14773 if e.act != Act::Silu
14774 || !e.segs.is_empty()
14775 || e.gate_proj.as_f32().is_none()
14776 || e.up_proj.as_f32().is_none()
14777 || e.down_proj.as_f32().is_none()
14778 || e.gate_proj.rows() != self.intermediate_size
14779 || e.gate_proj.cols() != self.hidden_size
14780 || e.up_proj.rows() != self.intermediate_size
14781 || e.up_proj.cols() != self.hidden_size
14782 || e.down_proj.rows() != self.hidden_size
14783 || e.down_proj.cols() != self.intermediate_size
14784 {
14785 return false;
14786 }
14787 }
14788 }
14789 FfnKind::DenseMoe(_) => return false,
14790 }
14791 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
14792 return false;
14793 }
14794 }
14795 if full_seen && self.anchor_core.is_some() {
14796 return false;
14797 }
14798 full_seen || self.num_layers > 0
14799 }
14800
14801 fn embryo_resident_wanted(&self) -> bool {
14806 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
14807 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
14808 && matches!(
14809 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
14810 Ok("1") | Ok("parallel")
14811 )
14812 && crate::gpu::enabled_here()
14813 && !self.graph_refused()
14814 && self.embryo_resident_eligible()
14815 }
14816
14817 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
14825 if ids.len() < 2
14826 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
14827 || !self.embryo_resident_wanted()
14828 {
14829 return None;
14830 }
14831 let model = self.ensure_embryo_graph()?;
14832 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
14833 .ok()
14834 .and_then(|v| v.parse::<usize>().ok())
14835 .filter(|&v| v >= 1)
14836 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
14837 .min(crate::gpu::EMBRYO_CHUNK_MAX);
14838 let hs = self.hidden_size;
14839 let n = ids.len();
14840 let mut pos = start;
14841 let mut last = None;
14842 let mut rows = Vec::with_capacity(cmax * hs);
14843 while pos < n {
14844 let end = (pos + cmax).min(n);
14845 rows.clear();
14846 for &id in &ids[pos..end] {
14847 rows.extend_from_slice(&self.embed_single(id));
14848 }
14849 let mut lg = Vec::new();
14850 if !crate::gpu::forward_embryo_graph_chunk(
14851 &model,
14852 self.graph_kv_id,
14853 &rows,
14854 pos,
14855 end - pos,
14856 &mut lg,
14857 ) {
14858 if pos == start {
14859 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14860 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
14861 }
14862 return None;
14863 }
14864 self.kv_cache.clear();
14868 self.clear_history();
14869 crate::gpu::graph_kv_reset(self.graph_kv_id);
14870 panic!(
14871 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
14872 );
14873 }
14874 last = Some(lg);
14875 pos = end;
14876 }
14877 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14878 eprintln!(
14879 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
14880 n - start,
14881 (n - start).div_ceil(cmax)
14882 );
14883 }
14884 last
14885 }
14886
14887 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
14888 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
14889 const UMAX: u32 = u32::MAX;
14890 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
14891 const REC: usize = 64;
14892 struct Pack {
14893 data: Vec<f32>,
14894 }
14895 impl Pack {
14896 fn put(&mut self, x: &[f32]) -> u32 {
14897 if x.is_empty() {
14898 return u32::MAX;
14899 }
14900 let off = self.data.len();
14901 self.data.extend_from_slice(x);
14902 off as u32
14903 }
14904 }
14905 let vmf = self.vmf_cfg;
14909 let gdn = self.gdn_cfg;
14910 let mut pack = Pack { data: Vec::new() };
14911 let mut meta = vec![0u32; HEADER];
14912 meta[0] = self.hidden_size as u32;
14913 meta[1] = self.intermediate_size as u32;
14914 meta[2] = self.vocab_size as u32;
14915 meta[3] = self.num_layers as u32;
14916 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
14917 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
14918 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
14919 if let Some(g) = gdn {
14920 meta[24] = g.num_v_heads as u32;
14921 meta[25] = g.num_k_heads as u32;
14922 meta[26] = g.key_head_dim as u32;
14923 meta[27] = g.value_head_dim as u32;
14924 meta[28] = g.conv_kernel as u32;
14925 meta[29] = g.conv_dim() as u32;
14926 }
14927 meta[7] = self.num_heads as u32;
14928 meta[8] = self.num_kv_heads as u32;
14929 meta[9] = self.head_dim as u32;
14930 meta[10] = self.kv_cache.max_seq_len as u32;
14931 let clusters = self.head_clusters.as_ref().unwrap();
14932 let cluster_count = clusters.len() / self.hidden_size;
14933 if clusters.len() % self.hidden_size != 0
14934 || cluster_count == 0
14935 || cluster_count > 1024
14936 || self.vocab_size % cluster_count != 0
14937 || self.weights.lm_head.rows() < self.vocab_size
14938 || self.weights.final_norm.len() != self.hidden_size
14939 {
14940 return None;
14941 }
14942 meta[11] = cluster_count as u32;
14943 meta[12] = (self.vocab_size / cluster_count) as u32;
14944 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
14945 meta[16] = self.rotary_dim as u32;
14946 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
14947 meta[19] = (self.rms_eps as f32).to_bits();
14948 let max_conv = self
14949 .weights
14950 .layers
14951 .iter()
14952 .filter_map(|lw| match &lw.attn {
14953 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
14954 _ => None,
14955 })
14956 .max()
14957 .unwrap_or(1);
14958 let phase_stride = vmf
14964 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
14965 .unwrap_or(0);
14966 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
14967 let state_stride = phase_stride.max(gdn_stride);
14968 let bounded = self.anchor_core.clone();
14972 let (anchor_window, anchor_sink) = bounded
14973 .as_ref()
14974 .map(|ac| (ac.window, ac.sink))
14975 .unwrap_or((0, 0));
14976 let kv_stride = if bounded.is_some() {
14977 2usize
14978 .saturating_mul(self.num_kv_heads)
14979 .saturating_mul(anchor_window)
14980 .saturating_mul(self.head_dim)
14981 } else {
14982 2usize
14983 .saturating_mul(self.num_kv_heads)
14984 .saturating_mul(self.kv_cache.max_seq_len)
14985 .saturating_mul(self.head_dim)
14986 };
14987 meta[14] = state_stride as u32;
14988 meta[15] = kv_stride as u32;
14989 meta[18] = anchor_window as u32;
14990 meta[20] = anchor_sink as u32;
14991 meta[21] = match &self.bounded_rope {
14992 Some(rope) => {
14993 let off = pack.put(&rope.cos);
14995 let _ = pack.put(&rope.sin);
14996 off
14997 }
14998 None => UMAX,
14999 };
15000 let mut full_seen = false;
15001 let mut bounded_seen = 0usize;
15002 let mut phase_seen = 0usize;
15006 let mut gdn_seen = 0usize;
15007 for (li, lw) in self.weights.layers.iter().enumerate() {
15008 let base = meta.len();
15009 meta.resize(base + REC, UMAX);
15010 meta[base] = match &lw.attn {
15011 AttnKind::Linear(w) if w.phase_delta => 1,
15012 AttnKind::Linear(_) => 0,
15013 AttnKind::Full { .. } => 2,
15014 AttnKind::Bounded(_) => 3,
15015 AttnKind::LinearGdn(_) => 4,
15016 _ => UMAX,
15017 };
15018 meta[base + 1] = pack.put(&lw.input_norm);
15019 meta[base + 2] = pack.put(&lw.post_norm);
15020 meta[base + 25] = match &lw.attn {
15021 AttnKind::Linear(_) => {
15022 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15023 phase_seen += 1;
15024 off
15025 }
15026 AttnKind::LinearGdn(_) => {
15027 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
15028 gdn_seen += 1;
15029 off
15030 }
15031 _ => UMAX,
15032 };
15033 match &lw.attn {
15034 AttnKind::LinearGdn(w) => {
15035 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
15038 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
15039 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
15040 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
15041 meta[base + 60] = pack.put(&w.conv1d);
15042 meta[base + 61] = pack.put(&w.a_log);
15043 meta[base + 62] = pack.put(&w.dt_bias);
15044 meta[base + 63] = pack.put(&w.norm);
15045 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15046 meta[base + 24] = 0;
15047 }
15048 AttnKind::Linear(w) => {
15049 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
15050 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
15051 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
15052 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
15053 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
15054 meta[base + 7] = pack.put(&decay);
15055 if let Some((kg, kb)) = &w.k_gate {
15056 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
15057 meta[base + 9] = pack.put(kb);
15058 }
15059 if let Some(conv) = &w.conv {
15060 meta[base + 10] = pack.put(conv);
15061 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
15062 } else {
15063 meta[base + 24] = 0;
15064 }
15065 }
15066 AttnKind::Full { wq, wk, wv, wo, .. } => {
15067 full_seen = true;
15068 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
15069 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
15070 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
15071 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
15072 meta[base + 26] = (li * kv_stride) as u32;
15073 }
15074 AttnKind::Bounded(w) => {
15075 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
15076 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
15077 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
15078 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
15079 meta[base + 26] = (bounded_seen * kv_stride) as u32;
15081 meta[base + 27] = pack.put(&w.sink_k);
15082 meta[base + 28] = pack.put(&w.sink_v);
15083 bounded_seen += 1;
15084 }
15085 _ => return None,
15086 }
15087 match &lw.ffn {
15088 FfnKind::Dense(d) => {
15089 meta[base + 15] = 0;
15090 meta[base + 16] = 0;
15091 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
15092 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
15093 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
15094 }
15095 FfnKind::Moe(m) => {
15096 let r = m.resonance.as_ref().unwrap();
15097 let (shared, _) = m.shared.as_ref().unwrap();
15098 meta[base + 15] = 1;
15099 meta[base + 16] = m.experts.len() as u32;
15100 meta[base + 17] = pack.put(&r.mu);
15101 meta[base + 18] = pack.put(&r.u);
15102 meta[base + 19] = pack.put(&r.bias);
15103 meta[base + 20] = r.k as u32;
15104 let mut shell = r.effective_shell(m.experts.len());
15113 shell.push(f32::NEG_INFINITY);
15114 meta[base + 30] = pack.put(&shell);
15115 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
15116 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
15117 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
15118 for (e, ex) in m.experts.iter().enumerate() {
15119 meta[base + 32 + e * 3] =
15120 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
15121 meta[base + 33 + e * 3] =
15122 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
15123 meta[base + 34 + e * 3] =
15124 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
15125 }
15126 }
15127 FfnKind::DenseMoe(_) => return None,
15128 }
15129 }
15130 if !full_seen && self.num_layers == 0 {
15131 return None;
15132 }
15133 let id = {
15134 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
15135 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
15136 };
15137 let model = crate::gpu::EmbryoGraphModel {
15138 id,
15139 hidden: self.hidden_size,
15140 intermediate: self.intermediate_size,
15141 vocab: self.vocab_size,
15142 layers: self.num_layers,
15143 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
15144 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
15145 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
15146 anchor_q_heads: self.num_heads,
15147 anchor_kv_heads: self.num_kv_heads,
15148 anchor_head_dim: self.head_dim,
15149 rotary_dim: self.rotary_dim,
15150 max_seq: self.kv_cache.max_seq_len,
15151 cluster_count,
15152 cluster_size: self.vocab_size / cluster_count,
15153 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
15154 state_stride,
15155 kv_stride,
15156 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
15157 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
15158 weights: pack.data,
15159 meta,
15160 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
15161 clusters: clusters.as_ref().clone(),
15162 final_norm: self.weights.final_norm.clone(),
15163 inv_freq: self.inv_freq.as_ref().clone(),
15164 bounded: bounded.is_some(),
15165 kv_layers: if bounded.is_some() {
15166 bounded_seen
15167 } else {
15168 self.num_layers
15169 },
15170 state_layers: phase_seen + gdn_seen,
15171 anchor_window,
15172 anchor_sink,
15173 phase_layers: phase_seen,
15174 gdn_layers: gdn_seen,
15175 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
15176 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
15177 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
15178 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
15179 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
15180 };
15181 self.embryo_graph = Some(std::sync::Arc::new(model));
15182 }
15183 self.embryo_graph.clone()
15184 }
15185
15186 fn forward_layers_span(
15187 &mut self,
15188 hidden: &[f32],
15189 position: usize,
15190 task_mask: Option<&TaskMask>,
15191 from: usize,
15192 upto: Option<usize>,
15193 ) -> Vec<f32> {
15194 debug_assert!(
15195 from == 0
15196 || (self.dsv4.is_none()
15197 && self.dsv41.is_none()
15198 && self.qwen4_exp.is_none()
15199 && self.g3n.is_none())
15200 );
15201 #[cfg(target_os = "macos")]
15207 if !crate::gpu_metal::wait_replay() {
15208 self.fail_metal_graph("the pending async replay failed before a plain forward");
15209 return vec![0.0; self.hidden_size];
15210 }
15211 if let Some(b) = &mut self.qwen4_exp {
15212 let _ = (task_mask, upto);
15213 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15214 let mut logits = Vec::new();
15215 crate::qwen4_exp::forward_token(
15216 &b.0,
15217 &b.1,
15218 &b.2,
15219 &mut b.3,
15220 token_id,
15221 position,
15222 &self.inv_freq,
15223 self.pool.as_deref(),
15224 &mut logits,
15225 true,
15226 );
15227 self.graph_logits = Some(logits);
15228 return vec![0.0; self.hidden_size];
15229 }
15230 if let Some(b) = &mut self.dsv4 {
15236 let _ = (task_mask, upto);
15237 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15238 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
15239 st.pos = position;
15240 let mut logits = Vec::new();
15241 crate::dsv4::forward_token(
15242 g,
15243 layers,
15244 &cfg,
15245 st,
15246 token_id,
15247 &self.inv_freq,
15248 self.pool.as_deref(),
15249 &mut logits,
15250 );
15251 self.graph_logits = Some(logits);
15252 self.dspark_probe(position, token_id);
15253 return vec![0.0; self.hidden_size];
15256 }
15257 if let Some(b) = &mut self.dsv41 {
15259 let _ = (task_mask, upto);
15260 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
15261 let mut logits = Vec::new();
15262 crate::dsv41::forward_token(
15263 &b.0,
15264 &b.1,
15265 &b.2,
15266 &mut b.3,
15267 token_id,
15268 position,
15269 self.pool.as_deref(),
15270 &mut logits,
15271 );
15272 self.graph_logits = Some(logits);
15273 return vec![0.0; self.hidden_size];
15274 }
15275 if let Some(b) = &self.g3n {
15278 let _ = (task_mask, upto);
15279 return crate::g3n::g3n_forward(
15280 &b.0,
15281 &b.1,
15282 hidden,
15283 position,
15284 &mut self.kv_cache.layers,
15285 self.num_heads,
15286 self.num_kv_heads,
15287 self.head_dim,
15288 self.pool.as_deref(),
15289 );
15290 }
15291 if from == 0
15298 && upto.is_none()
15299 && task_mask.is_none()
15300 && self.anchor_core.is_some()
15301 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
15302 {
15303 static ONCE: std::sync::Once = std::sync::Once::new();
15304 ONCE.call_once(|| {
15305 eprintln!(
15306 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
15307 unsupported={} eligible={}",
15308 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
15309 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
15310 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
15311 crate::gpu::enabled_here(),
15312 self.graph_refused(),
15313 self.embryo_resident_eligible(),
15314 );
15315 });
15316 }
15317 if from == 0
15318 && upto.is_none()
15319 && task_mask.is_none()
15320 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
15324 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
15325 && matches!(
15329 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
15330 Ok("1") | Ok("parallel")
15331 )
15332 && crate::gpu::enabled_here()
15333 && !self.graph_refused()
15334 && (position == 0 || self.device_sequence_position().is_some())
15340 && self.embryo_resident_eligible()
15341 && let Some(model) = self.ensure_embryo_graph()
15342 {
15343 let mut lg = Vec::new();
15344 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
15345 {
15346 self.graph_logits = Some(lg);
15347 return vec![0.0; self.hidden_size];
15348 }
15349 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
15350 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
15351 }
15352 self.mark_graph_refused();
15358 if position != 0 {
15359 self.kv_cache.clear();
15363 self.clear_history();
15364 crate::gpu::graph_kv_reset(self.graph_kv_id);
15365 panic!(
15366 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
15367 );
15368 }
15369 }
15370 let mut h = hidden.to_vec();
15371 self.mimo_moe_prepare();
15374 let _mimo_q8 = self.mimo_moe.is_on()
15375 .then(crate::qtensor::enter_full_gpu_q8_scope);
15376 let (nh, _nkv, _hd, hs, _rd, eps) = (
15379 self.num_heads,
15380 self.num_kv_heads,
15381 self.head_dim,
15382 self.hidden_size,
15383 self.rotary_dim,
15384 self.rms_eps,
15385 );
15386 let pool = self.pool.clone();
15387 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
15399 let graph_on = match graph_env.as_deref() {
15400 Some("0") => false,
15401 Some("prefill") => false, Some(_) => true,
15403 None => crate::gpu::wgpu_graph_default(),
15409 };
15410 let graph_trusted =
15411 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
15412 let race_eligible = graph_on
15413 && upto.is_none()
15414 && task_mask.is_none()
15415 && from == 0
15416 && !self.graph_refused();
15417 let mut tail_start = 0usize;
15418 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
15419 let t_graph = std::time::Instant::now();
15420 let mut lg = Vec::new();
15421 let mut gl = 0usize;
15422 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
15423 let declined = built.is_none();
15424 let built = match built {
15425 Some(Ok(hh)) => Some(hh),
15426 Some(Err(())) => {
15427 self.clear_sequence_state();
15431 self.graph_failed
15432 .store(true, std::sync::atomic::Ordering::Relaxed);
15433 self.cancel
15434 .store(true, std::sync::atomic::Ordering::Relaxed);
15435 tracing::error!("token graph failed after admission; sequence state cleared");
15436 return vec![0.0; self.hidden_size];
15437 }
15438 None => None,
15439 };
15440 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
15445 self.mark_graph_refused();
15446 }
15447 graph_note(built.is_some(), gl, self.num_layers);
15448 if let Some(hh) = built {
15449 let dur = t_graph.elapsed();
15450 if std::env::var("CMF_GRAPH_PROF").is_ok() {
15451 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
15452 }
15453 if gl > 0 && gl < self.num_layers {
15454 h = hh;
15460 tail_start = gl;
15461 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
15462 if !graph_trusted {
15463 crate::gpu::graph_race_record(true, dur);
15464 }
15465 if !lg.is_empty() {
15466 lg.resize(self.vocab_size, 0.0);
15469 if let Some(c) = self.final_softcap {
15470 softcap_in_place(&mut lg, c, self.pool.as_deref());
15471 }
15472 self.graph_logits = Some(lg);
15473 }
15474 return hh;
15475 }
15476 }
15482 }
15483 let span = from > 0 || upto.is_some();
15507 if span && graph_on && task_mask.is_none() && graph_trusted {
15508 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
15509 let mut lg = Vec::new();
15510 let mut gl = 0usize;
15511 let span_res =
15512 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
15513 let span_res = match span_res {
15514 Some(Ok(hh)) => Some(hh),
15515 Some(Err(())) => {
15516 self.clear_sequence_state();
15517 self.graph_failed
15518 .store(true, std::sync::atomic::Ordering::Relaxed);
15519 self.cancel
15520 .store(true, std::sync::atomic::Ordering::Relaxed);
15521 tracing::error!(
15522 "span token graph failed after admission; sequence state cleared"
15523 );
15524 return vec![0.0; self.hidden_size];
15525 }
15526 None => None,
15527 };
15528 graph_note(span_res.is_some(), gl, upto_excl - from);
15529 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15530 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15534 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15535 eprintln!(
15536 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15537 upto_excl - from,
15538 span_res.is_some()
15539 );
15540 }
15541 }
15542 if let Some(hh) = span_res {
15543 if gl == upto_excl - from {
15544 if !lg.is_empty() {
15545 lg.resize(self.vocab_size, 0.0);
15546 if let Some(c) = self.final_softcap {
15547 softcap_in_place(&mut lg, c, self.pool.as_deref());
15548 }
15549 self.graph_logits = Some(lg);
15550 }
15551 crate::gpu::set_layer(-1);
15552 return hh;
15553 }
15554 h = hh;
15556 tail_start = from + gl;
15557 }
15558 }
15559 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15564
15565 let host_tail = tail_start > from;
15575 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15576 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15577
15578 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15579 #[cfg(target_os = "macos")]
15580 let mut gpu_skip_until = 0usize;
15581 for li in tail_start.max(from)..self.num_layers {
15582 let _capacity_tail = automatic_gpu_prefix
15583 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15584 .map(|_| crate::gpu::enter_cpu_scope());
15585 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15587 if li > u {
15588 break;
15589 }
15590 }
15591 if let Some(mask) = task_mask {
15592 if !mask.layer_alive(li) {
15593 continue; }
15595 }
15596 #[cfg(target_os = "macos")]
15600 {
15601 if li < gpu_skip_until {
15602 continue;
15603 }
15604 if task_mask.is_none() {
15605 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15606 if self
15607 .graph_failed
15608 .load(std::sync::atomic::Ordering::Relaxed)
15609 {
15610 return vec![0.0; self.hidden_size];
15614 }
15615 if end > li {
15616 gpu_skip_until = end;
15617 if self.is_loop_end(end - 1) && end < self.num_layers {
15620 h = inference::rms_norm(
15621 &h,
15622 &self.weights.final_norm,
15623 self.rms_eps,
15624 self.norm_style,
15625 );
15626 }
15627 continue;
15628 }
15629 }
15630 }
15631
15632 if task_mask.is_none() {
15633 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15634 crate::gpu::BatchGraphOutcome::Completed => continue,
15635 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15636 crate::gpu::BatchGraphOutcome::Declined => {},
15637 }
15638 }
15639 #[cfg(feature = "gpu")]
15640 self.pull_lagging_host_kv(li, li + 1, position);
15641 let lw = &self.weights.layers[self.phys_layer(li)];
15642 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15643 if tp.parse::<usize>().ok() == Some(position) {
15644 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
15645 eprintln!(
15646 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15647 h[0], h[1]
15648 );
15649 }
15650 }
15651 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15654 inference::rms_norm_into(
15655 &h,
15656 &lw.input_norm,
15657 self.rms_eps,
15658 self.norm_style,
15659 &mut self.ws.n1,
15660 );
15661 drop(prof);
15662
15663 let attn_out = match &lw.attn {
15664 AttnKind::Mla(w) => {
15665 let inv_freq_l = self.layer_inv_freq(li);
15666 let rs = self.layer_rope_scale(li);
15667 let eps = self.rms_eps;
15668 let pool = self.pool.clone();
15669 mla_attention(
15670 w,
15671 &self.ws.n1,
15672 &mut self.kv_cache.layers[li],
15673 position,
15674 &inv_freq_l,
15675 rs,
15676 eps,
15677 pool.as_deref(),
15678 )
15679 }
15680 AttnKind::Linear(w) => {
15681 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
15682 vmf_phase_forward(
15683 &self.ws.n1,
15684 w,
15685 &cfg,
15686 &mut self.kv_cache.layers[li].linear_state,
15687 self.pool.as_deref(),
15688 )
15689 }
15690 AttnKind::Kda(w) => {
15691 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15692 crate::linear_core::kda_forward(
15693 &self.ws.n1,
15694 w,
15695 &cfg,
15696 &mut self.kv_cache.layers[li].linear_state,
15697 self.pool.as_deref(),
15698 )
15699 }
15700 AttnKind::LinearGdn(w) => {
15701 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15702 gdn_forward(
15703 &self.ws.n1,
15704 w,
15705 &cfg,
15706 &mut self.kv_cache.layers[li].linear_state,
15707 self.pool.as_deref(),
15708 )
15709 }
15710 AttnKind::ShortConv(w) => {
15711 let cfg = self
15712 .short_conv_cfg
15713 .expect("short-conv layer without short_conv_cfg");
15714 short_conv_forward(
15715 &self.ws.n1,
15716 w,
15717 &cfg,
15718 &mut self.kv_cache.layers[li].linear_state,
15719 self.pool.as_deref(),
15720 )
15721 }
15722 AttnKind::Bounded(w) => {
15723 let rope = self
15726 .bounded_rope
15727 .clone()
15728 .expect("bounded layer without an installed rotation table");
15729 let cfg = crate::bounded::BoundedAttnCfg {
15730 num_heads: self.num_heads,
15731 num_kv_heads: self.num_kv_heads,
15732 head_dim: self.head_dim,
15733 hidden_size: hs,
15734 scale: self.attn_scale,
15735 rope: &rope,
15736 pool: pool.as_deref(),
15737 };
15738 crate::bounded::bounded_attention(
15739 &self.ws.n1,
15740 w,
15741 &mut self.kv_cache.layers[li],
15742 &cfg,
15743 )
15744 }
15745 AttnKind::Full {
15746 wq,
15747 wk,
15748 wv,
15749 wo,
15750 q_norm,
15751 k_norm,
15752 output_gate,
15753 softplus_gate,
15754 bias,
15755 } if self.kv_cache.layers[li].o1_sealed() => {
15756 let inv_freq_l = self.layer_inv_freq(li);
15759 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15760 let cfg = QwenAttnCfg {
15761 num_heads: self.layer_num_heads(li),
15762 num_kv_heads: nkv_l,
15763 head_dim: hd_l,
15764 hidden_size: hs,
15765 position,
15766 inv_freq: &inv_freq_l,
15767 rotary_dim: rd_l,
15768 scale: self.attn_scale,
15769 softcap: self.attn_softcap,
15770 window: None,
15771 v_norm: self.attn_v_norm,
15772 qk_norm_after_rope: self.qk_norm_after_rope,
15773 gate_sigmoid: self.proj_gate_sigmoid,
15774 q_norm: q_norm.as_deref(),
15775 k_norm: k_norm.as_deref(),
15776 output_gate: *output_gate,
15777 softplus_gate: softplus_gate
15778 .as_ref()
15779 .map(|(gate, per_head)| (gate, *per_head)),
15780 rope_scale: self.layer_rope_scale(li),
15781 bias: bias
15782 .as_ref()
15783 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15784 rms_eps: eps,
15785 norm_style: self.norm_style,
15786 pool: pool.as_deref(),
15787 v_head_dim: self.layer_v_dim(li),
15788 };
15789 attention::qwen_attention_nystrom(
15790 &self.ws.n1,
15791 wq,
15792 wk,
15793 wv,
15794 wo,
15795 &mut self.kv_cache.layers[li],
15796 &cfg,
15797 )
15798 }
15799 AttnKind::Full {
15800 wq,
15801 wk,
15802 wv,
15803 wo,
15804 q_norm,
15805 k_norm,
15806 output_gate,
15807 softplus_gate,
15808 bias,
15809 } => 'attn: {
15810 let dropin_reason =
15815 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
15816 if let Some(reason) = dropin_reason {
15817 self.note_graph_decline("wgpu attn dropin", reason);
15818 }
15819 if graph_on
15820 && dropin_reason.is_none()
15821 && !*output_gate
15822 && softplus_gate.is_none()
15823 && self.attention_heads_per_layer.is_none()
15824 && bias.is_none()
15825 && task_mask.is_none()
15826 {
15827 let inv_freq_l = self.layer_inv_freq(li);
15828 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15829 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15830 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
15831 wq.mapped_q1(),
15832 wk.mapped_q1(),
15833 wv.mapped_q1(),
15834 wo.mapped_q1(),
15835 ) {
15836 let gm = gm.clone();
15837 let mut out = vec![0f32; hs];
15838 let cache = &self.kv_cache.layers[li];
15839 if crate::gpu::attn_dropin(
15840 &gm,
15841 self.graph_kv_id,
15842 li,
15843 &self.ws.n1,
15844 qi,
15845 ki,
15846 vi,
15847 oi,
15848 q_norm.as_deref(),
15849 k_norm.as_deref(),
15850 self.qk_norm_after_rope,
15851 &inv_freq_l,
15852 nh,
15853 nkv_l,
15854 hd_l,
15855 rd_l,
15856 hs,
15857 position,
15858 self.kv_cache.max_seq_len,
15859 gemma,
15860 eps as f32,
15861 cache.k_heads(),
15862 cache.v_heads(),
15863 &mut out,
15864 ) {
15865 break 'attn out;
15866 }
15867 }
15868 }
15869 let masked = task_mask
15870 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
15871 .unwrap_or(false);
15872 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
15873 let plain = self.layer_attn_plain(li);
15876 match (masked, f32_view) {
15877 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
15880 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
15881 attention::multi_head_attention(
15882 &self.ws.n1,
15883 q,
15884 k,
15885 v,
15886 o,
15887 &mut self.kv_cache.layers[li],
15888 self.num_heads,
15889 self.num_kv_heads,
15890 self.head_dim,
15891 self.hidden_size,
15892 position,
15893 &active_heads,
15894 &self.inv_freq,
15895 )
15896 }
15897 (masked, _) => {
15898 if masked {
15899 tracing::warn!(
15900 "layer {li}: head mask on quantized weights or on a \
15901 window/sink/per-layer-geometry layer not supported \
15902 yet — executing dense"
15903 );
15904 }
15905 let inv_freq_l = self.layer_inv_freq(li);
15906 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15907 let cfg = QwenAttnCfg {
15908 num_heads: self.layer_num_heads(li),
15909 num_kv_heads: nkv_l,
15910 head_dim: hd_l,
15911 hidden_size: hs,
15912 position,
15913 inv_freq: &inv_freq_l,
15914 rotary_dim: rd_l,
15915 scale: self.attn_scale,
15916 softcap: self.attn_softcap,
15917 window: self.layer_window(li),
15918 v_norm: self.attn_v_norm,
15919 qk_norm_after_rope: self.qk_norm_after_rope,
15920 gate_sigmoid: self.proj_gate_sigmoid,
15921 q_norm: q_norm.as_deref(),
15922 k_norm: k_norm.as_deref(),
15923 output_gate: *output_gate,
15924 softplus_gate: softplus_gate
15925 .as_ref()
15926 .map(|(gate, per_head)| (gate, *per_head)),
15927 rope_scale: self.layer_rope_scale(li),
15928 bias: bias
15929 .as_ref()
15930 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15931 rms_eps: eps,
15932 norm_style: self.norm_style,
15933 pool: pool.as_deref(),
15934 v_head_dim: self.layer_v_dim(li),
15935 };
15936 attention::qwen_attention(
15937 &self.ws.n1,
15938 wq,
15939 wk,
15940 wv,
15941 wo,
15942 &mut self.kv_cache.layers[li],
15943 &cfg,
15944 )
15945 }
15946 }
15947 }
15948 };
15949 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
15952 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
15953 None => attn_out,
15954 };
15955 let lw = &self.weights.layers[self.phys_layer(li)];
15956 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15957 inference::add_rmsnorm_fused_into(
15958 &mut h,
15959 &attn_out,
15960 &lw.post_norm,
15961 self.rms_eps,
15962 self.norm_style,
15963 &mut self.ws.p1,
15964 );
15965 drop(prof);
15966 let mut attn_out = attn_out;
15967 attention::recycle_buf(&mut attn_out);
15968 let post_normed = &self.ws.p1;
15969
15970 let ffn_masked = task_mask
15971 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
15972 .unwrap_or(false);
15973 let ffn_out = match (ffn_masked, &lw.ffn) {
15985 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
15989 let row = task_mask
15990 .and_then(|tm| tm.ffn_masks.get(li))
15991 .map(|v| v.as_slice());
15992 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
15993 }
15994 (true, FfnKind::Dense(d)) => {
15995 let tm = task_mask.unwrap();
15996 let alive = tm.ffn_active_count(li);
15997 let deep = alive * 2 <= self.intermediate_size;
15998 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
15999 let active = tm.ffn_active_indices(li);
16000 sparse_ffn_quant(
16001 d,
16002 post_normed,
16003 &active,
16004 self.hidden_size,
16005 self.pool.as_deref(),
16006 )
16007 } else if deep
16008 && let (Some(g), Some(u), Some(dn)) = (
16009 d.gate_proj.as_f32(),
16010 d.up_proj.as_f32(),
16011 d.down_proj.as_f32(),
16012 )
16013 {
16014 let active = tm.ffn_active_indices(li);
16015 inference::sparse_ffn_forward(
16016 post_normed,
16017 g,
16018 u,
16019 dn,
16020 self.hidden_size,
16021 self.intermediate_size,
16022 &active,
16023 self.pool.as_deref(),
16024 )
16025 } else {
16026 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
16027 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
16028 }
16029 }
16030 (true, FfnKind::Moe(m)) => {
16031 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
16035 ffn_forward(
16036 &lw.ffn,
16037 post_normed,
16038 self.pool.as_deref(),
16039 allowed.as_deref(),
16040 )
16041 }
16042 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
16043 dm,
16044 post_normed,
16045 &h,
16046 self.rms_eps,
16047 self.norm_style,
16048 self.pool.as_deref(),
16049 ),
16050 (false, _) => match &lw.ffn {
16051 FfnKind::DenseMoe(dm) => dense_moe_ffn(
16052 dm,
16053 post_normed,
16054 &h,
16055 self.rms_eps,
16056 self.norm_style,
16057 self.pool.as_deref(),
16058 ),
16059 FfnKind::Moe(m)
16060 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
16061 {
16062 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
16063 }
16064 _ => {
16065 let allowed = match (&lw.ffn, task_mask) {
16066 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
16067 _ => None,
16068 };
16069 ffn_forward(
16070 &lw.ffn,
16071 post_normed,
16072 self.pool.as_deref(),
16073 allowed.as_deref(),
16074 )
16075 }
16076 },
16077 };
16078 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
16079 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
16080 None => ffn_out,
16081 };
16082 for (i, &f) in ffn_out.iter().enumerate() {
16083 h[i] += f;
16084 }
16085 let mut ffn_out = ffn_out;
16086 attention::recycle_buf(&mut ffn_out);
16087
16088 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
16090 for v in h.iter_mut() {
16091 *v *= sc;
16092 }
16093 }
16094 if self.layer_dump.is_some() {
16096 self.dump_layer_row(position, li, &h);
16097 }
16098
16099 if self.is_loop_end(li) && li + 1 < self.num_layers {
16102 h = inference::rms_norm(
16103 &h,
16104 &self.weights.final_norm,
16105 self.rms_eps,
16106 self.norm_style,
16107 );
16108 }
16109
16110 if self.dyn_phi_layer == Some(li) {
16114 self.update_dyn_phi(&h);
16115 }
16116 }
16117 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
16119 crate::gpu::graph_race_record(false, t.elapsed());
16120 }
16121
16122 h
16123 }
16124
16125 fn update_dyn_phi(&mut self, h: &[f32]) {
16128 const A: f32 = 0.2;
16129 if self.dyn_phi_ema.len() != h.len() {
16130 self.dyn_phi_ema = vec![0.0; h.len()];
16131 self.dyn_phi_seen = 0;
16132 }
16133 if self.dyn_phi_seen == 0 {
16134 self.dyn_phi_ema.copy_from_slice(h);
16135 } else {
16136 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
16137 *e = (1.0 - A) * *e + A * v;
16138 }
16139 }
16140 self.dyn_phi_seen += 1;
16141 }
16142
16143 pub fn dyn_phi(&self) -> &[f32] {
16145 &self.dyn_phi_ema
16146 }
16147
16148 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
16150 self.dyn_phi_layer = layer;
16151 self.dyn_phi_ema.clear();
16152 self.dyn_phi_seen = 0;
16153 }
16154
16155 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
16157 let Some(model) = &self.model else {
16158 return Vec::new();
16159 };
16160 model
16161 .header
16162 .skills
16163 .iter()
16164 .enumerate()
16165 .filter_map(|(i, sk)| {
16166 if sk.is_v2() {
16170 return None;
16171 }
16172 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
16173 let sel = sk.selection.as_ref()?;
16174 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
16175 })
16176 .collect()
16177 }
16178
16179 pub fn active_skill(&self) -> Option<usize> {
16181 self.dyn_active
16182 }
16183
16184 pub fn enable_dynamic_routing(&mut self) -> usize {
16189 use crate::swarm::{DynRouter, RoutableSkill};
16190 let Some(model) = self.model.clone() else {
16191 return 0;
16192 };
16193 if let Some(r) = &model.header.router {
16200 tracing::warn!(
16201 "dynamic routing disabled: this file declares router policy '{}' with \
16202 granularity \"{}\" — the request-level decision applies instead",
16203 r.policy,
16204 r.granularity
16205 );
16206 return 0;
16207 }
16208 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
16214 || model.header.skills.iter().any(|s| s.is_v2())
16215 {
16216 tracing::warn!(
16217 "dynamic routing disabled: this file carries format-v2 skill records \
16218 (SKILLS_V2) — they route per request through a router policy only"
16219 );
16220 return 0;
16221 }
16222 if self.dyn_blend_loaded {
16225 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
16226 return 0;
16227 }
16228 if let Some(a) = self.dyn_active {
16232 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
16233 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
16234 return 0;
16235 }
16236 }
16237 let hidden = self.hidden_size;
16238 let mut skills = Vec::new();
16239 for (idx, id, _phi) in self.dynamic_skills() {
16240 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
16241 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
16242 skills.push(rs);
16243 }
16244 }
16245 }
16246 if skills.is_empty() {
16247 return 0;
16248 }
16249 let phi = skills[0].phi_layer;
16251 if skills.iter().any(|s| s.phi_layer != phi) {
16252 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
16253 }
16254 let n = skills.len();
16255 self.set_dyn_phi_layer(Some(phi));
16256 self.dyn_router = Some(DynRouter::new(skills));
16257 n
16258 }
16259
16260 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
16262 self.dyn_router
16263 .as_ref()
16264 .map(|r| r.switches.clone())
16265 .unwrap_or_default()
16266 }
16267
16268 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
16271 let _mimo_q8 = self.mimo_moe.is_on()
16272 .then(crate::qtensor::enter_full_gpu_q8_scope);
16273 let rows = self.weights.lm_head.rows();
16274 let mut logits = attention::take_buf(rows.min(self.vocab_size));
16275 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
16279 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
16280 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
16281 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
16282 rows, self.hidden_size, &mut logits)
16283 });
16284 if !served {
16285 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
16286 }
16287 logits.resize(self.vocab_size, 0.0);
16288 if let Some(m) = self.logit_multiplier {
16289 for l in logits.iter_mut() {
16290 *l *= m;
16291 }
16292 }
16293 if let Some(c) = self.final_softcap {
16294 softcap_in_place(&mut logits, c, self.pool.as_deref());
16295 }
16296 if let Some(cm) = self.head_clusters.as_ref() {
16297 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
16298 }
16299 logits
16300 }
16301
16302 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
16305 let h = hidden.len();
16306 let ncl = cm.len() / h.max(1);
16307 if ncl == 0 || logits.len() % ncl != 0 {
16308 return;
16309 }
16310 let cs = logits.len() / ncl;
16311 let mut lc = vec![0.0f32; ncl];
16313 for c in 0..ncl {
16314 let row = &cm[c * h..(c + 1) * h];
16315 let mut s = 0.0f32;
16316 for j in 0..h {
16317 s += row[j] * hidden[j];
16318 }
16319 lc[c] = s;
16320 }
16321 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16322 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
16323 for c in 0..ncl {
16324 let blk = &mut logits[c * cs..(c + 1) * cs];
16325 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
16326 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
16327 let add = lc[c] - lse - bl;
16328 for v in blk.iter_mut() {
16329 *v += add;
16330 }
16331 }
16332 }
16333
16334 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
16339 #[cfg(target_os = "macos")]
16340 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
16341 self.clear_sequence_state();
16342 crate::gpu::graph_race_begin_generation();
16346 if task_mask.is_none() {
16347 self.o1_begin();
16348 }
16349 let mut hidden = vec![0.0f32; self.hidden_size];
16350 for (pos, &id) in ids.iter().enumerate() {
16351 let emb = self.embed_single(id);
16352 hidden = self.forward_layers(&emb, pos, task_mask);
16353 }
16354 if let Err(err) = self.o1_seal_checked() {
16355 self.o1_fail(err);
16356 }
16357 if let Some(logits) = self.graph_logits.take() {
16360 return logits;
16361 }
16362 inference::rms_norm_into(
16363 &hidden,
16364 &self.weights.final_norm,
16365 self.rms_eps,
16366 self.norm_style,
16367 &mut self.ws.n1,
16368 );
16369 self.lm_head_forward(&self.ws.n1)
16370 }
16371}
16372
16373pub fn create_test_pipeline(
16375 hidden_size: usize,
16376 intermediate_size: usize,
16377 num_heads: usize,
16378 num_kv_heads: usize,
16379 head_dim: usize,
16380 num_layers: usize,
16381 vocab_size: usize,
16382) -> Pipeline {
16383 let synth = |n: usize, salt: usize| -> Vec<f32> {
16386 (0..n)
16387 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
16388 .collect()
16389 };
16390 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
16391 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
16392 };
16393 let layer_weights: Vec<LayerWeights> = (0..num_layers)
16394 .map(|li| LayerWeights {
16395 input_norm: vec![1.0; hidden_size],
16396 post_norm: vec![1.0; hidden_size],
16397 attn_out_norm: None,
16398 ffn_out_norm: None,
16399 layer_scale: None,
16400 ffn: FfnKind::Dense(DenseFfn {
16401 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
16402 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
16403 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
16404 act: Act::Silu,
16405 down_t: None,
16406 segs: Vec::new(),
16407 }),
16408 attn: AttnKind::Full {
16409 bias: None,
16410 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
16411 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
16412 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
16413 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
16414 q_norm: None,
16415 k_norm: None,
16416 output_gate: false,
16417 softplus_gate: None,
16418 },
16419 })
16420 .collect();
16421
16422 Pipeline::new(
16423 Tokenizer::byte_level(),
16424 PipelineWeights {
16425 embed_tokens: qt(vocab_size, hidden_size, 100),
16426 layers: layer_weights,
16427 lm_head: qt(vocab_size, hidden_size, 200),
16428 final_norm: vec![1.0; hidden_size],
16429 },
16430 hidden_size,
16431 intermediate_size,
16432 num_heads,
16433 num_kv_heads,
16434 head_dim,
16435 num_layers,
16436 num_layers, false, vocab_size,
16439 1e-6,
16440 10_000.0,
16441 NormStyle::Qwen,
16442 4096,
16443 SamplerConfig {
16444 seed: Some(42),
16445 ..Default::default()
16446 },
16447 )
16448}
16449
16450#[inline]
16455fn mask_bit(row: &[u8], j: usize) -> bool {
16456 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
16457}
16458
16459fn mask_gain() -> f32 {
16470 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
16471 *G.get_or_init(|| {
16472 std::env::var("CMF_FFN_MASK_GAIN")
16473 .ok()
16474 .and_then(|v| v.parse().ok())
16475 .unwrap_or(1.0)
16476 })
16477}
16478
16479fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
16480 let fill = meanfill().and_then(|(i, v)| {
16483 let li = crate::gpu::cur_layer();
16484 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
16485 });
16486 for r in 0..rows {
16487 let base = r * inter;
16488 for (bi, &byte) in row.iter().enumerate() {
16489 if byte == 0xFF {
16490 continue;
16491 }
16492 let j0 = bi * 8;
16493 for bit in 0..8 {
16494 let j = j0 + bit;
16495 if j < inter && byte & (1 << bit) == 0 {
16496 g[base + j] = fill.map_or(0.0, |f| f[j]);
16497 }
16498 }
16499 }
16500 }
16501 let gain = mask_gain();
16502 if gain != 1.0 {
16503 for v in g[..rows * inter].iter_mut() {
16504 *v *= gain;
16505 }
16506 }
16507}
16508
16509#[inline]
16511fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
16512 row.is_none_or(|r| mask_bit(r, i))
16513}
16514
16515fn all_bits_on(row: &[u8], n: usize) -> bool {
16518 (0..n).all(|i| mask_bit(row, i))
16519}
16520
16521fn tube_topk() -> usize {
16529 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16530 *K.get_or_init(|| {
16531 std::env::var("CMF_TUBE_TOPK")
16532 .ok()
16533 .and_then(|v| v.parse().ok())
16534 .unwrap_or(0)
16535 })
16536}
16537
16538fn tube_score_oracle() -> bool {
16539 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16540 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16541}
16542
16543fn tube_ffn_routed(
16550 d: &DenseFfn,
16551 xs: &[f32],
16552 b: usize,
16553 pool: Option<&Pool>,
16554 mask_row: Option<&[u8]>,
16555 k: usize,
16556) -> Vec<f32> {
16557 let hidden = d.down_proj.rows();
16558 let core = d.gate_proj.rows();
16559 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16560 let mut out = match (b, core_full, mask_row) {
16561 (1, true, _) => dense_ffn(d, xs, pool),
16562 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16563 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16564 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16565 };
16566 let cand: Vec<usize> = (0..d.segs.len())
16567 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16568 .collect();
16569 if cand.is_empty() {
16570 return out;
16571 }
16572 let oracle = tube_score_oracle();
16576 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16577 let mut scores = vec![0f32; b * cand.len()];
16578 for (ci, &i) in cand.iter().enumerate() {
16579 let seg = &d.segs[i];
16580 let w = seg.width;
16581 let mut g = vec![0.0f32; b * w];
16582 if b == 1 {
16583 seg.gate.matvec(xs, &mut g, pool);
16584 } else {
16585 seg.gate.matmat(xs, b, &mut g, pool);
16586 }
16587 for v in g.iter_mut() {
16588 *v = Act::Silu.combine(*v, 1.0);
16589 }
16590 if !oracle {
16591 for t in 0..b {
16592 scores[t * cand.len() + ci] =
16593 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16594 }
16595 }
16596 if oracle || b > 1 {
16597 let mut u = vec![0.0f32; b * w];
16598 if b == 1 {
16599 seg.up.matvec(xs, &mut u, pool);
16600 } else {
16601 seg.up.matmat(xs, b, &mut u, pool);
16602 }
16603 for (a, &v) in g.iter_mut().zip(u.iter()) {
16604 *a *= v;
16605 }
16606 if oracle {
16607 for t in 0..b {
16608 scores[t * cand.len() + ci] =
16609 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16610 }
16611 }
16612 }
16613 acts.push(g);
16614 }
16615 let keep = k.min(cand.len());
16617 let mut scratch: Vec<f32> = Vec::new();
16618 for t in 0..b {
16619 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16620 .map(|ci| (scores[t * cand.len() + ci], ci))
16621 .collect();
16622 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
16623 let mut alive = vec![false; cand.len()];
16624 for &(_, ci) in sc.iter().take(keep) {
16625 alive[ci] = true;
16626 }
16627 if b > 1 {
16628 for (ci, a) in acts.iter_mut().enumerate() {
16629 if !alive[ci] {
16630 let w = d.segs[cand[ci]].width;
16631 a[t * w..(t + 1) * w].fill(0.0);
16632 }
16633 }
16634 } else {
16635 for (ci, &i) in cand.iter().enumerate() {
16639 if !alive[ci] {
16640 continue;
16641 }
16642 let seg = &d.segs[i];
16643 let w = seg.width;
16644 let g = &mut acts[ci];
16645 if !tube_score_oracle() {
16646 scratch.clear();
16647 scratch.resize(w, 0.0);
16648 seg.up.matvec(xs, &mut scratch, pool);
16649 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
16650 *a *= v;
16651 }
16652 }
16653 let mut acc = vec![0.0f32; hidden];
16654 seg.down.matvec(g, &mut acc, pool);
16655 for (o, a) in out.iter_mut().zip(&acc) {
16656 *o += *a;
16657 }
16658 }
16659 }
16660 }
16661 if b > 1 {
16662 for (ci, &i) in cand.iter().enumerate() {
16663 let seg = &d.segs[i];
16664 let mut acc = vec![0.0f32; b * hidden];
16665 seg.down.matmat(&acts[ci], b, &mut acc, pool);
16666 for (o, a) in out.iter_mut().zip(&acc) {
16667 *o += *a;
16668 }
16669 }
16670 }
16671 out
16672}
16673
16674fn tube_ffn(
16680 d: &DenseFfn,
16681 xs: &[f32],
16682 b: usize,
16683 pool: Option<&Pool>,
16684 mask_row: Option<&[u8]>,
16685) -> Vec<f32> {
16686 if tube_topk() > 0 {
16687 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
16688 }
16689 let hidden = d.down_proj.rows();
16690 let core = d.gate_proj.rows();
16691 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16692 let mut out = match (b, core_full, mask_row) {
16693 (1, true, _) => dense_ffn(d, xs, pool),
16694 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16695 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16696 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16697 };
16698 TUBE_SCRATCH.with(|sc| {
16699 let mut sc = sc.borrow_mut();
16700 let [g, u, acc] = &mut *sc;
16701 for seg in &d.segs {
16702 if !tube_bit(mask_row, seg.start) {
16703 continue;
16704 }
16705 let w = seg.width;
16706 g.resize(b * w, 0.0);
16707 if b == 1
16708 && d.act == Act::Silu
16709 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
16710 {
16711 } else {
16713 u.resize(b * w, 0.0);
16714 if b == 1 {
16715 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
16716 } else {
16717 seg.gate.matmat(xs, b, g, pool);
16718 seg.up.matmat(xs, b, u, pool);
16719 }
16720 for i in 0..b * w {
16721 g[i] = d.act.combine(g[i], u[i]);
16722 }
16723 }
16724 acc.resize(b * hidden, 0.0);
16725 acc.fill(0.0);
16726 if b == 1 {
16727 seg.down.matvec(g, acc, pool);
16728 } else {
16729 seg.down.matmat(g, b, acc, pool);
16730 }
16731 for (o, a) in out.iter_mut().zip(acc.iter()) {
16732 *o += *a;
16733 }
16734 }
16735 out
16736 })
16737}
16738
16739thread_local! {
16740 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
16744 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
16745}
16746
16747static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
16750 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
16751
16752fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
16756 #[cfg(feature = "gpu")]
16757 {
16758 use std::sync::atomic::Ordering::Relaxed;
16759 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
16760 (
16761 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
16762 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
16763 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
16764 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
16765 )
16766 }
16767 #[cfg(not(feature = "gpu"))]
16768 (0.0, 0.0, 0.0, 0)
16769}
16770
16771fn wgpu_mirror_counters() -> [u64; 6] {
16774 #[cfg(feature = "gpu")]
16775 {
16776 crate::gpu_wgpu::MIRROR_EVENTS
16777 .each_ref()
16778 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
16779 }
16780 #[cfg(not(feature = "gpu"))]
16781 [0; 6]
16782}
16783
16784fn prefill_prof_on() -> bool {
16785 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16786 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
16787}
16788
16789fn dense_ffn_batch(
16790 d: &DenseFfn,
16791 xs: &[f32],
16792 b: usize,
16793 pool: Option<&Pool>,
16794 mask_row: Option<&[u8]>,
16795) -> Vec<f32> {
16796 let inter = d.gate_proj.rows();
16797 let hidden = d.down_proj.rows();
16798 let fused_act = d.act.graph_act();
16808 if mask_row.is_none()
16809 && fused_act.is_some()
16810 && b >= 32
16811 && crate::gpu::enabled_here()
16812 && !crate::gpu::mm_killed()
16813 && refit_dir().is_none()
16818 && !ffn_probe_active()
16823 {
16824 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16825 d.gate_proj.mapped_q4t(),
16826 d.up_proj.mapped_q4t(),
16827 d.down_proj.mapped_q4t(),
16828 ) {
16829 let mut out = vec![0.0f32; b * hidden];
16830 let act = fused_act.expect("checked above");
16831 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
16832 {
16833 return out;
16834 }
16835 }
16836 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16841 d.gate_proj.mapped_q4tp(),
16842 d.up_proj.mapped_q4tp(),
16843 d.down_proj.mapped_q4tp(),
16844 ) {
16845 let mut out = vec![0.0f32; b * hidden];
16846 let act = fused_act.expect("checked above");
16847 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
16848 {
16849 return out;
16850 }
16851 }
16852 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16858 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
16859 d.gate_proj.mapped_device_gemm(),
16860 d.up_proj.mapped_device_gemm(),
16861 d.down_proj.mapped_device_gemm(),
16862 ) {
16863 let mut out = vec![0.0f32; b * hidden];
16864 let act = fused_act.expect("checked above");
16865 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
16866 return out;
16867 }
16868 }
16869 }
16870 let mut g = vec![0.0f32; b * inter];
16871 d.gate_proj.matmat(xs, b, &mut g, pool);
16872 let mut u = vec![0.0f32; b * inter];
16873 d.up_proj.matmat(xs, b, &mut u, pool);
16874 if gate_topk() > 0 && d.act == Act::Silu {
16875 for t in 0..b {
16876 let row = &mut g[t * inter..(t + 1) * inter];
16877 for v in row.iter_mut() {
16878 *v = Act::Silu.combine(*v, 1.0);
16879 }
16880 keep_top_k(row, gate_topk());
16881 }
16882 for i in 0..b * inter {
16883 g[i] *= u[i];
16884 }
16885 } else {
16886 for i in 0..b * inter {
16887 g[i] = d.act.combine(g[i], u[i]);
16888 }
16889 }
16890 if let Some(row) = mask_row {
16891 zero_masked_cols(&mut g, b, inter, row);
16892 }
16893 if oracle_topk() > 0 {
16894 for t in 0..b {
16895 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
16896 }
16897 }
16898 let mut out = vec![0.0f32; b * hidden];
16899 d.down_proj.matmat(&g, b, &mut out, pool);
16900 if refit_dir().is_some() {
16901 let li = crate::gpu::cur_layer();
16902 if li >= 0 {
16903 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
16904 }
16905 }
16906 FFN_PROBE.with(|pr| {
16910 if let Some(acc) = pr.borrow_mut().as_mut() {
16911 let li = crate::gpu::cur_layer();
16912 if li < 0 {
16913 return;
16914 }
16915 let Some(row) = acc.get_mut(li as usize) else {
16916 return;
16917 };
16918 let sq = probe_sq();
16919 for t in 0..b {
16920 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
16921 *a += if sq {
16922 (v as f64) * (v as f64)
16923 } else {
16924 (v as f64).abs()
16925 };
16926 }
16927 }
16928 }
16929 });
16930 out
16931}
16932
16933fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
16938 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16939 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16940 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
16941 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
16942 if (!on && !dump) || b == 0 {
16943 return;
16944 }
16945 let hidden = xs.len() / b;
16946 if on {
16947 let mut acc = m.act_sq.borrow_mut();
16948 if acc.len() < hidden {
16949 acc.resize(hidden, 0.0);
16950 }
16951 for t in 0..b {
16952 let row = &xs[t * hidden..(t + 1) * hidden];
16953 for (a, &v) in acc.iter_mut().zip(row) {
16954 *a += (v as f64) * (v as f64);
16955 }
16956 }
16957 }
16958 if dump {
16959 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
16962 .ok()
16963 .and_then(|v| v.parse().ok())
16964 .unwrap_or(4096);
16965 let mut rows = m.act_rows.borrow_mut();
16966 if rows.len() < cap * hidden {
16967 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
16968 rows.extend_from_slice(&xs[..take * hidden]);
16969 }
16970 }
16971}
16972
16973#[derive(Clone, Copy)]
16976struct SendVecs(*mut Vec<f32>);
16977unsafe impl Send for SendVecs {}
16978unsafe impl Sync for SendVecs {}
16979impl SendVecs {
16980 #[inline]
16981 fn at(self, i: usize) -> *mut Vec<f32> {
16982 unsafe { self.0.add(i) }
16983 }
16984}
16985
16986fn moe_ffn_batch(
16987 m: &MoeFfn,
16988 xs: &[f32],
16989 b: usize,
16990 hidden: usize,
16991 pool: Option<&Pool>,
16992 allowed: Option<&[bool]>,
16993) -> Vec<f32> {
16994 accumulate_act(m, xs, b);
16995 let ne = m.experts.len();
16996 let mut logits = vec![0.0f32; b * ne];
16997 match &m.resonance {
16998 Some(r) => {
16999 let hdim = xs.len() / b.max(1);
17000 for bi in 0..b {
17001 r.scores(
17002 &xs[bi * hdim..(bi + 1) * hdim],
17003 &mut logits[bi * ne..(bi + 1) * ne],
17004 );
17005 }
17006 }
17007 None => m.router.matmat(xs, b, &mut logits, pool),
17008 }
17009
17010 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
17013 {
17014 let mut st = m.stats.borrow_mut();
17015 if st.len() < ne {
17016 st.resize(ne, 0);
17017 }
17018 for bi in 0..b {
17019 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
17020 for &e in &idx {
17021 st[e] += 1;
17022 assign[e].push((bi, p[e] / wsum));
17023 }
17024 }
17025 }
17026
17027 let mut out = vec![0.0f32; b * hidden];
17028 let cols = m.experts[0].gate_proj.cols();
17029 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
17030 let sb = list.len();
17031 let mut sub = vec![0.0f32; sb * cols];
17032 for (k, &(bi, _)) in list.iter().enumerate() {
17033 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17034 }
17035 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
17036 for (k, &(bi, w)) in list.iter().enumerate() {
17037 for i in 0..hidden {
17038 out[bi * hidden + i] += w * eo[k * hidden + i];
17039 }
17040 }
17041 };
17042 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
17048 if pool.is_some() && active.len() >= 8 {
17049 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
17050 {
17051 let panel_ptr = SendVecs(panels.as_mut_ptr());
17052 let experts = &m.experts;
17055 let (active_r, assign_r) = (&active, &assign);
17056 let inherit_cpu = crate::gpu::inherit_cpu_scope();
17057 let run = |start: usize, end: usize| {
17058 let _cpu_scope = inherit_cpu();
17059 #[cfg(target_os = "macos")]
17067 let _metal_cpu = crate::gpu::enter_cpu_scope();
17068 for ai in start..end {
17069 let e = active_r[ai];
17070 let list = &assign_r[e];
17071 let sb = list.len();
17072 let mut sub = vec![0.0f32; sb * cols];
17073 for (k, &(bi, _)) in list.iter().enumerate() {
17074 sub[k * cols..(k + 1) * cols]
17075 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
17076 }
17077 unsafe {
17079 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
17080 }
17081 }
17082 };
17083 match pool {
17084 Some(p) => p.run_rows(active.len(), &run),
17085 None => run(0, active.len()),
17086 }
17087 }
17088 for (ai, &e) in active.iter().enumerate() {
17089 for (k, &(bi, w)) in assign[e].iter().enumerate() {
17090 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
17091 for i in 0..hidden {
17092 out[bi * hidden + i] += w * eo[i];
17093 }
17094 }
17095 }
17096 } else {
17097 for &e in &active {
17098 run_expert(&m.experts[e], &assign[e], &mut out);
17099 }
17100 }
17101 if let Some((se, gate)) = &m.shared {
17102 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
17103 let mut gl = vec![0.0f32; b];
17104 gate.matmat(xs, b, &mut gl, pool);
17105 (0..b)
17106 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
17107 .collect()
17108 } else {
17109 (0..b).map(|bi| (bi, 1.0)).collect()
17110 };
17111 run_expert(se, &all, &mut out);
17112 }
17113 out
17114}
17115
17116fn moe_ffn_rows_exact(
17128 m: &MoeFfn,
17129 xs: &[f32],
17130 b: usize,
17131 hidden: usize,
17132 pool: Option<&Pool>,
17133) -> Vec<f32> {
17134 let mut out = vec![0.0f32; b * hidden];
17135 let per_row = |out: &mut [f32]| {
17136 for r in 0..b {
17137 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
17138 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
17139 }
17140 };
17141 let covered = !crate::gpu::enabled_here()
17142 && moe_batch_enabled()
17143 && m.shared.is_none()
17144 && m.resonance.is_none()
17145 && FFN_PROBE.with(|pr| pr.borrow().is_none())
17146 && m.experts.iter().all(|d| d.act == Act::Silu);
17147 if !covered {
17148 per_row(&mut out);
17149 return out;
17150 }
17151 let ne = m.experts.len();
17152 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
17154 for r in 0..b {
17155 let x = &xs[r * hidden..(r + 1) * hidden];
17156 accumulate_act(m, x, 1);
17157 let mut logits = vec![0.0f32; ne];
17158 m.router.matvec(x, &mut logits, pool);
17159 let (idx, p, wsum) = moe_route(&logits, m, None);
17160 {
17161 let mut st = m.stats.borrow_mut();
17162 if st.len() < ne {
17163 st.resize(ne, 0);
17164 }
17165 for &e in &idx {
17166 st[e] += 1;
17167 }
17168 }
17169 let w: Vec<f32> = idx
17170 .iter()
17171 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
17172 .collect();
17173 routes.push((idx, w));
17174 }
17175 if routes.iter().any(|(idx, _)| idx.is_empty()) {
17176 per_row(&mut out);
17177 return out;
17178 }
17179 let mut experts: Vec<usize> = Vec::new();
17181 let mut groups: Vec<Vec<usize>> = Vec::new();
17182 for (r, (idx, _)) in routes.iter().enumerate() {
17183 for &e in idx {
17184 match experts.iter().position(|&x| x == e) {
17185 Some(g) => groups[g].push(r),
17186 None => {
17187 experts.push(e);
17188 groups.push(vec![r]);
17189 }
17190 }
17191 }
17192 }
17193 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
17194 let inter = m.experts[experts[0]].gate_proj.rows();
17195 let pairs: Vec<(&QTensor, &QTensor)> = experts
17196 .iter()
17197 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
17198 .collect();
17199 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
17200 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
17201 per_row(&mut out);
17202 return out;
17203 }
17204 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
17205 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
17206 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
17207 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
17208 per_row(&mut out);
17209 return out;
17210 }
17211 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
17213 let mut p = 0usize;
17214 for (g, &e) in experts.iter().enumerate() {
17215 for &r in &groups[g] {
17216 slot.insert((r, e), p);
17217 p += 1;
17218 }
17219 }
17220 for (r, (idx, w)) in routes.iter().enumerate() {
17221 let terms: Vec<(&[f32], f32)> = idx
17222 .iter()
17223 .zip(w)
17224 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
17225 .collect();
17226 let row = &mut out[r * hidden..(r + 1) * hidden];
17227 for (i, dst) in row.iter_mut().enumerate() {
17228 let mut acc = 0f32;
17230 for (d, we) in &terms {
17231 acc += we * d[i];
17232 }
17233 *dst = acc;
17234 }
17235 }
17236 out
17237}
17238
17239pub(crate) fn softcap_in_place(v: &mut [f32], c: f32, pool: Option<&Pool>) {
17243 match pool {
17244 Some(p) if v.len() >= 1 << 15 => {
17245 let base = v.as_mut_ptr() as usize;
17246 p.run_rows(v.len(), &|s, e| {
17247 let sl = unsafe { std::slice::from_raw_parts_mut((base as *mut f32).add(s), e - s) };
17250 for x in sl {
17251 *x = c * (*x / c).tanh();
17252 }
17253 });
17254 }
17255 _ => {
17256 for x in v.iter_mut() {
17257 *x = c * (*x / c).tanh();
17258 }
17259 }
17260 }
17261}
17262
17263thread_local! {
17264 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
17268 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
17269}
17270
17271fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17273 if gate_topk() > 0
17276 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
17277 {
17278 return out;
17279 }
17280 let prism_body = d.gate_proj.has_prism_contract()
17296 || d.up_proj.has_prism_contract()
17297 || d.down_proj.has_prism_contract();
17298 if !prism_body
17299 && crate::gpu::enabled_here()
17300 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
17301 {
17302 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
17303 crate::gpu::ProbeArm::Gpu
17304 } else {
17305 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
17306 };
17307 match arm {
17308 crate::gpu::ProbeArm::Gpu => {
17309 let t0 = std::time::Instant::now();
17310 if let Some(out) = dense_ffn_gpu(d, x, pool) {
17311 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
17312 return out;
17313 }
17314 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
17318 }
17319 crate::gpu::ProbeArm::CpuTimed => {
17320 let t0 = std::time::Instant::now();
17321 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17322 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
17323 return out;
17324 }
17325 crate::gpu::ProbeArm::Cpu => {
17326 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
17327 }
17328 }
17329 }
17330 dense_ffn_cpu(d, x, pool)
17331}
17332
17333fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
17335 let inter = d.gate_proj.rows();
17336 FFN_SCRATCH.with(|s| {
17337 let mut s = s.borrow_mut();
17338 let [g, u, ..] = &mut *s;
17339 g.resize(inter, 0.0);
17340 if gate_topk() > 0 {
17343 u.resize(inter, 0.0);
17347 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17348 for i in 0..inter {
17349 g[i] = Act::Silu.combine(g[i], 1.0);
17350 }
17351 keep_top_k(g, gate_topk());
17352 for i in 0..inter {
17353 g[i] *= u[i];
17354 }
17355 } else if d.act == Act::Silu && {
17356 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17357 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
17358 } {
17359 } else {
17361 u.resize(inter, 0.0);
17362 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
17364 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17365 for i in 0..inter {
17366 g[i] = d.act.combine(g[i], u[i]);
17367 }
17368 }
17369 FFN_PROBE.with(|pr| {
17377 if let Some(acc) = pr.borrow_mut().as_mut() {
17378 let li = crate::gpu::cur_layer();
17379 if li >= 0 {
17380 if let Some(row) = acc.get_mut(li as usize) {
17381 match probe_topk() {
17382 0 if probe_sq() => {
17383 for (a, &v) in row.iter_mut().zip(g.iter()) {
17384 *a += (v as f64) * (v as f64);
17385 }
17386 }
17387 0 if probe_signed() => {
17388 for (a, &v) in row.iter_mut().zip(g.iter()) {
17389 *a += v as f64;
17390 }
17391 }
17392 0 => {
17393 for (a, &v) in row.iter_mut().zip(g.iter()) {
17394 *a += (v as f64).abs();
17395 }
17396 }
17397 k => {
17398 let n = g.len();
17399 let k = k.min(n);
17400 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17401 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17402 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17403 });
17404 let thr = *kth;
17405 for (a, &v) in row.iter_mut().zip(g.iter()) {
17406 if v.abs() >= thr {
17407 *a += 1.0;
17408 }
17409 }
17410 }
17411 }
17412 }
17413 }
17414 }
17415 });
17416 if oracle_topk() > 0 {
17417 keep_top_k(g, oracle_topk());
17418 }
17419 {
17420 let li = crate::gpu::cur_layer();
17421 if li >= 0 {
17422 adump_row(li as usize, g);
17423 }
17424 }
17425 let mut out = attention::take_buf(d.down_proj.rows());
17426 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
17427 d.down_proj.matvec(g, &mut out, pool);
17428 out
17429 })
17430}
17431
17432pub struct RefitAcc {
17445 pub support: Vec<u32>,
17446 pub gss: Vec<f32>,
17447 pub ya: Vec<f32>,
17448 pub hidden: usize,
17449 pub tokens: u64,
17450 pub buf_g: Vec<f32>,
17456 pub buf_o: Vec<f32>,
17457 pub buf_t: usize,
17458}
17459
17460type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
17464
17465static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
17466 std::sync::OnceLock::new();
17467
17468fn ffn_probe_active() -> bool {
17471 FFN_PROBE.with(|p| p.borrow().is_some())
17472}
17473
17474fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
17475 REFIT
17476 .get_or_init(|| {
17477 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
17478 (
17479 d,
17480 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
17481 )
17482 })
17483 })
17484 .as_ref()
17485}
17486
17487fn refit_accumulate(
17489 li: usize,
17490 g: &[f32],
17491 b: usize,
17492 inter: usize,
17493 out: &[f32],
17494 hidden: usize,
17495 pool: Option<&Pool>,
17496) {
17497 let Some((dir, map)) = refit_dir() else {
17498 return;
17499 };
17500 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17501 let (from, to) = *SPAN.get_or_init(|| {
17502 let g = |k: &str, d: usize| {
17503 std::env::var(k)
17504 .ok()
17505 .and_then(|v| v.parse().ok())
17506 .unwrap_or(d)
17507 };
17508 (
17509 g("CMF_FFN_REFIT_FROM", 0),
17510 g("CMF_FFN_REFIT_TO", usize::MAX),
17511 )
17512 });
17513 if li < from || li > to {
17514 return;
17515 }
17516 let mut guard = map.lock().unwrap();
17517 let (map, shared) = &mut *guard;
17518 let acc = match map.entry(li) {
17519 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
17520 std::collections::hash_map::Entry::Vacant(e) => {
17521 let path = format!("{dir}/support.{li}.u32");
17522 let Ok(bytes) = std::fs::read(&path) else {
17523 eprintln!("refit: no {path} — layer {li} skipped");
17524 return;
17525 };
17526 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
17527 let support: Vec<u32> = bytes[4..4 + n * 4]
17528 .chunks_exact(4)
17529 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17530 .collect();
17531 eprintln!(
17532 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
17533 (n * n + hidden * n) as f64 * 4.0 / 1e6
17534 );
17535 e.insert(RefitAcc {
17536 gss: vec![0.0; n * n],
17537 ya: vec![0.0; hidden * n],
17538 buf_g: Vec::new(),
17539 buf_o: Vec::new(),
17540 buf_t: 0,
17541 support,
17542 hidden,
17543 tokens: 0,
17544 })
17545 }
17546 };
17547 let ns = acc.support.len();
17548 let cap = refit_batch();
17550 if acc.buf_g.is_empty() {
17551 acc.buf_g = vec![0.0; ns * cap];
17552 acc.buf_o = vec![0.0; hidden * cap];
17553 }
17554 let take = b.min(cap - acc.buf_t);
17555 for t in 0..take {
17556 let col = acc.buf_t + t;
17557 for (j, &n) in acc.support.iter().enumerate() {
17558 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17559 }
17560 for h in 0..hidden {
17561 acc.buf_o[h * cap + col] = out[t * hidden + h];
17562 }
17563 }
17564 acc.buf_t += take;
17565 acc.tokens += take as u64;
17566 if acc.buf_t < cap {
17567 return;
17568 }
17569 let bt = acc.buf_t;
17570 acc.buf_t = 0;
17571 let RefitAcc {
17581 gss,
17582 ya,
17583 buf_g,
17584 buf_o,
17585 ..
17586 } = acc;
17587 let need = (ns * ns).max(hidden * ns);
17588 if shared.len() < need {
17589 shared.resize(need, 0.0);
17590 }
17591 let scratch = &mut shared[..];
17592 let _ = bt;
17593 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17594 add_into(gss, &scratch[..ns * ns], pool);
17595 if crate::gpu::gemm_nt_f32_transient(
17596 buf_o,
17597 buf_g,
17598 &mut scratch[..hidden * ns],
17599 hidden,
17600 cap,
17601 ns,
17602 ) {
17603 add_into(ya, &scratch[..hidden * ns], pool);
17604 } else {
17605 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17606 }
17607 } else {
17608 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17609 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17610 }
17611 }
17615
17616fn refit_batch() -> usize {
17618 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17619 *B.get_or_init(|| {
17620 std::env::var("CMF_FFN_REFIT_BATCH")
17621 .ok()
17622 .and_then(|v| v.parse().ok())
17623 .unwrap_or(4096)
17624 })
17625}
17626
17627fn accum_outer_t(
17630 c: &mut [f32],
17631 m: usize,
17632 n: usize,
17633 b: usize,
17634 left: &[f32],
17635 right: &[f32],
17636 pool: Option<&Pool>,
17637) {
17638 let ptr = SendMut(c.as_mut_ptr());
17639 let body = |i: usize| {
17640 let ptr = &ptr;
17641 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
17642 for t in 0..b {
17643 let a = left[i * b + t];
17644 if a == 0.0 {
17645 continue;
17646 }
17647 for (j, o) in row.iter_mut().enumerate() {
17648 *o += a * right[j * b + t];
17649 }
17650 }
17651 };
17652 match pool {
17653 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
17654 for i in s..e {
17655 body(i);
17656 }
17657 }),
17658 _ => {
17659 for i in 0..m {
17660 body(i);
17661 }
17662 }
17663 }
17664}
17665
17666fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
17669 let n = dst.len().min(src.len());
17670 match pool {
17671 Some(p) if n >= 1 << 16 => {
17672 let ptr = SendMut(dst.as_mut_ptr());
17673 let f = |s: usize, e: usize| {
17674 let ptr = &ptr;
17675 for blk in s..e {
17676 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
17677 for i in a..b {
17678 unsafe { *ptr.0.add(i) += src[i] };
17679 }
17680 }
17681 };
17682 p.run_rows(n.div_ceil(4096), &f);
17683 }
17684 _ => {
17685 for (d, v) in dst.iter_mut().zip(&src[..n]) {
17686 *d += *v;
17687 }
17688 }
17689 }
17690}
17691
17692fn accum_outer(
17697 c: &mut [f32],
17698 m: usize,
17699 n: usize,
17700 b: usize,
17701 left: &[f32],
17702 right: &[f32],
17703 pool: Option<&Pool>,
17704) {
17705 const TILE: usize = 32;
17706 let tiles = m.div_ceil(TILE);
17707 let cp = SendMut(c.as_mut_ptr());
17708 let body = |ti: usize| {
17709 let cp = &cp;
17710 let i0 = ti * TILE;
17711 let i1 = (i0 + TILE).min(m);
17712 for t in 0..b {
17713 let r = &right[t * n..t * n + n];
17714 for i in i0..i1 {
17715 let a = left[i * b + t];
17716 if a == 0.0 {
17717 continue;
17718 }
17719 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
17721 for (o, v) in row.iter_mut().zip(r) {
17722 *o += a * *v;
17723 }
17724 }
17725 }
17726 };
17727 match pool {
17728 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
17729 for ti in s..e {
17730 body(ti);
17731 }
17732 }),
17733 _ => {
17734 for ti in 0..tiles {
17735 body(ti);
17736 }
17737 }
17738 }
17739}
17740
17741pub fn refit_flush() -> usize {
17743 let Some((dir, map)) = refit_dir() else {
17744 return 0;
17745 };
17746 let guard = map.lock().unwrap();
17747 let mut n = 0;
17748 for (li, acc) in guard.0.iter() {
17749 let w = |name: &str, v: &[f32]| {
17752 let path = format!("{dir}/{name}.{li}.f32");
17753 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
17754 match std::fs::write(&path, &bytes) {
17755 Ok(()) => {}
17756 Err(e) => eprintln!(
17757 "refit: FAILED to write {path} ({} MB): {e}",
17758 bytes.len() / 1_000_000
17759 ),
17760 }
17761 };
17762 w("gss", &acc.gss);
17763 w("ya", &acc.ya);
17764 println!(
17765 "refit L{li}: {} support, {} tokens, hidden {}",
17766 acc.support.len(),
17767 acc.tokens,
17768 acc.hidden
17769 );
17770 n += 1;
17771 }
17772 n
17773}
17774
17775fn adump_row(li: usize, g: &[f32]) {
17780 use std::io::Write as _;
17781 static FILES: std::sync::OnceLock<
17782 Option<(
17783 String,
17784 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
17785 )>,
17786 > = std::sync::OnceLock::new();
17787 let Some((prefix, map)) = FILES
17788 .get_or_init(|| {
17789 std::env::var("CMF_FFN_ADUMP")
17790 .ok()
17791 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
17792 })
17793 .as_ref()
17794 else {
17795 return;
17796 };
17797 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17800 let (from, to) = *SPAN.get_or_init(|| {
17801 let g = |k: &str, d: usize| {
17802 std::env::var(k)
17803 .ok()
17804 .and_then(|v| v.parse().ok())
17805 .unwrap_or(d)
17806 };
17807 (
17808 g("CMF_FFN_ADUMP_FROM", 0),
17809 g("CMF_FFN_ADUMP_TO", usize::MAX),
17810 )
17811 });
17812 if li < from || li > to {
17813 return;
17814 }
17815 let mut map = map.lock().unwrap();
17816 let f = map.entry(li).or_insert_with(|| {
17817 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
17818 });
17819 let mut bytes = Vec::with_capacity(g.len() * 2);
17820 for v in g {
17821 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
17822 }
17823 let _ = f.write_all(&bytes);
17824}
17825
17826fn oracle_topk() -> usize {
17832 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17833 *K.get_or_init(|| {
17834 std::env::var("CMF_FFN_ORACLE_TOPK")
17835 .ok()
17836 .and_then(|v| v.parse().ok())
17837 .unwrap_or(0)
17838 })
17839}
17840
17841fn gate_topk() -> usize {
17847 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17848 *K.get_or_init(|| {
17849 std::env::var("CMF_FFN_GATE_TOPK")
17850 .ok()
17851 .and_then(|v| v.parse().ok())
17852 .unwrap_or(0)
17853 })
17854}
17855
17856fn gate_block() -> usize {
17863 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17864 *B.get_or_init(|| {
17865 std::env::var("CMF_FFN_GATE_BLOCK")
17866 .ok()
17867 .and_then(|v| v.parse().ok())
17868 .unwrap_or(1)
17869 })
17870}
17871
17872fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
17874 let n = g.len();
17875 let nb = n.div_ceil(block);
17876 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
17877 if kb >= nb {
17878 return;
17879 }
17880 let mut score: Vec<f32> = (0..nb)
17881 .map(|b| {
17882 g[b * block..((b + 1) * block).min(n)]
17883 .iter()
17884 .map(|v| v * v)
17885 .sum::<f32>()
17886 })
17887 .collect();
17888 let mut ord = score.clone();
17889 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
17890 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17891 });
17892 let thr = *kth;
17893 for b in 0..nb {
17894 if score[b] < thr {
17895 g[b * block..((b + 1) * block).min(n)].fill(0.0);
17896 }
17897 }
17898 score.clear();
17899}
17900
17901fn keep_top_k(g: &mut [f32], k: usize) {
17903 if gate_block() > 1 {
17904 return keep_top_blocks(g, k, gate_block());
17905 }
17906 let n = g.len();
17907 if k == 0 || k >= n {
17908 return;
17909 }
17910 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17911 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17912 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17913 });
17914 let thr = *kth;
17915 for v in g.iter_mut() {
17916 if v.abs() < thr {
17917 *v = 0.0;
17918 }
17919 }
17920}
17921
17922fn probe_sq() -> bool {
17926 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17927 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
17928}
17929
17930fn probe_signed() -> bool {
17934 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17935 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
17936}
17937
17938fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
17946 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
17947 M.get_or_init(|| {
17948 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
17949 let b = std::fs::read(&p).ok()?;
17950 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
17951 let vals: Vec<f32> = b[8..]
17952 .chunks_exact(4)
17953 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17954 .collect();
17955 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
17956 Some((inter, vals))
17957 })
17958 .as_ref()
17959}
17960
17961fn probe_topk() -> usize {
17964 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17965 *K.get_or_init(|| {
17966 std::env::var("CMF_FFN_PROBE_TOPK")
17967 .ok()
17968 .and_then(|v| v.parse().ok())
17969 .unwrap_or(0)
17970 })
17971}
17972
17973thread_local! {
17974 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
17977 const { std::cell::RefCell::new(None) };
17978}
17979
17980fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
17993 if d.gate_proj.has_prism_contract()
17998 || d.up_proj.has_prism_contract()
17999 || d.down_proj.has_prism_contract()
18000 {
18001 return None;
18002 }
18003 let dt = d.down_t.as_ref()?;
18004 let inter = d.gate_proj.rows();
18005 let hidden = dt.cols();
18006 if k == 0 || k >= inter || d.act != Act::Silu {
18007 return None;
18008 }
18009 DYN_SCRATCH.with(|sc| {
18010 let mut sc = sc.borrow_mut();
18011 let DynScratch {
18012 g,
18013 mag,
18014 live,
18015 parts,
18016 } = &mut *sc;
18017 g.resize(inter, 0.0);
18018 d.gate_proj.matvec(x, g, pool);
18019 for v in g.iter_mut() {
18020 *v = inference::silu(*v);
18021 }
18022 mag.clear();
18025 mag.extend(g.iter().map(|v| v.abs()));
18026 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
18027 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
18028 });
18029 let thr = *kth;
18030 live.clear();
18031 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
18032 let mut out = vec![0.0f32; hidden];
18033 match pool {
18034 Some(p) if live.len() >= 64 => {
18035 let nw = p.n_workers() + 1;
18036 parts.clear();
18037 parts.resize(nw * hidden, 0.0);
18038 let ptr = SendMut(parts.as_mut_ptr());
18039 let n = live.len();
18040 let live_ref: &[u32] = live;
18041 let g_ref: &[f32] = g;
18042 p.run(&|w, workers| {
18043 let chunk = n.div_ceil(workers);
18044 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
18045 if s >= e {
18046 return;
18047 }
18048 WORKER_SCRATCH.with(|ws| {
18049 let mut ws = ws.borrow_mut();
18050 let [scratch, acc] = &mut *ws;
18051 scratch.resize(hidden.max(x.len()), 0.0);
18052 acc.clear();
18053 acc.resize(hidden, 0.0);
18054 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
18055 if let Some(&nx) = live_ref[s..e].get(o + 1) {
18058 d.up_proj.prefetch_row(nx as usize);
18059 dt.prefetch_row(nx as usize);
18060 }
18061 let idx = nrm as usize;
18062 let up = d.up_proj.row_dot(idx, x, scratch);
18063 let a = g_ref[idx] * up;
18064 if a != 0.0 {
18065 dt.add_row_scaled(idx, a, acc, scratch);
18066 }
18067 }
18068 for (j, v) in acc.iter().enumerate() {
18069 unsafe { *ptr.at(w * hidden + j) = *v };
18070 }
18071 });
18072 });
18073 for w in 0..nw {
18074 for (j, o) in out.iter_mut().enumerate() {
18075 *o += parts[w * hidden + j];
18076 }
18077 }
18078 }
18079 _ => {
18080 WORKER_SCRATCH.with(|ws| {
18081 let mut ws = ws.borrow_mut();
18082 let [scratch, _acc] = &mut *ws;
18083 scratch.resize(hidden.max(x.len()), 0.0);
18084 for &nrm in live.iter() {
18085 let idx = nrm as usize;
18086 let up = d.up_proj.row_dot(idx, x, scratch);
18087 let a = g[idx] * up;
18088 if a != 0.0 {
18089 dt.add_row_scaled(idx, a, &mut out, scratch);
18090 }
18091 }
18092 });
18093 }
18094 }
18095 Some(out)
18096 })
18097}
18098
18099struct DynScratch {
18102 g: Vec<f32>,
18103 mag: Vec<f32>,
18104 live: Vec<u32>,
18105 parts: Vec<f32>,
18106}
18107
18108thread_local! {
18109 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
18110 std::cell::RefCell::new(DynScratch {
18111 g: Vec::new(),
18112 mag: Vec::new(),
18113 live: Vec::new(),
18114 parts: Vec::new(),
18115 })
18116 };
18117 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
18119 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
18120}
18121
18122fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
18127 let inter = d.gate_proj.rows();
18128 FFN_SCRATCH.with(|s| {
18129 let mut s = s.borrow_mut();
18130 let [g, u, ..] = &mut *s;
18131 g.resize(inter, 0.0);
18132 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
18133 } else {
18135 u.resize(inter, 0.0);
18136 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
18137 for i in 0..inter {
18138 g[i] = d.act.combine(g[i], u[i]);
18139 }
18140 }
18141 zero_masked_cols(g, 1, inter, mask_row);
18142 let mut out = attention::take_buf(d.down_proj.rows());
18143 d.down_proj.matvec(g, &mut out, pool);
18144 out
18145 })
18146}
18147
18148fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
18154 if d.gate_proj.has_prism_contract()
18155 || d.up_proj.has_prism_contract()
18156 || d.down_proj.has_prism_contract()
18157 {
18158 return None;
18159 }
18160 d.act.graph_act()?;
18163 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
18166 return None;
18167 }
18168 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
18169 let mut model_ref = None;
18170 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
18171 let model = model_ref?;
18172 let hidden = jobs[0].down.1;
18173 let mut out = attention::take_buf(hidden);
18174 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18175 Some(out)
18176 } else {
18177 let mut out = out;
18178 attention::recycle_buf(&mut out);
18179 None
18180 }
18181}
18182
18183#[allow(clippy::type_complexity)]
18188#[allow(clippy::type_complexity)]
18189pub(crate) fn moe_parts(
18190 t: &QTensor,
18191) -> Option<(
18192 &std::sync::Arc<cortiq_core::CmfModel>,
18193 usize,
18194 usize,
18195 usize,
18196 &[f32],
18197 &[f32],
18198 bool,
18199 bool,
18200 bool,
18201)> {
18202 match t {
18203 QTensor::Mapped {
18204 model,
18205 idx,
18206 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
18207 rows,
18208 cols,
18209 row_scale,
18210 col_field,
18211 ..
18212 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
18213 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
18214 )),
18215 QTensor::Mapped {
18217 model,
18218 idx,
18219 dtype: cortiq_core::TensorDtype::Q1,
18220 rows,
18221 cols,
18222 ..
18223 } => Some((
18224 model,
18225 *idx,
18226 *rows,
18227 *cols,
18228 &[][..],
18229 &[][..],
18230 true,
18231 false,
18232 false,
18233 )),
18234 QTensor::Mapped {
18236 model,
18237 idx,
18238 dtype: cortiq_core::TensorDtype::Q4Tiled,
18239 rows,
18240 cols,
18241 ..
18242 } => Some((
18243 model,
18244 *idx,
18245 *rows,
18246 *cols,
18247 &[][..],
18248 &[][..],
18249 false,
18250 true,
18251 false,
18252 )),
18253 QTensor::Mapped {
18255 model,
18256 idx,
18257 dtype: cortiq_core::TensorDtype::Q4TiledP,
18258 rows,
18259 cols,
18260 ..
18261 } => Some((
18262 model,
18263 *idx,
18264 *rows,
18265 *cols,
18266 &[][..],
18267 &[][..],
18268 false,
18269 true,
18270 false,
18271 )),
18272 QTensor::Mapped {
18276 model,
18277 idx,
18278 dtype: cortiq_core::TensorDtype::Q2TiledP,
18279 rows,
18280 cols,
18281 ..
18282 } => Some((
18283 model,
18284 *idx,
18285 *rows,
18286 *cols,
18287 &[][..],
18288 &[][..],
18289 false,
18290 true,
18291 true,
18292 )),
18293 _ => None,
18294 }
18295}
18296
18297#[cfg(target_os = "macos")]
18304#[allow(clippy::type_complexity)]
18305fn chunk_moe_parts(m: &MoeFfn, hidden: usize) -> Option<(&[f32], Vec<(usize, usize, usize)>, bool)> {
18306 if m.shared.is_some()
18307 || m.per_expert_scale.is_some()
18308 || m.resonance.is_some()
18309 || m.route_tau.is_some()
18310 || m.router_input_norm
18311 || m.experts.is_empty()
18312 || m.top_k == 0
18313 || std::env::var("CMF_RMS_TRACE").is_ok()
18314 || std::env::var("CMF_ACT_DUMP").is_ok()
18315 || std::env::var("CMF_METAL_MOE_PREFILL").as_deref() == Ok("0")
18316 {
18317 return None;
18318 }
18319 let (rf, rr, rc) = m.router.f32_parts()?;
18320 if rr != m.experts.len() || rc != hidden {
18321 return None;
18322 }
18323 let inter = m.experts[0].gate_proj.rows();
18324 let q8 = crate::gpu_metal::moe_q8::enabled()
18327 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18328 let experts = m
18329 .experts
18330 .iter()
18331 .map(|e| {
18332 if e.act != Act::Silu
18333 || !e.segs.is_empty()
18334 || e.gate_proj.rows() != inter
18335 || e.up_proj.rows() != inter
18336 || e.down_proj.cols() != inter
18337 || e.gate_proj.cols() != hidden
18338 || e.down_proj.rows() != hidden
18339 {
18340 return None;
18341 }
18342 if q8 {
18343 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18344 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18345 }
18346 Some((
18347 e.gate_proj.mapped_q4tp()?.1,
18348 e.up_proj.mapped_q4tp()?.1,
18349 e.down_proj.mapped_q4tp()?.1,
18350 ))
18351 })
18352 .collect::<Option<Vec<_>>>()?;
18353 Some((rf, experts, q8))
18354}
18355
18356#[cfg(target_os = "macos")]
18364fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
18365 if m.router_input_norm
18366 || m.route_tau.is_some()
18367 || m.mask.is_some()
18368 || m.per_expert_scale.is_some()
18369 || m.experts.is_empty()
18370 || m.top_k == 0
18371 || m.resonance.is_some()
18372 {
18373 return None;
18374 }
18375 let (sh, sg) = match &m.shared {
18380 Some((sh, sg)) => (Some(sh), sg.as_ref()),
18381 None if !m.router_sigmoid && m.expert_bias.is_none() => (None, None),
18382 None => return None,
18383 };
18384 let (rf, rr, rc) = m.router.f32_parts()?;
18385 if rr != m.experts.len() || rc != hidden {
18386 return None;
18387 }
18388 let shared_gated = sg.is_some();
18389 let sf = match sg {
18390 Some(sg) => {
18391 let (sf, sr, sc) = sg.f32_parts()?;
18392 if sr * sc != hidden {
18393 return None;
18394 }
18395 sf
18396 }
18397 None => &rf[..hidden],
18400 };
18401 if let Some(b) = &m.expert_bias {
18402 if b.len() != m.experts.len() {
18403 return None;
18404 }
18405 }
18406 let inter = m.experts[0].gate_proj.rows();
18407 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
18410 let q8 = crate::gpu_metal::moe_q8::enabled()
18414 && m.shared.is_none()
18415 && m.experts[0].gate_proj.q8_2f_parts().is_some();
18416 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
18417 if e.act != Act::Silu
18418 || e.gate_proj.rows() != inter
18419 || e.gate_proj.cols() != hidden
18420 || e.up_proj.rows() != inter
18421 || e.up_proj.cols() != hidden
18422 || e.down_proj.rows() != hidden
18423 || e.down_proj.cols() != inter
18424 {
18425 return None;
18426 }
18427 if q8 {
18428 let q8i = |t: &QTensor| t.q8_2f_parts().map(|p| p.0);
18429 return Some((q8i(&e.gate_proj)?, q8i(&e.up_proj)?, q8i(&e.down_proj)?));
18430 }
18431 let pick = |t: &QTensor| -> Option<usize> {
18432 if gu_q2 {
18433 t.mapped_q2tp().map(|(_, i)| i)
18434 } else {
18435 t.mapped_q4tp().map(|(_, i)| i)
18436 }
18437 };
18438 Some((
18439 pick(&e.gate_proj)?,
18440 pick(&e.up_proj)?,
18441 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
18442 ))
18443 };
18444 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
18445 let shared = match sh {
18446 Some(sh) => Some(trio(sh)?),
18447 None => None,
18448 };
18449 Some(crate::gpu::GpuMoe {
18450 router: rf,
18451 sgate: sf,
18452 experts,
18453 shared,
18454 n_exp: m.experts.len(),
18455 top_k: m.top_k,
18456 inter,
18457 norm_topk: m.norm_topk_prob,
18458 route_scale: m.routed_scaling,
18459 gu_q2,
18460 q8,
18461 sigmoid: m.router_sigmoid,
18462 bias: m.expert_bias.as_deref(),
18463 shared_gated,
18464 })
18465}
18466
18467fn forced_ids_at(step: usize) -> Option<u32> {
18473 static IDS: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
18474 IDS.get_or_init(|| {
18475 std::env::var("CMF_FORCE_IDS")
18476 .ok()
18477 .and_then(|p| std::fs::read_to_string(p).ok())
18478 .map(|s| s.split_whitespace().filter_map(|t| t.parse().ok()).collect())
18479 .unwrap_or_default()
18480 })
18481 .get(step)
18482 .copied()
18483}
18484
18485pub(crate) fn moe_push_job_parts<'a>(
18489 gate: &'a QTensor,
18490 up: &'a QTensor,
18491 down: &'a QTensor,
18492 x: &[f32],
18493 w: f32,
18494 swiglu_limit: f32,
18495 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18496 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18497) -> Option<()> {
18498 use crate::qtensor::prescale;
18499 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
18500 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
18501 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
18502 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18503 return None; }
18505 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
18508 return None;
18509 }
18510 if !gq2 && dq2 {
18511 return None;
18512 }
18513 model_ref.get_or_insert_with(|| gm.clone());
18514 let dt = |cf: &[f32]| {
18515 if cf.is_empty() {
18516 cortiq_core::TensorDtype::Q8Row
18517 } else {
18518 cortiq_core::TensorDtype::Q8_2f
18519 }
18520 };
18521 jobs.push(crate::gpu::MoeJob {
18522 gate: (gi, gr, gc, grs),
18523 up: (ui, ur, uc, urs),
18524 down: (di, dr, dc, drs),
18525 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
18526 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
18527 down_col: dcf,
18528 w,
18529 q1: gq1,
18530 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
18531 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
18532 gu_q2: gq2,
18533 swiglu_limit,
18534 act: 0,
18535 });
18536 Some(())
18537}
18538
18539fn moe_push_job<'a>(
18541 d: &'a DenseFfn,
18542 x: &[f32],
18543 w: f32,
18544 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
18545 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
18546) -> Option<()> {
18547 use crate::qtensor::prescale;
18548 let act = d.act.graph_act()?.code();
18551 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
18552 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
18553 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
18554 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
18555 return None; }
18557 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
18558 return None;
18559 }
18560 if !gq2 && dq2 {
18561 return None;
18562 }
18563 model_ref.get_or_insert_with(|| gm.clone());
18564 let gdt = if gcf.is_empty() {
18565 cortiq_core::TensorDtype::Q8Row
18566 } else {
18567 cortiq_core::TensorDtype::Q8_2f
18568 };
18569 let udt = if ucf.is_empty() {
18570 cortiq_core::TensorDtype::Q8Row
18571 } else {
18572 cortiq_core::TensorDtype::Q8_2f
18573 };
18574 jobs.push(crate::gpu::MoeJob {
18575 gate: (gi, gr, gc, grs),
18576 up: (ui, ur, uc, urs),
18577 down: (di, dr, dc, drs),
18578 xs_gate: prescale(x, gcf, gdt).into_owned(),
18579 xs_up: prescale(x, ucf, udt).into_owned(),
18580 down_col: dcf,
18581 w,
18582 q1: gq1,
18583 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
18584 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
18585 gu_q2: gq2,
18586 swiglu_limit: 0.0,
18587 act,
18588 });
18589 Some(())
18590}
18591
18592fn sparse_ffn_quant(
18599 d: &DenseFfn,
18600 x: &[f32],
18601 active: &[u16],
18602 hidden: usize,
18603 pool: Option<&Pool>,
18604) -> Vec<f32> {
18605 let n = active.len();
18606 let inter = d.gate_proj.rows();
18607 let mut act = vec![0.0f32; n];
18608 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
18611 let compute = |ai: usize| -> f32 {
18612 let idx = active[ai] as usize;
18613 if idx >= inter {
18614 return 0.0; }
18616 let mut s = if need_scratch {
18617 vec![0.0f32; hidden]
18618 } else {
18619 Vec::new()
18620 };
18621 let gate = d.gate_proj.row_dot(idx, x, &mut s);
18622 let up = d.up_proj.row_dot(idx, x, &mut s);
18623 d.act.combine(gate, up)
18624 };
18625 match pool {
18626 Some(p) if n >= 256 => {
18627 let ptr = SendMut(act.as_mut_ptr());
18628 p.run(&|widx, nw| {
18629 let chunk = n.div_ceil(nw);
18630 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
18631 for ai in s..e {
18632 unsafe { *ptr.at(ai) = compute(ai) };
18633 }
18634 });
18635 }
18636 _ => {
18637 for (ai, a) in act.iter_mut().enumerate() {
18638 *a = compute(ai);
18639 }
18640 }
18641 }
18642 let mut out = vec![0.0f32; hidden];
18644 for (ai, &idx) in active.iter().enumerate() {
18645 let w = act[ai];
18646 if w.abs() >= 1e-12 && (idx as usize) < inter {
18647 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
18648 }
18649 }
18650 out
18651}
18652
18653#[doc(hidden)]
18655pub fn sparse_ffn_quant_for_test(
18656 d: &DenseFfn,
18657 x: &[f32],
18658 active: &[u16],
18659 hidden: usize,
18660) -> Vec<f32> {
18661 sparse_ffn_quant(d, x, active, hidden, None)
18662}
18663
18664fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
18668 let deq = |t: &QTensor| -> Vec<f32> {
18669 let (rows, cols) = (t.rows(), t.cols());
18670 let mut out = vec![0.0f32; rows * cols];
18671 for r in 0..rows {
18672 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
18673 }
18674 out
18675 };
18676 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
18677}
18678
18679struct SendMut(*mut f32);
18681unsafe impl Send for SendMut {}
18682unsafe impl Sync for SendMut {}
18683impl SendMut {
18684 #[inline]
18685 #[allow(clippy::mut_from_ref)]
18688 unsafe fn at(&self, i: usize) -> &mut f32 {
18689 unsafe { &mut *self.0.add(i) }
18690 }
18691}
18692
18693pub(crate) fn moe_route(
18706 logits: &[f32],
18707 m: &MoeFfn,
18708 allowed: Option<&[bool]>,
18709) -> (Vec<usize>, Vec<f32>, f32) {
18710 moe_route_with_eps(logits, m, allowed, 1e-6)
18711}
18712
18713pub(crate) fn moe_route_with_eps(
18722 logits: &[f32],
18723 m: &MoeFfn,
18724 allowed: Option<&[bool]>,
18725 sigmoid_denom_eps: f32,
18726) -> (Vec<usize>, Vec<f32>, f32) {
18727 let ne = logits.len();
18728 let admit = |e: usize| {
18734 m.mask.as_ref().is_none_or(|mk| mk[e])
18735 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
18736 };
18737 if m.resonance.is_some() && m.top_k == 1 {
18750 let mut best: Option<usize> = None;
18751 for e in (0..ne).filter(|&e| admit(e)) {
18752 let l = logits[e];
18753 if l == f32::NEG_INFINITY || l.is_nan() {
18754 continue;
18755 }
18756 if best.is_none_or(|b| l > logits[b]) {
18757 best = Some(e);
18758 }
18759 }
18760 if let Some(b) = best {
18761 let mut p = vec![0.0f32; ne];
18762 p[b] = 1.0;
18763 return (vec![b], p, 1.0 / m.routed_scaling);
18764 }
18765 }
18766 let p: Vec<f32> = if m.router_sigmoid {
18772 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
18773 } else {
18774 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18775 if mx == f32::NEG_INFINITY {
18776 vec![1.0 / ne.max(1) as f32; ne]
18777 } else {
18778 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
18779 let s: f32 = e.iter().sum();
18780 for v in &mut e {
18781 *v /= s;
18782 }
18783 e
18784 }
18785 };
18786 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
18787 match &m.expert_bias {
18789 Some(b) => idx.sort_unstable_by(|&x, &y| {
18790 (p[y] + b[y])
18791 .partial_cmp(&(p[x] + b[x]))
18792 .unwrap()
18793 .then(x.cmp(&y))
18794 }),
18795 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
18796 }
18797 idx.truncate(m.top_k);
18798 if let Some(tau) = m.route_tau {
18802 let total: f32 = idx.iter().map(|&e| p[e]).sum();
18803 if total > 0.0 {
18804 let mut acc = 0.0f32;
18805 let mut keep = idx.len();
18806 for (i, &e) in idx.iter().enumerate() {
18807 acc += p[e];
18808 if acc >= tau * total {
18809 keep = i + 1;
18810 break;
18811 }
18812 }
18813 idx.truncate(keep);
18814 }
18815 }
18816 let wsum: f32 = if m.norm_topk_prob {
18817 let s: f32 = idx.iter().map(|&e| p[e]).sum();
18818 (if m.router_sigmoid {
18822 s + sigmoid_denom_eps
18823 } else {
18824 s
18825 }) / m.routed_scaling
18826 } else {
18827 1.0 / m.routed_scaling
18828 };
18829 (idx, p, wsum)
18830}
18831
18832fn moe_trace(idx: &[usize]) {
18834 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
18835}
18836
18837pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
18840 use std::io::Write;
18841 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
18842 std::sync::OnceLock::new();
18843 let Some(f) = F.get_or_init(|| {
18844 let p = std::env::var("CMF_MOE_TRACE").ok()?;
18845 Some(std::sync::Mutex::new(
18846 std::fs::OpenOptions::new()
18847 .create(true)
18848 .append(true)
18849 .open(p)
18850 .ok()?,
18851 ))
18852 }) else {
18853 return;
18854 };
18855 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
18856 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
18857}
18858
18859pub(crate) fn moe_ffn(
18862 m: &MoeFfn,
18863 x: &[f32],
18864 pool: Option<&Pool>,
18865 allowed: Option<&[bool]>,
18866) -> Vec<f32> {
18867 let r = moe_ffn_route(m, x, pool, allowed);
18868 moe_ffn_experts(m, x, &r, pool)
18869}
18870
18871pub(crate) struct MoeRoute {
18875 pub idx: Vec<usize>,
18876 pub p: Vec<f32>,
18877 pub wsum: f32,
18878 pub logits: Vec<f32>,
18879}
18880
18881pub(crate) fn moe_ffn_route(
18887 m: &MoeFfn,
18888 x: &[f32],
18889 pool: Option<&Pool>,
18890 allowed: Option<&[bool]>,
18891) -> MoeRoute {
18892 accumulate_act(m, x, 1);
18893 let ne = m.experts.len();
18894 let mut logits = vec![0.0f32; ne];
18895 match &m.resonance {
18896 Some(r) => r.scores(x, &mut logits),
18897 None => m.router.matvec(x, &mut logits, pool),
18898 }
18899 let (idx, p, wsum) = moe_route(&logits, m, allowed);
18900 {
18901 let mut st = m.stats.borrow_mut();
18902 if st.len() < ne {
18903 st.resize(ne, 0);
18904 }
18905 for &e in &idx {
18906 st[e] += 1;
18907 }
18908 }
18909 moe_trace(&idx);
18915 MoeRoute {
18916 idx,
18917 p,
18918 wsum,
18919 logits,
18920 }
18921}
18922
18923pub(crate) fn moe_ffn_experts(
18926 m: &MoeFfn,
18927 x: &[f32],
18928 r: &MoeRoute,
18929 pool: Option<&Pool>,
18930) -> Vec<f32> {
18931 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
18932 if crate::gpu::enabled_here() {
18937 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
18938 crate::gpu::ProbeArm::Gpu => {
18939 let t0 = std::time::Instant::now();
18940 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
18941 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18942 return out;
18943 }
18944 }
18945 crate::gpu::ProbeArm::CpuTimed => {
18946 let t0 = std::time::Instant::now();
18947 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18948 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18949 return out;
18950 }
18951 crate::gpu::ProbeArm::Cpu => {
18952 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18953 }
18954 }
18955 }
18956 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
18957}
18958
18959fn moe_ffn_banked(
18963 slot: &mut crate::mimo_moe::Slot,
18964 li: usize,
18965 m: &MoeFfn,
18966 x: &[f32],
18967 pool: Option<&Pool>,
18968) -> Vec<f32> {
18969 let t0 = std::time::Instant::now();
18970 let r = moe_ffn_route(m, x, pool, None);
18971 slot.note_route(t0.elapsed().as_nanos() as u64);
18972 match slot.forward(li, m, x, &r, pool) {
18973 Some(out) => out,
18974 None => crate::qtensor::float_activations_scope(|| {
18975 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
18976 }),
18977 }
18978}
18979
18980fn moe_ffn_banked_rows(
18982 slot: &mut crate::mimo_moe::Slot,
18983 li: usize,
18984 m: &MoeFfn,
18985 xs: &[f32],
18986 b: usize,
18987 hidden: usize,
18988 pool: Option<&Pool>,
18989) -> Vec<f32> {
18990 let t0 = std::time::Instant::now();
18991 let routes: Vec<_> = xs
18992 .chunks_exact(hidden)
18993 .map(|x| moe_ffn_route(m, x, pool, None))
18994 .collect();
18995 slot.note_route(t0.elapsed().as_nanos() as u64);
18996 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
18997 return out;
18998 }
18999 let mut out = Vec::with_capacity(b * hidden);
19000 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
19001 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
19002 crate::qtensor::float_activations_scope(|| {
19004 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
19005 })
19006 });
19007 out.extend(row);
19008 }
19009 out
19010}
19011
19012fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
19017 use std::sync::atomic::{AtomicBool, Ordering};
19018 if built {
19019 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
19020 if total_layers > 0 && layers_run < total_layers {
19021 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
19022 } else {
19023 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
19024 }
19025 } else {
19026 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
19027 }
19028 static SAID: AtomicBool = AtomicBool::new(false);
19029 if !SAID.swap(true, Ordering::Relaxed) {
19030 if built {
19031 tracing::info!("wgpu whole-token graph: ACTIVE");
19032 } else {
19033 tracing::warn!("wgpu whole-token graph refused — per-op path");
19034 }
19035 }
19036}
19037
19038pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19042pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19043pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19047pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
19049
19050pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
19054 std::sync::atomic::AtomicU64::new(0);
19055pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
19056 std::sync::atomic::AtomicU64::new(0);
19057pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
19058 std::sync::atomic::AtomicU64::new(0);
19059pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
19060 std::sync::atomic::AtomicU64::new(0);
19061pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
19062 std::sync::atomic::AtomicU64::new(0);
19063pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
19067 std::sync::atomic::AtomicU64::new(0);
19068pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
19069 std::sync::atomic::AtomicU64::new(0);
19070pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
19071 std::sync::atomic::AtomicU64::new(0);
19072pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
19073 std::sync::atomic::AtomicU64::new(0);
19074
19075fn moe_batch_enabled() -> bool {
19078 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
19079 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
19080}
19081
19082fn moe_ffn_cpu_batched(
19088 m: &MoeFfn,
19089 x: &[f32],
19090 idx: &[usize],
19091 p: &[f32],
19092 wsum: f32,
19093 pool: Option<&Pool>,
19094) -> Option<Vec<f32>> {
19095 if idx.is_empty() || !moe_batch_enabled() {
19096 return None;
19097 }
19098 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
19102 return None;
19103 }
19104 let n = idx.len() + usize::from(m.shared.is_some());
19105 let mut pairs = Vec::with_capacity(n);
19106 let mut downs = Vec::with_capacity(n);
19107 let mut ws = Vec::with_capacity(n);
19108 for &e in idx {
19109 let d = &m.experts[e];
19110 if d.act != Act::Silu {
19111 return None;
19112 }
19113 pairs.push((&d.gate_proj, &d.up_proj));
19114 downs.push(&d.down_proj);
19115 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
19116 }
19117 if let Some((se, gate)) = &m.shared {
19120 if se.act != Act::Silu {
19121 return None;
19122 }
19123 let g = gate.as_ref().map_or(1.0, |gate| {
19124 let mut gl = [0.0f32; 1];
19125 gate.matvec(x, &mut gl, pool);
19126 1.0 / (1.0 + (-gl[0]).exp())
19127 });
19128 pairs.push((&se.gate_proj, &se.up_proj));
19129 downs.push(&se.down_proj);
19130 ws.push(g);
19131 }
19132 let inter = pairs[0].0.rows();
19133 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
19134 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
19135 return None;
19136 }
19137 let mut out = attention::take_buf(x.len());
19138 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
19139 attention::recycle_buf(&mut out);
19140 return None;
19141 }
19142 Some(out)
19143}
19144
19145pub(crate) fn moe_cold_experts_cpu(
19151 experts: &[(&DenseFfn, f32)],
19152 x: &[f32],
19153 pool: Option<&Pool>,
19154) -> Vec<f32> {
19155 let mut out = attention::take_buf(x.len());
19156 if experts.is_empty() {
19157 return out;
19158 }
19159 let pairs: Vec<_> = experts
19160 .iter()
19161 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
19162 .collect();
19163 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
19164 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
19165 let inter = experts[0].0.gate_proj.rows();
19166 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
19167 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
19168 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
19169 {
19170 return out;
19171 }
19172 out.fill(0.0);
19173 for &(expert, weight) in experts {
19174 let mut one = dense_ffn(expert, x, pool);
19175 for (o, v) in out.iter_mut().zip(&one) {
19176 *o += weight * v;
19177 }
19178 attention::recycle_buf(&mut one);
19179 }
19180 out
19181}
19182
19183pub(crate) fn moe_cold_experts_rows_cpu(
19187 jobs: &[Vec<(&DenseFfn, f32)>],
19188 xs: &[f32],
19189 hidden: usize,
19190 pool: Option<&Pool>,
19191) -> Vec<f32> {
19192 let mut out = vec![0.0; xs.len()];
19193 let mut experts: Vec<&DenseFfn> = Vec::new();
19194 let mut groups: Vec<Vec<usize>> = Vec::new();
19195 let mut terms = vec![Vec::new(); jobs.len()];
19196 for (r, row) in jobs.iter().enumerate() {
19197 for &(e, w) in row {
19198 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
19199 Some(g) => g,
19200 None => {
19201 experts.push(e);
19202 groups.push(Vec::new());
19203 groups.len() - 1
19204 }
19205 };
19206 terms[r].push((g, groups[g].len(), w));
19207 groups[g].push(r);
19208 }
19209 }
19210 if experts.is_empty() {
19211 return out;
19212 }
19213 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
19214 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
19215 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
19216 let count: usize = lens.iter().sum();
19217 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
19218 let mut ds = vec![vec![0.0; hidden]; count];
19219 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
19220 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
19221 {
19222 let mut offset = 0;
19223 let offsets: Vec<_> = lens
19224 .iter()
19225 .map(|&n| {
19226 let start = offset;
19227 offset += n;
19228 start
19229 })
19230 .collect();
19231 for (r, terms) in terms.iter().enumerate() {
19232 for &(g, slot, w) in terms {
19233 for (o, &v) in out[r * hidden..(r + 1) * hidden]
19234 .iter_mut()
19235 .zip(&ds[offsets[g] + slot])
19236 {
19237 *o += w * v;
19238 }
19239 }
19240 }
19241 } else {
19242 for (r, jobs) in jobs.iter().enumerate() {
19243 if !jobs.is_empty() {
19244 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
19245 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
19246 attention::recycle_buf(&mut row);
19247 }
19248 }
19249 }
19250 out
19251}
19252
19253fn moe_ffn_cpu(
19255 m: &MoeFfn,
19256 x: &[f32],
19257 idx: &[usize],
19258 p: &[f32],
19259 wsum: f32,
19260 pool: Option<&Pool>,
19261) -> Vec<f32> {
19262 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
19263 return out;
19264 }
19265 let mut out = attention::take_buf(x.len());
19266 for &e in idx {
19267 let mut eo = dense_ffn(&m.experts[e], x, pool);
19268 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
19269 for i in 0..out.len() {
19270 out[i] += w * eo[i];
19271 }
19272 attention::recycle_buf(&mut eo);
19273 }
19274 if let Some((se, gate)) = &m.shared {
19275 let mut so = dense_ffn(se, x, pool);
19276 let g = gate.as_ref().map_or(1.0, |gate| {
19277 let mut gl = [0.0f32; 1];
19278 gate.matvec(x, &mut gl, pool);
19279 1.0 / (1.0 + (-gl[0]).exp())
19280 });
19281 for i in 0..out.len() {
19282 out[i] += g * so[i];
19283 }
19284 attention::recycle_buf(&mut so);
19285 }
19286 out
19287}
19288
19289#[allow(clippy::too_many_arguments)]
19297pub(crate) fn mla_attention(
19298 w: &MlaWeights,
19299 normed: &[f32],
19300 cache: &mut crate::kv_cache::LayerKvCache,
19301 position: usize,
19302 inv_freq: &[f32],
19303 rope_scale: f32,
19304 eps: f64,
19305 pool: Option<&Pool>,
19306) -> Vec<f32> {
19307 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
19308 let hd = dr + dn;
19309 let mut q = vec![0.0f32; nh * hd];
19310 match (&w.q_a, &w.q_a_norm) {
19311 (Some(qa), Some(qn)) => {
19312 let mut t = vec![0.0f32; qa.rows()];
19313 qa.matvec(normed, &mut t, pool);
19314 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
19315 w.q_proj.matvec(&tn, &mut q, pool);
19316 }
19317 _ => w.q_proj.matvec(normed, &mut q, pool),
19318 }
19319 let mut ca = vec![0.0f32; lora + dr];
19320 w.kv_a.matvec(normed, &mut ca, pool);
19321 let (c_lat, k_rope) = ca.split_at_mut(lora);
19322 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
19323 let mut kvb = vec![0.0f32; nh * (dn + dv)];
19324 w.kv_b.matvec(&latn, &mut kvb, pool);
19325 if !w.nope {
19326 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
19327 }
19328 for h in 0..nh {
19329 if !w.nope {
19330 attention::rope_rotate_scaled(
19331 &mut q[h * hd..h * hd + dr],
19332 position,
19333 inv_freq,
19334 rope_scale,
19335 );
19336 }
19337 }
19338 let mut k = vec![0.0f32; nh * hd];
19339 let mut v = vec![0.0f32; nh * hd];
19340 for h in 0..nh {
19341 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
19342 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
19343 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
19344 }
19345 cache.append(&k, &v, &vec![true; nh]);
19346 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
19347 attention::recycle_buf(&mut imp);
19348 let mut ov = vec![0.0f32; nh * dv];
19349 for h in 0..nh {
19350 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
19351 }
19352 let mut out = vec![0.0f32; w.o_proj.rows()];
19353 w.o_proj.matvec(&ov, &mut out, pool);
19354 out
19355}
19356
19357fn dense_moe_ffn(
19364 dm: &DenseMoeFfn,
19365 x_normed: &[f32],
19366 h_raw: &[f32],
19367 eps: f64,
19368 norm_style: NormStyle,
19369 pool: Option<&Pool>,
19370) -> Vec<f32> {
19371 let mut d = dense_ffn(&dm.dense, x_normed, pool);
19372 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
19373 let m = &dm.moe;
19374 let ne = m.experts.len();
19375 let mut logits = vec![0.0f32; ne];
19376 if m.router_input_norm {
19377 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
19378 let inv = 1.0 / (ss + eps as f32).sqrt();
19379 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
19380 m.router.matvec(&xr, &mut logits, pool);
19381 } else {
19382 m.router.matvec(h_raw, &mut logits, pool);
19383 }
19384 let (idx, p, wsum) = moe_route(&logits, m, None);
19385 {
19386 let mut st = m.stats.borrow_mut();
19387 if st.len() < ne {
19388 st.resize(ne, 0);
19389 }
19390 for &e in &idx {
19391 st[e] += 1;
19392 }
19393 }
19394 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
19395 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
19396 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
19397 for (di, mi) in d.iter_mut().zip(&mo) {
19398 *di += mi;
19399 }
19400 d
19401}
19402
19403fn moe_gpu_refused(why: &'static str) {
19410 use std::sync::atomic::{AtomicBool, Ordering};
19411 static SAID: AtomicBool = AtomicBool::new(false);
19412 if !SAID.swap(true, Ordering::Relaxed) {
19413 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
19414 }
19415}
19416
19417fn moe_ffn_gpu(
19418 m: &MoeFfn,
19419 x: &[f32],
19420 idx: &[usize],
19421 p: &[f32],
19422 wsum: f32,
19423 pool: Option<&Pool>,
19424) -> Option<Vec<f32>> {
19425 use crate::gpu::MoeJob;
19426
19427 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
19428 let mut model_ref = None;
19429 for &e in idx {
19430 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
19431 moe_gpu_refused("push_job(expert)");
19432 return None;
19433 }
19434 }
19435 if let Some((se, gate)) = &m.shared {
19436 let g = gate.as_ref().map_or(1.0, |gate| {
19437 let mut gl = [0.0f32; 1];
19438 gate.matvec(x, &mut gl, pool);
19439 1.0 / (1.0 + (-gl[0]).exp())
19440 });
19441 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
19442 moe_gpu_refused("push_job(shared)");
19443 return None;
19444 }
19445 }
19446 let Some(model) = model_ref else {
19447 moe_gpu_refused("no model_ref");
19448 return None;
19449 };
19450 let hidden = jobs[0].down.1;
19451 let mut out = vec![0.0f32; hidden];
19452 if crate::gpu::moe_block(&model, &jobs, &mut out) {
19453 Some(out)
19454 } else {
19455 moe_gpu_refused("gpu::moe_block");
19456 None
19457 }
19458}
19459
19460fn ffn_forward(
19462 ffn: &FfnKind,
19463 x: &[f32],
19464 pool: Option<&Pool>,
19465 experts_allowed: Option<&[bool]>,
19466) -> Vec<f32> {
19467 match ffn {
19468 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
19469 FfnKind::Dense(d) => dense_ffn(d, x, pool),
19470 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
19471 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19475 }
19476}
19477
19478fn ffn_forward_pair(
19482 ffn: &FfnKind,
19483 x1: &[f32],
19484 x2: &[f32],
19485 pool: Option<&Pool>,
19486 experts_allowed: Option<&[bool]>,
19487) -> (Vec<f32>, Vec<f32>) {
19488 let d = match ffn {
19489 FfnKind::Dense(d) if !d.segs.is_empty() => {
19492 return (
19493 tube_ffn(d, x1, 1, pool, None),
19494 tube_ffn(d, x2, 1, pool, None),
19495 );
19496 }
19497 FfnKind::Dense(d) => d,
19498 FfnKind::Moe(m) => {
19499 return (
19500 moe_ffn(m, x1, pool, experts_allowed),
19501 moe_ffn(m, x2, pool, experts_allowed),
19502 );
19503 }
19504 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
19505 };
19506 let inter = d.gate_proj.rows();
19507 FFN_SCRATCH.with(|s| {
19508 let mut s = s.borrow_mut();
19509 let [g1, g2, u1, u2] = &mut *s;
19510 g1.resize(inter, 0.0);
19511 g2.resize(inter, 0.0);
19512 u1.resize(inter, 0.0);
19513 u2.resize(inter, 0.0);
19514 QTensor::matvec2_many(
19517 [&d.gate_proj, &d.up_proj],
19518 x1,
19519 x2,
19520 [g1.as_mut_slice(), u1.as_mut_slice()],
19521 [g2.as_mut_slice(), u2.as_mut_slice()],
19522 pool,
19523 );
19524 for i in 0..inter {
19525 g1[i] = d.act.combine(g1[i], u1[i]);
19526 g2[i] = d.act.combine(g2[i], u2[i]);
19527 }
19528 let mut o1 = attention::take_buf(d.down_proj.rows());
19529 let mut o2 = attention::take_buf(d.down_proj.rows());
19530 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
19531 (o1, o2)
19532 })
19533}
19534
19535#[cfg(test)]
19536mod tests {
19537
19538 #[test]
19543 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
19544 use super::{
19545 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
19546 };
19547 let dense_card = ChunkStackFacts {
19548 plain_dense: true,
19549 discrete: true,
19550 gpu_on: true,
19551 ..Default::default()
19552 };
19553 assert!(dense_card.dense_on_discrete());
19554 assert_eq!(
19556 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
19557 DISCRETE_DENSE_PREFILL_CHUNK
19558 );
19559 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
19560 for (label, facts) in [
19561 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
19562 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
19563 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
19564 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
19565 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
19566 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
19567 ] {
19568 assert!(!facts.dense_on_discrete(), "{label}");
19569 assert_eq!(
19570 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
19571 48,
19572 "{label} keeps the historical x86 chunk"
19573 );
19574 }
19575 for dense in [false, true] {
19577 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
19578 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
19579 }
19580 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
19582 for dense in [false, true] {
19583 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
19584 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
19585 }
19586 }
19587 }
19588
19589 #[test]
19590 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
19591 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
19592 let full = |host_rows, device_rows| ReuseLayer {
19593 full: true,
19594 host_rows,
19595 device_rows,
19596 device_state: false,
19597 };
19598 assert_eq!(
19601 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
19602 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
19603 );
19604 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
19606 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
19608 assert_eq!(
19610 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
19611 ReusePlan::Pull(vec![(0, 300, 339)])
19612 );
19613 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
19615 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
19616 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
19617 let conv = |device_state| ReuseLayer {
19620 full: false,
19621 host_rows: 0,
19622 device_rows: None,
19623 device_state,
19624 };
19625 assert_eq!(
19626 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
19627 ReusePlan::Fresh
19628 );
19629 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
19630 }
19631
19632 #[test]
19633 fn nll_graph_policy_scopes_only_the_fused_head() {
19634 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
19635 ("vulkan graph", true, true, false, true, false),
19637 ("native Metal graph", true, true, true, true, true),
19639 ("masked", false, true, false, false, false),
19641 ("graph disabled", true, false, true, false, false),
19642 ] {
19643 let (graph_quality, graph_head_required) =
19644 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
19645 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
19646 assert_eq!(graph_head_required, want_head, "{label}: fused head");
19647 }
19648 }
19649
19650 #[test]
19651 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
19652 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
19653 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
19654 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
19655 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
19656 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
19657 }
19658
19659 #[test]
19660 fn cancel_flag_stops_generation() {
19661 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
19662 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
19665 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
19666 assert_eq!(r.finish_reason, "cancelled");
19667 assert!(
19668 r.token_ids.is_empty(),
19669 "no tokens after cancel: {:?}",
19670 r.token_ids
19671 );
19672 assert_eq!(p.kv_cache.seq_len(), 0);
19673 assert!(p.kv_history.is_empty());
19674 assert!(!p.graph_want_logits);
19675 assert!(p.graph_logits.is_none());
19676 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
19678 assert_ne!(r2.finish_reason, "cancelled");
19679 }
19680 use super::*;
19681
19682 #[test]
19683 fn explicit_sliding_schedule_selects_local_rope_profile() {
19684 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
19689 p.swa = Some((1024, usize::MAX));
19690 p.sliding_layers = Some(vec![true, true, true, false]);
19691 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
19692 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
19693 p.rope_scale = 1.277_258_9;
19694 p.rope_scale_local = 1.0;
19695
19696 for li in 0..3 {
19697 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
19698 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
19699 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
19700 }
19701 assert_eq!(p.layer_window(3), None);
19702 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
19703 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
19704 }
19705
19706 #[test]
19714 fn dynamic_ffn_equals_the_zeroing_arm() {
19715 let (hidden, inter) = (8usize, 32usize);
19716 let synth = |n: usize, salt: usize| -> Vec<f32> {
19717 (0..n)
19718 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
19719 .collect()
19720 };
19721 let down = synth(hidden * inter, 3);
19722 let mut down_t = vec![0.0f32; inter * hidden];
19723 for r in 0..hidden {
19724 for c in 0..inter {
19725 down_t[c * hidden + r] = down[r * inter + c];
19726 }
19727 }
19728 let d = DenseFfn {
19729 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19730 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19731 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
19732 act: Act::Silu,
19733 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
19734 segs: Vec::new(),
19735 };
19736 let x = synth(hidden, 11);
19737 let k = 12usize;
19738 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
19739 let mut g = vec![0.0f32; inter];
19741 d.gate_proj.matvec(&x, &mut g, None);
19742 let mut u = vec![0.0f32; inter];
19743 d.up_proj.matvec(&x, &mut u, None);
19744 for v in g.iter_mut() {
19745 *v = inference::silu(*v);
19746 }
19747 keep_top_k(&mut g, k);
19748 for i in 0..inter {
19749 g[i] *= u[i];
19750 }
19751 let mut want = vec![0.0f32; hidden];
19752 d.down_proj.matvec(&g, &mut want, None);
19753 for (a, b) in want.iter().zip(&got) {
19754 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
19755 }
19756 }
19757
19758 #[test]
19764 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
19765 let (hidden, core, tube) = (8usize, 12usize, 8usize);
19766 let inter = core + tube;
19767 let synth = |n: usize, salt: usize| -> Vec<f32> {
19768 (0..n)
19769 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
19770 .collect()
19771 };
19772 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
19773 let d_all = synth(hidden * inter, 3);
19774 let dense = DenseFfn {
19776 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
19777 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
19778 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
19779 act: Act::Silu,
19780 down_t: None,
19781 segs: Vec::new(),
19782 };
19783 let rows =
19784 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
19785 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
19786 let mut o = Vec::with_capacity(hidden * (b - a));
19787 for r in 0..hidden {
19788 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
19789 }
19790 o
19791 };
19792 let tubed = DenseFfn {
19793 down_t: None,
19794 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
19795 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
19796 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
19797 act: Act::Silu,
19798 segs: vec![FfnSeg {
19799 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
19800 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
19801 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
19802 start: core,
19803 width: tube,
19804 }],
19805 };
19806 let x = synth(hidden, 7);
19807 let want = dense_ffn(&dense, &x, None);
19808 let got = tube_ffn(&tubed, &x, 1, None, None);
19809 for (a, b) in want.iter().zip(&got) {
19810 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
19811 }
19812 let mut bits = vec![0u8; inter.div_ceil(8)];
19814 for n in 0..core {
19815 bits[n / 8] |= 1 << (n % 8);
19816 }
19817 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19818 let masked = dense_ffn_masked(&dense, &x, None, &bits);
19819 for (a, b) in masked.iter().zip(&closed) {
19820 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
19821 }
19822 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19824 for (a, b) in closed.iter().zip(&batch) {
19825 assert_eq!(a, b, "batch arm disagrees with decode arm");
19826 }
19827 }
19828
19829 #[test]
19831 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
19832 let (hidden, inter) = (16usize, 40usize);
19833 let synth = |n: usize, salt: usize| -> Vec<f32> {
19834 (0..n)
19835 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
19836 .collect()
19837 };
19838 let d = DenseFfn {
19839 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19840 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19841 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
19842 act: Act::Silu,
19843 down_t: None,
19844 segs: Vec::new(),
19845 };
19846 let x = synth(hidden, 9);
19847 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
19849
19850 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
19851
19852 let mut g = vec![0.0f32; inter];
19854 d.gate_proj.matvec(&x, &mut g, None);
19855 let mut u = vec![0.0f32; inter];
19856 d.up_proj.matvec(&x, &mut u, None);
19857 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
19858 for i in 0..inter {
19859 g[i] = if act_set.contains(&(i as u16)) {
19860 inference::silu(g[i]) * u[i]
19861 } else {
19862 0.0
19863 };
19864 }
19865 let mut reference = vec![0.0f32; hidden];
19866 d.down_proj.matvec(&g, &mut reference, None);
19867
19868 let max_d = sparse
19869 .iter()
19870 .zip(&reference)
19871 .map(|(a, b)| (a - b).abs())
19872 .fold(0.0f32, f32::max);
19873 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
19874 }
19875
19876 fn attach_test_mtp(p: &mut Pipeline) {
19878 let (h, inter, heads, kv, hd) = (
19879 p.hidden_size,
19880 p.intermediate_size,
19881 p.num_heads,
19882 p.num_kv_heads,
19883 p.head_dim,
19884 );
19885 let synth = |n: usize, salt: usize| -> Vec<f32> {
19886 (0..n)
19887 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
19888 .collect()
19889 };
19890 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19891 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19892 };
19893 p.mtp = Some(MtpModule {
19894 enorm: vec![1.0; h],
19895 hnorm: vec![1.0; h],
19896 eh_proj: qt(h, 2 * h, 301),
19897 layer: LayerWeights {
19898 input_norm: vec![1.0; h],
19899 post_norm: vec![1.0; h],
19900 attn_out_norm: None,
19901 ffn_out_norm: None,
19902 layer_scale: None,
19903 ffn: FfnKind::Dense(DenseFfn {
19904 gate_proj: qt(inter, h, 315),
19905 up_proj: qt(inter, h, 316),
19906 down_proj: qt(h, inter, 317),
19907 act: Act::Silu,
19908 down_t: None,
19909 segs: Vec::new(),
19910 }),
19911 attn: AttnKind::Full {
19912 bias: None,
19913 wq: qt(heads * hd, h, 311),
19914 wk: qt(kv * hd, h, 312),
19915 wv: qt(kv * hd, h, 313),
19916 wo: qt(h, heads * hd, 314),
19917 q_norm: None,
19918 k_norm: None,
19919 output_gate: false,
19920 softplus_gate: None,
19921 },
19922 },
19923 final_norm: vec![1.0; h],
19924 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
19925 });
19926 }
19927
19928 #[test]
19929 fn speculative_equals_vanilla_greedy() {
19930 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19934 let run = |spec: bool| {
19935 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19936 p.sampler_config.temperature = 0.0;
19937 attach_test_mtp(&mut p);
19938 p.speculative = spec;
19939 let r = p.generate("abcdef", 12, None, None).unwrap();
19940 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
19941 };
19942 let (vanilla, d0, _) = run(false);
19943 let (spec, d1, a1) = run(true);
19944 assert_eq!(d0, 0, "vanilla path must not draft");
19945 assert!(d1 > 0, "speculative path must draft");
19946 assert_eq!(
19947 vanilla, spec,
19948 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
19949 );
19950 }
19951
19952 #[test]
19953 fn speculative_accepts_constant_oracle() {
19954 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19956 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19957 p.sampler_config.temperature = 0.0;
19958 p.sampler_config.repetition_penalty = 1.0;
19959 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
19962 attach_test_mtp(&mut p);
19963 p.speculative = true;
19964 let r = p.generate("abcd", 10, None, None).unwrap();
19965 assert!(r.mtp_drafted > 0);
19966 assert_eq!(
19967 r.mtp_accepted, r.mtp_drafted,
19968 "constant logits → every draft accepted"
19969 );
19970 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
19973 }
19974
19975 #[test]
19976 fn empty_prompt_is_an_error_not_a_panic() {
19977 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
19978 let r = p.generate("", 4, None, None);
19979 assert!(r.is_err(), "empty prompt must be a clean error");
19980 }
19981
19982 #[test]
19983 fn every_token_enters_kv_exactly_once() {
19984 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19985 p.sampler_config.temperature = 0.0;
19987 let r = p.generate("abc", 2, None, None).unwrap();
19988 assert_eq!(r.prompt_tokens, 3);
19989 assert_eq!(
19993 p.kv_cache.seq_len(),
19994 3 + r.tokens_generated - 1,
19995 "each token must be cached exactly once (v1 cached the last prompt token twice)"
19996 );
19997 }
19998
19999 #[test]
20000 fn generation_is_reproducible_with_seed() {
20001 let run = || {
20002 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20003 p.generate("hello", 8, None, None).unwrap().token_ids
20004 };
20005 assert_eq!(run(), run());
20006 }
20007
20008 #[test]
20009 fn resetting_sampler_restarts_the_seeded_stream() {
20010 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20011 let config = SamplerConfig {
20012 seed: Some(1234),
20013 ..SamplerConfig::default()
20014 };
20015 p.set_sampler_config(config.clone());
20016 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
20017 p.set_sampler_config(config);
20018 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
20019 assert_eq!(first, second);
20020 }
20021
20022 #[test]
20023 fn eviction_bounds_the_cache() {
20024 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
20025 p.kv_cache.max_seq_len = 6;
20026 p.sampler_config.temperature = 0.0;
20027 let _ = p.generate("abcd", 12, None, None).unwrap();
20028 assert!(
20029 p.kv_cache.seq_len() <= 6 + 1,
20030 "cache must stay bounded by max_seq_len (got {})",
20031 p.kv_cache.seq_len()
20032 );
20033 }
20034
20035 #[test]
20036 fn confidence_matches_tokens_and_is_a_probability() {
20037 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20038 p.sampler_config.temperature = 0.0;
20039 p.sampler_config.repetition_penalty = 1.0;
20040 let r = p.generate("abcd", 10, None, None).unwrap();
20041 assert_eq!(
20042 r.token_confidence.len(),
20043 r.token_ids.len(),
20044 "one confidence per emitted token"
20045 );
20046 for &c in &r.token_confidence {
20047 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
20048 }
20049 let logits = [1.0f32, 3.0, 0.5, 3.0];
20051 let p0 = top1_prob_t(&logits, 1, 1.0);
20052 let p1 = top1_prob_t(&logits, 3, 1.0);
20053 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
20054 assert!(p0 > 0.0 && p0 < 1.0);
20055 let sharp = top1_prob_t(&logits, 1, 1.0);
20057 let soft = top1_prob_t(&logits, 1, 2.0);
20058 assert!(soft < sharp, "higher temperature lowers peak confidence");
20059 }
20060
20061 #[test]
20062 fn trace_is_opt_in_and_parallels_the_output() {
20063 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20065 p.sampler_config.temperature = 0.0;
20066 p.sampler_config.repetition_penalty = 1.0;
20067 let r = p.generate("abcd", 10, None, None).unwrap();
20068 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
20069
20070 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20072 p.sampler_config.temperature = 0.0;
20073 p.sampler_config.repetition_penalty = 1.0;
20074 p.set_trace(true);
20075 let r = p.generate("abcd", 10, None, None).unwrap();
20076 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
20077 for (i, tr) in r.traces.iter().enumerate() {
20078 assert_eq!(tr.t, i, "trace index is sequential");
20079 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
20080 assert_eq!(
20081 tr.confidence, r.token_confidence[i],
20082 "trace confidence matches the confidence channel"
20083 );
20084 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
20086 }
20087 }
20088
20089 #[test]
20090 fn explain_prefill_logits_match_greedy_first_token() {
20091 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20095 p.sampler_config.temperature = 0.0;
20096 p.sampler_config.repetition_penalty = 1.0;
20097 let ids = p.tokenizer.encode("abcd");
20098 let logits = p.prefill_next_logits(&ids, None);
20099 let argmax = logits
20100 .iter()
20101 .enumerate()
20102 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
20103 .unwrap()
20104 .0 as u32;
20105 let r = p.generate("abcd", 1, None, None).unwrap();
20106 assert_eq!(
20107 argmax, r.token_ids[0],
20108 "explain preview must match greedy emit"
20109 );
20110 }
20111
20112 #[test]
20113 fn laguna_shared_expert_is_unconditionally_added() {
20114 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
20115 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
20116 let zero_dense = || DenseFfn {
20117 gate_proj: matrix(vec![0.0; 4]),
20118 up_proj: matrix(vec![0.0; 4]),
20119 down_proj: matrix(vec![0.0; 4]),
20120 act: Act::Silu,
20121 down_t: None,
20122 segs: Vec::new(),
20123 };
20124 let shared = DenseFfn {
20125 gate_proj: identity(),
20126 up_proj: identity(),
20127 down_proj: identity(),
20128 act: Act::Silu,
20129 down_t: None,
20130 segs: Vec::new(),
20131 };
20132 let x = [1.0, 2.0];
20133 let expected = dense_ffn(&shared, &x, None);
20134 let moe = MoeFfn {
20135 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
20136 experts: vec![zero_dense()],
20137 top_k: 1,
20138 norm_topk_prob: true,
20139 router_sigmoid: true,
20140 expert_bias: None,
20141 routed_scaling: 1.0,
20142 route_tau: None,
20143 shared: Some((shared, None)),
20144 stats: std::cell::RefCell::new(Vec::new()),
20145 act_sq: std::cell::RefCell::new(Vec::new()),
20146 act_rows: std::cell::RefCell::new(Vec::new()),
20147 mask: None,
20148 per_expert_scale: None,
20149 router_input_norm: false,
20150 resonance: None,
20151 grown: Vec::new(),
20152 };
20153 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
20154 for (actual, expected) in actual.iter().zip(expected) {
20155 assert!((actual - expected).abs() < 1e-6);
20156 }
20157 }
20158
20159 fn mimo_test_pipeline() -> Pipeline {
20168 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
20169 let kvh = [1usize, 2, 2, 1];
20170 let synth = |n: usize, salt: usize| -> Vec<f32> {
20171 (0..n)
20172 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20173 .collect()
20174 };
20175 let qt = |rows: usize, cols: usize, salt: usize| {
20176 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20177 };
20178 let dense = |inter: usize, salt: usize| DenseFfn {
20179 gate_proj: qt(inter, hs, salt),
20180 up_proj: qt(inter, hs, salt + 1),
20181 down_proj: qt(hs, inter, salt + 2),
20182 act: Act::Silu,
20183 down_t: None,
20184 segs: Vec::new(),
20185 };
20186 let layers: Vec<LayerWeights> = (0..4)
20187 .map(|li| LayerWeights {
20188 input_norm: vec![1.0; hs],
20189 post_norm: vec![1.0; hs],
20190 attn_out_norm: None,
20191 ffn_out_norm: None,
20192 layer_scale: None,
20193 ffn: if li == 0 {
20194 FfnKind::Dense(dense(inter, 50))
20195 } else {
20196 FfnKind::Moe(MoeFfn {
20197 router: qt(4, hs, 60 + li),
20198 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
20199 top_k: 2,
20200 norm_topk_prob: true,
20201 router_sigmoid: true,
20202 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
20203 routed_scaling: 1.0,
20204 route_tau: None,
20205 shared: None,
20206 stats: std::cell::RefCell::new(Vec::new()),
20207 act_sq: std::cell::RefCell::new(Vec::new()),
20208 act_rows: std::cell::RefCell::new(Vec::new()),
20209 mask: None,
20210 per_expert_scale: None,
20211 router_input_norm: false,
20212 resonance: None,
20213 grown: Vec::new(),
20214 })
20215 },
20216 attn: AttnKind::Full {
20217 wq: qt(nh * hd, hs, li * 10 + 1),
20218 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
20219 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
20220 wo: qt(hs, nh * vd, li * 10 + 4),
20221 q_norm: None,
20222 k_norm: None,
20223 output_gate: false,
20224 softplus_gate: None,
20225 bias: None,
20226 },
20227 })
20228 .collect();
20229 let mut p = Pipeline::new(
20230 Tokenizer::byte_level(),
20231 PipelineWeights {
20232 embed_tokens: qt(vocab, hs, 100),
20233 layers,
20234 lm_head: qt(vocab, hs, 200),
20235 final_norm: vec![1.0; hs],
20236 },
20237 hs,
20238 inter,
20239 nh,
20240 1, hd,
20242 4,
20243 4,
20244 false,
20245 vocab,
20246 1e-6,
20247 1e7,
20248 NormStyle::Qwen,
20249 4096,
20250 SamplerConfig {
20251 seed: Some(7),
20252 ..Default::default()
20253 },
20254 );
20255 p.layer_dump = None;
20257 p.set_rotary(4, 1e7);
20258 p.sliding_layers = Some(vec![false, true, true, false]);
20259 p.swa = Some((3, usize::MAX));
20260 p.rotary_dim_local = Some(4);
20261 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
20262 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
20263 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
20264 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
20265 p
20266 }
20267
20268 #[test]
20269 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
20270 let mut p = mimo_test_pipeline();
20271 p.speculative = false;
20272 p.ignore_eos = true;
20273 p.sampler_config.temperature = 0.0;
20274 p.sampler_config.repetition_penalty = 1.0;
20275 let a = vec![3, 5, 7, 9, 11, 13];
20276 let b = vec![4, 8, 12, 16, 20, 24];
20277 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
20278 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
20279 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
20283 assert_eq!(actual, expected);
20284 assert!(p.kv_history.is_empty());
20285 let mut extended = a.clone();
20286 extended.push(17);
20287 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20288 p.reset_session();
20289 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
20290 assert_eq!(after_media, fresh);
20291 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
20292 p.reset_session();
20296 p.generate_from_ids(&a, 1, None, None).unwrap();
20297 let mut media_ids = p.kv_history.clone();
20298 assert!(!media_ids.is_empty());
20299 media_ids.extend_from_slice(&[19, 21, 23]);
20300 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
20301 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
20302 let mut oracle = mimo_test_pipeline();
20303 oracle.speculative = false;
20304 oracle.ignore_eos = true;
20305 oracle.sampler_config.temperature = 0.0;
20306 oracle.sampler_config.repetition_penalty = 1.0;
20307 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
20308 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
20309 assert!(p.kv_history.is_empty());
20310 let mut bad = rows;
20311 bad[0] = f32::NAN;
20312 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
20313 }
20314
20315 fn f32_bits(v: &[f32]) -> Vec<u32> {
20316 v.iter().map(|x| x.to_bits()).collect()
20317 }
20318
20319 #[test]
20326 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
20327 let mut p = mimo_test_pipeline();
20328 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
20329 assert_eq!(kv, vec![1, 2, 2, 1]);
20330 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
20331 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
20332 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
20333 let hs = p.hidden_size;
20334 let mut decode = Vec::new();
20335 for (pos, &id) in ids.iter().enumerate() {
20336 let e = p.embed_single(id);
20337 let h = p.forward_layers(&e, pos, None);
20338 decode.push(p.logits_from_hidden(&h));
20339 }
20340 for l in &p.kv_cache.layers {
20341 assert_eq!(l.seq_len, 12);
20342 assert_eq!(l.head_values(0).len(), 12 * 8);
20344 }
20345 assert!(decode.iter().flatten().all(|v| v.is_finite()));
20346
20347 p.clear_sequence_state();
20348 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20349 for pos in 0..ids.len() {
20350 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
20351 assert_eq!(
20352 f32_bits(&decode[pos]),
20353 f32_bits(&lg),
20354 "whole prompt, pos {pos}"
20355 );
20356 }
20357
20358 p.clear_sequence_state();
20359 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
20360 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
20361 for pos in 0..ids.len() {
20362 let row = if pos < 5 {
20363 &a[pos * hs..(pos + 1) * hs]
20364 } else {
20365 &b[(pos - 5) * hs..(pos - 4) * hs]
20366 };
20367 let lg = p.logits_from_hidden(row);
20368 assert_eq!(
20369 f32_bits(&decode[pos]),
20370 f32_bits(&lg),
20371 "two chunks, pos {pos}"
20372 );
20373 }
20374
20375 let last = |p: &mut Pipeline| {
20378 p.clear_sequence_state();
20379 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20380 p.logits_from_hidden(&hb[11 * hs..12 * hs])
20381 };
20382 let base = last(&mut p);
20383 let mut no_sinks = mimo_test_pipeline();
20384 for l in &mut no_sinks.kv_cache.layers {
20385 l.sinks = None;
20386 }
20387 assert_ne!(
20388 f32_bits(&last(&mut no_sinks)),
20389 f32_bits(&base),
20390 "sinks are live"
20391 );
20392 let mut wide = mimo_test_pipeline();
20393 wide.swa = Some((64, usize::MAX));
20394 assert_ne!(
20395 f32_bits(&last(&mut wide)),
20396 f32_bits(&base),
20397 "window is live"
20398 );
20399
20400 p.clear_sequence_state();
20402 p.ignore_eos = true;
20403 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
20404 assert_eq!(r.token_ids.len(), 4);
20405 }
20406
20407 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
20410 let (hs, nh) = (16usize, 4usize);
20411 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
20412 p.layer_dump = None;
20413 p.swa = Some((6, 4));
20414 p.proj_gate_sigmoid = true;
20415 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
20416 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
20417 let g: Vec<f32> = (0..nh * hs)
20418 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
20419 .collect();
20420 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
20421 }
20422 }
20423 p.swa_trim = trim;
20424 p
20425 }
20426
20427 #[test]
20433 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
20434 let mut a = spark_test_pipeline(None);
20435 let mut b = spark_test_pipeline(Some((2, 4)));
20436 assert_eq!(
20437 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
20438 vec![Some(6), Some(6), Some(6), None]
20439 );
20440 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
20441 let mut pos = 0usize;
20442 for &n in [5usize, 13, 7, 16].iter().cycle() {
20443 if pos >= ids.len() {
20444 break;
20445 }
20446 let end = (pos + n).min(ids.len());
20447 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20448 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
20449 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
20450 pos = end;
20451 }
20452 for _ in 0..23 {
20453 let e = a.embed_single(((pos * 7) % 64) as u32);
20454 let ha = a.forward_layers(&e, pos, None);
20455 let hb = b.forward_layers(&e, pos, None);
20456 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
20457 for li in 0..3 {
20458 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
20459 }
20460 pos += 1;
20461 }
20462 for round in 0..9 {
20463 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
20464 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
20465 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
20466 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
20467 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
20468 if round % 2 == 1 {
20469 for p in [&mut a, &mut b] {
20471 for l in &mut p.kv_cache.layers {
20472 l.truncate_last(2);
20473 }
20474 }
20475 } else {
20476 pos += 2;
20477 }
20478 }
20479 let e = a.embed_single(9);
20480 let (ha, hb) = (
20481 a.forward_layers(&e, pos, None),
20482 b.forward_layers(&e, pos, None),
20483 );
20484 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
20485 for li in 0..3 {
20486 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
20487 assert!(lb.base() > 0, "layer {li} trimmed");
20488 assert_eq!(la.base(), 0);
20489 assert_eq!(lb.pos_len(), la.seq_len);
20490 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
20491 }
20492 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
20493 assert_eq!(
20494 (gb.base(), gb.seq_len),
20495 (0, ga.seq_len),
20496 "the global layer keeps all"
20497 );
20498 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
20499 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
20500 }
20501
20502 #[test]
20507 fn swa_trim_wire_handoff_continues_bitwise() {
20508 let mut a = spark_test_pipeline(None);
20509 let mut b = spark_test_pipeline(Some((2, 4)));
20510 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
20511 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20512 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
20513 assert_eq!(f32_bits(&ha), f32_bits(&hb));
20514 let mut pos = ids.len();
20515 for _ in 0..5 {
20516 let e = a.embed_single(((pos * 5) % 64) as u32);
20517 assert_eq!(
20518 f32_bits(&a.forward_layers(&e, pos, None)),
20519 f32_bits(&b.forward_layers(&e, pos, None))
20520 );
20521 pos += 1;
20522 }
20523 let mut c = spark_test_pipeline(Some((2, 4)));
20524 for li in 0..4 {
20525 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
20526 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
20527 }
20528 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
20529 assert_eq!(c.kv_cache.seq_len(), pos);
20530 for step in 0..17 {
20531 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
20532 let ha = a.forward_layers(&e, pos, None);
20533 let hc = c.forward_layers(&e, pos, None);
20534 assert_eq!(
20535 f32_bits(&ha),
20536 f32_bits(&hc),
20537 "after the hand-off, step {step}"
20538 );
20539 pos += 1;
20540 }
20541 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
20542 }
20543
20544 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
20547 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
20548 let synth = |len: usize, salt: usize| -> Vec<f32> {
20549 (0..len)
20550 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
20551 .collect()
20552 };
20553 let qt = |rows: usize, cols: usize, salt: usize| {
20554 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20555 };
20556 let layers = (0..n)
20557 .map(|k| {
20558 let s = 500 + k * 40;
20559 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
20560 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
20561 MtpModule {
20562 enorm: vec![1.0; hs],
20563 hnorm: vec![1.0; hs],
20564 eh_proj: qt(hs, 2 * hs, s),
20565 layer: LayerWeights {
20566 input_norm: vec![1.0; hs],
20567 post_norm: vec![1.0; hs],
20568 attn_out_norm: None,
20569 ffn_out_norm: None,
20570 layer_scale: None,
20571 attn: AttnKind::Full {
20572 wq: qt(nh * hd, hs, s + 1),
20573 wk: qt(nkv * hd, hs, s + 2),
20574 wv: qt(nkv * vd, hs, s + 3),
20575 wo: qt(hs, nh * vd, s + 4),
20576 q_norm: None,
20577 k_norm: None,
20578 output_gate: false,
20579 softplus_gate: None,
20580 bias: None,
20581 },
20582 ffn: FfnKind::Dense(DenseFfn {
20583 gate_proj: qt(inter, hs, s + 5),
20584 up_proj: qt(inter, hs, s + 6),
20585 down_proj: qt(hs, inter, s + 7),
20586 act: Act::Silu,
20587 down_t: None,
20588 segs: Vec::new(),
20589 }),
20590 },
20591 final_norm: vec![1.0; hs],
20592 kv,
20593 }
20594 })
20595 .collect();
20596 mimo_mtp::MimoMtp::from_layers(layers)
20597 }
20598
20599 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
20600 p.clear_sequence_state();
20601 p.speculative = spec;
20602 p.ignore_eos = true;
20603 p.sampler_config.temperature = 0.0;
20604 p.generate_from_ids(ids, n, None, None).unwrap()
20605 }
20606
20607 #[test]
20613 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
20614 for post in [false, true] {
20617 let mut p = mimo_test_pipeline();
20618 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
20620 let mut st0 = mimo_test_mtp(3, 1.0);
20621 st0.post_norm_hidden = post;
20622 p.mimo_mtp = Some(st0);
20623 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
20624 let hs = p.hidden_size;
20625 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20626 p.mimo_note_rows(&hb, 0);
20627 let mut st = p.mimo_mtp.take().unwrap();
20628 let k = 3;
20631 let mut inc = Vec::new();
20632 for t in 0..ids.len() - k - 1 {
20633 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
20634 }
20635 let s = ids.len();
20638 let mut reference = vec![vec![0u32; k]; s - k - 1];
20639 let mut fresh = mimo_test_mtp(3, 1.0);
20640 for (layer, m) in fresh.layers.iter_mut().enumerate() {
20641 let n = s - layer - 1;
20642 let mut cats = vec![0.0f32; n * 2 * hs];
20643 for j in 0..n {
20644 let e = p.embed_single(ids[j + layer + 1]);
20645 let raw = &hb[j * hs..(j + 1) * hs];
20646 let g = if post {
20647 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
20648 } else {
20649 raw.to_vec()
20650 };
20651 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
20652 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
20653 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
20654 }
20655 let mut x = vec![0.0f32; n * hs];
20656 m.eh_proj.matmat(&cats, n, &mut x, None);
20657 p.mimo_mtp_block(m, &mut x, n, 0);
20658 for (t, row) in reference.iter_mut().enumerate() {
20659 let y = inference::rms_norm(
20660 &x[t * hs..(t + 1) * hs],
20661 &m.final_norm,
20662 p.rms_eps,
20663 p.norm_style,
20664 );
20665 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
20666 }
20667 }
20668 assert_eq!(inc, reference, "post_norm_hidden = {post}");
20669 let distinct: std::collections::HashSet<u32> =
20671 inc.iter().flatten().copied().collect();
20672 assert!(distinct.len() > 3, "{inc:?}");
20673 let last_t = ids.len() - k - 2;
20675 for m in &st.layers {
20676 assert_eq!(m.kv.seq_len, last_t + 1);
20677 }
20678 }
20679 }
20680
20681 #[test]
20688 fn mimo_speculative_greedy_equals_plain_greedy() {
20689 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20690 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
20691 let n = 24;
20692 let mut p = mimo_test_pipeline();
20693 let plain = mimo_greedy(&mut p, &ids, n, false);
20694 assert_eq!(plain.mtp_drafted, 0);
20695 assert_eq!(plain.token_ids.len(), n);
20696 let plain_kv = p.kv_cache.layers[0].seq_len;
20697
20698 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
20700 let spec = mimo_greedy(&mut p, &ids, n, true);
20701 assert!(spec.mtp_drafted > 0, "the round must draft");
20702 assert_eq!(spec.token_ids, plain.token_ids);
20703 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20704
20705 let mut truth: Vec<u32> = ids.clone();
20708 truth.extend(&plain.token_ids);
20709 let mut noisy = truth.clone();
20710 for (i, t) in noisy.iter_mut().enumerate() {
20711 if i % 5 == 0 {
20712 *t = (*t + 1) % 64;
20713 }
20714 }
20715 let mut st = mimo_test_mtp(3, 1.0);
20716 st.draft_override = Some(noisy);
20717 p.mimo_mtp = Some(st);
20718 let spec = mimo_greedy(&mut p, &ids, n, true);
20719 assert_eq!(spec.token_ids, plain.token_ids);
20720 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20721 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
20722 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
20723 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
20724 assert!(
20725 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
20726 "{:?}",
20727 stats.accept_hist
20728 );
20729 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
20730
20731 let mut st = mimo_test_mtp(3, 1.0);
20734 st.draft_override = Some(truth);
20735 p.mimo_mtp = Some(st);
20736 let spec = mimo_greedy(&mut p, &ids, n, true);
20737 assert_eq!(spec.token_ids, plain.token_ids);
20738 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
20739 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20740
20741 let off = mimo_greedy(&mut p, &ids, n, false);
20743 assert_eq!(off.token_ids, plain.token_ids);
20744 assert_eq!(off.mtp_drafted, 0);
20745 }
20746
20747 #[test]
20756 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
20757 let p = mimo_test_pipeline();
20758 assert_eq!(
20759 p.graph_attn_decline_reason(),
20760 Some("per-layer KV head counts")
20761 );
20762 assert_eq!(p.wgpu_graph_attn_decline(), None);
20763 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
20764 assert_eq!(
20765 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
20766 (1, 4, 4, None, false)
20767 );
20768 assert_eq!(g0.invf, p.inv_freq.as_slice());
20769 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
20770 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
20771 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
20772 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
20773 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
20774 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
20775 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
20776
20777 let emb = p.embed_single(3);
20780 let mut lg = Vec::new();
20781 assert!(
20782 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
20783 .is_none()
20784 );
20785 let mut hid = emb.clone();
20786 assert_eq!(
20787 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
20788 crate::gpu::BatchGraphOutcome::Declined
20789 );
20790 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
20791 assert!(p.try_multi_burst(3, 0, 4).is_none());
20792 assert!(
20793 p.graph_declines().is_empty(),
20794 "no attention decline logged: {:?}",
20795 p.graph_declines()
20796 );
20797 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
20802 assert_eq!(plain().graph_attn_decline_reason(), None);
20803 assert_eq!(plain().wgpu_graph_attn_decline(), None);
20804 assert!(
20805 plain().graph_attn_geom(0).is_none(),
20806 "uniform models keep the historical arms"
20807 );
20808 let mut q = plain();
20809 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
20810 assert_eq!(
20811 q.graph_attn_decline_reason(),
20812 Some("learned attention sinks")
20813 );
20814 assert_eq!(
20815 q.graph_attn_geom(1).unwrap().sink,
20816 Some(&[0.25f32, -0.25][..])
20817 );
20818 let mut q = plain();
20819 q.set_attn_geometry(None, Some(2)).unwrap();
20820 assert_eq!(
20821 q.graph_attn_decline_reason(),
20822 Some("V heads narrower than Q/K heads")
20823 );
20824 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
20825 let mut q = plain();
20826 q.sliding_layers = Some(vec![true, false]);
20827 q.swa = Some((4, usize::MAX));
20828 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
20829 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
20830 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
20831
20832 let mut q = mimo_test_pipeline();
20836 q.rope_scale = 2.0;
20837 q.rope_scale_local = 1.25;
20838 assert_eq!(q.wgpu_graph_attn_decline(), None);
20839 assert_eq!(q.graph_attn_geom(0).unwrap().rope_scale, 2.0);
20840 assert_eq!(q.graph_attn_geom(1).unwrap().rope_scale, 1.25);
20841
20842 let mut q = plain();
20845 q.rope_scale = 2.0;
20846 assert_eq!(
20847 q.wgpu_graph_attn_decline(),
20848 Some("scaled RoPE without per-layer geometry")
20849 );
20850 }
20851
20852 #[test]
20853 fn mimo_verify_rewind_preserves_lagging_host_caches() {
20854 let mut p = mimo_test_pipeline();
20855 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
20856 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
20857 for _ in 0..if li == 0 { 2 } else { 12 } {
20858 layer.append(&row, &row, &[]);
20859 }
20860 }
20861 p.mimo_verify_rewind(9).unwrap();
20862 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
20863 for layer in &p.kv_cache.layers[1..] {
20864 assert_eq!(layer.seq_len, 9);
20865 }
20866 }
20867
20868 #[test]
20872 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
20873 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
20874 let _ = std::fs::remove_dir_all(&dir);
20875 let mut p = mimo_test_pipeline();
20876 let hs = p.hidden_size;
20877 let ids = [5u32, 9, 11, 2, 40];
20878 p.layer_dump = Some(dir.join("decode"));
20879 for (pos, &id) in ids.iter().enumerate() {
20880 let e = p.embed_single(id);
20881 let _ = p.forward_layers(&e, pos, None);
20882 }
20883 p.clear_sequence_state();
20884 p.layer_dump = Some(dir.join("prefill"));
20885 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20886 for pos in 0..ids.len() {
20887 for li in 0..p.num_layers {
20888 let name = format!("p{pos:06}_l{li:02}.f32");
20889 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
20890 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
20891 assert_eq!(a.len(), hs * 4, "{name}");
20892 assert_eq!(a, b, "{name}");
20893 }
20894 }
20895 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
20896 let vals: Vec<f32> = last
20897 .chunks(4)
20898 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
20899 .collect();
20900 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
20901 let _ = std::fs::remove_dir_all(&dir);
20902 }
20903
20904 #[test]
20905 fn attn_geometry_and_sinks_are_validated() {
20906 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
20907 assert!(
20908 p.set_attn_geometry(Some(vec![2]), None).is_err(),
20909 "one entry per layer"
20910 );
20911 assert!(
20912 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
20913 "3 does not divide 4"
20914 );
20915 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
20916 assert!(p.set_attn_geometry(None, Some(0)).is_err());
20917 assert!(
20918 p.set_attn_geometry(None, Some(5)).is_err(),
20919 "V wider than the head"
20920 );
20921 p.set_attn_geometry(None, Some(4)).unwrap();
20922 assert_eq!(
20923 p.v_head_dim, None,
20924 "v_head_dim == head_dim is the uniform case"
20925 );
20926 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
20927 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
20928 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
20929 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
20930 assert!(
20931 p.kv_cache.layers[1].sinks.is_some(),
20932 "a reshape keeps the layer's sinks"
20933 );
20934 assert_eq!(p.layer_geom(1).0, 4);
20935 assert!(
20936 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
20937 "one sink per Q head"
20938 );
20939 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
20940 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
20941 }
20942
20943 #[test]
20946 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
20947 let cfg = || {
20948 Some(crate::nystrom::O1Cfg {
20949 layers: crate::nystrom::O1Layers::All,
20950 m: 4,
20951 w: 8,
20952 sink: 2,
20953 rect: crate::nystrom::O1Rect::Aggregate,
20954 })
20955 };
20956 let mut p = mimo_test_pipeline();
20957 p.set_o1(cfg());
20958 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
20959 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
20960 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
20961 q.sliding_layers = Some(vec![false, false, true]);
20962 q.swa = Some((4, usize::MAX));
20963 q.set_o1(cfg());
20964 assert_eq!(q.o1_flags, vec![true, false, false]);
20965 }
20966
20967 #[test]
20968 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
20969 const B: usize = 19;
20970 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20971 p.set_o1(Some(crate::nystrom::O1Cfg {
20972 layers: crate::nystrom::O1Layers::All,
20973 m: 4,
20974 w: 8,
20975 sink: 2,
20976 rect: crate::nystrom::O1Rect::Aggregate,
20977 }));
20978 p.o1_begin_with_prefix(Some(B));
20979 let ids: Vec<u32> = (0..B as u32).collect();
20980 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20981
20982 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
20983 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
20984 let next = p.embed_single(B as u32);
20985 let _ = p.forward_layers(&next, B, None);
20986 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
20987 }
20988
20989 #[test]
20990 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
20991 const B: usize = 19;
20992 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20993 let gdn_cfg = crate::linear_core::GdnCfg {
20997 num_v_heads: 2,
20998 num_k_heads: 1,
20999 key_head_dim: 2,
21000 value_head_dim: 4,
21001 conv_kernel: 3,
21002 hidden_size: 8,
21003 rms_eps: 1e-6,
21004 output_gate_sigmoid: false,
21005 };
21006 let synth = |n: usize, salt: usize| -> Vec<f32> {
21007 (0..n)
21008 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
21009 .collect()
21010 };
21011 let qt = |rows: usize, cols: usize, salt: usize| {
21012 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
21013 };
21014 let c_dim = gdn_cfg.conv_dim();
21015 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
21016 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
21017 in_proj_qkv: qt(c_dim, 8, 1),
21018 in_proj_z: qt(vd, 8, 2),
21019 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
21020 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
21021 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
21022 a_log: vec![0.2, 0.5],
21023 dt_bias: synth(gdn_cfg.num_v_heads, 6),
21024 norm: vec![1.0; gdn_cfg.value_head_dim],
21025 out_proj: qt(8, vd, 7),
21026 });
21027 p.gdn_cfg = Some(gdn_cfg);
21028 p.set_o1(Some(crate::nystrom::O1Cfg {
21029 layers: crate::nystrom::O1Layers::All,
21030 m: 4,
21031 w: 8,
21032 sink: 2,
21033 rect: crate::nystrom::O1Rect::Aggregate,
21034 }));
21035 p.o1_begin_with_prefix(Some(B));
21036 for pos in 0..B - 2 {
21037 let emb = p.embed_single(pos as u32);
21038 let _ = p.forward_layers(&emb, pos, None);
21039 }
21040 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
21041
21042 let e1 = p.embed_single((B - 2) as u32);
21043 let e2 = p.embed_single((B - 1) as u32);
21044 let _ = p.forward_pair(&e1, &e2, B - 2);
21045
21046 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
21047 assert!(
21048 p.kv_cache
21049 .layers
21050 .iter()
21051 .enumerate()
21052 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
21053 );
21054 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
21055 assert_ne!(
21056 p.kv_cache.layers[0].linear_state, lane1_state,
21057 "real pair must commit GDN lane 2 before returning"
21058 );
21059 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
21060 let next = p.embed_single(B as u32);
21061 let _ = p.forward_layers(&next, B, None);
21062 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
21063 }
21064
21065 #[test]
21066 fn o1_error_observation_stays_terminal_until_reset() {
21067 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21068 p.set_o1(Some(crate::nystrom::O1Cfg {
21069 layers: crate::nystrom::O1Layers::All,
21070 m: 4,
21071 w: 8,
21072 sink: 2,
21073 rect: crate::nystrom::O1Rect::Aggregate,
21074 }));
21075 p.o1_begin();
21076 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
21077
21078 assert!(p.o1_seal_checked().is_err());
21079 assert!(
21080 p.o1_seal_checked().is_err(),
21081 "retry must see the sticky error"
21082 );
21083 let k = vec![0.2f32; 4];
21084 let v = vec![0.3f32; 4];
21085 p.kv_cache.layers[0].append(&k, &v, &[]);
21086 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
21087
21088 p.reset_session();
21089 p.o1_begin();
21090 p.kv_cache.layers[0].append(&k, &v, &[]);
21091 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
21092 }
21093
21094 #[test]
21095 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
21096 let ids = vec![1u32, 2, 3, 4, 5, 6];
21097 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21098 p.graph_logits = Some(vec![123.0]);
21099 p.graph_want_logits = true;
21100 p.graph_failed
21101 .store(true, std::sync::atomic::Ordering::Relaxed);
21102 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21103 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
21104 assert!(err.contains("before NLL"));
21105 assert!(p.graph_logits.is_none());
21106 assert!(!p.graph_want_logits);
21107 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21108 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21109
21110 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21111 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21112 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21113 assert_eq!(actual.1, expected.1);
21114 assert!((actual.0 - expected.0).abs() < 1e-9);
21115 }
21116
21117 #[test]
21118 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
21119 let ids = vec![1u32, 2, 3, 4, 5, 6];
21120 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21121 p.nll_test_fail_at = Some(1);
21122 let err = p
21123 .nll_ids_from(&ids, 0)
21124 .expect_err("one-shot forward failure");
21125 assert!(err.contains("forward") || err.contains("score row"));
21126 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21127 assert!(!p.graph_want_logits);
21128 assert!(p.graph_logits.is_none());
21129 assert!(p.kv_history.is_empty());
21130
21131 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21132 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
21133 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
21134 assert_eq!(actual.1, expected.1);
21135 assert!((actual.0 - expected.0).abs() < 1e-9);
21136 }
21137
21138 #[test]
21139 fn nll_serial_failure_before_first_row_is_reported() {
21140 let ids = vec![1u32, 2, 3, 4];
21141 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21142 p.nll_test_force_serial = true;
21143 p.nll_test_fail_at = Some(0);
21144 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
21145 assert!(err.contains("serial forward"));
21146 assert!(p.kv_history.is_empty());
21147 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21148 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21149 }
21150
21151 #[test]
21152 fn ffn_probe_failure_discards_recorder_and_state() {
21153 let ids = vec![1u32, 2, 3, 4];
21154 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21155 p.nll_test_fail_at = Some(0);
21156 let err = p
21157 .probe_ffn_mass_batch(&ids)
21158 .expect_err("probe forward failure");
21159 assert!(err.contains("NLL"));
21160 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
21161 assert!(p.kv_history.is_empty());
21162 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21163 }
21164
21165 #[test]
21166 fn nll_test_controls_are_pipeline_scoped() {
21167 let ids = vec![1u32, 2, 3, 4];
21168 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21169 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21170 failing.nll_test_force_serial = true;
21171 failing.nll_test_fail_at = Some(0);
21172
21173 assert!(!failing.can_prefill_batched());
21174 assert!(unaffected.can_prefill_batched());
21175 let expected = unaffected
21176 .nll_ids_from(&ids, 0)
21177 .expect("unaffected pipeline remains usable");
21178 let err = failing
21179 .nll_ids_from(&ids, 0)
21180 .expect_err("failure injection belongs to failing pipeline");
21181 assert!(err.contains("serial forward"));
21182 assert!(failing.nll_test_fail_at.is_none());
21183 assert!(unaffected.can_prefill_batched());
21184 let actual = unaffected
21185 .nll_ids_from(&ids, 0)
21186 .expect("unaffected pipeline remains reusable");
21187 assert_eq!(actual.1, expected.1);
21188 assert!((actual.0 - expected.0).abs() < 1e-9);
21189 }
21190
21191 #[test]
21192 fn forward_ids_failure_channel_is_terminal_and_reusable() {
21193 let ids = vec![1u32, 2, 3, 4, 5, 6];
21194 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
21195 p.graph_logits = Some(vec![123.0]);
21196 p.graph_want_logits = true;
21197 p.graph_failed
21198 .store(true, std::sync::atomic::Ordering::Relaxed);
21199 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
21200
21201 let err = p
21202 .forward_ids(&ids, None)
21203 .expect_err("a failed forward must not become a valid head result");
21204 assert!(err.contains("forward_ids setup"));
21205 assert!(p.graph_logits.is_none());
21206 assert!(!p.graph_want_logits);
21207 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
21208 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
21209 assert_eq!(p.kv_cache.seq_len(), 0);
21210
21211 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
21212 .forward_ids(&ids, None)
21213 .expect("fresh forward_ids");
21214 let actual = p
21215 .forward_ids(&ids, None)
21216 .expect("pipeline remains reusable after a failed forward");
21217 assert_eq!(actual.len(), expected.len());
21218 assert!(
21219 actual
21220 .iter()
21221 .zip(expected)
21222 .all(|(a, b)| (a - b).abs() < 1e-9)
21223 );
21224 assert_eq!(p.kv_cache.seq_len(), ids.len());
21225 }
21226
21227 #[test]
21228 fn sigmoid_router_floor_is_explicit_per_architecture() {
21229 let zero = || DenseFfn {
21235 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21236 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21237 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21238 act: Act::Silu,
21239 down_t: None,
21240 segs: Vec::new(),
21241 };
21242 let m = MoeFfn {
21243 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
21244 experts: vec![zero(), zero()],
21245 top_k: 1,
21246 norm_topk_prob: true,
21247 router_sigmoid: true,
21248 expert_bias: None,
21249 routed_scaling: 2.5,
21250 route_tau: None,
21251 shared: None,
21252 stats: std::cell::RefCell::new(Vec::new()),
21253 act_sq: std::cell::RefCell::new(Vec::new()),
21254 act_rows: std::cell::RefCell::new(Vec::new()),
21255 mask: None,
21256 per_expert_scale: None,
21257 router_input_norm: false,
21258 resonance: None,
21259 grown: Vec::new(),
21260 };
21261 let logits = [-20.0f32, -20.0];
21262 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
21263 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
21264 let expected = (p[0] + 1e-20) / m.routed_scaling;
21265 assert!((glm_wsum - expected).abs() < 1e-15);
21266 assert!(generic_wsum > glm_wsum * 100.0);
21267 }
21268
21269 #[test]
21270 fn resonance_scores_match_formula_and_stable_tie() {
21271 let r = Resonance {
21272 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
21274 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
21275 k: 1,
21276 bias: vec![1.5, 0.5, 0.0],
21277 shell: Vec::new(),
21278 };
21279 let x = [1.0f32, 1.0];
21280 let mut got = vec![0.0; 3];
21281 r.scores(&x, &mut got);
21282 assert!((got[0] - 0.5).abs() < 1e-6);
21286 assert!((got[1] - 0.5).abs() < 1e-6);
21287 assert!(got[2].abs() < 1e-6);
21288 let best = got
21289 .iter()
21290 .enumerate()
21291 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
21292 .map(|(i, _)| i);
21293 assert_eq!(best, Some(0));
21294 assert!(got.iter().all(|v| v.is_finite()));
21295 }
21296
21297 #[test]
21302 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
21303 let plain = Resonance {
21309 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
21310 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
21311 k: 1,
21312 bias: vec![1.5, 0.5, 0.0, 0.0],
21313 shell: Vec::new(),
21314 };
21315 let shelled = Resonance {
21316 mu: plain.mu.clone(),
21317 u: plain.u.clone(),
21318 k: 1,
21319 bias: plain.bias.clone(),
21320 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
21321 };
21322 assert!(!plain.has_shell());
21323 assert!(shelled.has_shell());
21324 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
21325 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
21326 set_growth_shell(Some(true));
21327 assert!(growth_shell_enabled());
21328 let x = [1.0f32, 1.0];
21331 plain.scores(&x, &mut a);
21332 shelled.scores(&x, &mut b);
21333 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
21334 assert!(a[2] == 0.0 && a[3] == 0.0);
21335 let xo = [3.0f32, 0.0];
21338 plain.scores(&xo, &mut a);
21339 shelled.scores(&xo, &mut b);
21340 assert_eq!(a[2], -6.0);
21341 assert_eq!(a[3], -6.0);
21342 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
21343 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
21344 assert_eq!(shelled.effective_shell(4), shelled.shell);
21345 set_growth_shell(Some(false));
21348 assert!(!growth_shell_enabled());
21349 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
21350 shelled.scores(&xo, &mut b);
21351 assert_eq!(bits(&a), bits(&b));
21352 set_growth_shell(None);
21353 let short = Resonance {
21356 shell: vec![f32::INFINITY, f32::INFINITY],
21357 ..shelled
21358 };
21359 set_growth_shell(Some(true));
21360 short.scores(&xo, &mut b);
21361 assert_eq!(bits(&a), bits(&b));
21362 set_growth_shell(None);
21363 }
21364
21365 #[test]
21369 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
21370 let zero = || DenseFfn {
21371 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21372 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21373 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21374 act: Act::Silu,
21375 down_t: None,
21376 segs: Vec::new(),
21377 };
21378 let moe = |sigmoid: bool| MoeFfn {
21379 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
21380 experts: vec![zero(), zero(), zero(), zero()],
21381 top_k: 1,
21382 norm_topk_prob: true,
21383 router_sigmoid: sigmoid,
21384 expert_bias: None,
21385 routed_scaling: 1.0,
21386 route_tau: None,
21387 shared: None,
21388 stats: std::cell::RefCell::new(Vec::new()),
21389 act_sq: std::cell::RefCell::new(Vec::new()),
21390 act_rows: std::cell::RefCell::new(Vec::new()),
21391 mask: None,
21392 per_expert_scale: None,
21393 router_input_norm: false,
21394 resonance: None,
21395 grown: Vec::new(),
21396 };
21397 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
21398 for sigmoid in [false, true] {
21399 let m = moe(sigmoid);
21400 let (idx, p, wsum) = moe_route(&logits, &m, None);
21401 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
21402 assert_eq!(p[1], 0.0);
21403 assert_eq!(p[3], 0.0);
21404 assert!(p[2] > p[0] && p[0] > 0.0);
21405 assert!(p.iter().all(|v| v.is_finite()));
21406 let w = p[2] / wsum;
21407 if sigmoid {
21408 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
21410 } else {
21411 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
21412 }
21413 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
21416 assert_eq!(idx, vec![2]);
21417 }
21418 let m = moe(false);
21420 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
21421 assert_eq!(idx, vec![3]);
21422 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
21425 assert_eq!(idx, vec![0]);
21426 assert!(p.iter().all(|&v| v == 0.25));
21427 assert!(wsum.is_finite() && wsum > 0.0);
21428 }
21429
21430 #[test]
21435 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
21436 let zero = || DenseFfn {
21437 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21438 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21439 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
21440 act: Act::Silu,
21441 down_t: None,
21442 segs: Vec::new(),
21443 };
21444 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
21445 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
21446 experts: vec![zero(), zero(), zero()],
21447 top_k: 1,
21448 norm_topk_prob: norm_topk,
21449 router_sigmoid: false,
21450 expert_bias: None,
21451 routed_scaling: 1.0,
21452 route_tau: None,
21453 shared: None,
21454 stats: std::cell::RefCell::new(Vec::new()),
21455 act_sq: std::cell::RefCell::new(Vec::new()),
21456 act_rows: std::cell::RefCell::new(Vec::new()),
21457 mask: None,
21458 per_expert_scale: None,
21459 router_input_norm: false,
21460 resonance: resonant.then(|| Resonance {
21461 mu: vec![0.0; 6],
21462 u: Vec::new(),
21463 k: 0,
21464 bias: vec![0.0; 3],
21465 shell: Vec::new(),
21466 }),
21467 grown: Vec::new(),
21468 };
21469 let lo = -0.1f32;
21472 let hi = f32::from_bits(lo.to_bits() - 1);
21473 assert!(hi > lo && hi - lo < 2f32.powi(-25));
21474 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
21475 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
21477 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
21478 for norm in [true, false] {
21481 let m = moe(true, norm);
21482 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
21483 assert_eq!(idx, vec![1], "norm_topk {norm}");
21484 assert_eq!(p, vec![0.0, 1.0, 0.0]);
21485 assert_eq!(p[1] / wsum, 1.0);
21486 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
21489 assert_eq!(idx, vec![0]);
21490 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
21492 assert_eq!(idx, vec![2]);
21493 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
21494 assert_eq!(idx, vec![0]);
21495 assert_eq!(p[0] / wsum, 1.0);
21496 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
21499 assert_eq!(idx, vec![0]);
21500 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
21501 }
21502 }
21503}