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 | Self::Situ { .. } => None,
540 }
541 }
542}
543
544pub struct DenseFfn {
546 pub gate_proj: QTensor,
547 pub up_proj: QTensor,
548 pub down_proj: QTensor,
549 pub act: Act,
551 pub down_t: Option<QTensor>,
557 pub segs: Vec<FfnSeg>,
564}
565
566pub struct FfnSeg {
571 pub gate: QTensor,
572 pub up: QTensor,
573 pub down: QTensor,
574 pub start: usize,
575 pub width: usize,
576}
577
578pub enum FfnKind {
581 Dense(DenseFfn),
582 Moe(MoeFfn),
586 DenseMoe(Box<DenseMoeFfn>),
593}
594
595pub struct DenseMoeFfn {
597 pub dense: DenseFfn,
598 pub moe: MoeFfn,
599 pub post_norm_1: Vec<f32>,
601 pub pre_norm_2: Vec<f32>,
604 pub post_norm_2: Vec<f32>,
606}
607
608pub struct MoeFfn {
609 pub router: QTensor,
611 pub experts: Vec<DenseFfn>,
612 pub top_k: usize,
613 pub norm_topk_prob: bool,
614 pub router_sigmoid: bool,
617 pub expert_bias: Option<Vec<f32>>,
621 pub routed_scaling: f32,
624 pub route_tau: Option<f32>,
630 pub shared: Option<(DenseFfn, Option<QTensor>)>,
633 pub stats: std::cell::RefCell<Vec<u64>>,
637 pub act_sq: std::cell::RefCell<Vec<f64>>,
644 pub act_rows: std::cell::RefCell<Vec<f32>>,
650 pub mask: Option<Vec<bool>>,
655 pub per_expert_scale: Option<Vec<f32>>,
658 pub router_input_norm: bool,
662 pub resonance: Option<Resonance>,
666 pub grown: Vec<GrownExpert>,
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
676pub struct GrownExpert {
677 pub record: String,
679 pub record_index: usize,
681 pub layer: usize,
682 pub expert: usize,
686}
687
688pub struct Resonance {
690 pub mu: Vec<f32>,
692 pub u: Vec<f32>,
694 pub k: usize,
695 pub bias: Vec<f32>,
697 pub shell: Vec<f32>,
703}
704
705static GROWTH_SHELL: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
708
709pub fn growth_shell_enabled() -> bool {
714 use std::sync::atomic::Ordering;
715 match GROWTH_SHELL.load(Ordering::Relaxed) {
716 1 => true,
717 2 => false,
718 _ => {
719 let off = std::env::var("CMF_GROWTH_SHELL")
720 .map(|v| v.eq_ignore_ascii_case("off") || v == "0")
721 .unwrap_or(false);
722 GROWTH_SHELL.store(if off { 2 } else { 1 }, Ordering::Relaxed);
723 !off
724 }
725 }
726}
727
728pub fn set_growth_shell(on: Option<bool>) {
733 GROWTH_SHELL.store(
734 match on {
735 Some(true) => 1,
736 Some(false) => 2,
737 None => 0,
738 },
739 std::sync::atomic::Ordering::Relaxed,
740 );
741}
742
743impl Resonance {
744 pub fn has_shell(&self) -> bool {
746 self.shell.iter().any(|s| s.is_finite())
747 }
748
749 pub fn effective_shell(&self, ne: usize) -> Vec<f32> {
752 let mut out = vec![f32::INFINITY; ne];
753 if growth_shell_enabled() {
754 for (o, s) in out.iter_mut().zip(&self.shell) {
755 *o = *s;
756 }
757 }
758 out
759 }
760
761 pub fn scores(&self, x: &[f32], out: &mut [f32]) {
766 let h = x.len();
767 let ne = out.len();
768 let shell_on = growth_shell_enabled() && !self.shell.is_empty();
769 for e in 0..ne {
770 let mu = &self.mu[e * h..(e + 1) * h];
771 let mut d2 = 0.0f32;
772 for j in 0..h {
773 let d = x[j] - mu[j];
774 d2 += d * d;
775 }
776 let mut proj = 0.0f32;
777 for i in 0..self.k {
778 let u = &self.u[(e * self.k + i) * h..(e * self.k + i + 1) * h];
779 let mut p = 0.0f32;
780 for j in 0..h {
781 p += (x[j] - mu[j]) * u[j];
782 }
783 proj += p * p;
784 }
785 let err = d2 - proj;
786 out[e] = self.bias.get(e).copied().unwrap_or(0.0) - err;
787 if shell_on && err > self.shell.get(e).copied().unwrap_or(f32::INFINITY) {
788 out[e] = f32::NEG_INFINITY;
789 }
790 }
791 }
792}
793
794pub enum AttnKind {
797 Full {
799 wq: QTensor,
800 wk: QTensor,
801 wv: QTensor,
802 wo: QTensor,
803 q_norm: Option<Vec<f32>>,
804 k_norm: Option<Vec<f32>>,
805 output_gate: bool,
806 softplus_gate: Option<(QTensor, bool)>,
810 bias: Option<(Vec<f32>, Vec<f32>, Vec<f32>)>,
812 },
813 Linear(VmfPhaseWeights),
815 LinearGdn(GdnWeights),
817 ShortConv(ShortConvWeights),
820 Mla(Box<MlaWeights>),
828 Kda(Box<crate::linear_core::KdaWeights>),
832 Bounded(Box<crate::bounded::BoundedWeights>),
837}
838
839pub struct MlaWeights {
841 pub q_proj: QTensor,
845 pub q_a: Option<QTensor>,
848 pub q_a_norm: Option<Vec<f32>>,
849 pub kv_a: QTensor,
851 pub kv_a_norm: Vec<f32>,
853 pub kv_b: QTensor,
855 pub o_proj: QTensor,
857 pub nh: usize,
858 pub qk_rope: usize,
859 pub qk_nope: usize,
860 pub v_dim: usize,
861 pub lora: usize,
862 pub scale: f32,
864 pub nope: bool,
866}
867
868pub struct MtpModule {
873 pub enorm: Vec<f32>,
874 pub hnorm: Vec<f32>,
875 pub eh_proj: QTensor,
877 pub layer: LayerWeights,
878 pub final_norm: Vec<f32>,
879 pub kv: crate::kv_cache::LayerKvCache,
880}
881
882#[cfg(target_os = "macos")]
889enum MetalRowsItem<'a> {
890 Gdn {
891 run: Vec<crate::gpu_metal::GdnGpuLayer<'a>>,
892 first: usize,
893 },
894 Attn {
895 l: crate::gpu_metal::AttnGpuLayer<'a>,
896 li: usize,
897 q_norm: Option<&'a [f32]>,
898 k_norm: Option<&'a [f32]>,
899 output_gate: bool,
900 },
901}
902
903#[cfg(target_os = "macos")]
904struct MetalVerifyPending {
905 graph: crate::gpu_metal::VerifyGraph,
906 gdn_layers: Vec<usize>,
907 attn_layers: Vec<(usize, usize)>,
908}
909
910#[cfg(target_os = "macos")]
914struct MetalWarmPending {
915 graph: crate::gpu_metal::VerifyGraph,
916 cpu_stored: usize,
917 b: usize,
918}
919
920#[cfg(target_os = "macos")]
921enum MetalRowsRun {
922 Declined,
924 Failed,
927 Completed(MetalVerifyPending),
928}
929
930#[cfg(target_os = "macos")]
931enum MetalPrefillOutcome {
932 Declined,
933 Failed,
934 Completed(Vec<f32>),
935}
936
937#[cfg(target_os = "macos")]
938enum MetalBatchNllOutcome {
939 Declined,
940 Failed(String),
941 Completed(f64, usize),
942}
943
944#[derive(Clone, Copy)]
948enum SpecTrial {
949 Spec {
950 t0: std::time::Instant,
951 gen0: usize,
952 rounds: usize,
953 },
954 Plain {
955 t0: std::time::Instant,
956 gen0: usize,
957 },
958 Decided {
959 spec: bool,
960 recheck_at: usize,
961 },
962}
963
964pub(crate) fn spec_time_level() -> u8 {
968 static L: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
969 *L.get_or_init(|| match std::env::var("CMF_GRAPH_SPEC_TIME") {
970 Ok(v) => v.trim().parse::<u8>().map(|n| n.max(1)).unwrap_or(1),
971 Err(_) => 0,
972 })
973}
974
975struct SpecStampLog {
981 t_last: std::time::Instant,
982 items: Vec<(&'static str, f32)>,
983}
984
985static SPEC_STAMPS: std::sync::Mutex<Option<SpecStampLog>> = std::sync::Mutex::new(None);
986
987pub(crate) fn spec_stamp(name: &'static str) {
988 if spec_time_level() == 0 {
989 return;
990 }
991 if let Ok(mut g) = SPEC_STAMPS.lock() {
992 if let Some(log) = g.as_mut() {
993 let now = std::time::Instant::now();
994 log.items
995 .push((name, (now - log.t_last).as_secs_f32() * 1e3));
996 log.t_last = now;
997 }
998 }
999}
1000
1001fn spec_stamps_begin() {
1002 if spec_time_level() == 0 {
1003 return;
1004 }
1005 if let Ok(mut g) = SPEC_STAMPS.lock() {
1006 *g = Some(SpecStampLog {
1007 t_last: std::time::Instant::now(),
1008 items: Vec::with_capacity(64),
1009 });
1010 }
1011}
1012
1013fn spec_stamps_take() -> Vec<(&'static str, f32)> {
1014 SPEC_STAMPS
1015 .lock()
1016 .ok()
1017 .and_then(|mut g| g.take())
1018 .map(|l| l.items)
1019 .unwrap_or_default()
1020}
1021
1022fn spec_stamps_format(items: &[(&'static str, f32)]) -> String {
1025 let mut agg: Vec<(&'static str, f32, u32)> = Vec::with_capacity(items.len());
1026 for &(n, ms) in items {
1027 match agg.iter_mut().find(|e| e.0 == n) {
1028 Some(e) => {
1029 e.1 += ms;
1030 e.2 += 1;
1031 }
1032 None => agg.push((n, ms, 1)),
1033 }
1034 }
1035 let mut s = String::with_capacity(agg.len() * 16);
1036 for (n, ms, k) in agg {
1037 if k > 1 {
1038 s.push_str(&format!("{n} {ms:.1}/{k} "));
1039 } else {
1040 s.push_str(&format!("{n} {ms:.1} "));
1041 }
1042 }
1043 s
1044}
1045
1046#[derive(Default, Clone, Copy)]
1068struct SpecMon {
1069 round_ms: f64,
1070 tokens: f64,
1071 plain_ms: f64,
1072 n: u32,
1073 fails: u32,
1074 metal: bool,
1075}
1076
1077const SPEC_PROXY_TOKENS: f64 = 3.5;
1080const SPEC_PLAIN_MIN_MS: f64 = 200.0;
1083
1084impl SpecMon {
1085 fn round(&mut self, dt_ms: f64, produced: usize) {
1086 self.n += 1;
1087 if self.n == 1 {
1088 return; }
1090 let a = if self.n == 2 { 1.0 } else { 0.3 };
1091 self.round_ms += a * (dt_ms - self.round_ms);
1092 self.tokens += a * (produced as f64 - self.tokens);
1093 }
1094 fn pays(&self) -> bool {
1095 if self.plain_ms > 0.0 {
1096 self.tokens * self.plain_ms > self.round_ms * 1.03
1097 } else {
1098 self.metal && self.tokens >= SPEC_PROXY_TOKENS
1099 }
1100 }
1101 fn plain_done(&self, t0: std::time::Instant, gen0: usize, generated: usize) -> bool {
1103 let n = generated.saturating_sub(gen0);
1104 if n >= 8 {
1105 return true;
1106 }
1107 self.metal && n >= 2 && t0.elapsed().as_secs_f64() * 1e3 >= SPEC_PLAIN_MIN_MS
1108 }
1109}
1110
1111pub const KV_PREFIX_TAIL: usize = 128;
1114
1115#[derive(Debug, Clone, Default)]
1121pub struct KvPrefix {
1122 len: usize,
1123 hash: u64,
1124 tail: Vec<u32>,
1125 device: bool,
1130}
1131
1132impl KvPrefix {
1133 #[inline]
1134 fn fold(mut h: u64, ids: &[u32]) -> u64 {
1135 for &id in ids {
1136 h ^= id as u64;
1137 h = h.wrapping_mul(0x100000001b3);
1138 h ^= h >> 29;
1139 }
1140 h
1141 }
1142
1143 pub fn clear(&mut self) {
1144 self.len = 0;
1145 self.hash = 0xcbf29ce484222325;
1146 self.tail.clear();
1147 self.device = false;
1148 }
1149
1150 pub fn on_device(&self) -> bool {
1152 self.device
1153 }
1154
1155 pub fn set_on_device(&mut self, device: bool) {
1157 self.device = device;
1158 }
1159
1160 pub fn len(&self) -> usize {
1162 self.len
1163 }
1164
1165 pub fn is_empty(&self) -> bool {
1166 self.len == 0
1167 }
1168
1169 pub fn tail_len(&self) -> usize {
1171 self.tail.len()
1172 }
1173
1174 pub fn set(&mut self, ids: &[u32]) {
1176 self.clear();
1177 self.extend(ids);
1178 }
1179
1180 pub fn extend(&mut self, more: &[u32]) {
1182 if self.len == 0 && self.hash == 0 {
1183 self.hash = 0xcbf29ce484222325;
1184 }
1185 self.hash = Self::fold(self.hash, more);
1186 self.len += more.len();
1187 if more.len() >= KV_PREFIX_TAIL {
1188 self.tail.clear();
1189 self.tail.extend_from_slice(&more[more.len() - KV_PREFIX_TAIL..]);
1190 } else {
1191 let drop = (self.tail.len() + more.len()).saturating_sub(KV_PREFIX_TAIL);
1192 self.tail.drain(..drop);
1193 self.tail.extend_from_slice(more);
1194 }
1195 }
1196
1197 pub fn extension(&self, ids: &[u32]) -> usize {
1201 if self.len == 0 || ids.len() <= self.len {
1202 return 0;
1203 }
1204 let t = self.tail.len();
1205 if ids[self.len - t..self.len] != self.tail[..] {
1206 return 0;
1207 }
1208 if Self::fold(0xcbf29ce484222325, &ids[..self.len]) != self.hash {
1209 return 0;
1210 }
1211 self.len
1212 }
1213}
1214
1215pub struct GenerateResult {
1217 pub text: String,
1218 pub token_ids: Vec<u32>,
1219 pub prompt_tokens: usize,
1220 pub tokens_generated: usize,
1221 pub finish_reason: String,
1222 pub mtp_drafted: usize,
1224 pub mtp_accepted: usize,
1225 pub token_confidence: Vec<f32>,
1230 pub traces: Vec<TokenTrace>,
1233}
1234
1235#[derive(Clone, Debug)]
1240pub struct TokenTrace {
1241 pub t: usize,
1243 pub token_id: u32,
1245 pub confidence: f32,
1247 pub active_skill: Option<String>,
1249 pub recon: Option<f32>,
1253 pub switched: bool,
1256}
1257
1258#[cfg_attr(not(test), allow(dead_code))]
1263fn top1_prob_t(logits: &[f32], id: u32, temp: f32) -> f32 {
1264 let t = if temp > 1e-3 { temp } else { 1.0 };
1265 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1266 let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1267 if sum > 0.0 {
1268 (((logits[id as usize] - max) / t).exp()) / sum
1269 } else {
1270 0.0
1271 }
1272}
1273
1274fn prefill_batched() -> bool {
1277 std::env::var("CMF_PREFILL")
1278 .map(|v| v != "seq")
1279 .unwrap_or(true)
1280}
1281
1282#[inline]
1286fn nll_graph_policy(
1287 unmasked: bool,
1288 prefer_graph: bool,
1289 native_metal: bool,
1290) -> (bool, bool) {
1291 let graph_quality = unmasked && prefer_graph;
1292 let fused_head_quality = graph_quality && native_metal;
1293 (graph_quality, fused_head_quality)
1294}
1295
1296#[derive(Clone, Copy)]
1300enum PrefillIn<'a> {
1301 Ids(&'a [u32]),
1302 Hidden(&'a [f32]),
1303}
1304
1305impl Pipeline {
1312 fn can_prefill_batched(&self) -> bool {
1313 #[cfg(test)]
1314 let force_serial = self.nll_test_force_serial;
1315 #[cfg(not(test))]
1316 let force_serial = false;
1317 prefill_batched() && !force_serial && !self.weights.layers.is_empty()
1318 }
1319
1320 fn automatic_gpu_prefix(&self) -> Option<usize> {
1323 let (model, _, _, _) = self.weights.embed_tokens.graph_weight()?;
1324 crate::gpu::automatic_layer_prefix(&model, self.num_layers, self.physical_layers)
1325 }
1326
1327 pub fn prefill_chunk(&self) -> usize {
1331 let env = env_prefill_chunk();
1332 if env.is_some() || ChunkHost::here() != ChunkHost::Other {
1333 return prefill_chunk_rule(env, ChunkHost::here(), false);
1334 }
1335 prefill_chunk_rule(None, ChunkHost::Other, self.chunk_stack_facts().dense_on_discrete())
1336 }
1337
1338 fn chunk_stack_facts(&self) -> ChunkStackFacts {
1339 let plain_dense = !self.weights.layers.is_empty()
1340 && self.g3n.is_none()
1341 && self.dsv4.is_none()
1342 && self.dsv41.is_none()
1343 && self.qwen4_exp.is_none()
1344 && self.weights.layers.iter().all(|lw| {
1345 matches!(lw.attn, AttnKind::Full { .. }) && matches!(lw.ffn, FfnKind::Dense(_))
1346 });
1347 let gpu_on = crate::gpu::enabled();
1348 ChunkStackFacts {
1349 plain_dense,
1350 discrete: gpu_on && crate::gpu::discrete(),
1351 gpu_on,
1352 capacity_split: std::env::var_os("CMF_GPU_LAYERS").is_some()
1355 || (plain_dense && gpu_on && self.automatic_gpu_prefix().is_some()),
1356 multi_gpu: self.gpu_plan.is_some(),
1357 o1: self.o1_active(),
1358 }
1359 }
1360}
1361
1362pub fn prefill_chunk() -> usize {
1371 prefill_chunk_rule(env_prefill_chunk(), ChunkHost::here(), false)
1372}
1373
1374fn env_prefill_chunk() -> Option<usize> {
1375 std::env::var("CMF_PREFILL_CHUNK")
1376 .ok()
1377 .and_then(|v| v.parse::<usize>().ok())
1378}
1379
1380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1382enum ChunkHost {
1383 Macos,
1384 Aarch64,
1386 Other,
1388}
1389
1390impl ChunkHost {
1391 fn here() -> Self {
1392 if cfg!(target_os = "macos") {
1393 ChunkHost::Macos
1394 } else if cfg!(target_arch = "aarch64") {
1395 ChunkHost::Aarch64
1396 } else {
1397 ChunkHost::Other
1398 }
1399 }
1400}
1401
1402const DISCRETE_DENSE_PREFILL_CHUNK: usize = 512;
1409
1410fn prefill_chunk_rule(env: Option<usize>, host: ChunkHost, dense_on_discrete: bool) -> usize {
1416 if let Some(n) = env {
1417 return n.max(1);
1418 }
1419 match host {
1420 ChunkHost::Macos => 512,
1421 ChunkHost::Aarch64 => 256,
1424 ChunkHost::Other if dense_on_discrete => DISCRETE_DENSE_PREFILL_CHUNK,
1425 ChunkHost::Other => 48,
1426 }
1427}
1428
1429#[derive(Clone, Copy, Debug, Default)]
1431struct ChunkStackFacts {
1432 plain_dense: bool,
1435 discrete: bool,
1437 gpu_on: bool,
1439 capacity_split: bool,
1441 multi_gpu: bool,
1443 o1: bool,
1445}
1446
1447impl ChunkStackFacts {
1448 fn dense_on_discrete(self) -> bool {
1449 self.plain_dense
1450 && self.discrete
1451 && self.gpu_on
1452 && !self.capacity_split
1453 && !self.multi_gpu
1454 && !self.o1
1455 }
1456}
1457
1458#[inline]
1464fn mtp_prefill_pair_count(start: usize, end: usize, input_len: usize) -> usize {
1465 if end <= start || start >= input_len {
1466 return 0;
1467 }
1468 let rows = (end.min(input_len) - start).min(input_len - start);
1469 if end < input_len {
1470 rows
1471 } else {
1472 rows.saturating_sub(1)
1473 }
1474}
1475
1476pub type TokenCallback = Box<dyn FnMut(&str) -> bool + Send>;
1478
1479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481pub(crate) struct ReuseLayer {
1482 pub full: bool,
1485 pub host_rows: usize,
1488 pub device_rows: Option<usize>,
1490 pub device_state: bool,
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Eq)]
1496pub(crate) enum ReusePlan {
1497 Ready,
1499 Pull(Vec<(usize, usize, usize)>),
1502 Fresh,
1504}
1505
1506pub(crate) fn kv_reuse_plan(reuse_from: usize, layers: &[ReuseLayer]) -> ReusePlan {
1516 let mut pulls = Vec::new();
1517 for (li, l) in layers.iter().enumerate() {
1518 if !l.full {
1519 if l.device_state {
1520 return ReusePlan::Fresh;
1521 }
1522 continue;
1523 }
1524 if l.host_rows == reuse_from {
1525 continue;
1526 }
1527 if l.host_rows < reuse_from && l.device_rows.is_some_and(|d| d >= reuse_from) {
1528 pulls.push((li, l.host_rows, reuse_from));
1529 continue;
1530 }
1531 return ReusePlan::Fresh;
1532 }
1533 if pulls.is_empty() {
1534 ReusePlan::Ready
1535 } else {
1536 ReusePlan::Pull(pulls)
1537 }
1538}
1539
1540impl Pipeline {
1541 fn clear_sequence_state(&mut self) {
1549 #[cfg(target_os = "macos")]
1552 let _ = crate::gpu_metal::wait_replay();
1553 self.kv_cache.clear();
1554 self.clear_history();
1557 self.graph_logits = None;
1558 if let Some(b) = &mut self.dsv41 {
1559 b.3.clear();
1560 }
1561 crate::gpu::graph_kv_reset(self.graph_kv_id);
1562 crate::gpu::graph_kv_reset(self.mtp_kv_id());
1567 }
1568
1569 fn prepare_kv_reuse(&mut self, reuse_from: usize) -> bool {
1576 if self.graph_prefill_preferred() {
1577 return true;
1578 }
1579 let kv_id = self.graph_kv_id;
1580 let layers: Vec<ReuseLayer> = (0..self.num_layers)
1581 .map(|li| {
1582 let full = matches!(
1583 self.weights.layers[self.phys_layer(li)].attn,
1584 AttnKind::Full { .. }
1585 );
1586 ReuseLayer {
1587 full,
1588 host_rows: self.kv_cache.layers[li].pos_len(),
1591 device_rows: crate::gpu::graph_kv_stored(kv_id, li),
1592 device_state: crate::gpu::graph_state_resident(kv_id, li),
1593 }
1594 })
1595 .collect();
1596 if layers
1600 .iter()
1601 .all(|l| l.device_rows.is_none() && !l.device_state)
1602 {
1603 return true;
1604 }
1605 let plan = kv_reuse_plan(reuse_from, &layers);
1606 let (what, rows, n) = match &plan {
1607 ReusePlan::Ready => ("host ready", 0, 0),
1608 ReusePlan::Fresh => ("fresh", 0, 0),
1609 ReusePlan::Pull(p) => (
1610 "pull",
1611 p.iter().map(|&(_, a, b)| b - a).max().unwrap_or(0),
1612 p.len(),
1613 ),
1614 };
1615 let t0 = std::time::Instant::now();
1616 let ok = self.apply_kv_reuse_plan(reuse_from, plan, &layers);
1617 if std::env::var("CMF_PREFILL_PROF").is_ok() {
1618 eprintln!(
1619 "kv-reuse: {what}{}: {rows} device row(s) × {n} layer(s) to the host in {:.2} ms",
1620 if ok { "" } else { " (failed → fresh)" },
1621 t0.elapsed().as_secs_f64() * 1e3
1622 );
1623 }
1624 ok
1625 }
1626
1627 fn apply_kv_reuse_plan(
1628 &mut self,
1629 reuse_from: usize,
1630 plan: ReusePlan,
1631 layers: &[ReuseLayer],
1632 ) -> bool {
1633 let kv_id = self.graph_kv_id;
1634 match plan {
1635 ReusePlan::Fresh => return false,
1636 ReusePlan::Ready => {}
1637 ReusePlan::Pull(pulls) => {
1638 let (nkv, hd) = {
1643 let c = &self.kv_cache.layers[pulls[0].0];
1644 (c.num_kv_heads, c.head_dim)
1645 };
1646 let uniform = pulls.iter().all(|&(li, _, _)| {
1647 let c = &self.kv_cache.layers[li];
1648 (c.num_kv_heads, c.head_dim) == (nkv, hd)
1649 });
1650 let batched = if uniform {
1651 crate::gpu::graph_kv_read_rows(kv_id, &pulls, nkv, hd)
1652 } else {
1653 None
1654 };
1655 let rows: Vec<(Vec<f32>, Vec<f32>)> = match batched {
1656 Some(rows) => rows,
1657 None => {
1658 let mut rows = Vec::with_capacity(pulls.len());
1659 for &(li, from, to) in &pulls {
1660 let (lnkv, lhd) = {
1661 let c = &self.kv_cache.layers[li];
1662 (c.num_kv_heads, c.head_dim)
1663 };
1664 let Some((k, v, first_valid)) =
1665 crate::gpu::graph_kv_pull_host(kv_id, li, from, to, lnkv, lhd)
1666 else {
1667 return false;
1668 };
1669 let need_from = match self.layer_window(li) {
1673 Some(w) => from.max((to + 1).saturating_sub(w)),
1674 None => from,
1675 };
1676 if first_valid > need_from {
1677 return false;
1678 }
1679 rows.push((k, v));
1680 }
1681 rows
1682 }
1683 };
1684 for ((li, from, to), (k, v)) in pulls.into_iter().zip(rows) {
1685 let cache = &mut self.kv_cache.layers[li];
1686 let row = cache.num_kv_heads * cache.head_dim;
1687 for p in 0..to - from {
1688 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
1689 }
1690 if cache.pos_len() != to {
1691 return false;
1692 }
1693 }
1694 }
1695 }
1696 for (li, l) in layers.iter().enumerate() {
1700 if l.full
1701 && l.device_rows.is_some_and(|d| d > reuse_from)
1702 && !crate::gpu::graph_kv_set_stored(kv_id, li, reuse_from)
1703 {
1704 return false;
1705 }
1706 }
1707 true
1708 }
1709
1710 fn finish_generation(
1716 &mut self,
1717 mtp: &mut Option<MtpModule>,
1718 router: &mut Option<crate::swarm::DynRouter>,
1719 clear_sequence: bool,
1720 ) {
1721 if router.is_some() {
1725 let _ = self.set_active_skill(None);
1726 }
1727 #[cfg(target_os = "macos")]
1734 let clear_sequence = clear_sequence || !crate::gpu_metal::wait_replay();
1735 if clear_sequence {
1736 self.clear_sequence_state();
1737 if let Some(m) = mtp.as_mut() {
1738 m.kv.clear();
1744 }
1745 if let Some(m) = self.mtp.as_mut() {
1746 m.kv.clear();
1750 }
1751 }
1752 self.graph_want_logits = false;
1753 self.graph_head_required = false;
1754 self.graph_logits = None;
1755 self.graph_failed
1756 .store(false, std::sync::atomic::Ordering::Relaxed);
1757 self.cancel
1758 .store(false, std::sync::atomic::Ordering::Relaxed);
1759 self.dyn_router = router.take().or(self.dyn_router.take());
1760 self.mtp = mtp.take().or(self.mtp.take());
1761 self.mtp_graph_mode = None;
1762 self.spec_forced = None;
1763 }
1764
1765 fn check_forward_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1769 if self
1770 .graph_failed
1771 .swap(false, std::sync::atomic::Ordering::Relaxed)
1772 {
1773 self.cancel
1774 .store(false, std::sync::atomic::Ordering::Relaxed);
1775 self.clear_sequence_state();
1776 self.graph_logits = None;
1777 self.graph_want_logits = false;
1778 self.graph_head_required = false;
1779 return Err(format!("GPU graph failed during {phase} at position {pos}"));
1780 }
1781 Ok(())
1782 }
1783
1784 #[cfg(target_os = "macos")]
1785 fn fail_metal_graph(&mut self, reason: &str) {
1786 crate::pipeline::METAL_GRAPH_ERRORS
1787 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1788 self.clear_sequence_state();
1789 self.graph_logits = None;
1790 self.graph_failed
1791 .store(true, std::sync::atomic::Ordering::Relaxed);
1792 self.cancel
1793 .store(true, std::sync::atomic::Ordering::Relaxed);
1794 tracing::error!("native Metal TokenGraph failed closed: {reason}");
1795 }
1796
1797 fn nll_begin(&mut self) -> Result<(), String> {
1802 if self
1803 .graph_failed
1804 .swap(false, std::sync::atomic::Ordering::Relaxed)
1805 {
1806 self.cancel
1807 .store(false, std::sync::atomic::Ordering::Relaxed);
1808 self.clear_sequence_state();
1809 self.graph_logits = None;
1810 self.graph_want_logits = false;
1811 self.graph_head_required = false;
1812 return Err("GPU graph failed before NLL scoring".to_string());
1813 }
1814 self.clear_sequence_state();
1815 self.graph_logits = None;
1816 self.graph_want_logits = false;
1817 self.graph_head_required = false;
1818 Ok(())
1819 }
1820
1821 fn nll_end(&mut self) {
1825 self.clear_sequence_state();
1826 self.graph_logits = None;
1827 self.graph_want_logits = false;
1828 self.graph_head_required = false;
1829 self.graph_failed
1830 .store(false, std::sync::atomic::Ordering::Relaxed);
1831 }
1832
1833 fn nll_check_graph(&mut self, phase: &str, pos: usize) -> Result<(), String> {
1836 #[cfg(test)]
1837 if self.nll_test_fail_at == Some(pos) {
1838 self.nll_test_fail_at = None;
1839 self.graph_failed
1840 .store(true, std::sync::atomic::Ordering::Relaxed);
1841 self.cancel
1842 .store(true, std::sync::atomic::Ordering::Relaxed);
1843 }
1844 if self
1845 .graph_failed
1846 .swap(false, std::sync::atomic::Ordering::Relaxed)
1847 {
1848 self.cancel
1849 .store(false, std::sync::atomic::Ordering::Relaxed);
1850 self.clear_sequence_state();
1851 self.graph_logits = None;
1852 self.graph_want_logits = false;
1853 return Err(format!(
1854 "GPU graph failed during NLL {phase} at position {pos}"
1855 ));
1856 }
1857 Ok(())
1858 }
1859
1860 #[inline]
1864 pub fn phys_layer(&self, virtual_idx: usize) -> usize {
1865 virtual_idx % self.physical_layers
1866 }
1867
1868 #[inline]
1871 pub fn is_loop_end(&self, virtual_idx: usize) -> bool {
1872 self.loop_final_norm && (virtual_idx + 1) % self.physical_layers == 0
1873 }
1874
1875 #[allow(clippy::too_many_arguments)]
1877
1878 #[cfg(target_os = "macos")]
1897 fn graph_prefill_preferred(&self) -> bool {
1898 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
1899 if !crate::gpu::enabled_here()
1900 || !graph_force
1901 || std::env::var("CMF_GPU_BLOCK")
1902 .map(|v| v == "0")
1903 .unwrap_or(false)
1904 || std::env::var("CMF_PREFILL_GRAPH").as_deref() == Ok("0")
1907 {
1908 return false;
1909 }
1910 self.weights
1911 .layers
1912 .iter()
1913 .any(|lw| {
1914 matches!(&lw.attn, AttnKind::LinearGdn(w) if w.in_proj_qkv.metal_graph_parts().is_some())
1915 })
1916 }
1917
1918 #[cfg(not(target_os = "macos"))]
1925 fn batch_prefix_prefill(&self) -> bool {
1926 let forced = match std::env::var("CMF_BATCH_PREFIX").as_deref() {
1927 Ok("0") => return false,
1928 Ok("1") => true,
1929 _ => false,
1930 };
1931 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
1932 && crate::gpu::enabled_here()
1933 && !self.graph_refused()
1934 && (forced || self.graph_attn_decline_reason().is_some())
1935 && self.wgpu_graph_attn_decline().is_none()
1936 && self.attn_softcap == 0.0
1937 && self
1938 .weights
1939 .layers
1940 .iter()
1941 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
1942 && self.automatic_gpu_prefix().is_some()
1943 }
1944
1945 #[cfg(not(target_os = "macos"))]
1946 fn graph_prefill_preferred(&self) -> bool {
1947 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill);
1955 if !graph_on || !crate::gpu::enabled_here() {
1956 return false;
1957 }
1958 if self.embryo_resident_eligible() {
1962 return crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
1967 }
1968 if self.o1_active() {
1982 return false;
1983 }
1984 if self.wgpu_graph_attn_decline().is_some() {
1988 return false;
1989 }
1990 if self
1991 .weights
1992 .layers
1993 .iter()
1994 .any(|lw| matches!(&lw.attn, AttnKind::LinearGdn(_)))
1995 {
1996 return true;
1997 }
1998 self.weights
2009 .layers
2010 .iter()
2011 .any(|lw| matches!(&lw.ffn, FfnKind::Moe(_)))
2012 && self.automatic_gpu_prefix().is_none()
2013 }
2014
2015 #[cfg(target_os = "macos")]
2016 fn q1_graph_gpu(
2017 &mut self,
2018 start: usize,
2019 upto: Option<usize>,
2020 position: usize,
2021 h: &mut [f32],
2022 ) -> usize {
2023 let _mt0 = std::time::Instant::now(); use crate::gpu::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, MetalFfn, TokenGraph};
2025 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
2026 if self.attn_softcap > 0.0 || !crate::gpu::enabled_here()
2028 || !graph_force
2029 || std::env::var("CMF_GPU_BLOCK")
2030 .map(|v| v == "0")
2031 .unwrap_or(false)
2032 {
2033 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2034 eprintln!(
2035 "block-graph: front gate (softcap={} enabled_here={} graph_force={})",
2036 self.attn_softcap > 0.0,
2037 crate::gpu::enabled_here(),
2038 graph_force,
2039 );
2040 }
2041 if self.graph_head_required {
2042 self.fail_metal_graph("native graph front gate refused");
2043 }
2044 return start;
2045 }
2046 let swa_graph = self.metal_graph_swa();
2053 if (self.swa.is_some() && !swa_graph)
2054 || self.global_attn.is_some()
2055 || self.attention_heads_per_layer.is_some()
2056 || self.attn_v_norm
2057 || (self.graph_attn_decline_reason().is_some() && !swa_graph)
2059 || self.weights.layers.iter().any(|lw| {
2060 lw.attn_out_norm.is_some()
2061 || lw.ffn_out_norm.is_some()
2062 || lw.layer_scale.is_some()
2063 || matches!(&lw.ffn, FfnKind::Dense(d) if !matches!(d.act, Act::Silu | Act::Gelu))
2064 })
2065 {
2066 if let Some(reason) = self.graph_attn_decline_reason().filter(|_| !swa_graph) {
2069 self.note_graph_decline("metal block graph", reason);
2070 }
2071 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2072 eprintln!(
2073 "block-graph: arch ineligible (swa={} gattn={} hpl={} vnorm={} scale_delta={:.2e})",
2074 self.swa.is_some(),
2075 self.global_attn.is_some(),
2076 self.attention_heads_per_layer.is_some(),
2077 self.attn_v_norm,
2078 (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs(),
2079 );
2080 }
2081 if self.graph_head_required {
2082 self.fail_metal_graph("native graph architecture gate refused");
2083 }
2084 return start;
2085 }
2086 let limit = upto
2089 .map(|u| u + 1)
2090 .unwrap_or(self.num_layers)
2091 .min(self.num_layers);
2092
2093 enum Item<'a> {
2094 Gdn {
2095 run: Vec<GdnGpuLayer<'a>>,
2096 first: usize,
2097 },
2098 Attn {
2099 l: AttnGpuLayer<'a>,
2100 li: usize,
2101 q_norm: Option<&'a [f32]>,
2102 k_norm: Option<&'a [f32]>,
2103 output_gate: bool,
2104 proj_gate: Option<(&'a QTensor, bool)>,
2108 head_gate_w: Option<&'a [f32]>,
2111 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
2112 full_gpu: bool,
2115 },
2116 }
2117
2118 let attend_mode = std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into());
2125 let attend_contract = attend_mode != "0"
2126 && attend_mode != "off"
2127 && self.head_dim % 4 == 0
2128 && self.head_dim <= 256
2129 && self.rotary_dim >= 2
2130 && self.rotary_dim <= self.head_dim
2131 && (self.rotary_dim / 2) % 32 == 0
2132 && self.num_kv_heads > 0
2133 && self.num_heads % self.num_kv_heads == 0;
2134
2135 let mut plan: Vec<Item> = Vec::new();
2136 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
2137 let block_diag = std::env::var("CMF_GRAPH_DBG").is_ok();
2139 let mut scan = start;
2140 while scan < limit {
2141 let lw = &self.weights.layers[self.phys_layer(scan)];
2142 let ffn = match &lw.ffn {
2143 FfnKind::Dense(d) if d.segs.is_empty() => {
2144 let (Some(g), Some(u), Some(dn)) = (
2145 d.gate_proj.metal_graph_parts(),
2146 d.up_proj.metal_graph_parts(),
2147 d.down_proj.metal_graph_parts(),
2148 ) else {
2149 if block_diag {
2150 eprintln!(
2151 "block-graph: L{scan} FFN trio not graph-mappable — run ends"
2152 );
2153 }
2154 break;
2155 };
2156 MetalFfn::Dense {
2157 gate: g,
2158 up: u,
2159 down: dn,
2160 gelu: d.act == Act::Gelu,
2162 }
2163 }
2164 FfnKind::Moe(m) => {
2165 let Some(moe) = metal_moe_graph_parts(m, self.hidden_size) else {
2166 if block_diag {
2167 eprintln!(
2168 "block-graph: L{scan} MoE outside the graph contract — run ends"
2169 );
2170 }
2171 break;
2172 };
2173 if let QTensor::Mapped { model, .. } = &m.experts[0].gate_proj {
2174 model_ref.get_or_insert_with(|| model.clone());
2175 }
2176 MetalFfn::Moe(moe)
2177 }
2178 _ => {
2179 if block_diag {
2180 eprintln!("block-graph: L{scan} non-graph FFN — run ends");
2181 }
2182 break;
2183 }
2184 };
2185 match &lw.attn {
2186 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
2187 let parts = (
2188 w.in_proj_qkv.metal_graph_parts(),
2189 w.in_proj_z.metal_graph_parts(),
2190 w.in_proj_a.f32_parts(),
2191 w.in_proj_b.f32_parts(),
2192 w.out_proj.metal_graph_parts(),
2193 );
2194 let (Some(qkv), Some(z), Some(a), Some(b), Some(out)) = parts else {
2195 if block_diag {
2196 eprintln!(
2197 "block-graph: L{scan} GDN parts refused (qkv={} z={} a_f32={} b_f32={} out={})",
2198 w.in_proj_qkv.metal_graph_parts().is_some(),
2199 w.in_proj_z.metal_graph_parts().is_some(),
2200 w.in_proj_a.f32_parts().is_some(),
2201 w.in_proj_b.f32_parts().is_some(),
2202 w.out_proj.metal_graph_parts().is_some(),
2203 );
2204 }
2205 break;
2206 };
2207 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
2208 model_ref.get_or_insert_with(|| model.clone());
2209 }
2210 let gl = GdnGpuLayer {
2211 attn_norm: &lw.input_norm,
2212 post_norm: &lw.post_norm,
2213 qkv,
2214 z,
2215 a,
2216 b,
2217 out,
2218 ffn,
2219 conv1d: &w.conv1d,
2220 a_log: &w.a_log,
2221 dt_bias: &w.dt_bias,
2222 gnorm: &w.norm,
2223 };
2224 match plan.last_mut() {
2225 Some(Item::Gdn { run, .. }) => run.push(gl),
2226 _ => plan.push(Item::Gdn {
2227 run: vec![gl],
2228 first: scan,
2229 }),
2230 }
2231 }
2232 AttnKind::Full {
2233 wq,
2234 wk,
2235 wv,
2236 wo,
2237 q_norm,
2238 k_norm,
2239 output_gate,
2240 softplus_gate,
2241 bias,
2242 } if (!self.kv_cache.layers[scan].o1_sealed()
2243 || std::env::var("CMF_O1_METAL").as_deref() == Ok("1"))
2248 && softplus_gate
2252 .as_ref()
2253 .is_none_or(|(_, per_head)| *per_head && self.proj_gate_sigmoid) =>
2254 {
2255 let parts = (
2256 wq.metal_graph_parts(),
2257 wk.metal_graph_parts(),
2258 wv.metal_graph_parts(),
2259 wo.metal_graph_parts(),
2260 );
2261 let (Some(pq), Some(pk), Some(pv), Some(po)) = parts else {
2262 break;
2263 };
2264 if let QTensor::Mapped { model, .. } = wq {
2265 model_ref.get_or_insert_with(|| model.clone());
2266 }
2267 let cache = &self.kv_cache.layers[scan];
2268 let o1_metal = cache.o1.is_some()
2272 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1")
2273 && cache.o1_views().is_some();
2274 let head_gate_w = softplus_gate
2278 .as_ref()
2279 .and_then(|(g, _)| g.f32_parts())
2280 .filter(|&(_, r, c)| r == self.num_heads && c == self.hidden_size)
2281 .map(|(d, _, _)| d);
2282 let full_gpu = attend_contract
2283 && softplus_gate.is_none() == head_gate_w.is_none()
2284 && cache.mode == crate::kv_cache::KvMode::F32
2285 && (cache.o1.is_none() || o1_metal)
2286 && bias.is_none()
2287 && pq.1 == self.num_heads * self.head_dim * (1 + *output_gate as usize)
2288 && pk.1 == self.num_kv_heads * self.head_dim
2289 && pv.1 == self.num_kv_heads * self.head_dim
2290 && po.2 == self.num_heads * self.head_dim;
2291 plan.push(Item::Attn {
2292 l: AttnGpuLayer {
2293 attn_norm: &lw.input_norm,
2294 post_norm: &lw.post_norm,
2295 wq: pq,
2296 wk: pk,
2297 wv: pv,
2298 wo: po,
2299 ffn,
2300 },
2301 li: scan,
2302 q_norm: q_norm.as_deref(),
2303 k_norm: k_norm.as_deref(),
2304 output_gate: *output_gate,
2305 proj_gate: softplus_gate.as_ref().map(|(g, per_head)| (g, *per_head)),
2306 head_gate_w,
2307 bias: bias
2308 .as_ref()
2309 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
2310 full_gpu,
2311 });
2312 }
2313 _ => break,
2314 }
2315 scan += 1;
2316 }
2317 let Some(model) = model_ref else {
2318 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2319 eprintln!("q1-graph: no model ref (start {start}, scanned to {scan})");
2320 }
2321 if self.graph_head_required {
2322 self.fail_metal_graph("native graph has no mapped model reference");
2323 }
2324 return start;
2325 };
2326 if plan.is_empty() {
2327 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2328 eprintln!("q1-graph: empty plan at layer {start}");
2329 }
2330 if self.graph_head_required {
2331 self.fail_metal_graph("native graph plan is empty");
2332 }
2333 return start;
2334 }
2335 let has_moe = plan.iter().any(|it| match it {
2336 Item::Gdn { run, .. } => run.iter().any(|l| matches!(l.ffn, MetalFfn::Moe(_))),
2337 Item::Attn { l, .. } => matches!(l.ffn, MetalFfn::Moe(_)),
2338 });
2339 let has_gdn = plan.iter().any(|it| matches!(it, Item::Gdn { .. }));
2340 let dev_attend = attend_contract
2341 && (self.head_dim <= 128
2342 || has_moe
2343 || (self.head_dim <= 256 && has_gdn)
2349 || (self.head_dim <= 256 && swa_graph)
2353 || attend_mode == "force"
2354 || attend_mode == "256");
2355 if !dev_attend {
2356 for it in &mut plan {
2357 if let Item::Attn { li, full_gpu, .. } = it {
2358 let keep_o1 = self.kv_cache.layers[*li].o1.is_some()
2361 && std::env::var("CMF_O1_METAL").as_deref() == Ok("1");
2362 if !keep_o1 {
2363 *full_gpu = false;
2364 }
2365 }
2366 }
2367 }
2368 if std::env::var("CMF_GRAPH_DBG").is_ok() {
2369 use std::sync::atomic::{AtomicBool, Ordering};
2370 static SAID: AtomicBool = AtomicBool::new(false);
2371 if !SAID.swap(true, Ordering::Relaxed) {
2372 let fg = plan
2373 .iter()
2374 .filter(|it| matches!(it, Item::Attn { full_gpu: true, .. }))
2375 .count();
2376 let att = plan
2377 .iter()
2378 .filter(|it| matches!(it, Item::Attn { .. }))
2379 .count();
2380 eprintln!(
2381 "q1-graph: plan of {} items from layer {start} to {scan} | dev_attend={dev_attend} full_gpu {fg}/{att} | hd={} rd={} nkv={} nh={}",
2382 plan.len(),
2383 self.head_dim,
2384 self.rotary_dim,
2385 self.num_kv_heads,
2386 self.num_heads,
2387 );
2388 }
2389 }
2390 let dims = GraphDims {
2391 hidden: self.hidden_size,
2392 eps: self.rms_eps as f32,
2393 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2394 };
2395 let Some(mut graph) = TokenGraph::new(&model, dims, h) else {
2396 if self.graph_head_required {
2397 self.fail_metal_graph("native TokenGraph allocation refused");
2398 }
2399 return start;
2400 };
2401 if swa_graph && self.head_dim > 128 {
2402 graph.set_attend_blk_from(64);
2408 }
2409 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
2410 nv: cfg.num_v_heads,
2411 nk: cfg.num_k_heads,
2412 dk: cfg.key_head_dim,
2413 dv: cfg.value_head_dim,
2414 kk: cfg.conv_kernel,
2415 hidden: self.hidden_size,
2416 inter: self.intermediate_size,
2417 c_dim: cfg.conv_dim(),
2418 eps: cfg.rms_eps as f32,
2419 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
2420 });
2421 let mut valid = 0usize;
2425 let mut end = start;
2426 crate::gpu::stageprof(1, _mt0.elapsed()); if std::env::var("CMF_PLAN_DUMP").is_ok() {
2428 static ONCE: std::sync::Once = std::sync::Once::new();
2429 ONCE.call_once(|| {
2430 for it in &plan {
2431 match it {
2432 Item::Gdn { first, run } => {
2433 eprintln!("plan: Gdn first={first} len={}", run.len())
2434 }
2435 Item::Attn { li, full_gpu, .. } => {
2436 eprintln!("plan: Attn li={li} full_gpu={full_gpu}")
2437 }
2438 }
2439 }
2440 });
2441 }
2442 for item in &plan {
2443 let ok = match item {
2444 Item::Gdn { run, .. } => gcfg
2445 .as_ref()
2446 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
2447 .unwrap_or(false),
2448 Item::Attn { l, .. } => graph.attn_ok(l),
2449 };
2450 if !ok {
2451 if block_diag {
2452 eprintln!(
2453 "block-graph: plan item {} ({}) failed graph preflight",
2454 valid,
2455 match item {
2456 Item::Gdn { run, first } => format!("GDN run L{first}+{}", run.len()),
2457 Item::Attn { li, .. } => format!("Attn L{li}"),
2458 }
2459 );
2460 }
2461 break;
2462 }
2463 valid += 1;
2464 end += match item {
2465 Item::Gdn { run, .. } => run.len(),
2466 Item::Attn { .. } => 1,
2467 };
2468 }
2469 plan.truncate(valid);
2470 if plan.is_empty() {
2471 if self.graph_head_required {
2472 self.fail_metal_graph("native graph preflight produced no valid items");
2473 }
2474 return start;
2475 }
2476
2477 if self.graph_head_required && (upto.is_some() || end != self.num_layers) {
2478 self.fail_metal_graph("fused-head NLL requires a complete 64-layer graph");
2479 return start;
2480 }
2481
2482 let one_pass = |t: (usize, usize, usize)| {
2491 use cortiq_core::TensorDtype as D;
2492 matches!(
2493 model.tensors[t.0].dtype,
2494 D::Q4TiledP | D::Q4Tiled | D::Q4Block | D::Q8Row | D::Q8_2f | D::Q1
2495 )
2496 };
2497 let dense_fast = plan.iter().all(|it| match it {
2498 Item::Attn {
2499 l, li, full_gpu, ..
2500 } => {
2501 *full_gpu
2502 && self.kv_cache.layers[*li].o1.is_none()
2503 && [l.wq, l.wk, l.wv, l.wo].into_iter().all(one_pass)
2504 && match l.ffn {
2505 MetalFfn::Dense { gate, up, down, .. } => {
2506 one_pass(gate) && one_pass(up) && one_pass(down)
2507 }
2508 _ => false,
2509 }
2510 }
2511 Item::Gdn { .. } => false,
2512 });
2513 let ab = crate::gpu_metal::dense_ab_arm().filter(|_| dense_fast);
2514 let _mv_fast = match ab {
2515 Some((bits, _)) => {
2516 graph.set_dense_concurrent_raw(bits & crate::gpu_metal::DENSE_CONC != 0);
2517 crate::gpu_metal::MvFastGuard::set_raw(bits)
2518 }
2519 None => {
2520 graph.set_dense_concurrent(dense_fast);
2521 let q8r4 = if swa_graph {
2524 crate::gpu_metal::DENSE_Q8R4
2525 } else {
2526 0
2527 };
2528 crate::gpu_metal::MvFastGuard::set_bits(if dense_fast {
2529 crate::gpu_metal::DENSE_MV | crate::gpu_metal::DENSE_FUSE | q8r4
2530 } else {
2531 0
2532 })
2533 }
2534 };
2535
2536 let pool = self.pool.clone();
2539 let (nh, nkv, hd, hs, eps) = (
2540 self.num_heads,
2541 self.num_kv_heads,
2542 self.head_dim,
2543 self.hidden_size,
2544 self.rms_eps,
2545 );
2546 let norm_style = self.norm_style;
2547 let gemma = norm_style == cortiq_core::NormStyle::Gemma;
2548 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
2549 let kv_id = self.graph_kv_id;
2550 let mut pending: Vec<(usize, usize)> = Vec::new();
2553 let mut dev_attn: Vec<usize> = Vec::new();
2556 for item in &plan {
2557 let _xt0 = std::time::Instant::now();
2558 let _xkind: u32 = match item {
2559 Item::Gdn { .. } => 2,
2560 Item::Attn { .. } => 3,
2561 };
2562 if self.loop_final_norm {
2564 let item_start = match item {
2565 Item::Gdn { first, .. } => *first,
2566 Item::Attn { li, .. } => *li,
2567 };
2568 if item_start > start && self.is_loop_end(item_start - 1) {
2569 graph.encode_loop_norm(&self.weights.final_norm);
2570 }
2571 }
2572 match item {
2573 Item::Gdn { run, first } => {
2574 for l in &mut self.kv_cache.layers[*first..*first + run.len()] {
2575 if l.linear_state.len() != want {
2576 l.linear_state = vec![0f32; want];
2577 }
2578 }
2579 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
2580 .iter()
2581 .map(|l| l.linear_state.as_slice())
2582 .collect();
2583 let _ig = std::time::Instant::now();
2584 if !graph.encode_gdn_run(run, &ro, gcfg.as_ref().unwrap()) {
2585 tracing::error!("q1 graph: GDN run refused after validation");
2587 return start;
2588 }
2589 graph.commit_kind = 2;
2592 graph.commit();
2593 crate::gpu::stageprof(0, _ig.elapsed());
2594 pending.push((*first, run.len()));
2595 }
2596 Item::Attn {
2597 l,
2598 li,
2599 q_norm,
2600 k_norm,
2601 output_gate,
2602 proj_gate,
2603 head_gate_w,
2604 bias,
2605 full_gpu,
2606 } => {
2607 let _ia = std::time::Instant::now();
2608 let inv_freq_l = self.layer_inv_freq(*li);
2614 let rd_l = self.layer_geom(*li).2;
2615 let window_l = self.layer_window(*li);
2616 let head_gate_w = *head_gate_w;
2617 if *full_gpu {
2619 let cache = &self.kv_cache.layers[*li];
2620 let o1p = if cache.o1.is_some() {
2621 match cache.o1_views() {
2622 Some(views) => Some(crate::gpu::O1AttnParams {
2623 views,
2624 epoch: self.o1_epoch,
2625 }),
2626 None => None,
2628 }
2629 } else {
2630 None
2631 };
2632 let o1_layer = cache.o1.is_some();
2633 if o1_layer && o1p.is_none() {
2634 }
2636 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
2637 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
2638 let cpu_stored = if o1_layer { 0 } else { cpu_k[0].len() / hd };
2639 debug_assert!(
2642 cache.base() == 0 || window_l.is_some_and(|w| cpu_stored + 1 >= w)
2643 );
2644 let p = crate::gpu::AttnDeviceParams {
2645 kv_id,
2646 layer: *li,
2647 nh,
2648 nkv,
2649 hd,
2650 rd: rd_l,
2651 position,
2652 scale: self.attn_scale,
2653 eps: eps as f32,
2654 gemma,
2655 late_qk_norm: self.qk_norm_after_rope,
2656 output_gate: *output_gate,
2657 q_norm: *q_norm,
2658 k_norm: *k_norm,
2659 inv_freq: &inv_freq_l,
2660 cpu_k,
2661 cpu_v,
2662 cpu_stored,
2663 cpu_gen: cache.generation(),
2664 o1: o1p,
2665 window: window_l,
2666 head_gate: head_gate_w,
2667 };
2668 let o1_bad = o1_layer && p.o1.is_none();
2669 if !o1_bad && graph.attn_device_ok(l, &p) && graph.encode_attn_device(l, &p)
2670 {
2671 if p.o1.is_none() {
2673 dev_attn.push(*li);
2674 }
2675 graph.commit_kind = 3;
2676 graph.commit();
2677 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2681 continue;
2682 }
2683 }
2685 graph.encode_attn_prefix(l);
2686 if let Err(err) = graph.sync_checked() {
2687 self.fail_metal_graph(&err);
2688 return start;
2689 }
2690 if !pending.is_empty() {
2691 let idxs: Vec<usize> =
2692 pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2693 let mut outs: Vec<&mut [f32]> = self
2694 .kv_cache
2695 .layers
2696 .iter_mut()
2697 .enumerate()
2698 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2699 .map(|(_, s)| s.linear_state.as_mut_slice())
2700 .collect();
2701 graph.read_states(&mut outs);
2702 }
2703 let mut q_raw = attention::take_buf(l.wq.1);
2704 let mut k = attention::take_buf(l.wk.1);
2705 let mut v = attention::take_buf(l.wv.1);
2706 graph.read_qkv(&mut q_raw, &mut k, &mut v);
2707 let mut gate_raw = proj_gate.map(|(gp, _)| {
2710 let mut normed = attention::take_buf(hs);
2711 graph.read_normed(&mut normed);
2712 let mut raw = attention::take_buf(gp.rows());
2713 gp.matvec(&normed, &mut raw, pool.as_deref());
2714 attention::recycle_buf(&mut normed);
2715 raw
2716 });
2717 let cfg = QwenAttnCfg {
2718 num_heads: nh,
2719 num_kv_heads: nkv,
2720 head_dim: hd,
2721 hidden_size: hs,
2722 position,
2723 inv_freq: &inv_freq_l,
2724 rotary_dim: rd_l,
2725 scale: self.attn_scale,
2726 softcap: self.attn_softcap,
2727 window: window_l,
2728 v_norm: false,
2729 qk_norm_after_rope: self.qk_norm_after_rope,
2730 gate_sigmoid: self.proj_gate_sigmoid,
2731 q_norm: *q_norm,
2732 k_norm: *k_norm,
2733 output_gate: *output_gate,
2734 softplus_gate: None,
2735 rope_scale: 1.0,
2736 bias: *bias,
2737 rms_eps: eps,
2738 norm_style,
2739 pool: pool.as_deref(),
2740 v_head_dim: hd,
2741 };
2742 let oracle = std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1")
2745 || std::env::var("CMF_ATTN_DUMP").is_ok();
2746 let _ = full_gpu;
2747 let oracle_in = oracle.then(|| (q_raw.clone(), k.clone(), v.clone()));
2748 let mut ao = attention::qwen_attention_core(
2749 q_raw,
2750 k,
2751 v,
2752 &mut self.kv_cache.layers[*li],
2753 &cfg,
2754 );
2755 if let Ok(dir) = std::env::var("CMF_ATTN_DUMP") {
2759 if let Some((qr0, k0, v0)) = oracle_in.clone() {
2760 let (cq, _cg, _ck, _cv) =
2761 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2762 let cache = &self.kv_cache.layers[*li];
2763 let n = cache.head_keys(0).len() / hd;
2764 let mut bytes: Vec<u8> = Vec::new();
2765 for v in [nh as u32, nkv as u32, hd as u32, n as u32, position as u32] {
2766 bytes.extend_from_slice(&v.to_le_bytes());
2767 }
2768 for v in &cq {
2769 bytes.extend_from_slice(&v.to_le_bytes());
2770 }
2771 for g in 0..nkv {
2772 for v in cache.head_keys(g) {
2773 bytes.extend_from_slice(&v.to_le_bytes());
2774 }
2775 }
2776 for g in 0..nkv {
2777 for v in cache.head_values(g) {
2778 bytes.extend_from_slice(&v.to_le_bytes());
2779 }
2780 }
2781 let _ =
2782 std::fs::write(format!("{dir}/L{li}_pos{position}.bin"), &bytes);
2783 }
2784 }
2785 if let Some((qr0, k0, v0)) =
2786 oracle_in.filter(|_| std::env::var("CMF_ATTN_ORACLE").as_deref() == Ok("1"))
2787 {
2788 let (cq, _cg, ck, cv) =
2789 attention::finish_projection_debug(qr0, k0, v0, &cfg, position);
2790 let mut h_now = vec![0f32; hs];
2791 graph.read_h(&mut h_now);
2792 let cache = &self.kv_cache.layers[*li];
2793 let n_after = cache.head_keys(0).len() / hd;
2794 let stored = n_after.saturating_sub(1);
2798 let cpu_k: Vec<&[f32]> = (0..nkv)
2799 .map(|g| &cache.head_keys(g)[..stored * hd])
2800 .collect();
2801 let cpu_v: Vec<&[f32]> = (0..nkv)
2802 .map(|g| &cache.head_values(g)[..stored * hd])
2803 .collect();
2804 let p = crate::gpu::AttnDeviceParams {
2805 kv_id,
2806 layer: *li,
2807 nh,
2808 nkv,
2809 hd,
2810 rd: rd_l,
2814 position,
2815 scale: self.attn_scale,
2816 eps: eps as f32,
2817 gemma,
2818 late_qk_norm: self.qk_norm_after_rope,
2819 output_gate: *output_gate,
2820 q_norm: *q_norm,
2821 k_norm: *k_norm,
2822 inv_freq: &inv_freq_l,
2823 cpu_k,
2824 cpu_v,
2825 cpu_stored: stored,
2826 cpu_gen: cache.generation(),
2827 o1: None,
2828 window: window_l,
2829 head_gate: None,
2830 };
2831 if let Some((dq, dk, dv, dao)) = graph.debug_attn_device(l, &p, &h_now) {
2832 let md = |a: &[f32], b: &[f32]| {
2833 a.iter().zip(b).fold(0f32, |m, (x, y)| m.max((x - y).abs()))
2834 };
2835 let nn = |a: &[f32]| a.iter().map(|x| x * x).sum::<f32>().sqrt();
2836 eprintln!(
2837 "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}",
2838 nn(&cq),
2839 md(&cq, &dq),
2840 nn(&ck),
2841 md(&ck, &dk),
2842 nn(&cv),
2843 md(&cv, &dv),
2844 nn(&ao),
2845 md(&ao, &dao)
2846 );
2847 } else {
2848 eprintln!("attn-oracle L{li}: device probe declined");
2849 }
2850 }
2851 if let (Some(raw), Some((_, per_head))) = (gate_raw.as_deref(), *proj_gate) {
2852 attention::apply_projected_gate(
2855 &mut ao,
2856 raw,
2857 per_head,
2858 hd,
2859 self.proj_gate_sigmoid,
2860 );
2861 }
2862 if let Some(mut raw) = gate_raw.take() {
2863 attention::recycle_buf(&mut raw);
2864 }
2865 graph.encode_attn_suffix(l, &ao);
2866 graph.commit();
2869 attention::recycle_buf(&mut ao);
2870 }
2871 }
2872
2873 crate::gpu::stageprof(_xkind, _xt0.elapsed());
2874 }
2875 let mut lm_rows = None;
2880 if self.graph_want_logits
2881 && upto.is_none()
2882 && end == self.num_layers
2883 && std::env::var("CMF_GPU_LMHEAD")
2884 .map(|v| v != "0")
2885 .unwrap_or(true)
2886 {
2887 if let Some(lm) = self.weights.lm_head.metal_graph_parts() {
2888 if graph.lm_head_ok(lm) {
2889 graph.encode_lm_head(&self.weights.final_norm, lm);
2890 lm_rows = Some(lm.1);
2891 }
2892 }
2893 }
2894 if self.graph_head_required && lm_rows.is_none() {
2895 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2896 self.fail_metal_graph("fused graph head was requested but not encodable");
2897 return start;
2898 }
2899 let _sy0 = std::time::Instant::now();
2900 if let Err(err) = graph.sync_checked() {
2901 self.fail_metal_graph(&err);
2902 return start;
2903 }
2904 let _rs0 = std::time::Instant::now();
2905 if !pending.is_empty() {
2906 let idxs: Vec<usize> = pending.drain(..).flat_map(|(f, n)| f..f + n).collect();
2907 let mut outs: Vec<&mut [f32]> = self
2908 .kv_cache
2909 .layers
2910 .iter_mut()
2911 .enumerate()
2912 .filter(|(i, _)| idxs.binary_search(i).is_ok())
2913 .map(|(_, s)| s.linear_state.as_mut_slice())
2914 .collect();
2915 graph.read_states(&mut outs);
2916 }
2917 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() == Ok("1") {
2918 use std::sync::atomic::{AtomicU64, Ordering};
2919 static SY: AtomicU64 = AtomicU64::new(0);
2920 static RS: AtomicU64 = AtomicU64::new(0);
2921 static N: AtomicU64 = AtomicU64::new(0);
2922 SY.fetch_add((_rs0 - _sy0).as_nanos() as u64, Ordering::Relaxed);
2923 RS.fetch_add(_rs0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2924 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2925 if n % 100 == 0 {
2926 eprintln!(
2927 "postprof: sync-wait {:.1} ms/ток | read_states {:.1} ms/ток ({n})",
2928 SY.load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2929 RS.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2930 );
2931 }
2932 }
2933 if let Some(rows) = lm_rows {
2934 crate::gpu::hostprof_encode_done(_mt0);
2935 let mut lg = attention::take_buf(rows.min(self.vocab_size));
2936 graph.read_logits(&mut lg);
2937 crate::gpu::hostprof_total(_mt0);
2938 lg.resize(self.vocab_size, 0.0);
2939 if let Some(c) = self.final_softcap {
2940 for l in lg.iter_mut() {
2941 *l = c * (*l / c).tanh();
2942 }
2943 }
2944 self.graph_logits = Some(lg);
2945 }
2946 graph.read_h(h);
2947 if self.graph_head_required && self.graph_logits.is_none() {
2948 METAL_GRAPH_HEAD_MISS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2949 self.fail_metal_graph("fused graph head completed without logits readback");
2950 return start;
2951 }
2952 METAL_GRAPH_TOK_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2953 METAL_GRAPH_LAYERS.fetch_add(
2954 end.saturating_sub(start) as u64,
2955 std::sync::atomic::Ordering::Relaxed,
2956 );
2957 if self.graph_head_required {
2958 METAL_GRAPH_HEAD_OK.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2959 }
2960 for li in dev_attn {
2964 let mut krow = attention::take_buf(nkv * hd);
2965 let mut vrow = attention::take_buf(nkv * hd);
2966 if crate::gpu::kv_mirror_read_last(kv_id, li, nkv, hd, &mut krow, &mut vrow) {
2967 let cache = &mut self.kv_cache.layers[li];
2968 cache.append(&krow, &vrow, &[]);
2969 let n = cache.seq_len;
2970 let mut imp = attention::take_buf(n);
2971 crate::gpu::kv_mirror_take_imp(kv_id, li, &mut imp);
2972 cache.accumulate_imp(&imp);
2973 attention::recycle_buf(&mut imp);
2974 }
2975 attention::recycle_buf(&mut krow);
2976 attention::recycle_buf(&mut vrow);
2977 }
2978 if let Some((_, arm)) = ab {
2979 crate::gpu_metal::dense_ab_record(arm, _mt0.elapsed());
2980 }
2981 end
2982 }
2983
2984 pub fn new(
2985 tokenizer: Tokenizer,
2986 weights: PipelineWeights,
2987 hidden_size: usize,
2988 intermediate_size: usize,
2989 num_heads: usize,
2990 num_kv_heads: usize,
2991 head_dim: usize,
2992 num_layers: usize,
2993 physical_layers: usize,
2994 loop_final_norm: bool,
2995 vocab_size: usize,
2996 rms_eps: f64,
2997 rope_base: f32,
2998 norm_style: NormStyle,
2999 max_seq_len: usize,
3000 sampler_config: SamplerConfig,
3001 ) -> Self {
3002 let rng = match sampler_config.seed {
3003 Some(s) => SplitMix64::new(s),
3004 None => SplitMix64::from_entropy(),
3005 };
3006 let inv_freq = std::sync::Arc::new(attention::rope_inv_freq(head_dim, rope_base));
3007 let pool = Pool::from_env();
3008 if let Some(p) = &pool {
3009 tracing::info!("worker pool: {} threads", p.n_workers());
3010 if let Some(model) = weights
3012 .lm_head
3013 .model_arc()
3014 .or_else(|| weights.embed_tokens.model_arc())
3015 {
3016 let regions: Vec<&[u8]> =
3017 model.tensors.iter().map(|t| model.entry_bytes(t)).collect();
3018 p.bind_numa(®ions);
3019 }
3020 }
3021 Self {
3022 gpu_plan: None,
3023 tokenizer: std::sync::Arc::new(tokenizer),
3024 kv_cache: KvCache::new(num_layers, num_kv_heads, head_dim, max_seq_len),
3025 sampler_config,
3026 weights,
3027 hidden_size,
3028 intermediate_size,
3029 num_heads,
3030 num_kv_heads,
3031 head_dim,
3032 num_layers,
3033 physical_layers,
3034 loop_final_norm,
3035 vocab_size,
3036 rms_eps,
3037 rope_base,
3038 norm_style,
3039 rotary_dim: head_dim,
3040 attention_heads_per_layer: None,
3041 kv_heads_per_layer: None,
3042 v_head_dim: None,
3043 layer_dump: std::env::var_os("CMF_LAYER_DUMP")
3044 .filter(|v| !v.is_empty())
3045 .map(std::path::PathBuf::from),
3046 graph_declines: std::cell::RefCell::new(Vec::new()),
3047 mimo_moe: Default::default(),
3048 vmf_cfg: None,
3049 gdn_cfg: None,
3050 kda_cfg: None,
3051 g3n: None,
3052 dsv4: None,
3053 dsv41: None,
3054 dsv41_vision: None,
3055 dsv41_prefill: None,
3056 qwen4_exp: None,
3057 dsv4_mtp: Vec::new(),
3058 dspark: None,
3059 dspark_pending: Vec::new(),
3060 dspark_hist: Vec::new(),
3061 dspark_real: Vec::new(),
3062 dspark_trunk_picks: Vec::new(),
3063 dspark_exp: Vec::new(),
3064 dspark_draft_ns: 0,
3065 logit_multiplier: None,
3066 cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3067 graph_failed: std::sync::atomic::AtomicBool::new(false),
3068 kv_history: Vec::new(),
3069 kv_history_device: false,
3070 short_conv_cfg: None,
3071 mtp: None,
3072 mimo_mtp: None,
3073 verify_exact_moe: false,
3074 speculative: std::env::var("CMF_MTP").map(|v| v != "0").unwrap_or(true),
3075 ignore_eos: false,
3076 draft_full_streak: 0,
3077 spec_k_adapt: None,
3078 spec_acc_ewma: 0.7,
3079 rng,
3080 sampler_scratch: SamplerScratch::default(),
3081 spec_forced: None,
3082 spec_q: Vec::new(),
3083 spec_p: Vec::new(),
3084 spec_res: Vec::new(),
3085 spec_qs: Vec::new(),
3086 spec_ps: Vec::new(),
3087 spec_ress: Vec::new(),
3088 mtp_graph_mode: None,
3089 #[cfg(target_os = "macos")]
3090 metal_verify: None,
3091 inv_freq,
3092 ws: ForwardScratch::new(hidden_size),
3093 pool,
3094 model: None,
3095 dyn_force_f32: false,
3096 dyn_skill_layers: Vec::new(),
3097 dyn_active: None,
3098 dyn_blend_loaded: false,
3099 dyn_phi_layer: None,
3100 dyn_phi_ema: Vec::new(),
3101 dyn_phi_seen: 0,
3102 dyn_router: None,
3103 o1_cfg: None,
3104 o1_epoch: 0,
3105 o1_flags: Vec::new(),
3106 trace: false,
3107 calib_temp: 1.0,
3108 confidence_on: true,
3109 embed_multiplier: 1.0,
3110 attn_scale: 1.0 / (head_dim as f32).sqrt(),
3111 swa: None,
3112 swa_trim: None,
3113 sliding_layers: None,
3114 anchor_core: None,
3115 bounded_rope: None,
3116 kv_prefix: KvPrefix::default(),
3117 last_prefill_tokens: 0,
3118 inv_freq_local: None,
3119 rotary_dim_local: None,
3120 rope_scale: 1.0,
3121 rope_scale_local: 1.0,
3122 global_attn: None,
3123 inv_freq_global: None,
3124 attn_v_norm: false,
3125 qk_norm_after_rope: false,
3126 proj_gate_sigmoid: false,
3127 final_softcap: None,
3128 head_clusters: None,
3129 attn_softcap: 0.0,
3130 graph_want_logits: false,
3131 graph_head_required: false,
3132 graph_logits: None,
3133 embryo_graph: None,
3134 graph_refused: std::sync::atomic::AtomicBool::new(false),
3135 graph_kv_id: {
3136 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
3137 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
3138 },
3139 #[cfg(test)]
3140 nll_test_fail_at: None,
3141 #[cfg(test)]
3142 nll_test_force_serial: false,
3143 }
3144 }
3145
3146 pub fn set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) {
3154 if let Err(e) = self.try_set_o1(cfg) {
3155 tracing::error!("{e}");
3156 }
3157 }
3158
3159 pub fn bounded_native(&self) -> bool {
3163 self.anchor_core.is_some()
3164 }
3165
3166 pub fn device_state_bytes(&self) -> Option<(u64, u64)> {
3169 crate::gpu::embryo_device_state_bytes(self.graph_kv_id)
3170 }
3171
3172 pub fn o1_refusal(&self) -> Option<String> {
3174 self.anchor_core.as_ref().map(|ac| {
3175 format!(
3176 "--o1 / CMF_O1 refused: the anchor is native bounded \
3177 (anchor_core kind={} window={} sink={}); the file's operator \
3178 is executed as-is and no post-hoc Nyström overlay applies",
3179 ac.kind, ac.window, ac.sink
3180 )
3181 })
3182 }
3183
3184 pub fn try_set_o1(&mut self, cfg: Option<crate::nystrom::O1Cfg>) -> Result<(), String> {
3186 if let Some(c) = &cfg {
3187 if let Some(why) = self.o1_refusal() {
3188 self.o1_flags = Vec::new();
3189 self.o1_cfg = None;
3190 return Err(why);
3191 }
3192 if crate::nystrom::o1_deferred_boundary(c.w, c.sink).is_none() {
3193 self.o1_flags.clear();
3194 self.o1_cfg = None;
3195 return Err(format!(
3196 "o1 disabled: w + sink + slack + 1 overflows usize (w={}, sink={})",
3197 c.w, c.sink
3198 ));
3199 }
3200 }
3201 self.o1_flags = match &cfg {
3202 Some(c) => {
3203 let mut flags = c.layer_flags(self.num_layers);
3204 for (li, f) in flags.iter_mut().enumerate() {
3205 if *f
3211 && (!matches!(
3212 self.weights.layers[self.phys_layer(li)].attn,
3213 AttnKind::Full { .. }
3214 ) || self.layer_window(li).is_some()
3215 || self.kv_cache.layers[li].sinks.is_some()
3216 || self.layer_v_dim(li) != self.layer_geom(li).1)
3217 {
3218 *f = false;
3219 }
3220 }
3221 flags
3222 }
3223 None => Vec::new(),
3224 };
3225 if let Some(c) = &cfg {
3226 let n = self.o1_flags.iter().filter(|&&f| f).count();
3227 tracing::info!(
3228 "o1 nystrom attention: {n}/{} layer(s), m={} w={} sink={} rect={:?}",
3229 self.num_layers,
3230 c.m,
3231 c.w,
3232 c.sink,
3233 c.rect
3234 );
3235 }
3236 self.o1_cfg = cfg;
3237 Ok(())
3238 }
3239
3240 pub fn install_bounded(
3246 &mut self,
3247 cfg: &cortiq_core::AnchorCoreConfig,
3248 ) -> Result<(), String> {
3249 if !cortiq_core::AnchorCoreConfig::KINDS.contains(&cfg.kind.as_str()) {
3250 return Err(format!(
3251 "anchor_core kind '{}' is not executable by this runtime",
3252 cfg.kind
3253 ));
3254 }
3255 if cfg.window == 0 {
3256 return Err("anchor_core.window must be >= 1".into());
3257 }
3258 let mut n = 0usize;
3259 for li in 0..self.num_layers {
3260 let pl = self.phys_layer(li);
3261 if let AttnKind::Bounded(w) = &self.weights.layers[pl].attn {
3262 if w.window != cfg.window || w.sink != cfg.sink {
3263 return Err(format!(
3264 "layer {li}: bounded weights (window {} sink {}) disagree with \
3265 anchor_core (window {} sink {})",
3266 w.window, w.sink, cfg.window, cfg.sink
3267 ));
3268 }
3269 self.kv_cache.layers[li].install_bounded(cfg.window);
3270 n += 1;
3271 }
3272 }
3273 if n == 0 {
3274 return Err("anchor_core is present but no layer executes it".into());
3275 }
3276 let rope = crate::bounded::BoundedRope::new(cfg.window, &self.inv_freq, self.rope_scale);
3277 self.bounded_rope = Some(std::sync::Arc::new(rope));
3278 self.anchor_core = Some(cfg.clone());
3279 self.embryo_graph = None;
3280 tracing::info!(
3281 "bounded anchor {}: {n} layer(s), window {} sink {} — {} B of ring per layer",
3282 cfg.kind,
3283 cfg.window,
3284 cfg.sink,
3285 self.kv_cache.layers.iter().map(|l| l.bounded_state_bytes()).max().unwrap_or(0)
3286 );
3287 Ok(())
3288 }
3289
3290 pub fn install_wire_identity(&mut self, identity: u64) {
3294 for li in 0..self.kv_cache.layers.len() {
3295 let pl = self.phys_layer(li);
3296 let kind = match self.weights.layers.get(pl).map(|l| &l.attn) {
3297 Some(AttnKind::Bounded(_)) => crate::kv_cache::WireKind::Bounded,
3298 Some(AttnKind::Linear(_))
3299 | Some(AttnKind::LinearGdn(_))
3300 | Some(AttnKind::ShortConv(_))
3301 | Some(AttnKind::Kda(_)) => crate::kv_cache::WireKind::Linear,
3302 _ => crate::kv_cache::WireKind::Full,
3303 };
3304 let l = &mut self.kv_cache.layers[li];
3305 l.wire_kind = kind;
3306 l.wire_identity = identity;
3307 l.wire_layer = li as u32;
3310 }
3311 }
3312
3313 pub fn clear_history(&mut self) {
3315 self.kv_history.clear();
3316 self.kv_history_device = false;
3317 self.kv_prefix.clear();
3318 }
3319
3320 pub fn graph_refused(&self) -> bool {
3324 self.graph_refused
3325 .load(std::sync::atomic::Ordering::Relaxed)
3326 }
3327
3328 pub fn mark_graph_refused(&self) {
3330 if !self
3331 .graph_refused
3332 .swap(true, std::sync::atomic::Ordering::Relaxed)
3333 {
3334 tracing::info!(
3335 "token graph: unsupported for this pipeline (seq {}) — not retrying",
3336 self.graph_kv_id
3337 );
3338 }
3339 }
3340
3341 pub fn device_sequence_position(&self) -> Option<usize> {
3345 crate::gpu::embryo_device_next_position(self.graph_kv_id)
3346 }
3347
3348 fn prefix_owner_matches(&self, n: usize, recorded_on_device: bool) -> bool {
3353 let dev = self.device_sequence_position();
3354 if recorded_on_device {
3355 dev == Some(n) && self.embryo_resident_wanted()
3356 } else {
3357 dev.is_none()
3358 }
3359 }
3360
3361 pub(crate) fn invalidate_for_weight_change(&mut self) {
3369 self.clear_sequence_state();
3370 self.embryo_graph = None;
3371 }
3372
3373 fn cached_prefix_len(&self, input_ids: &[u32]) -> usize {
3379 let (n, on_device) = if self.bounded_native() {
3380 (self.kv_prefix.extension(input_ids), self.kv_prefix.on_device())
3381 } else {
3382 let h = &self.kv_history;
3383 if !h.is_empty() && h.len() < input_ids.len() && input_ids[..h.len()] == h[..] {
3384 (h.len(), self.kv_history_device)
3385 } else {
3386 (0, false)
3387 }
3388 };
3389 if n > 0 && !self.prefix_owner_matches(n, on_device) {
3392 tracing::warn!(
3393 "kv-reuse refused: the cached prefix ({n} positions) was built on the {} path, \
3394 the device now holds {:?} — re-prefilling from zero",
3395 if on_device { "resident device" } else { "host" },
3396 self.device_sequence_position()
3397 );
3398 return 0;
3399 }
3400 n
3401 }
3402
3403 pub fn reusable_prefix_len(&self, input_ids: &[u32]) -> usize {
3406 self.cached_prefix_len(input_ids)
3407 }
3408
3409 fn record_consumed_prefix(&mut self, consumed: &[u32], reused: usize) {
3415 let on_device = self.device_sequence_position().is_some();
3416 if self.bounded_native() {
3417 let keep = reused > 0 && reused == self.kv_prefix.len() && reused <= consumed.len();
3418 let prev_device = self.kv_prefix.on_device();
3419 self.kv_history.clear();
3420 self.kv_history_device = false;
3421 if keep && prev_device == on_device {
3422 self.kv_prefix.extend(&consumed[reused..]);
3423 } else {
3424 self.kv_prefix.set(consumed);
3425 }
3426 self.kv_prefix.set_on_device(on_device);
3427 } else {
3428 self.kv_history = consumed.to_vec();
3429 self.kv_history_device = on_device;
3430 }
3431 }
3432
3433 pub fn o1_active(&self) -> bool {
3435 self.o1_cfg.is_some() && self.o1_flags.iter().any(|&f| f)
3436 }
3437
3438 pub fn generation_batch_k(&self) -> usize {
3450 if let Some(k) = std::env::var("CMF_BATCH_K")
3451 .ok()
3452 .and_then(|v| v.parse::<usize>().ok())
3453 {
3454 return k;
3455 }
3456 #[cfg(not(target_os = "macos"))]
3457 if self.graph_prefill_preferred() && !self.o1_active() {
3458 return 32;
3459 }
3460 0
3461 }
3462
3463 pub fn generation_graph_prefill(&self) -> bool {
3464 let graph = self.graph_prefill_preferred();
3465 #[cfg(not(target_os = "macos"))]
3476 if graph
3477 && self.generation_batch_k() > 0
3478 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
3479 {
3480 return false;
3481 }
3482 graph
3483 }
3484
3485 pub fn o1_device_stats(&self) -> (usize, u64) {
3490 crate::gpu::o1_device_stats(self.graph_kv_id)
3491 }
3492
3493 pub fn o1_begin(&mut self) {
3498 self.o1_begin_with_prefix(None);
3499 }
3500
3501 pub fn o1_begin_with_prefix(&mut self, requested_prefix: Option<usize>) {
3505 if let Some(c) = &self.o1_cfg {
3506 let (m, w, sink, rect) = (c.m, c.w, c.sink, c.rect);
3507 let boundary = requested_prefix.map(|p| {
3508 p.max(
3509 crate::nystrom::o1_deferred_boundary(w, sink)
3510 .expect("o1 config boundary validated in set_o1"),
3511 )
3512 });
3513 for (li, &f) in self.o1_flags.iter().enumerate() {
3514 if f {
3515 self.kv_cache.layers[li].o1_begin_with_boundary(m, w, sink, rect, boundary);
3516 }
3517 }
3518 }
3519 }
3520
3521 fn o1_effective_boundary(&self, requested_prefix: usize) -> Option<usize> {
3523 self.o1_cfg.as_ref().and_then(|c| {
3524 crate::nystrom::o1_deferred_boundary(c.w, c.sink)
3525 .map(|floor| requested_prefix.max(floor))
3526 })
3527 }
3528
3529 fn o1_note_transition(&mut self) {
3530 let mut transitioned = false;
3534 for (li, &flagged) in self.o1_flags.iter().enumerate() {
3535 if flagged {
3536 transitioned |= self.kv_cache.layers[li].take_o1_transition();
3537 }
3538 }
3539 if transitioned {
3540 self.o1_epoch = self.o1_epoch.wrapping_add(1);
3541 }
3542 }
3543
3544 fn o1_pending(&self) -> bool {
3545 self.o1_flags.iter().enumerate().any(|(li, &f)| {
3546 f && self.kv_cache.layers[li].seq_len > 0
3547 && self.kv_cache.layers[li].o1_pending_boundary().is_some()
3548 })
3549 }
3550
3551 fn o1_fail(&mut self, err: String) {
3552 tracing::error!("o1 deferred seal failed; terminating sequence: {err}");
3553 self.clear_sequence_state();
3554 self.graph_failed
3555 .store(true, std::sync::atomic::Ordering::Relaxed);
3556 self.cancel
3557 .store(true, std::sync::atomic::Ordering::Relaxed);
3558 }
3559
3560 pub fn o1_seal_checked(&mut self) -> Result<bool, String> {
3565 if self.o1_cfg.is_none() {
3566 return Ok(false);
3567 }
3568 let mut participating = false;
3569 for li in 0..self.num_layers {
3570 if !self.o1_flags.get(li).copied().unwrap_or(false) {
3571 continue;
3572 }
3573 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3574 return Err(err);
3575 }
3576 if self.kv_cache.layers[li].seq_len == 0 {
3577 continue;
3578 }
3579 participating = true;
3580 let num_heads = self.layer_num_heads(li);
3581 self.kv_cache.layers[li].o1_seal_checked(num_heads)?;
3582 }
3583 self.o1_note_transition();
3584 for li in 0..self.num_layers {
3585 if self.o1_flags.get(li).copied().unwrap_or(false) {
3586 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3587 return Err(err);
3588 }
3589 }
3590 }
3591 Ok(participating
3592 && (0..self.num_layers).all(|li| {
3593 !self.o1_flags.get(li).copied().unwrap_or(false)
3594 || self.kv_cache.layers[li].seq_len == 0
3595 || self.kv_cache.layers[li].o1_sealed()
3596 }))
3597 }
3598
3599 fn o1_progress(&mut self) {
3602 if !self.o1_active() {
3603 return;
3604 }
3605 for li in 0..self.num_layers {
3606 if self.o1_flags.get(li).copied().unwrap_or(false) {
3607 if let Some(err) = self.kv_cache.layers[li].take_o1_error() {
3608 self.o1_fail(err);
3609 return;
3610 }
3611 }
3612 }
3613 self.o1_note_transition();
3617 if !self.o1_pending() {
3618 return;
3619 }
3620 if let Err(err) = self.o1_seal_checked() {
3621 self.o1_fail(err);
3622 }
3623 }
3624
3625 fn check_o1_progress_failure(&mut self, phase: &str) -> Result<(), String> {
3630 if self
3631 .graph_failed
3632 .swap(false, std::sync::atomic::Ordering::Relaxed)
3633 {
3634 self.cancel
3635 .store(false, std::sync::atomic::Ordering::Relaxed);
3636 self.clear_sequence_state();
3637 return Err(format!("{phase}: deferred O(1) transition failed"));
3638 }
3639 Ok(())
3640 }
3641
3642 pub fn o1_seal(&mut self) {
3646 if let Err(err) = self.o1_seal_checked() {
3647 self.o1_fail(err);
3648 }
3649 }
3650
3651 pub fn set_trace(&mut self, on: bool) {
3653 self.trace = on;
3654 }
3655
3656 pub fn set_sampler_config(&mut self, config: SamplerConfig) {
3659 self.rng = match config.seed {
3660 Some(seed) => SplitMix64::new(seed),
3661 None => SplitMix64::from_entropy(),
3662 };
3663 self.sampler_config = config;
3664 }
3665
3666 pub fn set_confidence(&mut self, on: bool) {
3671 self.confidence_on = on;
3672 }
3673
3674 pub fn set_calib_temp(&mut self, t: f32) {
3677 self.calib_temp = if t > 1e-3 { t } else { 1.0 };
3678 }
3679
3680 pub fn calib_temp(&self) -> f32 {
3682 self.calib_temp
3683 }
3684
3685 pub fn rope_inv_freq(&self) -> &[f32] {
3689 self.inv_freq.as_slice()
3690 }
3691
3692 pub fn set_rotary(&mut self, rotary_dim: usize, base: f32) {
3695 self.rotary_dim = rotary_dim.min(self.head_dim);
3696 self.inv_freq = std::sync::Arc::new(attention::rope_inv_freq(self.rotary_dim, base));
3697 self.embryo_graph = None;
3701 }
3702
3703 fn attn_cfg(&self, position: usize) -> QwenAttnCfg<'_> {
3704 QwenAttnCfg {
3705 num_heads: self.num_heads,
3706 num_kv_heads: self.num_kv_heads,
3707 head_dim: self.head_dim,
3708 hidden_size: self.hidden_size,
3709 position,
3710 inv_freq: &self.inv_freq,
3711 rotary_dim: self.rotary_dim,
3712 scale: self.attn_scale,
3713 softcap: self.attn_softcap,
3714 window: None,
3715 v_norm: false,
3716 qk_norm_after_rope: self.qk_norm_after_rope,
3717 gate_sigmoid: self.proj_gate_sigmoid,
3718 q_norm: None,
3719 k_norm: None,
3720 output_gate: false,
3721 softplus_gate: None,
3722 rope_scale: self.rope_scale,
3723 bias: None,
3724 rms_eps: self.rms_eps,
3725 norm_style: self.norm_style,
3726 pool: self.pool.as_deref(),
3727 v_head_dim: self.v_head_dim.unwrap_or(self.head_dim),
3728 }
3729 }
3730
3731 pub fn generate(
3733 &mut self,
3734 prompt: &str,
3735 max_tokens: usize,
3736 task_mask: Option<&TaskMask>,
3737 on_token: Option<TokenCallback>,
3738 ) -> Result<GenerateResult, String> {
3739 let input_ids = self.tokenizer.with_bos(self.tokenizer.encode(prompt));
3740 self.generate_from_ids(&input_ids, max_tokens, task_mask, on_token)
3741 }
3742
3743 pub fn generate_from_vl(
3746 &mut self,
3747 input: &crate::dsv41_vision::PreparedVlInputs,
3748 max_tokens: usize,
3749 task_mask: Option<&TaskMask>,
3750 on_token: Option<TokenCallback>,
3751 ) -> Result<GenerateResult, String> {
3752 let Some(dsv41) = &self.dsv41 else {
3753 return Err("V4.1 multimodal input requires a DeepSeek-V4.1 pipeline".into());
3754 };
3755 if input.token_ids.is_empty() {
3756 return Err("empty V4.1 multimodal prompt".into());
3757 }
3758 if input.token_types.len() != input.token_ids.len() {
3759 return Err(format!(
3760 "V4.1 token type count {} != token count {}",
3761 input.token_types.len(),
3762 input.token_ids.len()
3763 ));
3764 }
3765 let dim = dsv41.2.dim;
3766 let mut embeddings = vec![None; input.token_ids.len()];
3767 let mut participates = vec![true; input.token_ids.len()];
3768 if !input.images.is_empty() {
3769 let vision = self
3770 .dsv41_vision
3771 .as_ref()
3772 .ok_or_else(|| "V4.1 image prompt has no loaded vision tower".to_string())?;
3773 for image in &input.images {
3774 let end = image.start.saturating_add(image.types.len());
3775 if end > input.token_ids.len() {
3776 return Err(format!(
3777 "V4.1 image span {}..{} exceeds prompt length {}",
3778 image.start,
3779 end,
3780 input.token_ids.len()
3781 ));
3782 }
3783 let mut span = vec![0.0f32; image.types.len() * dim];
3784 vision.fill_image_span(image, &mut span, self.pool.as_deref())?;
3785 for (offset, &kind) in image.types.iter().enumerate() {
3786 let pos = image.start + offset;
3787 if input.token_types[pos] != kind {
3788 return Err(format!(
3789 "V4.1 image type mismatch at position {pos}: {} != {kind}",
3790 input.token_types[pos]
3791 ));
3792 }
3793 embeddings[pos] = Some(span[offset * dim..(offset + 1) * dim].to_vec());
3794 participates[pos] = false;
3795 }
3796 }
3797 }
3798 for (pos, &kind) in input.token_types.iter().enumerate() {
3799 if kind == crate::dsv41_vision::TEXT && embeddings[pos].is_some() {
3800 return Err(format!("V4.1 text position {pos} has an image embedding"));
3801 }
3802 if kind != crate::dsv41_vision::TEXT && embeddings[pos].is_none() {
3803 return Err(format!("V4.1 image position {pos} has no image embedding"));
3804 }
3805 }
3806 self.dsv41_prefill = Some((embeddings, participates));
3807 let result = self.generate_from_ids(&input.token_ids, max_tokens, task_mask, on_token);
3808 self.dsv41_prefill = None;
3809 result
3810 }
3811
3812 fn drop_open_mask<'m>(&self, m: Option<&'m TaskMask>) -> Option<&'m TaskMask> {
3814 m.filter(|m| !m.fully_open(self.intermediate_size, self.num_heads))
3815 }
3816
3817 pub fn generate_from_ids(
3825 &mut self,
3826 input_ids: &[u32],
3827 max_tokens: usize,
3828 task_mask: Option<&TaskMask>,
3829 on_token: Option<TokenCallback>,
3830 ) -> Result<GenerateResult, String> {
3831 self.generate_with_prompt_rows(input_ids, None, max_tokens, task_mask, on_token)
3832 }
3833
3834 pub fn generate_from_embeds(
3840 &mut self,
3841 input_ids: &[u32],
3842 prompt_rows: &[f32],
3843 max_tokens: usize,
3844 task_mask: Option<&TaskMask>,
3845 on_token: Option<TokenCallback>,
3846 ) -> Result<GenerateResult, String> {
3847 if input_ids.is_empty()
3848 || input_ids.len().checked_mul(self.hidden_size) != Some(prompt_rows.len())
3849 {
3850 return Err("embedded prompt dimensions must be [tokens, hidden_size]".into());
3851 }
3852 if prompt_rows.iter().any(|x| !x.is_finite()) {
3853 return Err("embedded prompt contains non-finite values".into());
3854 }
3855 if !self.can_prefill_batched() || self.dyn_router.is_some()
3856 || self.o1_active() || self.mtp.is_some() || self.gpu_plan.is_some()
3857 {
3858 return Err("embedded prompts require the ordinary transformer path without O(1), dynamic routing, GPU splitting or a generic MTP head".into());
3859 }
3860 self.generate_with_prompt_rows(input_ids, Some(prompt_rows), max_tokens, task_mask, on_token)
3861 }
3862
3863 fn generate_with_prompt_rows(
3864 &mut self,
3865 input_ids: &[u32],
3866 prompt_rows: Option<&[f32]>,
3867 max_tokens: usize,
3868 task_mask: Option<&TaskMask>,
3869 mut on_token: Option<TokenCallback>,
3870 ) -> Result<GenerateResult, String> {
3871 #[cfg(target_os = "macos")]
3872 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
3873 if std::env::var("CMF_TRACE_H").is_ok() {
3874 eprintln!("input_ids: {input_ids:?}");
3875 }
3876 if input_ids.is_empty() {
3877 return Err("empty prompt: nothing to generate from".to_string());
3878 }
3879 self.graph_failed
3883 .store(false, std::sync::atomic::Ordering::Relaxed);
3884 let task_mask = self.drop_open_mask(task_mask);
3889
3890 let mut reuse_from = {
3898 let on = !std::env::var("CMF_KV_REUSE").is_ok_and(|v| v == "0");
3899 if on
3900 && prompt_rows.is_none()
3901 && task_mask.is_none()
3902 && self.mtp.is_none()
3903 && !(self.mimo_mtp.is_some() && self.speculative)
3904 && self.o1_cfg.is_none()
3905 && self.dsv41.is_none()
3906 {
3907 self.cached_prefix_len(input_ids)
3908 } else {
3909 0
3910 }
3911 };
3912 if reuse_from > 0 && !self.prepare_kv_reuse(reuse_from) {
3915 reuse_from = 0;
3916 }
3917 self.last_prefill_tokens = input_ids.len() - reuse_from;
3918 let bounded_native = self.bounded_native();
3919 if reuse_from == 0 {
3920 self.clear_sequence_state();
3922 } else if std::env::var("CMF_PREFILL_PROF").is_ok() {
3923 eprintln!(
3924 "kv-reuse: {} of {} prompt positions already cached",
3925 reuse_from,
3926 input_ids.len()
3927 );
3928 }
3929 crate::gpu::graph_race_begin_generation();
3930 let o1_prefill = if self.o1_active() && task_mask.is_none() {
3934 std::env::var("CMF_O1_PREFILL")
3935 .ok()
3936 .and_then(|v| v.parse::<usize>().ok())
3937 .filter(|&p| p > 0)
3938 } else {
3939 None
3940 };
3941 if task_mask.is_none() {
3942 self.o1_begin_with_prefix(o1_prefill);
3943 }
3944
3945 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
3951 #[cfg(target_os = "macos")]
3990 let metal_graph = crate::gpu::q1_force()
3991 && crate::gpu::enabled_here()
3992 && std::env::var("CMF_GPU_BLOCK")
3993 .map(|v| v != "0")
3994 .unwrap_or(true);
3995 #[cfg(not(target_os = "macos"))]
3996 let metal_graph = false;
3997 let spec_sample_env = std::env::var("CMF_GRAPH_SPEC_SAMPLE").ok();
3998 let spec_cheap_round = self.sampler_config.temperature < 1e-6
4002 || sampler::sparse_ok(&self.sampler_config);
4003 let spec_sampling_ok = self.sampler_config.temperature < 1e-6
4004 || match spec_sample_env.as_deref() {
4005 Some("1") => true,
4006 Some(_) => false,
4007 None => metal_graph && spec_cheap_round,
4008 };
4009 let (mut dense_n, mut dense_q4tp) = (0usize, 0usize);
4025 for lw in &self.weights.layers {
4026 if let FfnKind::Dense(d) = &lw.ffn {
4027 dense_n += 1;
4028 if matches!(d.gate_proj.graph_weight(), Some((_, _, 6, _)))
4029 && matches!(d.up_proj.graph_weight(), Some((_, _, 6, _)))
4030 && matches!(d.down_proj.graph_weight(), Some((_, _, 6, _)))
4031 {
4032 dense_q4tp += 1;
4033 }
4034 }
4035 }
4036 let spec_default_ok = dense_n == 0 || dense_q4tp * 10 >= dense_n * 9;
4037 let penalized = !metal_graph
4049 && (self.sampler_config.repetition_penalty != 1.0
4050 || self.sampler_config.presence_penalty != 0.0
4051 || !self.sampler_config.suppress_tokens.is_empty());
4052 #[cfg(feature = "gpu")]
4057 let metal_wgpu = graph_on && crate::gpu_wgpu::wgpu_backend_is_metal();
4058 #[cfg(not(feature = "gpu"))]
4059 let metal_wgpu = false;
4060 let spec_env = std::env::var("CMF_GRAPH_SPEC").ok();
4061 let spec_wanted = match spec_env.as_deref() {
4062 Some("0") => false,
4063 Some(_) => {
4064 if metal_wgpu {
4065 tracing::warn!(
4066 "CMF_GRAPH_SPEC forced on wgpu/Metal: the batched verify graph is not \
4067 verified on this backend (garbage measured on Qwen3.5-0.8B)"
4068 );
4069 }
4070 true
4071 }
4072 None => spec_default_ok && !penalized && !metal_wgpu,
4073 };
4074 let graph_spec = self.speculative
4078 && (graph_on || metal_graph)
4079 && self.mtp.is_some()
4080 && task_mask.is_none()
4081 && !self.o1_active()
4082 && spec_sampling_ok
4083 && spec_wanted;
4084 #[cfg(target_os = "macos")]
4088 if metal_graph {
4089 static SAID: std::sync::Once = std::sync::Once::new();
4090 SAID.call_once(|| {
4091 let spec = if graph_spec {
4092 let k = std::env::var("CMF_GRAPH_SPEC_K")
4093 .ok()
4094 .and_then(|v| v.parse::<usize>().ok())
4095 .filter(|&v| (1..=8).contains(&v))
4096 .unwrap_or(7);
4097 let arm = if self.sampler_config.temperature < 1e-6 {
4098 "greedy"
4099 } else {
4100 "sampling"
4101 };
4102 format!(
4103 "spec k={k} {arm} (batched verify, draft shortlist {}, trial: proxy)",
4104 Self::draft_vocab_rows(usize::MAX)
4105 )
4106 } else if !self.speculative {
4107 "spec off (CMF_MTP=0)".to_string()
4108 } else if self.mtp.is_none() {
4109 "spec off (no MTP head)".to_string()
4110 } else if !spec_sampling_ok {
4111 if spec_cheap_round {
4112 "spec off (CMF_GRAPH_SPEC_SAMPLE=0)".to_string()
4113 } else {
4114 "spec off (sampling without a top-k: the dense chain \
4115 costs more than it saves)"
4116 .to_string()
4117 }
4118 } else if !spec_wanted {
4119 "spec off (CMF_GRAPH_SPEC=0 or non-q4tp FFNs)".to_string()
4120 } else if task_mask.is_some() {
4121 "spec off (task mask)".to_string()
4122 } else {
4123 "spec off (O(1) attention)".to_string()
4124 };
4125 let on = |var: &str| {
4126 if std::env::var(var).as_deref() == Ok("0") {
4127 "off"
4128 } else {
4129 "on"
4130 }
4131 };
4132 tracing::info!(
4133 "metal native: {spec}, state4 {}, async replay {}, prefill graph {}, \
4134 MTP graph {}, attend {}, probe {}",
4135 if crate::gpu_metal::state4_on() { "on" } else { "off" },
4136 if crate::gpu_metal::async_replay_on() { "on" } else { "off" },
4137 on("CMF_METAL_PREFILL"),
4138 on("CMF_MTP_GRAPH"),
4139 std::env::var("CMF_GPU_ATTEND").unwrap_or_else(|_| "auto".into()),
4140 if crate::gpu::probe_enabled() { "bypassed (q1 force)" } else { "off" },
4141 );
4142 });
4143 }
4144 let pair_pays = self.gdn_cfg.is_none() || std::env::var("CMF_MTP").as_deref() == Ok("1");
4151 let spec_active = self.speculative
4152 && self.mtp.is_some()
4153 && task_mask.is_none()
4154 && !self.o1_active()
4155 && ((!graph_on && pair_pays && self.sampler_config.temperature < 1e-6) || graph_spec);
4156 let mut mtp = if spec_active { self.mtp.take() } else { None };
4159 if std::env::var("CMF_MTP_CHAIN_PROBE").is_ok() {
4160 eprintln!(
4161 "mtp-probe gate: spec_active={spec_active} mtp={} speculative={} graph_on={graph_on} temp_ok={}",
4162 mtp.is_some(),
4163 self.speculative,
4164 self.sampler_config.temperature < 1e-6,
4165 );
4166 }
4167 if let Some(m) = &mut mtp {
4168 m.kv.clear();
4169 crate::gpu::graph_kv_reset(self.mtp_kv_id());
4171 self.mtp_graph_mode = None;
4172 }
4173 let mimo_spec = self.speculative
4177 && self.mimo_mtp.is_some()
4178 && task_mask.is_none()
4179 && !self.o1_active()
4180 && self.dyn_router.is_none()
4181 && self.sampler_config.temperature < 1e-6
4182 && std::env::var("CMF_MIMO_MTP").as_deref() != Ok("0");
4183 if let Some(st) = self.mimo_mtp.as_mut() {
4184 st.reset();
4185 if mimo_spec && std::env::var_os("CMF_MIMO_MTP_PROBE").is_some() {
4186 Self::mimo_mtp_hist_cap(st, input_ids.len());
4187 }
4188 }
4189 let mut router = if mtp.is_none() {
4193 self.dyn_router.take()
4194 } else {
4195 None
4196 };
4197 let mut reuse_from = reuse_from;
4198 if let Some(r) = &mut router {
4199 r.reset(); self.dyn_phi_seen = 0; if self.dyn_active.is_some() {
4202 let _ = self.set_active_skill(None);
4205 reuse_from = 0;
4206 self.last_prefill_tokens = input_ids.len();
4207 }
4208 }
4209
4210 let mut all_ids = input_ids.to_vec();
4211 let mut generated = 0usize;
4212 let mut finish_reason = "max_tokens".to_string();
4213 let mut drafted = 0usize;
4214 let mut accepted = 0usize;
4215 let mut dsv4_spec_bad = 0usize;
4222 let mut dsv4_spec_retry_at = 0usize;
4223 let mut confidence: Vec<f32> = Vec::new();
4224 let trace_on = self.trace;
4225 let calib_temp = self.calib_temp;
4226 let mut traces: Vec<TokenTrace> = Vec::new();
4227
4228 let mut hidden = vec![0.0f32; self.hidden_size];
4234 let mut pos = reuse_from;
4235 let fuse_lm = mtp.is_none()
4244 && router.is_none()
4245 && std::env::var("CMF_GPU_LMHEAD").as_deref() != Ok("0");
4246 self.graph_logits = None;
4247 self.graph_want_logits = false;
4248 let _tpf = std::time::Instant::now();
4249 let batch_k = self.generation_batch_k();
4250 if let Some(rows) = prompt_rows {
4251 let hs = self.hidden_size;
4252 let chunk = self.prefill_chunk().max(1);
4253 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4254 let end = (pos + chunk).min(input_ids.len());
4255 let hb = match self.prefill_input_rows(
4256 PrefillIn::Hidden(&rows[pos * hs..end * hs]), pos, task_mask,
4257 ) {
4258 Ok(hb) => hb,
4259 Err(err) => {
4260 self.finish_generation(&mut mtp, &mut router, true);
4261 return Err(err);
4262 }
4263 };
4264 if mimo_spec { self.mimo_note_rows(&hb, pos); }
4265 hidden.copy_from_slice(&hb[hb.len() - hs..]);
4266 pos = end;
4267 }
4268 }
4269 while self.qwen4_exp.is_some()
4280 && mtp.is_none()
4281 && pos < input_ids.len()
4282 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4283 {
4284 let end = (pos + crate::qwen4_exp::prefill_chunk()).min(input_ids.len());
4287 let want_logits = end == input_ids.len();
4288 let mut lg = Vec::new();
4289 if let Some(b) = &mut self.qwen4_exp {
4290 crate::qwen4_exp::forward_tokens(
4291 &b.0,
4292 &b.1,
4293 &b.2,
4294 &mut b.3,
4295 &input_ids[pos..end],
4296 pos,
4297 &self.inv_freq,
4298 self.pool.as_deref(),
4299 &mut lg,
4300 want_logits,
4301 );
4302 }
4303 if want_logits {
4304 self.graph_logits = Some(lg);
4305 }
4306 pos = end;
4307 hidden.fill(0.0);
4308 }
4309 while self.dsv4.is_some()
4310 && mtp.is_none()
4311 && pos < input_ids.len()
4312 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4313 {
4314 let end = (pos + prefill_chunk()).min(input_ids.len());
4315 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4316 let mut lg = Vec::new();
4317 if let Some(b) = &mut self.dsv4 {
4318 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
4319 crate::dsv4::forward_chunk(
4320 g,
4321 layers,
4322 &cfg,
4323 st,
4324 &ids,
4325 pos,
4326 &self.inv_freq,
4327 self.pool.as_deref(),
4328 &mut lg,
4329 end == input_ids.len(),
4330 );
4331 }
4332 if end == input_ids.len() {
4333 self.graph_logits = Some(lg);
4334 }
4335 pos = end;
4336 hidden = vec![0.0; self.hidden_size];
4337 }
4338 let dsv41_prefill = self.dsv41_prefill.take();
4339 while self.dsv41.is_some()
4340 && mtp.is_none()
4341 && pos < input_ids.len()
4342 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4343 {
4344 let end = (pos + prefill_chunk()).min(input_ids.len());
4345 let ids: Vec<u32> = input_ids[pos..end].to_vec();
4346 let mut lg = Vec::new();
4347 if let Some(b) = &mut self.dsv41 {
4348 let (g, layers, cfg, st) = (&b.0, &b.1, &b.2, &mut b.3);
4349 if let Some((embeddings, participates)) = dsv41_prefill.as_ref() {
4350 crate::dsv41::forward_chunk_masked_with_embeddings(
4351 g,
4352 layers,
4353 cfg,
4354 st,
4355 &ids,
4356 pos,
4357 &embeddings[pos..end],
4358 &participates[pos..end],
4359 self.pool.as_deref(),
4360 &mut lg,
4361 );
4362 } else {
4363 crate::dsv41::forward_chunk(
4364 g,
4365 layers,
4366 cfg,
4367 st,
4368 &ids,
4369 pos,
4370 self.pool.as_deref(),
4371 &mut lg,
4372 );
4373 }
4374 }
4375 if end == input_ids.len() {
4376 self.graph_logits = Some(lg);
4377 }
4378 pos = end;
4379 hidden = vec![0.0; self.hidden_size];
4380 }
4381 let dyn_prefill = router.is_some();
4386 let o1_prefill_limit = o1_prefill
4394 .and_then(|requested| self.o1_effective_boundary(requested))
4395 .map(|boundary| boundary.min(input_ids.len()));
4396 let mut o1_sealed = false;
4397 if let Some(limit) = o1_prefill_limit {
4398 if self.can_prefill_batched() && limit > 2 {
4401 let chunk = self.prefill_chunk();
4402 let hs = self.hidden_size;
4403 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4404 let end = (pos + chunk).min(limit);
4405 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4406 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4407 pos = end;
4408 }
4409 } else {
4410 while pos < limit && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4411 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, None);
4412 pos += 1;
4413 }
4414 }
4415 if pos >= limit {
4416 o1_sealed = match self.o1_seal_checked() {
4417 Ok(sealed) => sealed,
4418 Err(err) => {
4419 self.finish_generation(&mut mtp, &mut router, true);
4420 return Err(err);
4421 }
4422 };
4423 tracing::info!(
4424 "o1 bounded prompt prefix: requested={} effective={} processed={} of {} token(s)",
4425 o1_prefill.unwrap_or(0),
4426 self.o1_effective_boundary(o1_prefill.unwrap_or(0))
4427 .unwrap_or(limit),
4428 limit,
4429 input_ids.len()
4430 );
4431 }
4432 }
4433 let graph_prefill = self.graph_prefill_preferred();
4439 #[cfg(target_os = "macos")]
4447 if task_mask.is_none()
4448 && !dyn_prefill
4449 && (crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in())
4450 && crate::gpu::enabled_here()
4451 && self.gdn_cfg.is_some()
4452 && self.g3n.is_none()
4453 && input_ids.len() > 8
4454 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err()
4455 && std::env::var("CMF_METAL_PREFILL").as_deref() != Ok("0")
4456 {
4457 let chunk: usize = std::env::var("CMF_METAL_PREFILL_CHUNK")
4458 .ok()
4459 .and_then(|v| v.parse().ok())
4460 .filter(|&v| (16..=512).contains(&v))
4461 .unwrap_or(256);
4462 let hs = self.hidden_size;
4463 let _tp = std::time::Instant::now();
4464 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4465 let end = (pos + chunk).min(input_ids.len());
4466 let hb = match self.prefill_batch_metal(&input_ids[pos..end], pos) {
4467 MetalPrefillOutcome::Completed(hb) => hb,
4468 MetalPrefillOutcome::Declined => break,
4469 MetalPrefillOutcome::Failed => {
4470 self.finish_generation(&mut mtp, &mut router, true);
4471 return Err("ordinary Metal prefill failed after admission".into());
4472 }
4473 };
4474 if let Some(m) = &mut mtp {
4475 let n_pairs = if end < input_ids.len() {
4476 end - pos
4477 } else {
4478 end - pos - 1
4479 };
4480 if n_pairs > 0 {
4481 let pairs: Vec<(&[f32], u32)> = (0..n_pairs)
4482 .map(|j| (&hb[j * hs..(j + 1) * hs], input_ids[pos + j + 1]))
4483 .collect();
4484 if !self.mtp_warm_batch_metal(m, &pairs, pos) {
4485 for (j, (h, t)) in pairs.iter().enumerate() {
4486 let h = h.to_vec();
4487 let _ = self.mtp_step(m, &h, *t, pos + j);
4488 }
4489 }
4490 }
4491 }
4492 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4493 pos = end;
4494 }
4495 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4496 eprintln!(
4497 "metal-prefill: {} of {} tokens in {:.1} ms",
4498 pos,
4499 input_ids.len(),
4500 _tp.elapsed().as_secs_f64() * 1e3
4501 );
4502 }
4503 }
4504 self.mimo_moe_prepare();
4505 #[cfg(not(target_os = "macos"))]
4511 if task_mask.is_none()
4512 && !dyn_prefill
4513 && !graph_prefill
4514 && mtp.is_none()
4515 && o1_prefill.is_none()
4516 && !self.o1_active()
4517 && input_ids.len() > 2
4518 && self.batch_prefix_prefill()
4519 {
4520 let chunk = self.prefill_chunk().max(1);
4521 let hs = self.hidden_size;
4522 let t_bp = std::time::Instant::now();
4523 let pos0 = pos;
4524 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4525 let end = (pos + chunk).min(input_ids.len());
4526 let bk = end - pos;
4527 let mut hiddens = vec![0f32; bk * hs];
4528 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4529 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4530 }
4531 let positions: Vec<usize> = (pos..end).collect();
4532 let mut run = 0usize;
4533 let outcome = self.try_batch_graph_wgpu_prefix(
4534 &mut hiddens,
4535 &positions,
4536 bk,
4537 None,
4538 Some(&mut run),
4539 );
4540 match outcome {
4541 crate::gpu::BatchGraphOutcome::Completed => {
4542 let hb = if run < self.num_layers {
4543 self.prefill_batch_span(
4544 PrefillIn::Hidden(&hiddens),
4545 pos,
4546 None,
4547 run,
4548 self.num_layers,
4549 )
4550 } else {
4551 hiddens
4552 };
4553 if mimo_spec {
4554 self.mimo_note_rows(&hb, pos);
4555 }
4556 hidden.copy_from_slice(&hb[(bk - 1) * hs..]);
4557 pos = end;
4558 }
4559 crate::gpu::BatchGraphOutcome::Failed => {
4560 self.finish_generation(&mut mtp, &mut router, true);
4561 return Err("batched prefix prefill failed after admission".into());
4562 }
4563 crate::gpu::BatchGraphOutcome::Declined => {
4564 #[cfg(feature = "gpu")]
4567 if pos > pos0 {
4568 self.pull_lagging_host_kv(0, self.num_layers, pos);
4569 }
4570 break;
4571 }
4572 }
4573 }
4574 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4575 eprintln!(
4576 "batch-prefix prefill: {} of {} tokens in {:.1} ms",
4577 pos - pos0,
4578 input_ids.len(),
4579 t_bp.elapsed().as_secs_f64() * 1e3
4580 );
4581 }
4582 }
4583 if task_mask.is_none()
4584 && !dyn_prefill
4585 && !graph_prefill
4586 && self.can_prefill_batched()
4587 && self.g3n.is_none()
4588 && o1_prefill.is_none()
4589 && input_ids.len() > 2
4590 {
4591 let chunk = self.prefill_chunk();
4597 let hs = self.hidden_size;
4598 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4599 let end = (pos + chunk).min(input_ids.len());
4600 let hb = self.prefill_batch(&input_ids[pos..end], pos);
4601 if mimo_spec {
4602 self.mimo_note_rows(&hb, pos);
4603 }
4604 if let Some(m) = &mut mtp {
4605 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4606 .ok()
4607 .and_then(|v| v.parse().ok())
4608 .unwrap_or(0);
4609 for p in pos..end {
4610 if p + 1 < input_ids.len() {
4611 if probe >= 1 && p + 2 < input_ids.len() {
4612 let (d1, mut hx) = self.mtp_step_h(
4616 m,
4617 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4618 input_ids[p + 1],
4619 p,
4620 );
4621 let mut ok = d1 == input_ids[p + 2];
4622 Self::chain_probe_note(0, ok);
4623 let mut d_prev = d1;
4624 let mut extra = 0usize;
4625 for j in 1..probe {
4626 if p + 2 + j >= input_ids.len() {
4627 break;
4628 }
4629 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, p + 1 + j);
4630 extra += 1;
4631 ok = ok && dj == input_ids[p + 2 + j];
4632 Self::chain_probe_note(j, ok);
4633 d_prev = dj;
4634 hx = hj;
4635 }
4636 m.kv.truncate_last(extra);
4637 } else {
4638 let _ = self.mtp_step(
4639 m,
4640 &hb[(p - pos) * hs..(p - pos + 1) * hs],
4641 input_ids[p + 1],
4642 p,
4643 );
4644 }
4645 }
4646 }
4647 }
4648 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
4649 pos = end;
4650 }
4651 }
4652 let pair_off = std::env::var("CMF_PAIR").is_ok_and(|v| v == "0");
4653 if task_mask.is_none()
4654 && !dyn_prefill
4655 && !graph_prefill
4656 && !pair_off
4657 && self.pair_supported()
4658 && o1_prefill.is_none()
4659 {
4660 while pos + 1 < input_ids.len()
4661 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4662 {
4663 let e1 = self.embed_single(input_ids[pos]);
4664 let e2 = self.embed_single(input_ids[pos + 1]);
4665 let (h1, h2) = self.forward_pair(&e1, &e2, pos);
4666 if mimo_spec {
4667 self.mimo_note_rows(&h1, pos);
4668 self.mimo_note_rows(&h2, pos + 1);
4669 }
4670 self.commit_linear_scratch();
4672 if let Some(m) = &mut mtp {
4673 let _ = self.mtp_step(m, &h1, input_ids[pos + 1], pos);
4674 if pos + 2 < input_ids.len() {
4675 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4676 .ok()
4677 .and_then(|v| v.parse().ok())
4678 .unwrap_or(0);
4679 if probe >= 1 && pos + 3 < input_ids.len() {
4680 let (d1, mut hx) = self.mtp_step_h(m, &h2, input_ids[pos + 2], pos + 1);
4684 let mut ok = d1 == input_ids[pos + 3];
4685 Self::chain_probe_note(0, ok);
4686 let mut d_prev = d1;
4687 let mut extra = 0usize;
4688 for j in 1..probe {
4689 if pos + 3 + j >= input_ids.len() {
4690 break;
4691 }
4692 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 2 + j);
4693 extra += 1;
4694 ok = ok && dj == input_ids[pos + 3 + j];
4695 Self::chain_probe_note(j, ok);
4696 d_prev = dj;
4697 hx = hj;
4698 }
4699 m.kv.truncate_last(extra);
4700 } else {
4701 let _ = self.mtp_step(m, &h2, input_ids[pos + 2], pos + 1);
4702 }
4703 }
4704 }
4705 hidden = h2;
4706 pos += 2;
4707 }
4708 }
4709 let o1_batch_ready = o1_sealed
4722 && o1_prefill.is_some()
4723 && mtp.is_none()
4724 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
4725 && (0..self.num_layers).all(|li| {
4726 let cache = &self.kv_cache.layers[self.phys_layer(li)];
4727 cache.o1.is_none() || cache.o1_views().is_some()
4728 });
4729 let mtp_batch_prefill = mtp.is_some()
4734 && graph_prefill
4735 && task_mask.is_none()
4736 && !dyn_prefill
4737 && !self.o1_active()
4738 && std::env::var("CMF_MTP_CHAIN_PROBE").is_err();
4739 if batch_k > 0
4740 && (graph_prefill || o1_batch_ready)
4741 && task_mask.is_none()
4742 && (!self.o1_active() || o1_batch_ready)
4743 && (mtp.is_none() || mtp_batch_prefill)
4744 && !dyn_prefill
4745 && pos + 1 < input_ids.len()
4746 {
4747 let hs = self.hidden_size;
4748 let chunk = batch_k;
4749 while pos < input_ids.len() {
4750 let end = (pos + chunk).min(input_ids.len());
4751 let bk = end - pos;
4752 let mut hiddens = vec![0f32; bk * hs];
4753 for (j, &id) in input_ids[pos..end].iter().enumerate() {
4754 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&self.embed_single(id));
4755 }
4756 let positions: Vec<usize> = (pos..end).collect();
4757 let t_chunk = std::time::Instant::now();
4758 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
4759 let ok_b = outcome == crate::gpu::BatchGraphOutcome::Completed;
4760 if std::env::var("CMF_GRAPH_PROF").is_ok() {
4761 let ms = t_chunk.elapsed().as_secs_f64() * 1000.0;
4762 eprintln!(
4763 "batch-chunk: phase=prompt mode={} k={bk} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
4764 if o1_batch_ready {
4765 "o1"
4766 } else if mtp_batch_prefill {
4767 "ordinary_mtp"
4768 } else {
4769 "ordinary"
4770 },
4771 bk as f64 / (ms / 1000.0)
4772 );
4773 }
4774 {
4775 use std::sync::atomic::{AtomicBool, Ordering};
4776 static SAID: AtomicBool = AtomicBool::new(false);
4777 if !SAID.swap(true, Ordering::Relaxed) {
4778 if ok_b {
4779 tracing::info!(
4780 "batched prefill: ACTIVE mode={} (k={bk})",
4781 if o1_batch_ready {
4782 "o1"
4783 } else if mtp_batch_prefill {
4784 "ordinary_mtp"
4785 } else {
4786 "ordinary"
4787 }
4788 );
4789 } else {
4790 tracing::warn!("batched prefill {:?} — per-position graph", outcome);
4791 }
4792 }
4793 }
4794 if ok_b {
4795 if mimo_spec {
4796 self.mimo_note_rows(&hiddens, pos);
4797 }
4798 if mtp_batch_prefill {
4799 let n_pairs = mtp_prefill_pair_count(pos, end, input_ids.len());
4800 if n_pairs > 0 {
4801 let rows: Vec<Vec<f32>> = (0..n_pairs)
4807 .map(|j| hiddens[j * hs..(j + 1) * hs].to_vec())
4808 .collect();
4809 let pairs: Vec<(&[f32], u32)> = rows
4810 .iter()
4811 .enumerate()
4812 .map(|(j, row)| (row.as_slice(), input_ids[pos + j + 1]))
4813 .collect();
4814 if std::env::var("CMF_GRAPH_PROF").is_ok() {
4815 eprintln!(
4816 "mtp-warm: phase=prompt mode=ordinary_mtp first_pos={} pairs={} last_pos={}",
4817 pos,
4818 n_pairs,
4819 pos + n_pairs - 1,
4820 );
4821 }
4822 let warm_error = if let Some(m) = mtp.as_mut() {
4823 self.mtp_warm_prefill_pairs(m, &pairs, pos).err()
4824 } else {
4825 None
4826 };
4827 if let Some(err) = warm_error {
4828 self.finish_generation(&mut mtp, &mut router, true);
4833 return Err(err.to_string());
4834 }
4835 }
4836 }
4837 hidden.copy_from_slice(&hiddens[(bk - 1) * hs..]);
4838 pos = end;
4839 } else if outcome == crate::gpu::BatchGraphOutcome::Failed {
4840 self.finish_generation(&mut mtp, &mut router, true);
4845 return Err(if o1_batch_ready {
4846 "sealed O(1) batch graph failed after admission".to_string()
4847 } else {
4848 "ordinary recurrent batch graph failed after admission".to_string()
4849 });
4850 } else {
4851 break; }
4853 }
4854 }
4855 if graph_prefill
4859 && task_mask.is_none()
4860 && mtp.is_none()
4861 && !dyn_prefill
4862 && pos == 0
4863 && input_ids.len() > 1
4864 && !self.cancel.load(std::sync::atomic::Ordering::Relaxed)
4865 {
4866 if let Some(lg) = self.embryo_prefill_chunked(input_ids, 0) {
4867 self.graph_logits = Some(lg);
4868 hidden = vec![0.0; self.hidden_size];
4869 pos = input_ids.len();
4870 }
4871 }
4872 while pos < input_ids.len() && !self.cancel.load(std::sync::atomic::Ordering::Relaxed) {
4873 self.graph_want_logits = fuse_lm && pos + 1 == input_ids.len();
4874 hidden = self.forward_layers(&self.embed_single(input_ids[pos]), pos, task_mask);
4875 if mimo_spec {
4876 self.mimo_note_rows(&hidden, pos);
4877 }
4878 if let Some(m) = &mut mtp {
4879 if pos + 1 < input_ids.len() {
4880 let probe: usize = std::env::var("CMF_MTP_CHAIN_PROBE")
4886 .ok()
4887 .and_then(|v| v.parse().ok())
4888 .unwrap_or(0);
4889 if probe >= 1 && pos + 2 < input_ids.len() {
4890 let (d1, mut hx) = self.mtp_step_h(m, &hidden, input_ids[pos + 1], pos);
4891 let mut ok = d1 == input_ids[pos + 2];
4892 Self::chain_probe_note(0, ok);
4893 let mut d_prev = d1;
4894 let mut extra = 0usize;
4895 for j in 1..probe {
4896 if pos + 2 + j >= input_ids.len() {
4897 break;
4898 }
4899 let (dj, hj) = self.mtp_step_h(m, &hx, d_prev, pos + 1 + j);
4900 extra += 1;
4901 ok = ok && dj == input_ids[pos + 2 + j];
4902 Self::chain_probe_note(j, ok);
4903 d_prev = dj;
4904 hx = hj;
4905 }
4906 m.kv.truncate_last(extra);
4909 } else {
4910 let _ = self.mtp_step(m, &hidden, input_ids[pos + 1], pos);
4911 }
4912 }
4913 }
4914 pos += 1;
4915 }
4916 if std::env::var("CMF_PREFILL_PROF").is_ok() {
4917 eprintln!(
4918 "prefill: {} tokens in {:.1} ms (batch_k={batch_k})",
4919 input_ids.len(),
4920 _tpf.elapsed().as_secs_f64() * 1000.0
4921 );
4922 }
4923 if self
4924 .graph_failed
4925 .swap(false, std::sync::atomic::Ordering::Relaxed)
4926 {
4927 self.finish_generation(&mut mtp, &mut router, true);
4932 return Err("GPU token graph failed during prefill".to_string());
4933 }
4934 if self
4937 .cancel
4938 .swap(false, std::sync::atomic::Ordering::Relaxed)
4939 {
4940 self.finish_generation(&mut mtp, &mut router, true);
4944 return Ok(GenerateResult {
4945 text: String::new(),
4946 token_ids: Vec::new(),
4947 prompt_tokens: input_ids.len(),
4948 tokens_generated: 0,
4949 finish_reason: "cancelled".to_string(),
4950 mtp_drafted: 0,
4951 mtp_accepted: 0,
4952 token_confidence: Vec::new(),
4953 traces: Vec::new(),
4954 });
4955 }
4956
4957 if !o1_sealed {
4960 match self.o1_seal_checked() {
4961 Ok(_) => {}
4962 Err(err) => {
4963 self.finish_generation(&mut mtp, &mut router, true);
4964 return Err(err);
4965 }
4966 }
4967 }
4968
4969 macro_rules! commit {
4971 ($id:expr) => {{
4972 all_ids.push($id);
4973 generated += 1;
4974 self.note_draft_id($id);
4975 if self.tokenizer.is_eos($id) && !self.ignore_eos {
4976 finish_reason = "stop".to_string();
4977 false
4978 } else {
4979 let token_text = self.tokenizer.decode_token($id);
4980 let mut go = true;
4981 if let Some(ref mut cb) = on_token {
4982 if !cb(&token_text) {
4983 finish_reason = "cancelled".to_string();
4984 go = false;
4985 }
4986 }
4987 go
4988 }
4989 }};
4990 }
4991
4992 let mut spec_trial = SpecTrial::Spec {
5003 t0: std::time::Instant::now(),
5004 gen0: generated,
5005 rounds: 0,
5006 };
5007 let mut spec_mon = SpecMon {
5013 metal: graph_spec && crate::gpu::q1_force() && spec_cheap_round,
5014 ..SpecMon::default()
5015 };
5016 let mut spec_watchdog_off = false;
5017 let mut spec_walls: Vec<f32> = Vec::new();
5020 let mut spec_round_end: Option<std::time::Instant> = None;
5023 if mimo_spec {
5024 if let Ok(path) = std::env::var("CMF_MIMO_MTP_PROBE") {
5025 if let Some(mut st) = self.mimo_mtp.take() {
5026 self.mimo_mtp_probe(&mut st, input_ids, &path);
5027 self.mimo_mtp = Some(st);
5028 }
5029 }
5030 }
5031 let mut next_pos = input_ids.len();
5033 'decode: while generated < max_tokens {
5034 if self
5035 .graph_failed
5036 .swap(false, std::sync::atomic::Ordering::Relaxed)
5037 {
5038 self.finish_generation(&mut mtp, &mut router, true);
5043 return Err("GPU token graph failed during decode".to_string());
5044 }
5045 if self
5046 .cancel
5047 .swap(false, std::sync::atomic::Ordering::Relaxed)
5048 {
5049 finish_reason = "cancelled".to_string();
5050 break 'decode;
5051 }
5052 if mimo_spec && next_pos > 0 {
5057 self.mimo_note_rows(&hidden, next_pos - 1);
5060 }
5061 let forced = self.spec_forced.take();
5062 let mut logits = match (forced, self.graph_logits.take()) {
5063 (Some(_), _) => Vec::new(),
5064 (None, Some(lg)) => lg,
5065 (None, None) => {
5066 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Head);
5067 inference::rms_norm_into(
5068 &hidden,
5069 &self.weights.final_norm,
5070 self.rms_eps,
5071 self.norm_style,
5072 &mut self.ws.n1,
5073 );
5074 self.lm_head_forward(&self.ws.n1)
5075 }
5076 };
5077 if generated
5080 == std::env::var("CMF_LOGIT_DUMP_STEP")
5081 .ok()
5082 .and_then(|v| v.parse().ok())
5083 .unwrap_or(0)
5084 {
5085 if let Ok(path) = std::env::var("CMF_LOGIT_DUMP") {
5086 let mut bytes: Vec<u8> = Vec::with_capacity((hidden.len() + logits.len()) * 4);
5087 for v in hidden.iter().chain(logits.iter()) {
5088 bytes.extend_from_slice(&v.to_le_bytes());
5089 }
5090 if let Err(e) = std::fs::write(&path, &bytes) {
5091 eprintln!("logit dump: failed to write {path}: {e}");
5092 self.finish_generation(&mut mtp, &mut router, true);
5093 return Err(format!("logit dump write failed: {e}"));
5094 }
5095 }
5096 }
5097 if let Ok(dir) = std::env::var("CMF_LOGIT_DUMP_ALL") {
5101 if !logits.is_empty() {
5102 let path = std::path::Path::new(&dir).join(format!("step{generated:05}.f32"));
5103 let bytes: Vec<u8> = logits.iter().flat_map(|v| v.to_le_bytes()).collect();
5104 if let Err(e) =
5105 std::fs::create_dir_all(&dir).and_then(|_| std::fs::write(&path, &bytes))
5106 {
5107 eprintln!("logit dump: failed to write {}: {e}", path.display());
5108 }
5109 }
5110 }
5111 let t_next = match forced {
5112 Some(c) => c,
5113 None => {
5114 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::Sampler);
5115 sampler::sample_with_scratch_pool(
5116 &logits,
5117 &self.sampler_config,
5118 self.sampler_config.penalty_past(&all_ids, bounded_native),
5119 &mut self.rng,
5120 &mut self.sampler_scratch,
5121 self.pool.as_deref(),
5122 )
5123 }
5124 };
5125 if self.confidence_on {
5126 confidence.push(if logits.is_empty() {
5127 0.0
5128 } else {
5129 sampler::top1_prob_pool(
5130 self.pool.as_deref(),
5131 &mut self.sampler_scratch,
5132 &logits,
5133 t_next,
5134 calib_temp,
5135 )
5136 });
5137 }
5138 if !logits.is_empty() {
5139 attention::recycle_buf(&mut logits);
5140 }
5141 if trace_on {
5142 let skill = router.as_ref().and_then(|r| r.active_id());
5146 traces.push(TokenTrace {
5147 t: generated,
5148 token_id: t_next,
5149 confidence: confidence.last().copied().unwrap_or(0.0),
5150 active_skill: skill,
5151 recon: None,
5152 switched: false,
5153 });
5154 }
5155 if !commit!(t_next) {
5156 break 'decode;
5157 }
5158 if generated >= max_tokens {
5159 break 'decode;
5160 }
5161
5162 self.swa_trim_tails();
5165 if self.dsv41.is_none() && self.kv_cache.needs_eviction() {
5166 static SAID: std::sync::Once = std::sync::Once::new();
5172 SAID.call_once(|| {
5173 tracing::warn!(
5174 "KV cache full at {} positions — evicting half; quality \
5175 will degrade. Raise CMF_MAX_SEQ.",
5176 self.kv_cache.max_seq_len,
5177 );
5178 });
5179 let keep = (self.kv_cache.max_seq_len / 2).max(1);
5180 self.kv_cache.evict(keep);
5181 }
5182
5183 if graph_spec {
5186 match spec_trial {
5187 SpecTrial::Plain { t0, gen0 } if spec_mon.plain_done(t0, gen0, generated) => {
5188 spec_mon.plain_ms =
5189 t0.elapsed().as_secs_f64() * 1e3 / (generated - gen0) as f64;
5190 let keep = spec_mon.pays();
5191 tracing::info!(
5192 "speculation trial: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5193 spec_mon.tokens,
5194 spec_mon.round_ms,
5195 spec_mon.plain_ms,
5196 if keep { "speculating" } else { "plain" }
5197 );
5198 spec_mon.fails = 0;
5199 spec_trial = SpecTrial::Decided {
5200 spec: keep,
5201 recheck_at: if keep { usize::MAX } else { generated + 128 },
5202 };
5203 }
5204 SpecTrial::Decided { recheck_at, .. } if generated >= recheck_at => {
5205 spec_mon.n = 0;
5206 spec_trial = SpecTrial::Spec {
5207 t0: std::time::Instant::now(),
5208 gen0: generated,
5209 rounds: 0,
5210 };
5211 }
5212 _ => {}
5213 }
5214 spec_watchdog_off = matches!(
5215 spec_trial,
5216 SpecTrial::Plain { .. } | SpecTrial::Decided { spec: false, .. }
5217 );
5218 }
5219 if mimo_spec && generated + 1 < max_tokens && next_pos > 0 {
5221 let budget = max_tokens - generated - 1;
5222 if let Some(mut st) = self.mimo_mtp.take() {
5223 let k = st.depth.min(budget);
5224 let r = self.mimo_spec_round(&mut st, next_pos, &all_ids, k);
5225 self.mimo_mtp = Some(st);
5226 let r = match r {
5227 Ok(r) => r,
5228 Err(err) => {
5229 self.finish_generation(&mut mtp, &mut router, true);
5230 return Err(err);
5231 }
5232 };
5233 if let Some(r) = r {
5234 drafted += r.drafted;
5235 accepted += r.accepted.len();
5236 let mut stopped = false;
5237 for &id in &r.accepted {
5238 if self.confidence_on {
5239 confidence.push(0.0);
5240 }
5241 if !commit!(id) {
5242 stopped = true;
5243 break;
5244 }
5245 }
5246 if stopped {
5247 break 'decode;
5248 }
5249 next_pos += r.accepted.len() + 1;
5250 hidden = r.hidden;
5251 self.graph_logits = Some(r.logits);
5254 continue 'decode;
5255 }
5256 }
5257 }
5258 #[cfg(feature = "gpu")]
5261 if self.speculative
5262 && self.qwen4_exp.is_some()
5263 && task_mask.is_none()
5264 && self.sampler_config.temperature < 1e-6
5265 && generated + 1 < max_tokens
5266 && next_pos > 0
5267 && std::env::var("CMF_QWEN_MTP").as_deref() != Ok("0")
5268 {
5269 let r = match &mut self.qwen4_exp {
5270 Some(b) => crate::qwen4_exp::spec_round(
5271 &b.0,
5272 &b.1,
5273 &b.2,
5274 &mut b.3,
5275 next_pos,
5276 &all_ids,
5277 &self.inv_freq,
5278 self.pool.as_deref(),
5279 ),
5280 None => None,
5281 };
5282 if let Some(r) = r {
5283 drafted += r.drafted;
5284 accepted += r.accepted.len();
5285 let mut stopped = false;
5286 for &id in &r.accepted {
5287 if self.confidence_on {
5288 confidence.push(0.0);
5289 }
5290 if !commit!(id) {
5291 stopped = true;
5292 break;
5293 }
5294 }
5295 if stopped {
5296 break 'decode;
5297 }
5298 next_pos += r.accepted.len() + 1;
5299 hidden.fill(0.0);
5300 self.graph_logits = Some(r.logits);
5301 continue 'decode;
5302 }
5303 }
5304 match &mut mtp {
5305 #[cfg(feature = "gpu")]
5307 Some(m)
5308 if graph_spec
5309 && !spec_watchdog_off
5310 && generated + 1 < max_tokens
5311 && next_pos > 0 =>
5312 {
5313 let t_round = std::time::Instant::now();
5314 if spec_time_level() >= 2 {
5315 if let Some(t) = spec_round_end.take() {
5316 eprintln!(
5317 "spec-gap {:.2} ms (host between rounds)",
5318 t.elapsed().as_secs_f64() * 1e3
5319 );
5320 }
5321 }
5322 spec_stamps_begin();
5323 #[cfg(target_os = "macos")]
5328 let allocs0 = crate::gpu_metal::IO_BUF_ALLOCS
5329 .load(std::sync::atomic::Ordering::Relaxed);
5330 #[cfg(not(target_os = "macos"))]
5331 let allocs0 = 0u64;
5332 if let Some((extra, n_pos, new_h)) = self.graph_spec_step(
5333 m,
5334 &hidden,
5335 t_next,
5336 next_pos,
5337 &mut drafted,
5338 &mut accepted,
5339 &mut all_ids,
5340 max_tokens - generated,
5341 ) {
5342 next_pos = n_pos;
5343 hidden = new_h;
5344 let level = spec_time_level();
5345 if level > 0 {
5346 let wall = t_round.elapsed().as_secs_f32() * 1e3;
5347 let stamps = spec_stamps_take();
5348 let median = if spec_walls.len() >= 3 {
5351 let mut s = spec_walls.clone();
5352 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5353 Some(s[s.len() / 2])
5354 } else {
5355 None
5356 };
5357 let outlier = median.is_some_and(|m| wall > 1.4 * m);
5358 #[cfg(target_os = "macos")]
5359 let allocs = crate::gpu_metal::IO_BUF_ALLOCS
5360 .load(std::sync::atomic::Ordering::Relaxed)
5361 - allocs0;
5362 #[cfg(not(target_os = "macos"))]
5363 let allocs = allocs0;
5364 eprintln!(
5365 "spec-round wall {wall:.1} ms → {} tokens{}{}",
5366 extra.len() + 1,
5367 if allocs > 0 {
5368 format!(" [{allocs} new device buffers]")
5369 } else {
5370 String::new()
5371 },
5372 match (outlier, median) {
5373 (true, Some(m)) => format!(" OUTLIER (median {m:.1})"),
5374 _ => String::new(),
5375 }
5376 );
5377 if level >= 2 || outlier {
5378 let sum: f32 = stamps.iter().map(|s| s.1).sum();
5379 eprintln!(
5380 "spec-stamps: {}| untracked {:.1}",
5381 spec_stamps_format(&stamps),
5382 wall - sum
5383 );
5384 }
5385 if spec_mon.n >= 1 {
5386 spec_walls.push(wall);
5387 }
5388 }
5389 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, extra.len() + 1);
5393 spec_trial = Self::spec_trial_round(
5396 spec_trial,
5397 &mut spec_mon,
5398 generated + extra.len() + 1,
5399 );
5400 let mut stopped = false;
5401 for &id in &extra {
5402 if self.confidence_on {
5403 confidence.push(0.0);
5404 }
5405 if !commit!(id) {
5406 stopped = true;
5407 break;
5408 }
5409 }
5410 if stopped {
5411 break 'decode;
5412 }
5413 if spec_time_level() >= 2 {
5414 spec_round_end = Some(std::time::Instant::now());
5415 }
5416 continue 'decode;
5417 }
5418 if self
5419 .graph_failed
5420 .swap(false, std::sync::atomic::Ordering::Relaxed)
5421 {
5422 self.finish_generation(&mut mtp, &mut router, true);
5428 return Err("GPU MTP graph failed during speculative decode".to_string());
5429 }
5430 spec_mon.round(t_round.elapsed().as_secs_f64() * 1e3, 1);
5441 spec_mon.tokens = 0.0;
5442 spec_mon.fails = 3;
5443 spec_trial = Self::spec_trial_round(spec_trial, &mut spec_mon, generated + 1);
5444 hidden = self.forward_layers(&self.embed_single(t_next), next_pos, task_mask);
5445 next_pos += 1;
5446 continue 'decode;
5447 }
5448 Some(m) if !graph_spec && generated + 1 < max_tokens => {
5450 let draft = self.mtp_step(m, &hidden, t_next, next_pos - 1);
5451 drafted += 1;
5452 let emb1 = self.embed_single(t_next);
5453 let emb2 = self.embed_single(draft);
5454 let (h1, h2) = self.forward_pair(&emb1, &emb2, next_pos);
5455
5456 inference::rms_norm_into(
5457 &h1,
5458 &self.weights.final_norm,
5459 self.rms_eps,
5460 self.norm_style,
5461 &mut self.ws.n1,
5462 );
5463 let mut logits1 = self.lm_head_forward(&self.ws.n1);
5464 let t_after = sampler::sample_with_scratch_pool(
5465 &logits1,
5466 &self.sampler_config,
5467 self.sampler_config.penalty_past(&all_ids, bounded_native),
5468 &mut self.rng,
5469 &mut self.sampler_scratch,
5470 self.pool.as_deref(),
5471 );
5472 if self.confidence_on {
5473 confidence.push(sampler::top1_prob_pool(
5474 self.pool.as_deref(),
5475 &mut self.sampler_scratch,
5476 &logits1,
5477 t_after,
5478 calib_temp,
5479 ));
5480 }
5481 attention::recycle_buf(&mut logits1);
5482 if trace_on {
5483 traces.push(TokenTrace {
5486 t: generated,
5487 token_id: t_after,
5488 confidence: confidence.last().copied().unwrap_or(0.0),
5489 active_skill: None,
5490 recon: None,
5491 switched: false,
5492 });
5493 }
5494 let stop = !commit!(t_after);
5495
5496 if t_after == draft {
5497 accepted += 1;
5498 self.commit_linear_scratch();
5499 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5500 hidden = h2;
5501 next_pos += 2;
5502 } else {
5503 for layer in &mut self.kv_cache.layers {
5505 layer.truncate_last(1);
5506 }
5507 if !stop {
5508 let _ = self.mtp_step(m, &h1, t_after, next_pos);
5509 hidden = self.forward_layers(
5510 &self.embed_single(t_after),
5511 next_pos + 1,
5512 None,
5513 );
5514 }
5515 next_pos += 2;
5516 }
5517 if stop {
5518 break 'decode;
5519 }
5520 }
5521 _ => {
5523 #[cfg(feature = "gpu")]
5528 if Self::dsv4_spec_on() && self.dsv4.is_some() {
5529 static SAID: std::sync::Once = std::sync::Once::new();
5530 SAID.call_once(|| {
5531 eprintln!(
5532 "dsv4-spec гейт: mtp={} mask={} router={} trace={} temp={} rep={} ",
5533 !self.dsv4_mtp.is_empty(),
5534 task_mask.is_none(),
5535 router.is_none(),
5536 !trace_on,
5537 self.sampler_config.temperature < 1e-6,
5538 self.sampler_config.repetition_penalty == 1.0,
5539 );
5540 });
5541 }
5542 #[cfg(feature = "gpu")]
5543 if Self::dsv4_spec_on()
5544 && self.dsv4.is_some()
5545 && !self.dsv4_mtp.is_empty()
5546 && task_mask.is_none()
5547 && router.is_none()
5548 && !trace_on
5549 && self.sampler_config.temperature < 1e-6
5550 && self.sampler_config.repetition_penalty == 1.0
5551 && generated + 1 < max_tokens
5552 && all_ids.len() >= 2
5553 && generated >= dsv4_spec_retry_at
5554 {
5555 let tip_token = all_ids[all_ids.len() - 2];
5556 let drafted0 = drafted;
5557 let round = self.dsv4_spec_step(
5558 tip_token,
5559 t_next,
5560 next_pos,
5561 max_tokens.saturating_sub(generated),
5562 &mut drafted,
5563 &mut accepted,
5564 );
5565 if drafted > drafted0 {
5566 let useful = round.as_ref().is_some_and(|(extra, _)| !extra.is_empty());
5567 if useful {
5568 dsv4_spec_bad = 0;
5569 } else {
5570 dsv4_spec_bad += 1;
5571 if dsv4_spec_bad >= 2 {
5572 dsv4_spec_bad = 0;
5573 dsv4_spec_retry_at = generated.saturating_add(32);
5574 tracing::info!(
5575 "dsv4: draft не окупился дважды — точный walk на 32 токена"
5576 );
5577 }
5578 }
5579 }
5580 if let Some((extra, n_pos)) = round {
5581 next_pos = n_pos;
5582 let mut stopped = false;
5583 for &id in &extra {
5584 if self.confidence_on {
5585 confidence.push(0.0);
5586 }
5587 if !commit!(id) {
5588 stopped = true;
5589 break;
5590 }
5591 }
5592 if stopped {
5593 break 'decode;
5594 }
5595 continue 'decode;
5596 }
5597 }
5598 self.graph_want_logits = fuse_lm;
5599 let mut t_fwd = t_next;
5605 let pure_greedy = self.sampler_config.temperature < 1e-6
5606 && self.sampler_config.repetition_penalty == 1.0
5607 && self.sampler_config.suppress_tokens.is_empty();
5608 let burst_k = std::env::var("CMF_MULTISTEP")
5613 .ok()
5614 .and_then(|v| v.parse::<usize>().ok())
5615 .unwrap_or(0);
5616 if pure_greedy
5617 && burst_k >= 1
5618 && fuse_lm
5619 && task_mask.is_none()
5620 && router.is_none()
5621 && !trace_on
5622 && !self.confidence_on
5623 {
5624 let mut stopped = false;
5625 loop {
5626 let room = max_tokens.saturating_sub(generated);
5627 if room <= 2 {
5628 break;
5629 }
5630 let k = burst_k.min(room - 1);
5631 if k < 1 {
5632 break;
5633 }
5634 let Some(ids) = self.try_multi_burst(t_fwd, next_pos, k) else {
5635 if self
5636 .graph_failed
5637 .swap(false, std::sync::atomic::Ordering::Relaxed)
5638 {
5639 self.finish_generation(&mut mtp, &mut router, true);
5640 return Err(
5641 "GPU token graph failed during greedy burst".to_string()
5642 );
5643 }
5644 break;
5645 };
5646 next_pos += k;
5647 for &id in &ids {
5648 if !commit!(id) {
5649 stopped = true;
5650 break;
5651 }
5652 }
5653 if stopped {
5654 break;
5655 }
5656 t_fwd = *ids.last().unwrap();
5657 }
5658 if stopped {
5659 break 'decode;
5660 }
5661 }
5662 #[cfg(target_os = "macos")]
5672 if graph_spec
5673 && spec_watchdog_off
5674 && next_pos > 0
5675 && self.mtp_graph_mode == Some(true)
5676 && crate::gpu::q1_force()
5677 {
5678 if let Some(m) = mtp.as_mut() {
5679 let _ = self.mtp_step_metal(m, &hidden, t_fwd, next_pos - 1, false);
5680 }
5681 }
5682 hidden = self.forward_layers(&self.embed_single(t_fwd), next_pos, task_mask);
5683 next_pos += 1;
5684 if let Some(r) = &mut router {
5687 let phi = self.dyn_phi_ema.clone();
5688 let decision = r.step(&phi, generated);
5689 if let Some(new_active) = decision {
5690 let _ = self.set_active_skill(new_active);
5691 }
5692 if trace_on {
5695 if let Some(last) = traces.last_mut() {
5696 let e = r.last_best_e();
5697 last.recon = e.is_finite().then_some(e);
5698 last.switched = decision.is_some();
5699 }
5700 }
5701 }
5702 }
5703 }
5704 }
5705
5706 let cancelled = finish_reason == "cancelled";
5707 let dyn_switched = router.as_ref().is_some_and(|r| !r.switches.is_empty());
5711 if mimo_spec {
5712 if let Some(st) = self.mimo_mtp.as_ref() {
5713 let line = st.stats.line();
5714 tracing::info!("{line}");
5715 if std::env::var_os("CMF_MIMO_MTP_STATS").is_some() {
5716 eprintln!("{line}");
5717 }
5718 }
5719 }
5720 self.finish_generation(&mut mtp, &mut router, cancelled);
5721
5722 let output_ids = &all_ids[input_ids.len()..];
5723 let forwarded = input_ids.len() + output_ids.len().saturating_sub(1);
5727 let consumed = std::mem::take(&mut all_ids);
5732 if dyn_switched {
5733 self.clear_sequence_state();
5734 } else if cancelled || mimo_spec || prompt_rows.is_some() {
5735 self.clear_history();
5736 } else {
5737 self.record_consumed_prefix(&consumed[..forwarded.min(consumed.len())], reuse_from);
5738 }
5739 all_ids = consumed;
5740 let output_ids = &all_ids[input_ids.len()..];
5741 confidence.truncate(output_ids.len()); traces.truncate(output_ids.len());
5743 Ok(GenerateResult {
5744 text: self.tokenizer.decode(output_ids),
5745 token_ids: output_ids.to_vec(),
5746 prompt_tokens: input_ids.len(),
5747 tokens_generated: generated,
5748 finish_reason,
5749 mtp_drafted: drafted,
5750 mtp_accepted: accepted,
5751 token_confidence: confidence,
5752 traces,
5753 })
5754 }
5755
5756 fn mtp_step(
5760 &mut self,
5761 m: &mut MtpModule,
5762 hidden: &[f32],
5763 next_token: u32,
5764 position: usize,
5765 ) -> u32 {
5766 self.mtp_step_h(m, hidden, next_token, position).0
5767 }
5768
5769 fn chain_probe_note(depth: usize, prefix_ok: bool) {
5773 use std::sync::Mutex;
5774 static T: Mutex<Vec<(u64, u64)>> = Mutex::new(Vec::new());
5775 let mut t = T.lock().unwrap();
5776 if t.len() <= depth {
5777 t.resize(depth + 1, (0, 0));
5778 }
5779 t[depth].0 += 1;
5780 t[depth].1 += prefix_ok as u64;
5781 if depth == 0 && t[0].0 % 128 == 0 {
5782 let line: Vec<String> = t
5783 .iter()
5784 .enumerate()
5785 .map(|(d, (n, k))| {
5786 format!(
5787 "d{}={:.0}%({n})",
5788 d + 1,
5789 100.0 * *k as f64 / (*n).max(1) as f64
5790 )
5791 })
5792 .collect();
5793 eprintln!("mtp-chain: {}", line.join(" "));
5794 }
5795 }
5796
5797 fn mtp_step_hl(
5805 &mut self,
5806 m: &mut MtpModule,
5807 hidden: &[f32],
5808 next_token: u32,
5809 position: usize,
5810 ) -> (Vec<f32>, Vec<f32>) {
5811 #[cfg(target_os = "macos")]
5816 if self.mtp_graph_mode != Some(false) && crate::gpu::q1_force() {
5817 if let Some(r) = self.mtp_step_metal(m, hidden, next_token, position, true) {
5818 self.mtp_graph_mode = Some(true);
5819 return r;
5820 }
5821 if self.mtp_graph_mode == Some(true) {
5822 tracing::error!("mtp Metal graph failed after admission");
5823 self.clear_sequence_state();
5824 self.graph_failed
5825 .store(true, std::sync::atomic::Ordering::Relaxed);
5826 self.cancel
5827 .store(true, std::sync::atomic::Ordering::Relaxed);
5828 return (Vec::new(), Vec::new());
5829 }
5830 self.mtp_graph_mode = Some(false);
5831 }
5832 #[cfg(feature = "gpu")]
5833 if self.mtp_graph_mode != Some(false) {
5834 if !self.mtp_graph_ok(m) {
5835 if self.mtp_graph_mode == Some(true) {
5836 tracing::error!("mtp graph became unavailable after admission");
5841 self.clear_sequence_state();
5842 self.graph_failed
5843 .store(true, std::sync::atomic::Ordering::Relaxed);
5844 self.cancel
5845 .store(true, std::sync::atomic::Ordering::Relaxed);
5846 return (Vec::new(), Vec::new());
5847 }
5848 self.mtp_graph_mode = Some(false);
5849 } else {
5850 if let Some(r) = self.mtp_step_graph(m, hidden, next_token, position) {
5851 self.mtp_graph_mode = Some(true);
5852 return r;
5853 }
5854 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
5855 return (Vec::new(), Vec::new());
5862 }
5863 tracing::error!("mtp graph failed or declined after admission");
5867 self.clear_sequence_state();
5868 self.graph_failed
5869 .store(true, std::sync::atomic::Ordering::Relaxed);
5870 self.cancel
5871 .store(true, std::sync::atomic::Ordering::Relaxed);
5872 return (Vec::new(), Vec::new());
5873 }
5874 }
5875 let e = self.embed_single(next_token);
5879 let mut cat = vec![0.0f32; 2 * self.hidden_size];
5880 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
5881 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
5882 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
5883 let mut x = vec![0.0f32; self.hidden_size];
5884 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
5885
5886 let lw = &m.layer;
5888 inference::rms_norm_into(
5889 &x,
5890 &lw.input_norm,
5891 self.rms_eps,
5892 self.norm_style,
5893 &mut self.ws.n1,
5894 );
5895 let attn = match &lw.attn {
5896 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
5898 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
5899 AttnKind::Full {
5900 wq,
5901 wk,
5902 wv,
5903 wo,
5904 q_norm,
5905 k_norm,
5906 output_gate,
5907 softplus_gate,
5908 bias,
5909 } => {
5910 let mut cfg = self.attn_cfg(position);
5911 cfg.q_norm = q_norm.as_deref();
5912 cfg.k_norm = k_norm.as_deref();
5913 cfg.output_gate = *output_gate;
5914 cfg.softplus_gate = softplus_gate
5915 .as_ref()
5916 .map(|(gate, per_head)| (gate, *per_head));
5917 cfg.bias = bias
5918 .as_ref()
5919 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
5920 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
5921 }
5922 AttnKind::Linear(_)
5923 | AttnKind::LinearGdn(_)
5924 | AttnKind::ShortConv(_)
5925 | AttnKind::Bounded(_) => {
5926 unreachable!("MTP block is full attention")
5927 }
5928 };
5929 for (i, &a) in attn.iter().enumerate() {
5930 x[i] += a;
5931 }
5932 inference::rms_norm_into(
5933 &x,
5934 &lw.post_norm,
5935 self.rms_eps,
5936 self.norm_style,
5937 &mut self.ws.p1,
5938 );
5939 let ffn = ffn_forward(&lw.ffn, &self.ws.p1, self.pool.as_deref(), None);
5940 for (i, &f) in ffn.iter().enumerate() {
5941 x[i] += f;
5942 }
5943
5944 inference::rms_norm_into(
5945 &x,
5946 &m.final_norm,
5947 self.rms_eps,
5948 self.norm_style,
5949 &mut self.ws.n1,
5950 );
5951 let lg = self.lm_head_forward(&self.ws.n1);
5952 (lg, x)
5953 }
5954
5955 fn mtp_step_h(
5957 &mut self,
5958 m: &mut MtpModule,
5959 hidden: &[f32],
5960 next_token: u32,
5961 position: usize,
5962 ) -> (u32, Vec<f32>) {
5963 let (mut lg, x) = self.mtp_step_hl(m, hidden, next_token, position);
5964 let draft = sampler::argmax(&lg);
5965 attention::recycle_buf(&mut lg);
5966 (draft, x)
5967 }
5968
5969 fn spec_trial_round(trial: SpecTrial, mon: &mut SpecMon, generated: usize) -> SpecTrial {
5975 match trial {
5976 SpecTrial::Spec { t0, gen0, rounds } => {
5977 let rounds = rounds + 1;
5978 if rounds >= 5 {
5979 if mon.plain_ms > 0.0 {
5980 let keep = mon.pays();
5981 mon.fails = 0;
5982 tracing::info!(
5983 "speculation re-check: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok — {}",
5984 mon.tokens,
5985 mon.round_ms,
5986 mon.plain_ms,
5987 if keep { "speculating" } else { "plain" }
5988 );
5989 SpecTrial::Decided {
5990 spec: keep,
5991 recheck_at: if keep { usize::MAX } else { generated + 128 },
5992 }
5993 } else if mon.pays() {
5994 mon.fails = 0;
5999 tracing::info!(
6000 "speculation trial: {:.2} tok/round in {:.1} ms — speculating (plain not timed)",
6001 mon.tokens,
6002 mon.round_ms,
6003 );
6004 SpecTrial::Decided {
6005 spec: true,
6006 recheck_at: usize::MAX,
6007 }
6008 } else {
6009 SpecTrial::Plain {
6010 t0: std::time::Instant::now(),
6011 gen0: generated,
6012 }
6013 }
6014 } else {
6015 SpecTrial::Spec { t0, gen0, rounds }
6016 }
6017 }
6018 SpecTrial::Decided { spec: true, .. } => {
6019 if mon.pays() {
6020 mon.fails = 0;
6021 trial
6022 } else {
6023 mon.fails += 1;
6024 if mon.fails >= 4 {
6025 if mon.plain_ms <= 0.0 {
6026 tracing::info!(
6030 "speculation doubtful: {:.2} tok/round in {:.1} ms — timing plain",
6031 mon.tokens,
6032 mon.round_ms,
6033 );
6034 return SpecTrial::Plain {
6035 t0: std::time::Instant::now(),
6036 gen0: generated,
6037 };
6038 }
6039 tracing::info!(
6040 "speculation stopped: {:.2} tok/round in {:.1} ms vs plain {:.1} ms/tok",
6041 mon.tokens,
6042 mon.round_ms,
6043 mon.plain_ms
6044 );
6045 SpecTrial::Decided {
6046 spec: false,
6047 recheck_at: generated + 128,
6048 }
6049 } else {
6050 trial
6051 }
6052 }
6053 }
6054 other => other,
6055 }
6056 }
6057
6058 fn mtp_kv_id(&self) -> u64 {
6061 self.graph_kv_id | (1u64 << 40)
6062 }
6063
6064 const MTP_LAYER_BASE: usize = 0;
6069
6070 #[cfg(feature = "gpu")]
6077 fn rewind_mtp_graph_mirror(&self, stored: usize) -> bool {
6078 self.mtp_graph_mode != Some(true)
6079 || crate::gpu::graph_kv_set_stored(self.mtp_kv_id(), Self::MTP_LAYER_BASE, stored)
6080 }
6081
6082 #[cfg(feature = "gpu")]
6088 fn rewind_trunk_graph_mirrors(&self, stored: usize) -> bool {
6089 let mut ok = true;
6090 let mut expected = false;
6091 for li in 0..self.num_layers {
6092 if matches!(
6093 self.weights.layers[self.phys_layer(li)].attn,
6094 AttnKind::Full { .. }
6095 ) {
6096 expected = true;
6097 ok &= crate::gpu::graph_kv_set_stored(self.graph_kv_id, li, stored);
6098 }
6099 }
6100 !expected || ok
6101 }
6102
6103 fn graph_gdn_layer_count(&self) -> usize {
6107 (0..self.num_layers)
6108 .filter(|&li| {
6109 matches!(
6110 &self.weights.layers[self.phys_layer(li)].attn,
6111 AttnKind::LinearGdn(_)
6112 )
6113 })
6114 .count()
6115 }
6116
6117 fn mtp_block_input(&mut self, m: &MtpModule, hidden: &[f32], next_token: u32) -> Vec<f32> {
6120 let e = self.embed_single(next_token);
6121 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6122 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6123 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6124 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6125 let mut x = vec![0.0f32; self.hidden_size];
6126 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6127 x
6128 }
6129
6130 #[cfg(feature = "gpu")]
6133 fn mtp_block_graph_ok(&self, m: &MtpModule) -> bool {
6134 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0") {
6135 return false;
6136 }
6137 if !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
6138 || !crate::gpu::enabled_here()
6139 || self.attn_softcap > 0.0
6140 || self.attention_heads_per_layer.is_some()
6141 || self.v_head_dim.is_some()
6144 {
6145 return false;
6146 }
6147 matches!(
6148 &m.layer.attn,
6149 AttnKind::Full {
6150 softplus_gate: None,
6151 ..
6152 }
6153 ) && matches!(&m.layer.ffn, FfnKind::Dense(_))
6154 }
6155
6156 #[cfg(feature = "gpu")]
6160 fn mtp_graph_ok(&self, m: &MtpModule) -> bool {
6161 if !self.mtp_block_graph_ok(m) {
6162 return false;
6163 }
6164 let AttnKind::Full { wq, wk, wv, wo, .. } = &m.layer.attn else {
6165 return false;
6166 };
6167 let FfnKind::Dense(d) = &m.layer.ffn else {
6168 return false;
6169 };
6170 d.segs.is_empty()
6171 && wq.graph_weight().is_some()
6172 && wk.graph_weight().is_some()
6173 && wv.graph_weight().is_some()
6174 && wo.graph_weight().is_some()
6175 && d.gate_proj.graph_weight().is_some()
6176 && d.up_proj.graph_weight().is_some()
6177 && d.down_proj.graph_weight().is_some()
6178 && self.weights.lm_head.graph_weight().is_some()
6179 }
6180
6181 #[cfg(feature = "gpu")]
6187 fn mtp_step_graph(
6188 &mut self,
6189 m: &mut MtpModule,
6190 hidden: &[f32],
6191 next_token: u32,
6192 position: usize,
6193 ) -> Option<(Vec<f32>, Vec<f32>)> {
6194 if !self.mtp_graph_ok(m) {
6195 return None;
6196 }
6197 let lw = &m.layer;
6198 let AttnKind::Full {
6199 wq,
6200 wk,
6201 wv,
6202 wo,
6203 q_norm,
6204 k_norm,
6205 output_gate,
6206 softplus_gate,
6207 bias,
6208 } = &lw.attn
6209 else {
6210 return None;
6211 };
6212 if softplus_gate.is_some() {
6213 return None;
6214 }
6215 let FfnKind::Dense(d) = &lw.ffn else {
6216 return None;
6217 };
6218 if !d.segs.is_empty() {
6219 return None; }
6221 let mut x = self.mtp_block_input(m, hidden, next_token);
6224 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6225 let (_, i, kind, rs) = t.graph_weight()?;
6226 Some(crate::gpu::GraphW {
6227 idx: i,
6228 kind,
6229 row_scale: rs,
6230 data: &[],
6231 prism: crate::gpu::GraphPrismOp::None,
6232 affine: false,
6233 })
6234 }
6235 let (model, _, _, _) = wq.graph_weight()?;
6236 let model = model.clone();
6237 let (lm_gw, lm_rows) = {
6238 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6239 let rows = if kind == 6 {
6243 self.draft_head_rows(self.weights.lm_head.rows())
6244 } else {
6245 self.weights.lm_head.rows()
6246 };
6247 (
6248 crate::gpu::GraphW {
6249 idx: i,
6250 kind,
6251 row_scale: rs,
6252 data: &[],
6253 prism: crate::gpu::GraphPrismOp::None,
6254 affine: false,
6255 },
6256 rows,
6257 )
6258 };
6259 let layer = crate::gpu::GraphLayer {
6260 input_norm: &lw.input_norm,
6261 attn: crate::gpu::GraphAttn::Full {
6262 wq: gw(wq)?,
6263 wk: gw(wk)?,
6264 wv: gw(wv)?,
6265 wo: gw(wo)?,
6266 q_norm: q_norm.as_deref(),
6267 k_norm: k_norm.as_deref(),
6268 late_qk_norm: self.qk_norm_after_rope,
6269 bias: bias
6270 .as_ref()
6271 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6272 output_gate: *output_gate,
6273 cpu_k: m.kv.k_heads(),
6274 cpu_v: m.kv.v_heads(),
6275 cpu_base: m.kv.base(),
6276 geom: None,
6277 head_gate: None,
6278 },
6279 post_norm: &lw.post_norm,
6280 ffn: crate::gpu::GraphFfn::Dense {
6281 gate: gw(&d.gate_proj)?,
6282 up: gw(&d.up_proj)?,
6283 down: gw(&d.down_proj)?,
6284 act: d.act.graph_act()?,
6285 },
6286 };
6287 let nh = self.num_heads;
6288 let (nkv, hd, rd) = self.layer_geom(0);
6289 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6290 let mut logits = Vec::new();
6291 let ok = crate::gpu::forward_token_graph(
6292 &model,
6293 self.mtp_kv_id(),
6294 std::slice::from_ref(&layer),
6295 &[None],
6296 self.o1_epoch,
6297 &self.inv_freq,
6298 &mut x,
6299 nh,
6300 nkv,
6301 hd,
6302 self.attn_scale,
6303 rd,
6304 self.hidden_size,
6305 self.intermediate_size,
6306 position,
6307 self.kv_cache.max_seq_len,
6308 gemma,
6309 self.rms_eps as f32,
6310 Some((&lm_gw, lm_rows)),
6311 &m.final_norm,
6312 &mut logits,
6313 &[],
6314 1,
6315 None,
6316 None,
6317 None,
6318 Self::MTP_LAYER_BASE,
6319 true,
6320 );
6321 match ok {
6322 crate::gpu::TokenGraphOutcome::Completed => {}
6323 crate::gpu::TokenGraphOutcome::Declined => return None,
6324 crate::gpu::TokenGraphOutcome::Failed => {
6325 self.clear_sequence_state();
6329 self.graph_failed
6330 .store(true, std::sync::atomic::Ordering::Relaxed);
6331 self.cancel
6332 .store(true, std::sync::atomic::Ordering::Relaxed);
6333 return None;
6334 }
6335 }
6336 logits.resize(self.vocab_size, 0.0);
6337 Some((logits, x))
6338 }
6339
6340 #[cfg(feature = "gpu")]
6348 fn mtp_warm_graph(
6349 &mut self,
6350 m: &mut MtpModule,
6351 pairs: &[(&[f32], u32)],
6352 first_pos: usize,
6353 ) -> crate::gpu::BatchGraphOutcome {
6354 if pairs.is_empty() {
6355 return crate::gpu::BatchGraphOutcome::Completed;
6356 }
6357 if !self.mtp_block_graph_ok(m) {
6358 return crate::gpu::BatchGraphOutcome::Declined;
6359 }
6360 let hs = self.hidden_size;
6361 let mut hiddens = Vec::with_capacity(pairs.len() * hs);
6364 for (h, t) in pairs {
6365 hiddens.extend_from_slice(&self.mtp_block_input(m, h, *t));
6366 }
6367 let lw = &m.layer;
6368 let AttnKind::Full {
6369 wq,
6370 wk,
6371 wv,
6372 wo,
6373 q_norm,
6374 k_norm,
6375 output_gate,
6376 bias,
6377 ..
6378 } = &lw.attn
6379 else {
6380 return crate::gpu::BatchGraphOutcome::Declined;
6381 };
6382 let FfnKind::Dense(d) = &lw.ffn else {
6383 return crate::gpu::BatchGraphOutcome::Declined;
6384 };
6385 if !d.segs.is_empty() {
6386 return crate::gpu::BatchGraphOutcome::Declined; }
6388 let (AttnKind::Full { softplus_gate: None, .. }, Some(gact)) =
6391 (&lw.attn, d.act.graph_act())
6392 else {
6393 return crate::gpu::BatchGraphOutcome::Declined;
6394 };
6395 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
6396 let (_, i, kind, rs) = t.graph_weight()?;
6397 Some(crate::gpu::GraphW {
6398 idx: i,
6399 kind,
6400 row_scale: rs,
6401 data: &[],
6402 prism: crate::gpu::GraphPrismOp::None,
6403 affine: false,
6404 })
6405 }
6406 let Some((model, _, _, _)) = wq.graph_weight() else {
6407 return crate::gpu::BatchGraphOutcome::Declined;
6408 };
6409 let model = model.clone();
6410 let (Some(gwq), Some(gwk), Some(gwv), Some(gwo), Some(gg), Some(gu), Some(gd)) = (
6411 gw(wq),
6412 gw(wk),
6413 gw(wv),
6414 gw(wo),
6415 gw(&d.gate_proj),
6416 gw(&d.up_proj),
6417 gw(&d.down_proj),
6418 ) else {
6419 return crate::gpu::BatchGraphOutcome::Declined;
6420 };
6421 let layer = crate::gpu::GraphLayer {
6422 input_norm: &lw.input_norm,
6423 attn: crate::gpu::GraphAttn::Full {
6424 wq: gwq,
6425 wk: gwk,
6426 wv: gwv,
6427 wo: gwo,
6428 q_norm: q_norm.as_deref(),
6429 k_norm: k_norm.as_deref(),
6430 late_qk_norm: self.qk_norm_after_rope,
6431 bias: bias
6432 .as_ref()
6433 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
6434 output_gate: *output_gate,
6435 cpu_k: m.kv.k_heads(),
6436 cpu_v: m.kv.v_heads(),
6437 cpu_base: m.kv.base(),
6438 geom: None,
6439 head_gate: None,
6440 },
6441 post_norm: &lw.post_norm,
6442 ffn: crate::gpu::GraphFfn::Dense {
6443 gate: gg,
6444 up: gu,
6445 down: gd,
6446 act: gact,
6447 },
6448 };
6449 let positions: Vec<usize> = (first_pos..first_pos + pairs.len()).collect();
6450 let nh = self.num_heads;
6451 let (nkv, hd, rd) = self.layer_geom(0);
6452 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
6453 crate::gpu::forward_batch_graph(
6454 &model,
6455 self.mtp_kv_id(),
6456 std::slice::from_ref(&layer),
6457 &self.inv_freq,
6458 &mut hiddens,
6459 nh,
6460 nkv,
6461 hd,
6462 rd,
6463 hs,
6464 self.intermediate_size,
6465 &positions,
6466 self.kv_cache.max_seq_len,
6467 gemma,
6468 self.rms_eps as f32,
6469 self.attn_scale,
6470 pairs.len(),
6471 &[],
6472 0,
6473 None,
6474 None,
6475 )
6476 }
6477
6478 #[cfg(feature = "gpu")]
6485 fn mtp_warm_graph_fallback(
6486 &mut self,
6487 m: &mut MtpModule,
6488 pairs: &[(&[f32], u32)],
6489 first_pos: usize,
6490 ) -> bool {
6491 if pairs.is_empty() {
6492 return true;
6493 }
6494 let graphable = self.mtp_block_graph_ok(m);
6495 if !graphable {
6496 if self.mtp_graph_mode == Some(true) {
6500 return false;
6501 }
6502 self.mtp_graph_mode = Some(false);
6503 for (j, (h, t)) in pairs.iter().enumerate() {
6504 self.mtp_warm(m, h, *t, first_pos + j);
6505 }
6506 return true;
6507 }
6508
6509 for (j, (h, t)) in pairs.iter().enumerate() {
6514 if self.mtp_step_graph(m, h, *t, first_pos + j).is_none() {
6515 return false;
6516 }
6517 }
6518 self.mtp_graph_mode = Some(true);
6519 true
6520 }
6521
6522 #[cfg(feature = "gpu")]
6527 fn mtp_warm_prefill_pairs(
6528 &mut self,
6529 m: &mut MtpModule,
6530 pairs: &[(&[f32], u32)],
6531 first_pos: usize,
6532 ) -> Result<(), &'static str> {
6533 if self.mtp_graph_mode == Some(false) || !self.mtp_graph_ok(m) {
6538 if self.mtp_graph_mode == Some(true) {
6539 return Err("MTP token graph became unavailable after admission");
6540 }
6541 self.mtp_graph_mode = Some(false);
6542 for (j, (h, t)) in pairs.iter().enumerate() {
6543 self.mtp_warm(m, h, *t, first_pos + j);
6544 }
6545 return Ok(());
6546 }
6547 match self.mtp_warm_graph(m, pairs, first_pos) {
6548 crate::gpu::BatchGraphOutcome::Completed => {
6549 if !pairs.is_empty() {
6550 self.mtp_graph_mode = Some(true);
6551 }
6552 Ok(())
6553 }
6554 crate::gpu::BatchGraphOutcome::Declined => {
6555 if self.mtp_warm_graph_fallback(m, pairs, first_pos) {
6556 Ok(())
6557 } else {
6558 Err("MTP warm-up fallback failed after device admission")
6559 }
6560 }
6561 crate::gpu::BatchGraphOutcome::Failed => {
6562 Err("MTP warm batch graph failed after admission")
6563 }
6564 }
6565 }
6566
6567 #[cfg(not(feature = "gpu"))]
6568 fn mtp_warm_prefill_pairs(
6569 &mut self,
6570 m: &mut MtpModule,
6571 pairs: &[(&[f32], u32)],
6572 first_pos: usize,
6573 ) -> Result<(), &'static str> {
6574 for (j, (h, t)) in pairs.iter().enumerate() {
6575 self.mtp_warm(m, h, *t, first_pos + j);
6576 }
6577 Ok(())
6578 }
6579
6580 fn mtp_warm(&mut self, m: &mut MtpModule, hidden: &[f32], next_token: u32, position: usize) {
6584 let e = self.embed_single(next_token);
6585 let mut cat = vec![0.0f32; 2 * self.hidden_size];
6586 let (cat_e, cat_h) = cat.split_at_mut(self.hidden_size);
6587 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
6588 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
6589 let mut x = vec![0.0f32; self.hidden_size];
6590 m.eh_proj.matvec(&cat, &mut x, self.pool.as_deref());
6591 inference::rms_norm_into(
6592 &x,
6593 &m.layer.input_norm,
6594 self.rms_eps,
6595 self.norm_style,
6596 &mut self.ws.n1,
6597 );
6598 let attn = match &m.layer.attn {
6599 AttnKind::Full {
6600 wq,
6601 wk,
6602 wv,
6603 wo,
6604 q_norm,
6605 k_norm,
6606 output_gate,
6607 softplus_gate,
6608 bias,
6609 } => {
6610 let mut cfg = self.attn_cfg(position);
6611 cfg.q_norm = q_norm.as_deref();
6612 cfg.k_norm = k_norm.as_deref();
6613 cfg.output_gate = *output_gate;
6614 cfg.softplus_gate = softplus_gate.as_ref().map(|(g, p)| (g, *p));
6615 cfg.bias = bias
6616 .as_ref()
6617 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice()));
6618 attention::qwen_attention(&self.ws.n1, wq, wk, wv, wo, &mut m.kv, &cfg)
6619 }
6620 _ => return,
6621 };
6622 let _ = attn;
6623 }
6624
6625 #[cfg(feature = "gpu")]
6632 #[allow(clippy::too_many_arguments)]
6633 fn graph_spec_step(
6634 &mut self,
6635 m: &mut MtpModule,
6636 hidden: &[f32],
6637 t_next: u32,
6638 next_pos: usize,
6639 drafted: &mut usize,
6640 accepted: &mut usize,
6641 all_ids: &mut Vec<u32>,
6645 room: usize,
6650 ) -> Option<(Vec<u32>, usize, Vec<f32>)> {
6651 #[cfg(target_os = "macos")]
6662 let metal_native = crate::gpu::q1_force();
6663 #[cfg(not(target_os = "macos"))]
6664 let metal_native = false;
6665 #[cfg(feature = "gpu")]
6666 let k_default = if metal_native {
6667 7
6670 } else if crate::gpu_wgpu::verify_i8_on() {
6671 5
6672 } else {
6673 4
6674 };
6675 #[cfg(not(feature = "gpu"))]
6676 let k_default = 4;
6677 let k_env: Option<usize> = std::env::var("CMF_GRAPH_SPEC_K")
6678 .ok()
6679 .and_then(|v| v.parse().ok())
6680 .filter(|&v| (1..=8).contains(&v));
6681 let (k_start, k_max) = if metal_native { (7, 7) } else { (3, k_default.max(5)) };
6686 let k_full: usize = k_env.unwrap_or_else(|| self.spec_k_adapt.unwrap_or(k_start));
6687 let k_spec = k_full.min(room).max(1);
6688 let k_capped = k_spec < k_full;
6691 if next_pos == 0 {
6692 return None;
6693 }
6694 let t_round = std::time::Instant::now();
6695 let subs = || crate::gpu_wgpu::SUBMITS.load(std::sync::atomic::Ordering::Relaxed);
6711 let sub0 = subs();
6712 let cfg = self.sampler_config.clone();
6717 let penalized = !(cfg.repetition_penalty == 1.0
6718 && cfg.presence_penalty == 0.0
6719 && cfg.suppress_tokens.is_empty());
6720 let greedy_pen = cfg.temperature < 1e-6 && penalized;
6725 let sampling = cfg.temperature >= 1e-6;
6726 let sparse = sampling && sampler::sparse_ok(&cfg);
6732 let base_len = all_ids.len();
6733 if sampling && !sparse && self.spec_q.len() < k_spec {
6734 self.spec_q.resize_with(k_spec, Vec::new);
6735 }
6736 if sparse && self.spec_qs.len() < k_spec {
6737 self.spec_qs.resize_with(k_spec, Vec::new);
6738 }
6739 let mut drafts = Vec::with_capacity(k_spec);
6744 let mut hx = hidden.to_vec();
6745 let spec_dbg = std::env::var("CMF_SPEC_DBG").is_ok();
6748 spec_stamp("pro");
6749 #[cfg(target_os = "macos")]
6755 if metal_native && !sampling && !greedy_pen && self.mtp_graph_mode != Some(false) {
6756 match self.mtp_draft_chain_metal(m, hidden, t_next, next_pos - 1, k_spec) {
6757 Ok(ids) => {
6758 self.mtp_graph_mode = Some(true);
6759 drafts = ids;
6760 }
6761 Err(true) => {
6762 tracing::error!("mtp Metal draft chain failed after commit");
6763 self.clear_sequence_state();
6764 self.graph_failed
6765 .store(true, std::sync::atomic::Ordering::Relaxed);
6766 self.cancel
6767 .store(true, std::sync::atomic::Ordering::Relaxed);
6768 return None;
6769 }
6770 Err(false) => {}
6771 }
6772 }
6773 for j in drafts.len()..k_spec {
6774 let tok_in = if j == 0 { t_next } else { drafts[j - 1] };
6775 let mut dbg_ref: Option<(Vec<f32>, Vec<f32>)> = None;
6776 if spec_dbg {
6777 let saved = self.mtp_graph_mode;
6778 self.mtp_graph_mode = Some(false);
6779 let r = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
6780 self.mtp_graph_mode = saved;
6781 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6782 return None;
6783 }
6784 m.kv.truncate_last(1);
6785 dbg_ref = Some(r);
6786 }
6787 let (mut lg, hj) = self.mtp_step_hl(m, &hx, tok_in, next_pos - 1 + j);
6788 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
6789 return None;
6790 }
6791 if let Some((lg_cpu, h_cpu)) = dbg_ref {
6792 let n = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
6793 let dl = lg
6794 .iter()
6795 .zip(&lg_cpu)
6796 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
6797 let dh = hj
6798 .iter()
6799 .zip(&h_cpu)
6800 .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
6801 eprintln!(
6802 "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 {}",
6803 next_pos - 1 + j,
6804 sampler::argmax(&lg_cpu),
6805 sampler::argmax(&lg),
6806 n(&h_cpu),
6807 n(&hj),
6808 m.kv.seq_len
6809 );
6810 }
6811 let dj = if sparse {
6812 let mut q = std::mem::take(&mut self.spec_qs[j]);
6813 let ok = sampler::sparse_distribution_into(
6814 &lg,
6815 &cfg,
6816 all_ids,
6817 &mut self.sampler_scratch,
6818 self.pool.as_deref(),
6819 &mut q,
6820 );
6821 let d = if ok {
6822 sampler::draw_sparse(&q, &mut self.rng)
6823 } else {
6824 let t = sampler::argmax(&lg);
6826 q.clear();
6827 q.push((t, 1.0));
6828 t
6829 };
6830 self.spec_qs[j] = q;
6831 all_ids.push(d);
6832 d
6833 } else if sampling {
6834 let mut q = std::mem::take(&mut self.spec_q[j]);
6835 sampler::distribution_into(
6836 &lg,
6837 &cfg,
6838 all_ids,
6839 &mut self.sampler_scratch,
6840 self.pool.as_deref(),
6841 &mut q,
6842 );
6843 let d = sampler::draw(&q, &mut self.rng);
6844 self.spec_q[j] = q;
6845 all_ids.push(d); d
6847 } else if greedy_pen {
6848 let d = sampler::argmax_penalized(
6849 &lg,
6850 &cfg,
6851 all_ids,
6852 &mut self.sampler_scratch,
6853 self.pool.as_deref(),
6854 );
6855 all_ids.push(d);
6856 d
6857 } else {
6858 sampler::argmax(&lg)
6859 };
6860 attention::recycle_buf(&mut lg);
6861 drafts.push(dj);
6862 hx = hj;
6863 spec_stamp("d.pick");
6864 }
6865 all_ids.truncate(base_len);
6866 *drafted += k_spec;
6867 let t_draft = t_round.elapsed();
6868 let sub_draft = subs();
6869 let b = k_spec + 1;
6872 let mut hiddens = vec![0.0f32; b * self.hidden_size];
6873 for (i, &t) in std::iter::once(&t_next).chain(drafts.iter()).enumerate() {
6874 let e = self.embed_single(t);
6875 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&e);
6876 }
6877 let positions: Vec<usize> = (next_pos..next_pos + b).collect();
6878 spec_stamp("v.emb");
6879 let (lm_gw, lm_rows) = {
6880 let (_, i, kind, rs) = self.weights.lm_head.graph_weight()?;
6881 (
6882 crate::gpu::GraphW {
6883 idx: i,
6884 kind,
6885 row_scale: rs,
6886 data: &[],
6887 prism: crate::gpu::GraphPrismOp::None,
6888 affine: false,
6889 },
6890 self.weights.lm_head.rows(),
6891 )
6892 };
6893 let mut logits = Vec::new();
6894 let final_norm = self.weights.final_norm.clone();
6895 #[cfg(target_os = "macos")]
6904 let greedy_dev = metal_native
6905 && !sampling
6906 && !greedy_pen
6907 && !self.confidence_on
6908 && self.final_softcap.is_none()
6909 && self.vocab_size == lm_rows
6915 && std::env::var_os("CMF_METAL_VERIFY_CHECK").is_none()
6916 && std::env::var_os("CMF_LOGIT_DUMP").is_none()
6917 && std::env::var("CMF_METAL_DEV_ARGMAX").as_deref() != Ok("0");
6918 #[cfg(not(target_os = "macos"))]
6919 let greedy_dev = false;
6920 let mut dev_ids: Vec<u32> = Vec::new();
6921 #[cfg(target_os = "macos")]
6922 let verify_outcome = if metal_native {
6923 let lm = self.weights.lm_head.q1_parts()?;
6924 let n_score = self.vocab_size.min(lm_rows);
6925 self.try_batch_graph_metal(
6926 &mut hiddens,
6927 &positions,
6928 b,
6929 Some((lm, &final_norm, &mut logits)),
6930 if greedy_dev {
6931 Some((n_score, &mut dev_ids))
6932 } else {
6933 None
6934 },
6935 )
6936 } else {
6937 self.try_batch_graph_wgpu(
6938 &mut hiddens,
6939 &positions,
6940 b,
6941 Some(crate::gpu::SpecTail {
6942 lm: lm_gw,
6943 lm_rows,
6944 final_norm: &final_norm,
6945 logits_out: &mut logits,
6946 }),
6947 )
6948 };
6949 #[cfg(not(target_os = "macos"))]
6950 let verify_outcome = self.try_batch_graph_wgpu(
6951 &mut hiddens,
6952 &positions,
6953 b,
6954 Some(crate::gpu::SpecTail {
6955 lm: lm_gw,
6956 lm_rows,
6957 final_norm: &final_norm,
6958 logits_out: &mut logits,
6959 }),
6960 );
6961 match verify_outcome {
6962 crate::gpu::BatchGraphOutcome::Completed => {}
6963 crate::gpu::BatchGraphOutcome::Declined => {
6964 m.kv.truncate_last(k_spec);
6968 if !metal_native && !self.rewind_mtp_graph_mirror(next_pos) {
6969 self.clear_sequence_state();
6970 self.graph_failed
6971 .store(true, std::sync::atomic::Ordering::Relaxed);
6972 self.cancel
6973 .store(true, std::sync::atomic::Ordering::Relaxed);
6974 tracing::error!("MTP graph mirror rewind failed after verify decline");
6975 }
6976 return None;
6977 }
6978 crate::gpu::BatchGraphOutcome::Failed => {
6979 self.clear_sequence_state();
6983 self.graph_failed
6984 .store(true, std::sync::atomic::Ordering::Relaxed);
6985 self.cancel
6986 .store(true, std::sync::atomic::Ordering::Relaxed);
6987 tracing::error!("MTP verify batch graph failed after admission");
6988 return None;
6989 }
6990 }
6991 #[cfg(target_os = "macos")]
6997 if metal_native && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("1") {
6998 let snap: Vec<Vec<f32>> = self
6999 .kv_cache
7000 .layers
7001 .iter()
7002 .map(|l| l.linear_state.clone())
7003 .collect();
7004 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7005 let toks: Vec<u32> = std::iter::once(t_next)
7006 .chain(drafts.iter().copied())
7007 .collect();
7008 let want_save = self.graph_want_logits;
7009 self.graph_want_logits = false;
7010 for (i, &t) in toks.iter().enumerate() {
7011 let hi = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7012 let _ = self.graph_logits.take();
7013 if std::env::var("CMF_SPEC_PLAIN_HIDDEN").as_deref() == Ok("1") {
7017 hiddens[i * self.hidden_size..(i + 1) * self.hidden_size].copy_from_slice(&hi);
7018 }
7019 let ref_lg = self.logits_from_hidden(&hi);
7020 let row = &logits[i * lm_rows..(i + 1) * lm_rows];
7021 let ra = sampler::argmax(&ref_lg);
7022 let va = sampler::argmax(row);
7023 let mut md = 0f32;
7024 let mut rms = 0f64;
7025 for j in 0..lm_rows.min(ref_lg.len()) {
7026 let d = (ref_lg[j] - row[j]).abs();
7027 md = md.max(d);
7028 rms += (d as f64) * (d as f64);
7029 }
7030 let mut hd = 0f32;
7031 for j in 0..self.hidden_size {
7032 hd = hd.max((hi[j] - hiddens[i * self.hidden_size + j]).abs());
7033 }
7034 eprintln!(
7035 "verify-check row {i} tok {t} pos {}: ref argmax {ra} verify argmax {va} {} | max|dlogit| {md:.3} rms {:.4} | max|dhidden| {hd:.4}",
7036 next_pos + i,
7037 if ra == va { "OK" } else { "MISMATCH" },
7038 (rms / lm_rows as f64).sqrt()
7039 );
7040 }
7041 self.graph_want_logits = want_save;
7042 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7045 if l.linear_state.len() == st.len() {
7046 l.linear_state.copy_from_slice(&st);
7047 } else {
7048 l.linear_state = st;
7049 }
7050 }
7051 for (li, (l, n0)) in self.kv_cache.layers.iter_mut().zip(attn_lens).enumerate() {
7052 let extra = l.seq_len.saturating_sub(n0);
7053 if extra > 0 {
7054 l.truncate_last(extra);
7055 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, n0);
7056 }
7057 }
7058 }
7059 let t_verify = t_round.elapsed();
7060 let sub_verify = subs();
7061 let mut a = 0usize;
7066 let mut forced: Option<u32> = None;
7067 let ids: Vec<u32> = if sparse {
7068 let mut p = std::mem::take(&mut self.spec_ps);
7069 let mut res = std::mem::take(&mut self.spec_ress);
7070 while a < k_spec {
7071 let ok = sampler::sparse_distribution_into(
7072 &logits[a * lm_rows..(a + 1) * lm_rows],
7073 &cfg,
7074 all_ids,
7075 &mut self.sampler_scratch,
7076 self.pool.as_deref(),
7077 &mut p,
7078 );
7079 if !ok {
7080 let t = sampler::argmax(&logits[a * lm_rows..(a + 1) * lm_rows]);
7081 p.clear();
7082 p.push((t, 1.0));
7083 }
7084 match sampler::spec_accept_or_correct_sparse(
7085 &p,
7086 &self.spec_qs[a],
7087 drafts[a],
7088 &mut self.rng,
7089 &mut res,
7090 ) {
7091 None => {
7092 all_ids.push(drafts[a]);
7093 a += 1;
7094 }
7095 Some(c) => {
7096 forced = Some(c);
7097 break;
7098 }
7099 }
7100 }
7101 all_ids.truncate(base_len);
7102 self.spec_ps = p;
7103 self.spec_ress = res;
7104 drafts.clone()
7105 } else if sampling {
7106 let mut p = std::mem::take(&mut self.spec_p);
7107 let mut res = std::mem::take(&mut self.spec_res);
7108 while a < k_spec {
7109 sampler::distribution_into(
7110 &logits[a * lm_rows..(a + 1) * lm_rows],
7111 &cfg,
7112 all_ids,
7113 &mut self.sampler_scratch,
7114 self.pool.as_deref(),
7115 &mut p,
7116 );
7117 match sampler::spec_accept_or_correct(
7118 &p,
7119 &self.spec_q[a],
7120 drafts[a],
7121 &mut self.rng,
7122 &mut res,
7123 self.pool.as_deref(),
7124 ) {
7125 None => {
7126 all_ids.push(drafts[a]);
7127 a += 1;
7128 }
7129 Some(c) => {
7130 forced = Some(c);
7131 break;
7132 }
7133 }
7134 }
7135 all_ids.truncate(base_len);
7136 self.spec_p = p;
7137 self.spec_res = res;
7138 drafts.clone()
7140 } else if greedy_pen {
7141 let mut ids: Vec<u32> = Vec::with_capacity(b);
7145 for i in 0..b {
7146 let t = sampler::argmax_penalized(
7147 &logits[i * lm_rows..(i + 1) * lm_rows],
7148 &cfg,
7149 all_ids,
7150 &mut self.sampler_scratch,
7151 self.pool.as_deref(),
7152 );
7153 ids.push(t);
7154 if i < k_spec && t == drafts[i] {
7155 all_ids.push(t);
7156 } else {
7157 break;
7158 }
7159 }
7160 all_ids.truncate(base_len);
7161 while a < k_spec && a < ids.len() && ids[a] == drafts[a] {
7162 a += 1;
7163 }
7164 ids
7167 } else if greedy_dev && dev_ids.len() == b {
7168 let ids = std::mem::take(&mut dev_ids);
7169 while a < k_spec && ids[a] == drafts[a] {
7170 a += 1;
7171 }
7172 ids
7173 } else {
7174 if logits.len() < b * lm_rows {
7175 self.clear_sequence_state();
7178 self.graph_failed
7179 .store(true, std::sync::atomic::Ordering::Relaxed);
7180 self.cancel
7181 .store(true, std::sync::atomic::Ordering::Relaxed);
7182 tracing::error!("Metal verify returned neither logits nor argmax ids");
7183 return None;
7184 }
7185 let ids: Vec<u32> = (0..b)
7186 .map(|i| sampler::argmax(&logits[i * lm_rows..(i + 1) * lm_rows]))
7187 .collect();
7188 while a < k_spec && ids[a] == drafts[a] {
7189 a += 1;
7190 }
7191 ids
7192 };
7193 spec_stamp("acc");
7194 if spec_dbg {
7195 eprintln!(
7196 "spec-dbg round: t_next {t_next} drafts {:?} verified {:?} accepted {a}",
7197 drafts, ids
7198 );
7199 }
7200 #[cfg(target_os = "macos")]
7204 let commit_ref: Option<(Vec<Vec<f32>>, Vec<(usize, Vec<f32>, Vec<f32>)>)> = if metal_native
7205 && std::env::var("CMF_METAL_VERIFY_CHECK").as_deref() == Ok("2")
7206 {
7207 let snap: Vec<Vec<f32>> = self
7208 .kv_cache
7209 .layers
7210 .iter()
7211 .map(|l| l.linear_state.clone())
7212 .collect();
7213 let attn_lens: Vec<usize> = self.kv_cache.layers.iter().map(|l| l.seq_len).collect();
7214 let toks: Vec<u32> = std::iter::once(t_next)
7215 .chain(drafts.iter().copied())
7216 .collect();
7217 let want_save = self.graph_want_logits;
7218 self.graph_want_logits = false;
7219 for (i, &t) in toks.iter().take(a + 1).enumerate() {
7220 let _ = self.forward_layers(&self.embed_single(t), next_pos + i, None);
7221 let _ = self.graph_logits.take();
7222 }
7223 self.graph_want_logits = want_save;
7224 let plain_states: Vec<Vec<f32>> = self
7225 .kv_cache
7226 .layers
7227 .iter()
7228 .map(|l| l.linear_state.clone())
7229 .collect();
7230 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7231 let mut rows = Vec::new();
7232 for (li, (l, n0)) in self
7233 .kv_cache
7234 .layers
7235 .iter_mut()
7236 .zip(attn_lens.iter())
7237 .enumerate()
7238 {
7239 let extra = l.seq_len.saturating_sub(*n0);
7240 if extra > 0 {
7241 let mut kk = Vec::new();
7242 let mut vv = Vec::new();
7243 for g in 0..nkv {
7244 kk.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7245 vv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7246 }
7247 rows.push((li, kk, vv));
7248 l.truncate_last(extra);
7249 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, *n0);
7250 }
7251 }
7252 for (l, st) in self.kv_cache.layers.iter_mut().zip(snap) {
7253 if l.linear_state.len() == st.len() {
7254 l.linear_state.copy_from_slice(&st);
7255 } else {
7256 l.linear_state = st;
7257 }
7258 }
7259 Some((plain_states, rows))
7260 } else {
7261 None
7262 };
7263 let warm_off = std::env::var("CMF_SPEC_WARM").is_ok_and(|v| v == "0");
7264 #[cfg(target_os = "macos")]
7273 let mut warm_pending: Option<MetalWarmPending> = None;
7274 #[cfg(target_os = "macos")]
7275 if metal_native {
7276 m.kv.truncate_last(k_spec.saturating_sub(1));
7277 if self.mtp_graph_mode == Some(true) {
7278 crate::gpu_metal::kv_mirror_set_stored(
7281 self.mtp_kv_id(),
7282 Self::MTP_LAYER_BASE,
7283 m.kv.seq_len,
7284 );
7285 if !warm_off && a > 0 {
7286 let pairs: Vec<(&[f32], u32)> = (0..a)
7287 .map(|j| {
7288 (
7289 &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size],
7290 ids[j],
7291 )
7292 })
7293 .collect();
7294 warm_pending = self.mtp_warm_batch_submit(m, &pairs, next_pos);
7295 }
7296 }
7297 spec_stamp("c.wsub");
7298 }
7299 #[cfg(target_os = "macos")]
7301 if metal_native {
7302 if !self.metal_verify_commit(a) {
7305 self.clear_sequence_state();
7306 self.graph_failed
7307 .store(true, std::sync::atomic::Ordering::Relaxed);
7308 self.cancel
7309 .store(true, std::sync::atomic::Ordering::Relaxed);
7310 tracing::error!("Metal verify state/KV handoff failed after admission");
7311 return None;
7312 }
7313 if let Some((plain_states, rows)) = commit_ref {
7314 crate::gpu_metal::queue_fence();
7315 let _ = crate::gpu_metal::wait_replay();
7318 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
7319 let mut worst_s = 0f32;
7320 let mut worst_li = 0usize;
7321 for (li, (l, ps)) in self.kv_cache.layers.iter().zip(&plain_states).enumerate() {
7322 if l.linear_state.len() != ps.len() || ps.is_empty() {
7323 continue;
7324 }
7325 let d = l
7326 .linear_state
7327 .iter()
7328 .zip(ps)
7329 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7330 let n = ps.iter().fold(0f32, |m, y| m.max(y.abs()));
7331 let rel = d / n.max(1e-6);
7332 if rel > worst_s {
7333 worst_s = rel;
7334 worst_li = li;
7335 }
7336 }
7337 let mut worst_k = 0f32;
7338 for (li, kk, vv) in &rows {
7339 let l = &self.kv_cache.layers[*li];
7340 let n0 = l.seq_len - (kk.len() / (nkv * hd));
7341 let mut ck = Vec::new();
7342 let mut cv = Vec::new();
7343 for g in 0..nkv {
7344 ck.extend_from_slice(&l.head_keys(g)[n0 * hd..]);
7345 cv.extend_from_slice(&l.head_values(g)[n0 * hd..]);
7346 }
7347 if ck.len() == kk.len() {
7348 let dk = ck
7349 .iter()
7350 .zip(kk)
7351 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7352 let dv = cv
7353 .iter()
7354 .zip(vv)
7355 .fold(0f32, |m, (x, y)| m.max((x - y).abs()));
7356 worst_k = worst_k.max(dk).max(dv);
7357 } else {
7358 eprintln!(
7359 "commit-check L{li}: kv row count mismatch {} vs {}",
7360 ck.len(),
7361 kk.len()
7362 );
7363 }
7364 }
7365 eprintln!(
7366 "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}"
7367 );
7368 }
7369 }
7370 if !metal_native && a + 1 < b {
7371 let expected_gdn_layers = self.graph_gdn_layer_count();
7372 if expected_gdn_layers > 0
7373 && !crate::gpu::gdn_spec_restore(self.graph_kv_id, a, next_pos, expected_gdn_layers)
7374 {
7375 self.clear_sequence_state();
7376 self.graph_failed
7377 .store(true, std::sync::atomic::Ordering::Relaxed);
7378 self.cancel
7379 .store(true, std::sync::atomic::Ordering::Relaxed);
7380 tracing::error!("GDN speculative restore failed after verify");
7381 return None;
7382 }
7383 }
7384 if !metal_native && !self.rewind_trunk_graph_mirrors(next_pos + a + 1) {
7385 self.clear_sequence_state();
7390 self.graph_failed
7391 .store(true, std::sync::atomic::Ordering::Relaxed);
7392 self.cancel
7393 .store(true, std::sync::atomic::Ordering::Relaxed);
7394 tracing::error!("trunk graph KV rewind failed after speculative verify");
7395 return None;
7396 }
7397 *accepted += a;
7398 if !metal_native {
7409 m.kv.truncate_last(k_spec.saturating_sub(1));
7411 }
7412 spec_stamp("c.trunc");
7413 if !metal_native
7414 && self.mtp_graph_mode == Some(true)
7415 && !self.rewind_mtp_graph_mirror(next_pos)
7416 {
7417 self.clear_sequence_state();
7421 self.graph_failed
7422 .store(true, std::sync::atomic::Ordering::Relaxed);
7423 self.cancel
7424 .store(true, std::sync::atomic::Ordering::Relaxed);
7425 tracing::error!("MTP graph mirror rewind failed after verify commit");
7426 return None;
7427 }
7428 if !warm_off && a > 0 {
7429 let mut warmed = false;
7432 #[cfg(target_os = "macos")]
7433 if metal_native && self.mtp_graph_mode == Some(true) {
7434 warmed = match warm_pending.take() {
7438 Some(p) => self.mtp_warm_batch_finish(m, p),
7439 None => false,
7440 };
7441 if !warmed {
7442 warmed = true;
7443 for j in 0..a {
7444 let row =
7445 hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec();
7446 if self
7447 .mtp_step_metal(m, &row, ids[j], next_pos + j, false)
7448 .is_none()
7449 {
7450 warmed = false;
7451 break;
7452 }
7453 }
7454 }
7455 }
7456 if !warmed && self.mtp_graph_mode != Some(false) && !metal_native {
7457 let rows: Vec<Vec<f32>> = (0..a)
7458 .map(|j| hiddens[j * self.hidden_size..(j + 1) * self.hidden_size].to_vec())
7459 .collect();
7460 let pairs: Vec<(&[f32], u32)> = rows
7461 .iter()
7462 .zip(ids.iter())
7463 .map(|(r, &t)| (r.as_slice(), t))
7464 .collect();
7465 match self.mtp_warm_prefill_pairs(m, &pairs, next_pos) {
7466 Ok(()) => warmed = true,
7467 Err(err) => {
7468 tracing::error!("{err}");
7474 self.clear_sequence_state();
7475 self.graph_failed
7476 .store(true, std::sync::atomic::Ordering::Relaxed);
7477 self.cancel
7478 .store(true, std::sync::atomic::Ordering::Relaxed);
7479 return None;
7480 }
7481 }
7482 }
7483 if !warmed {
7484 for j in 0..a {
7485 let row = &hiddens[j * self.hidden_size..(j + 1) * self.hidden_size];
7486 let row = row.to_vec();
7487 self.mtp_warm(m, &row, ids[j], next_pos + j);
7488 }
7489 }
7490 }
7491 spec_stamp("c.warm");
7495 if let Some(c) = forced {
7496 self.spec_forced = Some(c);
7497 self.graph_logits = None;
7498 } else if greedy_dev && logits.is_empty() {
7499 self.spec_forced = Some(ids[a]);
7502 self.graph_logits = None;
7503 } else {
7504 let mut row = logits[a * lm_rows..(a + 1) * lm_rows].to_vec();
7505 row.resize(self.vocab_size, 0.0);
7506 if let Some(c) = self.final_softcap {
7507 for l in row.iter_mut() {
7508 *l = c * (*l / c).tanh();
7509 }
7510 }
7511 self.graph_logits = Some(row);
7512 }
7513 let new_hidden = hiddens[a * self.hidden_size..(a + 1) * self.hidden_size].to_vec();
7514 spec_stamp("c.row");
7515 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7521 let end = subs();
7522 eprintln!(
7523 "spec-round: draft {:.1} ms/{} sub | verify {:.1} ms/{} sub | \
7524 commit {:.1} ms/{} sub (accepted {a} of {k_spec}, full-head streak {})",
7525 t_draft.as_secs_f64() * 1e3,
7526 sub_draft - sub0,
7527 (t_verify - t_draft).as_secs_f64() * 1e3,
7528 sub_verify - sub_draft,
7529 (t_round.elapsed() - t_verify).as_secs_f64() * 1e3,
7530 end - sub_verify,
7531 self.draft_full_streak,
7532 );
7533 }
7534 if k_env.is_none() && !metal_native && !k_capped {
7539 let f = a as f32 / k_spec.max(1) as f32;
7543 self.spec_acc_ewma += 0.2 * (f - self.spec_acc_ewma);
7544 let mut k_next = k_spec;
7545 if self.spec_acc_ewma >= 0.75 && k_spec < k_max {
7546 k_next = k_spec + 1;
7547 } else if self.spec_acc_ewma < 0.4 && k_spec > 2 {
7548 k_next = k_spec - 1;
7549 }
7550 if k_next != k_spec {
7551 self.spec_acc_ewma = 0.6;
7552 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
7553 eprintln!("spec-k: {k_spec} → {k_next}");
7554 }
7555 }
7556 self.spec_k_adapt = Some(k_next);
7557 }
7558 spec_stamp("end");
7559 Some((drafts[..a].to_vec(), next_pos + a + 1, new_hidden))
7560 }
7561
7562 pub fn measure_pair_fusion(&mut self, iters: usize) -> (f64, f64) {
7571 if !self.pair_supported() {
7572 return (0.0, 0.0);
7573 }
7574 let graph_env = std::env::var_os("CMF_GPU_WGPU_GRAPH");
7581 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
7582 let emb1 = self.embed_single(1);
7583 let emb2 = self.embed_single(2);
7584 let pos = self.kv_cache.seq_len();
7585
7586 let t0 = std::time::Instant::now();
7587 for _ in 0..iters {
7588 let _ = self.forward_layers(&emb1, pos, None);
7589 let _ = self.forward_layers(&emb2, pos + 1, None);
7590 for l in &mut self.kv_cache.layers {
7591 l.truncate_last(2);
7592 }
7593 }
7594 let singles_ms = t0.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7595
7596 let t1 = std::time::Instant::now();
7597 for _ in 0..iters {
7598 let _ = self.forward_pair(&emb1, &emb2, pos);
7599 for l in &mut self.kv_cache.layers {
7600 l.truncate_last(2);
7601 }
7602 }
7603 let pair_ms = t1.elapsed().as_secs_f64() * 1000.0 / iters as f64;
7604 match graph_env {
7605 Some(value) => unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", value) },
7606 None => unsafe { std::env::remove_var("CMF_GPU_WGPU_GRAPH") },
7607 }
7608 (singles_ms, pair_ms)
7609 }
7610
7611 fn pair_supported(&self) -> bool {
7619 !self.weights.layers.is_empty()
7626 && self.g3n.is_none()
7627 && !self
7628 .weights
7629 .layers
7630 .iter()
7631 .any(|lw| matches!(&lw.attn, AttnKind::Mla(_) | AttnKind::Kda(_)))
7632 }
7633
7634 fn forward_pair(
7635 &mut self,
7636 emb1: &[f32],
7637 emb2: &[f32],
7638 position: usize,
7639 ) -> (Vec<f32>, Vec<f32>) {
7640 self.mimo_moe_prepare();
7643 let mut h1 = emb1.to_vec();
7644 let mut h2 = emb2.to_vec();
7645 let (_nkv, _hd, hs, _rd, eps) = (
7646 self.num_kv_heads,
7647 self.head_dim,
7648 self.hidden_size,
7649 self.rotary_dim,
7650 self.rms_eps,
7651 );
7652 let pool = self.pool.clone();
7653
7654 for li in 0..self.num_layers {
7655 let lw = &self.weights.layers[self.phys_layer(li)];
7656 inference::rms_norm_into(
7659 &h1,
7660 &lw.input_norm,
7661 self.rms_eps,
7662 self.norm_style,
7663 &mut self.ws.n1,
7664 );
7665 inference::rms_norm_into(
7666 &h2,
7667 &lw.input_norm,
7668 self.rms_eps,
7669 self.norm_style,
7670 &mut self.ws.n2,
7671 );
7672
7673 let (a1, a2) = match &lw.attn {
7674 AttnKind::Mla(_) => unreachable!("MLA has no MTP/pair path"),
7675 AttnKind::Kda(_) => unreachable!("KDA has no MTP/pair path"),
7676 AttnKind::Bounded(w) => {
7677 let rope = self
7680 .bounded_rope
7681 .clone()
7682 .expect("bounded layer without an installed rotation table");
7683 let cfg = crate::bounded::BoundedAttnCfg {
7684 num_heads: self.num_heads,
7685 num_kv_heads: self.num_kv_heads,
7686 head_dim: self.head_dim,
7687 hidden_size: hs,
7688 scale: self.attn_scale,
7689 rope: &rope,
7690 pool: pool.as_deref(),
7691 };
7692 let a1 = crate::bounded::bounded_attention(
7693 &self.ws.n1,
7694 w,
7695 &mut self.kv_cache.layers[li],
7696 &cfg,
7697 );
7698 let a2 = crate::bounded::bounded_attention(
7699 &self.ws.n2,
7700 w,
7701 &mut self.kv_cache.layers[li],
7702 &cfg,
7703 );
7704 (a1, a2)
7705 }
7706 AttnKind::Linear(w) => {
7707 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
7708 let layer = &mut self.kv_cache.layers[li];
7709 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7710 vmf_phase_pair(
7711 &self.ws.n1,
7712 &self.ws.n2,
7713 w,
7714 &cfg,
7715 state,
7716 scratch,
7717 self.pool.as_deref(),
7718 )
7719 }
7720 AttnKind::LinearGdn(w) => {
7721 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
7722 let layer = &mut self.kv_cache.layers[li];
7723 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7724 gdn_pair(
7725 &self.ws.n1,
7726 &self.ws.n2,
7727 w,
7728 &cfg,
7729 state,
7730 scratch,
7731 self.pool.as_deref(),
7732 )
7733 }
7734 AttnKind::ShortConv(w) => {
7735 let cfg = self
7736 .short_conv_cfg
7737 .expect("short-conv layer without short_conv_cfg");
7738 let layer = &mut self.kv_cache.layers[li];
7739 let (state, scratch) = (&mut layer.linear_state, &mut layer.linear_scratch);
7740 short_conv_pair(
7741 &self.ws.n1,
7742 &self.ws.n2,
7743 w,
7744 &cfg,
7745 state,
7746 scratch,
7747 self.pool.as_deref(),
7748 )
7749 }
7750 AttnKind::Full {
7751 wq,
7752 wk,
7753 wv,
7754 wo,
7755 q_norm,
7756 k_norm,
7757 output_gate,
7758 softplus_gate,
7759 bias,
7760 } => {
7761 let inv_freq_l = self.layer_inv_freq(li);
7762 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
7763 let cfg = QwenAttnCfg {
7764 num_heads: self.layer_num_heads(li),
7765 num_kv_heads: nkv_l,
7766 head_dim: hd_l,
7767 hidden_size: hs,
7768 position,
7769 inv_freq: &inv_freq_l,
7770 rotary_dim: rd_l,
7771 scale: self.attn_scale,
7772 softcap: self.attn_softcap,
7773 window: self.layer_window(li),
7774 v_norm: self.attn_v_norm,
7775 qk_norm_after_rope: self.qk_norm_after_rope,
7776 gate_sigmoid: self.proj_gate_sigmoid,
7777 q_norm: q_norm.as_deref(),
7778 k_norm: k_norm.as_deref(),
7779 output_gate: *output_gate,
7780 softplus_gate: softplus_gate
7781 .as_ref()
7782 .map(|(gate, per_head)| (gate, *per_head)),
7783 rope_scale: self.layer_rope_scale(li),
7784 bias: bias
7785 .as_ref()
7786 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
7787 rms_eps: eps,
7788 norm_style: self.norm_style,
7789 pool: pool.as_deref(),
7790 v_head_dim: self.layer_v_dim(li),
7791 };
7792 attention::qwen_attention_pair(
7793 &self.ws.n1,
7794 &self.ws.n2,
7795 wq,
7796 wk,
7797 wv,
7798 wo,
7799 &mut self.kv_cache.layers[li],
7800 &cfg,
7801 )
7802 }
7803 };
7804 let (a1, a2) = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
7805 Some(w) => (
7806 inference::rms_norm(&a1, w, self.rms_eps, self.norm_style),
7807 inference::rms_norm(&a2, w, self.rms_eps, self.norm_style),
7808 ),
7809 None => (a1, a2),
7810 };
7811 for i in 0..self.hidden_size {
7812 h1[i] += a1[i];
7813 h2[i] += a2[i];
7814 }
7815 let (mut a1, mut a2) = (a1, a2);
7816 attention::recycle_buf(&mut a1);
7817 attention::recycle_buf(&mut a2);
7818
7819 let lw = &self.weights.layers[self.phys_layer(li)];
7820 inference::rms_norm_into(
7821 &h1,
7822 &lw.post_norm,
7823 self.rms_eps,
7824 self.norm_style,
7825 &mut self.ws.p1,
7826 );
7827 inference::rms_norm_into(
7828 &h2,
7829 &lw.post_norm,
7830 self.rms_eps,
7831 self.norm_style,
7832 &mut self.ws.p2,
7833 );
7834 let (f1, f2) = match &lw.ffn {
7835 FfnKind::DenseMoe(dm) => (
7838 dense_moe_ffn(
7839 dm,
7840 &self.ws.p1,
7841 &h1,
7842 self.rms_eps,
7843 self.norm_style,
7844 self.pool.as_deref(),
7845 ),
7846 dense_moe_ffn(
7847 dm,
7848 &self.ws.p2,
7849 &h2,
7850 self.rms_eps,
7851 self.norm_style,
7852 self.pool.as_deref(),
7853 ),
7854 ),
7855 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => (
7856 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p1, self.pool.as_deref()),
7857 moe_ffn_banked(&mut self.mimo_moe, li, m, &self.ws.p2, self.pool.as_deref()),
7858 ),
7859 _ => ffn_forward_pair(
7860 &lw.ffn,
7861 &self.ws.p1,
7862 &self.ws.p2,
7863 self.pool.as_deref(),
7864 None,
7865 ),
7866 };
7867 let (f1, f2) = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
7868 Some(w) => (
7869 inference::rms_norm(&f1, w, self.rms_eps, self.norm_style),
7870 inference::rms_norm(&f2, w, self.rms_eps, self.norm_style),
7871 ),
7872 None => (f1, f2),
7873 };
7874 for i in 0..self.hidden_size {
7875 h1[i] += f1[i];
7876 h2[i] += f2[i];
7877 }
7878 let (mut f1, mut f2) = (f1, f2);
7879 attention::recycle_buf(&mut f1);
7880 attention::recycle_buf(&mut f2);
7881 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
7882 for i in 0..self.hidden_size {
7883 h1[i] *= sc;
7884 h2[i] *= sc;
7885 }
7886 }
7887 if self.is_loop_end(li) && li + 1 < self.num_layers {
7889 h1 = inference::rms_norm(
7890 &h1,
7891 &self.weights.final_norm,
7892 self.rms_eps,
7893 self.norm_style,
7894 );
7895 h2 = inference::rms_norm(
7896 &h2,
7897 &self.weights.final_norm,
7898 self.rms_eps,
7899 self.norm_style,
7900 );
7901 }
7902 }
7903 if self.o1_active() {
7909 self.commit_linear_scratch();
7910 }
7911 self.o1_progress();
7912 self.swa_trim_tails();
7913 (h1, h2)
7914 }
7915
7916 fn commit_linear_scratch(&mut self) {
7918 for layer in &mut self.kv_cache.layers {
7919 if !layer.linear_scratch.is_empty() {
7920 std::mem::swap(&mut layer.linear_state, &mut layer.linear_scratch);
7921 layer.linear_scratch.clear();
7922 }
7923 }
7924 }
7925
7926 pub fn forward_ids(
7929 &mut self,
7930 ids: &[u32],
7931 task_mask: Option<&TaskMask>,
7932 ) -> Result<Vec<f32>, String> {
7933 #[cfg(target_os = "macos")]
7934 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
7935 if ids.is_empty() {
7936 return Err("empty id sequence".to_string());
7937 }
7938 self.clear_sequence_state();
7939 self.check_forward_graph("forward_ids setup", 0)?;
7940 if task_mask.is_none() {
7941 self.o1_begin();
7942 }
7943 let mut hidden = vec![0.0f32; self.hidden_size];
7944 let mut pos = 0usize;
7945 if let Some(b) = &mut self.dsv41 {
7946 let pool = self.pool.clone();
7947 let mut logits = Vec::new();
7948 crate::dsv41::forward_chunk(
7949 &b.0,
7950 &b.1,
7951 &b.2,
7952 &mut b.3,
7953 ids,
7954 0,
7955 pool.as_deref(),
7956 &mut logits,
7957 );
7958 if let Err(err) = self.o1_seal_checked() {
7959 self.clear_sequence_state();
7960 return Err(err);
7961 }
7962 return Ok(logits);
7963 }
7964 if self.can_prefill_batched() && !self.graph_prefill_preferred() && ids.len() > 2 {
7972 let chunk = self.prefill_chunk();
7976 let hs = self.hidden_size;
7977 while pos < ids.len() {
7978 let end = (pos + chunk).min(ids.len());
7979 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
7980 self.check_forward_graph("forward_ids batched prefill", end - 1)?;
7981 hidden.copy_from_slice(&hb[(end - pos - 1) * hs..]);
7982 pos = end;
7983 }
7984 }
7985 if task_mask.is_none()
7994 && !self.graph_prefill_preferred()
7995 && !std::env::var("CMF_PAIR").is_ok_and(|v| v == "0")
7996 && self.pair_supported()
7997 {
7998 while pos + 1 < ids.len() {
7999 let e1 = self.embed_single(ids[pos]);
8000 let e2 = self.embed_single(ids[pos + 1]);
8001 let (_, h2) = self.forward_pair(&e1, &e2, pos);
8002 self.check_forward_graph("forward_ids pair", pos + 1)?;
8003 self.commit_linear_scratch();
8004 hidden = h2;
8005 pos += 2;
8006 }
8007 }
8008 if task_mask.is_none() && pos == 0 && ids.len() > 1 {
8011 if let Some(lg) = self.embryo_prefill_chunked(ids, 0) {
8012 self.graph_logits = Some(lg);
8013 hidden = vec![0.0; self.hidden_size];
8014 pos = ids.len();
8015 }
8016 }
8017 while pos < ids.len() {
8018 hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8019 self.check_forward_graph("forward_ids", pos)?;
8020 pos += 1;
8021 }
8022 if let Some(logits) = self.graph_logits.take() {
8023 if let Err(err) = self.o1_seal_checked() {
8027 self.clear_sequence_state();
8028 return Err(err);
8029 }
8030 return Ok(logits);
8031 }
8032 if let Err(err) = self.o1_seal_checked() {
8036 self.clear_sequence_state();
8037 return Err(err);
8038 }
8039 let normed = inference::rms_norm(
8040 &hidden,
8041 &self.weights.final_norm,
8042 self.rms_eps,
8043 self.norm_style,
8044 );
8045 Ok(self.lm_head_forward(&normed))
8046 }
8047
8048 #[doc(hidden)]
8052 pub fn dsv41_serial_logits(&mut self, ids: &[u32]) -> Result<Vec<Vec<f32>>, String> {
8053 #[cfg(target_os = "macos")]
8054 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8055 if ids.is_empty() {
8056 return Err("empty id sequence".to_string());
8057 }
8058 self.clear_sequence_state();
8059 self.dsv41
8060 .as_ref()
8061 .ok_or_else(|| "dsv41 serial logits require a DeepSeek-V4.1 model".to_string())?;
8062 self.o1_begin();
8063 let rows = {
8064 let pool = self.pool.clone();
8065 let b = self
8066 .dsv41
8067 .as_mut()
8068 .expect("dsv41 checked above; state cannot change during forward");
8069 let mut rows = Vec::with_capacity(ids.len());
8070 for (position, &id) in ids.iter().enumerate() {
8071 let mut logits = Vec::new();
8072 crate::dsv41::forward_token(
8073 &b.0,
8074 &b.1,
8075 &b.2,
8076 &mut b.3,
8077 id,
8078 position,
8079 pool.as_deref(),
8080 &mut logits,
8081 );
8082 rows.push(logits);
8083 }
8084 rows
8085 };
8086 self.o1_seal();
8087 Ok(rows)
8088 }
8089
8090 pub fn ppl_ids(&mut self, ids: &[u32]) -> Result<f64, String> {
8097 let (nll, cnt) = self.nll_ids_from(ids, 0)?;
8098 Ok((nll / cnt.max(1) as f64).exp())
8099 }
8100
8101 pub fn probe_ffn_mass(&mut self, ids: &[u32]) -> Vec<Vec<f64>> {
8106 self.clear_sequence_state();
8107 FFN_PROBE.with(|p| {
8108 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8109 });
8110 crate::gpu::cpu_scope(|| {
8111 for (pos, &id) in ids.iter().enumerate() {
8112 let emb = self.embed_single(id);
8113 let _ = self.forward_layers(&emb, pos, None);
8114 }
8115 });
8116 self.clear_sequence_state();
8117 FFN_PROBE
8118 .with(|p| p.borrow_mut().take())
8119 .unwrap_or_default()
8120 }
8121
8122 pub fn probe_ffn_mass_batch(&mut self, ids: &[u32]) -> Result<Vec<Vec<f64>>, String> {
8126 if let Err(err) = self.nll_begin() {
8127 let _ = FFN_PROBE.with(|p| p.borrow_mut().take());
8131 self.nll_end();
8132 return Err(err);
8133 }
8134 FFN_PROBE.with(|p| {
8135 *p.borrow_mut() = Some(vec![vec![0f64; self.intermediate_size]; self.num_layers]);
8136 });
8137 let result: Result<(), String> = (|| {
8138 for chunk in ids.chunks(256) {
8139 if chunk.len() < 2 {
8140 continue;
8141 }
8142 self.nll_ids_masked(chunk, 0, None)?;
8143 }
8144 Ok(())
8145 })();
8146 self.nll_end();
8147 let probe = FFN_PROBE
8148 .with(|p| p.borrow_mut().take())
8149 .unwrap_or_default();
8150 match result {
8151 Ok(()) => Ok(probe),
8152 Err(err) => {
8153 drop(probe);
8154 Err(err)
8155 }
8156 }
8157 }
8158
8159 pub fn ppl_ids_masked(&mut self, ids: &[u32], mask: &TaskMask) -> Result<f64, String> {
8163 self.nll_begin()?;
8164 let result: Result<f64, String> = (|| {
8165 let mut nll = 0f64;
8166 let mut cnt = 0usize;
8167 let mut hidden = vec![0f32; self.hidden_size];
8168 for (pos, &id) in ids.iter().enumerate() {
8169 if pos > 0 {
8170 inference::rms_norm_into(
8171 &hidden,
8172 &self.weights.final_norm,
8173 self.rms_eps,
8174 self.norm_style,
8175 &mut self.ws.n1,
8176 );
8177 let mut logits = self.lm_head_forward(&self.ws.n1);
8178 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
8179 let sum: f64 = logits.iter().map(|&v| ((v - max) as f64).exp()).sum();
8180 let p = ((logits[id as usize] - max) as f64).exp() / sum.max(1e-300);
8181 nll -= p.max(1e-300).ln();
8182 cnt += 1;
8183 attention::recycle_buf(&mut logits);
8184 }
8185 let emb = self.embed_single(id);
8186 hidden = self.forward_layers(&emb, pos, Some(mask));
8187 self.nll_check_graph("masked serial forward", pos)?;
8188 let _ = self.graph_logits.take();
8192 }
8193 Ok((nll / cnt.max(1) as f64).exp())
8194 })();
8195 self.nll_end();
8196 result
8197 }
8198
8199 pub fn nll_ids_masked(
8218 &mut self,
8219 ids: &[u32],
8220 start: usize,
8221 task_mask: Option<&TaskMask>,
8222 ) -> Result<(f64, usize), String> {
8223 let task_mask = self.drop_open_mask(task_mask);
8224 self.nll_ids_inner(ids, start, task_mask)
8225 }
8226
8227 pub fn nll_ids_from(&mut self, ids: &[u32], start: usize) -> Result<(f64, usize), String> {
8228 self.nll_ids_inner(ids, start, None)
8229 }
8230
8231 fn nll_ids_inner(
8232 &mut self,
8233 ids: &[u32],
8234 start: usize,
8235 task_mask: Option<&TaskMask>,
8236 ) -> Result<(f64, usize), String> {
8237 self.nll_begin()?;
8238 let result: Result<(f64, usize), String> = (|| {
8239 let mut nll = 0f64;
8240 let mut cnt = 0usize;
8241 let (graph_quality, fused_head_quality) = nll_graph_policy(
8254 task_mask.is_none(),
8255 self.graph_prefill_preferred(),
8256 crate::gpu::q1_force(),
8257 );
8258 self.graph_head_required = fused_head_quality;
8259 self.graph_want_logits = fused_head_quality;
8260 #[cfg(target_os = "macos")]
8261 if graph_quality && std::env::var("CMF_METAL_BATCH_NLL").as_deref() != Ok("0") {
8262 match self.nll_batch_metal(ids, start) {
8263 MetalBatchNllOutcome::Completed(nll, count) => {
8264 return Ok((nll, count));
8265 }
8266 MetalBatchNllOutcome::Declined => {}
8267 MetalBatchNllOutcome::Failed(err) => return Err(err),
8268 }
8269 }
8270 let force_serial = std::env::var("CMF_NLL_SERIAL").as_deref() == Ok("1");
8275 if self.can_prefill_batched() && !graph_quality && !force_serial {
8276 const CHUNK: usize = 128;
8282 const LM_SUB: usize = 32;
8283 let n = ids.len().saturating_sub(1);
8284 let hs = self.hidden_size;
8285 let rows = self.weights.lm_head.rows();
8286 let mut pos = 0usize;
8287 let state_trace = std::env::var("CMF_STATE_TRACE").is_ok();
8288 while pos < n {
8289 let end = (pos + CHUNK).min(n);
8290 let bsz = end - pos;
8291 let hb = self.prefill_rows(&ids[pos..end], pos, task_mask)?;
8292 self.nll_check_graph("batched prefill", pos)?;
8293 if state_trace && end % 256 == 0 {
8294 self.trace_recurrent_state(end);
8295 }
8296 let mut k0 = 0usize;
8297 while k0 < bsz {
8298 let k1 = (k0 + LM_SUB).min(bsz);
8299 let sb = k1 - k0;
8300 if pos + k1 <= start {
8303 k0 = k1;
8304 continue;
8305 }
8306 let mut normed = vec![0.0f32; sb * hs];
8307 for k in 0..sb {
8308 let r = inference::rms_norm(
8309 &hb[(k0 + k) * hs..(k0 + k + 1) * hs],
8310 &self.weights.final_norm,
8311 self.rms_eps,
8312 self.norm_style,
8313 );
8314 normed[k * hs..(k + 1) * hs].copy_from_slice(&r);
8315 }
8316 let mut logits = vec![0.0f32; sb * rows];
8317 self.weights
8318 .lm_head
8319 .matmat(&normed, sb, &mut logits, self.pool.as_deref());
8320 for k in 0..sb {
8321 if pos + k0 + k < start {
8322 continue;
8323 }
8324 self.nll_check_graph("batched score row", pos + k0 + k)?;
8325 let lg = &mut logits[k * rows..k * rows + self.vocab_size.min(rows)];
8326 if let Some(mu) = self.logit_multiplier {
8327 for v in lg.iter_mut() {
8328 *v *= mu;
8329 }
8330 }
8331 if let Some(c) = self.final_softcap {
8335 for v in lg.iter_mut() {
8336 *v = c * (*v / c).tanh();
8337 }
8338 }
8339 if let Some(cm) = self.head_clusters.clone() {
8342 self.hierarchical_head_logprobs(
8343 &normed[k * hs..(k + 1) * hs],
8344 &cm,
8345 lg,
8346 );
8347 }
8348 let lg = &logits[k * rows..k * rows + self.vocab_size.min(rows)];
8349 let target = ids[pos + k0 + k + 1] as usize;
8350 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8351 let lse: f64 = lg
8352 .iter()
8353 .map(|&v| ((v - max) as f64).exp())
8354 .sum::<f64>()
8355 .ln()
8356 + max as f64;
8357 nll += lse - lg[target] as f64;
8358 cnt += 1;
8359 if std::env::var("CMF_PPL_TRACE").is_ok() {
8360 let top = lg
8361 .iter()
8362 .enumerate()
8363 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8364 .map(|(i, _)| i)
8365 .unwrap_or(0);
8366 eprintln!(
8367 "BTRACE pos {} target {} nll {:.4} top {} lg_t {:.3} lg_top {:.3}",
8368 pos + k0 + k,
8369 target,
8370 lse - lg[target] as f64,
8371 top,
8372 lg[target],
8373 lg[top]
8374 );
8375 }
8376 }
8377 k0 = k1;
8378 }
8379 pos = end;
8380 }
8381 return Ok((nll, cnt));
8382 }
8383 for pos in 0..ids.len().saturating_sub(1) {
8384 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, task_mask);
8385 self.nll_check_graph("serial forward", pos)?;
8386 let out_of_band = self.graph_logits.take();
8394 if self.graph_head_required && out_of_band.is_none() {
8395 METAL_GRAPH_HEAD_MISS.fetch_add(
8396 1,
8397 std::sync::atomic::Ordering::Relaxed,
8398 );
8399 return Err(format!(
8400 "fused Metal graph head did not complete at NLL position {pos}"
8401 ));
8402 }
8403 if pos < start {
8404 continue;
8405 }
8406 let logits = match out_of_band {
8407 Some(lg) => lg,
8408 None => {
8409 let normed = inference::rms_norm(
8410 &hidden,
8411 &self.weights.final_norm,
8412 self.rms_eps,
8413 self.norm_style,
8414 );
8415 self.lm_head_forward(&normed)
8419 }
8420 };
8421 let target = ids[pos + 1] as usize;
8422 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8423 let lse: f64 = logits
8424 .iter()
8425 .map(|&v| ((v - max) as f64).exp())
8426 .sum::<f64>()
8427 .ln()
8428 + max as f64;
8429 let tok_nll = lse - logits[target] as f64;
8430 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8431 let top = logits
8432 .iter()
8433 .enumerate()
8434 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8435 .map(|(i, _)| i)
8436 .unwrap_or(0);
8437 eprintln!(
8438 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8439 logits[target], logits[top]
8440 );
8441 }
8442 nll += tok_nll;
8443 cnt += 1;
8444 }
8445 Ok((nll, cnt))
8446 })();
8447 self.nll_end();
8448 result
8449 }
8450
8451 fn nll_from_hidden(&mut self, hidden: &[f32], target: u32, pos: usize) -> f64 {
8456 let normed = inference::rms_norm(
8457 hidden,
8458 &self.weights.final_norm,
8459 self.rms_eps,
8460 self.norm_style,
8461 );
8462 let mut logits = self.lm_head_forward(&normed);
8465 let target = target as usize;
8466 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8467 let lse: f64 = logits
8468 .iter()
8469 .map(|&v| ((v - max) as f64).exp())
8470 .sum::<f64>()
8471 .ln()
8472 + max as f64;
8473 let tok_nll = lse - logits[target] as f64;
8474 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8475 let top = logits
8476 .iter()
8477 .enumerate()
8478 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8479 .map(|(i, _)| i)
8480 .unwrap_or(0);
8481 eprintln!(
8482 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8483 logits[target], logits[top]
8484 );
8485 }
8486 attention::recycle_buf(&mut logits);
8487 tok_nll
8488 }
8489
8490 fn trace_recurrent_state(&self, pos: usize) {
8498 let stats = |v: &[f32]| -> (f64, f64) {
8499 if v.is_empty() {
8500 return (0.0, 0.0);
8501 }
8502 let (mut ss, mut mx) = (0f64, 0f64);
8503 for &x in v {
8504 ss += (x as f64) * (x as f64);
8505 mx = mx.max((x as f64).abs());
8506 }
8507 ((ss / v.len() as f64).sqrt(), mx)
8508 };
8509 for (li, l) in self.kv_cache.layers.iter().enumerate() {
8510 let lw = &self.weights.layers[self.phys_layer(li)];
8511 let (kind, s_len) = match &lw.attn {
8512 AttnKind::Linear(_) => (
8513 "vmf",
8514 self.vmf_cfg.map(|c| c.state_len()).unwrap_or(0),
8515 ),
8516 AttnKind::LinearGdn(_) => (
8517 "gdn",
8518 self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0),
8519 ),
8520 AttnKind::Bounded(_) => ("bounded", 0),
8521 AttnKind::Full { .. } => ("full", 0),
8522 _ => ("other", 0),
8523 };
8524 let (rms, max) = stats(&l.linear_state);
8525 let s_part = if kind == "vmf" {
8526 &l.linear_state[..s_len.min(l.linear_state.len())]
8527 } else if kind == "gdn" {
8528 let ring = l.linear_state.len().saturating_sub(s_len).min(l.linear_state.len());
8529 &l.linear_state[ring..]
8530 } else {
8531 &l.linear_state[..0]
8532 };
8533 let (s_rms, s_max) = stats(s_part);
8534 let (ring_rms, ring_len) = match &l.bounded {
8535 Some(b) => (stats(&b.ring_k).0, b.ring_k.len()),
8536 None => (0.0, 0),
8537 };
8538 eprintln!(
8539 "STATE pos={pos} layer={li} kind={kind} state_len={} rms={rms:.5} max={max:.4} \
8540 S_rms={s_rms:.5} S_max={s_max:.4} ring_k_rms={ring_rms:.5} ring_len={ring_len} kv_rows={}",
8541 l.linear_state.len(),
8542 l.seq_len
8543 );
8544 }
8545 }
8546
8547 pub fn nll_ids_o1(&mut self, ids: &[u32], prefill: usize) -> Result<(f64, usize), String> {
8565 self.nll_begin()?;
8570 let requested_prefix = (prefill > 0).then_some(prefill);
8571 self.o1_begin_with_prefix(requested_prefix);
8572 let n = ids.len().saturating_sub(1);
8573 let requested_start = prefill.min(n);
8574 let exact_end = if self.o1_active() {
8579 match requested_prefix {
8580 Some(requested) => self.o1_effective_boundary(requested),
8581 None => self
8582 .o1_cfg
8583 .as_ref()
8584 .and_then(|c| crate::nystrom::o1_deferred_boundary(c.w, c.sink)),
8585 }
8586 .unwrap_or(requested_start)
8587 .min(n)
8588 } else {
8589 requested_start
8590 };
8591 let mut nll = 0f64;
8592 let mut cnt = 0usize;
8593
8594 let mut pos = 0usize;
8598 if self.can_prefill_batched() {
8599 const CHUNK: usize = 128;
8600 while pos < exact_end {
8601 let end = (pos + CHUNK).min(exact_end);
8602 let hiddens = self.prefill_batch(&ids[pos..end], pos);
8603 if self
8604 .graph_failed
8605 .swap(false, std::sync::atomic::Ordering::Relaxed)
8606 {
8607 self.cancel
8608 .store(false, std::sync::atomic::Ordering::Relaxed);
8609 self.nll_end();
8610 return Err("GPU graph failed during O(1) NLL prefix".into());
8611 }
8612 for row in 0..end - pos {
8613 let score_pos = pos + row;
8614 if score_pos >= requested_start && score_pos < n {
8615 nll += self.nll_from_hidden(
8616 &hiddens[row * self.hidden_size..(row + 1) * self.hidden_size],
8617 ids[score_pos + 1],
8618 score_pos,
8619 );
8620 cnt += 1;
8621 }
8622 }
8623 pos = end;
8624 }
8625 } else {
8626 while pos < exact_end {
8627 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
8628 if self
8629 .graph_failed
8630 .swap(false, std::sync::atomic::Ordering::Relaxed)
8631 {
8632 self.cancel
8633 .store(false, std::sync::atomic::Ordering::Relaxed);
8634 self.nll_end();
8635 return Err("GPU graph failed during O(1) NLL prefix".into());
8636 }
8637 if pos >= requested_start && pos < n {
8638 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
8639 cnt += 1;
8640 }
8641 pos += 1;
8642 }
8643 }
8644 self.o1_seal_checked().map_err(|err| {
8645 self.nll_end();
8646 err
8647 })?;
8648
8649 let batch_k = std::env::var("CMF_BATCH_K")
8658 .ok()
8659 .and_then(|v| v.parse::<usize>().ok())
8660 .unwrap_or(0);
8661 let batch_admitted = batch_k > 0
8662 && self.can_prefill_batched()
8663 && self.o1_active()
8664 && std::env::var("CMF_O1_GPU").as_deref() == Ok("1")
8665 && (0..self.num_layers).all(|li| {
8666 let cache = &self.kv_cache.layers[self.phys_layer(li)];
8667 cache.o1.is_none() || cache.o1_views().is_some()
8668 });
8669 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8670 eprintln!(
8671 "nll-batch: phase=post-seal admission={} requested_k={} scored_rows={}",
8672 batch_admitted,
8673 batch_k,
8674 n.saturating_sub(exact_end),
8675 );
8676 }
8677 let mut batch_completed = false;
8678 if batch_admitted && exact_end < n {
8679 let hs = self.hidden_size;
8680 let mut batch_pos = exact_end;
8681 while batch_pos < n {
8682 let end = (batch_pos + batch_k).min(n);
8683 let bk = end - batch_pos;
8684 let mut hiddens = vec![0.0f32; bk * hs];
8685 for (row, &id) in ids[batch_pos..end].iter().enumerate() {
8686 hiddens[row * hs..(row + 1) * hs].copy_from_slice(&self.embed_single(id));
8687 }
8688 let positions: Vec<usize> = (batch_pos..end).collect();
8689 let t_batch = std::time::Instant::now();
8690 let outcome = self.try_batch_graph_wgpu(&mut hiddens, &positions, bk, None);
8691 if std::env::var("CMF_GRAPH_PROF").is_ok() {
8692 let ms = t_batch.elapsed().as_secs_f64() * 1000.0;
8693 eprintln!(
8694 "nll-batch: phase=post-seal mode=o1 k={bk} pos={}..{} outcome={outcome:?} {ms:.1} ms ({:.1} tok/s)",
8695 batch_pos,
8696 end.saturating_sub(1),
8697 bk as f64 / (ms / 1000.0),
8698 );
8699 }
8700 if let Err(err) = self.nll_check_graph("batch graph", batch_pos) {
8701 self.nll_end();
8702 return Err(err);
8703 }
8704 match outcome {
8705 crate::gpu::BatchGraphOutcome::Completed => {
8706 batch_completed = true;
8707 for row in 0..bk {
8708 nll += self.nll_from_hidden(
8709 &hiddens[row * hs..(row + 1) * hs],
8710 ids[batch_pos + row + 1],
8711 batch_pos + row,
8712 );
8713 cnt += 1;
8714 }
8715 batch_pos = end;
8716 }
8717 crate::gpu::BatchGraphOutcome::Declined => {
8718 if batch_completed {
8719 self.nll_end();
8720 return Err(format!(
8721 "O(1) NLL batch declined after completed chunk at position {batch_pos}"
8722 ));
8723 }
8724 break;
8725 }
8726 crate::gpu::BatchGraphOutcome::Failed => {
8727 self.nll_end();
8728 return Err(format!(
8729 "O(1) NLL batch graph failed after admission at position {batch_pos}"
8730 ));
8731 }
8732 }
8733 }
8734 if batch_completed && cnt == n.saturating_sub(requested_start) {
8735 self.nll_end();
8736 return Ok((nll, cnt));
8737 }
8738 }
8739
8740 for pos in exact_end..n {
8745 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
8746 if self
8747 .graph_failed
8748 .swap(false, std::sync::atomic::Ordering::Relaxed)
8749 {
8750 self.cancel
8751 .store(false, std::sync::atomic::Ordering::Relaxed);
8752 self.nll_end();
8753 return Err(format!(
8754 "GPU graph failed during O(1) NLL serial scoring at position {pos}"
8755 ));
8756 }
8757 nll += self.nll_from_hidden(&hidden, ids[pos + 1], pos);
8758 cnt += 1;
8759 }
8760 self.nll_end();
8761 Ok((nll, cnt))
8762 }
8763
8764 pub fn calib_ids(&mut self, ids: &[u32], temps: &[f32]) -> (Vec<bool>, Vec<Vec<f32>>) {
8772 self.clear_sequence_state();
8773 let n = ids.len().saturating_sub(1);
8774 let mut correct = Vec::with_capacity(n);
8775 let mut pmax = Vec::with_capacity(n);
8776 for pos in 0..n {
8777 let emb = self.embed_single(ids[pos]);
8778 let hidden = self.forward_layers(&emb, pos, None);
8779 let logits = if let Some(logits) = self.graph_logits.take() {
8780 logits
8781 } else {
8782 let normed = inference::rms_norm(
8783 &hidden,
8784 &self.weights.final_norm,
8785 self.rms_eps,
8786 self.norm_style,
8787 );
8788 self.lm_head_forward(&normed)
8792 };
8793 let target = ids[pos + 1] as usize;
8794 let (mut amax, mut mval) = (0usize, f32::NEG_INFINITY);
8795 for (i, &v) in logits.iter().enumerate() {
8796 if v > mval {
8797 mval = v;
8798 amax = i;
8799 }
8800 }
8801 correct.push(amax == target);
8802 let row: Vec<f32> = temps
8803 .iter()
8804 .map(|&t| {
8805 let tt = t.max(1e-3);
8806 let s: f32 = logits.iter().map(|&v| ((v - mval) / tt).exp()).sum();
8807 1.0 / s.max(1e-12) })
8809 .collect();
8810 pmax.push(row);
8811 }
8812 self.clear_sequence_state();
8813 (correct, pmax)
8814 }
8815
8816 pub fn ppl_ids_dynamic(&mut self, ids: &[u32]) -> Result<(f64, usize), String> {
8823 if self.dyn_router.is_none() {
8824 return Ok((self.ppl_ids(ids)?, 0));
8825 }
8826 self.nll_begin()?;
8827 let saved_active = self.dyn_active;
8828 let mut router = self
8829 .dyn_router
8830 .take()
8831 .ok_or_else(|| "dynamic router disappeared before PPL scoring".to_string())?;
8832 router.reset();
8833 self.dyn_phi_seen = 0;
8834 let _ = self.set_active_skill(None);
8835
8836 let result: Result<(f64, usize), String> = (|| {
8837 let mut nll = 0f64;
8838 let mut cnt = 0usize;
8839 for pos in 0..ids.len().saturating_sub(1) {
8840 let hidden = self.forward_layers(&self.embed_single(ids[pos]), pos, None);
8841 self.nll_check_graph("dynamic serial forward", pos)?;
8842 let out_of_band = self.graph_logits.take();
8843 let mut logits = match out_of_band {
8844 Some(lg) => lg,
8845 None => {
8846 let normed = inference::rms_norm(
8847 &hidden,
8848 &self.weights.final_norm,
8849 self.rms_eps,
8850 self.norm_style,
8851 );
8852 self.lm_head_forward(&normed)
8856 }
8857 };
8858 let target = ids[pos + 1] as usize;
8859 let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
8860 let lse: f64 = logits
8861 .iter()
8862 .map(|&v| ((v - max) as f64).exp())
8863 .sum::<f64>()
8864 .ln()
8865 + max as f64;
8866 let tok_nll = lse - logits[target] as f64;
8867 if std::env::var("CMF_PPL_TRACE").is_ok() && pos < 48 {
8868 let top = logits
8869 .iter()
8870 .enumerate()
8871 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
8872 .map(|(i, _)| i)
8873 .unwrap_or(0);
8874 eprintln!(
8875 "pos {pos:3} tgt {target:6} nll {tok_nll:7.3} | top1 {top:6} lg[t]={:.2} lg[top]={:.2}",
8876 logits[target], logits[top]
8877 );
8878 }
8879 nll += tok_nll;
8880 cnt += 1;
8881 attention::recycle_buf(&mut logits);
8882 let phi = self.dyn_phi_ema.clone();
8884 if let Some(new_active) = router.step(&phi, pos) {
8885 let _ = self.set_active_skill(new_active);
8886 }
8887 }
8888 Ok(((nll / cnt.max(1) as f64).exp(), router.switches.len()))
8889 })();
8890
8891 let _ = self.set_active_skill(saved_active);
8894 self.dyn_router = Some(router);
8895 self.nll_end();
8896 result
8897 }
8898
8899 pub fn probe_phi(&mut self, ids: &[u32], layer: usize) -> Vec<f32> {
8901 self.clear_sequence_state();
8902 let mut acc = vec![0f32; self.hidden_size];
8903 for (pos, &id) in ids.iter().enumerate() {
8904 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
8905 for (a, v) in acc.iter_mut().zip(&h) {
8906 *a += v;
8907 }
8908 }
8909 let n = ids.len().max(1) as f32;
8910 for a in acc.iter_mut() {
8911 *a /= n;
8912 }
8913 self.clear_sequence_state();
8914 acc
8915 }
8916
8917 pub fn probe_phi_span(
8931 &mut self,
8932 ids: &[u32],
8933 layer: usize,
8934 span: std::ops::Range<usize>,
8935 ) -> Vec<f32> {
8936 #[cfg(target_os = "macos")]
8937 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8938 let end = span.end.min(ids.len());
8939 let start = span.start.min(end);
8940 let reset = |p: &mut Self| p.clear_sequence_state();
8941 reset(self);
8942 let mut acc = vec![0f32; self.hidden_size];
8943 for (pos, &id) in ids[..end].iter().enumerate() {
8944 let h = self.forward_layers_upto(&self.embed_single(id), pos, None, Some(layer));
8945 if pos >= start {
8946 for (a, v) in acc.iter_mut().zip(&h) {
8947 *a += v;
8948 }
8949 }
8950 }
8951 let n = end - start;
8952 if n > 0 {
8953 let n = n as f32;
8954 for a in acc.iter_mut() {
8955 *a /= n;
8956 }
8957 }
8958 reset(self);
8959 acc
8960 }
8961
8962 pub fn decode_step_logits(&mut self, token: u32, position: usize) -> Vec<f32> {
8969 #[cfg(target_os = "macos")]
8970 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
8971 self.graph_logits = None;
8972 let hidden = self.forward_layers(&self.embed_single(token), position, None);
8973 if let Some(logits) = self.graph_logits.take() {
8974 return logits;
8975 }
8976 inference::rms_norm_into(
8977 &hidden,
8978 &self.weights.final_norm,
8979 self.rms_eps,
8980 self.norm_style,
8981 &mut self.ws.n1,
8982 );
8983 self.lm_head_forward(&self.ws.n1)
8984 }
8985
8986 fn prefill_batch(&mut self, ids: &[u32], start_pos: usize) -> Vec<f32> {
8992 self.prefill_batch_masked(ids, start_pos, None)
8993 }
8994
8995 fn prefill_batch_masked(
9001 &mut self,
9002 ids: &[u32],
9003 start_pos: usize,
9004 task_mask: Option<&TaskMask>,
9005 ) -> Vec<f32> {
9006 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, usize::MAX)
9007 }
9008
9009 fn prefill_rows(
9017 &mut self,
9018 ids: &[u32],
9019 pos: usize,
9020 task_mask: Option<&TaskMask>,
9021 ) -> Result<Vec<f32>, String> {
9022 self.prefill_input_rows(PrefillIn::Ids(ids), pos, task_mask)
9023 }
9024
9025 fn prefill_input_rows(
9026 &mut self,
9027 input: PrefillIn<'_>,
9028 pos: usize,
9029 task_mask: Option<&TaskMask>,
9030 ) -> Result<Vec<f32>, String> {
9031 self.mimo_moe_prepare();
9032 let hs = self.hidden_size;
9033 let bk = match input {
9034 PrefillIn::Ids(ids) => ids.len(),
9035 PrefillIn::Hidden(rows) => rows.len() / hs,
9036 };
9037 #[cfg(not(target_os = "macos"))]
9038 if task_mask.is_none()
9039 && !self.o1_active()
9040 && bk > 1
9041 && (self.batch_prefix_prefill()
9042 || (self.verify_exact_moe
9043 && crate::gpu::enabled_here()
9044 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)))
9045 {
9046 let mut hiddens = match input {
9047 PrefillIn::Hidden(rows) => rows.to_vec(),
9048 PrefillIn::Ids(ids) => ids.iter().flat_map(|&id| self.embed_single(id)).collect(),
9049 };
9050 let positions: Vec<usize> = (pos..pos + bk).collect();
9051 let mut run = 0usize;
9052 match self.try_batch_graph_wgpu_prefix(
9053 &mut hiddens,
9054 &positions,
9055 bk,
9056 None,
9057 Some(&mut run),
9058 ) {
9059 crate::gpu::BatchGraphOutcome::Completed => {
9060 let out = if run < self.num_layers {
9061 self.prefill_batch_span(
9062 PrefillIn::Hidden(&hiddens),
9063 pos,
9064 None,
9065 run,
9066 self.num_layers,
9067 )
9068 } else {
9069 hiddens
9070 };
9071 return if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9072 Err("MiMo attention graph failed after admission".into())
9073 } else { Ok(out) };
9074 }
9075 crate::gpu::BatchGraphOutcome::Failed => {
9076 return Err("batched prefix prefill failed after admission".into());
9077 }
9078 crate::gpu::BatchGraphOutcome::Declined => {
9079 #[cfg(feature = "gpu")]
9081 self.pull_lagging_host_kv(0, self.num_layers, pos);
9082 }
9083 }
9084 }
9085 let out = self.prefill_batch_span(input, pos, task_mask, 0, usize::MAX);
9086 if self.graph_failed.load(std::sync::atomic::Ordering::Relaxed) {
9087 Err("batch tail graph failed after admission".into())
9088 } else { Ok(out) }
9089 }
9090
9091 fn prefill_batch_span(
9097 &mut self,
9098 input: PrefillIn<'_>,
9099 start_pos: usize,
9100 task_mask: Option<&TaskMask>,
9101 from: usize,
9102 upto_excl: usize,
9103 ) -> Vec<f32> {
9104 let hs = self.hidden_size;
9105 let b = match input {
9106 PrefillIn::Ids(ids) => ids.len(),
9107 PrefillIn::Hidden(hb) => hb.len() / hs,
9108 };
9109 let upto_excl = upto_excl.min(self.num_layers);
9110 let mut h: Vec<f32>;
9114 let mut h_ready;
9115 match input {
9116 PrefillIn::Ids(_) => {
9117 h = vec![0.0; b * hs];
9118 h_ready = false;
9119 }
9120 PrefillIn::Hidden(hb) => {
9121 h = hb.to_vec();
9122 h_ready = true;
9123 }
9124 }
9125 let fill_h = |h: &mut Vec<f32>, me: &Self| {
9126 if let PrefillIn::Ids(ids) = input {
9127 for (bi, &id) in ids.iter().enumerate() {
9128 let e = me.embed_single(id);
9129 h[bi * hs..(bi + 1) * hs].copy_from_slice(&e);
9130 }
9131 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9132 if let Ok(t) = tp.parse::<usize>() {
9133 if t >= start_pos && t < start_pos + ids.len() {
9134 let bi = t - start_pos;
9135 let row = &h[bi * hs..(bi + 1) * hs];
9136 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9137 eprintln!(
9138 "BATCH pos {t} embed: id {} |h| = {n:.6} h0 {:.6} h1 {:.6} | b={} start={start_pos} ids[..8]={:?}",
9139 ids[bi],
9140 row[0],
9141 row[1],
9142 ids.len(),
9143 &ids[..ids.len().min(8)]
9144 );
9145 }
9146 }
9147 }
9148 }
9149 };
9150 let (_nkv, _hd, _rd, eps) = (
9151 self.num_kv_heads,
9152 self.head_dim,
9153 self.rotary_dim,
9154 self.rms_eps,
9155 );
9156 let pool = self.pool.clone();
9157 let norm_style = self.norm_style;
9158 self.mimo_moe_prepare();
9159 let automatic_gpu_prefix = self.automatic_gpu_prefix();
9160
9161 #[cfg(target_os = "macos")]
9162 let mut chunk_skip_until = 0usize;
9163 for li in from..upto_excl {
9164 let _capacity_tail = automatic_gpu_prefix
9165 .filter(|&prefix| {
9166 li >= prefix && !(self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false))
9167 })
9168 .map(|_| crate::gpu::enter_cpu_scope());
9169 crate::gpu::set_layer(li as i64); #[cfg(feature = "gpu")]
9174 let _fast_gemm = self
9175 .proj_gate_sigmoid
9176 .then(crate::gpu::enter_prefill_fast_gemm);
9177 #[cfg(target_os = "macos")]
9183 if task_mask.is_none() {
9184 if li < chunk_skip_until {
9185 continue;
9186 }
9187 if !h_ready && li == 0 && self.weights.embed_tokens.q8_row_parts().is_none() {
9193 fill_h(&mut h, self);
9194 h_ready = true;
9195 }
9196 let ids_for_embed = match input {
9197 PrefillIn::Ids(ids) => (!h_ready && li == 0).then_some(ids),
9198 PrefillIn::Hidden(_) => None,
9199 };
9200 let end = self.chunk_run_gpu(li, &mut h, b, start_pos, ids_for_embed, upto_excl);
9201 if end > li {
9202 h_ready = true;
9203 chunk_skip_until = end;
9204 if self.is_loop_end(end - 1) && end < self.num_layers {
9207 for bi in 0..b {
9208 let normed = inference::rms_norm(
9209 &h[bi * hs..(bi + 1) * hs],
9210 &self.weights.final_norm,
9211 eps,
9212 norm_style,
9213 );
9214 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9215 }
9216 }
9217 continue;
9218 }
9219 }
9220 if !h_ready {
9221 fill_h(&mut h, self);
9222 h_ready = true;
9223 }
9224 if task_mask.is_none() && self.verify_exact_moe {
9225 let positions: Vec<_> = (start_pos..start_pos + b).collect();
9226 match self.mimo_graph_layer_rows(li, &mut h, &positions) {
9227 crate::gpu::BatchGraphOutcome::Completed => continue,
9228 crate::gpu::BatchGraphOutcome::Failed => return h,
9229 crate::gpu::BatchGraphOutcome::Declined => {},
9230 }
9231 }
9232 #[cfg(feature = "gpu")]
9233 self.pull_lagging_host_kv(li, li + 1, start_pos);
9234 let lw = &self.weights.layers[self.phys_layer(li)];
9235 let t_attn = std::time::Instant::now();
9236 match &lw.attn {
9238 AttnKind::Kda(w) => {
9239 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
9241 let mut normed = vec![0.0f32; b * hs];
9242 for bi in 0..b {
9243 inference::rms_norm_into(
9244 &h[bi * hs..(bi + 1) * hs],
9245 &lw.input_norm,
9246 eps,
9247 norm_style,
9248 &mut normed[bi * hs..(bi + 1) * hs],
9249 );
9250 }
9251 let attn = crate::linear_core::kda_forward_batch(
9252 &normed,
9253 b,
9254 w,
9255 &cfg,
9256 &mut self.kv_cache.layers[li].linear_state,
9257 pool.as_deref(),
9258 );
9259 for (dst, &a) in h.iter_mut().zip(&attn) {
9260 *dst += a;
9261 }
9262 }
9263 AttnKind::LinearGdn(w) => {
9264 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
9266 let mut normed = vec![0.0f32; b * hs];
9267 for bi in 0..b {
9268 let r = inference::rms_norm(
9269 &h[bi * hs..(bi + 1) * hs],
9270 &lw.input_norm,
9271 eps,
9272 norm_style,
9273 );
9274 normed[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9275 }
9276 let attn = crate::linear_core::gdn_forward_batch(
9277 &normed,
9278 b,
9279 w,
9280 &cfg,
9281 &mut self.kv_cache.layers[li].linear_state,
9282 pool.as_deref(),
9283 );
9284 for (dst, &a) in h.iter_mut().zip(&attn) {
9285 *dst += a;
9286 }
9287 }
9288 AttnKind::ShortConv(w) => {
9289 let cfg = self
9292 .short_conv_cfg
9293 .expect("short-conv layer without short_conv_cfg");
9294 let mut normed = vec![0.0f32; b * hs];
9295 for bi in 0..b {
9296 inference::rms_norm_into(
9297 &h[bi * hs..(bi + 1) * hs],
9298 &lw.input_norm,
9299 eps,
9300 norm_style,
9301 &mut normed[bi * hs..(bi + 1) * hs],
9302 );
9303 }
9304 let attn = short_conv_forward_batch(
9305 &normed,
9306 b,
9307 w,
9308 &cfg,
9309 &mut self.kv_cache.layers[li].linear_state,
9310 pool.as_deref(),
9311 );
9312 for (dst, &a) in h.iter_mut().zip(&attn) {
9313 *dst += a;
9314 }
9315 }
9316 AttnKind::Mla(w) => {
9317 let inv_freq_l = self.layer_inv_freq(li);
9320 let rs = self.layer_rope_scale(li);
9321 let mut normed = vec![0.0f32; hs];
9322 for bi in 0..b {
9323 inference::rms_norm_into(
9324 &h[bi * hs..(bi + 1) * hs],
9325 &lw.input_norm,
9326 eps,
9327 norm_style,
9328 &mut normed,
9329 );
9330 let ao = mla_attention(
9331 w,
9332 &normed,
9333 &mut self.kv_cache.layers[li],
9334 start_pos + bi,
9335 &inv_freq_l,
9336 rs,
9337 eps,
9338 pool.as_deref(),
9339 );
9340 for (dst, &a) in h[bi * hs..(bi + 1) * hs].iter_mut().zip(&ao) {
9341 *dst += a;
9342 }
9343 }
9344 }
9345 AttnKind::Full {
9346 wq,
9347 wk,
9348 wv,
9349 wo,
9350 q_norm,
9351 k_norm,
9352 output_gate,
9353 softplus_gate,
9354 bias,
9355 } => {
9356 let mut normed = vec![0.0f32; b * hs];
9360 for bi in 0..b {
9361 inference::rms_norm_into(
9362 &h[bi * hs..(bi + 1) * hs],
9363 &lw.input_norm,
9364 eps,
9365 norm_style,
9366 &mut normed[bi * hs..(bi + 1) * hs],
9367 );
9368 }
9369 let inv_freq_l = self.layer_inv_freq(li);
9370 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
9371 let cfg = QwenAttnCfg {
9372 num_heads: self.layer_num_heads(li),
9373 num_kv_heads: nkv_l,
9374 head_dim: hd_l,
9375 hidden_size: hs,
9376 position: start_pos,
9377 inv_freq: &inv_freq_l,
9378 rotary_dim: rd_l,
9379 scale: self.attn_scale,
9380 softcap: self.attn_softcap,
9381 window: self.layer_window(li),
9382 v_norm: self.attn_v_norm,
9383 qk_norm_after_rope: self.qk_norm_after_rope,
9384 gate_sigmoid: self.proj_gate_sigmoid,
9385 q_norm: q_norm.as_deref(),
9386 k_norm: k_norm.as_deref(),
9387 output_gate: *output_gate,
9388 softplus_gate: softplus_gate
9389 .as_ref()
9390 .map(|(gate, per_head)| (gate, *per_head)),
9391 rope_scale: self.layer_rope_scale(li),
9392 bias: bias
9393 .as_ref()
9394 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
9395 rms_eps: eps,
9396 norm_style,
9397 pool: pool.as_deref(),
9398 v_head_dim: self.layer_v_dim(li),
9399 };
9400 #[cfg(feature = "gpu")]
9401 let _mirror = self
9402 .prefill_mirror_target(li)
9403 .map(crate::gpu::enter_prefill_mirror);
9404 let mut attn = attention::qwen_attention_batch(
9405 &normed,
9406 b,
9407 wq,
9408 wk,
9409 wv,
9410 wo,
9411 &mut self.kv_cache.layers[li],
9412 &cfg,
9413 );
9414 if let Some(w) = &lw.attn_out_norm {
9415 for bi in 0..b {
9416 inference::rms_norm_into(
9417 &attn[bi * hs..(bi + 1) * hs],
9418 w,
9419 eps,
9420 norm_style,
9421 &mut normed[bi * hs..(bi + 1) * hs],
9422 );
9423 }
9424 attn.copy_from_slice(&normed);
9425 }
9426 for (dst, &a) in h.iter_mut().zip(&attn) {
9427 *dst += a;
9428 }
9429 }
9430 AttnKind::Bounded(w) => {
9431 let mut normed = vec![0.0f32; b * hs];
9434 for bi in 0..b {
9435 inference::rms_norm_into(
9436 &h[bi * hs..(bi + 1) * hs],
9437 &lw.input_norm,
9438 eps,
9439 norm_style,
9440 &mut normed[bi * hs..(bi + 1) * hs],
9441 );
9442 }
9443 let rope = self
9444 .bounded_rope
9445 .clone()
9446 .expect("bounded layer without an installed rotation table");
9447 let cfg = crate::bounded::BoundedAttnCfg {
9448 num_heads: self.num_heads,
9449 num_kv_heads: self.num_kv_heads,
9450 head_dim: self.head_dim,
9451 hidden_size: hs,
9452 scale: self.attn_scale,
9453 rope: &rope,
9454 pool: pool.as_deref(),
9455 };
9456 let mut attn = crate::bounded::bounded_attention_batch(
9457 &normed,
9458 b,
9459 w,
9460 &mut self.kv_cache.layers[li],
9461 &cfg,
9462 );
9463 if let Some(wn) = &lw.attn_out_norm {
9464 for bi in 0..b {
9465 inference::rms_norm_into(
9466 &attn[bi * hs..(bi + 1) * hs],
9467 wn,
9468 eps,
9469 norm_style,
9470 &mut normed[bi * hs..(bi + 1) * hs],
9471 );
9472 }
9473 attn.copy_from_slice(&normed);
9474 }
9475 for (dst, &a) in h.iter_mut().zip(&attn) {
9476 *dst += a;
9477 }
9478 attention::recycle_buf(&mut attn);
9479 }
9480 AttnKind::Linear(w) => {
9481 for bi in 0..b {
9482 let normed = inference::rms_norm(
9483 &h[bi * hs..(bi + 1) * hs],
9484 &lw.input_norm,
9485 eps,
9486 norm_style,
9487 );
9488 vmf_phase_forward(
9489 &normed,
9490 w,
9491 &self.vmf_cfg.expect("linear layer without vmf_cfg"),
9492 &mut self.kv_cache.layers[li].linear_state,
9493 pool.as_deref(),
9494 )
9495 .iter()
9496 .enumerate()
9497 .for_each(|(i, &a)| h[bi * hs + i] += a);
9498 }
9499 }
9500 }
9501
9502 let lw = &self.weights.layers[self.phys_layer(li)];
9504 let mut post = vec![0.0f32; b * hs];
9505 for bi in 0..b {
9506 let r =
9507 inference::rms_norm(&h[bi * hs..(bi + 1) * hs], &lw.post_norm, eps, norm_style);
9508 post[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9509 }
9510 let mask_row = task_mask
9513 .filter(|m| m.ffn_active_count(li) < self.intermediate_size)
9514 .and_then(|m| m.ffn_masks.get(li))
9515 .map(|v| v.as_slice());
9516 let attn_ns = t_attn.elapsed().as_nanos() as u64;
9517 let t_ffn = std::time::Instant::now();
9518 let mut ffn = match &lw.ffn {
9519 FfnKind::Dense(d) if !d.segs.is_empty() => {
9520 tube_ffn(d, &post, b, pool.as_deref(), mask_row)
9521 }
9522 FfnKind::Dense(d) => dense_ffn_batch(d, &post, b, pool.as_deref(), mask_row),
9523 FfnKind::Moe(m) if self.verify_exact_moe && self.mimo_moe.is_dynamic(li, false) => {
9524 moe_ffn_banked_rows(&mut self.mimo_moe, li, m, &post, b, hs, pool.as_deref())
9525 }
9526 FfnKind::Moe(m) if self.verify_exact_moe => {
9527 moe_ffn_rows_exact(m, &post, b, hs, pool.as_deref())
9528 }
9529 FfnKind::Moe(m) if self.mimo_moe.is_dynamic(li, false) => {
9532 let before = m.stats.borrow().clone();
9533 let out = crate::gpu::cpu_scope(|| {
9534 moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None)
9535 });
9536 self.mimo_moe.prime(li, m, &before);
9537 out
9538 }
9539 FfnKind::Moe(m) => moe_ffn_batch(m, &post, b, hs, pool.as_deref(), None),
9540 FfnKind::DenseMoe(dm) => {
9543 let mut out = vec![0.0f32; b * hs];
9544 for bi in 0..b {
9545 let r = dense_moe_ffn(
9546 dm,
9547 &post[bi * hs..(bi + 1) * hs],
9548 &h[bi * hs..(bi + 1) * hs],
9549 eps,
9550 norm_style,
9551 pool.as_deref(),
9552 );
9553 out[bi * hs..(bi + 1) * hs].copy_from_slice(&r);
9554 }
9555 out
9556 }
9557 };
9558 if prefill_prof_on() {
9559 PREFILL_SPLIT[0].fetch_add(attn_ns, std::sync::atomic::Ordering::Relaxed);
9560 PREFILL_SPLIT[1].fetch_add(t_ffn.elapsed().as_nanos() as u64, std::sync::atomic::Ordering::Relaxed);
9561 }
9562 if let Some(w) = &lw.ffn_out_norm {
9563 for bi in 0..b {
9564 inference::rms_norm_into(
9565 &ffn[bi * hs..(bi + 1) * hs],
9566 w,
9567 eps,
9568 norm_style,
9569 &mut post[bi * hs..(bi + 1) * hs],
9570 );
9571 }
9572 ffn.copy_from_slice(&post);
9573 }
9574 for (dst, &f) in h.iter_mut().zip(&ffn) {
9575 *dst += f;
9576 }
9577 if let Some(sc) = lw.layer_scale {
9578 for v in h.iter_mut() {
9579 *v *= sc;
9580 }
9581 }
9582 if self.layer_dump.is_some() {
9584 for bi in 0..b {
9585 self.dump_layer_row(start_pos + bi, li, &h[bi * hs..(bi + 1) * hs]);
9586 }
9587 }
9588 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
9589 if let Ok(t) = tp.parse::<usize>() {
9590 if t >= start_pos && t < start_pos + b {
9591 let bi = t - start_pos;
9592 let row = &h[bi * hs..(bi + 1) * hs];
9593 let n: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
9594 eprintln!(
9595 "BATCH pos {t} after layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
9596 row[0], row[1]
9597 );
9598 }
9599 }
9600 }
9601 if std::env::var("CMF_DEBUG_LAYERS").is_ok() {
9605 let row = &h[(b - 1) * hs..b * hs];
9606 let rms =
9607 (row.iter().map(|&v| (v as f64) * (v as f64)).sum::<f64>() / hs as f64).sqrt();
9608 let mx = row.iter().fold(0f32, |m, &v| m.max(v.abs()));
9609 eprintln!(
9610 "layer {li:>3} {:>10} ffn={:<5} rms={rms:>12.4} max={mx:>12.4}",
9611 match &self.weights.layers[self.phys_layer(li)].attn {
9612 AttnKind::LinearGdn(_) => "gdn",
9613 AttnKind::Linear(_) => "vmf",
9614 AttnKind::ShortConv(_) => "conv",
9615 _ => "attn",
9616 },
9617 match &lw.ffn {
9618 FfnKind::Moe(_) => "moe",
9619 FfnKind::Dense(_) => "dense",
9620 FfnKind::DenseMoe(_) => "dense+moe",
9621 },
9622 );
9623 }
9624 if self.is_loop_end(li) && li + 1 < self.num_layers {
9626 for bi in 0..b {
9627 let normed = inference::rms_norm(
9628 &h[bi * hs..(bi + 1) * hs],
9629 &self.weights.final_norm,
9630 eps,
9631 norm_style,
9632 );
9633 h[bi * hs..(bi + 1) * hs].copy_from_slice(&normed);
9634 }
9635 }
9636 if std::env::var("CMF_TRACE_H").is_ok() {
9637 let n = h[..hs].iter().map(|v| v.abs()).sum::<f32>() / hs as f32;
9638 let mx = h[..hs].iter().fold(0.0f32, |a, &v| a.max(v.abs()));
9639 eprintln!(
9640 "layer {li}: mean|h|={n:.4} max|h|={mx:.2} scale={:?}",
9641 lw.layer_scale
9642 );
9643 }
9644 }
9645 crate::gpu::set_layer(-1); if prefill_prof_on() {
9647 let ms = |a: &std::sync::atomic::AtomicU64| {
9648 a.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6
9649 };
9650 let sp = &attention::ATTN_SPLIT;
9651 let (qkv_enq, qkv_wait, ca_wait, zi) = wgpu_prefill_counters();
9652 eprintln!(
9653 "prefill-split: attention {:.1} ms (proj {:.1} [qkv enqueue {:.1} wait {:.1}, \
9654 host gate {:.1}], host loop {:.1}, attend {:.1} \
9655 [q pack {:.1}, device {:.1} of which readback {:.1}], o-proj {:.1}), \
9656 ffn {:.1} ms (cumulative; zi_mm GEMMs {zi})",
9657 ms(&PREFILL_SPLIT[0]),
9658 ms(&sp[0]),
9659 qkv_enq,
9660 qkv_wait,
9661 ms(&sp[6]),
9662 ms(&sp[1]),
9663 ms(&sp[2]),
9664 ms(&sp[4]),
9665 ms(&sp[5]),
9666 ca_wait,
9667 ms(&sp[3]),
9668 ms(&PREFILL_SPLIT[1]),
9669 );
9670 let [calls, attended, wo, ring, reseed, f16] = wgpu_mirror_counters();
9671 eprintln!(
9672 "prefill-mirror: {calls} calls, {attended} attended ({wo} with O on the card), \
9673 {ring} ring refusals, {reseed} reseeds, {f16} f16 fallbacks, {} upload \
9674 fallbacks (cumulative)",
9675 attention::MIRROR_UPLOADS.load(std::sync::atomic::Ordering::Relaxed),
9676 );
9677 }
9678 self.o1_progress();
9683 self.swa_trim_tails();
9685 h
9686 }
9687
9688 fn embed_single(&self, id: u32) -> Vec<f32> {
9690 let mut out = vec![0.0f32; self.hidden_size];
9691 if (id as usize) < self.weights.embed_tokens.rows() {
9692 self.weights.embed_tokens.row_f32(id as usize, &mut out);
9693 }
9694 if self.embed_multiplier != 1.0 {
9695 for v in out.iter_mut() {
9696 *v *= self.embed_multiplier;
9697 }
9698 }
9699 if self.dsv4.is_some()
9703 || self.dsv41.is_some()
9704 || self.qwen4_exp.is_some()
9705 {
9706 let mut v = vec![0.0f32; self.hidden_size.max(1)];
9707 v[0] = id as f32;
9708 return v;
9709 }
9710 if let Some(b) = &self.g3n {
9713 return b.0.extend_embedding(id, &out, self.pool.as_deref());
9714 }
9715 out
9716 }
9717
9718 #[cfg(target_os = "macos")]
9724 fn chunk_run_gpu(
9725 &mut self,
9726 li0: usize,
9727 h: &mut [f32],
9728 b: usize,
9729 pos0: usize,
9730 embed_ids: Option<&[u32]>,
9731 cap: usize,
9732 ) -> usize {
9733 if !crate::gpu::enabled_here()
9737 || std::env::var("CMF_GPU_CHUNK")
9738 .map(|v| v == "0")
9739 .unwrap_or(false)
9740 || b < 32
9741 || (self.swa.is_some() && !self.metal_graph_swa())
9744 || self.global_attn.is_some()
9745 || (self.graph_attn_decline_reason().is_some() && !self.metal_graph_swa())
9747 || self.o1_active()
9750 || self.attn_v_norm
9751 || (self.attn_scale - 1.0 / (self.head_dim as f32).sqrt()).abs() > 1e-9
9752 {
9753 return li0;
9754 }
9755 let Some(model) = self.model.clone() else {
9756 return li0;
9757 };
9758 let (nh, nkv, hd, hs) = (
9759 self.num_heads,
9760 self.num_kv_heads,
9761 self.head_dim,
9762 self.hidden_size,
9763 );
9764 let loop_end = if self.loop_final_norm {
9768 ((li0 / self.physical_layers) + 1) * self.physical_layers
9769 } else {
9770 self.num_layers
9771 };
9772 let mut layers: Vec<crate::gpu_metal::ChunkLayer> = Vec::new();
9773 let mut stored_at: Vec<usize> = Vec::new();
9774 let run_end = self.num_layers.min(loop_end).min(cap);
9775 let tables: Vec<std::sync::Arc<Vec<f32>>> =
9778 (li0..run_end).map(|li| self.layer_inv_freq(li)).collect();
9779 for li in li0..run_end {
9780 let lw = &self.weights.layers[self.phys_layer(li)];
9781 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
9782 break;
9783 }
9784 let AttnKind::Full {
9785 wq,
9786 wk,
9787 wv,
9788 wo,
9789 q_norm,
9790 k_norm,
9791 output_gate: false,
9792 softplus_gate,
9793 bias,
9794 } = &lw.attn
9795 else {
9796 break;
9797 };
9798 let head_gate = match softplus_gate {
9800 None => None,
9801 Some((g, true)) if self.proj_gate_sigmoid => match g.f32_parts() {
9802 Some((d, r, c)) if r == nh && c == hs => Some(d),
9803 _ => break,
9804 },
9805 Some(_) => break,
9806 };
9807 let FfnKind::Dense(d) = &lw.ffn else { break };
9808 if !matches!(d.act, Act::Silu | Act::Gelu) || !d.segs.is_empty() {
9809 break;
9810 }
9811 fn cw(t: &QTensor) -> Option<(usize, usize, usize, &[f32])> {
9816 t.q8_row_parts()
9817 .or_else(|| t.q4t_parts().map(|(i, r, c)| (i, r, c, &[][..])))
9818 .or_else(|| t.q4tp_parts().map(|(i, r, c)| (i, r, c, &[][..])))
9819 }
9820 let parts = (
9821 cw(wq),
9822 cw(wk),
9823 cw(wv),
9824 cw(wo),
9825 cw(&d.gate_proj),
9826 cw(&d.up_proj),
9827 cw(&d.down_proj),
9828 );
9829 let (Some(pq), Some(pk), Some(pv), Some(po), Some(pg), Some(pu), Some(pd)) = parts
9830 else {
9831 break;
9832 };
9833 let layer = &self.kv_cache.layers[li];
9834 if layer.mode != crate::kv_cache::KvMode::F32 || layer.o1.is_some() {
9835 break;
9836 }
9837 debug_assert!(
9839 layer.base() == 0
9840 || self
9841 .layer_window(li)
9842 .is_some_and(|w| layer.head_len(0) + 1 >= w)
9843 );
9844 stored_at.push(layer.head_len(0));
9845 layers.push(crate::gpu_metal::ChunkLayer {
9846 model: &model,
9847 kv_id: self.graph_kv_id,
9848 layer: li,
9849 wq: pq,
9850 wk: pk,
9851 wv: pv,
9852 wo: po,
9853 gate: pg,
9854 up: pu,
9855 down: pd,
9856 input_norm: &lw.input_norm,
9857 post_norm: &lw.post_norm,
9858 bias: bias
9859 .as_ref()
9860 .map(|(a, bb, cc)| (a.as_slice(), bb.as_slice(), cc.as_slice())),
9861 q_norm: q_norm.as_deref(),
9862 k_norm: k_norm.as_deref(),
9863 inv_freq: &tables[li - li0],
9864 rd: self.layer_geom(li).2,
9865 nh,
9866 nkv,
9867 hd,
9868 hs,
9869 inter: d.gate_proj.rows(),
9870 gemma: matches!(self.norm_style, cortiq_core::NormStyle::Gemma),
9871 late_qk_norm: self.qk_norm_after_rope,
9872 eps: self.rms_eps as f32,
9873 window: self.layer_window(li),
9874 head_gate,
9875 gelu: d.act == Act::Gelu,
9876 });
9877 }
9878 if layers.is_empty() {
9879 return li0;
9880 }
9881 let row = nkv * hd;
9882 let mut store: Vec<(Vec<f32>, Vec<f32>, Vec<f32>)> = stored_at
9883 .iter()
9884 .map(|&st| (vec![0f32; b * row], vec![0f32; b * row], vec![0f32; st + b]))
9885 .collect();
9886 let mut io: Vec<crate::gpu_metal::ChunkIo> = Vec::with_capacity(layers.len());
9887 for (i, (ok, ov, oi)) in store.iter_mut().enumerate() {
9888 let li = layers[i].layer;
9889 let layer = &self.kv_cache.layers[li];
9890 io.push(crate::gpu_metal::ChunkIo {
9891 cpu_stored: stored_at[i],
9892 cpu_gen: layer.generation(),
9893 cpu_k: (0..nkv).map(|g| layer.head_keys(g)).collect(),
9894 cpu_v: (0..nkv).map(|g| layer.head_values(g)).collect(),
9895 out_k: ok,
9896 out_v: ov,
9897 imp: oi,
9898 });
9899 }
9900 let n_run = layers.len();
9901 let last = layers.last().map(|l| l.layer + 1).unwrap_or(li0);
9902 let ep = embed_ids.and_then(|ids| {
9905 self.weights
9906 .embed_tokens
9907 .q8_row_parts()
9908 .map(|(idx, rows, _c, rs)| crate::gpu_metal::ChunkEmbed {
9909 idx,
9910 rows,
9911 row_scale: rs,
9912 ids,
9913 mult: self.embed_multiplier,
9914 })
9915 });
9916 if embed_ids.is_some() && ep.is_none() {
9917 return li0;
9918 }
9919 if !crate::gpu_metal::chunk_run_gpu(&layers, &mut io, h, b, pos0, ep.as_ref()) {
9920 return li0;
9921 }
9922 drop(io);
9923 drop(layers);
9924 for (i, (ok, ov, oi)) in store.iter().enumerate().take(n_run) {
9927 let li = li0 + i;
9928 let layer = &mut self.kv_cache.layers[li];
9929 for bi in 0..b {
9930 layer.append(
9931 &ok[bi * row..(bi + 1) * row],
9932 &ov[bi * row..(bi + 1) * row],
9933 &[],
9934 );
9935 }
9936 layer.accumulate_imp(oi);
9937 }
9938 last
9939 }
9940
9941 fn layer_is_local(&self, li: usize) -> bool {
9944 if let Some(layers) = &self.sliding_layers {
9945 return layers.get(li).copied().unwrap_or(false);
9946 }
9947 match self.swa {
9948 Some((_, pattern)) => (li + 1) % pattern.max(1) != 0,
9949 None => false,
9950 }
9951 }
9952
9953 fn layer_inv_freq(&self, li: usize) -> std::sync::Arc<Vec<f32>> {
9956 if self.layer_is_local(li) {
9957 if let Some(f) = &self.inv_freq_local {
9958 return f.clone();
9959 }
9960 } else if let Some(f) = &self.inv_freq_global {
9961 return f.clone();
9962 }
9963 self.inv_freq.clone()
9964 }
9965
9966 fn layer_window(&self, li: usize) -> Option<usize> {
9968 self.swa
9969 .and_then(|(w, _)| self.layer_is_local(li).then_some(w))
9970 }
9971
9972 fn swa_trim_tails(&mut self) {
9981 let Some((slack, align)) = self.swa_trim else {
9982 return;
9983 };
9984 if self.mtp.is_some() || self.mimo_mtp.is_some() || !self.dsv4_mtp.is_empty() {
9985 return;
9986 }
9987 for li in 0..self.num_layers.min(self.kv_cache.layers.len()) {
9988 if let Some(w) = self.layer_window(li) {
9989 self.kv_cache.layers[li].trim_window(w, slack, align);
9990 }
9991 }
9992 }
9993
9994 fn layer_num_heads(&self, li: usize) -> usize {
9995 self.attention_heads_per_layer
9996 .as_ref()
9997 .and_then(|v| v.get(li).copied())
9998 .unwrap_or(self.num_heads)
9999 }
10000
10001 fn layer_rope_scale(&self, li: usize) -> f32 {
10002 if self.layer_is_local(li) {
10003 self.rope_scale_local
10004 } else {
10005 self.rope_scale
10006 }
10007 }
10008
10009 fn layer_geom(&self, li: usize) -> (usize, usize, usize) {
10012 if !self.layer_is_local(li) {
10013 if let Some((ghd, gkv)) = self.global_attn {
10014 return (gkv, ghd, ghd);
10015 }
10016 }
10017 (
10018 self.layer_num_kv_heads(li),
10019 self.head_dim,
10020 if self.layer_is_local(li) {
10021 self.rotary_dim_local.unwrap_or(self.rotary_dim)
10022 } else {
10023 self.rotary_dim
10024 },
10025 )
10026 }
10027
10028 fn layer_num_kv_heads(&self, li: usize) -> usize {
10031 self.kv_heads_per_layer
10032 .as_ref()
10033 .and_then(|v| v.get(self.phys_layer(li)).copied())
10034 .unwrap_or(self.num_kv_heads)
10035 }
10036
10037 fn layer_v_dim(&self, li: usize) -> usize {
10039 let (_, hd, _) = self.layer_geom(li);
10040 self.v_head_dim.unwrap_or(hd).min(hd)
10041 }
10042
10043 pub fn set_attn_geometry(
10052 &mut self,
10053 kv_heads_per_layer: Option<Vec<usize>>,
10054 v_head_dim: Option<usize>,
10055 ) -> Result<(), String> {
10056 if kv_heads_per_layer.is_some() || v_head_dim.is_some() {
10057 if self.global_attn.is_some() {
10058 return Err(
10059 "per-layer KV heads / v_head_dim cannot combine with Gemma-4 global \
10060 attention geometry"
10061 .into(),
10062 );
10063 }
10064 if self
10065 .weights
10066 .layers
10067 .iter()
10068 .any(|lw| matches!(lw.attn, AttnKind::Mla(_)))
10069 {
10070 return Err("per-layer KV heads / v_head_dim cannot combine with MLA".into());
10071 }
10072 }
10073 if let Some(vd) = v_head_dim {
10074 if vd == 0 || vd > self.head_dim {
10075 return Err(format!(
10076 "v_head_dim {vd} must be in 1..={} (head_dim)",
10077 self.head_dim
10078 ));
10079 }
10080 }
10081 if let Some(v) = &kv_heads_per_layer {
10082 if v.len() != self.physical_layers {
10083 return Err(format!(
10084 "kv_heads_per_layer has {} entries, expected {} layers",
10085 v.len(),
10086 self.physical_layers
10087 ));
10088 }
10089 for (li, &nkv) in v.iter().enumerate() {
10090 let is_attn = matches!(
10091 self.weights.layers.get(li).map(|lw| &lw.attn),
10092 Some(AttnKind::Full { .. }) | None
10093 );
10094 if !is_attn {
10095 continue;
10096 }
10097 let nh = self
10098 .attention_heads_per_layer
10099 .as_ref()
10100 .and_then(|h| h.get(li).copied())
10101 .unwrap_or(self.num_heads);
10102 if nkv == 0 || nh % nkv != 0 {
10103 return Err(format!(
10104 "layer {li}: {nkv} KV heads must be nonzero and divide {nh} Q heads"
10105 ));
10106 }
10107 }
10108 }
10109 self.kv_heads_per_layer = kv_heads_per_layer;
10110 self.v_head_dim = v_head_dim.filter(|&vd| vd != self.head_dim);
10111 if self.kv_heads_per_layer.is_some() {
10112 for li in 0..self.kv_cache.layers.len() {
10113 let full = matches!(
10114 self.weights
10115 .layers
10116 .get(self.phys_layer(li))
10117 .map(|lw| &lw.attn),
10118 Some(AttnKind::Full { .. })
10119 );
10120 let nkv = self.layer_num_kv_heads(li);
10121 let cache = &self.kv_cache.layers[li];
10122 if full && (cache.num_kv_heads != nkv || cache.head_dim != self.head_dim) {
10123 let sinks = cache.sinks.clone();
10124 self.kv_cache.layers[li] =
10125 crate::kv_cache::LayerKvCache::new(nkv, self.head_dim);
10126 self.kv_cache.layers[li].sinks = sinks;
10127 }
10128 }
10129 }
10130 Ok(())
10131 }
10132
10133 pub fn set_layer_sinks(&mut self, phys: usize, sinks: Vec<f32>) -> Result<(), String> {
10137 let Some(lw) = self.weights.layers.get(phys) else {
10138 return Err(format!("sinks for layer {phys}: no such layer"));
10139 };
10140 if !matches!(lw.attn, AttnKind::Full { .. }) {
10141 return Err(format!(
10142 "sinks for layer {phys}: only softmax (Full) attention layers take sinks"
10143 ));
10144 }
10145 let nh = self
10146 .attention_heads_per_layer
10147 .as_ref()
10148 .and_then(|h| h.get(phys).copied())
10149 .unwrap_or(self.num_heads);
10150 if sinks.len() != nh {
10151 return Err(format!(
10152 "sinks for layer {phys}: {} values, expected one per Q head ({nh})",
10153 sinks.len()
10154 ));
10155 }
10156 if let Some(bad) = sinks.iter().find(|v| !v.is_finite()) {
10157 return Err(format!("sinks for layer {phys}: non-finite value {bad}"));
10158 }
10159 for li in 0..self.kv_cache.layers.len() {
10160 if self.phys_layer(li) == phys {
10161 self.kv_cache.layers[li].sinks = Some(sinks.clone());
10162 }
10163 }
10164 Ok(())
10165 }
10166
10167 pub fn graph_attn_decline_reason(&self) -> Option<&'static str> {
10177 if self.kv_heads_per_layer.is_some() {
10178 return Some("per-layer KV head counts");
10179 }
10180 if self.v_head_dim.is_some_and(|vd| vd != self.head_dim) {
10181 return Some("V heads narrower than Q/K heads");
10182 }
10183 if self.kv_cache.layers.iter().any(|l| l.sinks.is_some()) {
10184 return Some("learned attention sinks");
10185 }
10186 if self.swa.is_some() || self.sliding_layers.is_some() {
10187 return Some("sliding-window layers");
10188 }
10189 None
10190 }
10191
10192 #[cfg(target_os = "macos")]
10206 fn metal_graph_swa(&self) -> bool {
10207 (self.swa.is_some() || self.sliding_layers.is_some())
10208 && self.attention_heads_per_layer.is_none()
10209 && !self.attn_v_norm
10210 && self.attn_softcap == 0.0
10211 && self.kv_heads_per_layer.is_none()
10212 && self.v_head_dim.map_or(true, |vd| vd == self.head_dim)
10213 && !self.kv_cache.layers.iter().any(|l| l.sinks.is_some())
10214 && self.global_attn.is_none()
10215 && self.inv_freq_global.is_none()
10216 && (0..self.num_layers).all(|li| self.layer_rope_scale(li) == 1.0)
10217 && !(0..self.num_layers).any(|li| {
10218 self.layer_is_local(li)
10219 && self.inv_freq_local.is_none()
10220 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10221 })
10222 }
10223
10224 pub fn wgpu_graph_attn_decline(&self) -> Option<&'static str> {
10231 self.graph_attn_decline_reason()?;
10232 if self.global_attn.is_some() {
10233 return Some("per-layer head width (Gemma-4 global layers) with per-layer geometry");
10234 }
10235 if self.attention_heads_per_layer.is_some() {
10236 return Some("per-layer Q head counts with per-layer geometry");
10237 }
10238 if self.attn_v_norm {
10239 return Some("V norm with per-layer geometry");
10240 }
10241 if (0..self.num_layers).any(|li| self.layer_rope_scale(li) != 1.0) {
10242 return Some("scaled RoPE positions with per-layer geometry");
10243 }
10244 if self.weights.layers.iter().any(|lw| {
10245 lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some()
10246 }) {
10247 return Some("sandwich norms / layer scale with per-layer geometry");
10248 }
10249 if self.weights.layers.iter().any(|lw| {
10250 matches!(
10251 &lw.attn,
10252 AttnKind::Full {
10253 output_gate: true,
10254 ..
10255 }
10256 )
10257 }) && self.v_head_dim.is_some()
10258 {
10259 return Some("gated attention with V narrower than K");
10260 }
10261 if (0..self.num_layers).any(|li| {
10262 self.layer_is_local(li)
10263 && self.inv_freq_local.is_none()
10264 && self.rotary_dim_local.is_some_and(|r| r != self.rotary_dim)
10265 }) {
10266 return Some("local rotary width without a local RoPE table");
10267 }
10268 None
10269 }
10270
10271 fn graph_attn_geom(&self, li: usize) -> Option<crate::gpu::GraphAttnGeom<'_>> {
10276 self.graph_attn_decline_reason()?;
10277 let (nkv, _hd, rd) = self.layer_geom(li);
10278 let invf: &[f32] = if self.layer_is_local(li) {
10279 match &self.inv_freq_local {
10280 Some(f) => f.as_slice(),
10281 None => self.inv_freq.as_slice(),
10282 }
10283 } else {
10284 match &self.inv_freq_global {
10285 Some(f) => f.as_slice(),
10286 None => self.inv_freq.as_slice(),
10287 }
10288 };
10289 Some(crate::gpu::GraphAttnGeom {
10290 nkv,
10291 dv: self.layer_v_dim(li),
10292 rd,
10293 invf,
10294 window: self.layer_window(li),
10295 sink: self.kv_cache.layers[li].sinks.as_deref(),
10296 })
10297 }
10298
10299 #[cfg(feature = "gpu")]
10311 fn prefill_mirror_target(&self, li: usize) -> Option<crate::gpu::PrefillMirror> {
10312 if !self.proj_gate_sigmoid
10313 || std::env::var("CMF_PREFILL_MIRROR").as_deref() == Ok("0")
10314 || !crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
10315 || self.graph_refused()
10316 || self.o1_active()
10317 || self.attn_softcap > 0.0
10318 || self.wgpu_graph_attn_decline().is_some()
10319 {
10320 return None;
10321 }
10322 let g = self.graph_attn_geom(li)?;
10323 let (nkv, hd, _) = self.layer_geom(li);
10324 if g.nkv != nkv || g.dv != hd || g.sink.is_some() || hd != self.layer_geom(0).1 {
10326 return None;
10327 }
10328 Some(crate::gpu::PrefillMirror {
10329 kv_id: self.graph_kv_id,
10330 layer: li,
10331 limit: self.kv_cache.max_seq_len,
10332 })
10333 }
10334
10335 #[cfg(feature = "gpu")]
10344 fn pull_lagging_host_kv(&mut self, from: usize, upto: usize, position: usize) {
10345 let kv_id = self.graph_kv_id;
10346 for li in from..upto.min(self.num_layers) {
10347 if !matches!(
10348 self.weights.layers[self.phys_layer(li)].attn,
10349 AttnKind::Full { .. }
10350 ) {
10351 continue;
10352 }
10353 let host = self.kv_cache.layers[li].pos_len();
10355 if host >= position {
10356 continue;
10357 }
10358 let Some(dev) = crate::gpu::graph_kv_stored(kv_id, li) else {
10359 continue;
10360 };
10361 let to = dev.min(position);
10362 if to <= host {
10363 continue;
10364 }
10365 let (nkv, hd) = {
10366 let c = &self.kv_cache.layers[li];
10367 (c.num_kv_heads, c.head_dim)
10368 };
10369 let Some((k, v, first_valid)) =
10370 crate::gpu::graph_kv_pull_host(kv_id, li, host, to, nkv, hd)
10371 else {
10372 continue;
10373 };
10374 let need_from = match self.layer_window(li) {
10377 Some(w) => host.max((position + 1).saturating_sub(w)),
10378 None => host,
10379 };
10380 if first_valid > need_from {
10381 tracing::warn!(
10382 "layer {li}: device KV rows {host}..{to} no longer resident \
10383 (from {first_valid}); host attention will miss them"
10384 );
10385 }
10386 let row = nkv * hd;
10387 let cache = &mut self.kv_cache.layers[li];
10388 for p in 0..to - host {
10389 cache.append(&k[p * row..(p + 1) * row], &v[p * row..(p + 1) * row], &[]);
10390 }
10391 }
10392 }
10393
10394 fn note_graph_decline(&self, site: &'static str, reason: &'static str) {
10397 let mut seen = self.graph_declines.borrow_mut();
10398 if !seen.iter().any(|&(s, r)| s == site && r == reason) {
10399 tracing::warn!("{site} declined: {reason} (CPU attention path)");
10400 seen.push((site, reason));
10401 }
10402 }
10403
10404 pub fn graph_declines(&self) -> Vec<String> {
10407 self.graph_declines
10408 .borrow()
10409 .iter()
10410 .map(|(site, reason)| format!("{site} declined: {reason} (CPU attention path)"))
10411 .collect()
10412 }
10413
10414 fn dump_layer_row(&self, pos: usize, li: usize, row: &[f32]) {
10423 let Some(dir) = &self.layer_dump else {
10424 return;
10425 };
10426 let mut bytes = Vec::with_capacity(row.len() * 4);
10427 for v in row {
10428 bytes.extend_from_slice(&v.to_le_bytes());
10429 }
10430 let path = dir.join(format!("p{pos:06}_l{li:02}.f32"));
10431 if let Err(e) = std::fs::create_dir_all(dir).and_then(|_| std::fs::write(&path, &bytes)) {
10432 use std::sync::atomic::{AtomicBool, Ordering};
10433 static SAID: AtomicBool = AtomicBool::new(false);
10434 if !SAID.swap(true, Ordering::Relaxed) {
10435 tracing::error!("CMF_LAYER_DUMP: cannot write {}: {e}", path.display());
10436 }
10437 }
10438 }
10439
10440 fn mimo_moe_prepare(&mut self) {
10443 if !self.mimo_moe.is_undecided() {
10444 return;
10445 }
10446 let slot = {
10447 let layers: Vec<(usize, &MoeFfn)> = (0..self.num_layers)
10448 .filter_map(
10449 |li| match &self.weights.layers.get(self.phys_layer(li))?.ffn {
10450 FfnKind::Moe(m) => Some((li, m)),
10451 _ => None,
10452 },
10453 )
10454 .collect();
10455 if layers.is_empty()
10458 || self.physical_layers != self.num_layers
10459 || self.gpu_plan.is_some()
10460 {
10461 crate::mimo_moe::Slot::Off
10462 } else {
10463 let graph_prefix =
10467 self.wgpu_graph_attn_decline().is_none() && crate::gpu::wgpu_graph_default();
10468 crate::mimo_moe::Slot::decide(&layers, self.num_layers, graph_prefix)
10469 }
10470 };
10471 self.mimo_moe = slot;
10472 }
10473
10474 #[cfg(test)]
10475 pub(crate) fn test_graph_kv_id(&self) -> u64 {
10476 self.graph_kv_id
10477 }
10478
10479 pub(crate) fn mimo_graph_layer_rows(
10483 &mut self,
10484 li: usize,
10485 h: &mut [f32],
10486 positions: &[usize],
10487 ) -> crate::gpu::BatchGraphOutcome {
10488 use crate::gpu::BatchGraphOutcome as Out;
10489 let b = positions.len();
10490 if !(1..=4).contains(&b)
10491 || h.len() != b * self.hidden_size
10492 || !self.mimo_moe.is_dynamic(li, true)
10493 || !crate::gpu::enabled_here()
10494 || !crate::gpu::wgpu_active()
10495 || self.o1_active()
10496 || self.physical_layers != self.num_layers
10497 || std::env::var("CMF_GPU_WGPU_GRAPH").as_deref() == Ok("0")
10501 || std::env::var("CMF_MIMO_ATTN_GRAPH").as_deref() == Ok("0")
10502 || self.wgpu_graph_attn_decline().is_some()
10503 {
10504 return Out::Declined;
10505 }
10506 let attn_started = std::time::Instant::now();
10507 let outcome = {
10508 let lw = &self.weights.layers[li];
10509 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
10510 return Out::Declined;
10511 }
10512 let FfnKind::Moe(m) = &lw.ffn else {
10513 return Out::Declined;
10514 };
10515 let AttnKind::Full {
10516 wq,
10517 wk,
10518 wv,
10519 wo,
10520 q_norm,
10521 k_norm,
10522 output_gate,
10523 softplus_gate,
10524 bias,
10525 } = &lw.attn
10526 else {
10527 return Out::Declined;
10528 };
10529 if *output_gate || softplus_gate.is_some() {
10530 return Out::Declined;
10531 }
10532 let Some(model) = wq.graph_weight().map(|(m, ..)| m.clone()).or_else(|| {
10533 m.experts
10534 .first()?
10535 .gate_proj
10536 .mapped_q4tp()
10537 .map(|(m, _)| m.clone())
10538 }) else {
10539 return Out::Declined;
10540 };
10541 fn gw<'a>(
10542 t: &'a QTensor,
10543 owner: &std::sync::Arc<cortiq_core::CmfModel>,
10544 ) -> Option<crate::gpu::GraphW<'a>> {
10545 if let Some((m, idx, kind, rs)) = t.graph_weight() {
10546 if m.uid() != owner.uid() || t.has_prism_contract() {
10547 return None;
10548 }
10549 return Some(crate::gpu::GraphW {
10550 idx,
10551 kind,
10552 row_scale: rs,
10553 data: &[],
10554 prism: crate::gpu::GraphPrismOp::None,
10555 affine: false,
10556 });
10557 }
10558 t.as_f32().map(|data| crate::gpu::GraphW {
10559 idx: 0,
10560 kind: 4,
10561 row_scale: &[],
10562 data,
10563 prism: crate::gpu::GraphPrismOp::None,
10564 affine: false,
10565 })
10566 }
10567 let (Some(q), Some(k), Some(v), Some(o)) = (
10568 gw(wq, &model),
10569 gw(wk, &model),
10570 gw(wv, &model),
10571 gw(wo, &model),
10572 ) else {
10573 return Out::Declined;
10574 };
10575 let layer = crate::gpu::GraphLayer {
10576 input_norm: &lw.input_norm,
10577 post_norm: &lw.post_norm,
10578 ffn: crate::gpu::GraphFfn::AttentionOnly,
10579 attn: crate::gpu::GraphAttn::Full {
10580 wq: q,
10581 wk: k,
10582 wv: v,
10583 wo: o,
10584 q_norm: q_norm.as_deref(),
10585 k_norm: k_norm.as_deref(),
10586 late_qk_norm: self.qk_norm_after_rope,
10587 bias: bias
10588 .as_ref()
10589 .map(|(q, k, v)| (q.as_slice(), k.as_slice(), v.as_slice())),
10590 output_gate: false,
10591 cpu_k: self.kv_cache.layers[li].k_heads(),
10592 cpu_v: self.kv_cache.layers[li].v_heads(),
10593 cpu_base: self.kv_cache.layers[li].base(),
10594 geom: self.graph_attn_geom(li),
10595 head_gate: None,
10596 },
10597 };
10598 let (nkv, hd, rd) = self.layer_geom(li);
10599 crate::gpu::forward_batch_graph_at(
10600 &model,
10601 self.graph_kv_id,
10602 li,
10603 &[layer],
10604 &self.inv_freq,
10605 h,
10606 self.layer_num_heads(li),
10607 nkv,
10608 hd,
10609 rd,
10610 self.hidden_size,
10611 1,
10612 positions,
10613 self.kv_cache.max_seq_len,
10614 self.norm_style == cortiq_core::NormStyle::Gemma,
10615 self.rms_eps as f32,
10616 self.attn_scale,
10617 b,
10618 &[],
10619 self.o1_epoch,
10620 None,
10621 None,
10622 )
10623 };
10624 match outcome {
10625 Out::Completed => {}
10626 Out::Declined => return Out::Declined,
10627 Out::Failed => {
10628 self.graph_failed
10629 .store(true, std::sync::atomic::Ordering::Relaxed);
10630 return Out::Failed;
10631 }
10632 }
10633 let attn_ns = attn_started.elapsed().as_nanos() as u64;
10634 let hs = self.hidden_size;
10635 let lw = &self.weights.layers[li];
10636 let FfnKind::Moe(m) = &lw.ffn else {
10637 unreachable!()
10638 };
10639 let mut post = vec![0.0; h.len()];
10640 for (x, y) in h.chunks_exact(hs).zip(post.chunks_exact_mut(hs)) {
10641 inference::rms_norm_into(x, &lw.post_norm, self.rms_eps, self.norm_style, y);
10642 }
10643 let mut ffn = if b == 1 {
10644 moe_ffn_banked(&mut self.mimo_moe, li, m, &post, self.pool.as_deref())
10645 } else {
10646 moe_ffn_banked_rows(
10647 &mut self.mimo_moe,
10648 li,
10649 m,
10650 &post,
10651 b,
10652 hs,
10653 self.pool.as_deref(),
10654 )
10655 };
10656 for (x, &f) in h.iter_mut().zip(&ffn) {
10657 *x += f;
10658 }
10659 attention::recycle_buf(&mut ffn);
10660 if self.layer_dump.is_some() {
10661 for (&pos, row) in positions.iter().zip(h.chunks_exact(hs)) {
10662 self.dump_layer_row(pos, li, row);
10663 }
10664 }
10665 crate::mimo_moe::note_attention_graph(b, attn_ns);
10666 Out::Completed
10667 }
10668
10669 fn layer_attn_plain(&self, li: usize) -> bool {
10670 self.kv_heads_per_layer.is_none()
10671 && self.v_head_dim.is_none()
10672 && self.global_attn.is_none()
10673 && self.layer_window(li).is_none()
10674 && self.kv_cache.layers[li].sinks.is_none()
10675 }
10676
10677 fn forward_layers(
10679 &mut self,
10680 hidden: &[f32],
10681 position: usize,
10682 task_mask: Option<&TaskMask>,
10683 ) -> Vec<f32> {
10684 let out = self.forward_layers_upto(hidden, position, task_mask, None);
10685 self.o1_progress();
10686 self.swa_trim_tails();
10687 out
10688 }
10689
10690 pub fn embed_id(&self, id: u32) -> Vec<f32> {
10698 self.embed_single(id)
10699 }
10700
10701 pub fn split_supported(&self) -> Result<(), String> {
10705 if self.dsv4.is_some() {
10706 return Err(
10707 "network split: DeepSeek-V4 runs its own fused stack (not splittable yet)".into(),
10708 );
10709 }
10710 if self.dsv41.is_some() {
10711 return Err(
10712 "network split: DeepSeek-V4.1 owns the shared CED/CSA2 state (not splittable)"
10713 .into(),
10714 );
10715 }
10716 if self.qwen4_exp.is_some() {
10717 return Err(
10718 "network split: Qwen3.8-Flash-Next hyper/QSA stack is not splittable yet".into(),
10719 );
10720 }
10721 if self.g3n.is_some() {
10722 return Err(
10723 "network split: Gemma-3n runs its own AltUp stack (not splittable yet)".into(),
10724 );
10725 }
10726 Ok(())
10727 }
10728
10729 pub fn forward_span(
10734 &mut self,
10735 hidden: &[f32],
10736 position: usize,
10737 from: usize,
10738 upto: usize,
10739 task_mask: Option<&TaskMask>,
10740 ) -> Result<Vec<f32>, String> {
10741 self.split_supported()?;
10742 if from > upto || upto >= self.num_layers {
10743 return Err(format!(
10744 "forward_span: layer range {from}..={upto} outside 0..{}",
10745 self.num_layers
10746 ));
10747 }
10748 if hidden.len() != self.hidden_size {
10749 return Err(format!(
10750 "forward_span: hidden len {} ≠ hidden_size {}",
10751 hidden.len(),
10752 self.hidden_size
10753 ));
10754 }
10755 let out = self.forward_layers_span(hidden, position, task_mask, from, Some(upto));
10756 self.o1_progress();
10757 self.swa_trim_tails();
10758 if self
10759 .graph_failed
10760 .swap(false, std::sync::atomic::Ordering::Relaxed)
10761 {
10762 self.cancel
10763 .store(false, std::sync::atomic::Ordering::Relaxed);
10764 self.clear_sequence_state();
10765 return Err("forward_span: deferred O(1) transition failed".into());
10766 }
10767 Ok(out)
10768 }
10769
10770 pub fn logits_from_hidden(&mut self, hidden: &[f32]) -> Vec<f32> {
10773 let normed = inference::rms_norm(
10774 hidden,
10775 &self.weights.final_norm,
10776 self.rms_eps,
10777 self.norm_style,
10778 );
10779 self.lm_head_forward(&normed)
10780 }
10781
10782 pub fn sample_next(&mut self, logits: &[f32], past_tokens: &[u32]) -> u32 {
10784 sampler::sample_with_scratch(
10785 logits,
10786 &self.sampler_config,
10787 past_tokens,
10788 &mut self.rng,
10789 &mut self.sampler_scratch,
10790 )
10791 }
10792
10793 pub fn reset_session(&mut self) {
10795 self.clear_sequence_state();
10796 }
10797
10798 pub fn prefill_span_ids(
10804 &mut self,
10805 ids: &[u32],
10806 start_pos: usize,
10807 upto: usize,
10808 task_mask: Option<&TaskMask>,
10809 ) -> Result<Vec<f32>, String> {
10810 self.split_supported()?;
10811 if upto >= self.num_layers {
10812 return Err(format!(
10813 "prefill_span_ids: upto {upto} outside 0..{}",
10814 self.num_layers
10815 ));
10816 }
10817 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
10821 let out =
10822 self.prefill_batch_span(PrefillIn::Ids(ids), start_pos, task_mask, 0, upto + 1);
10823 self.check_o1_progress_failure("prefill_span_ids")?;
10824 Ok(out)
10825 } else {
10826 let hs = self.hidden_size;
10827 let mut out = Vec::with_capacity(ids.len() * hs);
10828 for (i, &id) in ids.iter().enumerate() {
10829 let emb = self.embed_id(id);
10830 out.extend_from_slice(&self.forward_span(
10831 &emb,
10832 start_pos + i,
10833 0,
10834 upto,
10835 task_mask,
10836 )?);
10837 }
10838 Ok(out)
10839 }
10840 }
10841
10842 pub fn prefill_span_hidden(
10845 &mut self,
10846 hidden: &[f32],
10847 start_pos: usize,
10848 from: usize,
10849 upto: usize,
10850 task_mask: Option<&TaskMask>,
10851 ) -> Result<Vec<f32>, String> {
10852 self.split_supported()?;
10853 let hs = self.hidden_size;
10854 if hidden.is_empty() || hidden.len() % hs != 0 {
10855 return Err(format!(
10856 "prefill_span_hidden: {} floats is not a multiple of hidden {hs}",
10857 hidden.len()
10858 ));
10859 }
10860 if from > upto || upto >= self.num_layers {
10861 return Err(format!(
10862 "prefill_span_hidden: layer range {from}..={upto} outside 0..{}",
10863 self.num_layers
10864 ));
10865 }
10866 if self.can_prefill_batched() && !self.graph_prefill_preferred() {
10867 let out = self.prefill_batch_span(
10868 PrefillIn::Hidden(hidden),
10869 start_pos,
10870 task_mask,
10871 from,
10872 upto + 1,
10873 );
10874 self.check_o1_progress_failure("prefill_span_hidden")?;
10875 Ok(out)
10876 } else {
10877 let b = hidden.len() / hs;
10878 let mut out = Vec::with_capacity(hidden.len());
10879 for i in 0..b {
10880 let h = self.forward_span(
10881 &hidden[i * hs..(i + 1) * hs],
10882 start_pos + i,
10883 from,
10884 upto,
10885 task_mask,
10886 )?;
10887 out.extend_from_slice(&h);
10888 }
10889 Ok(out)
10890 }
10891 }
10892
10893 fn try_token_graph_wgpu(
10897 &self,
10898 hidden: &[f32],
10899 position: usize,
10900 logits_out: &mut Vec<f32>,
10901 layers_run: &mut usize,
10902 ) -> Option<Result<Vec<f32>, ()>> {
10903 self.try_token_graph_wgpu_steps(
10904 hidden,
10905 position,
10906 logits_out,
10907 1,
10908 None,
10909 Some(layers_run),
10910 0,
10911 self.num_layers,
10912 )
10913 }
10914
10915 fn try_token_graph_wgpu_span(
10919 &self,
10920 hidden: &[f32],
10921 position: usize,
10922 logits_out: &mut Vec<f32>,
10923 from: usize,
10924 upto_excl: usize,
10925 layers_run: &mut usize,
10926 ) -> Option<Result<Vec<f32>, ()>> {
10927 self.try_token_graph_wgpu_steps(
10928 hidden,
10929 position,
10930 logits_out,
10931 1,
10932 None,
10933 Some(layers_run),
10934 from,
10935 upto_excl,
10936 )
10937 }
10938
10939 fn try_multi_burst(&self, t_next: u32, position: usize, k: usize) -> Option<Vec<u32>> {
10943 if self.o1_active() || self.attn_softcap > 0.0 {
10944 return None;
10945 }
10946 if let Some(reason) = self.wgpu_graph_attn_decline() {
10949 self.note_graph_decline("wgpu multi-burst", reason);
10950 return None;
10951 }
10952 let graph_on = crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode);
10953 if !graph_on || self.graph_refused() {
10954 return None;
10961 }
10962 let emb = self.embed_single(t_next);
10963 let mut lg = Vec::new();
10964 let mut ids = Vec::new();
10965 match self.try_token_graph_wgpu_steps(
10966 &emb,
10967 position,
10968 &mut lg,
10969 k,
10970 Some(&mut ids),
10971 None,
10972 0,
10973 self.num_layers,
10974 ) {
10975 Some(Ok(_)) => {}
10976 Some(Err(())) => {
10977 self.graph_failed
10982 .store(true, std::sync::atomic::Ordering::Relaxed);
10983 return None;
10984 }
10985 None => return None,
10986 }
10987 (ids.len() == k).then_some(ids)
10988 }
10989
10990 fn try_token_graph_wgpu_steps(
10994 &self,
10995 hidden: &[f32],
10996 position: usize,
10997 logits_out: &mut Vec<f32>,
10998 steps: usize,
10999 ids_out: Option<&mut Vec<u32>>,
11000 layers_run: Option<&mut usize>,
11001 from: usize,
11002 upto_excl: usize,
11003 ) -> Option<Result<Vec<f32>, ()>> {
11004 let upto_excl = match self.mimo_moe.graph_prefix_end() {
11007 Some(end) if end < upto_excl => {
11008 if steps != 1 || layers_run.is_none() || from >= end {
11009 return None;
11010 }
11011 end
11012 }
11013 _ => upto_excl,
11014 };
11015 let o1_gpu = std::env::var("CMF_O1_GPU").as_deref() == Ok("1");
11018 if (self.o1_active() && !o1_gpu) || self.attn_softcap > 0.0 {
11019 return None;
11023 }
11024 if let Some(reason) = self.wgpu_graph_attn_decline() {
11031 self.note_graph_decline("wgpu token graph", reason);
11032 return None;
11033 }
11034 let o1_views: Vec<Option<Vec<crate::nystrom::O1DeviceView<'_>>>> = (from..upto_excl)
11039 .map(|li| {
11040 if !o1_gpu {
11041 return None;
11042 }
11043 self.kv_cache.layers[self.phys_layer(li)].o1_views()
11044 })
11045 .collect();
11046 if self.o1_active() && o1_gpu {
11047 let want: usize = (from..upto_excl)
11050 .filter(|li| self.kv_cache.layers[self.phys_layer(*li)].o1.is_some())
11051 .count();
11052 let have = o1_views.iter().filter(|v| v.is_some()).count();
11053 if want == 0 || have != want {
11054 use std::sync::atomic::{AtomicUsize, Ordering};
11064 static LAST: AtomicUsize = AtomicUsize::new(usize::MAX);
11065 let code = have * 1000 + want;
11066 if LAST.swap(code, Ordering::Relaxed) != code {
11067 tracing::warn!(
11068 "o1 graph: {have} of {want} layers sealed — per-op until all seal"
11069 );
11070 }
11071 return None;
11072 }
11073 }
11074 let nh = self.num_heads;
11075 let (nkv, hd, rd) = self.layer_geom(0);
11076 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11077 let mut layers = Vec::with_capacity(upto_excl - from);
11078 let mut model = None;
11079 let dbg = std::env::var("CMF_GRAPH_DEBUG").is_ok();
11080 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
11081 if let Some((m, i, kind, rs)) = t
11082 .graph_weight()
11083 .or_else(|| t.graph_weight_descriptor())
11084 {
11085 let name = &m.tensors[i].name;
11086 let prism = if crate::prism::is_inverse_embedding(m, name) {
11087 crate::gpu::GraphPrismOp::InverseEmbedding
11088 } else if crate::prism::is_forward_weight(m, name) {
11089 crate::gpu::GraphPrismOp::Forward
11090 } else {
11091 crate::gpu::GraphPrismOp::None
11092 };
11093 return Some(crate::gpu::GraphW {
11094 idx: i,
11095 kind,
11096 row_scale: rs,
11097 data: &[],
11098 prism,
11099 affine: crate::prism::is_affine_target(m, name),
11100 });
11101 }
11102 match t.as_f32() {
11104 Some(d) => Some(crate::gpu::GraphW {
11105 idx: 0,
11106 kind: 4,
11107 row_scale: &[],
11108 data: d,
11109 prism: crate::gpu::GraphPrismOp::None,
11110 affine: false,
11111 }),
11112 None => {
11113 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
11114 eprintln!("batch graph: weight has no graph/f32 representation");
11115 }
11116 None
11117 }
11118 }
11119 }
11120 for li in from..upto_excl {
11121 let lw = &self.weights.layers[self.phys_layer(li)];
11122 if dbg {
11123 let ak = match &lw.attn {
11124 AttnKind::Mla(_) => "Mla".into(),
11125 AttnKind::Full {
11126 output_gate, bias, ..
11127 } => format!("Full gate={output_gate} bias={}", bias.is_some()),
11128 AttnKind::LinearGdn(_) => "LinearGdn".into(),
11129 AttnKind::Kda(_) => "Kda".into(),
11130 AttnKind::Linear(_) => "Linear".into(),
11131 AttnKind::ShortConv(_) => "ShortConv".into(),
11132 AttnKind::Bounded(_) => "Bounded".into(),
11133 };
11134 let fk = match &lw.ffn {
11135 FfnKind::Dense(_) => "Dense",
11136 FfnKind::Moe(_) => "Moe",
11137 FfnKind::DenseMoe(_) => "DenseMoe",
11138 };
11139 eprintln!("graph L{li}: attn={ak} ffn={fk}");
11140 }
11141 let gffn = match &lw.ffn {
11142 FfnKind::DenseMoe(_) => return None, FfnKind::Dense(d) if !d.segs.is_empty() => return None,
11146 FfnKind::Dense(d) => {
11147 let Some(act) = d.act.graph_act() else {
11151 self.note_graph_decline(
11152 "wgpu token graph",
11153 "dense FFN activation without a graph kernel",
11154 );
11155 return None;
11156 };
11157 crate::gpu::GraphFfn::Dense {
11158 gate: gw(&d.gate_proj)?,
11159 up: gw(&d.up_proj)?,
11160 down: gw(&d.down_proj)?,
11161 act,
11162 }
11163 }
11164 FfnKind::Moe(m) => {
11165 if m.route_tau.is_some() || m.mask.is_some() {
11173 return None;
11174 }
11175 let shared = m.shared.as_ref();
11176 let has_shared = shared.is_some();
11177 let shared_gated = matches!(shared, Some((_, Some(_))));
11178 let sgate = match shared {
11179 Some((_, Some(sg))) => gw(sg)?,
11180 _ => gw(&m.router)?,
11184 };
11185 let router = gw(&m.router)?;
11186 if router.prism != crate::gpu::GraphPrismOp::None
11192 || sgate.prism != crate::gpu::GraphPrismOp::None
11193 || router.affine
11194 || sgate.affine
11195 {
11196 tracing::warn!(
11197 "resident MoE declined: Prism/affine router or shared gate transform is not implemented"
11198 );
11199 return None;
11200 }
11201 let inter = m.experts.first()?.gate_proj.rows();
11202 let mut experts = Vec::with_capacity(m.experts.len() + 1);
11203 let mut q4tp: Option<bool> = None;
11206 let mut gu_q2: Option<bool> = None;
11209 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
11210 if !matches!(e.act, Act::Silu)
11211 || e.gate_proj.rows() != inter
11212 || e.up_proj.rows() != inter
11213 {
11214 return None;
11215 }
11216 for expert_weight in [&e.gate_proj, &e.up_proj, &e.down_proj] {
11221 let Some((em, ei, _, _)) = expert_weight
11222 .graph_weight()
11223 .or_else(|| expert_weight.graph_weight_descriptor())
11224 else {
11225 return None;
11226 };
11227 let name = &em.tensors[ei].name;
11228 if crate::prism::is_forward_weight(em, name)
11229 || crate::prism::is_inverse_embedding(em, name)
11230 || crate::prism::is_affine_target(em, name)
11231 {
11232 tracing::warn!(
11233 "resident MoE declined: expert Prism/affine transform is not implemented"
11234 );
11235 return None;
11236 }
11237 }
11238 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
11239 Some((mm, gi)) => (
11240 mm,
11241 gi,
11242 e.up_proj.mapped_q4t()?.1,
11243 e.down_proj.mapped_q4t()?.1,
11244 false,
11245 false,
11246 ),
11247 None => match e.gate_proj.mapped_q2tp() {
11248 Some((mm, gi)) => (
11249 mm,
11250 gi,
11251 e.up_proj.mapped_q2tp()?.1,
11252 e.down_proj.mapped_q4tp()?.1,
11253 true,
11254 true,
11255 ),
11256 None => {
11257 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
11258 (
11259 mm,
11260 gi,
11261 e.up_proj.mapped_q4tp()?.1,
11262 e.down_proj.mapped_q4tp()?.1,
11263 true,
11264 false,
11265 )
11266 }
11267 },
11268 };
11269 if *q4tp.get_or_insert(is_p) != is_p || *gu_q2.get_or_insert(is_q2) != is_q2
11270 {
11271 tracing::warn!(
11277 "MoE layer mixes expert layouts (q4tp={is_p}, q2tp gate/up={is_q2}) — every expert of a layer, INCLUDING the shared one, must share a layout. The whole-token graph declines this layer."
11278 );
11279 return None;
11280 }
11281 model.get_or_insert_with(|| mm.clone());
11282 experts.push((gi, ui, di));
11283 }
11284 crate::gpu::GraphFfn::Moe {
11285 router,
11286 shared_gate: sgate,
11287 experts,
11288 n_exp: m.experts.len(),
11289 top_k: std::env::var("CMF_TOPK_PROBE")
11295 .ok()
11296 .and_then(|v| v.parse::<usize>().ok())
11297 .filter(|k| *k > 0 && *k <= m.top_k)
11298 .unwrap_or(m.top_k),
11299 inter,
11300 norm_topk: m.norm_topk_prob,
11301 q4tp: q4tp?,
11302 gu_q2: gu_q2.unwrap_or(false),
11303 sigmoid: m.router_sigmoid,
11304 bias: m.expert_bias.as_deref(),
11305 has_shared,
11306 shared_gated,
11307 route_scale: m.routed_scaling,
11308 }
11309 }
11310 };
11311 let attn = match &lw.attn {
11312 AttnKind::Full {
11313 wq,
11314 wk,
11315 wv,
11316 wo,
11317 q_norm,
11318 k_norm,
11319 output_gate,
11320 softplus_gate,
11321 bias,
11322 } => {
11323 if self.attention_heads_per_layer.is_some() {
11324 return None;
11325 }
11326 let head_gate = match softplus_gate {
11330 None => None,
11331 Some((g, true)) if self.proj_gate_sigmoid && !*output_gate => {
11332 Some(gw(g)?)
11333 }
11334 Some(_) => {
11335 self.note_graph_decline(
11336 "wgpu token graph",
11337 "projected softplus / per-element output gate",
11338 );
11339 return None;
11340 }
11341 };
11342 let (m, _, _, _) = wq
11343 .graph_weight()
11344 .or_else(|| wq.graph_weight_descriptor())?;
11345 model = Some(m.clone());
11346 crate::gpu::GraphAttn::Full {
11347 wq: gw(wq)?,
11348 wk: gw(wk)?,
11349 wv: gw(wv)?,
11350 wo: gw(wo)?,
11351 q_norm: q_norm.as_deref(),
11352 k_norm: k_norm.as_deref(),
11353 late_qk_norm: self.qk_norm_after_rope,
11354 bias: bias
11355 .as_ref()
11356 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
11357 output_gate: *output_gate,
11358 cpu_k: self.kv_cache.layers[li].k_heads(),
11359 cpu_v: self.kv_cache.layers[li].v_heads(),
11360 cpu_base: self.kv_cache.layers[li].base(),
11361 geom: self.graph_attn_geom(li),
11362 head_gate,
11363 }
11364 }
11365 AttnKind::LinearGdn(w) => {
11366 let cfg = self.gdn_cfg?;
11367 let (m, _, _, _) = w
11368 .in_proj_qkv
11369 .graph_weight()
11370 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
11371 model = Some(m.clone());
11372 crate::gpu::GraphAttn::Gdn {
11373 qkv: gw(&w.in_proj_qkv)?,
11374 z: gw(&w.in_proj_z)?,
11375 a: gw(&w.in_proj_a)?,
11376 b: gw(&w.in_proj_b)?,
11377 out: gw(&w.out_proj)?,
11378 conv1d: &w.conv1d,
11379 a_log: &w.a_log,
11380 dt_bias: &w.dt_bias,
11381 norm: &w.norm,
11382 nv: cfg.num_v_heads,
11383 nk: cfg.num_k_heads,
11384 dk: cfg.key_head_dim,
11385 dv: cfg.value_head_dim,
11386 kk: cfg.conv_kernel,
11387 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11388 }
11389 }
11390 AttnKind::ShortConv(w) => {
11391 let cfg = self.short_conv_cfg?;
11392 let (m, _, _, _) = w
11393 .in_proj
11394 .graph_weight()
11395 .or_else(|| w.in_proj.graph_weight_descriptor())?;
11396 model = Some(m.clone());
11397 crate::gpu::GraphAttn::ShortConv {
11398 inp: gw(&w.in_proj)?,
11399 out: gw(&w.out_proj)?,
11400 taps: &w.conv,
11401 kernel: cfg.kernel,
11402 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
11403 }
11404 }
11405 _ => return None,
11406 };
11407 layers.push(crate::gpu::GraphLayer {
11408 input_norm: &lw.input_norm,
11409 attn,
11410 post_norm: &lw.post_norm,
11411 ffn: gffn,
11412 });
11413 }
11414 let model = model?;
11415 let lm_gw = if upto_excl == self.num_layers
11421 && self.graph_want_logits
11422 && std::env::var("CMF_GPU_LMHEAD")
11423 .map(|v| v != "0")
11424 .unwrap_or(true)
11425 {
11426 self.weights
11427 .lm_head
11428 .graph_weight()
11429 .or_else(|| self.weights.lm_head.graph_weight_descriptor())
11430 .map(|(m, i, kind, rs)| {
11431 let name = &m.tensors[i].name;
11432 let prism = if crate::prism::is_inverse_embedding(m, name) {
11433 crate::gpu::GraphPrismOp::InverseEmbedding
11434 } else if crate::prism::is_forward_weight(m, name) {
11435 crate::gpu::GraphPrismOp::Forward
11436 } else {
11437 crate::gpu::GraphPrismOp::None
11438 };
11439 (
11440 crate::gpu::GraphW {
11441 idx: i,
11442 kind,
11443 row_scale: rs,
11444 data: &[],
11445 prism,
11446 affine: crate::prism::is_affine_target(m, name),
11447 },
11448 self.weights.lm_head.rows(),
11449 )
11450 })
11451 } else {
11452 None
11453 };
11454 let lm = lm_gw.as_ref().map(|(gw, rows)| (gw, *rows));
11455 let emb_gw = if steps > 1 {
11457 self.weights
11458 .embed_tokens
11459 .graph_weight()
11460 .or_else(|| self.weights.embed_tokens.graph_weight_descriptor())
11461 .map(|(m, i, kind, rs)| {
11462 let name = &m.tensors[i].name;
11463 let prism = if crate::prism::is_inverse_embedding(m, name) {
11464 crate::gpu::GraphPrismOp::InverseEmbedding
11465 } else if crate::prism::is_forward_weight(m, name) {
11466 crate::gpu::GraphPrismOp::Forward
11467 } else {
11468 crate::gpu::GraphPrismOp::None
11469 };
11470 (
11471 crate::gpu::GraphW {
11472 idx: i,
11473 kind,
11474 row_scale: rs,
11475 data: &[],
11476 prism,
11477 affine: crate::prism::is_affine_target(m, name),
11478 },
11479 self.weights.embed_tokens.rows(),
11480 self.embed_multiplier,
11481 )
11482 })
11483 } else {
11484 None
11485 };
11486
11487 let loop_norm_at: Vec<usize> = if self.loop_final_norm {
11493 (from..upto_excl.min(self.num_layers - 1))
11494 .filter(|&li| (li + 1) % self.physical_layers == 0)
11495 .map(|li| li - from)
11496 .collect()
11497 } else {
11498 Vec::new()
11499 };
11500 let mut h = hidden.to_vec();
11501 let dump_hidden = std::env::var_os("CMF_LOGIT_DUMP").is_some();
11507 let outcome = crate::gpu::forward_token_graph(
11508 &model,
11509 self.graph_kv_id,
11510 &layers,
11511 &o1_views,
11512 self.o1_epoch,
11513 &self.inv_freq,
11514 &mut h,
11515 nh,
11516 nkv,
11517 hd,
11518 self.attn_scale,
11519 rd,
11520 self.hidden_size,
11521 self.intermediate_size,
11522 position,
11523 self.kv_cache.max_seq_len,
11524 gemma,
11525 self.rms_eps as f32,
11526 lm,
11527 &self.weights.final_norm,
11528 logits_out,
11529 &loop_norm_at,
11530 steps,
11531 emb_gw.as_ref().map(|(gw, rows, m)| (gw, *rows, *m)),
11532 ids_out,
11533 layers_run,
11534 from,
11535 dump_hidden,
11536 );
11537 match outcome {
11538 crate::gpu::TokenGraphOutcome::Completed => Some(Ok(h)),
11539 crate::gpu::TokenGraphOutcome::Failed => Some(Err(())),
11540 crate::gpu::TokenGraphOutcome::Declined => None,
11541 }
11542 }
11543
11544 #[cfg(target_os = "macos")]
11553 #[allow(clippy::type_complexity)]
11554 fn metal_rows_plan(
11555 &self,
11556 ) -> Option<(
11557 Vec<MetalRowsItem<'_>>,
11558 std::sync::Arc<cortiq_core::CmfModel>,
11559 Option<crate::gpu_metal::GdnGpuCfg>,
11560 )> {
11561 use crate::gpu_metal::{AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, MetalFfn};
11562 let graph_force = crate::gpu::q1_force() || crate::gpu::q2tp_gpu_opt_in();
11563 if !graph_force
11564 || !crate::gpu::enabled_here()
11565 || std::env::var("CMF_GPU_BLOCK")
11566 .map(|v| v == "0")
11567 .unwrap_or(false)
11568 || self.attn_softcap > 0.0
11569 || self.o1_active()
11570 || self.swa.is_some()
11571 || self.global_attn.is_some()
11572 || self.attention_heads_per_layer.is_some()
11573 || self.graph_attn_decline_reason().is_some()
11575 || self.attn_v_norm
11576 || self.loop_final_norm
11577 {
11578 return None;
11579 }
11580 let attend_contract = self.head_dim % 4 == 0
11581 && self.head_dim <= 256
11582 && self.rotary_dim >= 2
11583 && self.rotary_dim <= self.head_dim
11584 && (self.rotary_dim / 2) % 32 == 0
11585 && self.num_kv_heads > 0
11586 && self.num_heads % self.num_kv_heads == 0;
11587 if !attend_contract {
11588 return None;
11589 }
11590 let mut plan: Vec<MetalRowsItem> = Vec::new();
11591 let mut model_ref: Option<std::sync::Arc<cortiq_core::CmfModel>> = None;
11592 for li in 0..self.num_layers {
11593 let lw = &self.weights.layers[self.phys_layer(li)];
11594 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
11595 return None;
11596 }
11597 let ffn = match &lw.ffn {
11598 FfnKind::Dense(d) if d.act == Act::Silu && d.segs.is_empty() => {
11599 let (Some(g), Some(u), Some(dn)) = (
11600 d.gate_proj.metal_graph_parts(),
11601 d.up_proj.metal_graph_parts(),
11602 d.down_proj.metal_graph_parts(),
11603 ) else {
11604 return None;
11605 };
11606 MetalFfn::Dense {
11607 gate: g,
11608 up: u,
11609 down: dn,
11610 gelu: false, }
11612 }
11613 _ => return None,
11614 };
11615 match &lw.attn {
11616 AttnKind::LinearGdn(w) if self.gdn_cfg.is_some() => {
11617 let (Some(qkv), Some(z), Some(a), Some(bb), Some(out)) = (
11618 w.in_proj_qkv.metal_graph_parts(),
11619 w.in_proj_z.metal_graph_parts(),
11620 w.in_proj_a.f32_parts(),
11621 w.in_proj_b.f32_parts(),
11622 w.out_proj.metal_graph_parts(),
11623 ) else {
11624 return None;
11625 };
11626 if let QTensor::Mapped { model, .. } = &w.in_proj_qkv {
11627 model_ref.get_or_insert_with(|| model.clone());
11628 }
11629 let gl = GdnGpuLayer {
11630 attn_norm: &lw.input_norm,
11631 post_norm: &lw.post_norm,
11632 qkv,
11633 z,
11634 a,
11635 b: bb,
11636 out,
11637 ffn,
11638 conv1d: &w.conv1d,
11639 a_log: &w.a_log,
11640 dt_bias: &w.dt_bias,
11641 gnorm: &w.norm,
11642 };
11643 match plan.last_mut() {
11644 Some(MetalRowsItem::Gdn { run, .. }) => run.push(gl),
11645 _ => plan.push(MetalRowsItem::Gdn {
11646 run: vec![gl],
11647 first: li,
11648 }),
11649 }
11650 }
11651 AttnKind::Full {
11652 wq,
11653 wk,
11654 wv,
11655 wo,
11656 q_norm,
11657 k_norm,
11658 output_gate,
11659 softplus_gate: None,
11660 bias: None,
11661 } => {
11662 let (Some(pq), Some(pk), Some(pv), Some(po)) =
11663 (
11664 wq.metal_graph_parts(),
11665 wk.metal_graph_parts(),
11666 wv.metal_graph_parts(),
11667 wo.metal_graph_parts(),
11668 )
11669 else {
11670 return None;
11671 };
11672 if let QTensor::Mapped { model, .. } = wq {
11673 model_ref.get_or_insert_with(|| model.clone());
11674 }
11675 let cache = &self.kv_cache.layers[li];
11676 if cache.mode != crate::kv_cache::KvMode::F32 || cache.o1.is_some() {
11677 return None;
11678 }
11679 plan.push(MetalRowsItem::Attn {
11680 l: AttnGpuLayer {
11681 attn_norm: &lw.input_norm,
11682 post_norm: &lw.post_norm,
11683 wq: pq,
11684 wk: pk,
11685 wv: pv,
11686 wo: po,
11687 ffn,
11688 },
11689 li,
11690 q_norm: q_norm.as_deref(),
11691 k_norm: k_norm.as_deref(),
11692 output_gate: *output_gate,
11693 });
11694 }
11695 _ => return None,
11696 }
11697 }
11698 let model = model_ref?;
11699 let gcfg = self.gdn_cfg.map(|cfg| GdnGpuCfg {
11700 nv: cfg.num_v_heads,
11701 nk: cfg.num_k_heads,
11702 dk: cfg.key_head_dim,
11703 dv: cfg.value_head_dim,
11704 kk: cfg.conv_kernel,
11705 hidden: self.hidden_size,
11706 inter: self.intermediate_size,
11707 c_dim: cfg.conv_dim(),
11708 eps: cfg.rms_eps as f32,
11709 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11710 });
11711 Some((plan, model, gcfg))
11712 }
11713
11714 #[cfg(target_os = "macos")]
11716 #[allow(clippy::too_many_arguments)]
11717 fn metal_attn_params<'a>(
11718 li: usize,
11719 cache: &'a crate::kv_cache::LayerKvCache,
11720 q_norm: Option<&'a [f32]>,
11721 k_norm: Option<&'a [f32]>,
11722 output_gate: bool,
11723 inv_freq: &'a [f32],
11724 geom: (usize, usize, usize, usize),
11725 pos0: usize,
11726 kv_id: u64,
11727 scale: f32,
11728 eps: f32,
11729 gemma: bool,
11730 late_qk_norm: bool,
11731 ) -> (crate::gpu_metal::AttnDeviceParams<'a>, usize) {
11732 let (nh, nkv, hd, rd) = geom;
11733 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
11734 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
11735 let cpu_stored = cpu_k[0].len() / hd;
11736 (
11737 crate::gpu_metal::AttnDeviceParams {
11738 kv_id,
11739 layer: li,
11740 nh,
11741 nkv,
11742 hd,
11743 rd,
11744 position: pos0,
11745 scale,
11746 eps,
11747 gemma,
11748 late_qk_norm,
11749 output_gate,
11750 q_norm,
11751 k_norm,
11752 inv_freq,
11753 cpu_k,
11754 cpu_v,
11755 cpu_stored,
11756 cpu_gen: cache.generation(),
11757 o1: None,
11758 window: None,
11759 head_gate: None,
11760 },
11761 cpu_stored,
11762 )
11763 }
11764
11765 #[cfg(target_os = "macos")]
11770 #[allow(clippy::type_complexity)]
11771 fn metal_rows_run(
11772 &mut self,
11773 hiddens: &mut [f32],
11774 pos0: usize,
11775 b: usize,
11776 prefill: bool,
11777 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
11778 mut argmax_out: Option<(usize, &mut Vec<u32>)>,
11782 ) -> MetalRowsRun {
11783 use crate::gpu_metal::{GraphDims, VerifyGraph};
11784 if !crate::gpu_metal::wait_replay() {
11790 tracing::error!("Metal rows graph: the pending async replay failed");
11791 return MetalRowsRun::Failed;
11792 }
11793 spec_stamp("v.wait");
11794 let want = self.gdn_cfg.map(|c| c.state_len()).unwrap_or(0);
11800 if want > 0 {
11801 let phys = self.physical_layers.max(1);
11802 for (li, l) in self.kv_cache.layers.iter_mut().enumerate() {
11803 let is_gdn = self
11804 .weights
11805 .layers
11806 .get(li % phys)
11807 .is_some_and(|lw| matches!(lw.attn, AttnKind::LinearGdn(_)));
11808 if is_gdn && l.linear_state.len() != want {
11809 l.linear_state = vec![0f32; want];
11810 }
11811 }
11812 }
11813 let Some((plan, model, gcfg)) = self.metal_rows_plan() else {
11814 return MetalRowsRun::Declined;
11815 };
11816 spec_stamp("v.plan");
11817 let dims = GraphDims {
11818 hidden: self.hidden_size,
11819 eps: self.rms_eps as f32,
11820 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
11821 };
11822 let Some(mut graph) = (if prefill {
11823 VerifyGraph::new_prefill(&model, dims, hiddens, b)
11824 } else {
11825 VerifyGraph::new(&model, dims, hiddens, b)
11826 }) else {
11827 return MetalRowsRun::Declined;
11828 };
11829 let geom = (
11830 self.num_heads,
11831 self.num_kv_heads,
11832 self.head_dim,
11833 self.rotary_dim,
11834 );
11835 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
11836 let eps = self.rms_eps as f32;
11837 let kv_id = self.graph_kv_id;
11838 let inv_freq = self.inv_freq.clone();
11839 for item in &plan {
11840 let ok = match item {
11841 MetalRowsItem::Gdn { run, .. } => gcfg
11842 .as_ref()
11843 .map(|gc| run.iter().all(|l| graph.gdn_ok(l, gc)))
11844 .unwrap_or(false),
11845 MetalRowsItem::Attn {
11846 l,
11847 li,
11848 q_norm,
11849 k_norm,
11850 output_gate,
11851 } => {
11852 let (p, _) = Self::metal_attn_params(
11853 *li,
11854 &self.kv_cache.layers[*li],
11855 *q_norm,
11856 *k_norm,
11857 *output_gate,
11858 &inv_freq,
11859 geom,
11860 pos0,
11861 kv_id,
11862 self.attn_scale,
11863 eps,
11864 gemma,
11865 self.qk_norm_after_rope,
11866 );
11867 graph.attn_ok(l, &p)
11868 }
11869 };
11870 if !ok {
11871 use std::sync::atomic::{AtomicBool, Ordering};
11872 static SAID: AtomicBool = AtomicBool::new(false);
11873 if !SAID.swap(true, Ordering::Relaxed) {
11874 tracing::warn!("metal rows graph: a layer failed preflight — declining");
11875 }
11876 return MetalRowsRun::Declined;
11877 }
11878 }
11879 let lm = match &spec {
11880 Some((lm, _, _)) => {
11881 if !graph.lm_head_ok(*lm) {
11882 return MetalRowsRun::Declined;
11883 }
11884 Some(*lm)
11885 }
11886 None => None,
11887 };
11888 let mut gdn_layers = Vec::new();
11889 let mut attn_layers = Vec::new();
11890 for item in &plan {
11891 match item {
11892 MetalRowsItem::Gdn { run, first } => {
11893 let ro: Vec<&[f32]> = self.kv_cache.layers[*first..*first + run.len()]
11894 .iter()
11895 .map(|l| l.linear_state.as_slice())
11896 .collect();
11897 if !graph.encode_gdn_run_b(run, &ro, gcfg.as_ref().unwrap()) {
11898 return MetalRowsRun::Declined;
11899 }
11900 gdn_layers.extend(*first..*first + run.len());
11901 }
11902 MetalRowsItem::Attn {
11903 l,
11904 li,
11905 q_norm,
11906 k_norm,
11907 output_gate,
11908 } => {
11909 let (p, cpu_stored) = Self::metal_attn_params(
11910 *li,
11911 &self.kv_cache.layers[*li],
11912 *q_norm,
11913 *k_norm,
11914 *output_gate,
11915 &inv_freq,
11916 geom,
11917 pos0,
11918 kv_id,
11919 self.attn_scale,
11920 eps,
11921 gemma,
11922 self.qk_norm_after_rope,
11923 );
11924 if !graph.encode_attn_b(l, &p) {
11925 return MetalRowsRun::Declined;
11926 }
11927 attn_layers.push((*li, cpu_stored));
11928 }
11929 }
11930 }
11931 if let (Some(lm), Some((_, final_norm, _))) = (lm, spec.as_ref()) {
11932 if !graph.encode_lm_head_b(final_norm, lm) {
11933 return MetalRowsRun::Declined;
11934 }
11935 if let Some((n, _)) = argmax_out.as_ref() {
11940 if !graph.encode_argmax_b(*n) {
11941 argmax_out = None;
11942 }
11943 }
11944 }
11945 spec_stamp("v.enc");
11946 if !graph.sync() {
11947 return MetalRowsRun::Failed;
11948 }
11949 spec_stamp("v.gpu");
11950 match (spec, argmax_out) {
11951 (Some(_), Some((_, ids))) => {
11952 ids.resize(b, 0);
11953 if !graph.read_argmax(ids) {
11954 return MetalRowsRun::Failed;
11955 }
11956 spec_stamp("v.am");
11957 }
11958 (Some((lm, _, logits)), None) => {
11959 logits.resize(b * lm.1, 0.0);
11960 if !graph.read_logits(logits) {
11961 return MetalRowsRun::Failed;
11962 }
11963 spec_stamp("v.lg");
11964 }
11965 (None, _) => {}
11966 }
11967 if !graph.read_hidden(hiddens) {
11968 return MetalRowsRun::Failed;
11969 }
11970 spec_stamp("v.hid");
11971 MetalRowsRun::Completed(MetalVerifyPending {
11972 graph,
11973 gdn_layers,
11974 attn_layers,
11975 })
11976 }
11977
11978 #[cfg(target_os = "macos")]
11984 fn try_batch_graph_metal(
11985 &mut self,
11986 hiddens: &mut [f32],
11987 positions: &[usize],
11988 b: usize,
11989 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
11990 argmax_out: Option<(usize, &mut Vec<u32>)>,
11991 ) -> crate::gpu::BatchGraphOutcome {
11992 let _t0 = std::time::Instant::now();
11993 if positions.len() != b
11994 || positions.windows(2).any(|w| w[1] != w[0] + 1)
11995 || hiddens.len() != b * self.hidden_size
11996 {
11997 return crate::gpu::BatchGraphOutcome::Declined;
11998 }
11999 let pending = match self.metal_rows_run(hiddens, positions[0], b, false, spec, argmax_out) {
12000 MetalRowsRun::Declined => return crate::gpu::BatchGraphOutcome::Declined,
12001 MetalRowsRun::Failed => return crate::gpu::BatchGraphOutcome::Failed,
12002 MetalRowsRun::Completed(pending) => pending,
12003 };
12004 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
12005 eprintln!(
12006 "metal-verify: {:.1} ms | b={b}",
12007 _t0.elapsed().as_secs_f64() * 1e3
12008 );
12009 }
12010 self.metal_verify = Some(pending);
12011 crate::gpu::BatchGraphOutcome::Completed
12012 }
12013
12014 #[cfg(target_os = "macos")]
12019 fn prefill_rows_metal(
12020 &mut self,
12021 ids: &[u32],
12022 start_pos: usize,
12023 spec: Option<((usize, usize, usize), &[f32], &mut Vec<f32>)>,
12024 ) -> MetalPrefillOutcome {
12025 let b = ids.len();
12026 if b == 0 || b > 512 {
12027 return MetalPrefillOutcome::Declined;
12028 }
12029 METAL_PREFILL_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12030 let with_head = spec.is_some();
12031 let hs = self.hidden_size;
12032 let mut hiddens = vec![0f32; b * hs];
12033 for (j, &id) in ids.iter().enumerate() {
12034 let e = self.embed_single(id);
12035 hiddens[j * hs..(j + 1) * hs].copy_from_slice(&e);
12036 }
12037 let mut pending = match self.metal_rows_run(&mut hiddens, start_pos, b, true, spec, None) {
12038 MetalRowsRun::Declined => return MetalPrefillOutcome::Declined,
12039 MetalRowsRun::Failed => {
12040 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12041 return MetalPrefillOutcome::Failed;
12042 }
12043 MetalRowsRun::Completed(pending) => pending,
12044 };
12045 let idxs = pending.gdn_layers.clone();
12047 let mut outs: Vec<&mut [f32]> = self
12048 .kv_cache
12049 .layers
12050 .iter_mut()
12051 .enumerate()
12052 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12053 .map(|(_, l)| l.linear_state.as_mut_slice())
12054 .collect();
12055 if !pending.graph.finish_states(&mut outs) {
12056 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12057 return MetalPrefillOutcome::Failed;
12058 }
12059 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12060 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12064 for (li, cpu_stored) in &pending.attn_layers {
12065 let mut kbuf = vec![0f32; b * nkv * hd];
12066 let mut vbuf = vec![0f32; b * nkv * hd];
12067 if !crate::gpu_metal::kv_mirror_read_rows(
12068 self.graph_kv_id,
12069 *li,
12070 nkv,
12071 hd,
12072 *cpu_stored,
12073 b,
12074 &mut kbuf,
12075 &mut vbuf,
12076 ) {
12077 METAL_PREFILL_ERRORS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12078 return MetalPrefillOutcome::Failed;
12079 }
12080 rows.push((*li, *cpu_stored, kbuf, vbuf));
12081 }
12082 for (li, cpu_stored, kbuf, vbuf) in rows {
12083 let cache = &mut self.kv_cache.layers[li];
12084 for r in 0..b {
12085 cache.append(
12086 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12087 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12088 &[],
12089 );
12090 }
12091 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + b);
12092 }
12093 METAL_PREFILL_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12094 if with_head {
12095 METAL_PREFILL_HEAD_ROWS.fetch_add(b as u64, std::sync::atomic::Ordering::Relaxed);
12096 }
12097 MetalPrefillOutcome::Completed(hiddens)
12098 }
12099
12100 #[cfg(target_os = "macos")]
12101 fn prefill_batch_metal(&mut self, ids: &[u32], start_pos: usize) -> MetalPrefillOutcome {
12102 self.prefill_rows_metal(ids, start_pos, None)
12103 }
12104
12105 #[cfg(target_os = "macos")]
12110 fn nll_batch_metal(&mut self, ids: &[u32], start: usize) -> MetalBatchNllOutcome {
12111 if ids.len() < 2 || self.o1_active() || self.head_clusters.is_some() {
12112 return MetalBatchNllOutcome::Declined;
12113 }
12114 let Some(lm) = self.weights.lm_head.metal_graph_parts() else {
12115 return MetalBatchNllOutcome::Declined;
12116 };
12117 let chunk = std::env::var("CMF_METAL_PREFILL_CHUNK")
12118 .ok()
12119 .and_then(|v| v.parse::<usize>().ok())
12120 .filter(|&v| (1..=512).contains(&v))
12121 .unwrap_or(32);
12122 let final_norm = self.weights.final_norm.clone();
12123 let mut nll = 0.0f64;
12124 let mut count = 0usize;
12125 let mut pos = 0usize;
12126 let mut completed = 0usize;
12127 while pos < ids.len() {
12128 let end = (pos + chunk).min(ids.len());
12129 let mut logits = Vec::new();
12130 let outcome = self.prefill_rows_metal(
12131 &ids[pos..end],
12132 pos,
12133 Some((lm, &final_norm, &mut logits)),
12134 );
12135 match outcome {
12136 MetalPrefillOutcome::Declined => {
12137 return if completed == 0 {
12138 MetalBatchNllOutcome::Declined
12139 } else {
12140 MetalBatchNllOutcome::Failed(format!(
12141 "ordinary Metal NLL batch declined after {completed} chunks"
12142 ))
12143 };
12144 }
12145 MetalPrefillOutcome::Failed => {
12146 return MetalBatchNllOutcome::Failed(
12147 "ordinary Metal NLL batch failed after admission".to_string(),
12148 );
12149 }
12150 MetalPrefillOutcome::Completed(_) => {}
12151 }
12152 completed += 1;
12153 let vocab = self.vocab_size.min(lm.1);
12154 if logits.len() != (end - pos) * lm.1 || vocab == 0 {
12155 return MetalBatchNllOutcome::Failed(
12156 "ordinary Metal NLL head returned an invalid shape".to_string(),
12157 );
12158 }
12159 for row in 0..(end - pos) {
12160 let absolute = pos + row;
12161 if absolute < start || absolute + 1 >= ids.len() {
12162 continue;
12163 }
12164 let lg = &mut logits[row * lm.1..row * lm.1 + vocab];
12165 if let Some(mu) = self.logit_multiplier {
12166 for v in lg.iter_mut() {
12167 *v *= mu;
12168 }
12169 }
12170 if let Some(c) = self.final_softcap {
12171 for v in lg.iter_mut() {
12172 *v = c * (*v / c).tanh();
12173 }
12174 }
12175 let target = ids[absolute + 1] as usize;
12176 if target >= vocab {
12177 return MetalBatchNllOutcome::Failed(format!(
12178 "target token {target} exceeds Metal head rows {vocab}"
12179 ));
12180 }
12181 let max = lg.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
12182 let lse: f64 = lg
12183 .iter()
12184 .map(|&v| ((v - max) as f64).exp())
12185 .sum::<f64>()
12186 .ln()
12187 + max as f64;
12188 nll += lse - lg[target] as f64;
12189 count += 1;
12190 }
12191 pos = end;
12192 }
12193 MetalBatchNllOutcome::Completed(nll, count)
12194 }
12195
12196 #[cfg(target_os = "macos")]
12200 fn metal_verify_commit(&mut self, a: usize) -> bool {
12201 let Some(mut pending) = self.metal_verify.take() else {
12202 return false;
12203 };
12204 let n = a + 1;
12205 let idxs = pending.gdn_layers.clone();
12207 let mut outs: Vec<&mut [f32]> = self
12208 .kv_cache
12209 .layers
12210 .iter_mut()
12211 .enumerate()
12212 .filter(|(i, _)| idxs.binary_search(i).is_ok())
12213 .map(|(_, l)| l.linear_state.as_mut_slice())
12214 .collect();
12215 if !pending.graph.commit(n, &mut outs) {
12216 return false;
12217 }
12218 spec_stamp("c.replay");
12219 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12220 let mut rows = Vec::with_capacity(pending.attn_layers.len());
12224 for (li, cpu_stored) in &pending.attn_layers {
12225 let mut kbuf = vec![0f32; n * nkv * hd];
12226 let mut vbuf = vec![0f32; n * nkv * hd];
12227 if !crate::gpu_metal::kv_mirror_read_rows(
12228 self.graph_kv_id,
12229 *li,
12230 nkv,
12231 hd,
12232 *cpu_stored,
12233 n,
12234 &mut kbuf,
12235 &mut vbuf,
12236 ) {
12237 return false;
12238 }
12239 rows.push((*li, *cpu_stored, kbuf, vbuf));
12240 }
12241 for (li, cpu_stored, kbuf, vbuf) in rows {
12242 let cache = &mut self.kv_cache.layers[li];
12243 for r in 0..n {
12244 cache.append(
12245 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12246 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12247 &[],
12248 );
12249 }
12250 crate::gpu_metal::kv_mirror_set_stored(self.graph_kv_id, li, cpu_stored + n);
12251 }
12252 spec_stamp("c.kv");
12253 true
12254 }
12255
12256 #[cfg(target_os = "macos")]
12263 fn mtp_warm_batch_submit(
12264 &mut self,
12265 m: &mut MtpModule,
12266 pairs: &[(&[f32], u32)],
12267 first_pos: usize,
12268 ) -> Option<MetalWarmPending> {
12269 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, VerifyGraph};
12270 let b = pairs.len();
12271 if b == 0 || b > 512 || m.kv.mode != crate::kv_cache::KvMode::F32 || m.kv.o1.is_some() {
12272 return None;
12273 }
12274 let AttnKind::Full {
12275 wq,
12276 wk,
12277 wv,
12278 wo,
12279 q_norm,
12280 k_norm,
12281 output_gate,
12282 softplus_gate: None,
12283 bias: None,
12284 } = &m.layer.attn
12285 else {
12286 return None;
12287 };
12288 let FfnKind::Dense(d) = &m.layer.ffn else {
12289 return None;
12290 };
12291 if !d.segs.is_empty() {
12292 return None;
12293 }
12294 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12295 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
12296 else {
12297 return None;
12298 };
12299 let (Some(g), Some(u), Some(dn)) = (
12300 d.gate_proj.q1_parts(),
12301 d.up_proj.q1_parts(),
12302 d.down_proj.q1_parts(),
12303 ) else {
12304 return None;
12305 };
12306 let Some(eh) = m.eh_proj.q1_parts() else {
12307 return None;
12308 };
12309 let QTensor::Mapped { model, .. } = wq else {
12310 return None;
12311 };
12312 let model = model.clone();
12313 let hs = self.hidden_size;
12314 let mut cat = vec![0f32; b * 2 * hs];
12316 for (j, (h, tok)) in pairs.iter().enumerate() {
12317 let e = self.embed_single(*tok);
12318 let (ce, ch) = cat[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
12319 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, ce);
12320 inference::rms_norm_into(h, &m.hnorm, self.rms_eps, self.norm_style, ch);
12321 }
12322 let dims = GraphDims {
12323 hidden: hs,
12324 eps: self.rms_eps as f32,
12325 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12326 };
12327 spec_stamp("w.cat");
12328 let Some(mut graph) = VerifyGraph::new_via_proj(&model, dims, eh, &cat, b) else {
12329 return None;
12330 };
12331 spec_stamp("w.new");
12332 let l = AttnGpuLayer {
12333 attn_norm: &m.layer.input_norm,
12334 post_norm: &m.layer.post_norm,
12335 wq: pq,
12336 wk: pk,
12337 wv: pv,
12338 wo: po,
12339 ffn: MetalFfn::Dense {
12340 gate: g,
12341 up: u,
12342 down: dn,
12343 gelu: d.act == Act::Gelu,
12344 },
12345 };
12346 let (nh, nkv, hd, rd) = (
12347 self.num_heads,
12348 self.num_kv_heads,
12349 self.head_dim,
12350 self.rotary_dim,
12351 );
12352 let inv_freq = self.inv_freq.clone();
12353 let cpu_stored;
12354 {
12355 let cache = &m.kv;
12356 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12357 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12358 cpu_stored = cpu_k[0].len() / hd;
12359 if cpu_stored > first_pos {
12364 spec_stamp("w.decl");
12365 return None;
12366 }
12367 let p = AttnDeviceParams {
12368 kv_id: self.mtp_kv_id(),
12369 layer: Self::MTP_LAYER_BASE,
12370 nh,
12371 nkv,
12372 hd,
12373 rd,
12374 position: first_pos,
12375 scale: self.attn_scale,
12376 eps: self.rms_eps as f32,
12377 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12378 late_qk_norm: self.qk_norm_after_rope,
12379 output_gate: *output_gate,
12380 q_norm: q_norm.as_deref(),
12381 k_norm: k_norm.as_deref(),
12382 inv_freq: &inv_freq,
12383 cpu_k,
12384 cpu_v,
12385 cpu_stored,
12386 cpu_gen: cache.generation(),
12387 o1: None,
12388 window: None,
12389 head_gate: None,
12390 };
12391 if !graph.attn_ok(&l, &p) || !graph.encode_attn_b(&l, &p) {
12392 return None;
12393 }
12394 }
12395 spec_stamp("w.enc");
12396 if !graph.submit() {
12397 return None;
12398 }
12399 spec_stamp("w.sub");
12400 Some(MetalWarmPending {
12401 graph,
12402 cpu_stored,
12403 b,
12404 })
12405 }
12406
12407 #[cfg(target_os = "macos")]
12410 fn mtp_warm_batch_metal(
12411 &mut self,
12412 m: &mut MtpModule,
12413 pairs: &[(&[f32], u32)],
12414 first_pos: usize,
12415 ) -> bool {
12416 match self.mtp_warm_batch_submit(m, pairs, first_pos) {
12417 Some(p) => self.mtp_warm_batch_finish(m, p),
12418 None => false,
12419 }
12420 }
12421
12422 #[cfg(target_os = "macos")]
12427 fn mtp_warm_batch_finish(&mut self, m: &mut MtpModule, pending: MetalWarmPending) -> bool {
12428 let MetalWarmPending {
12429 mut graph,
12430 cpu_stored,
12431 b,
12432 } = pending;
12433 let (nkv, hd) = (self.num_kv_heads, self.head_dim);
12434 if !graph.sync() {
12435 return false;
12436 }
12437 spec_stamp("w.gpu");
12438 let mut kbuf = vec![0f32; b * nkv * hd];
12439 let mut vbuf = vec![0f32; b * nkv * hd];
12440 if !crate::gpu_metal::kv_mirror_read_rows(
12441 self.mtp_kv_id(),
12442 Self::MTP_LAYER_BASE,
12443 nkv,
12444 hd,
12445 cpu_stored,
12446 b,
12447 &mut kbuf,
12448 &mut vbuf,
12449 ) {
12450 return false;
12451 }
12452 for r in 0..b {
12453 m.kv.append(
12454 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12455 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12456 &[],
12457 );
12458 }
12459 crate::gpu_metal::kv_mirror_set_stored(
12460 self.mtp_kv_id(),
12461 Self::MTP_LAYER_BASE,
12462 cpu_stored + b,
12463 );
12464 spec_stamp("w.kv");
12465 true
12466 }
12467
12468 pub(crate) fn note_draft_id(&mut self, id: u32) {
12475 let cut = Self::draft_vocab_rows(usize::MAX).max(131_072);
12476 if (id as usize) >= cut {
12477 self.draft_full_streak = 16;
12478 } else {
12479 self.draft_full_streak = self.draft_full_streak.saturating_sub(1);
12480 }
12481 }
12482
12483 fn draft_head_rows(&self, head_rows: usize) -> usize {
12486 if self.draft_full_streak > 0 {
12487 head_rows
12488 } else {
12489 Self::draft_vocab_rows(head_rows)
12490 }
12491 }
12492
12493 fn draft_vocab_rows(head_rows: usize) -> usize {
12496 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
12497 let n = *N.get_or_init(|| {
12498 std::env::var("CMF_DRAFT_VOCAB")
12499 .ok()
12500 .and_then(|v| v.parse().ok())
12501 .unwrap_or(65536)
12502 });
12503 if n == 0 { head_rows } else { n.min(head_rows) }
12504 }
12505
12506 #[cfg(target_os = "macos")]
12511 fn mtp_step_metal(
12512 &mut self,
12513 m: &mut MtpModule,
12514 hidden: &[f32],
12515 next_token: u32,
12516 position: usize,
12517 want_logits: bool,
12518 ) -> Option<(Vec<f32>, Vec<f32>)> {
12519 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
12520 if std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
12521 || !crate::gpu::q1_force()
12522 || !crate::gpu::enabled_here()
12523 || self.attn_softcap > 0.0
12524 || self.attention_heads_per_layer.is_some()
12525 || m.kv.mode != crate::kv_cache::KvMode::F32
12526 || m.kv.o1.is_some()
12527 {
12528 return None;
12529 }
12530 let AttnKind::Full {
12531 wq,
12532 wk,
12533 wv,
12534 wo,
12535 q_norm,
12536 k_norm,
12537 output_gate,
12538 softplus_gate: None,
12539 bias: None,
12540 } = &m.layer.attn
12541 else {
12542 return None;
12543 };
12544 let FfnKind::Dense(d) = &m.layer.ffn else {
12545 return None;
12546 };
12547 if d.act != Act::Silu || !d.segs.is_empty() {
12548 return None;
12549 }
12550 let (pq, pk, pv, po) = (
12551 wq.q1_parts()?,
12552 wk.q1_parts()?,
12553 wv.q1_parts()?,
12554 wo.q1_parts()?,
12555 );
12556 let (g, u, dn) = (
12557 d.gate_proj.q1_parts()?,
12558 d.up_proj.q1_parts()?,
12559 d.down_proj.q1_parts()?,
12560 );
12561 let QTensor::Mapped { model, .. } = wq else {
12562 return None;
12563 };
12564 let model = model.clone();
12565 let lm = if want_logits {
12566 Some(self.weights.lm_head.q1_parts()?)
12567 } else {
12568 None
12569 };
12570 let dims = GraphDims {
12571 hidden: self.hidden_size,
12572 eps: self.rms_eps as f32,
12573 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12574 };
12575 let hs = self.hidden_size;
12578 let mut x = vec![0f32; hs];
12579 let mut graph = TokenGraph::new(&model, dims, &x)?;
12580 let mut folded = false;
12581 if let Some(eh) = m.eh_proj.q1_parts() {
12582 let e = self.embed_single(next_token);
12583 let mut cat = vec![0.0f32; 2 * hs];
12584 let (cat_e, cat_h) = cat.split_at_mut(hs);
12585 inference::rms_norm_into(&e, &m.enorm, self.rms_eps, self.norm_style, cat_e);
12586 inference::rms_norm_into(hidden, &m.hnorm, self.rms_eps, self.norm_style, cat_h);
12587 folded = graph.encode_input_proj(eh, &cat);
12588 }
12589 if !folded {
12590 x = self.mtp_block_input(m, hidden, next_token);
12591 graph = TokenGraph::new(&model, dims, &x)?;
12592 }
12593 spec_stamp("d.in");
12594 let l = AttnGpuLayer {
12595 attn_norm: &m.layer.input_norm,
12596 post_norm: &m.layer.post_norm,
12597 wq: pq,
12598 wk: pk,
12599 wv: pv,
12600 wo: po,
12601 ffn: MetalFfn::Dense {
12602 gate: g,
12603 up: u,
12604 down: dn,
12605 gelu: d.act == Act::Gelu,
12606 },
12607 };
12608 let (nh, nkv, hd, rd) = (
12609 self.num_heads,
12610 self.num_kv_heads,
12611 self.head_dim,
12612 self.rotary_dim,
12613 );
12614 let inv_freq = self.inv_freq.clone();
12615 {
12616 let cache = &m.kv;
12617 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12618 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12619 let cpu_stored = cpu_k[0].len() / hd;
12620 let p = AttnDeviceParams {
12621 kv_id: self.mtp_kv_id(),
12622 layer: Self::MTP_LAYER_BASE,
12623 nh,
12624 nkv,
12625 hd,
12626 rd,
12627 position,
12628 scale: self.attn_scale,
12629 eps: self.rms_eps as f32,
12630 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12631 late_qk_norm: self.qk_norm_after_rope,
12632 output_gate: *output_gate,
12633 q_norm: q_norm.as_deref(),
12634 k_norm: k_norm.as_deref(),
12635 inv_freq: &inv_freq,
12636 cpu_k,
12637 cpu_v,
12638 cpu_stored,
12639 cpu_gen: cache.generation(),
12640 o1: None,
12641 window: None,
12642 head_gate: None,
12643 };
12644 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
12645 return None;
12646 }
12647 }
12648 let draft_rows = if let Some(lm) = lm {
12654 self.draft_head_rows(lm.1)
12655 } else {
12656 0
12657 };
12658 if let Some(lm) = lm {
12659 if !graph.lm_head_ok(lm) {
12660 return None;
12661 }
12662 if draft_rows < lm.1 {
12663 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
12664 return None;
12665 }
12666 } else {
12667 graph.encode_lm_head(&m.final_norm, lm);
12668 }
12669 }
12670 spec_stamp("d.enc");
12671 if graph.sync_checked().is_err() {
12672 return None;
12673 }
12674 spec_stamp("d.gpu");
12675 let mut logits = Vec::new();
12676 if let Some(lm) = lm {
12677 let n_read = draft_rows.min(lm.1).min(self.vocab_size);
12678 logits = attention::take_buf(n_read);
12679 graph.read_logits(&mut logits);
12680 logits.resize(self.vocab_size, f32::NEG_INFINITY);
12682 }
12683 graph.finish(&mut x);
12684 let mut krow = attention::take_buf(nkv * hd);
12685 let mut vrow = attention::take_buf(nkv * hd);
12686 if crate::gpu_metal::kv_mirror_read_last(
12687 self.mtp_kv_id(),
12688 Self::MTP_LAYER_BASE,
12689 nkv,
12690 hd,
12691 &mut krow,
12692 &mut vrow,
12693 ) {
12694 m.kv.append(&krow, &vrow, &[]);
12695 }
12696 attention::recycle_buf(&mut krow);
12697 attention::recycle_buf(&mut vrow);
12698 spec_stamp("d.rd");
12699 Some((logits, x))
12700 }
12701
12702 fn mtp_chain_on() -> bool {
12715 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
12716 *ON.get_or_init(|| std::env::var("CMF_MTP_CHAIN").as_deref() != Ok("0"))
12717 }
12718
12719 #[cfg(target_os = "macos")]
12731 fn mtp_draft_chain_metal(
12732 &mut self,
12733 m: &mut MtpModule,
12734 hidden: &[f32],
12735 t_next: u32,
12736 position: usize,
12737 k: usize,
12738 ) -> Result<Vec<u32>, bool> {
12739 use crate::gpu_metal::{AttnDeviceParams, AttnGpuLayer, GraphDims, MetalFfn, TokenGraph};
12740 if k == 0
12741 || k > 64
12742 || !Self::mtp_chain_on()
12743 || std::env::var("CMF_MTP_GRAPH").as_deref() == Ok("0")
12744 || !crate::gpu::q1_force()
12745 || !crate::gpu::enabled_here()
12746 || self.attn_softcap > 0.0
12747 || self.attention_heads_per_layer.is_some()
12748 || m.kv.mode != crate::kv_cache::KvMode::F32
12749 || m.kv.o1.is_some()
12750 || self.dsv4.is_some()
12752 || self.dsv41.is_some()
12753 || self.qwen4_exp.is_some()
12754 || self.g3n.is_some()
12755 {
12756 return Err(false);
12757 }
12758 let AttnKind::Full {
12759 wq,
12760 wk,
12761 wv,
12762 wo,
12763 q_norm,
12764 k_norm,
12765 output_gate,
12766 softplus_gate: None,
12767 bias: None,
12768 } = &m.layer.attn
12769 else {
12770 return Err(false);
12771 };
12772 let FfnKind::Dense(d) = &m.layer.ffn else {
12773 return Err(false);
12774 };
12775 if d.act != Act::Silu || !d.segs.is_empty() {
12776 return Err(false);
12777 }
12778 let (Some(pq), Some(pk), Some(pv), Some(po)) =
12779 (wq.q1_parts(), wk.q1_parts(), wv.q1_parts(), wo.q1_parts())
12780 else {
12781 return Err(false);
12782 };
12783 let (Some(g), Some(u), Some(dn)) = (
12784 d.gate_proj.q1_parts(),
12785 d.up_proj.q1_parts(),
12786 d.down_proj.q1_parts(),
12787 ) else {
12788 return Err(false);
12789 };
12790 let (Some(eh), Some(lm)) = (m.eh_proj.q1_parts(), self.weights.lm_head.q1_parts()) else {
12791 return Err(false);
12792 };
12793 let QTensor::Mapped { model, .. } = wq else {
12794 return Err(false);
12795 };
12796 let model = model.clone();
12797 let QTensor::Mapped {
12800 model: em,
12801 idx: eidx,
12802 dtype: cortiq_core::TensorDtype::Q4TiledP,
12803 ..
12804 } = &self.weights.embed_tokens
12805 else {
12806 return Err(false);
12807 };
12808 if !std::sync::Arc::ptr_eq(em, &model)
12809 || crate::prism::is_inverse_embedding(&model, &model.tensors[*eidx].name)
12810 {
12811 return Err(false);
12812 }
12813 let embed = (
12814 *eidx,
12815 self.weights.embed_tokens.rows(),
12816 self.weights.embed_tokens.cols(),
12817 );
12818 if embed.2 != self.hidden_size || hidden.len() != self.hidden_size {
12819 return Err(false);
12820 }
12821 let dims = GraphDims {
12822 hidden: self.hidden_size,
12823 eps: self.rms_eps as f32,
12824 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12825 };
12826 let Some(mut graph) = TokenGraph::new(&model, dims, hidden) else {
12827 return Err(false);
12828 };
12829 if !graph.chain_embed_ok(embed) || !graph.lm_head_ok(lm) {
12830 return Err(false);
12831 }
12832 let l = AttnGpuLayer {
12833 attn_norm: &m.layer.input_norm,
12834 post_norm: &m.layer.post_norm,
12835 wq: pq,
12836 wk: pk,
12837 wv: pv,
12838 wo: po,
12839 ffn: MetalFfn::Dense {
12840 gate: g,
12841 up: u,
12842 down: dn,
12843 gelu: d.act == Act::Gelu,
12844 },
12845 };
12846 let (nh, nkv, hd, rd) = (
12847 self.num_heads,
12848 self.num_kv_heads,
12849 self.head_dim,
12850 self.rotary_dim,
12851 );
12852 let inv_freq = self.inv_freq.clone();
12853 let draft_rows = self.draft_head_rows(lm.1);
12854 let n_arg = draft_rows.min(lm.1).min(self.vocab_size);
12855 if n_arg == 0 {
12856 return Err(false);
12857 }
12858 let split = std::env::var("CMF_MTP_CHAIN_SPLIT").as_deref() == Ok("1");
12865 let t_chain = std::time::Instant::now();
12866 graph.chain_ids_init(t_next, k);
12867 let cpu_stored;
12868 {
12869 let cache = &m.kv;
12870 let cpu_k: Vec<&[f32]> = (0..nkv).map(|g| cache.head_keys(g)).collect();
12871 let cpu_v: Vec<&[f32]> = (0..nkv).map(|g| cache.head_values(g)).collect();
12872 cpu_stored = cpu_k[0].len() / hd;
12873 for j in 0..k {
12874 if !graph.encode_chain_input(
12875 embed,
12876 j as u32,
12877 &m.enorm,
12878 &m.hnorm,
12879 self.embed_multiplier,
12880 eh,
12881 ) {
12882 return Err(false);
12883 }
12884 let p = AttnDeviceParams {
12888 kv_id: self.mtp_kv_id(),
12889 layer: Self::MTP_LAYER_BASE,
12890 nh,
12891 nkv,
12892 hd,
12893 rd,
12894 position: position + j,
12895 scale: self.attn_scale,
12896 eps: self.rms_eps as f32,
12897 gemma: self.norm_style == cortiq_core::NormStyle::Gemma,
12898 late_qk_norm: self.qk_norm_after_rope,
12899 output_gate: *output_gate,
12900 q_norm: q_norm.as_deref(),
12901 k_norm: k_norm.as_deref(),
12902 inv_freq: &inv_freq,
12903 cpu_k: cpu_k.clone(),
12904 cpu_v: cpu_v.clone(),
12905 cpu_stored: cpu_stored + j,
12906 cpu_gen: cache.generation(),
12907 o1: None,
12908 window: None,
12909 head_gate: None,
12910 };
12911 if !graph.attn_device_ok(&l, &p) || !graph.encode_attn_device(&l, &p) {
12912 return Err(false);
12913 }
12914 if draft_rows < lm.1 {
12915 if !graph.encode_lm_head_part(&m.final_norm, lm, draft_rows) {
12916 return Err(false);
12917 }
12918 } else {
12919 graph.encode_lm_head(&m.final_norm, lm);
12920 }
12921 if !graph.encode_argmax(n_arg, j as u32 + 1) {
12922 return Err(false);
12923 }
12924 if split {
12925 graph.commit();
12928 }
12929 }
12930 }
12931 let t_enc = t_chain.elapsed();
12932 if graph.sync_checked().is_err() {
12933 return Err(true);
12934 }
12935 if std::env::var_os("CMF_GRAPH_SPEC_TIME").is_some() {
12936 eprintln!(
12937 "mtp-chain: encode {:.1} ms | wait {:.1} ms (k={k}, head rows {draft_rows}{})",
12938 t_enc.as_secs_f64() * 1e3,
12939 (t_chain.elapsed() - t_enc).as_secs_f64() * 1e3,
12940 if split { ", split" } else { "" }
12941 );
12942 }
12943 let mut ids = vec![0u32; k];
12944 if !graph.chain_ids_read(&mut ids) {
12945 return Err(true);
12946 }
12947 let mut kbuf = vec![0f32; k * nkv * hd];
12948 let mut vbuf = vec![0f32; k * nkv * hd];
12949 if !crate::gpu_metal::kv_mirror_read_rows(
12950 self.mtp_kv_id(),
12951 Self::MTP_LAYER_BASE,
12952 nkv,
12953 hd,
12954 cpu_stored,
12955 k,
12956 &mut kbuf,
12957 &mut vbuf,
12958 ) {
12959 return Err(true);
12960 }
12961 for r in 0..k {
12962 m.kv.append(
12963 &kbuf[r * nkv * hd..(r + 1) * nkv * hd],
12964 &vbuf[r * nkv * hd..(r + 1) * nkv * hd],
12965 &[],
12966 );
12967 }
12968 Ok(ids)
12969 }
12970
12971 fn try_batch_graph_wgpu(
12972 &self,
12973 hiddens: &mut [f32],
12974 positions: &[usize],
12975 k: usize,
12976 spec: Option<crate::gpu::SpecTail<'_>>,
12977 ) -> crate::gpu::BatchGraphOutcome {
12978 self.try_batch_graph_wgpu_prefix(hiddens, positions, k, spec, None)
12979 }
12980
12981 fn try_batch_graph_wgpu_prefix(
12986 &self,
12987 hiddens: &mut [f32],
12988 positions: &[usize],
12989 k: usize,
12990 spec: Option<crate::gpu::SpecTail<'_>>,
12991 layers_run: Option<&mut usize>,
12992 ) -> crate::gpu::BatchGraphOutcome {
12993 let graph_end = match self.mimo_moe.graph_prefix_end() {
12994 Some(end) if end < self.num_layers => {
12995 if layers_run.is_none() || spec.is_some() || end == 0 {
12996 return crate::gpu::BatchGraphOutcome::Declined;
12997 }
12998 end
12999 }
13000 _ => self.num_layers,
13001 };
13002 let _tb = std::time::Instant::now();
13003 let batch_debug = std::env::var_os("CMF_BATCH_DEBUG").is_some();
13004 if self.attn_softcap > 0.0 {
13005 return crate::gpu::BatchGraphOutcome::Declined; }
13007 if let Some(reason) = self.wgpu_graph_attn_decline() {
13010 self.note_graph_decline("wgpu batch graph", reason);
13011 return crate::gpu::BatchGraphOutcome::Declined;
13012 }
13013 let nh = self.num_heads;
13014 let (nkv, hd, rd) = self.layer_geom(0);
13015 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
13016 fn gw(t: &QTensor) -> Option<crate::gpu::GraphW<'_>> {
13017 if let Some((m, i, kind, rs)) = t
13018 .graph_weight()
13019 .or_else(|| t.graph_weight_descriptor())
13020 {
13021 let name = &m.tensors[i].name;
13022 let prism = if crate::prism::is_inverse_embedding(m, name) {
13023 crate::gpu::GraphPrismOp::InverseEmbedding
13024 } else if crate::prism::is_forward_weight(m, name) {
13025 crate::gpu::GraphPrismOp::Forward
13026 } else {
13027 crate::gpu::GraphPrismOp::None
13028 };
13029 return Some(crate::gpu::GraphW {
13030 idx: i,
13031 kind,
13032 row_scale: rs,
13033 data: &[],
13034 prism,
13035 affine: crate::prism::is_affine_target(m, name),
13036 });
13037 }
13038 if std::env::var_os("CMF_BATCH_DEBUG").is_some() {
13039 eprintln!(
13040 "batch graph: tensor has no graph descriptor/f32 fallback rows={} cols={}",
13041 t.rows(),
13042 t.cols()
13043 );
13044 }
13045 t.as_f32().map(|d| crate::gpu::GraphW {
13046 idx: 0,
13047 kind: 4,
13048 row_scale: &[],
13049 data: d,
13050 prism: crate::gpu::GraphPrismOp::None,
13051 affine: false,
13052 })
13053 }
13054 let built: Option<(
13055 Vec<crate::gpu::GraphLayer<'_>>,
13056 std::sync::Arc<cortiq_core::CmfModel>,
13057 )> = (|| {
13058 let mut layers = Vec::with_capacity(graph_end);
13059 let mut model = None;
13060 for li in 0..graph_end {
13061 let lw = &self.weights.layers[self.phys_layer(li)];
13062 let gffn = match &lw.ffn {
13069 FfnKind::Dense(d) if !d.segs.is_empty() => {
13070 if batch_debug {
13071 eprintln!("batch graph: dense segmented FFN at layer {li}");
13072 }
13073 return None;
13074 }
13075 FfnKind::Dense(d) => {
13076 let Some(act) = d.act.graph_act() else {
13077 if batch_debug {
13078 eprintln!(
13079 "batch graph: dense FFN activation {:?} without a graph kernel at layer {li}",
13080 d.act
13081 );
13082 }
13083 return None;
13084 };
13085 crate::gpu::GraphFfn::Dense {
13086 gate: gw(&d.gate_proj)?,
13087 up: gw(&d.up_proj)?,
13088 down: gw(&d.down_proj)?,
13089 act,
13090 }
13091 }
13092 FfnKind::Moe(m) => {
13093 if m.route_tau.is_some() || m.mask.is_some() {
13100 return None;
13101 }
13102 let shared = m.shared.as_ref();
13106 let has_shared = shared.is_some();
13107 let shared_gated = matches!(shared, Some((_, Some(_))));
13108 let sgate = match shared {
13109 Some((_, Some(sg))) => gw(sg)?,
13110 _ => gw(&m.router)?,
13114 };
13115 let router = gw(&m.router)?;
13116 if router.prism != crate::gpu::GraphPrismOp::None
13122 || router.affine
13123 || sgate.prism != crate::gpu::GraphPrismOp::None
13124 || sgate.affine
13125 {
13126 return None;
13127 }
13128 let inter = m.experts.first()?.gate_proj.rows();
13129 let mut experts = Vec::with_capacity(m.experts.len() + 1);
13130 let mut q4tp: Option<bool> = None;
13131 let mut gu_q2: Option<bool> = None;
13132 for e in m.experts.iter().chain(shared.map(|(se, _)| se)) {
13133 if !matches!(e.act, Act::Silu)
13134 || e.gate_proj.rows() != inter
13135 || e.up_proj.rows() != inter
13136 {
13137 return None;
13138 }
13139 let (mm, gi, ui, di, is_p, is_q2) = match e.gate_proj.mapped_q4t() {
13143 Some((mm, gi)) => (
13144 mm,
13145 gi,
13146 e.up_proj.mapped_q4t()?.1,
13147 e.down_proj.mapped_q4t()?.1,
13148 false,
13149 false,
13150 ),
13151 None => match e.gate_proj.mapped_q2tp() {
13152 Some((mm, gi)) => (
13153 mm,
13154 gi,
13155 e.up_proj.mapped_q2tp()?.1,
13156 e.down_proj.mapped_q4tp()?.1,
13157 true,
13158 true,
13159 ),
13160 None => {
13161 let (mm, gi) = e.gate_proj.mapped_q4tp()?;
13162 (
13163 mm,
13164 gi,
13165 e.up_proj.mapped_q4tp()?.1,
13166 e.down_proj.mapped_q4tp()?.1,
13167 true,
13168 false,
13169 )
13170 }
13171 },
13172 };
13173 if *q4tp.get_or_insert(is_p) != is_p
13174 || *gu_q2.get_or_insert(is_q2) != is_q2
13175 {
13176 return None;
13177 }
13178 if [gi, ui, di].into_iter().any(|idx| {
13179 mm.tensors
13180 .get(idx)
13181 .is_some_and(|t| {
13182 crate::prism::is_forward_weight(mm, &t.name)
13183 || crate::prism::is_affine_target(mm, &t.name)
13184 })
13185 }) {
13186 return None;
13187 }
13188 model.get_or_insert_with(|| mm.clone());
13189 experts.push((gi, ui, di));
13190 }
13191 crate::gpu::GraphFfn::Moe {
13192 router,
13193 shared_gate: sgate,
13194 experts,
13195 n_exp: m.experts.len(),
13196 top_k: m.top_k,
13197 inter,
13198 norm_topk: m.norm_topk_prob,
13199 q4tp: q4tp?,
13200 gu_q2: gu_q2.unwrap_or(false),
13201 sigmoid: m.router_sigmoid,
13202 bias: m.expert_bias.as_deref(),
13203 has_shared,
13204 shared_gated,
13205 route_scale: m.routed_scaling,
13206 }
13207 }
13208 _ => return None,
13209 };
13210 let attn = match &lw.attn {
13211 AttnKind::Full {
13212 wq,
13213 wk,
13214 wv,
13215 wo,
13216 q_norm,
13217 k_norm,
13218 output_gate,
13219 softplus_gate,
13220 bias,
13221 } => {
13222 if softplus_gate.is_some() || self.attention_heads_per_layer.is_some() {
13223 if batch_debug {
13224 eprintln!(
13225 "batch graph: unsupported Full attention gate at layer {li} softplus={} heads={}",
13226 softplus_gate.is_some(),
13227 self.attention_heads_per_layer.is_some()
13228 );
13229 }
13230 return None;
13231 }
13232 let (m, _, _, _) = wq
13233 .graph_weight()
13234 .or_else(|| wq.graph_weight_descriptor())?;
13235 model = Some(m.clone());
13236 crate::gpu::GraphAttn::Full {
13237 wq: gw(wq)?,
13238 wk: gw(wk)?,
13239 wv: gw(wv)?,
13240 wo: gw(wo)?,
13241 q_norm: q_norm.as_deref(),
13242 k_norm: k_norm.as_deref(),
13243 late_qk_norm: self.qk_norm_after_rope,
13244 bias: bias
13245 .as_ref()
13246 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
13247 output_gate: *output_gate,
13248 cpu_k: self.kv_cache.layers[li].k_heads(),
13249 cpu_v: self.kv_cache.layers[li].v_heads(),
13250 cpu_base: self.kv_cache.layers[li].base(),
13251 geom: self.graph_attn_geom(li),
13252 head_gate: None,
13255 }
13256 }
13257 AttnKind::LinearGdn(w) => {
13258 let Some(cfg) = self.gdn_cfg else {
13259 if batch_debug {
13260 eprintln!("batch graph: no GDN config at layer {li}");
13261 }
13262 return None;
13263 };
13264 let (m, _, _, _) = w
13265 .in_proj_qkv
13266 .graph_weight()
13267 .or_else(|| w.in_proj_qkv.graph_weight_descriptor())?;
13268 model = Some(m.clone());
13269 crate::gpu::GraphAttn::Gdn {
13270 qkv: gw(&w.in_proj_qkv)?,
13271 z: gw(&w.in_proj_z)?,
13272 a: gw(&w.in_proj_a)?,
13273 b: gw(&w.in_proj_b)?,
13274 out: gw(&w.out_proj)?,
13275 conv1d: &w.conv1d,
13276 a_log: &w.a_log,
13277 dt_bias: &w.dt_bias,
13278 norm: &w.norm,
13279 nv: cfg.num_v_heads,
13280 nk: cfg.num_k_heads,
13281 dk: cfg.key_head_dim,
13282 dv: cfg.value_head_dim,
13283 kk: cfg.conv_kernel,
13284 cpu_state: &self.kv_cache.layers[self.phys_layer(li)].linear_state,
13285 }
13286 }
13287 _ => return None,
13288 };
13289 layers.push(crate::gpu::GraphLayer {
13290 input_norm: &lw.input_norm,
13291 attn,
13292 post_norm: &lw.post_norm,
13293 ffn: gffn,
13294 });
13295 }
13296 Some((layers, model?))
13297 })();
13298 let Some((layers, model)) = built else {
13299 {
13300 use std::sync::atomic::{AtomicBool, Ordering};
13301 static SAID: AtomicBool = AtomicBool::new(false);
13302 if !SAID.swap(true, Ordering::Relaxed) {
13303 tracing::warn!("batch graph: BUILDER refused (layer weights/kinds)");
13304 }
13305 }
13306 return crate::gpu::BatchGraphOutcome::Declined;
13307 };
13308 if std::env::var("CMF_GRAPH_SPEC_TIME").is_ok() {
13309 eprintln!("batch-build: {:.1} ms", _tb.elapsed().as_secs_f64() * 1e3);
13310 }
13311 crate::gpu::forward_batch_graph(
13312 &model,
13313 self.graph_kv_id,
13314 &layers,
13315 &self.inv_freq,
13316 hiddens,
13317 nh,
13318 nkv,
13319 hd,
13320 rd,
13321 self.hidden_size,
13322 self.intermediate_size,
13323 positions,
13324 self.kv_cache.max_seq_len,
13325 gemma,
13326 self.rms_eps as f32,
13327 self.attn_scale,
13328 k,
13329 &(0..graph_end)
13330 .map(|li| self.kv_cache.layers[self.phys_layer(li)].o1_views())
13331 .collect::<Vec<_>>(),
13332 self.o1_epoch,
13333 spec,
13334 layers_run,
13335 )
13336 }
13337
13338 fn draft_probe() -> bool {
13342 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13343 *ON.get_or_init(|| std::env::var("CMF_DSV4_DRAFT_PROBE").is_ok_and(|v| v != "0"))
13344 }
13345
13346 #[cfg(feature = "gpu")]
13358 fn dsv4_spec_on() -> bool {
13359 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13360 *ON.get_or_init(|| {
13361 if let Ok(v) = std::env::var("CMF_DSV4_SPEC_RUN") {
13365 return v != "0";
13366 }
13367 std::env::var("CMF_DSV4_SPEC")
13374 .map(|v| v != "0")
13375 .unwrap_or_else(|_| {
13376 crate::gpu_wgpu::DRAFT_RESERVE.load(std::sync::atomic::Ordering::Relaxed) > 0
13377 })
13378 })
13379 }
13380
13381 #[cfg(feature = "gpu")]
13388 fn dsv4_spec_step(
13389 &mut self,
13390 tip_token: u32,
13391 t_next: u32,
13392 next_pos: usize,
13393 max_extra: usize,
13394 drafted: &mut usize,
13395 accepted_ctr: &mut usize,
13396 ) -> Option<(Vec<u32>, usize)> {
13397 let t_all = std::time::Instant::now();
13398 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
13399 thread_local! {
13400 static LAST: std::cell::Cell<Option<std::time::Instant>> =
13401 const { std::cell::Cell::new(None) };
13402 }
13403 LAST.with(|l| {
13404 if let Some(prev) = l.get() {
13405 eprintln!(
13406 "между раундами {:.1} мс",
13407 prev.elapsed().as_secs_f64() * 1e3
13408 );
13409 }
13410 l.set(Some(std::time::Instant::now()));
13411 });
13412 }
13413 if std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13414 eprintln!("spec_step: вход pos={next_pos}");
13415 }
13416 let n_layers = self.dsv4.as_ref().map(|b| b.1.len())?;
13417 let cfg = self.dsv4.as_ref().map(|b| b.2)?;
13418 if self.dspark.is_none() {
13420 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13421 if t.is_empty() {
13422 return None;
13423 }
13424 crate::dsv4::dspark_arm(&t, cfg.dim);
13425 self.dspark = Some(crate::dsv4::DsparkState::new(
13426 self.dsv4_mtp.len(),
13427 &cfg,
13428 t.len(),
13429 ));
13430 }
13431 let targets = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13432 let pack = crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg);
13433 if pack.is_none() && std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok() {
13434 eprintln!("spec_step: пак не построился (targets {targets:?})");
13435 }
13436 let pack = pack?;
13437 let block = crate::dsv4::dspark_block();
13438 let b_box = self.dsv4.as_mut()?;
13439 let (g, layers, st) = (&b_box.0, &b_box.1, &mut b_box.3);
13440 let ds = self.dspark.as_mut()?;
13441 let dbg = std::env::var("CMF_DSV4_SPEC_DEBUG").is_ok();
13444 if !crate::dsv4::dspark_take(&mut ds.main_hidden) && !ds.have_hidden {
13445 if dbg {
13446 eprintln!("spec_step: нет захвата");
13447 }
13448 return None;
13449 }
13450 ds.have_hidden = true;
13451 let tip_pos = next_pos.checked_sub(1)?;
13452 let draft_started = std::time::Instant::now();
13453 let mut conf = Vec::new();
13454 let props = crate::dsv4::dspark_draft_gpu(
13455 g,
13456 &self.dsv4_mtp,
13457 &cfg,
13458 ds,
13459 pack,
13460 st.kv_id,
13461 tip_token,
13462 tip_pos,
13463 self.pool.as_deref(),
13464 &mut conf,
13465 );
13466 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
13467 *drafted += block;
13468 if props.is_empty() || props[0] != t_next {
13469 if dbg {
13470 eprintln!(
13471 "spec_step: черновик {} (props0={:?} t_next={t_next})",
13472 if props.is_empty() {
13473 "пуст"
13474 } else {
13475 "мимо"
13476 },
13477 props.first()
13478 );
13479 }
13480 return None;
13481 }
13482 let mut k_verify = crate::dsv4::dspark_verify_k()
13489 .min(props.len())
13490 .min(max_extra.saturating_add(1));
13491 let conf_min = {
13497 static M: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
13498 *M.get_or_init(|| {
13499 std::env::var("CMF_DSPARK_CONF_MIN")
13500 .ok()
13501 .and_then(|v| v.parse().ok())
13502 .unwrap_or(0.0)
13503 })
13504 };
13505 if conf_min > 0.0 && conf.len() >= props.len() {
13506 let mut keep = 1usize;
13507 while keep < k_verify && conf.get(keep).copied().unwrap_or(0.0) >= conf_min {
13508 keep += 1;
13509 }
13510 k_verify = k_verify.min(keep.max(2));
13511 }
13512 if k_verify < 2 {
13513 return None;
13514 }
13515 let mut fed = Vec::with_capacity(k_verify);
13516 fed.push(t_next);
13517 fed.extend_from_slice(&props[1..k_verify]);
13518 let mut argmax = Vec::new();
13519 let mut logits_all = Vec::new();
13520 let mut walked = Vec::new();
13521 let txn = crate::dsv4::dsv4_verify_chunk(
13522 g,
13523 layers,
13524 &cfg,
13525 st,
13526 &fed,
13527 next_pos,
13528 &self.inv_freq,
13529 self.pool.as_deref(),
13530 &targets,
13531 &mut argmax,
13532 &mut logits_all,
13533 &mut walked,
13534 );
13535 if txn.is_none() && dbg {
13536 eprintln!("spec_step: verify отказал");
13537 }
13538 let txn = txn?;
13539 let spec_gpu_end = txn.gpu_end;
13540 let b = fed.len();
13541 let mut accepted = 1usize;
13542 while accepted < b && fed[accepted] == argmax[accepted - 1] {
13543 accepted += 1;
13544 }
13545 if std::env::var("CMF_DSV4_SPEC_FORCE_REJECT").is_ok_and(|v| v != "0") {
13550 accepted = 1;
13551 }
13552 if std::env::var("CMF_DSV4_SPEC_TRACE").is_ok() {
13553 eprintln!("spec@{next_pos}: fed={fed:?} argmax={argmax:?} accepted={accepted}");
13554 }
13555 let t_fin = std::time::Instant::now();
13556 if !crate::dsv4::dsv4_spec_finish(
13557 g,
13558 layers,
13559 &cfg,
13560 st,
13561 txn,
13562 accepted,
13563 &fed,
13564 &self.inv_freq,
13565 self.pool.as_deref(),
13566 ) {
13567 tracing::warn!("dsv4: спекулятивный откат не удался — состояние подозрительно");
13568 return None;
13569 }
13570 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
13571 eprintln!(
13572 "finish(k={accepted}): {:.1} мс",
13573 t_fin.elapsed().as_secs_f64() * 1e3
13574 );
13575 }
13576 *accepted_ctr += accepted - 1;
13577 let (hc, dim) = (cfg.hc_mult, cfg.dim);
13582 let dev_caps: Vec<usize> = targets
13587 .iter()
13588 .copied()
13589 .filter(|&t| t < spec_gpu_end)
13590 .collect();
13591 let mut caps_all = vec![0.0f32; dev_caps.len() * b * hc * dim];
13592 if !crate::gpu_wgpu::dsv4_spec_cap_read_all(b, dev_caps.len(), hc * dim, &mut caps_all) {
13593 return None;
13594 }
13595 for t in 0..accepted {
13596 let tip = t + 1 == accepted;
13597 for (slot, &tl) in targets.iter().enumerate() {
13598 if let Some(di) = dev_caps.iter().position(|&d| d == tl) {
13599 let lo = (di * b + t) * hc * dim;
13600 crate::dsv4::dspark_capture(
13601 &caps_all[lo..lo + hc * dim],
13602 &cfg,
13603 slot,
13604 &mut ds.main_hidden,
13605 );
13606 } else if tip
13607 && crate::dsv4::dspark_peek_slot(slot, dim, {
13608 let lo = slot * dim;
13609 &mut ds.main_hidden[lo..lo + dim]
13610 })
13611 {
13612 } else {
13617 crate::dsv4::dspark_capture(
13621 &walked[t * hc * dim..(t + 1) * hc * dim],
13622 &cfg,
13623 slot,
13624 &mut ds.main_hidden,
13625 );
13626 }
13627 }
13628 crate::dsv4::dspark_ring_append(
13629 g,
13630 &self.dsv4_mtp,
13631 &cfg,
13632 ds,
13633 next_pos + t,
13634 self.pool.as_deref(),
13635 );
13636 }
13637 let row = logits_all[(accepted - 1) * cfg.vocab..accepted * cfg.vocab].to_vec();
13638 self.graph_logits = Some(row);
13639 if std::env::var("CMF_DSV4_TRUNK_PICK_DUMP").is_ok() {
13644 crate::dsv4::trunk_freq_note(&crate::dsv4::pick_tally_take());
13645 crate::dsv4::pick_tally_arm();
13646 }
13647 if std::env::var("CMF_DSV4_SPEC_TIME").is_ok() {
13648 eprintln!(
13649 "spec_step total {:.1} мс (k={accepted})",
13650 t_all.elapsed().as_secs_f64() * 1e3
13651 );
13652 }
13653 Some((fed[1..accepted].to_vec(), next_pos + accepted))
13654 }
13655
13656 fn dspark_probe(&mut self, position: usize, token_id: u32) {
13657 if self.dsv4_mtp.is_empty() || !Self::draft_probe() {
13658 return;
13659 }
13660 let trunk_now = crate::dsv4::pick_tally_take();
13662 crate::dsv4::trunk_freq_note(&trunk_now);
13663 if !trunk_now.is_empty() {
13664 self.dspark_trunk_picks.push(trunk_now);
13665 let keep = crate::dsv4::dspark_block();
13666 if self.dspark_trunk_picks.len() > keep {
13667 self.dspark_trunk_picks.remove(0);
13668 }
13669 }
13670 for p in std::mem::take(&mut self.dspark_pending) {
13673 let Some(i) = position.checked_sub(p.0 + 1) else {
13674 continue;
13675 };
13676 let mut p = p;
13677 if i < p.1.len() {
13678 if p.2 && p.1[i] == token_id {
13679 p.3 = i + 1;
13680 } else {
13681 p.2 = false;
13682 }
13683 if i + 1 < p.1.len() {
13684 self.dspark_pending.push(p);
13685 continue;
13686 }
13687 }
13688 self.dspark_hist.push(p.3);
13689 self.dspark_real.push(token_id);
13690 }
13691 let Some(b) = &mut self.dsv4 else { return };
13692 let (g, layers, cfg) = (&b.0, &b.1, b.2);
13693 let n_layers = layers.len();
13694 if self.dspark.is_none() {
13695 let t = crate::dsv4::dspark_targets(&self.dsv4_mtp, &cfg, n_layers);
13696 if t.is_empty() {
13697 return;
13698 }
13699 eprintln!(
13700 "DSpark: захват со слоёв {t:?}, блок {}",
13701 crate::dsv4::dspark_block()
13702 );
13703 crate::dsv4::dspark_arm(&t, cfg.dim);
13704 self.dspark = Some(crate::dsv4::DsparkState::new(
13705 self.dsv4_mtp.len(),
13706 &cfg,
13707 t.len(),
13708 ));
13709 }
13710 let ds = self.dspark.as_mut().unwrap();
13711 if !crate::dsv4::dspark_take(&mut ds.main_hidden) {
13712 return; }
13714 let mut conf = Vec::new();
13715 crate::dsv4::pick_tally_arm();
13716 let draft_started = std::time::Instant::now();
13721 #[cfg(feature = "gpu")]
13722 let gpu_draft = crate::dsv4::dspark_gpu_on();
13723 #[cfg(not(feature = "gpu"))]
13724 let gpu_draft = false;
13725 let props = if gpu_draft {
13726 #[cfg(feature = "gpu")]
13727 {
13728 let kv_id = b.3.kv_id;
13729 match crate::dsv4::dspark_pack_get(&self.dsv4_mtp, &cfg) {
13730 Some(pk) => crate::dsv4::dspark_draft_gpu(
13731 g,
13732 &self.dsv4_mtp,
13733 &cfg,
13734 ds,
13735 pk,
13736 kv_id,
13737 token_id,
13738 position,
13739 self.pool.as_deref(),
13740 &mut conf,
13741 ),
13742 None => Vec::new(),
13743 }
13744 }
13745 #[cfg(not(feature = "gpu"))]
13746 Vec::new()
13747 } else {
13748 crate::gpu::cpu_scope(|| {
13749 crate::dsv4::dspark_draft(
13750 g,
13751 &self.dsv4_mtp,
13752 &cfg,
13753 ds,
13754 token_id,
13755 position,
13756 self.pool.as_deref(),
13757 &mut conf,
13758 )
13759 })
13760 };
13761 self.dspark_draft_ns += draft_started.elapsed().as_nanos();
13762 let draft_picks = crate::dsv4::pick_tally_take();
13763 crate::dsv4::dspark_freq_note(&draft_picks);
13764 crate::dsv4::pick_tally_arm();
13767 if !props.is_empty() {
13768 let (tu, tt) = {
13772 let flat: Vec<(usize, Vec<usize>)> = self
13773 .dspark_trunk_picks
13774 .iter()
13775 .flat_map(|v| v.iter().cloned())
13776 .collect();
13777 let mut per: std::collections::HashMap<usize, Vec<usize>> =
13779 std::collections::HashMap::new();
13780 for (li, picks) in flat {
13781 per.entry(li).or_default().extend(picks);
13782 }
13783 let n = per.len().max(1);
13784 let mut u = 0usize;
13785 let mut t = 0usize;
13786 for (_, v) in per {
13787 t += v.len();
13788 u += v.iter().collect::<std::collections::HashSet<_>>().len();
13789 }
13790 (u / n, t / n)
13791 };
13792 let (du, dt) = crate::dsv4::tally_unique(&draft_picks);
13793 self.dspark_exp.push((tu, tt, du, dt));
13794 self.dspark_pending.push((position, props, true, 0));
13795 }
13796 if self.dspark_hist.len() >= 8 && self.dspark_hist.len() % 8 == 0 {
13797 let n = self.dspark_hist.len() as f32;
13798 let mean: f32 = self.dspark_hist.iter().sum::<usize>() as f32 / n;
13799 let block = crate::dsv4::dspark_block();
13800 let mut at = vec![0usize; block + 1];
13801 for &k in &self.dspark_hist {
13802 at[k] += 1;
13803 }
13804 let mut surv = Vec::with_capacity(block);
13806 for i in 1..=block {
13807 let k = at[i..].iter().sum::<usize>() as f32 / n;
13808 surv.push(format!("{k:.2}"));
13809 }
13810 let distinct = self
13811 .dspark_real
13812 .iter()
13813 .collect::<std::collections::HashSet<_>>()
13814 .len();
13815 let (tu, tt, du, dt) = self.dspark_exp.iter().fold((0, 0, 0, 0), |a, b| {
13816 (a.0 + b.0, a.1 + b.1, a.2 + b.2, a.3 + b.3)
13817 });
13818 let m = self.dspark_exp.len().max(1);
13819 eprintln!(
13820 "DSpark: черновиков {}, принято в среднем {mean:.2} из {block} \
13821 (токенов за проход {:.2}), распределение {at:?}, выживание [{}]",
13822 self.dspark_hist.len(),
13823 mean + 1.0,
13824 surv.join(" ")
13825 );
13826 eprintln!(
13827 "DSpark: разных токенов {distinct} из {} (вырожденность), \
13828 эксперты ствол {}/{} на слой за {block} токенов, \
13829 черновик {}/{} за блок, draft {:.2} мс/блок",
13830 self.dspark_real.len(),
13831 tu / m,
13832 tt / m,
13833 du / m,
13834 dt / m,
13835 self.dspark_draft_ns as f64 / self.dspark_exp.len().max(1) as f64 / 1e6
13836 );
13837 }
13838 }
13839
13840 fn forward_layers_upto(
13841 &mut self,
13842 hidden: &[f32],
13843 position: usize,
13844 task_mask: Option<&TaskMask>,
13845 upto: Option<usize>,
13846 ) -> Vec<f32> {
13847 if let Some(plan) = self.gpu_plan.clone() {
13853 if upto.is_none() && plan.len() > 1 {
13854 let mut h = hidden.to_vec();
13855 for &(dev, from, upto_incl) in plan.iter() {
13856 h = crate::gpu::with_device(dev, || {
13857 self.forward_layers_span(&h, position, task_mask, from, Some(upto_incl))
13858 });
13859 }
13860 return h;
13861 }
13862 }
13863 self.forward_layers_span(hidden, position, task_mask, 0, upto)
13864 }
13865
13866 pub fn set_gpu_plan(&mut self, devices: Option<&[usize]>) -> Result<(), String> {
13871 self.set_gpu_plan_at(devices, None)
13872 }
13873
13874 pub fn set_gpu_plan_at(
13878 &mut self,
13879 devices: Option<&[usize]>,
13880 at: Option<usize>,
13881 ) -> Result<(), String> {
13882 let Some(devs) = devices.filter(|d| d.len() > 1) else {
13883 self.gpu_plan = None;
13884 return Ok(());
13885 };
13886 self.split_supported()?;
13887 let n = self.num_layers;
13888 if devs.len() > n {
13889 return Err(format!("{} devices for {n} layers", devs.len()));
13890 }
13891 if let Some(k) = at {
13892 if k == 0 || k >= n {
13893 return Err(format!("split at {k}: the model has {n} layers"));
13894 }
13895 if devs.len() == 2 {
13896 self.gpu_plan = Some(std::sync::Arc::new(vec![
13897 (devs[0], 0, k - 1),
13898 (devs[1], k, n - 1),
13899 ]));
13900 return Ok(());
13901 }
13902 return Err(format!(
13903 "an explicit split point takes exactly 2 devices, got {}",
13904 devs.len()
13905 ));
13906 }
13907 let per = n.div_ceil(devs.len());
13908 let mut plan = Vec::with_capacity(devs.len());
13909 let mut from = 0usize;
13910 for &d in devs {
13911 if from >= n {
13912 break;
13913 }
13914 let upto = (from + per - 1).min(n - 1);
13915 plan.push((d, from, upto));
13916 from = upto + 1;
13917 }
13918 self.gpu_plan = Some(std::sync::Arc::new(plan));
13919 Ok(())
13920 }
13921
13922 pub fn gpu_plan(&self) -> Option<Vec<(usize, usize, usize)>> {
13924 self.gpu_plan.as_ref().map(|p| p.as_ref().clone())
13925 }
13926
13927 fn embryo_qtensor_f32(t: &QTensor) -> Vec<f32> {
13933 if let Some(x) = t.as_f32() {
13934 return x.to_vec();
13935 }
13936 let mut out = vec![0.0; t.rows() * t.cols()];
13937 for r in 0..t.rows() {
13938 t.row_f32(r, &mut out[r * t.cols()..(r + 1) * t.cols()]);
13939 }
13940 out
13941 }
13942
13943 fn embryo_resident_eligible(&self) -> bool {
13944 if (self.vmf_cfg.is_none() && self.gdn_cfg.is_none())
13947 || self.num_layers != self.physical_layers
13948 || self.loop_final_norm
13949 || self.weights.layers.len() != self.num_layers
13950 || self.head_clusters.is_none()
13951 || self.final_softcap.is_some()
13952 || self.logit_multiplier.is_some()
13953 || self.attn_softcap != 0.0
13954 || self.mtp.is_some()
13955 || self.g3n.is_some()
13956 || self.dsv4.is_some()
13957 || self.dsv41.is_some()
13958 || self.qwen4_exp.is_some()
13959 || self.dyn_router.is_some()
13966 || self.dyn_phi_layer.is_some()
13967 || self.dyn_blend_loaded
13968 || self.o1_cfg.is_some()
13969 || self.swa.is_some()
13970 || self.sliding_layers.is_some()
13971 || self.global_attn.is_some()
13972 || self.attention_heads_per_layer.is_some()
13973 || self.attn_v_norm
13974 || self
13975 .kv_cache
13976 .layers
13977 .iter()
13978 .any(|l| l.mode != crate::kv_cache::KvMode::F32)
13979 || self.rope_scale != 1.0
13980 || self.rope_scale_local != 1.0
13981 || self.attn_scale != 1.0 / (self.head_dim as f32).sqrt()
13982 || self.hidden_size == 0
13983 || self.hidden_size > 1024
13984 || self.intermediate_size > 1024
13985 || self.num_heads == 0
13986 || self.num_kv_heads == 0
13987 || self.num_heads % self.num_kv_heads != 0
13988 || self.num_heads.saturating_mul(self.head_dim) > 1024
13989 || self.num_kv_heads.saturating_mul(self.head_dim) > 1024
13990 || self.vocab_size == 0
13991 || self.kv_cache.max_seq_len == 0
13992 || self.rotary_dim == 0
13993 || self.rotary_dim > self.head_dim
13994 || self.rotary_dim % 2 != 0
13995 || self.inv_freq.len() < self.rotary_dim / 2
13996 {
13997 return false;
13998 }
13999 if self.weights.lm_head.as_f32().is_none()
14010 || self.weights.embed_tokens.as_f32().is_none()
14011 || self.weights.lm_head.rows() < self.vocab_size
14012 || self.weights.lm_head.cols() != self.hidden_size
14013 || self.weights.embed_tokens.rows() < self.vocab_size
14014 || self.weights.embed_tokens.cols() != self.hidden_size
14015 || self.weights.final_norm.len() != self.hidden_size
14016 {
14017 return false;
14018 }
14019 if let Some(cfg) = self.vmf_cfg {
14020 if cfg.num_heads.saturating_mul(cfg.nphase) > 1024
14021 || cfg.num_heads.saturating_mul(cfg.nphase.saturating_add(1)) > 1024
14022 || cfg.num_heads.saturating_mul(cfg.value_head_dim) > 1024
14023 || cfg.state_len() == 0
14024 {
14025 return false;
14026 }
14027 }
14028 if let Some(g) = self.gdn_cfg {
14029 if g.num_v_heads == 0
14034 || g.num_k_heads == 0
14035 || g.num_v_heads % g.num_k_heads != 0
14036 || g.key_head_dim == 0
14037 || g.key_head_dim > 128
14038 || g.value_head_dim == 0
14039 || g.value_head_dim > 256
14040 || g.value_head_dim % 4 != 0
14041 || g.conv_kernel == 0
14042 || g.num_v_heads > 512
14043 || g.num_v_heads.saturating_mul(g.value_head_dim) > 1024
14044 || g.conv_dim() > 2048
14045 || g.conv_dim() % 4 != 0
14046 || g.hidden_size != self.hidden_size
14047 || g.output_gate_sigmoid
14048 || g.rms_eps != self.rms_eps
14049 || g.state_len() == 0
14050 {
14051 return false;
14052 }
14053 }
14054 let mut full_seen = false;
14055 for lw in &self.weights.layers {
14056 if lw.attn_out_norm.is_some() || lw.ffn_out_norm.is_some() || lw.layer_scale.is_some() {
14057 return false;
14058 }
14059 match &lw.attn {
14060 AttnKind::LinearGdn(w) => {
14061 let Some(g) = self.gdn_cfg else {
14062 return false;
14063 };
14064 let (nv, dv, kk) = (g.num_v_heads, g.value_head_dim, g.conv_kernel);
14065 if w.in_proj_qkv.rows() != g.conv_dim()
14066 || w.in_proj_qkv.cols() != self.hidden_size
14067 || w.in_proj_qkv.as_f32().is_none()
14068 || w.in_proj_z.rows() != nv * dv
14069 || w.in_proj_z.cols() != self.hidden_size
14070 || w.in_proj_z.as_f32().is_none()
14071 || w.in_proj_a.rows() != nv
14072 || w.in_proj_a.cols() != self.hidden_size
14073 || w.in_proj_a.as_f32().is_none()
14074 || w.in_proj_b.rows() != nv
14075 || w.in_proj_b.cols() != self.hidden_size
14076 || w.in_proj_b.as_f32().is_none()
14077 || w.conv1d.len() != g.conv_dim() * kk
14078 || w.a_log.len() != nv
14079 || w.dt_bias.len() != nv
14080 || w.norm.len() != dv
14081 || w.out_proj.rows() != self.hidden_size
14082 || w.out_proj.cols() != nv * dv
14083 || w.out_proj.as_f32().is_none()
14084 {
14085 return false;
14086 }
14087 }
14088 AttnKind::Linear(w) => {
14089 let Some(cfg) = self.vmf_cfg else {
14090 return false;
14091 };
14092 if w.thq.rows() != cfg.num_heads * cfg.nphase
14093 || w.thq.cols() != self.hidden_size
14094 || w.thq.as_f32().is_none()
14095 || w.thk.rows() != cfg.num_heads * cfg.nphase
14096 || w.thk.cols() != self.hidden_size
14097 || w.thk.as_f32().is_none()
14098 || w.v_proj.rows() != cfg.num_heads * cfg.value_head_dim
14099 || w.v_proj.cols() != self.hidden_size
14100 || w.v_proj.as_f32().is_none()
14101 || w.out_proj.rows() != self.hidden_size
14102 || w.out_proj.cols() != cfg.num_heads * cfg.value_head_dim
14103 || w.out_proj.as_f32().is_none()
14104 || w.decay.len() != cfg.num_heads * 2 * cfg.nphase
14105 {
14106 return false;
14107 }
14108 if let Some((kg, kb)) = &w.k_gate {
14109 if kg.rows() != cfg.num_heads
14110 || kg.cols() != self.hidden_size
14111 || kg.as_f32().is_none()
14112 || kb.len() != cfg.num_heads
14113 {
14114 return false;
14115 }
14116 }
14117 if let Some(conv) = &w.conv {
14118 if conv.len() % self.hidden_size != 0 || conv.len() / self.hidden_size < 2 {
14119 return false;
14120 }
14121 }
14122 }
14123 AttnKind::Full {
14124 wq,
14125 wk,
14126 wv,
14127 wo,
14128 q_norm,
14129 k_norm,
14130 output_gate,
14131 softplus_gate,
14132 bias,
14133 } => {
14134 if full_seen
14135 || q_norm.is_some()
14136 || k_norm.is_some()
14137 || *output_gate
14138 || softplus_gate.is_some()
14139 || bias.is_some()
14140 || wq.as_f32().is_none()
14141 || wk.as_f32().is_none()
14142 || wv.as_f32().is_none()
14143 || wo.as_f32().is_none()
14144 || wq.rows() != self.num_heads * self.head_dim
14145 || wk.rows() != self.num_kv_heads * self.head_dim
14146 || wv.rows() != self.num_kv_heads * self.head_dim
14147 || wq.cols() != self.hidden_size
14148 || wk.cols() != self.hidden_size
14149 || wv.cols() != self.hidden_size
14150 || wo.rows() != self.hidden_size
14151 || wo.cols() != self.num_heads * self.head_dim
14152 {
14153 return false;
14154 }
14155 full_seen = true;
14156 }
14157 AttnKind::Bounded(w) => {
14158 let Some(ac) = self.anchor_core.as_ref() else {
14161 return false;
14162 };
14163 if self.bounded_rope.is_none()
14164 || w.window != ac.window
14165 || w.sink != ac.sink
14166 || w.window == 0
14167 || w.window + w.sink > 256
14168 || w.sink_k.len() != self.num_kv_heads * w.sink * self.head_dim
14169 || w.sink_v.len() != self.num_kv_heads * w.sink * self.head_dim
14170 || w.wq.as_f32().is_none()
14171 || w.wk.as_f32().is_none()
14172 || w.wv.as_f32().is_none()
14173 || w.wo.as_f32().is_none()
14174 || w.wq.rows() != self.num_heads * self.head_dim
14175 || w.wk.rows() != self.num_kv_heads * self.head_dim
14176 || w.wv.rows() != self.num_kv_heads * self.head_dim
14177 || w.wq.cols() != self.hidden_size
14178 || w.wk.cols() != self.hidden_size
14179 || w.wv.cols() != self.hidden_size
14180 || w.wo.rows() != self.hidden_size
14181 || w.wo.cols() != self.num_heads * self.head_dim
14182 {
14183 return false;
14184 }
14185 }
14186 _ => return false,
14187 }
14188 match &lw.ffn {
14189 FfnKind::Dense(d) => {
14190 if d.act != Act::Silu
14191 || !d.segs.is_empty()
14192 || d.gate_proj.as_f32().is_none()
14193 || d.up_proj.as_f32().is_none()
14194 || d.down_proj.as_f32().is_none()
14195 || d.gate_proj.rows() != self.intermediate_size
14196 || d.gate_proj.cols() != self.hidden_size
14197 || d.up_proj.rows() != self.intermediate_size
14198 || d.up_proj.cols() != self.hidden_size
14199 || d.down_proj.rows() != self.hidden_size
14200 || d.down_proj.cols() != self.intermediate_size
14201 {
14202 return false;
14203 }
14204 }
14205 FfnKind::Moe(m) => {
14206 if m.resonance.is_none()
14207 || m.top_k != 1
14208 || m.router_sigmoid
14209 || !m.norm_topk_prob
14210 || m.expert_bias.is_some()
14211 || m.routed_scaling != 1.0
14212 || m.route_tau.is_some()
14213 || m.shared.is_none()
14214 || m.mask.is_some()
14215 || m.per_expert_scale.is_some()
14216 || m.router_input_norm
14217 || m.experts.is_empty()
14218 || m.experts.len() > 8
14219 {
14220 return false;
14221 }
14222 let r = m.resonance.as_ref().unwrap();
14223 if r.mu.len() != m.experts.len() * self.hidden_size
14224 || r.bias.len() != m.experts.len()
14225 || r.u.len() != m.experts.len() * r.k * self.hidden_size
14226 || r.k > 128
14227 {
14228 return false;
14229 }
14230 let Some((shared, gate)) = &m.shared else {
14231 return false;
14232 };
14233 if gate.is_some() || shared.act != Act::Silu || !shared.segs.is_empty() {
14234 return false;
14235 }
14236 if shared.gate_proj.as_f32().is_none()
14237 || shared.up_proj.as_f32().is_none()
14238 || shared.down_proj.as_f32().is_none()
14239 || shared.gate_proj.rows() != self.intermediate_size
14240 || shared.gate_proj.cols() != self.hidden_size
14241 || shared.up_proj.rows() != self.intermediate_size
14242 || shared.up_proj.cols() != self.hidden_size
14243 || shared.down_proj.rows() != self.hidden_size
14244 || shared.down_proj.cols() != self.intermediate_size
14245 {
14246 return false;
14247 }
14248 for e in &m.experts {
14249 if e.act != Act::Silu
14250 || !e.segs.is_empty()
14251 || e.gate_proj.as_f32().is_none()
14252 || e.up_proj.as_f32().is_none()
14253 || e.down_proj.as_f32().is_none()
14254 || e.gate_proj.rows() != self.intermediate_size
14255 || e.gate_proj.cols() != self.hidden_size
14256 || e.up_proj.rows() != self.intermediate_size
14257 || e.up_proj.cols() != self.hidden_size
14258 || e.down_proj.rows() != self.hidden_size
14259 || e.down_proj.cols() != self.intermediate_size
14260 {
14261 return false;
14262 }
14263 }
14264 }
14265 FfnKind::DenseMoe(_) => return false,
14266 }
14267 if lw.input_norm.len() != self.hidden_size || lw.post_norm.len() != self.hidden_size {
14268 return false;
14269 }
14270 }
14271 if full_seen && self.anchor_core.is_some() {
14272 return false;
14273 }
14274 full_seen || self.num_layers > 0
14275 }
14276
14277 fn embryo_resident_wanted(&self) -> bool {
14282 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
14283 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
14284 && matches!(
14285 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
14286 Ok("1") | Ok("parallel")
14287 )
14288 && crate::gpu::enabled_here()
14289 && !self.graph_refused()
14290 && self.embryo_resident_eligible()
14291 }
14292
14293 fn embryo_prefill_chunked(&mut self, ids: &[u32], start: usize) -> Option<Vec<f32>> {
14301 if ids.len() < 2
14302 || std::env::var("CMF_EMBRYO_CHUNK").as_deref() == Ok("0")
14303 || !self.embryo_resident_wanted()
14304 {
14305 return None;
14306 }
14307 let model = self.ensure_embryo_graph()?;
14308 let cmax = std::env::var("CMF_EMBRYO_CHUNK")
14309 .ok()
14310 .and_then(|v| v.parse::<usize>().ok())
14311 .filter(|&v| v >= 1)
14312 .unwrap_or(crate::gpu::EMBRYO_CHUNK_MAX)
14313 .min(crate::gpu::EMBRYO_CHUNK_MAX);
14314 let hs = self.hidden_size;
14315 let n = ids.len();
14316 let mut pos = start;
14317 let mut last = None;
14318 let mut rows = Vec::with_capacity(cmax * hs);
14319 while pos < n {
14320 let end = (pos + cmax).min(n);
14321 rows.clear();
14322 for &id in &ids[pos..end] {
14323 rows.extend_from_slice(&self.embed_single(id));
14324 }
14325 let mut lg = Vec::new();
14326 if !crate::gpu::forward_embryo_graph_chunk(
14327 &model,
14328 self.graph_kv_id,
14329 &rows,
14330 pos,
14331 end - pos,
14332 &mut lg,
14333 ) {
14334 if pos == start {
14335 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14336 eprintln!("embryo-dbg: chunk prefill refused at position {pos}");
14337 }
14338 return None;
14339 }
14340 self.kv_cache.clear();
14344 self.clear_history();
14345 crate::gpu::graph_kv_reset(self.graph_kv_id);
14346 panic!(
14347 "resident Embryo chunk prefill refused at position {pos}; refusing an in-flight host fallback"
14348 );
14349 }
14350 last = Some(lg);
14351 pos = end;
14352 }
14353 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14354 eprintln!(
14355 "embryo-dbg: chunk prefill {} ids from {start} in {} submits (chunk {cmax})",
14356 n - start,
14357 (n - start).div_ceil(cmax)
14358 );
14359 }
14360 last
14361 }
14362
14363 fn ensure_embryo_graph(&mut self) -> Option<std::sync::Arc<crate::gpu::EmbryoGraphModel>> {
14364 if self.embryo_graph.is_none() && self.embryo_resident_eligible() {
14365 const UMAX: u32 = u32::MAX;
14366 const HEADER: usize = crate::gpu::EMBRYO_META_HEADER;
14367 const REC: usize = 64;
14368 struct Pack {
14369 data: Vec<f32>,
14370 }
14371 impl Pack {
14372 fn put(&mut self, x: &[f32]) -> u32 {
14373 if x.is_empty() {
14374 return u32::MAX;
14375 }
14376 let off = self.data.len();
14377 self.data.extend_from_slice(x);
14378 off as u32
14379 }
14380 }
14381 let vmf = self.vmf_cfg;
14385 let gdn = self.gdn_cfg;
14386 let mut pack = Pack { data: Vec::new() };
14387 let mut meta = vec![0u32; HEADER];
14388 meta[0] = self.hidden_size as u32;
14389 meta[1] = self.intermediate_size as u32;
14390 meta[2] = self.vocab_size as u32;
14391 meta[3] = self.num_layers as u32;
14392 meta[4] = vmf.map(|c| c.num_heads).unwrap_or(0) as u32;
14393 meta[5] = vmf.map(|c| c.nphase).unwrap_or(0) as u32;
14394 meta[6] = vmf.map(|c| c.value_head_dim).unwrap_or(0) as u32;
14395 if let Some(g) = gdn {
14396 meta[24] = g.num_v_heads as u32;
14397 meta[25] = g.num_k_heads as u32;
14398 meta[26] = g.key_head_dim as u32;
14399 meta[27] = g.value_head_dim as u32;
14400 meta[28] = g.conv_kernel as u32;
14401 meta[29] = g.conv_dim() as u32;
14402 }
14403 meta[7] = self.num_heads as u32;
14404 meta[8] = self.num_kv_heads as u32;
14405 meta[9] = self.head_dim as u32;
14406 meta[10] = self.kv_cache.max_seq_len as u32;
14407 let clusters = self.head_clusters.as_ref().unwrap();
14408 let cluster_count = clusters.len() / self.hidden_size;
14409 if clusters.len() % self.hidden_size != 0
14410 || cluster_count == 0
14411 || cluster_count > 1024
14412 || self.vocab_size % cluster_count != 0
14413 || self.weights.lm_head.rows() < self.vocab_size
14414 || self.weights.final_norm.len() != self.hidden_size
14415 {
14416 return None;
14417 }
14418 meta[11] = cluster_count as u32;
14419 meta[12] = (self.vocab_size / cluster_count) as u32;
14420 meta[13] = vmf.map(|c| c.state_len()).unwrap_or(0) as u32;
14421 meta[16] = self.rotary_dim as u32;
14422 meta[17] = matches!(self.norm_style, NormStyle::Gemma) as u32;
14423 meta[19] = (self.rms_eps as f32).to_bits();
14424 let max_conv = self
14425 .weights
14426 .layers
14427 .iter()
14428 .filter_map(|lw| match &lw.attn {
14429 AttnKind::Linear(w) => w.conv.as_ref().map(|v| v.len() / self.hidden_size),
14430 _ => None,
14431 })
14432 .max()
14433 .unwrap_or(1);
14434 let phase_stride = vmf
14440 .map(|c| c.state_len() + max_conv.saturating_sub(1) * self.hidden_size)
14441 .unwrap_or(0);
14442 let gdn_stride = gdn.map(|g| g.state_len()).unwrap_or(0);
14443 let state_stride = phase_stride.max(gdn_stride);
14444 let bounded = self.anchor_core.clone();
14448 let (anchor_window, anchor_sink) = bounded
14449 .as_ref()
14450 .map(|ac| (ac.window, ac.sink))
14451 .unwrap_or((0, 0));
14452 let kv_stride = if bounded.is_some() {
14453 2usize
14454 .saturating_mul(self.num_kv_heads)
14455 .saturating_mul(anchor_window)
14456 .saturating_mul(self.head_dim)
14457 } else {
14458 2usize
14459 .saturating_mul(self.num_kv_heads)
14460 .saturating_mul(self.kv_cache.max_seq_len)
14461 .saturating_mul(self.head_dim)
14462 };
14463 meta[14] = state_stride as u32;
14464 meta[15] = kv_stride as u32;
14465 meta[18] = anchor_window as u32;
14466 meta[20] = anchor_sink as u32;
14467 meta[21] = match &self.bounded_rope {
14468 Some(rope) => {
14469 let off = pack.put(&rope.cos);
14471 let _ = pack.put(&rope.sin);
14472 off
14473 }
14474 None => UMAX,
14475 };
14476 let mut full_seen = false;
14477 let mut bounded_seen = 0usize;
14478 let mut phase_seen = 0usize;
14482 let mut gdn_seen = 0usize;
14483 for (li, lw) in self.weights.layers.iter().enumerate() {
14484 let base = meta.len();
14485 meta.resize(base + REC, UMAX);
14486 meta[base] = match &lw.attn {
14487 AttnKind::Linear(w) if w.phase_delta => 1,
14488 AttnKind::Linear(_) => 0,
14489 AttnKind::Full { .. } => 2,
14490 AttnKind::Bounded(_) => 3,
14491 AttnKind::LinearGdn(_) => 4,
14492 _ => UMAX,
14493 };
14494 meta[base + 1] = pack.put(&lw.input_norm);
14495 meta[base + 2] = pack.put(&lw.post_norm);
14496 meta[base + 25] = match &lw.attn {
14497 AttnKind::Linear(_) => {
14498 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
14499 phase_seen += 1;
14500 off
14501 }
14502 AttnKind::LinearGdn(_) => {
14503 let off = ((phase_seen + gdn_seen) * state_stride) as u32;
14504 gdn_seen += 1;
14505 off
14506 }
14507 _ => UMAX,
14508 };
14509 match &lw.attn {
14510 AttnKind::LinearGdn(w) => {
14511 meta[base + 56] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_qkv));
14514 meta[base + 57] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_z));
14515 meta[base + 58] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_a));
14516 meta[base + 59] = pack.put(&Self::embryo_qtensor_f32(&w.in_proj_b));
14517 meta[base + 60] = pack.put(&w.conv1d);
14518 meta[base + 61] = pack.put(&w.a_log);
14519 meta[base + 62] = pack.put(&w.dt_bias);
14520 meta[base + 63] = pack.put(&w.norm);
14521 meta[base + 29] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
14522 meta[base + 24] = 0;
14523 }
14524 AttnKind::Linear(w) => {
14525 meta[base + 3] = pack.put(&Self::embryo_qtensor_f32(&w.thq));
14526 meta[base + 4] = pack.put(&Self::embryo_qtensor_f32(&w.thk));
14527 meta[base + 5] = pack.put(&Self::embryo_qtensor_f32(&w.v_proj));
14528 meta[base + 6] = pack.put(&Self::embryo_qtensor_f32(&w.out_proj));
14529 let decay: Vec<f32> = w.decay.iter().map(|&x| x as f32).collect();
14530 meta[base + 7] = pack.put(&decay);
14531 if let Some((kg, kb)) = &w.k_gate {
14532 meta[base + 8] = pack.put(&Self::embryo_qtensor_f32(kg));
14533 meta[base + 9] = pack.put(kb);
14534 }
14535 if let Some(conv) = &w.conv {
14536 meta[base + 10] = pack.put(conv);
14537 meta[base + 24] = (conv.len() / self.hidden_size) as u32;
14538 } else {
14539 meta[base + 24] = 0;
14540 }
14541 }
14542 AttnKind::Full { wq, wk, wv, wo, .. } => {
14543 full_seen = true;
14544 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(wq));
14545 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(wk));
14546 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(wv));
14547 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(wo));
14548 meta[base + 26] = (li * kv_stride) as u32;
14549 }
14550 AttnKind::Bounded(w) => {
14551 meta[base + 11] = pack.put(&Self::embryo_qtensor_f32(&w.wq));
14552 meta[base + 12] = pack.put(&Self::embryo_qtensor_f32(&w.wk));
14553 meta[base + 13] = pack.put(&Self::embryo_qtensor_f32(&w.wv));
14554 meta[base + 14] = pack.put(&Self::embryo_qtensor_f32(&w.wo));
14555 meta[base + 26] = (bounded_seen * kv_stride) as u32;
14557 meta[base + 27] = pack.put(&w.sink_k);
14558 meta[base + 28] = pack.put(&w.sink_v);
14559 bounded_seen += 1;
14560 }
14561 _ => return None,
14562 }
14563 match &lw.ffn {
14564 FfnKind::Dense(d) => {
14565 meta[base + 15] = 0;
14566 meta[base + 16] = 0;
14567 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&d.gate_proj));
14568 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&d.up_proj));
14569 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&d.down_proj));
14570 }
14571 FfnKind::Moe(m) => {
14572 let r = m.resonance.as_ref().unwrap();
14573 let (shared, _) = m.shared.as_ref().unwrap();
14574 meta[base + 15] = 1;
14575 meta[base + 16] = m.experts.len() as u32;
14576 meta[base + 17] = pack.put(&r.mu);
14577 meta[base + 18] = pack.put(&r.u);
14578 meta[base + 19] = pack.put(&r.bias);
14579 meta[base + 20] = r.k as u32;
14580 let mut shell = r.effective_shell(m.experts.len());
14589 shell.push(f32::NEG_INFINITY);
14590 meta[base + 30] = pack.put(&shell);
14591 meta[base + 21] = pack.put(&Self::embryo_qtensor_f32(&shared.gate_proj));
14592 meta[base + 22] = pack.put(&Self::embryo_qtensor_f32(&shared.up_proj));
14593 meta[base + 23] = pack.put(&Self::embryo_qtensor_f32(&shared.down_proj));
14594 for (e, ex) in m.experts.iter().enumerate() {
14595 meta[base + 32 + e * 3] =
14596 pack.put(&Self::embryo_qtensor_f32(&ex.gate_proj));
14597 meta[base + 33 + e * 3] =
14598 pack.put(&Self::embryo_qtensor_f32(&ex.up_proj));
14599 meta[base + 34 + e * 3] =
14600 pack.put(&Self::embryo_qtensor_f32(&ex.down_proj));
14601 }
14602 }
14603 FfnKind::DenseMoe(_) => return None,
14604 }
14605 }
14606 if !full_seen && self.num_layers == 0 {
14607 return None;
14608 }
14609 let id = {
14610 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
14611 NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
14612 };
14613 let model = crate::gpu::EmbryoGraphModel {
14614 id,
14615 hidden: self.hidden_size,
14616 intermediate: self.intermediate_size,
14617 vocab: self.vocab_size,
14618 layers: self.num_layers,
14619 phase_heads: vmf.map(|c| c.num_heads).unwrap_or(0),
14620 nphase: vmf.map(|c| c.nphase).unwrap_or(0),
14621 phase_dv: vmf.map(|c| c.value_head_dim).unwrap_or(0),
14622 anchor_q_heads: self.num_heads,
14623 anchor_kv_heads: self.num_kv_heads,
14624 anchor_head_dim: self.head_dim,
14625 rotary_dim: self.rotary_dim,
14626 max_seq: self.kv_cache.max_seq_len,
14627 cluster_count,
14628 cluster_size: self.vocab_size / cluster_count,
14629 phase_state_len: vmf.map(|c| c.state_len()).unwrap_or(0),
14630 state_stride,
14631 kv_stride,
14632 norm_gemma: matches!(self.norm_style, NormStyle::Gemma),
14633 phase_mass: vmf.map(|c| c.phase_mass).unwrap_or(0.0),
14634 weights: pack.data,
14635 meta,
14636 lm_head: Self::embryo_qtensor_f32(&self.weights.lm_head),
14637 clusters: clusters.as_ref().clone(),
14638 final_norm: self.weights.final_norm.clone(),
14639 inv_freq: self.inv_freq.as_ref().clone(),
14640 bounded: bounded.is_some(),
14641 kv_layers: if bounded.is_some() {
14642 bounded_seen
14643 } else {
14644 self.num_layers
14645 },
14646 state_layers: phase_seen + gdn_seen,
14647 anchor_window,
14648 anchor_sink,
14649 phase_layers: phase_seen,
14650 gdn_layers: gdn_seen,
14651 gdn_heads: gdn.map(|g| g.num_v_heads).unwrap_or(0),
14652 gdn_k_heads: gdn.map(|g| g.num_k_heads).unwrap_or(0),
14653 gdn_dk: gdn.map(|g| g.key_head_dim).unwrap_or(0),
14654 gdn_dv: gdn.map(|g| g.value_head_dim).unwrap_or(0),
14655 gdn_kk: gdn.map(|g| g.conv_kernel).unwrap_or(0),
14656 };
14657 self.embryo_graph = Some(std::sync::Arc::new(model));
14658 }
14659 self.embryo_graph.clone()
14660 }
14661
14662 fn forward_layers_span(
14663 &mut self,
14664 hidden: &[f32],
14665 position: usize,
14666 task_mask: Option<&TaskMask>,
14667 from: usize,
14668 upto: Option<usize>,
14669 ) -> Vec<f32> {
14670 debug_assert!(
14671 from == 0
14672 || (self.dsv4.is_none()
14673 && self.dsv41.is_none()
14674 && self.qwen4_exp.is_none()
14675 && self.g3n.is_none())
14676 );
14677 #[cfg(target_os = "macos")]
14683 if !crate::gpu_metal::wait_replay() {
14684 self.fail_metal_graph("the pending async replay failed before a plain forward");
14685 return vec![0.0; self.hidden_size];
14686 }
14687 if let Some(b) = &mut self.qwen4_exp {
14688 let _ = (task_mask, upto);
14689 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
14690 let mut logits = Vec::new();
14691 crate::qwen4_exp::forward_token(
14692 &b.0,
14693 &b.1,
14694 &b.2,
14695 &mut b.3,
14696 token_id,
14697 position,
14698 &self.inv_freq,
14699 self.pool.as_deref(),
14700 &mut logits,
14701 true,
14702 );
14703 self.graph_logits = Some(logits);
14704 return vec![0.0; self.hidden_size];
14705 }
14706 if let Some(b) = &mut self.dsv4 {
14712 let _ = (task_mask, upto);
14713 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
14714 let (g, layers, cfg, st) = (&b.0, &b.1, b.2, &mut b.3);
14715 st.pos = position;
14716 let mut logits = Vec::new();
14717 crate::dsv4::forward_token(
14718 g,
14719 layers,
14720 &cfg,
14721 st,
14722 token_id,
14723 &self.inv_freq,
14724 self.pool.as_deref(),
14725 &mut logits,
14726 );
14727 self.graph_logits = Some(logits);
14728 self.dspark_probe(position, token_id);
14729 return vec![0.0; self.hidden_size];
14732 }
14733 if let Some(b) = &mut self.dsv41 {
14735 let _ = (task_mask, upto);
14736 let token_id = hidden.first().copied().unwrap_or(0.0) as u32;
14737 let mut logits = Vec::new();
14738 crate::dsv41::forward_token(
14739 &b.0,
14740 &b.1,
14741 &b.2,
14742 &mut b.3,
14743 token_id,
14744 position,
14745 self.pool.as_deref(),
14746 &mut logits,
14747 );
14748 self.graph_logits = Some(logits);
14749 return vec![0.0; self.hidden_size];
14750 }
14751 if let Some(b) = &self.g3n {
14754 let _ = (task_mask, upto);
14755 return crate::g3n::g3n_forward(
14756 &b.0,
14757 &b.1,
14758 hidden,
14759 position,
14760 &mut self.kv_cache.layers,
14761 self.num_heads,
14762 self.num_kv_heads,
14763 self.head_dim,
14764 self.pool.as_deref(),
14765 );
14766 }
14767 if from == 0
14774 && upto.is_none()
14775 && task_mask.is_none()
14776 && self.anchor_core.is_some()
14777 && std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1")
14778 {
14779 static ONCE: std::sync::Once = std::sync::Once::new();
14780 ONCE.call_once(|| {
14781 eprintln!(
14782 "embryo-dbg: graph_on decode={} prefill={} resident_env={:?} enabled_here={} \
14783 unsupported={} eligible={}",
14784 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode),
14785 crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill),
14786 std::env::var("CMF_EMBRYO_RESIDENT").ok(),
14787 crate::gpu::enabled_here(),
14788 self.graph_refused(),
14789 self.embryo_resident_eligible(),
14790 );
14791 });
14792 }
14793 if from == 0
14794 && upto.is_none()
14795 && task_mask.is_none()
14796 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Decode)
14800 && crate::gpu::wgpu_graph_on(crate::gpu::GraphPhase::Prefill)
14801 && matches!(
14805 std::env::var("CMF_EMBRYO_RESIDENT").as_deref(),
14806 Ok("1") | Ok("parallel")
14807 )
14808 && crate::gpu::enabled_here()
14809 && !self.graph_refused()
14810 && (position == 0 || self.device_sequence_position().is_some())
14816 && self.embryo_resident_eligible()
14817 && let Some(model) = self.ensure_embryo_graph()
14818 {
14819 let mut lg = Vec::new();
14820 if crate::gpu::forward_embryo_graph(&model, self.graph_kv_id, hidden, position, &mut lg)
14821 {
14822 self.graph_logits = Some(lg);
14823 return vec![0.0; self.hidden_size];
14824 }
14825 if std::env::var("CMF_EMBRYO_DBG").as_deref() == Ok("1") {
14826 eprintln!("embryo-dbg: forward_embryo_graph refused at position {position}");
14827 }
14828 self.mark_graph_refused();
14834 if position != 0 {
14835 self.kv_cache.clear();
14839 self.clear_history();
14840 crate::gpu::graph_kv_reset(self.graph_kv_id);
14841 panic!(
14842 "resident Embryo graph refused at position {position}; refusing an in-flight host fallback"
14843 );
14844 }
14845 }
14846 let mut h = hidden.to_vec();
14847 self.mimo_moe_prepare();
14850 let _mimo_q8 = self.mimo_moe.is_on()
14851 .then(crate::qtensor::enter_full_gpu_q8_scope);
14852 let (nh, _nkv, _hd, hs, _rd, eps) = (
14855 self.num_heads,
14856 self.num_kv_heads,
14857 self.head_dim,
14858 self.hidden_size,
14859 self.rotary_dim,
14860 self.rms_eps,
14861 );
14862 let pool = self.pool.clone();
14863 let graph_env = std::env::var("CMF_GPU_WGPU_GRAPH").ok();
14875 let graph_on = match graph_env.as_deref() {
14876 Some("0") => false,
14877 Some("prefill") => false, Some(_) => true,
14879 None => crate::gpu::wgpu_graph_default(),
14885 };
14886 let graph_trusted =
14887 graph_env.is_some() || crate::gpu::wgpu_graph_default() || self.gdn_cfg.is_some();
14888 let race_eligible = graph_on
14889 && upto.is_none()
14890 && task_mask.is_none()
14891 && from == 0
14892 && !self.graph_refused();
14893 let mut tail_start = 0usize;
14894 if race_eligible && crate::gpu::graph_race_use_graph(graph_trusted) {
14895 let t_graph = std::time::Instant::now();
14896 let mut lg = Vec::new();
14897 let mut gl = 0usize;
14898 let built = self.try_token_graph_wgpu(hidden, position, &mut lg, &mut gl);
14899 let declined = built.is_none();
14900 let built = match built {
14901 Some(Ok(hh)) => Some(hh),
14902 Some(Err(())) => {
14903 self.clear_sequence_state();
14907 self.graph_failed
14908 .store(true, std::sync::atomic::Ordering::Relaxed);
14909 self.cancel
14910 .store(true, std::sync::atomic::Ordering::Relaxed);
14911 tracing::error!("token graph failed after admission; sequence state cleared");
14912 return vec![0.0; self.hidden_size];
14913 }
14914 None => None,
14915 };
14916 if declined && !self.o1_active() && self.attn_softcap == 0.0 {
14921 self.mark_graph_refused();
14922 }
14923 graph_note(built.is_some(), gl, self.num_layers);
14924 if let Some(hh) = built {
14925 let dur = t_graph.elapsed();
14926 if std::env::var("CMF_GRAPH_PROF").is_ok() {
14927 eprintln!("graph-call: {:.2} ms total", dur.as_secs_f64() * 1000.0);
14928 }
14929 if gl > 0 && gl < self.num_layers {
14930 h = hh;
14936 tail_start = gl;
14937 } else if graph_trusted || !crate::gpu::graph_race_first_token_hopeless(dur) {
14938 if !graph_trusted {
14939 crate::gpu::graph_race_record(true, dur);
14940 }
14941 if !lg.is_empty() {
14942 lg.resize(self.vocab_size, 0.0);
14945 if let Some(c) = self.final_softcap {
14946 for l in lg.iter_mut() {
14947 *l = c * (*l / c).tanh();
14948 }
14949 }
14950 self.graph_logits = Some(lg);
14951 }
14952 return hh;
14953 }
14954 }
14960 }
14961 let span = from > 0 || upto.is_some();
14985 if span && graph_on && task_mask.is_none() && graph_trusted {
14986 let upto_excl = upto.map_or(self.num_layers, |u| u + 1);
14987 let mut lg = Vec::new();
14988 let mut gl = 0usize;
14989 let span_res =
14990 self.try_token_graph_wgpu_span(hidden, position, &mut lg, from, upto_excl, &mut gl);
14991 let span_res = match span_res {
14992 Some(Ok(hh)) => Some(hh),
14993 Some(Err(())) => {
14994 self.clear_sequence_state();
14995 self.graph_failed
14996 .store(true, std::sync::atomic::Ordering::Relaxed);
14997 self.cancel
14998 .store(true, std::sync::atomic::Ordering::Relaxed);
14999 tracing::error!(
15000 "span token graph failed after admission; sequence state cleared"
15001 );
15002 return vec![0.0; self.hidden_size];
15003 }
15004 None => None,
15005 };
15006 graph_note(span_res.is_some(), gl, upto_excl - from);
15007 if std::env::var("CMF_GPU_DEBUG").is_ok() {
15008 static SEEN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
15012 if SEEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4 {
15013 eprintln!(
15014 "span graph: covered {gl} of {} layers [{from}..{upto_excl}) res={}",
15015 upto_excl - from,
15016 span_res.is_some()
15017 );
15018 }
15019 }
15020 if let Some(hh) = span_res {
15021 if gl == upto_excl - from {
15022 if !lg.is_empty() {
15023 lg.resize(self.vocab_size, 0.0);
15024 if let Some(c) = self.final_softcap {
15025 for l in lg.iter_mut() {
15026 *l = c * (*l / c).tanh();
15027 }
15028 }
15029 self.graph_logits = Some(lg);
15030 }
15031 crate::gpu::set_layer(-1);
15032 return hh;
15033 }
15034 h = hh;
15036 tail_start = from + gl;
15037 }
15038 }
15039 let t_race_cpu = (race_eligible && !graph_trusted).then(std::time::Instant::now);
15044
15045 let host_tail = tail_start > from;
15055 let _host_tail = (host_tail && !self.mimo_moe.is_on()).then(crate::gpu::enter_cpu_scope);
15056 let automatic_gpu_prefix = self.automatic_gpu_prefix();
15057
15058 let _prof_layers = crate::cpuprof::time(crate::cpuprof::Slot::Layers);
15059 #[cfg(target_os = "macos")]
15060 let mut gpu_skip_until = 0usize;
15061 for li in tail_start.max(from)..self.num_layers {
15062 let _capacity_tail = automatic_gpu_prefix
15063 .filter(|&prefix| li >= prefix && !self.mimo_moe.is_dynamic(li, host_tail))
15064 .map(|_| crate::gpu::enter_cpu_scope());
15065 crate::gpu::set_layer(li as i64); if let Some(u) = upto {
15067 if li > u {
15068 break;
15069 }
15070 }
15071 if let Some(mask) = task_mask {
15072 if !mask.layer_alive(li) {
15073 continue; }
15075 }
15076 #[cfg(target_os = "macos")]
15080 {
15081 if li < gpu_skip_until {
15082 continue;
15083 }
15084 if task_mask.is_none() {
15085 let end = self.q1_graph_gpu(li, upto, position, &mut h);
15086 if self
15087 .graph_failed
15088 .load(std::sync::atomic::Ordering::Relaxed)
15089 {
15090 return vec![0.0; self.hidden_size];
15094 }
15095 if end > li {
15096 gpu_skip_until = end;
15097 if self.is_loop_end(end - 1) && end < self.num_layers {
15100 h = inference::rms_norm(
15101 &h,
15102 &self.weights.final_norm,
15103 self.rms_eps,
15104 self.norm_style,
15105 );
15106 }
15107 continue;
15108 }
15109 }
15110 }
15111
15112 if task_mask.is_none() {
15113 match self.mimo_graph_layer_rows(li, &mut h, &[position]) {
15114 crate::gpu::BatchGraphOutcome::Completed => continue,
15115 crate::gpu::BatchGraphOutcome::Failed => return vec![0.0; self.hidden_size],
15116 crate::gpu::BatchGraphOutcome::Declined => {},
15117 }
15118 }
15119 #[cfg(feature = "gpu")]
15120 self.pull_lagging_host_kv(li, li + 1, position);
15121 let lw = &self.weights.layers[self.phys_layer(li)];
15122 if let Ok(tp) = std::env::var("CMF_TRACE_POS") {
15123 if tp.parse::<usize>().ok() == Some(position) {
15124 let n: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
15125 eprintln!(
15126 "TRACE pos {position} layer {li}: |h| = {n:.6} h0 {:.6} h1 {:.6}",
15127 h[0], h[1]
15128 );
15129 }
15130 }
15131 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15134 inference::rms_norm_into(
15135 &h,
15136 &lw.input_norm,
15137 self.rms_eps,
15138 self.norm_style,
15139 &mut self.ws.n1,
15140 );
15141 drop(prof);
15142
15143 let attn_out = match &lw.attn {
15144 AttnKind::Mla(w) => {
15145 let inv_freq_l = self.layer_inv_freq(li);
15146 let rs = self.layer_rope_scale(li);
15147 let eps = self.rms_eps;
15148 let pool = self.pool.clone();
15149 mla_attention(
15150 w,
15151 &self.ws.n1,
15152 &mut self.kv_cache.layers[li],
15153 position,
15154 &inv_freq_l,
15155 rs,
15156 eps,
15157 pool.as_deref(),
15158 )
15159 }
15160 AttnKind::Linear(w) => {
15161 let cfg = self.vmf_cfg.expect("linear layer without vmf_cfg");
15162 vmf_phase_forward(
15163 &self.ws.n1,
15164 w,
15165 &cfg,
15166 &mut self.kv_cache.layers[li].linear_state,
15167 self.pool.as_deref(),
15168 )
15169 }
15170 AttnKind::Kda(w) => {
15171 let cfg = self.kda_cfg.expect("kda layer without kda_cfg");
15172 crate::linear_core::kda_forward(
15173 &self.ws.n1,
15174 w,
15175 &cfg,
15176 &mut self.kv_cache.layers[li].linear_state,
15177 self.pool.as_deref(),
15178 )
15179 }
15180 AttnKind::LinearGdn(w) => {
15181 let cfg = self.gdn_cfg.expect("gdn layer without gdn_cfg");
15182 gdn_forward(
15183 &self.ws.n1,
15184 w,
15185 &cfg,
15186 &mut self.kv_cache.layers[li].linear_state,
15187 self.pool.as_deref(),
15188 )
15189 }
15190 AttnKind::ShortConv(w) => {
15191 let cfg = self
15192 .short_conv_cfg
15193 .expect("short-conv layer without short_conv_cfg");
15194 short_conv_forward(
15195 &self.ws.n1,
15196 w,
15197 &cfg,
15198 &mut self.kv_cache.layers[li].linear_state,
15199 self.pool.as_deref(),
15200 )
15201 }
15202 AttnKind::Bounded(w) => {
15203 let rope = self
15206 .bounded_rope
15207 .clone()
15208 .expect("bounded layer without an installed rotation table");
15209 let cfg = crate::bounded::BoundedAttnCfg {
15210 num_heads: self.num_heads,
15211 num_kv_heads: self.num_kv_heads,
15212 head_dim: self.head_dim,
15213 hidden_size: hs,
15214 scale: self.attn_scale,
15215 rope: &rope,
15216 pool: pool.as_deref(),
15217 };
15218 crate::bounded::bounded_attention(
15219 &self.ws.n1,
15220 w,
15221 &mut self.kv_cache.layers[li],
15222 &cfg,
15223 )
15224 }
15225 AttnKind::Full {
15226 wq,
15227 wk,
15228 wv,
15229 wo,
15230 q_norm,
15231 k_norm,
15232 output_gate,
15233 softplus_gate,
15234 bias,
15235 } if self.kv_cache.layers[li].o1_sealed() => {
15236 let inv_freq_l = self.layer_inv_freq(li);
15239 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15240 let cfg = QwenAttnCfg {
15241 num_heads: self.layer_num_heads(li),
15242 num_kv_heads: nkv_l,
15243 head_dim: hd_l,
15244 hidden_size: hs,
15245 position,
15246 inv_freq: &inv_freq_l,
15247 rotary_dim: rd_l,
15248 scale: self.attn_scale,
15249 softcap: self.attn_softcap,
15250 window: None,
15251 v_norm: self.attn_v_norm,
15252 qk_norm_after_rope: self.qk_norm_after_rope,
15253 gate_sigmoid: self.proj_gate_sigmoid,
15254 q_norm: q_norm.as_deref(),
15255 k_norm: k_norm.as_deref(),
15256 output_gate: *output_gate,
15257 softplus_gate: softplus_gate
15258 .as_ref()
15259 .map(|(gate, per_head)| (gate, *per_head)),
15260 rope_scale: self.layer_rope_scale(li),
15261 bias: bias
15262 .as_ref()
15263 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15264 rms_eps: eps,
15265 norm_style: self.norm_style,
15266 pool: pool.as_deref(),
15267 v_head_dim: self.layer_v_dim(li),
15268 };
15269 attention::qwen_attention_nystrom(
15270 &self.ws.n1,
15271 wq,
15272 wk,
15273 wv,
15274 wo,
15275 &mut self.kv_cache.layers[li],
15276 &cfg,
15277 )
15278 }
15279 AttnKind::Full {
15280 wq,
15281 wk,
15282 wv,
15283 wo,
15284 q_norm,
15285 k_norm,
15286 output_gate,
15287 softplus_gate,
15288 bias,
15289 } => 'attn: {
15290 let dropin_reason =
15295 graph_on.then(|| self.graph_attn_decline_reason()).flatten();
15296 if let Some(reason) = dropin_reason {
15297 self.note_graph_decline("wgpu attn dropin", reason);
15298 }
15299 if graph_on
15300 && dropin_reason.is_none()
15301 && !*output_gate
15302 && softplus_gate.is_none()
15303 && self.attention_heads_per_layer.is_none()
15304 && bias.is_none()
15305 && task_mask.is_none()
15306 {
15307 let inv_freq_l = self.layer_inv_freq(li);
15308 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15309 let gemma = self.norm_style == cortiq_core::NormStyle::Gemma;
15310 if let (Some((gm, qi)), Some((_, ki)), Some((_, vi)), Some((_, oi))) = (
15311 wq.mapped_q1(),
15312 wk.mapped_q1(),
15313 wv.mapped_q1(),
15314 wo.mapped_q1(),
15315 ) {
15316 let gm = gm.clone();
15317 let mut out = vec![0f32; hs];
15318 let cache = &self.kv_cache.layers[li];
15319 if crate::gpu::attn_dropin(
15320 &gm,
15321 self.graph_kv_id,
15322 li,
15323 &self.ws.n1,
15324 qi,
15325 ki,
15326 vi,
15327 oi,
15328 q_norm.as_deref(),
15329 k_norm.as_deref(),
15330 self.qk_norm_after_rope,
15331 &inv_freq_l,
15332 nh,
15333 nkv_l,
15334 hd_l,
15335 rd_l,
15336 hs,
15337 position,
15338 self.kv_cache.max_seq_len,
15339 gemma,
15340 eps as f32,
15341 cache.k_heads(),
15342 cache.v_heads(),
15343 &mut out,
15344 ) {
15345 break 'attn out;
15346 }
15347 }
15348 }
15349 let masked = task_mask
15350 .map(|m| m.head_flags(li, self.num_heads).iter().any(|&a| !a))
15351 .unwrap_or(false);
15352 let f32_view = (wq.as_f32(), wk.as_f32(), wv.as_f32(), wo.as_f32());
15353 let plain = self.layer_attn_plain(li);
15356 match (masked, f32_view) {
15357 (true, (Some(q), Some(k), Some(v), Some(o))) if plain => {
15360 let active_heads = task_mask.unwrap().head_flags(li, self.num_heads);
15361 attention::multi_head_attention(
15362 &self.ws.n1,
15363 q,
15364 k,
15365 v,
15366 o,
15367 &mut self.kv_cache.layers[li],
15368 self.num_heads,
15369 self.num_kv_heads,
15370 self.head_dim,
15371 self.hidden_size,
15372 position,
15373 &active_heads,
15374 &self.inv_freq,
15375 )
15376 }
15377 (masked, _) => {
15378 if masked {
15379 tracing::warn!(
15380 "layer {li}: head mask on quantized weights or on a \
15381 window/sink/per-layer-geometry layer not supported \
15382 yet — executing dense"
15383 );
15384 }
15385 let inv_freq_l = self.layer_inv_freq(li);
15386 let (nkv_l, hd_l, rd_l) = self.layer_geom(li);
15387 let cfg = QwenAttnCfg {
15388 num_heads: self.layer_num_heads(li),
15389 num_kv_heads: nkv_l,
15390 head_dim: hd_l,
15391 hidden_size: hs,
15392 position,
15393 inv_freq: &inv_freq_l,
15394 rotary_dim: rd_l,
15395 scale: self.attn_scale,
15396 softcap: self.attn_softcap,
15397 window: self.layer_window(li),
15398 v_norm: self.attn_v_norm,
15399 qk_norm_after_rope: self.qk_norm_after_rope,
15400 gate_sigmoid: self.proj_gate_sigmoid,
15401 q_norm: q_norm.as_deref(),
15402 k_norm: k_norm.as_deref(),
15403 output_gate: *output_gate,
15404 softplus_gate: softplus_gate
15405 .as_ref()
15406 .map(|(gate, per_head)| (gate, *per_head)),
15407 rope_scale: self.layer_rope_scale(li),
15408 bias: bias
15409 .as_ref()
15410 .map(|(a, b, c)| (a.as_slice(), b.as_slice(), c.as_slice())),
15411 rms_eps: eps,
15412 norm_style: self.norm_style,
15413 pool: pool.as_deref(),
15414 v_head_dim: self.layer_v_dim(li),
15415 };
15416 attention::qwen_attention(
15417 &self.ws.n1,
15418 wq,
15419 wk,
15420 wv,
15421 wo,
15422 &mut self.kv_cache.layers[li],
15423 &cfg,
15424 )
15425 }
15426 }
15427 }
15428 };
15429 let attn_out = match &self.weights.layers[self.phys_layer(li)].attn_out_norm {
15432 Some(w) => inference::rms_norm(&attn_out, w, self.rms_eps, self.norm_style),
15433 None => attn_out,
15434 };
15435 let lw = &self.weights.layers[self.phys_layer(li)];
15436 let prof = crate::cpuprof::time(crate::cpuprof::Slot::Norms);
15437 inference::add_rmsnorm_fused_into(
15438 &mut h,
15439 &attn_out,
15440 &lw.post_norm,
15441 self.rms_eps,
15442 self.norm_style,
15443 &mut self.ws.p1,
15444 );
15445 drop(prof);
15446 let mut attn_out = attn_out;
15447 attention::recycle_buf(&mut attn_out);
15448 let post_normed = &self.ws.p1;
15449
15450 let ffn_masked = task_mask
15451 .map(|m| m.ffn_active_count(li) < self.intermediate_size)
15452 .unwrap_or(false);
15453 let ffn_out = match (ffn_masked, &lw.ffn) {
15465 (_, FfnKind::Dense(d)) if !d.segs.is_empty() => {
15469 let row = task_mask
15470 .and_then(|tm| tm.ffn_masks.get(li))
15471 .map(|v| v.as_slice());
15472 tube_ffn(d, post_normed, 1, self.pool.as_deref(), row)
15473 }
15474 (true, FfnKind::Dense(d)) => {
15475 let tm = task_mask.unwrap();
15476 let alive = tm.ffn_active_count(li);
15477 let deep = alive * 2 <= self.intermediate_size;
15478 if deep && d.down_proj.sparse_col_ok() && !d.gate_proj.has_prism_contract() {
15479 let active = tm.ffn_active_indices(li);
15480 sparse_ffn_quant(
15481 d,
15482 post_normed,
15483 &active,
15484 self.hidden_size,
15485 self.pool.as_deref(),
15486 )
15487 } else if deep
15488 && let (Some(g), Some(u), Some(dn)) = (
15489 d.gate_proj.as_f32(),
15490 d.up_proj.as_f32(),
15491 d.down_proj.as_f32(),
15492 )
15493 {
15494 let active = tm.ffn_active_indices(li);
15495 inference::sparse_ffn_forward(
15496 post_normed,
15497 g,
15498 u,
15499 dn,
15500 self.hidden_size,
15501 self.intermediate_size,
15502 &active,
15503 self.pool.as_deref(),
15504 )
15505 } else {
15506 let row = tm.ffn_masks.get(li).map(|v| v.as_slice());
15507 dense_ffn_batch(d, post_normed, 1, self.pool.as_deref(), row)
15508 }
15509 }
15510 (true, FfnKind::Moe(m)) => {
15511 let allowed = task_mask.and_then(|tm| tm.expert_flags(li, m.experts.len()));
15515 ffn_forward(
15516 &lw.ffn,
15517 post_normed,
15518 self.pool.as_deref(),
15519 allowed.as_deref(),
15520 )
15521 }
15522 (true, FfnKind::DenseMoe(dm)) => dense_moe_ffn(
15523 dm,
15524 post_normed,
15525 &h,
15526 self.rms_eps,
15527 self.norm_style,
15528 self.pool.as_deref(),
15529 ),
15530 (false, _) => match &lw.ffn {
15531 FfnKind::DenseMoe(dm) => dense_moe_ffn(
15532 dm,
15533 post_normed,
15534 &h,
15535 self.rms_eps,
15536 self.norm_style,
15537 self.pool.as_deref(),
15538 ),
15539 FfnKind::Moe(m)
15540 if task_mask.is_none() && self.mimo_moe.is_dynamic(li, host_tail) =>
15541 {
15542 moe_ffn_banked(&mut self.mimo_moe, li, m, post_normed, self.pool.as_deref())
15543 }
15544 _ => {
15545 let allowed = match (&lw.ffn, task_mask) {
15546 (FfnKind::Moe(m), Some(tm)) => tm.expert_flags(li, m.experts.len()),
15547 _ => None,
15548 };
15549 ffn_forward(
15550 &lw.ffn,
15551 post_normed,
15552 self.pool.as_deref(),
15553 allowed.as_deref(),
15554 )
15555 }
15556 },
15557 };
15558 let ffn_out = match &self.weights.layers[self.phys_layer(li)].ffn_out_norm {
15559 Some(w) => inference::rms_norm(&ffn_out, w, self.rms_eps, self.norm_style),
15560 None => ffn_out,
15561 };
15562 for (i, &f) in ffn_out.iter().enumerate() {
15563 h[i] += f;
15564 }
15565 let mut ffn_out = ffn_out;
15566 attention::recycle_buf(&mut ffn_out);
15567
15568 if let Some(sc) = self.weights.layers[self.phys_layer(li)].layer_scale {
15570 for v in h.iter_mut() {
15571 *v *= sc;
15572 }
15573 }
15574 if self.layer_dump.is_some() {
15576 self.dump_layer_row(position, li, &h);
15577 }
15578
15579 if self.is_loop_end(li) && li + 1 < self.num_layers {
15582 h = inference::rms_norm(
15583 &h,
15584 &self.weights.final_norm,
15585 self.rms_eps,
15586 self.norm_style,
15587 );
15588 }
15589
15590 if self.dyn_phi_layer == Some(li) {
15594 self.update_dyn_phi(&h);
15595 }
15596 }
15597 crate::gpu::set_layer(-1); if let Some(t) = t_race_cpu {
15599 crate::gpu::graph_race_record(false, t.elapsed());
15600 }
15601
15602 h
15603 }
15604
15605 fn update_dyn_phi(&mut self, h: &[f32]) {
15608 const A: f32 = 0.2;
15609 if self.dyn_phi_ema.len() != h.len() {
15610 self.dyn_phi_ema = vec![0.0; h.len()];
15611 self.dyn_phi_seen = 0;
15612 }
15613 if self.dyn_phi_seen == 0 {
15614 self.dyn_phi_ema.copy_from_slice(h);
15615 } else {
15616 for (e, &v) in self.dyn_phi_ema.iter_mut().zip(h) {
15617 *e = (1.0 - A) * *e + A * v;
15618 }
15619 }
15620 self.dyn_phi_seen += 1;
15621 }
15622
15623 pub fn dyn_phi(&self) -> &[f32] {
15625 &self.dyn_phi_ema
15626 }
15627
15628 pub fn set_dyn_phi_layer(&mut self, layer: Option<usize>) {
15630 self.dyn_phi_layer = layer;
15631 self.dyn_phi_ema.clear();
15632 self.dyn_phi_seen = 0;
15633 }
15634
15635 pub fn dynamic_skills(&self) -> Vec<(usize, String, usize)> {
15637 let Some(model) = &self.model else {
15638 return Vec::new();
15639 };
15640 model
15641 .header
15642 .skills
15643 .iter()
15644 .enumerate()
15645 .filter_map(|(i, sk)| {
15646 if sk.is_v2() {
15650 return None;
15651 }
15652 let ok = matches!(self.dyn_skill_layers.get(i), Some(Some(_)));
15653 let sel = sk.selection.as_ref()?;
15654 (ok).then(|| (i, sk.id.clone(), sel.phi_layer))
15655 })
15656 .collect()
15657 }
15658
15659 pub fn active_skill(&self) -> Option<usize> {
15661 self.dyn_active
15662 }
15663
15664 pub fn enable_dynamic_routing(&mut self) -> usize {
15669 use crate::swarm::{DynRouter, RoutableSkill};
15670 let Some(model) = self.model.clone() else {
15671 return 0;
15672 };
15673 if let Some(r) = &model.header.router {
15680 tracing::warn!(
15681 "dynamic routing disabled: this file declares router policy '{}' with \
15682 granularity \"{}\" — the request-level decision applies instead",
15683 r.policy,
15684 r.granularity
15685 );
15686 return 0;
15687 }
15688 if model.required_features & cortiq_core::format::features::SKILLS_V2 != 0
15694 || model.header.skills.iter().any(|s| s.is_v2())
15695 {
15696 tracing::warn!(
15697 "dynamic routing disabled: this file carries format-v2 skill records \
15698 (SKILLS_V2) — they route per request through a router policy only"
15699 );
15700 return 0;
15701 }
15702 if self.dyn_blend_loaded {
15705 tracing::warn!("dynamic routing unavailable on a blend-loaded pipeline");
15706 return 0;
15707 }
15708 if let Some(a) = self.dyn_active {
15712 if !matches!(self.dyn_skill_layers.get(a), Some(Some(_))) {
15713 tracing::warn!("loaded skill is not FFN-eligible — dynamic routing unavailable");
15714 return 0;
15715 }
15716 }
15717 let hidden = self.hidden_size;
15718 let mut skills = Vec::new();
15719 for (idx, id, _phi) in self.dynamic_skills() {
15720 if let Some(sel) = model.header.skills[idx].selection.as_ref() {
15721 if let Some(rs) = RoutableSkill::from_descriptor(idx, id, sel, hidden) {
15722 skills.push(rs);
15723 }
15724 }
15725 }
15726 if skills.is_empty() {
15727 return 0;
15728 }
15729 let phi = skills[0].phi_layer;
15731 if skills.iter().any(|s| s.phi_layer != phi) {
15732 tracing::warn!("routable skills disagree on phi_layer; using {phi}");
15733 }
15734 let n = skills.len();
15735 self.set_dyn_phi_layer(Some(phi));
15736 self.dyn_router = Some(DynRouter::new(skills));
15737 n
15738 }
15739
15740 pub fn route_switches(&self) -> Vec<(usize, Option<String>, Option<String>)> {
15742 self.dyn_router
15743 .as_ref()
15744 .map(|r| r.switches.clone())
15745 .unwrap_or_default()
15746 }
15747
15748 fn lm_head_forward(&self, hidden: &[f32]) -> Vec<f32> {
15751 let _mimo_q8 = self.mimo_moe.is_on()
15752 .then(crate::qtensor::enter_full_gpu_q8_scope);
15753 let rows = self.weights.lm_head.rows();
15754 let mut logits = attention::take_buf(rows.min(self.vocab_size));
15755 let served = self.mimo_moe.is_on() && crate::gpu::mimo_q8_short_enabled()
15759 && rows == self.vocab_size && !self.weights.lm_head.has_prism_contract()
15760 && self.weights.lm_head.graph_weight().is_some_and(|(model, idx, kind, _)| {
15761 kind == 7 && crate::gpu::q82_short_rows(model, idx, hidden, 1,
15762 rows, self.hidden_size, &mut logits)
15763 });
15764 if !served {
15765 self.weights.lm_head.matvec(hidden, &mut logits, self.pool.as_deref());
15766 }
15767 logits.resize(self.vocab_size, 0.0);
15768 if let Some(m) = self.logit_multiplier {
15769 for l in logits.iter_mut() {
15770 *l *= m;
15771 }
15772 }
15773 if let Some(c) = self.final_softcap {
15774 for l in logits.iter_mut() {
15775 *l = c * (*l / c).tanh();
15776 }
15777 }
15778 if let Some(cm) = self.head_clusters.as_ref() {
15779 self.hierarchical_head_logprobs(hidden, cm, &mut logits);
15780 }
15781 logits
15782 }
15783
15784 fn hierarchical_head_logprobs(&self, hidden: &[f32], cm: &[f32], logits: &mut [f32]) {
15787 let h = hidden.len();
15788 let ncl = cm.len() / h.max(1);
15789 if ncl == 0 || logits.len() % ncl != 0 {
15790 return;
15791 }
15792 let cs = logits.len() / ncl;
15793 let mut lc = vec![0.0f32; ncl];
15795 for c in 0..ncl {
15796 let row = &cm[c * h..(c + 1) * h];
15797 let mut s = 0.0f32;
15798 for j in 0..h {
15799 s += row[j] * hidden[j];
15800 }
15801 lc[c] = s;
15802 }
15803 let mx = lc.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
15804 let lse: f32 = mx + lc.iter().map(|v| (v - mx).exp()).sum::<f32>().ln();
15805 for c in 0..ncl {
15806 let blk = &mut logits[c * cs..(c + 1) * cs];
15807 let bm = blk.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
15808 let bl: f32 = bm + blk.iter().map(|v| (v - bm).exp()).sum::<f32>().ln();
15809 let add = lc[c] - lse - bl;
15810 for v in blk.iter_mut() {
15811 *v += add;
15812 }
15813 }
15814 }
15815
15816 pub fn prefill_next_logits(&mut self, ids: &[u32], task_mask: Option<&TaskMask>) -> Vec<f32> {
15821 #[cfg(target_os = "macos")]
15822 crate::gpu_metal::set_io_namespace(self.graph_kv_id);
15823 self.clear_sequence_state();
15824 crate::gpu::graph_race_begin_generation();
15828 if task_mask.is_none() {
15829 self.o1_begin();
15830 }
15831 let mut hidden = vec![0.0f32; self.hidden_size];
15832 for (pos, &id) in ids.iter().enumerate() {
15833 let emb = self.embed_single(id);
15834 hidden = self.forward_layers(&emb, pos, task_mask);
15835 }
15836 if let Err(err) = self.o1_seal_checked() {
15837 self.o1_fail(err);
15838 }
15839 if let Some(logits) = self.graph_logits.take() {
15842 return logits;
15843 }
15844 inference::rms_norm_into(
15845 &hidden,
15846 &self.weights.final_norm,
15847 self.rms_eps,
15848 self.norm_style,
15849 &mut self.ws.n1,
15850 );
15851 self.lm_head_forward(&self.ws.n1)
15852 }
15853}
15854
15855pub fn create_test_pipeline(
15857 hidden_size: usize,
15858 intermediate_size: usize,
15859 num_heads: usize,
15860 num_kv_heads: usize,
15861 head_dim: usize,
15862 num_layers: usize,
15863 vocab_size: usize,
15864) -> Pipeline {
15865 let synth = |n: usize, salt: usize| -> Vec<f32> {
15868 (0..n)
15869 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.2)
15870 .collect()
15871 };
15872 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
15873 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
15874 };
15875 let layer_weights: Vec<LayerWeights> = (0..num_layers)
15876 .map(|li| LayerWeights {
15877 input_norm: vec![1.0; hidden_size],
15878 post_norm: vec![1.0; hidden_size],
15879 attn_out_norm: None,
15880 ffn_out_norm: None,
15881 layer_scale: None,
15882 ffn: FfnKind::Dense(DenseFfn {
15883 gate_proj: qt(intermediate_size, hidden_size, li * 10 + 5),
15884 up_proj: qt(intermediate_size, hidden_size, li * 10 + 6),
15885 down_proj: qt(hidden_size, intermediate_size, li * 10 + 7),
15886 act: Act::Silu,
15887 down_t: None,
15888 segs: Vec::new(),
15889 }),
15890 attn: AttnKind::Full {
15891 bias: None,
15892 wq: qt(num_heads * head_dim, hidden_size, li * 10 + 1),
15893 wk: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 2),
15894 wv: qt(num_kv_heads * head_dim, hidden_size, li * 10 + 3),
15895 wo: qt(hidden_size, num_heads * head_dim, li * 10 + 4),
15896 q_norm: None,
15897 k_norm: None,
15898 output_gate: false,
15899 softplus_gate: None,
15900 },
15901 })
15902 .collect();
15903
15904 Pipeline::new(
15905 Tokenizer::byte_level(),
15906 PipelineWeights {
15907 embed_tokens: qt(vocab_size, hidden_size, 100),
15908 layers: layer_weights,
15909 lm_head: qt(vocab_size, hidden_size, 200),
15910 final_norm: vec![1.0; hidden_size],
15911 },
15912 hidden_size,
15913 intermediate_size,
15914 num_heads,
15915 num_kv_heads,
15916 head_dim,
15917 num_layers,
15918 num_layers, false, vocab_size,
15921 1e-6,
15922 10_000.0,
15923 NormStyle::Qwen,
15924 4096,
15925 SamplerConfig {
15926 seed: Some(42),
15927 ..Default::default()
15928 },
15929 )
15930}
15931
15932#[inline]
15937fn mask_bit(row: &[u8], j: usize) -> bool {
15938 (row.get(j >> 3).copied().unwrap_or(0) >> (j & 7)) & 1 != 0
15939}
15940
15941fn mask_gain() -> f32 {
15952 static G: std::sync::OnceLock<f32> = std::sync::OnceLock::new();
15953 *G.get_or_init(|| {
15954 std::env::var("CMF_FFN_MASK_GAIN")
15955 .ok()
15956 .and_then(|v| v.parse().ok())
15957 .unwrap_or(1.0)
15958 })
15959}
15960
15961fn zero_masked_cols(g: &mut [f32], rows: usize, inter: usize, row: &[u8]) {
15962 let fill = meanfill().and_then(|(i, v)| {
15965 let li = crate::gpu::cur_layer();
15966 (*i == inter && li >= 0).then(|| &v[li as usize * inter..(li as usize + 1) * inter])
15967 });
15968 for r in 0..rows {
15969 let base = r * inter;
15970 for (bi, &byte) in row.iter().enumerate() {
15971 if byte == 0xFF {
15972 continue;
15973 }
15974 let j0 = bi * 8;
15975 for bit in 0..8 {
15976 let j = j0 + bit;
15977 if j < inter && byte & (1 << bit) == 0 {
15978 g[base + j] = fill.map_or(0.0, |f| f[j]);
15979 }
15980 }
15981 }
15982 }
15983 let gain = mask_gain();
15984 if gain != 1.0 {
15985 for v in g[..rows * inter].iter_mut() {
15986 *v *= gain;
15987 }
15988 }
15989}
15990
15991#[inline]
15993fn tube_bit(row: Option<&[u8]>, i: usize) -> bool {
15994 row.is_none_or(|r| mask_bit(r, i))
15995}
15996
15997fn all_bits_on(row: &[u8], n: usize) -> bool {
16000 (0..n).all(|i| mask_bit(row, i))
16001}
16002
16003fn tube_topk() -> usize {
16011 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
16012 *K.get_or_init(|| {
16013 std::env::var("CMF_TUBE_TOPK")
16014 .ok()
16015 .and_then(|v| v.parse().ok())
16016 .unwrap_or(0)
16017 })
16018}
16019
16020fn tube_score_oracle() -> bool {
16021 static O: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16022 *O.get_or_init(|| std::env::var("CMF_TUBE_SCORE").is_ok_and(|v| v == "oracle"))
16023}
16024
16025fn tube_ffn_routed(
16032 d: &DenseFfn,
16033 xs: &[f32],
16034 b: usize,
16035 pool: Option<&Pool>,
16036 mask_row: Option<&[u8]>,
16037 k: usize,
16038) -> Vec<f32> {
16039 let hidden = d.down_proj.rows();
16040 let core = d.gate_proj.rows();
16041 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16042 let mut out = match (b, core_full, mask_row) {
16043 (1, true, _) => dense_ffn(d, xs, pool),
16044 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16045 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16046 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16047 };
16048 let cand: Vec<usize> = (0..d.segs.len())
16049 .filter(|&i| tube_bit(mask_row, d.segs[i].start))
16050 .collect();
16051 if cand.is_empty() {
16052 return out;
16053 }
16054 let oracle = tube_score_oracle();
16058 let mut acts: Vec<Vec<f32>> = Vec::with_capacity(cand.len());
16059 let mut scores = vec![0f32; b * cand.len()];
16060 for (ci, &i) in cand.iter().enumerate() {
16061 let seg = &d.segs[i];
16062 let w = seg.width;
16063 let mut g = vec![0.0f32; b * w];
16064 if b == 1 {
16065 seg.gate.matvec(xs, &mut g, pool);
16066 } else {
16067 seg.gate.matmat(xs, b, &mut g, pool);
16068 }
16069 for v in g.iter_mut() {
16070 *v = Act::Silu.combine(*v, 1.0);
16071 }
16072 if !oracle {
16073 for t in 0..b {
16074 scores[t * cand.len() + ci] =
16075 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16076 }
16077 }
16078 if oracle || b > 1 {
16079 let mut u = vec![0.0f32; b * w];
16080 if b == 1 {
16081 seg.up.matvec(xs, &mut u, pool);
16082 } else {
16083 seg.up.matmat(xs, b, &mut u, pool);
16084 }
16085 for (a, &v) in g.iter_mut().zip(u.iter()) {
16086 *a *= v;
16087 }
16088 if oracle {
16089 for t in 0..b {
16090 scores[t * cand.len() + ci] =
16091 g[t * w..(t + 1) * w].iter().map(|v| v * v).sum::<f32>();
16092 }
16093 }
16094 }
16095 acts.push(g);
16096 }
16097 let keep = k.min(cand.len());
16099 let mut scratch: Vec<f32> = Vec::new();
16100 for t in 0..b {
16101 let mut sc: Vec<(f32, usize)> = (0..cand.len())
16102 .map(|ci| (scores[t * cand.len() + ci], ci))
16103 .collect();
16104 sc.sort_unstable_by(|x, y| y.0.total_cmp(&x.0));
16105 let mut alive = vec![false; cand.len()];
16106 for &(_, ci) in sc.iter().take(keep) {
16107 alive[ci] = true;
16108 }
16109 if b > 1 {
16110 for (ci, a) in acts.iter_mut().enumerate() {
16111 if !alive[ci] {
16112 let w = d.segs[cand[ci]].width;
16113 a[t * w..(t + 1) * w].fill(0.0);
16114 }
16115 }
16116 } else {
16117 for (ci, &i) in cand.iter().enumerate() {
16121 if !alive[ci] {
16122 continue;
16123 }
16124 let seg = &d.segs[i];
16125 let w = seg.width;
16126 let g = &mut acts[ci];
16127 if !tube_score_oracle() {
16128 scratch.clear();
16129 scratch.resize(w, 0.0);
16130 seg.up.matvec(xs, &mut scratch, pool);
16131 for (a, &v) in g.iter_mut().zip(scratch.iter()) {
16132 *a *= v;
16133 }
16134 }
16135 let mut acc = vec![0.0f32; hidden];
16136 seg.down.matvec(g, &mut acc, pool);
16137 for (o, a) in out.iter_mut().zip(&acc) {
16138 *o += *a;
16139 }
16140 }
16141 }
16142 }
16143 if b > 1 {
16144 for (ci, &i) in cand.iter().enumerate() {
16145 let seg = &d.segs[i];
16146 let mut acc = vec![0.0f32; b * hidden];
16147 seg.down.matmat(&acts[ci], b, &mut acc, pool);
16148 for (o, a) in out.iter_mut().zip(&acc) {
16149 *o += *a;
16150 }
16151 }
16152 }
16153 out
16154}
16155
16156fn tube_ffn(
16162 d: &DenseFfn,
16163 xs: &[f32],
16164 b: usize,
16165 pool: Option<&Pool>,
16166 mask_row: Option<&[u8]>,
16167) -> Vec<f32> {
16168 if tube_topk() > 0 {
16169 return tube_ffn_routed(d, xs, b, pool, mask_row, tube_topk());
16170 }
16171 let hidden = d.down_proj.rows();
16172 let core = d.gate_proj.rows();
16173 let core_full = mask_row.is_none_or(|r| all_bits_on(r, core));
16174 let mut out = match (b, core_full, mask_row) {
16175 (1, true, _) => dense_ffn(d, xs, pool),
16176 (1, false, Some(row)) => dense_ffn_masked(d, xs, pool, row),
16177 (_, true, _) => dense_ffn_batch(d, xs, b, pool, None),
16178 (_, false, row) => dense_ffn_batch(d, xs, b, pool, row),
16179 };
16180 TUBE_SCRATCH.with(|sc| {
16181 let mut sc = sc.borrow_mut();
16182 let [g, u, acc] = &mut *sc;
16183 for seg in &d.segs {
16184 if !tube_bit(mask_row, seg.start) {
16185 continue;
16186 }
16187 let w = seg.width;
16188 g.resize(b * w, 0.0);
16189 if b == 1
16190 && d.act == Act::Silu
16191 && QTensor::matvec_silu_mul(&seg.gate, &seg.up, xs, g, pool)
16192 {
16193 } else {
16195 u.resize(b * w, 0.0);
16196 if b == 1 {
16197 QTensor::matvec_many([&seg.gate, &seg.up], xs, [g, u], pool);
16198 } else {
16199 seg.gate.matmat(xs, b, g, pool);
16200 seg.up.matmat(xs, b, u, pool);
16201 }
16202 for i in 0..b * w {
16203 g[i] = d.act.combine(g[i], u[i]);
16204 }
16205 }
16206 acc.resize(b * hidden, 0.0);
16207 acc.fill(0.0);
16208 if b == 1 {
16209 seg.down.matvec(g, acc, pool);
16210 } else {
16211 seg.down.matmat(g, b, acc, pool);
16212 }
16213 for (o, a) in out.iter_mut().zip(acc.iter()) {
16214 *o += *a;
16215 }
16216 }
16217 out
16218 })
16219}
16220
16221thread_local! {
16222 static TUBE_SCRATCH: std::cell::RefCell<[Vec<f32>; 3]> =
16226 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new()]) };
16227}
16228
16229static PREFILL_SPLIT: [std::sync::atomic::AtomicU64; 2] =
16232 [std::sync::atomic::AtomicU64::new(0), std::sync::atomic::AtomicU64::new(0)];
16233
16234fn wgpu_prefill_counters() -> (f64, f64, f64, u64) {
16238 #[cfg(feature = "gpu")]
16239 {
16240 use std::sync::atomic::Ordering::Relaxed;
16241 let ms = |a: &std::sync::atomic::AtomicU64| a.load(Relaxed) as f64 / 1e6;
16242 (
16243 ms(&crate::gpu_wgpu::GEMM_MANY_NS[0]),
16244 ms(&crate::gpu_wgpu::GEMM_MANY_NS[1]),
16245 ms(&crate::gpu_wgpu::CHUNK_ATTEND_WAIT_NS),
16246 crate::gpu_wgpu::ZI_GEMM_CALLS.load(Relaxed),
16247 )
16248 }
16249 #[cfg(not(feature = "gpu"))]
16250 (0.0, 0.0, 0.0, 0)
16251}
16252
16253fn wgpu_mirror_counters() -> [u64; 6] {
16256 #[cfg(feature = "gpu")]
16257 {
16258 crate::gpu_wgpu::MIRROR_EVENTS
16259 .each_ref()
16260 .map(|a| a.load(std::sync::atomic::Ordering::Relaxed))
16261 }
16262 #[cfg(not(feature = "gpu"))]
16263 [0; 6]
16264}
16265
16266fn prefill_prof_on() -> bool {
16267 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16268 *ON.get_or_init(|| std::env::var_os("CMF_PREFILL_PROF").is_some())
16269}
16270
16271fn dense_ffn_batch(
16272 d: &DenseFfn,
16273 xs: &[f32],
16274 b: usize,
16275 pool: Option<&Pool>,
16276 mask_row: Option<&[u8]>,
16277) -> Vec<f32> {
16278 let inter = d.gate_proj.rows();
16279 let hidden = d.down_proj.rows();
16280 let fused_act = d.act.graph_act();
16290 if mask_row.is_none()
16291 && fused_act.is_some()
16292 && b >= 32
16293 && crate::gpu::enabled_here()
16294 && !crate::gpu::mm_killed()
16295 && refit_dir().is_none()
16300 && !ffn_probe_active()
16305 {
16306 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16307 d.gate_proj.mapped_q4t(),
16308 d.up_proj.mapped_q4t(),
16309 d.down_proj.mapped_q4t(),
16310 ) {
16311 let mut out = vec![0.0f32; b * hidden];
16312 let act = fused_act.expect("checked above");
16313 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, false, act, &mut out)
16314 {
16315 return out;
16316 }
16317 }
16318 if let (Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16323 d.gate_proj.mapped_q4tp(),
16324 d.up_proj.mapped_q4tp(),
16325 d.down_proj.mapped_q4tp(),
16326 ) {
16327 let mut out = vec![0.0f32; b * hidden];
16328 let act = fused_act.expect("checked above");
16329 if crate::gpu::q4_ffn_act(model, w1, w3, w2, xs, b, hidden, inter, true, act, &mut out)
16330 {
16331 return out;
16332 }
16333 }
16334 if let (true, Some((model, w1)), Some((_, w3)), Some((_, w2))) = (
16340 std::env::var("CMF_FFN_KEEP").as_deref() != Ok("0"),
16341 d.gate_proj.mapped_device_gemm(),
16342 d.up_proj.mapped_device_gemm(),
16343 d.down_proj.mapped_device_gemm(),
16344 ) {
16345 let mut out = vec![0.0f32; b * hidden];
16346 let act = fused_act.expect("checked above");
16347 if crate::gpu::ffn_act_keep(model, w1, w3, w2, xs, b, hidden, inter, act, &mut out) {
16348 return out;
16349 }
16350 }
16351 }
16352 let mut g = vec![0.0f32; b * inter];
16353 d.gate_proj.matmat(xs, b, &mut g, pool);
16354 let mut u = vec![0.0f32; b * inter];
16355 d.up_proj.matmat(xs, b, &mut u, pool);
16356 if gate_topk() > 0 && d.act == Act::Silu {
16357 for t in 0..b {
16358 let row = &mut g[t * inter..(t + 1) * inter];
16359 for v in row.iter_mut() {
16360 *v = Act::Silu.combine(*v, 1.0);
16361 }
16362 keep_top_k(row, gate_topk());
16363 }
16364 for i in 0..b * inter {
16365 g[i] *= u[i];
16366 }
16367 } else {
16368 for i in 0..b * inter {
16369 g[i] = d.act.combine(g[i], u[i]);
16370 }
16371 }
16372 if let Some(row) = mask_row {
16373 zero_masked_cols(&mut g, b, inter, row);
16374 }
16375 if oracle_topk() > 0 {
16376 for t in 0..b {
16377 keep_top_k(&mut g[t * inter..(t + 1) * inter], oracle_topk());
16378 }
16379 }
16380 let mut out = vec![0.0f32; b * hidden];
16381 d.down_proj.matmat(&g, b, &mut out, pool);
16382 if refit_dir().is_some() {
16383 let li = crate::gpu::cur_layer();
16384 if li >= 0 {
16385 refit_accumulate(li as usize, &g, b, inter, &out, hidden, pool);
16386 }
16387 }
16388 FFN_PROBE.with(|pr| {
16392 if let Some(acc) = pr.borrow_mut().as_mut() {
16393 let li = crate::gpu::cur_layer();
16394 if li < 0 {
16395 return;
16396 }
16397 let Some(row) = acc.get_mut(li as usize) else {
16398 return;
16399 };
16400 let sq = probe_sq();
16401 for t in 0..b {
16402 for (a, &v) in row.iter_mut().zip(&g[t * inter..(t + 1) * inter]) {
16403 *a += if sq {
16404 (v as f64) * (v as f64)
16405 } else {
16406 (v as f64).abs()
16407 };
16408 }
16409 }
16410 }
16411 });
16412 out
16413}
16414
16415fn accumulate_act(m: &MoeFfn, xs: &[f32], b: usize) {
16420 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16421 static DUMP: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
16422 let on = *ON.get_or_init(|| std::env::var("CMF_RMS_TRACE").is_ok());
16423 let dump = *DUMP.get_or_init(|| std::env::var("CMF_ACT_DUMP").is_ok());
16424 if (!on && !dump) || b == 0 {
16425 return;
16426 }
16427 let hidden = xs.len() / b;
16428 if on {
16429 let mut acc = m.act_sq.borrow_mut();
16430 if acc.len() < hidden {
16431 acc.resize(hidden, 0.0);
16432 }
16433 for t in 0..b {
16434 let row = &xs[t * hidden..(t + 1) * hidden];
16435 for (a, &v) in acc.iter_mut().zip(row) {
16436 *a += (v as f64) * (v as f64);
16437 }
16438 }
16439 }
16440 if dump {
16441 let cap: usize = std::env::var("CMF_ACT_DUMP_ROWS")
16444 .ok()
16445 .and_then(|v| v.parse().ok())
16446 .unwrap_or(4096);
16447 let mut rows = m.act_rows.borrow_mut();
16448 if rows.len() < cap * hidden {
16449 let take = b.min((cap * hidden - rows.len()) / hidden.max(1));
16450 rows.extend_from_slice(&xs[..take * hidden]);
16451 }
16452 }
16453}
16454
16455#[derive(Clone, Copy)]
16458struct SendVecs(*mut Vec<f32>);
16459unsafe impl Send for SendVecs {}
16460unsafe impl Sync for SendVecs {}
16461impl SendVecs {
16462 #[inline]
16463 fn at(self, i: usize) -> *mut Vec<f32> {
16464 unsafe { self.0.add(i) }
16465 }
16466}
16467
16468fn moe_ffn_batch(
16469 m: &MoeFfn,
16470 xs: &[f32],
16471 b: usize,
16472 hidden: usize,
16473 pool: Option<&Pool>,
16474 allowed: Option<&[bool]>,
16475) -> Vec<f32> {
16476 accumulate_act(m, xs, b);
16477 let ne = m.experts.len();
16478 let mut logits = vec![0.0f32; b * ne];
16479 match &m.resonance {
16480 Some(r) => {
16481 let hdim = xs.len() / b.max(1);
16482 for bi in 0..b {
16483 r.scores(
16484 &xs[bi * hdim..(bi + 1) * hdim],
16485 &mut logits[bi * ne..(bi + 1) * ne],
16486 );
16487 }
16488 }
16489 None => m.router.matmat(xs, b, &mut logits, pool),
16490 }
16491
16492 let mut assign: Vec<Vec<(usize, f32)>> = vec![Vec::new(); ne];
16495 {
16496 let mut st = m.stats.borrow_mut();
16497 if st.len() < ne {
16498 st.resize(ne, 0);
16499 }
16500 for bi in 0..b {
16501 let (idx, p, wsum) = moe_route(&logits[bi * ne..(bi + 1) * ne], m, allowed);
16502 for &e in &idx {
16503 st[e] += 1;
16504 assign[e].push((bi, p[e] / wsum));
16505 }
16506 }
16507 }
16508
16509 let mut out = vec![0.0f32; b * hidden];
16510 let cols = m.experts[0].gate_proj.cols();
16511 let run_expert = |d: &DenseFfn, list: &[(usize, f32)], out: &mut [f32]| {
16512 let sb = list.len();
16513 let mut sub = vec![0.0f32; sb * cols];
16514 for (k, &(bi, _)) in list.iter().enumerate() {
16515 sub[k * cols..(k + 1) * cols].copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
16516 }
16517 let eo = dense_ffn_batch(d, &sub, sb, pool, None);
16518 for (k, &(bi, w)) in list.iter().enumerate() {
16519 for i in 0..hidden {
16520 out[bi * hidden + i] += w * eo[k * hidden + i];
16521 }
16522 }
16523 };
16524 let active: Vec<usize> = (0..ne).filter(|&e| !assign[e].is_empty()).collect();
16530 if pool.is_some() && active.len() >= 8 {
16531 let mut panels: Vec<Vec<f32>> = vec![Vec::new(); active.len()];
16532 {
16533 let panel_ptr = SendVecs(panels.as_mut_ptr());
16534 let experts = &m.experts;
16537 let (active_r, assign_r) = (&active, &assign);
16538 let inherit_cpu = crate::gpu::inherit_cpu_scope();
16539 let run = |start: usize, end: usize| {
16540 let _cpu_scope = inherit_cpu();
16541 for ai in start..end {
16542 let e = active_r[ai];
16543 let list = &assign_r[e];
16544 let sb = list.len();
16545 let mut sub = vec![0.0f32; sb * cols];
16546 for (k, &(bi, _)) in list.iter().enumerate() {
16547 sub[k * cols..(k + 1) * cols]
16548 .copy_from_slice(&xs[bi * cols..(bi + 1) * cols]);
16549 }
16550 unsafe {
16552 *panel_ptr.at(ai) = dense_ffn_batch(&experts[e], &sub, sb, None, None);
16553 }
16554 }
16555 };
16556 match pool {
16557 Some(p) => p.run_rows(active.len(), &run),
16558 None => run(0, active.len()),
16559 }
16560 }
16561 for (ai, &e) in active.iter().enumerate() {
16562 for (k, &(bi, w)) in assign[e].iter().enumerate() {
16563 let eo = &panels[ai][k * hidden..(k + 1) * hidden];
16564 for i in 0..hidden {
16565 out[bi * hidden + i] += w * eo[i];
16566 }
16567 }
16568 }
16569 } else {
16570 for &e in &active {
16571 run_expert(&m.experts[e], &assign[e], &mut out);
16572 }
16573 }
16574 if let Some((se, gate)) = &m.shared {
16575 let all: Vec<(usize, f32)> = if let Some(gate) = gate {
16576 let mut gl = vec![0.0f32; b];
16577 gate.matmat(xs, b, &mut gl, pool);
16578 (0..b)
16579 .map(|bi| (bi, 1.0 / (1.0 + (-gl[bi]).exp())))
16580 .collect()
16581 } else {
16582 (0..b).map(|bi| (bi, 1.0)).collect()
16583 };
16584 run_expert(se, &all, &mut out);
16585 }
16586 out
16587}
16588
16589fn moe_ffn_rows_exact(
16601 m: &MoeFfn,
16602 xs: &[f32],
16603 b: usize,
16604 hidden: usize,
16605 pool: Option<&Pool>,
16606) -> Vec<f32> {
16607 let mut out = vec![0.0f32; b * hidden];
16608 let per_row = |out: &mut [f32]| {
16609 for r in 0..b {
16610 let o = moe_ffn(m, &xs[r * hidden..(r + 1) * hidden], pool, None);
16611 out[r * hidden..(r + 1) * hidden].copy_from_slice(&o);
16612 }
16613 };
16614 let covered = !crate::gpu::enabled_here()
16615 && moe_batch_enabled()
16616 && m.shared.is_none()
16617 && m.resonance.is_none()
16618 && FFN_PROBE.with(|pr| pr.borrow().is_none())
16619 && m.experts.iter().all(|d| d.act == Act::Silu);
16620 if !covered {
16621 per_row(&mut out);
16622 return out;
16623 }
16624 let ne = m.experts.len();
16625 let mut routes: Vec<(Vec<usize>, Vec<f32>)> = Vec::with_capacity(b);
16627 for r in 0..b {
16628 let x = &xs[r * hidden..(r + 1) * hidden];
16629 accumulate_act(m, x, 1);
16630 let mut logits = vec![0.0f32; ne];
16631 m.router.matvec(x, &mut logits, pool);
16632 let (idx, p, wsum) = moe_route(&logits, m, None);
16633 {
16634 let mut st = m.stats.borrow_mut();
16635 if st.len() < ne {
16636 st.resize(ne, 0);
16637 }
16638 for &e in &idx {
16639 st[e] += 1;
16640 }
16641 }
16642 let w: Vec<f32> = idx
16643 .iter()
16644 .map(|&e| p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]))
16645 .collect();
16646 routes.push((idx, w));
16647 }
16648 if routes.iter().any(|(idx, _)| idx.is_empty()) {
16649 per_row(&mut out);
16650 return out;
16651 }
16652 let mut experts: Vec<usize> = Vec::new();
16654 let mut groups: Vec<Vec<usize>> = Vec::new();
16655 for (r, (idx, _)) in routes.iter().enumerate() {
16656 for &e in idx {
16657 match experts.iter().position(|&x| x == e) {
16658 Some(g) => groups[g].push(r),
16659 None => {
16660 experts.push(e);
16661 groups.push(vec![r]);
16662 }
16663 }
16664 }
16665 }
16666 let n_pairs: usize = groups.iter().map(|g| g.len()).sum();
16667 let inter = m.experts[experts[0]].gate_proj.rows();
16668 let pairs: Vec<(&QTensor, &QTensor)> = experts
16669 .iter()
16670 .map(|&e| (&m.experts[e].gate_proj, &m.experts[e].up_proj))
16671 .collect();
16672 let mut gs: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; inter]).collect();
16673 if !QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut gs, pool) {
16674 per_row(&mut out);
16675 return out;
16676 }
16677 let downs: Vec<&QTensor> = experts.iter().map(|&e| &m.experts[e].down_proj).collect();
16678 let lens: Vec<usize> = groups.iter().map(|g| g.len()).collect();
16679 let mut ds: Vec<Vec<f32>> = (0..n_pairs).map(|_| vec![0f32; hidden]).collect();
16680 if !QTensor::moe_down_rows(&downs, &lens, &gs, &mut ds, pool) {
16681 per_row(&mut out);
16682 return out;
16683 }
16684 let mut slot = std::collections::HashMap::with_capacity(n_pairs);
16686 let mut p = 0usize;
16687 for (g, &e) in experts.iter().enumerate() {
16688 for &r in &groups[g] {
16689 slot.insert((r, e), p);
16690 p += 1;
16691 }
16692 }
16693 for (r, (idx, w)) in routes.iter().enumerate() {
16694 let terms: Vec<(&[f32], f32)> = idx
16695 .iter()
16696 .zip(w)
16697 .map(|(&e, &we)| (ds[slot[&(r, e)]].as_slice(), we))
16698 .collect();
16699 let row = &mut out[r * hidden..(r + 1) * hidden];
16700 for (i, dst) in row.iter_mut().enumerate() {
16701 let mut acc = 0f32;
16703 for (d, we) in &terms {
16704 acc += we * d[i];
16705 }
16706 *dst = acc;
16707 }
16708 }
16709 out
16710}
16711
16712thread_local! {
16713 static FFN_SCRATCH: std::cell::RefCell<[Vec<f32>; 4]> =
16717 const { std::cell::RefCell::new([Vec::new(), Vec::new(), Vec::new(), Vec::new()]) };
16718}
16719
16720fn dense_ffn(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
16722 if gate_topk() > 0
16725 && let Some(out) = dense_ffn_dynamic(d, x, pool, gate_topk())
16726 {
16727 return out;
16728 }
16729 let prism_body = d.gate_proj.has_prism_contract()
16745 || d.up_proj.has_prism_contract()
16746 || d.down_proj.has_prism_contract();
16747 if !prism_body
16748 && crate::gpu::enabled_here()
16749 && (d.gate_proj.rows() >= crate::gpu::min_rows() || d.gate_proj.is_q1())
16750 {
16751 let arm = if d.gate_proj.is_q1() && crate::gpu::q1_force() {
16752 crate::gpu::ProbeArm::Gpu
16753 } else {
16754 crate::gpu::probe_arm(crate::gpu::OpClass::Ffn)
16755 };
16756 match arm {
16757 crate::gpu::ProbeArm::Gpu => {
16758 let t0 = std::time::Instant::now();
16759 if let Some(out) = dense_ffn_gpu(d, x, pool) {
16760 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
16761 return out;
16762 }
16763 crate::gpu::probe_note_decline(crate::gpu::OpClass::Ffn);
16767 }
16768 crate::gpu::ProbeArm::CpuTimed => {
16769 let t0 = std::time::Instant::now();
16770 let out = crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
16771 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
16772 return out;
16773 }
16774 crate::gpu::ProbeArm::Cpu => {
16775 return crate::gpu::cpu_scope(|| dense_ffn_cpu(d, x, pool));
16776 }
16777 }
16778 }
16779 dense_ffn_cpu(d, x, pool)
16780}
16781
16782fn dense_ffn_cpu(d: &DenseFfn, x: &[f32], pool: Option<&Pool>) -> Vec<f32> {
16784 let inter = d.gate_proj.rows();
16785 FFN_SCRATCH.with(|s| {
16786 let mut s = s.borrow_mut();
16787 let [g, u, ..] = &mut *s;
16788 g.resize(inter, 0.0);
16789 if gate_topk() > 0 {
16792 u.resize(inter, 0.0);
16796 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
16797 for i in 0..inter {
16798 g[i] = Act::Silu.combine(g[i], 1.0);
16799 }
16800 keep_top_k(g, gate_topk());
16801 for i in 0..inter {
16802 g[i] *= u[i];
16803 }
16804 } else if d.act == Act::Silu && {
16805 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
16806 QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool)
16807 } {
16808 } else {
16810 u.resize(inter, 0.0);
16811 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnGateUp);
16813 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
16814 for i in 0..inter {
16815 g[i] = d.act.combine(g[i], u[i]);
16816 }
16817 }
16818 FFN_PROBE.with(|pr| {
16826 if let Some(acc) = pr.borrow_mut().as_mut() {
16827 let li = crate::gpu::cur_layer();
16828 if li >= 0 {
16829 if let Some(row) = acc.get_mut(li as usize) {
16830 match probe_topk() {
16831 0 if probe_sq() => {
16832 for (a, &v) in row.iter_mut().zip(g.iter()) {
16833 *a += (v as f64) * (v as f64);
16834 }
16835 }
16836 0 if probe_signed() => {
16837 for (a, &v) in row.iter_mut().zip(g.iter()) {
16838 *a += v as f64;
16839 }
16840 }
16841 0 => {
16842 for (a, &v) in row.iter_mut().zip(g.iter()) {
16843 *a += (v as f64).abs();
16844 }
16845 }
16846 k => {
16847 let n = g.len();
16848 let k = k.min(n);
16849 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
16850 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
16851 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
16852 });
16853 let thr = *kth;
16854 for (a, &v) in row.iter_mut().zip(g.iter()) {
16855 if v.abs() >= thr {
16856 *a += 1.0;
16857 }
16858 }
16859 }
16860 }
16861 }
16862 }
16863 }
16864 });
16865 if oracle_topk() > 0 {
16866 keep_top_k(g, oracle_topk());
16867 }
16868 {
16869 let li = crate::gpu::cur_layer();
16870 if li >= 0 {
16871 adump_row(li as usize, g);
16872 }
16873 }
16874 let mut out = attention::take_buf(d.down_proj.rows());
16875 let _prof = crate::cpuprof::time(crate::cpuprof::Slot::FfnDown);
16876 d.down_proj.matvec(g, &mut out, pool);
16877 out
16878 })
16879}
16880
16881pub struct RefitAcc {
16894 pub support: Vec<u32>,
16895 pub gss: Vec<f32>,
16896 pub ya: Vec<f32>,
16897 pub hidden: usize,
16898 pub tokens: u64,
16899 pub buf_g: Vec<f32>,
16905 pub buf_o: Vec<f32>,
16906 pub buf_t: usize,
16907}
16908
16909type RefitState = (std::collections::HashMap<usize, RefitAcc>, Vec<f32>);
16913
16914static REFIT: std::sync::OnceLock<Option<(String, std::sync::Mutex<RefitState>)>> =
16915 std::sync::OnceLock::new();
16916
16917fn ffn_probe_active() -> bool {
16920 FFN_PROBE.with(|p| p.borrow().is_some())
16921}
16922
16923fn refit_dir() -> Option<&'static (String, std::sync::Mutex<RefitState>)> {
16924 REFIT
16925 .get_or_init(|| {
16926 std::env::var("CMF_FFN_REFIT").ok().map(|d| {
16927 (
16928 d,
16929 std::sync::Mutex::new((std::collections::HashMap::new(), Vec::new())),
16930 )
16931 })
16932 })
16933 .as_ref()
16934}
16935
16936fn refit_accumulate(
16938 li: usize,
16939 g: &[f32],
16940 b: usize,
16941 inter: usize,
16942 out: &[f32],
16943 hidden: usize,
16944 pool: Option<&Pool>,
16945) {
16946 let Some((dir, map)) = refit_dir() else {
16947 return;
16948 };
16949 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
16950 let (from, to) = *SPAN.get_or_init(|| {
16951 let g = |k: &str, d: usize| {
16952 std::env::var(k)
16953 .ok()
16954 .and_then(|v| v.parse().ok())
16955 .unwrap_or(d)
16956 };
16957 (
16958 g("CMF_FFN_REFIT_FROM", 0),
16959 g("CMF_FFN_REFIT_TO", usize::MAX),
16960 )
16961 });
16962 if li < from || li > to {
16963 return;
16964 }
16965 let mut guard = map.lock().unwrap();
16966 let (map, shared) = &mut *guard;
16967 let acc = match map.entry(li) {
16968 std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
16969 std::collections::hash_map::Entry::Vacant(e) => {
16970 let path = format!("{dir}/support.{li}.u32");
16971 let Ok(bytes) = std::fs::read(&path) else {
16972 eprintln!("refit: no {path} — layer {li} skipped");
16973 return;
16974 };
16975 let n = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
16976 let support: Vec<u32> = bytes[4..4 + n * 4]
16977 .chunks_exact(4)
16978 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
16979 .collect();
16980 eprintln!(
16981 "refit: layer {li} support {n} ({:.0} MB of accumulator)",
16982 (n * n + hidden * n) as f64 * 4.0 / 1e6
16983 );
16984 e.insert(RefitAcc {
16985 gss: vec![0.0; n * n],
16986 ya: vec![0.0; hidden * n],
16987 buf_g: Vec::new(),
16988 buf_o: Vec::new(),
16989 buf_t: 0,
16990 support,
16991 hidden,
16992 tokens: 0,
16993 })
16994 }
16995 };
16996 let ns = acc.support.len();
16997 let cap = refit_batch();
16999 if acc.buf_g.is_empty() {
17000 acc.buf_g = vec![0.0; ns * cap];
17001 acc.buf_o = vec![0.0; hidden * cap];
17002 }
17003 let take = b.min(cap - acc.buf_t);
17004 for t in 0..take {
17005 let col = acc.buf_t + t;
17006 for (j, &n) in acc.support.iter().enumerate() {
17007 acc.buf_g[j * cap + col] = g[t * inter + n as usize];
17008 }
17009 for h in 0..hidden {
17010 acc.buf_o[h * cap + col] = out[t * hidden + h];
17011 }
17012 }
17013 acc.buf_t += take;
17014 acc.tokens += take as u64;
17015 if acc.buf_t < cap {
17016 return;
17017 }
17018 let bt = acc.buf_t;
17019 acc.buf_t = 0;
17020 let RefitAcc {
17030 gss,
17031 ya,
17032 buf_g,
17033 buf_o,
17034 ..
17035 } = acc;
17036 let need = (ns * ns).max(hidden * ns);
17037 if shared.len() < need {
17038 shared.resize(need, 0.0);
17039 }
17040 let scratch = &mut shared[..];
17041 let _ = bt;
17042 if crate::gpu::gemm_nt_f32_transient(buf_g, buf_g, &mut scratch[..ns * ns], ns, cap, ns) {
17043 add_into(gss, &scratch[..ns * ns], pool);
17044 if crate::gpu::gemm_nt_f32_transient(
17045 buf_o,
17046 buf_g,
17047 &mut scratch[..hidden * ns],
17048 hidden,
17049 cap,
17050 ns,
17051 ) {
17052 add_into(ya, &scratch[..hidden * ns], pool);
17053 } else {
17054 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17055 }
17056 } else {
17057 accum_outer_t(gss, ns, ns, cap, buf_g, buf_g, pool);
17058 accum_outer_t(ya, hidden, ns, cap, buf_o, buf_g, pool);
17059 }
17060 }
17064
17065fn refit_batch() -> usize {
17067 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17068 *B.get_or_init(|| {
17069 std::env::var("CMF_FFN_REFIT_BATCH")
17070 .ok()
17071 .and_then(|v| v.parse().ok())
17072 .unwrap_or(4096)
17073 })
17074}
17075
17076fn accum_outer_t(
17079 c: &mut [f32],
17080 m: usize,
17081 n: usize,
17082 b: usize,
17083 left: &[f32],
17084 right: &[f32],
17085 pool: Option<&Pool>,
17086) {
17087 let ptr = SendMut(c.as_mut_ptr());
17088 let body = |i: usize| {
17089 let ptr = &ptr;
17090 let row = unsafe { std::slice::from_raw_parts_mut(ptr.0.add(i * n), n) };
17091 for t in 0..b {
17092 let a = left[i * b + t];
17093 if a == 0.0 {
17094 continue;
17095 }
17096 for (j, o) in row.iter_mut().enumerate() {
17097 *o += a * right[j * b + t];
17098 }
17099 }
17100 };
17101 match pool {
17102 Some(p) if m > 1 => p.run_rows(m, &|s, e| {
17103 for i in s..e {
17104 body(i);
17105 }
17106 }),
17107 _ => {
17108 for i in 0..m {
17109 body(i);
17110 }
17111 }
17112 }
17113}
17114
17115fn add_into(dst: &mut [f32], src: &[f32], pool: Option<&Pool>) {
17118 let n = dst.len().min(src.len());
17119 match pool {
17120 Some(p) if n >= 1 << 16 => {
17121 let ptr = SendMut(dst.as_mut_ptr());
17122 let f = |s: usize, e: usize| {
17123 let ptr = &ptr;
17124 for blk in s..e {
17125 let (a, b) = (blk * 4096, ((blk + 1) * 4096).min(n));
17126 for i in a..b {
17127 unsafe { *ptr.0.add(i) += src[i] };
17128 }
17129 }
17130 };
17131 p.run_rows(n.div_ceil(4096), &f);
17132 }
17133 _ => {
17134 for (d, v) in dst.iter_mut().zip(&src[..n]) {
17135 *d += *v;
17136 }
17137 }
17138 }
17139}
17140
17141fn accum_outer(
17146 c: &mut [f32],
17147 m: usize,
17148 n: usize,
17149 b: usize,
17150 left: &[f32],
17151 right: &[f32],
17152 pool: Option<&Pool>,
17153) {
17154 const TILE: usize = 32;
17155 let tiles = m.div_ceil(TILE);
17156 let cp = SendMut(c.as_mut_ptr());
17157 let body = |ti: usize| {
17158 let cp = &cp;
17159 let i0 = ti * TILE;
17160 let i1 = (i0 + TILE).min(m);
17161 for t in 0..b {
17162 let r = &right[t * n..t * n + n];
17163 for i in i0..i1 {
17164 let a = left[i * b + t];
17165 if a == 0.0 {
17166 continue;
17167 }
17168 let row = unsafe { std::slice::from_raw_parts_mut(cp.0.add(i * n), n) };
17170 for (o, v) in row.iter_mut().zip(r) {
17171 *o += a * *v;
17172 }
17173 }
17174 }
17175 };
17176 match pool {
17177 Some(p) if tiles > 1 => p.run_rows(tiles, &|s, e| {
17178 for ti in s..e {
17179 body(ti);
17180 }
17181 }),
17182 _ => {
17183 for ti in 0..tiles {
17184 body(ti);
17185 }
17186 }
17187 }
17188}
17189
17190pub fn refit_flush() -> usize {
17192 let Some((dir, map)) = refit_dir() else {
17193 return 0;
17194 };
17195 let guard = map.lock().unwrap();
17196 let mut n = 0;
17197 for (li, acc) in guard.0.iter() {
17198 let w = |name: &str, v: &[f32]| {
17201 let path = format!("{dir}/{name}.{li}.f32");
17202 let bytes: Vec<u8> = v.iter().flat_map(|x| x.to_le_bytes()).collect();
17203 match std::fs::write(&path, &bytes) {
17204 Ok(()) => {}
17205 Err(e) => eprintln!(
17206 "refit: FAILED to write {path} ({} MB): {e}",
17207 bytes.len() / 1_000_000
17208 ),
17209 }
17210 };
17211 w("gss", &acc.gss);
17212 w("ya", &acc.ya);
17213 println!(
17214 "refit L{li}: {} support, {} tokens, hidden {}",
17215 acc.support.len(),
17216 acc.tokens,
17217 acc.hidden
17218 );
17219 n += 1;
17220 }
17221 n
17222}
17223
17224fn adump_row(li: usize, g: &[f32]) {
17229 use std::io::Write as _;
17230 static FILES: std::sync::OnceLock<
17231 Option<(
17232 String,
17233 std::sync::Mutex<std::collections::HashMap<usize, std::fs::File>>,
17234 )>,
17235 > = std::sync::OnceLock::new();
17236 let Some((prefix, map)) = FILES
17237 .get_or_init(|| {
17238 std::env::var("CMF_FFN_ADUMP")
17239 .ok()
17240 .map(|p| (p, std::sync::Mutex::new(std::collections::HashMap::new())))
17241 })
17242 .as_ref()
17243 else {
17244 return;
17245 };
17246 static SPAN: std::sync::OnceLock<(usize, usize)> = std::sync::OnceLock::new();
17249 let (from, to) = *SPAN.get_or_init(|| {
17250 let g = |k: &str, d: usize| {
17251 std::env::var(k)
17252 .ok()
17253 .and_then(|v| v.parse().ok())
17254 .unwrap_or(d)
17255 };
17256 (
17257 g("CMF_FFN_ADUMP_FROM", 0),
17258 g("CMF_FFN_ADUMP_TO", usize::MAX),
17259 )
17260 });
17261 if li < from || li > to {
17262 return;
17263 }
17264 let mut map = map.lock().unwrap();
17265 let f = map.entry(li).or_insert_with(|| {
17266 std::fs::File::create(format!("{prefix}.{li}.f16")).expect("adump file")
17267 });
17268 let mut bytes = Vec::with_capacity(g.len() * 2);
17269 for v in g {
17270 bytes.extend_from_slice(&cortiq_core::quant::f32_to_f16(*v).to_le_bytes());
17271 }
17272 let _ = f.write_all(&bytes);
17273}
17274
17275fn oracle_topk() -> usize {
17281 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17282 *K.get_or_init(|| {
17283 std::env::var("CMF_FFN_ORACLE_TOPK")
17284 .ok()
17285 .and_then(|v| v.parse().ok())
17286 .unwrap_or(0)
17287 })
17288}
17289
17290fn gate_topk() -> usize {
17296 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17297 *K.get_or_init(|| {
17298 std::env::var("CMF_FFN_GATE_TOPK")
17299 .ok()
17300 .and_then(|v| v.parse().ok())
17301 .unwrap_or(0)
17302 })
17303}
17304
17305fn gate_block() -> usize {
17312 static B: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17313 *B.get_or_init(|| {
17314 std::env::var("CMF_FFN_GATE_BLOCK")
17315 .ok()
17316 .and_then(|v| v.parse().ok())
17317 .unwrap_or(1)
17318 })
17319}
17320
17321fn keep_top_blocks(g: &mut [f32], keep_n: usize, block: usize) {
17323 let n = g.len();
17324 let nb = n.div_ceil(block);
17325 let kb = (keep_n.div_ceil(block)).clamp(1, nb);
17326 if kb >= nb {
17327 return;
17328 }
17329 let mut score: Vec<f32> = (0..nb)
17330 .map(|b| {
17331 g[b * block..((b + 1) * block).min(n)]
17332 .iter()
17333 .map(|v| v * v)
17334 .sum::<f32>()
17335 })
17336 .collect();
17337 let mut ord = score.clone();
17338 let (_, kth, _) = ord.select_nth_unstable_by(kb - 1, |a, b| {
17339 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17340 });
17341 let thr = *kth;
17342 for b in 0..nb {
17343 if score[b] < thr {
17344 g[b * block..((b + 1) * block).min(n)].fill(0.0);
17345 }
17346 }
17347 score.clear();
17348}
17349
17350fn keep_top_k(g: &mut [f32], k: usize) {
17352 if gate_block() > 1 {
17353 return keep_top_blocks(g, k, gate_block());
17354 }
17355 let n = g.len();
17356 if k == 0 || k >= n {
17357 return;
17358 }
17359 let mut mag: Vec<f32> = g.iter().map(|v| v.abs()).collect();
17360 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17361 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17362 });
17363 let thr = *kth;
17364 for v in g.iter_mut() {
17365 if v.abs() < thr {
17366 *v = 0.0;
17367 }
17368 }
17369}
17370
17371fn probe_sq() -> bool {
17375 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17376 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SQ").is_ok())
17377}
17378
17379fn probe_signed() -> bool {
17383 static S: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
17384 *S.get_or_init(|| std::env::var("CMF_FFN_PROBE_SIGNED").is_ok())
17385}
17386
17387fn meanfill() -> Option<&'static (usize, Vec<f32>)> {
17395 static M: std::sync::OnceLock<Option<(usize, Vec<f32>)>> = std::sync::OnceLock::new();
17396 M.get_or_init(|| {
17397 let p = std::env::var("CMF_FFN_MEANFILL").ok()?;
17398 let b = std::fs::read(&p).ok()?;
17399 let inter = u32::from_le_bytes(b[4..8].try_into().ok()?) as usize;
17400 let vals: Vec<f32> = b[8..]
17401 .chunks_exact(4)
17402 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
17403 .collect();
17404 eprintln!("meanfill: {} value(s), inter {inter}", vals.len());
17405 Some((inter, vals))
17406 })
17407 .as_ref()
17408}
17409
17410fn probe_topk() -> usize {
17413 static K: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
17414 *K.get_or_init(|| {
17415 std::env::var("CMF_FFN_PROBE_TOPK")
17416 .ok()
17417 .and_then(|v| v.parse().ok())
17418 .unwrap_or(0)
17419 })
17420}
17421
17422thread_local! {
17423 static FFN_PROBE: std::cell::RefCell<Option<Vec<Vec<f64>>>> =
17426 const { std::cell::RefCell::new(None) };
17427}
17428
17429fn dense_ffn_dynamic(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, k: usize) -> Option<Vec<f32>> {
17442 if d.gate_proj.has_prism_contract()
17447 || d.up_proj.has_prism_contract()
17448 || d.down_proj.has_prism_contract()
17449 {
17450 return None;
17451 }
17452 let dt = d.down_t.as_ref()?;
17453 let inter = d.gate_proj.rows();
17454 let hidden = dt.cols();
17455 if k == 0 || k >= inter || d.act != Act::Silu {
17456 return None;
17457 }
17458 DYN_SCRATCH.with(|sc| {
17459 let mut sc = sc.borrow_mut();
17460 let DynScratch {
17461 g,
17462 mag,
17463 live,
17464 parts,
17465 } = &mut *sc;
17466 g.resize(inter, 0.0);
17467 d.gate_proj.matvec(x, g, pool);
17468 for v in g.iter_mut() {
17469 *v = inference::silu(*v);
17470 }
17471 mag.clear();
17474 mag.extend(g.iter().map(|v| v.abs()));
17475 let (_, kth, _) = mag.select_nth_unstable_by(k - 1, |a, b| {
17476 b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
17477 });
17478 let thr = *kth;
17479 live.clear();
17480 live.extend((0..inter as u32).filter(|&n| g[n as usize].abs() >= thr));
17481 let mut out = vec![0.0f32; hidden];
17482 match pool {
17483 Some(p) if live.len() >= 64 => {
17484 let nw = p.n_workers() + 1;
17485 parts.clear();
17486 parts.resize(nw * hidden, 0.0);
17487 let ptr = SendMut(parts.as_mut_ptr());
17488 let n = live.len();
17489 let live_ref: &[u32] = live;
17490 let g_ref: &[f32] = g;
17491 p.run(&|w, workers| {
17492 let chunk = n.div_ceil(workers);
17493 let (s, e) = (w * chunk, ((w + 1) * chunk).min(n));
17494 if s >= e {
17495 return;
17496 }
17497 WORKER_SCRATCH.with(|ws| {
17498 let mut ws = ws.borrow_mut();
17499 let [scratch, acc] = &mut *ws;
17500 scratch.resize(hidden.max(x.len()), 0.0);
17501 acc.clear();
17502 acc.resize(hidden, 0.0);
17503 for (o, &nrm) in live_ref[s..e].iter().enumerate() {
17504 if let Some(&nx) = live_ref[s..e].get(o + 1) {
17507 d.up_proj.prefetch_row(nx as usize);
17508 dt.prefetch_row(nx as usize);
17509 }
17510 let idx = nrm as usize;
17511 let up = d.up_proj.row_dot(idx, x, scratch);
17512 let a = g_ref[idx] * up;
17513 if a != 0.0 {
17514 dt.add_row_scaled(idx, a, acc, scratch);
17515 }
17516 }
17517 for (j, v) in acc.iter().enumerate() {
17518 unsafe { *ptr.at(w * hidden + j) = *v };
17519 }
17520 });
17521 });
17522 for w in 0..nw {
17523 for (j, o) in out.iter_mut().enumerate() {
17524 *o += parts[w * hidden + j];
17525 }
17526 }
17527 }
17528 _ => {
17529 WORKER_SCRATCH.with(|ws| {
17530 let mut ws = ws.borrow_mut();
17531 let [scratch, _acc] = &mut *ws;
17532 scratch.resize(hidden.max(x.len()), 0.0);
17533 for &nrm in live.iter() {
17534 let idx = nrm as usize;
17535 let up = d.up_proj.row_dot(idx, x, scratch);
17536 let a = g[idx] * up;
17537 if a != 0.0 {
17538 dt.add_row_scaled(idx, a, &mut out, scratch);
17539 }
17540 }
17541 });
17542 }
17543 }
17544 Some(out)
17545 })
17546}
17547
17548struct DynScratch {
17551 g: Vec<f32>,
17552 mag: Vec<f32>,
17553 live: Vec<u32>,
17554 parts: Vec<f32>,
17555}
17556
17557thread_local! {
17558 static DYN_SCRATCH: std::cell::RefCell<DynScratch> = const {
17559 std::cell::RefCell::new(DynScratch {
17560 g: Vec::new(),
17561 mag: Vec::new(),
17562 live: Vec::new(),
17563 parts: Vec::new(),
17564 })
17565 };
17566 static WORKER_SCRATCH: std::cell::RefCell<[Vec<f32>; 2]> =
17568 const { std::cell::RefCell::new([Vec::new(), Vec::new()]) };
17569}
17570
17571fn dense_ffn_masked(d: &DenseFfn, x: &[f32], pool: Option<&Pool>, mask_row: &[u8]) -> Vec<f32> {
17576 let inter = d.gate_proj.rows();
17577 FFN_SCRATCH.with(|s| {
17578 let mut s = s.borrow_mut();
17579 let [g, u, ..] = &mut *s;
17580 g.resize(inter, 0.0);
17581 if d.act == Act::Silu && QTensor::matvec_silu_mul(&d.gate_proj, &d.up_proj, x, g, pool) {
17582 } else {
17584 u.resize(inter, 0.0);
17585 QTensor::matvec_many([&d.gate_proj, &d.up_proj], x, [g, u], pool);
17586 for i in 0..inter {
17587 g[i] = d.act.combine(g[i], u[i]);
17588 }
17589 }
17590 zero_masked_cols(g, 1, inter, mask_row);
17591 let mut out = attention::take_buf(d.down_proj.rows());
17592 d.down_proj.matvec(g, &mut out, pool);
17593 out
17594 })
17595}
17596
17597fn dense_ffn_gpu(d: &DenseFfn, x: &[f32], _pool: Option<&Pool>) -> Option<Vec<f32>> {
17603 if d.gate_proj.has_prism_contract()
17604 || d.up_proj.has_prism_contract()
17605 || d.down_proj.has_prism_contract()
17606 {
17607 return None;
17608 }
17609 if d.act != Act::Silu {
17611 return None;
17612 }
17613 if d.gate_proj.rows() < crate::gpu::min_rows() && !d.gate_proj.is_q1() {
17616 return None;
17617 }
17618 let mut jobs: Vec<crate::gpu::MoeJob> = Vec::with_capacity(1);
17619 let mut model_ref = None;
17620 moe_push_job(d, x, 1.0, &mut jobs, &mut model_ref)?;
17621 let model = model_ref?;
17622 let hidden = jobs[0].down.1;
17623 let mut out = attention::take_buf(hidden);
17624 if crate::gpu::moe_block(&model, &jobs, &mut out) {
17625 Some(out)
17626 } else {
17627 let mut out = out;
17628 attention::recycle_buf(&mut out);
17629 None
17630 }
17631}
17632
17633#[allow(clippy::type_complexity)]
17638#[allow(clippy::type_complexity)]
17639pub(crate) fn moe_parts(
17640 t: &QTensor,
17641) -> Option<(
17642 &std::sync::Arc<cortiq_core::CmfModel>,
17643 usize,
17644 usize,
17645 usize,
17646 &[f32],
17647 &[f32],
17648 bool,
17649 bool,
17650 bool,
17651)> {
17652 match t {
17653 QTensor::Mapped {
17654 model,
17655 idx,
17656 dtype: dt @ (cortiq_core::TensorDtype::Q8_2f | cortiq_core::TensorDtype::Q8Row),
17657 rows,
17658 cols,
17659 row_scale,
17660 col_field,
17661 ..
17662 } if (*dt == cortiq_core::TensorDtype::Q8Row) || !col_field.is_empty() => Some((
17663 model, *idx, *rows, *cols, row_scale, col_field, false, false, false,
17664 )),
17665 QTensor::Mapped {
17667 model,
17668 idx,
17669 dtype: cortiq_core::TensorDtype::Q1,
17670 rows,
17671 cols,
17672 ..
17673 } => Some((
17674 model,
17675 *idx,
17676 *rows,
17677 *cols,
17678 &[][..],
17679 &[][..],
17680 true,
17681 false,
17682 false,
17683 )),
17684 QTensor::Mapped {
17686 model,
17687 idx,
17688 dtype: cortiq_core::TensorDtype::Q4Tiled,
17689 rows,
17690 cols,
17691 ..
17692 } => Some((
17693 model,
17694 *idx,
17695 *rows,
17696 *cols,
17697 &[][..],
17698 &[][..],
17699 false,
17700 true,
17701 false,
17702 )),
17703 QTensor::Mapped {
17705 model,
17706 idx,
17707 dtype: cortiq_core::TensorDtype::Q4TiledP,
17708 rows,
17709 cols,
17710 ..
17711 } => Some((
17712 model,
17713 *idx,
17714 *rows,
17715 *cols,
17716 &[][..],
17717 &[][..],
17718 false,
17719 true,
17720 false,
17721 )),
17722 QTensor::Mapped {
17726 model,
17727 idx,
17728 dtype: cortiq_core::TensorDtype::Q2TiledP,
17729 rows,
17730 cols,
17731 ..
17732 } => Some((
17733 model,
17734 *idx,
17735 *rows,
17736 *cols,
17737 &[][..],
17738 &[][..],
17739 false,
17740 true,
17741 true,
17742 )),
17743 _ => None,
17744 }
17745}
17746
17747#[cfg(target_os = "macos")]
17755fn metal_moe_graph_parts(m: &MoeFfn, hidden: usize) -> Option<crate::gpu::GpuMoe<'_>> {
17756 if m.router_input_norm
17757 || m.route_tau.is_some()
17758 || m.mask.is_some()
17759 || m.per_expert_scale.is_some()
17760 || m.experts.is_empty()
17761 || m.top_k == 0
17762 || m.resonance.is_some()
17763 {
17764 return None;
17765 }
17766 let (sh, sg) = match &m.shared {
17769 Some((sh, sg)) => (sh, sg.as_ref()),
17770 None => return None,
17771 };
17772 let (rf, rr, rc) = m.router.f32_parts()?;
17773 if rr != m.experts.len() || rc != hidden {
17774 return None;
17775 }
17776 let shared_gated = sg.is_some();
17777 let sf = match sg {
17778 Some(sg) => {
17779 let (sf, sr, sc) = sg.f32_parts()?;
17780 if sr * sc != hidden {
17781 return None;
17782 }
17783 sf
17784 }
17785 None => &rf[..hidden],
17788 };
17789 if let Some(b) = &m.expert_bias {
17790 if b.len() != m.experts.len() {
17791 return None;
17792 }
17793 }
17794 let inter = m.experts[0].gate_proj.rows();
17795 let gu_q2 = m.experts[0].gate_proj.mapped_q2tp().is_some();
17798 let trio = |e: &DenseFfn| -> Option<(usize, usize, usize)> {
17799 if e.act != Act::Silu
17800 || e.gate_proj.rows() != inter
17801 || e.gate_proj.cols() != hidden
17802 || e.up_proj.rows() != inter
17803 || e.up_proj.cols() != hidden
17804 || e.down_proj.rows() != hidden
17805 || e.down_proj.cols() != inter
17806 {
17807 return None;
17808 }
17809 let pick = |t: &QTensor| -> Option<usize> {
17810 if gu_q2 {
17811 t.mapped_q2tp().map(|(_, i)| i)
17812 } else {
17813 t.mapped_q4tp().map(|(_, i)| i)
17814 }
17815 };
17816 Some((
17817 pick(&e.gate_proj)?,
17818 pick(&e.up_proj)?,
17819 e.down_proj.mapped_q4tp().map(|(_, i)| i)?,
17820 ))
17821 };
17822 let experts = m.experts.iter().map(trio).collect::<Option<Vec<_>>>()?;
17823 let shared = trio(sh)?;
17824 Some(crate::gpu::GpuMoe {
17825 router: rf,
17826 sgate: sf,
17827 experts,
17828 shared,
17829 n_exp: m.experts.len(),
17830 top_k: m.top_k,
17831 inter,
17832 norm_topk: m.norm_topk_prob,
17833 route_scale: m.routed_scaling,
17834 gu_q2,
17835 sigmoid: m.router_sigmoid,
17836 bias: m.expert_bias.as_deref(),
17837 shared_gated,
17838 })
17839}
17840
17841pub(crate) fn moe_push_job_parts<'a>(
17845 gate: &'a QTensor,
17846 up: &'a QTensor,
17847 down: &'a QTensor,
17848 x: &[f32],
17849 w: f32,
17850 swiglu_limit: f32,
17851 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
17852 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
17853) -> Option<()> {
17854 use crate::qtensor::prescale;
17855 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(gate)?;
17856 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(up)?;
17857 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(down)?;
17858 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
17859 return None; }
17861 if gq2 && (dq2 || !dq4 || down.mapped_q4tp().is_none()) {
17864 return None;
17865 }
17866 if !gq2 && dq2 {
17867 return None;
17868 }
17869 model_ref.get_or_insert_with(|| gm.clone());
17870 let dt = |cf: &[f32]| {
17871 if cf.is_empty() {
17872 cortiq_core::TensorDtype::Q8Row
17873 } else {
17874 cortiq_core::TensorDtype::Q8_2f
17875 }
17876 };
17877 jobs.push(crate::gpu::MoeJob {
17878 gate: (gi, gr, gc, grs),
17879 up: (ui, ur, uc, urs),
17880 down: (di, dr, dc, drs),
17881 xs_gate: prescale(x, gcf, dt(gcf)).into_owned(),
17882 xs_up: prescale(x, ucf, dt(ucf)).into_owned(),
17883 down_col: dcf,
17884 w,
17885 q1: gq1,
17886 q4t: gq4 && !gq2 && gate.mapped_q4tp().is_none(),
17887 q4tp: gq4 && (gq2 || gate.mapped_q4tp().is_some()),
17888 gu_q2: gq2,
17889 swiglu_limit,
17890 });
17891 Some(())
17892}
17893
17894fn moe_push_job<'a>(
17896 d: &'a DenseFfn,
17897 x: &[f32],
17898 w: f32,
17899 jobs: &mut Vec<crate::gpu::MoeJob<'a>>,
17900 model_ref: &mut Option<std::sync::Arc<cortiq_core::CmfModel>>,
17901) -> Option<()> {
17902 use crate::qtensor::prescale;
17903 if d.act != Act::Silu {
17904 return None; }
17906 let (gm, gi, gr, gc, grs, gcf, gq1, gq4, gq2) = moe_parts(&d.gate_proj)?;
17907 let (_, ui, ur, uc, urs, ucf, uq1, uq4, uq2) = moe_parts(&d.up_proj)?;
17908 let (_, di, dr, dc, drs, dcf, dq1, dq4, dq2) = moe_parts(&d.down_proj)?;
17909 if gq1 != uq1 || uq1 != dq1 || gq4 != uq4 || uq4 != dq4 || gq2 != uq2 {
17910 return None; }
17912 if gq2 && (dq2 || !dq4 || d.down_proj.mapped_q4tp().is_none()) {
17913 return None;
17914 }
17915 if !gq2 && dq2 {
17916 return None;
17917 }
17918 model_ref.get_or_insert_with(|| gm.clone());
17919 let gdt = if gcf.is_empty() {
17920 cortiq_core::TensorDtype::Q8Row
17921 } else {
17922 cortiq_core::TensorDtype::Q8_2f
17923 };
17924 let udt = if ucf.is_empty() {
17925 cortiq_core::TensorDtype::Q8Row
17926 } else {
17927 cortiq_core::TensorDtype::Q8_2f
17928 };
17929 jobs.push(crate::gpu::MoeJob {
17930 gate: (gi, gr, gc, grs),
17931 up: (ui, ur, uc, urs),
17932 down: (di, dr, dc, drs),
17933 xs_gate: prescale(x, gcf, gdt).into_owned(),
17934 xs_up: prescale(x, ucf, udt).into_owned(),
17935 down_col: dcf,
17936 w,
17937 q1: gq1,
17938 q4t: gq4 && !gq2 && d.gate_proj.mapped_q4tp().is_none(),
17939 q4tp: gq4 && (gq2 || d.gate_proj.mapped_q4tp().is_some()),
17940 gu_q2: gq2,
17941 swiglu_limit: 0.0,
17942 });
17943 Some(())
17944}
17945
17946fn sparse_ffn_quant(
17953 d: &DenseFfn,
17954 x: &[f32],
17955 active: &[u16],
17956 hidden: usize,
17957 pool: Option<&Pool>,
17958) -> Vec<f32> {
17959 let n = active.len();
17960 let inter = d.gate_proj.rows();
17961 let mut act = vec![0.0f32; n];
17962 let need_scratch = !(d.gate_proj.sparse_col_ok() && d.up_proj.sparse_col_ok());
17965 let compute = |ai: usize| -> f32 {
17966 let idx = active[ai] as usize;
17967 if idx >= inter {
17968 return 0.0; }
17970 let mut s = if need_scratch {
17971 vec![0.0f32; hidden]
17972 } else {
17973 Vec::new()
17974 };
17975 let gate = d.gate_proj.row_dot(idx, x, &mut s);
17976 let up = d.up_proj.row_dot(idx, x, &mut s);
17977 d.act.combine(gate, up)
17978 };
17979 match pool {
17980 Some(p) if n >= 256 => {
17981 let ptr = SendMut(act.as_mut_ptr());
17982 p.run(&|widx, nw| {
17983 let chunk = n.div_ceil(nw);
17984 let (s, e) = (widx * chunk, ((widx + 1) * chunk).min(n));
17985 for ai in s..e {
17986 unsafe { *ptr.at(ai) = compute(ai) };
17987 }
17988 });
17989 }
17990 _ => {
17991 for (ai, a) in act.iter_mut().enumerate() {
17992 *a = compute(ai);
17993 }
17994 }
17995 }
17996 let mut out = vec![0.0f32; hidden];
17998 for (ai, &idx) in active.iter().enumerate() {
17999 let w = act[ai];
18000 if w.abs() >= 1e-12 && (idx as usize) < inter {
18001 d.down_proj.add_col_scaled(idx as usize, w, &mut out);
18002 }
18003 }
18004 out
18005}
18006
18007#[doc(hidden)]
18009pub fn sparse_ffn_quant_for_test(
18010 d: &DenseFfn,
18011 x: &[f32],
18012 active: &[u16],
18013 hidden: usize,
18014) -> Vec<f32> {
18015 sparse_ffn_quant(d, x, active, hidden, None)
18016}
18017
18018fn dequant_dense_f32(d: &DenseFfn) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
18022 let deq = |t: &QTensor| -> Vec<f32> {
18023 let (rows, cols) = (t.rows(), t.cols());
18024 let mut out = vec![0.0f32; rows * cols];
18025 for r in 0..rows {
18026 t.row_f32(r, &mut out[r * cols..(r + 1) * cols]);
18027 }
18028 out
18029 };
18030 (deq(&d.gate_proj), deq(&d.up_proj), deq(&d.down_proj))
18031}
18032
18033struct SendMut(*mut f32);
18035unsafe impl Send for SendMut {}
18036unsafe impl Sync for SendMut {}
18037impl SendMut {
18038 #[inline]
18039 #[allow(clippy::mut_from_ref)]
18042 unsafe fn at(&self, i: usize) -> &mut f32 {
18043 unsafe { &mut *self.0.add(i) }
18044 }
18045}
18046
18047pub(crate) fn moe_route(
18060 logits: &[f32],
18061 m: &MoeFfn,
18062 allowed: Option<&[bool]>,
18063) -> (Vec<usize>, Vec<f32>, f32) {
18064 moe_route_with_eps(logits, m, allowed, 1e-6)
18065}
18066
18067pub(crate) fn moe_route_with_eps(
18076 logits: &[f32],
18077 m: &MoeFfn,
18078 allowed: Option<&[bool]>,
18079 sigmoid_denom_eps: f32,
18080) -> (Vec<usize>, Vec<f32>, f32) {
18081 let ne = logits.len();
18082 let admit = |e: usize| {
18088 m.mask.as_ref().is_none_or(|mk| mk[e])
18089 && allowed.is_none_or(|a| a.get(e).copied().unwrap_or(false))
18090 };
18091 if m.resonance.is_some() && m.top_k == 1 {
18104 let mut best: Option<usize> = None;
18105 for e in (0..ne).filter(|&e| admit(e)) {
18106 let l = logits[e];
18107 if l == f32::NEG_INFINITY || l.is_nan() {
18108 continue;
18109 }
18110 if best.is_none_or(|b| l > logits[b]) {
18111 best = Some(e);
18112 }
18113 }
18114 if let Some(b) = best {
18115 let mut p = vec![0.0f32; ne];
18116 p[b] = 1.0;
18117 return (vec![b], p, 1.0 / m.routed_scaling);
18118 }
18119 }
18120 let p: Vec<f32> = if m.router_sigmoid {
18126 logits.iter().map(|&l| 1.0 / (1.0 + (-l).exp())).collect()
18127 } else {
18128 let mx = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
18129 if mx == f32::NEG_INFINITY {
18130 vec![1.0 / ne.max(1) as f32; ne]
18131 } else {
18132 let mut e: Vec<f32> = logits.iter().map(|&l| (l - mx).exp()).collect();
18133 let s: f32 = e.iter().sum();
18134 for v in &mut e {
18135 *v /= s;
18136 }
18137 e
18138 }
18139 };
18140 let mut idx: Vec<usize> = (0..ne).filter(|&e| admit(e)).collect();
18141 match &m.expert_bias {
18143 Some(b) => idx.sort_unstable_by(|&x, &y| {
18144 (p[y] + b[y])
18145 .partial_cmp(&(p[x] + b[x]))
18146 .unwrap()
18147 .then(x.cmp(&y))
18148 }),
18149 None => idx.sort_unstable_by(|&x, &y| p[y].partial_cmp(&p[x]).unwrap().then(x.cmp(&y))),
18150 }
18151 idx.truncate(m.top_k);
18152 if let Some(tau) = m.route_tau {
18156 let total: f32 = idx.iter().map(|&e| p[e]).sum();
18157 if total > 0.0 {
18158 let mut acc = 0.0f32;
18159 let mut keep = idx.len();
18160 for (i, &e) in idx.iter().enumerate() {
18161 acc += p[e];
18162 if acc >= tau * total {
18163 keep = i + 1;
18164 break;
18165 }
18166 }
18167 idx.truncate(keep);
18168 }
18169 }
18170 let wsum: f32 = if m.norm_topk_prob {
18171 let s: f32 = idx.iter().map(|&e| p[e]).sum();
18172 (if m.router_sigmoid {
18176 s + sigmoid_denom_eps
18177 } else {
18178 s
18179 }) / m.routed_scaling
18180 } else {
18181 1.0 / m.routed_scaling
18182 };
18183 (idx, p, wsum)
18184}
18185
18186fn moe_trace(idx: &[usize]) {
18188 moe_trace_at(crate::gpu::cur_layer() as i32, idx)
18189}
18190
18191pub(crate) fn moe_trace_at(li: i32, idx: &[usize]) {
18194 use std::io::Write;
18195 static F: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
18196 std::sync::OnceLock::new();
18197 let Some(f) = F.get_or_init(|| {
18198 let p = std::env::var("CMF_MOE_TRACE").ok()?;
18199 Some(std::sync::Mutex::new(
18200 std::fs::OpenOptions::new()
18201 .create(true)
18202 .append(true)
18203 .open(p)
18204 .ok()?,
18205 ))
18206 }) else {
18207 return;
18208 };
18209 let ids: Vec<String> = idx.iter().map(|e| e.to_string()).collect();
18210 let _ = writeln!(f.lock().unwrap(), "{li}:{}", ids.join(","));
18211}
18212
18213pub(crate) fn moe_ffn(
18216 m: &MoeFfn,
18217 x: &[f32],
18218 pool: Option<&Pool>,
18219 allowed: Option<&[bool]>,
18220) -> Vec<f32> {
18221 let r = moe_ffn_route(m, x, pool, allowed);
18222 moe_ffn_experts(m, x, &r, pool)
18223}
18224
18225pub(crate) struct MoeRoute {
18229 pub idx: Vec<usize>,
18230 pub p: Vec<f32>,
18231 pub wsum: f32,
18232 pub logits: Vec<f32>,
18233}
18234
18235pub(crate) fn moe_ffn_route(
18241 m: &MoeFfn,
18242 x: &[f32],
18243 pool: Option<&Pool>,
18244 allowed: Option<&[bool]>,
18245) -> MoeRoute {
18246 accumulate_act(m, x, 1);
18247 let ne = m.experts.len();
18248 let mut logits = vec![0.0f32; ne];
18249 match &m.resonance {
18250 Some(r) => r.scores(x, &mut logits),
18251 None => m.router.matvec(x, &mut logits, pool),
18252 }
18253 let (idx, p, wsum) = moe_route(&logits, m, allowed);
18254 {
18255 let mut st = m.stats.borrow_mut();
18256 if st.len() < ne {
18257 st.resize(ne, 0);
18258 }
18259 for &e in &idx {
18260 st[e] += 1;
18261 }
18262 }
18263 moe_trace(&idx);
18269 MoeRoute {
18270 idx,
18271 p,
18272 wsum,
18273 logits,
18274 }
18275}
18276
18277pub(crate) fn moe_ffn_experts(
18280 m: &MoeFfn,
18281 x: &[f32],
18282 r: &MoeRoute,
18283 pool: Option<&Pool>,
18284) -> Vec<f32> {
18285 let (idx, p, wsum) = (&r.idx, &r.p, r.wsum);
18286 if crate::gpu::enabled_here() {
18291 match crate::gpu::probe_arm(crate::gpu::OpClass::Ffn) {
18292 crate::gpu::ProbeArm::Gpu => {
18293 let t0 = std::time::Instant::now();
18294 if let Some(out) = moe_ffn_gpu(m, x, &idx, &p, wsum, pool) {
18295 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, true, t0.elapsed());
18296 return out;
18297 }
18298 }
18299 crate::gpu::ProbeArm::CpuTimed => {
18300 let t0 = std::time::Instant::now();
18301 let out = crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18302 crate::gpu::probe_record(crate::gpu::OpClass::Ffn, false, t0.elapsed());
18303 return out;
18304 }
18305 crate::gpu::ProbeArm::Cpu => {
18306 return crate::gpu::cpu_scope(|| moe_ffn_cpu(m, x, &idx, &p, wsum, pool));
18307 }
18308 }
18309 }
18310 moe_ffn_cpu(m, x, &idx, &p, wsum, pool)
18311}
18312
18313fn moe_ffn_banked(
18317 slot: &mut crate::mimo_moe::Slot,
18318 li: usize,
18319 m: &MoeFfn,
18320 x: &[f32],
18321 pool: Option<&Pool>,
18322) -> Vec<f32> {
18323 let t0 = std::time::Instant::now();
18324 let r = moe_ffn_route(m, x, pool, None);
18325 slot.note_route(t0.elapsed().as_nanos() as u64);
18326 match slot.forward(li, m, x, &r, pool) {
18327 Some(out) => out,
18328 None => crate::qtensor::float_activations_scope(|| {
18329 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, &r, pool))
18330 }),
18331 }
18332}
18333
18334fn moe_ffn_banked_rows(
18336 slot: &mut crate::mimo_moe::Slot,
18337 li: usize,
18338 m: &MoeFfn,
18339 xs: &[f32],
18340 b: usize,
18341 hidden: usize,
18342 pool: Option<&Pool>,
18343) -> Vec<f32> {
18344 let t0 = std::time::Instant::now();
18345 let routes: Vec<_> = xs
18346 .chunks_exact(hidden)
18347 .map(|x| moe_ffn_route(m, x, pool, None))
18348 .collect();
18349 slot.note_route(t0.elapsed().as_nanos() as u64);
18350 if let Some(out) = slot.forward_rows(li, m, xs, &routes, pool) {
18351 return out;
18352 }
18353 let mut out = Vec::with_capacity(b * hidden);
18354 for (x, r) in xs.chunks_exact(hidden).zip(&routes) {
18355 let row = slot.forward(li, m, x, r, pool).unwrap_or_else(|| {
18356 crate::qtensor::float_activations_scope(|| {
18358 crate::gpu::cpu_scope(|| moe_ffn_experts(m, x, r, pool))
18359 })
18360 });
18361 out.extend(row);
18362 }
18363 out
18364}
18365
18366fn graph_note(built: bool, layers_run: usize, total_layers: usize) {
18371 use std::sync::atomic::{AtomicBool, Ordering};
18372 if built {
18373 GRAPH_TOK_OK.fetch_add(1, Ordering::Relaxed);
18374 if total_layers > 0 && layers_run < total_layers {
18375 GRAPH_TOK_PREFIX.fetch_add(1, Ordering::Relaxed);
18376 } else {
18377 GRAPH_TOK_FULL.fetch_add(1, Ordering::Relaxed);
18378 }
18379 } else {
18380 GRAPH_TOK_MISS.fetch_add(1, Ordering::Relaxed);
18381 }
18382 static SAID: AtomicBool = AtomicBool::new(false);
18383 if !SAID.swap(true, Ordering::Relaxed) {
18384 if built {
18385 tracing::info!("wgpu whole-token graph: ACTIVE");
18386 } else {
18387 tracing::warn!("wgpu whole-token graph refused — per-op path");
18388 }
18389 }
18390}
18391
18392pub static GRAPH_TOK_OK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18396pub static GRAPH_TOK_MISS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18397pub static GRAPH_TOK_PREFIX: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18401pub static GRAPH_TOK_FULL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
18403
18404pub static METAL_GRAPH_TOK_OK: std::sync::atomic::AtomicU64 =
18408 std::sync::atomic::AtomicU64::new(0);
18409pub static METAL_GRAPH_HEAD_OK: std::sync::atomic::AtomicU64 =
18410 std::sync::atomic::AtomicU64::new(0);
18411pub static METAL_GRAPH_HEAD_MISS: std::sync::atomic::AtomicU64 =
18412 std::sync::atomic::AtomicU64::new(0);
18413pub static METAL_GRAPH_LAYERS: std::sync::atomic::AtomicU64 =
18414 std::sync::atomic::AtomicU64::new(0);
18415pub static METAL_GRAPH_ERRORS: std::sync::atomic::AtomicU64 =
18416 std::sync::atomic::AtomicU64::new(0);
18417pub static METAL_PREFILL_CHUNKS: std::sync::atomic::AtomicU64 =
18421 std::sync::atomic::AtomicU64::new(0);
18422pub static METAL_PREFILL_ROWS: std::sync::atomic::AtomicU64 =
18423 std::sync::atomic::AtomicU64::new(0);
18424pub static METAL_PREFILL_HEAD_ROWS: std::sync::atomic::AtomicU64 =
18425 std::sync::atomic::AtomicU64::new(0);
18426pub static METAL_PREFILL_ERRORS: std::sync::atomic::AtomicU64 =
18427 std::sync::atomic::AtomicU64::new(0);
18428
18429fn moe_batch_enabled() -> bool {
18432 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
18433 *ON.get_or_init(|| std::env::var("CMF_MOE_BATCH").as_deref() != Ok("0"))
18434}
18435
18436fn moe_ffn_cpu_batched(
18442 m: &MoeFfn,
18443 x: &[f32],
18444 idx: &[usize],
18445 p: &[f32],
18446 wsum: f32,
18447 pool: Option<&Pool>,
18448) -> Option<Vec<f32>> {
18449 if idx.is_empty() || !moe_batch_enabled() {
18450 return None;
18451 }
18452 if FFN_PROBE.with(|pr| pr.borrow().is_some()) {
18456 return None;
18457 }
18458 let n = idx.len() + usize::from(m.shared.is_some());
18459 let mut pairs = Vec::with_capacity(n);
18460 let mut downs = Vec::with_capacity(n);
18461 let mut ws = Vec::with_capacity(n);
18462 for &e in idx {
18463 let d = &m.experts[e];
18464 if d.act != Act::Silu {
18465 return None;
18466 }
18467 pairs.push((&d.gate_proj, &d.up_proj));
18468 downs.push(&d.down_proj);
18469 ws.push(p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]));
18470 }
18471 if let Some((se, gate)) = &m.shared {
18474 if se.act != Act::Silu {
18475 return None;
18476 }
18477 let g = gate.as_ref().map_or(1.0, |gate| {
18478 let mut gl = [0.0f32; 1];
18479 gate.matvec(x, &mut gl, pool);
18480 1.0 / (1.0 + (-gl[0]).exp())
18481 });
18482 pairs.push((&se.gate_proj, &se.up_proj));
18483 downs.push(&se.down_proj);
18484 ws.push(g);
18485 }
18486 let inter = pairs[0].0.rows();
18487 let mut gs: Vec<Vec<f32>> = (0..pairs.len()).map(|_| vec![0f32; inter]).collect();
18488 if !QTensor::moe_gate_up_many(&pairs, x, &mut gs, pool) {
18489 return None;
18490 }
18491 let mut out = attention::take_buf(x.len());
18492 if !QTensor::moe_down_many(&downs, &gs, &ws, &mut out, pool) {
18493 attention::recycle_buf(&mut out);
18494 return None;
18495 }
18496 Some(out)
18497}
18498
18499pub(crate) fn moe_cold_experts_cpu(
18505 experts: &[(&DenseFfn, f32)],
18506 x: &[f32],
18507 pool: Option<&Pool>,
18508) -> Vec<f32> {
18509 let mut out = attention::take_buf(x.len());
18510 if experts.is_empty() {
18511 return out;
18512 }
18513 let pairs: Vec<_> = experts
18514 .iter()
18515 .map(|(e, _)| (&e.gate_proj, &e.up_proj))
18516 .collect();
18517 let downs: Vec<_> = experts.iter().map(|(e, _)| &e.down_proj).collect();
18518 let weights: Vec<_> = experts.iter().map(|(_, w)| *w).collect();
18519 let inter = experts[0].0.gate_proj.rows();
18520 let mut activations: Vec<Vec<f32>> = (0..experts.len()).map(|_| vec![0.0; inter]).collect();
18521 if QTensor::moe_gate_up_many(&pairs, x, &mut activations, pool)
18522 && QTensor::moe_down_many(&downs, &activations, &weights, &mut out, pool)
18523 {
18524 return out;
18525 }
18526 out.fill(0.0);
18527 for &(expert, weight) in experts {
18528 let mut one = dense_ffn(expert, x, pool);
18529 for (o, v) in out.iter_mut().zip(&one) {
18530 *o += weight * v;
18531 }
18532 attention::recycle_buf(&mut one);
18533 }
18534 out
18535}
18536
18537pub(crate) fn moe_cold_experts_rows_cpu(
18541 jobs: &[Vec<(&DenseFfn, f32)>],
18542 xs: &[f32],
18543 hidden: usize,
18544 pool: Option<&Pool>,
18545) -> Vec<f32> {
18546 let mut out = vec![0.0; xs.len()];
18547 let mut experts: Vec<&DenseFfn> = Vec::new();
18548 let mut groups: Vec<Vec<usize>> = Vec::new();
18549 let mut terms = vec![Vec::new(); jobs.len()];
18550 for (r, row) in jobs.iter().enumerate() {
18551 for &(e, w) in row {
18552 let g = match experts.iter().position(|&d| std::ptr::eq(d, e)) {
18553 Some(g) => g,
18554 None => {
18555 experts.push(e);
18556 groups.push(Vec::new());
18557 groups.len() - 1
18558 }
18559 };
18560 terms[r].push((g, groups[g].len(), w));
18561 groups[g].push(r);
18562 }
18563 }
18564 if experts.is_empty() {
18565 return out;
18566 }
18567 let pairs: Vec<_> = experts.iter().map(|e| (&e.gate_proj, &e.up_proj)).collect();
18568 let downs: Vec<_> = experts.iter().map(|e| &e.down_proj).collect();
18569 let lens: Vec<_> = groups.iter().map(Vec::len).collect();
18570 let count: usize = lens.iter().sum();
18571 let mut acts = vec![vec![0.0; experts[0].gate_proj.rows()]; count];
18572 let mut ds = vec![vec![0.0; hidden]; count];
18573 if QTensor::moe_gate_up_rows(&pairs, &groups, xs, &mut acts, pool)
18574 && QTensor::moe_down_rows(&downs, &lens, &acts, &mut ds, pool)
18575 {
18576 let mut offset = 0;
18577 let offsets: Vec<_> = lens
18578 .iter()
18579 .map(|&n| {
18580 let start = offset;
18581 offset += n;
18582 start
18583 })
18584 .collect();
18585 for (r, terms) in terms.iter().enumerate() {
18586 for &(g, slot, w) in terms {
18587 for (o, &v) in out[r * hidden..(r + 1) * hidden]
18588 .iter_mut()
18589 .zip(&ds[offsets[g] + slot])
18590 {
18591 *o += w * v;
18592 }
18593 }
18594 }
18595 } else {
18596 for (r, jobs) in jobs.iter().enumerate() {
18597 if !jobs.is_empty() {
18598 let mut row = moe_cold_experts_cpu(jobs, &xs[r * hidden..(r + 1) * hidden], pool);
18599 out[r * hidden..(r + 1) * hidden].copy_from_slice(&row);
18600 attention::recycle_buf(&mut row);
18601 }
18602 }
18603 }
18604 out
18605}
18606
18607fn moe_ffn_cpu(
18609 m: &MoeFfn,
18610 x: &[f32],
18611 idx: &[usize],
18612 p: &[f32],
18613 wsum: f32,
18614 pool: Option<&Pool>,
18615) -> Vec<f32> {
18616 if let Some(out) = moe_ffn_cpu_batched(m, x, idx, p, wsum, pool) {
18617 return out;
18618 }
18619 let mut out = attention::take_buf(x.len());
18620 for &e in idx {
18621 let mut eo = dense_ffn(&m.experts[e], x, pool);
18622 let w = p[e] / wsum * m.per_expert_scale.as_ref().map_or(1.0, |v| v[e]);
18623 for i in 0..out.len() {
18624 out[i] += w * eo[i];
18625 }
18626 attention::recycle_buf(&mut eo);
18627 }
18628 if let Some((se, gate)) = &m.shared {
18629 let mut so = dense_ffn(se, x, pool);
18630 let g = gate.as_ref().map_or(1.0, |gate| {
18631 let mut gl = [0.0f32; 1];
18632 gate.matvec(x, &mut gl, pool);
18633 1.0 / (1.0 + (-gl[0]).exp())
18634 });
18635 for i in 0..out.len() {
18636 out[i] += g * so[i];
18637 }
18638 attention::recycle_buf(&mut so);
18639 }
18640 out
18641}
18642
18643#[allow(clippy::too_many_arguments)]
18651pub(crate) fn mla_attention(
18652 w: &MlaWeights,
18653 normed: &[f32],
18654 cache: &mut crate::kv_cache::LayerKvCache,
18655 position: usize,
18656 inv_freq: &[f32],
18657 rope_scale: f32,
18658 eps: f64,
18659 pool: Option<&Pool>,
18660) -> Vec<f32> {
18661 let (nh, dr, dn, dv, lora) = (w.nh, w.qk_rope, w.qk_nope, w.v_dim, w.lora);
18662 let hd = dr + dn;
18663 let mut q = vec![0.0f32; nh * hd];
18664 match (&w.q_a, &w.q_a_norm) {
18665 (Some(qa), Some(qn)) => {
18666 let mut t = vec![0.0f32; qa.rows()];
18667 qa.matvec(normed, &mut t, pool);
18668 let tn = inference::rms_norm(&t, qn, eps, NormStyle::Qwen);
18669 w.q_proj.matvec(&tn, &mut q, pool);
18670 }
18671 _ => w.q_proj.matvec(normed, &mut q, pool),
18672 }
18673 let mut ca = vec![0.0f32; lora + dr];
18674 w.kv_a.matvec(normed, &mut ca, pool);
18675 let (c_lat, k_rope) = ca.split_at_mut(lora);
18676 let latn = inference::rms_norm(c_lat, &w.kv_a_norm, eps, NormStyle::Qwen);
18677 let mut kvb = vec![0.0f32; nh * (dn + dv)];
18678 w.kv_b.matvec(&latn, &mut kvb, pool);
18679 if !w.nope {
18680 attention::rope_rotate_scaled(k_rope, position, inv_freq, rope_scale);
18681 }
18682 for h in 0..nh {
18683 if !w.nope {
18684 attention::rope_rotate_scaled(
18685 &mut q[h * hd..h * hd + dr],
18686 position,
18687 inv_freq,
18688 rope_scale,
18689 );
18690 }
18691 }
18692 let mut k = vec![0.0f32; nh * hd];
18693 let mut v = vec![0.0f32; nh * hd];
18694 for h in 0..nh {
18695 k[h * hd..h * hd + dr].copy_from_slice(k_rope);
18696 k[h * hd + dr..(h + 1) * hd].copy_from_slice(&kvb[h * (dn + dv)..h * (dn + dv) + dn]);
18697 v[h * hd..h * hd + dv].copy_from_slice(&kvb[h * (dn + dv) + dn..(h + 1) * (dn + dv)]);
18698 }
18699 cache.append(&k, &v, &vec![true; nh]);
18700 let (ao, mut imp) = attention::attend_all_heads(&q, cache, nh, 1, hd, w.scale, None, 0.0);
18701 attention::recycle_buf(&mut imp);
18702 let mut ov = vec![0.0f32; nh * dv];
18703 for h in 0..nh {
18704 ov[h * dv..(h + 1) * dv].copy_from_slice(&ao[h * hd..h * hd + dv]);
18705 }
18706 let mut out = vec![0.0f32; w.o_proj.rows()];
18707 w.o_proj.matvec(&ov, &mut out, pool);
18708 out
18709}
18710
18711fn dense_moe_ffn(
18718 dm: &DenseMoeFfn,
18719 x_normed: &[f32],
18720 h_raw: &[f32],
18721 eps: f64,
18722 norm_style: NormStyle,
18723 pool: Option<&Pool>,
18724) -> Vec<f32> {
18725 let mut d = dense_ffn(&dm.dense, x_normed, pool);
18726 d = inference::rms_norm(&d, &dm.post_norm_1, eps, norm_style);
18727 let m = &dm.moe;
18728 let ne = m.experts.len();
18729 let mut logits = vec![0.0f32; ne];
18730 if m.router_input_norm {
18731 let ss: f32 = h_raw.iter().map(|v| v * v).sum::<f32>() / h_raw.len() as f32;
18732 let inv = 1.0 / (ss + eps as f32).sqrt();
18733 let xr: Vec<f32> = h_raw.iter().map(|v| v * inv).collect();
18734 m.router.matvec(&xr, &mut logits, pool);
18735 } else {
18736 m.router.matvec(h_raw, &mut logits, pool);
18737 }
18738 let (idx, p, wsum) = moe_route(&logits, m, None);
18739 {
18740 let mut st = m.stats.borrow_mut();
18741 if st.len() < ne {
18742 st.resize(ne, 0);
18743 }
18744 for &e in &idx {
18745 st[e] += 1;
18746 }
18747 }
18748 let x2 = inference::rms_norm(h_raw, &dm.pre_norm_2, eps, norm_style);
18749 let mo = moe_ffn_cpu(m, &x2, &idx, &p, wsum, pool);
18750 let mo = inference::rms_norm(&mo, &dm.post_norm_2, eps, norm_style);
18751 for (di, mi) in d.iter_mut().zip(&mo) {
18752 *di += mi;
18753 }
18754 d
18755}
18756
18757fn moe_gpu_refused(why: &'static str) {
18764 use std::sync::atomic::{AtomicBool, Ordering};
18765 static SAID: AtomicBool = AtomicBool::new(false);
18766 if !SAID.swap(true, Ordering::Relaxed) {
18767 tracing::warn!("MoE GPU block refused ({why}) — experts run on the CPU");
18768 }
18769}
18770
18771fn moe_ffn_gpu(
18772 m: &MoeFfn,
18773 x: &[f32],
18774 idx: &[usize],
18775 p: &[f32],
18776 wsum: f32,
18777 pool: Option<&Pool>,
18778) -> Option<Vec<f32>> {
18779 use crate::gpu::MoeJob;
18780
18781 let mut jobs: Vec<MoeJob> = Vec::with_capacity(idx.len() + 1);
18782 let mut model_ref = None;
18783 for &e in idx {
18784 if moe_push_job(&m.experts[e], x, p[e] / wsum, &mut jobs, &mut model_ref).is_none() {
18785 moe_gpu_refused("push_job(expert)");
18786 return None;
18787 }
18788 }
18789 if let Some((se, gate)) = &m.shared {
18790 let g = gate.as_ref().map_or(1.0, |gate| {
18791 let mut gl = [0.0f32; 1];
18792 gate.matvec(x, &mut gl, pool);
18793 1.0 / (1.0 + (-gl[0]).exp())
18794 });
18795 if moe_push_job(se, x, g, &mut jobs, &mut model_ref).is_none() {
18796 moe_gpu_refused("push_job(shared)");
18797 return None;
18798 }
18799 }
18800 let Some(model) = model_ref else {
18801 moe_gpu_refused("no model_ref");
18802 return None;
18803 };
18804 let hidden = jobs[0].down.1;
18805 let mut out = vec![0.0f32; hidden];
18806 if crate::gpu::moe_block(&model, &jobs, &mut out) {
18807 Some(out)
18808 } else {
18809 moe_gpu_refused("gpu::moe_block");
18810 None
18811 }
18812}
18813
18814fn ffn_forward(
18816 ffn: &FfnKind,
18817 x: &[f32],
18818 pool: Option<&Pool>,
18819 experts_allowed: Option<&[bool]>,
18820) -> Vec<f32> {
18821 match ffn {
18822 FfnKind::Dense(d) if !d.segs.is_empty() => tube_ffn(d, x, 1, pool, None),
18823 FfnKind::Dense(d) => dense_ffn(d, x, pool),
18824 FfnKind::Moe(m) => moe_ffn(m, x, pool, experts_allowed),
18825 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
18829 }
18830}
18831
18832fn ffn_forward_pair(
18836 ffn: &FfnKind,
18837 x1: &[f32],
18838 x2: &[f32],
18839 pool: Option<&Pool>,
18840 experts_allowed: Option<&[bool]>,
18841) -> (Vec<f32>, Vec<f32>) {
18842 let d = match ffn {
18843 FfnKind::Dense(d) if !d.segs.is_empty() => {
18846 return (
18847 tube_ffn(d, x1, 1, pool, None),
18848 tube_ffn(d, x2, 1, pool, None),
18849 );
18850 }
18851 FfnKind::Dense(d) => d,
18852 FfnKind::Moe(m) => {
18853 return (
18854 moe_ffn(m, x1, pool, experts_allowed),
18855 moe_ffn(m, x2, pool, experts_allowed),
18856 );
18857 }
18858 FfnKind::DenseMoe(_) => unreachable!("DenseMoe dispatches via dense_moe_ffn"),
18859 };
18860 let inter = d.gate_proj.rows();
18861 FFN_SCRATCH.with(|s| {
18862 let mut s = s.borrow_mut();
18863 let [g1, g2, u1, u2] = &mut *s;
18864 g1.resize(inter, 0.0);
18865 g2.resize(inter, 0.0);
18866 u1.resize(inter, 0.0);
18867 u2.resize(inter, 0.0);
18868 QTensor::matvec2_many(
18871 [&d.gate_proj, &d.up_proj],
18872 x1,
18873 x2,
18874 [g1.as_mut_slice(), u1.as_mut_slice()],
18875 [g2.as_mut_slice(), u2.as_mut_slice()],
18876 pool,
18877 );
18878 for i in 0..inter {
18879 g1[i] = d.act.combine(g1[i], u1[i]);
18880 g2[i] = d.act.combine(g2[i], u2[i]);
18881 }
18882 let mut o1 = attention::take_buf(d.down_proj.rows());
18883 let mut o2 = attention::take_buf(d.down_proj.rows());
18884 d.down_proj.matvec2(g1, g2, &mut o1, &mut o2, pool);
18885 (o1, o2)
18886 })
18887}
18888
18889#[cfg(test)]
18890mod tests {
18891
18892 #[test]
18897 fn prefill_chunk_rule_widens_only_dense_on_discrete() {
18898 use super::{
18899 prefill_chunk_rule, ChunkHost, ChunkStackFacts, DISCRETE_DENSE_PREFILL_CHUNK,
18900 };
18901 let dense_card = ChunkStackFacts {
18902 plain_dense: true,
18903 discrete: true,
18904 gpu_on: true,
18905 ..Default::default()
18906 };
18907 assert!(dense_card.dense_on_discrete());
18908 assert_eq!(
18910 prefill_chunk_rule(None, ChunkHost::Other, dense_card.dense_on_discrete()),
18911 DISCRETE_DENSE_PREFILL_CHUNK
18912 );
18913 assert!(DISCRETE_DENSE_PREFILL_CHUNK > 48);
18914 for (label, facts) in [
18915 ("GDN hybrid / MoE / DeepSeek stack", ChunkStackFacts { plain_dense: false, ..dense_card }),
18916 ("integrated GPU", ChunkStackFacts { discrete: false, ..dense_card }),
18917 ("CPU only", ChunkStackFacts { gpu_on: false, discrete: false, ..dense_card }),
18918 ("capacity split", ChunkStackFacts { capacity_split: true, ..dense_card }),
18919 ("multi-GPU plan", ChunkStackFacts { multi_gpu: true, ..dense_card }),
18920 ("O(1) layers", ChunkStackFacts { o1: true, ..dense_card }),
18921 ] {
18922 assert!(!facts.dense_on_discrete(), "{label}");
18923 assert_eq!(
18924 prefill_chunk_rule(None, ChunkHost::Other, facts.dense_on_discrete()),
18925 48,
18926 "{label} keeps the historical x86 chunk"
18927 );
18928 }
18929 for dense in [false, true] {
18931 assert_eq!(prefill_chunk_rule(None, ChunkHost::Macos, dense), 512);
18932 assert_eq!(prefill_chunk_rule(None, ChunkHost::Aarch64, dense), 256);
18933 }
18934 for host in [ChunkHost::Macos, ChunkHost::Aarch64, ChunkHost::Other] {
18936 for dense in [false, true] {
18937 assert_eq!(prefill_chunk_rule(Some(48), host, dense), 48);
18938 assert_eq!(prefill_chunk_rule(Some(0), host, dense), 1);
18939 }
18940 }
18941 }
18942
18943 #[test]
18944 fn kv_reuse_plan_pulls_rows_decode_wrote_only_on_the_device() {
18945 use super::{ReuseLayer, ReusePlan, kv_reuse_plan};
18946 let full = |host_rows, device_rows| ReuseLayer {
18947 full: true,
18948 host_rows,
18949 device_rows,
18950 device_state: false,
18951 };
18952 assert_eq!(
18955 kv_reuse_plan(339, &[full(300, Some(339)), full(300, Some(339))]),
18956 ReusePlan::Pull(vec![(0, 300, 339), (1, 300, 339)])
18957 );
18958 assert_eq!(kv_reuse_plan(339, &[full(339, None)]), ReusePlan::Ready);
18960 assert_eq!(kv_reuse_plan(339, &[full(339, Some(345))]), ReusePlan::Ready);
18962 assert_eq!(
18964 kv_reuse_plan(339, &[full(300, Some(339)), full(339, None)]),
18965 ReusePlan::Pull(vec![(0, 300, 339)])
18966 );
18967 assert_eq!(kv_reuse_plan(339, &[full(300, Some(320))]), ReusePlan::Fresh);
18969 assert_eq!(kv_reuse_plan(339, &[full(300, None)]), ReusePlan::Fresh);
18970 assert_eq!(kv_reuse_plan(339, &[full(350, None)]), ReusePlan::Fresh);
18971 let conv = |device_state| ReuseLayer {
18974 full: false,
18975 host_rows: 0,
18976 device_rows: None,
18977 device_state,
18978 };
18979 assert_eq!(
18980 kv_reuse_plan(339, &[conv(true), full(300, Some(339))]),
18981 ReusePlan::Fresh
18982 );
18983 assert_eq!(kv_reuse_plan(339, &[conv(false), full(339, None)]), ReusePlan::Ready);
18984 }
18985
18986 #[test]
18987 fn nll_graph_policy_scopes_only_the_fused_head() {
18988 for (label, unmasked, prefer_graph, native_metal, want_graph, want_head) in [
18989 ("vulkan graph", true, true, false, true, false),
18991 ("native Metal graph", true, true, true, true, true),
18993 ("masked", false, true, false, false, false),
18995 ("graph disabled", true, false, true, false, false),
18996 ] {
18997 let (graph_quality, graph_head_required) =
18998 super::nll_graph_policy(unmasked, prefer_graph, native_metal);
18999 assert_eq!(graph_quality, want_graph, "{label}: graph quality");
19000 assert_eq!(graph_head_required, want_head, "{label}: fused head");
19001 }
19002 }
19003
19004 #[test]
19005 fn mtp_prefill_pair_boundaries_skip_only_final_prompt_row() {
19006 assert_eq!(mtp_prefill_pair_count(0, 128, 256), 128);
19007 assert_eq!(mtp_prefill_pair_count(128, 256, 256), 127);
19008 assert_eq!(mtp_prefill_pair_count(0, 256, 256), 255);
19009 assert_eq!(mtp_prefill_pair_count(256, 256, 256), 0);
19010 assert_eq!(mtp_prefill_pair_count(300, 320, 256), 0);
19011 }
19012
19013 #[test]
19014 fn cancel_flag_stops_generation() {
19015 let mut p = create_test_pipeline(16, 32, 2, 2, 8, 2, 32);
19016 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
19019 let r = p.generate_from_ids(&[1, 2, 3], 8, None, None).unwrap();
19020 assert_eq!(r.finish_reason, "cancelled");
19021 assert!(
19022 r.token_ids.is_empty(),
19023 "no tokens after cancel: {:?}",
19024 r.token_ids
19025 );
19026 assert_eq!(p.kv_cache.seq_len(), 0);
19027 assert!(p.kv_history.is_empty());
19028 assert!(!p.graph_want_logits);
19029 assert!(p.graph_logits.is_none());
19030 let r2 = p.generate_from_ids(&[1, 2, 3], 4, None, None).unwrap();
19032 assert_ne!(r2.finish_reason, "cancelled");
19033 }
19034 use super::*;
19035
19036 #[test]
19037 fn explicit_sliding_schedule_selects_local_rope_profile() {
19038 let mut p = create_test_pipeline(32, 64, 4, 1, 8, 4, 32);
19043 p.swa = Some((1024, usize::MAX));
19044 p.sliding_layers = Some(vec![true, true, true, false]);
19045 p.inv_freq = std::sync::Arc::new(vec![0.125; 4]);
19046 p.inv_freq_local = Some(std::sync::Arc::new(vec![0.25; 4]));
19047 p.rope_scale = 1.277_258_9;
19048 p.rope_scale_local = 1.0;
19049
19050 for li in 0..3 {
19051 assert_eq!(p.layer_window(li), Some(1024), "local layer {li}");
19052 assert_eq!(p.layer_inv_freq(li).as_slice(), &[0.25; 4]);
19053 assert_eq!(p.layer_rope_scale(li), 1.0, "local layer {li}");
19054 }
19055 assert_eq!(p.layer_window(3), None);
19056 assert_eq!(p.layer_inv_freq(3).as_slice(), &[0.125; 4]);
19057 assert_eq!(p.layer_rope_scale(3), 1.277_258_9);
19058 }
19059
19060 #[test]
19068 fn dynamic_ffn_equals_the_zeroing_arm() {
19069 let (hidden, inter) = (8usize, 32usize);
19070 let synth = |n: usize, salt: usize| -> Vec<f32> {
19071 (0..n)
19072 .map(|i| (((i * 29 + salt * 13 + 7) % 89) as f32 / 89.0 - 0.5) * 0.6)
19073 .collect()
19074 };
19075 let down = synth(hidden * inter, 3);
19076 let mut down_t = vec![0.0f32; inter * hidden];
19077 for r in 0..hidden {
19078 for c in 0..inter {
19079 down_t[c * hidden + r] = down[r * inter + c];
19080 }
19081 }
19082 let d = DenseFfn {
19083 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19084 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19085 down_proj: QTensor::from_f32(down.clone(), hidden, inter),
19086 act: Act::Silu,
19087 down_t: Some(QTensor::from_f32(down_t, inter, hidden)),
19088 segs: Vec::new(),
19089 };
19090 let x = synth(hidden, 11);
19091 let k = 12usize;
19092 let got = dense_ffn_dynamic(&d, &x, None, k).expect("down_t present");
19093 let mut g = vec![0.0f32; inter];
19095 d.gate_proj.matvec(&x, &mut g, None);
19096 let mut u = vec![0.0f32; inter];
19097 d.up_proj.matvec(&x, &mut u, None);
19098 for v in g.iter_mut() {
19099 *v = inference::silu(*v);
19100 }
19101 keep_top_k(&mut g, k);
19102 for i in 0..inter {
19103 g[i] *= u[i];
19104 }
19105 let mut want = vec![0.0f32; hidden];
19106 d.down_proj.matvec(&g, &mut want, None);
19107 for (a, b) in want.iter().zip(&got) {
19108 assert!((a - b).abs() < 1e-5, "dynamic {b} vs reference {a}");
19109 }
19110 }
19111
19112 #[test]
19118 fn tube_ffn_open_equals_dense_and_closed_equals_masked() {
19119 let (hidden, core, tube) = (8usize, 12usize, 8usize);
19120 let inter = core + tube;
19121 let synth = |n: usize, salt: usize| -> Vec<f32> {
19122 (0..n)
19123 .map(|i| (((i * 41 + salt * 17 + 5) % 97) as f32 / 97.0 - 0.5) * 0.5)
19124 .collect()
19125 };
19126 let (g_all, u_all) = (synth(inter * hidden, 1), synth(inter * hidden, 2));
19127 let d_all = synth(hidden * inter, 3);
19128 let dense = DenseFfn {
19130 gate_proj: QTensor::from_f32(g_all.clone(), inter, hidden),
19131 up_proj: QTensor::from_f32(u_all.clone(), inter, hidden),
19132 down_proj: QTensor::from_f32(d_all.clone(), hidden, inter),
19133 act: Act::Silu,
19134 down_t: None,
19135 segs: Vec::new(),
19136 };
19137 let rows =
19138 |v: &[f32], a: usize, b: usize| -> Vec<f32> { v[a * hidden..b * hidden].to_vec() };
19139 let cols = |v: &[f32], a: usize, b: usize| -> Vec<f32> {
19140 let mut o = Vec::with_capacity(hidden * (b - a));
19141 for r in 0..hidden {
19142 o.extend_from_slice(&v[r * inter + a..r * inter + b]);
19143 }
19144 o
19145 };
19146 let tubed = DenseFfn {
19147 down_t: None,
19148 gate_proj: QTensor::from_f32(rows(&g_all, 0, core), core, hidden),
19149 up_proj: QTensor::from_f32(rows(&u_all, 0, core), core, hidden),
19150 down_proj: QTensor::from_f32(cols(&d_all, 0, core), hidden, core),
19151 act: Act::Silu,
19152 segs: vec![FfnSeg {
19153 gate: QTensor::from_f32(rows(&g_all, core, inter), tube, hidden),
19154 up: QTensor::from_f32(rows(&u_all, core, inter), tube, hidden),
19155 down: QTensor::from_f32(cols(&d_all, core, inter), hidden, tube),
19156 start: core,
19157 width: tube,
19158 }],
19159 };
19160 let x = synth(hidden, 7);
19161 let want = dense_ffn(&dense, &x, None);
19162 let got = tube_ffn(&tubed, &x, 1, None, None);
19163 for (a, b) in want.iter().zip(&got) {
19164 assert!((a - b).abs() < 1e-5, "open tube: {a} vs {b}");
19165 }
19166 let mut bits = vec![0u8; inter.div_ceil(8)];
19168 for n in 0..core {
19169 bits[n / 8] |= 1 << (n % 8);
19170 }
19171 let closed = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19172 let masked = dense_ffn_masked(&dense, &x, None, &bits);
19173 for (a, b) in masked.iter().zip(&closed) {
19174 assert!((a - b).abs() < 1e-5, "closed tube: {a} vs {b}");
19175 }
19176 let batch = tube_ffn(&tubed, &x, 1, None, Some(&bits));
19178 for (a, b) in closed.iter().zip(&batch) {
19179 assert_eq!(a, b, "batch arm disagrees with decode arm");
19180 }
19181 }
19182
19183 #[test]
19185 fn sparse_ffn_quant_equals_dense_with_inactive_zeroed() {
19186 let (hidden, inter) = (16usize, 40usize);
19187 let synth = |n: usize, salt: usize| -> Vec<f32> {
19188 (0..n)
19189 .map(|i| (((i * 37 + salt * 11 + 3) % 101) as f32 / 101.0 - 0.5) * 0.4)
19190 .collect()
19191 };
19192 let d = DenseFfn {
19193 gate_proj: QTensor::from_f32(synth(inter * hidden, 1), inter, hidden),
19194 up_proj: QTensor::from_f32(synth(inter * hidden, 2), inter, hidden),
19195 down_proj: QTensor::from_f32(synth(hidden * inter, 3), hidden, inter),
19196 act: Act::Silu,
19197 down_t: None,
19198 segs: Vec::new(),
19199 };
19200 let x = synth(hidden, 9);
19201 let active: Vec<u16> = (0..inter as u16).filter(|i| i % 3 == 0).collect();
19203
19204 let sparse = sparse_ffn_quant(&d, &x, &active, hidden, None);
19205
19206 let mut g = vec![0.0f32; inter];
19208 d.gate_proj.matvec(&x, &mut g, None);
19209 let mut u = vec![0.0f32; inter];
19210 d.up_proj.matvec(&x, &mut u, None);
19211 let act_set: std::collections::HashSet<u16> = active.iter().copied().collect();
19212 for i in 0..inter {
19213 g[i] = if act_set.contains(&(i as u16)) {
19214 inference::silu(g[i]) * u[i]
19215 } else {
19216 0.0
19217 };
19218 }
19219 let mut reference = vec![0.0f32; hidden];
19220 d.down_proj.matvec(&g, &mut reference, None);
19221
19222 let max_d = sparse
19223 .iter()
19224 .zip(&reference)
19225 .map(|(a, b)| (a - b).abs())
19226 .fold(0.0f32, f32::max);
19227 assert!(max_d < 1e-5, "sparse != dense-zeroed: max|Δ| = {max_d}");
19228 }
19229
19230 fn attach_test_mtp(p: &mut Pipeline) {
19232 let (h, inter, heads, kv, hd) = (
19233 p.hidden_size,
19234 p.intermediate_size,
19235 p.num_heads,
19236 p.num_kv_heads,
19237 p.head_dim,
19238 );
19239 let synth = |n: usize, salt: usize| -> Vec<f32> {
19240 (0..n)
19241 .map(|i| (((i * 29 + salt * 23 + 5) % 101) as f32 / 101.0 - 0.5) * 0.2)
19242 .collect()
19243 };
19244 let qt = |rows: usize, cols: usize, salt: usize| -> QTensor {
19245 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19246 };
19247 p.mtp = Some(MtpModule {
19248 enorm: vec![1.0; h],
19249 hnorm: vec![1.0; h],
19250 eh_proj: qt(h, 2 * h, 301),
19251 layer: LayerWeights {
19252 input_norm: vec![1.0; h],
19253 post_norm: vec![1.0; h],
19254 attn_out_norm: None,
19255 ffn_out_norm: None,
19256 layer_scale: None,
19257 ffn: FfnKind::Dense(DenseFfn {
19258 gate_proj: qt(inter, h, 315),
19259 up_proj: qt(inter, h, 316),
19260 down_proj: qt(h, inter, 317),
19261 act: Act::Silu,
19262 down_t: None,
19263 segs: Vec::new(),
19264 }),
19265 attn: AttnKind::Full {
19266 bias: None,
19267 wq: qt(heads * hd, h, 311),
19268 wk: qt(kv * hd, h, 312),
19269 wv: qt(kv * hd, h, 313),
19270 wo: qt(h, heads * hd, 314),
19271 q_norm: None,
19272 k_norm: None,
19273 output_gate: false,
19274 softplus_gate: None,
19275 },
19276 },
19277 final_norm: vec![1.0; h],
19278 kv: crate::kv_cache::LayerKvCache::new(kv, hd),
19279 });
19280 }
19281
19282 #[test]
19283 fn speculative_equals_vanilla_greedy() {
19284 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19288 let run = |spec: bool| {
19289 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19290 p.sampler_config.temperature = 0.0;
19291 attach_test_mtp(&mut p);
19292 p.speculative = spec;
19293 let r = p.generate("abcdef", 12, None, None).unwrap();
19294 (r.token_ids, r.mtp_drafted, r.mtp_accepted)
19295 };
19296 let (vanilla, d0, _) = run(false);
19297 let (spec, d1, a1) = run(true);
19298 assert_eq!(d0, 0, "vanilla path must not draft");
19299 assert!(d1 > 0, "speculative path must draft");
19300 assert_eq!(
19301 vanilla, spec,
19302 "speculative must reproduce the exact greedy sequence (accepted {a1}/{d1})"
19303 );
19304 }
19305
19306 #[test]
19307 fn speculative_accepts_constant_oracle() {
19308 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
19310 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19311 p.sampler_config.temperature = 0.0;
19312 p.sampler_config.repetition_penalty = 1.0;
19313 p.weights.lm_head = QTensor::from_f32(vec![0.01; 64 * 8], 64, 8);
19316 attach_test_mtp(&mut p);
19317 p.speculative = true;
19318 let r = p.generate("abcd", 10, None, None).unwrap();
19319 assert!(r.mtp_drafted > 0);
19320 assert_eq!(
19321 r.mtp_accepted, r.mtp_drafted,
19322 "constant logits → every draft accepted"
19323 );
19324 assert!(r.token_ids.windows(2).all(|w| w[0] == w[1]));
19327 }
19328
19329 #[test]
19330 fn empty_prompt_is_an_error_not_a_panic() {
19331 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
19332 let r = p.generate("", 4, None, None);
19333 assert!(r.is_err(), "empty prompt must be a clean error");
19334 }
19335
19336 #[test]
19337 fn every_token_enters_kv_exactly_once() {
19338 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19339 p.sampler_config.temperature = 0.0;
19341 let r = p.generate("abc", 2, None, None).unwrap();
19342 assert_eq!(r.prompt_tokens, 3);
19343 assert_eq!(
19347 p.kv_cache.seq_len(),
19348 3 + r.tokens_generated - 1,
19349 "each token must be cached exactly once (v1 cached the last prompt token twice)"
19350 );
19351 }
19352
19353 #[test]
19354 fn generation_is_reproducible_with_seed() {
19355 let run = || {
19356 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19357 p.generate("hello", 8, None, None).unwrap().token_ids
19358 };
19359 assert_eq!(run(), run());
19360 }
19361
19362 #[test]
19363 fn resetting_sampler_restarts_the_seeded_stream() {
19364 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
19365 let config = SamplerConfig {
19366 seed: Some(1234),
19367 ..SamplerConfig::default()
19368 };
19369 p.set_sampler_config(config.clone());
19370 let first = p.generate("hello", 8, None, None).unwrap().token_ids;
19371 p.set_sampler_config(config);
19372 let second = p.generate("hello", 8, None, None).unwrap().token_ids;
19373 assert_eq!(first, second);
19374 }
19375
19376 #[test]
19377 fn eviction_bounds_the_cache() {
19378 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 260);
19379 p.kv_cache.max_seq_len = 6;
19380 p.sampler_config.temperature = 0.0;
19381 let _ = p.generate("abcd", 12, None, None).unwrap();
19382 assert!(
19383 p.kv_cache.seq_len() <= 6 + 1,
19384 "cache must stay bounded by max_seq_len (got {})",
19385 p.kv_cache.seq_len()
19386 );
19387 }
19388
19389 #[test]
19390 fn confidence_matches_tokens_and_is_a_probability() {
19391 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19392 p.sampler_config.temperature = 0.0;
19393 p.sampler_config.repetition_penalty = 1.0;
19394 let r = p.generate("abcd", 10, None, None).unwrap();
19395 assert_eq!(
19396 r.token_confidence.len(),
19397 r.token_ids.len(),
19398 "one confidence per emitted token"
19399 );
19400 for &c in &r.token_confidence {
19401 assert!((0.0..=1.0).contains(&c), "confidence out of [0,1]: {c}");
19402 }
19403 let logits = [1.0f32, 3.0, 0.5, 3.0];
19405 let p0 = top1_prob_t(&logits, 1, 1.0);
19406 let p1 = top1_prob_t(&logits, 3, 1.0);
19407 assert!((p0 - p1).abs() < 1e-6, "equal logits → equal prob");
19408 assert!(p0 > 0.0 && p0 < 1.0);
19409 let sharp = top1_prob_t(&logits, 1, 1.0);
19411 let soft = top1_prob_t(&logits, 1, 2.0);
19412 assert!(soft < sharp, "higher temperature lowers peak confidence");
19413 }
19414
19415 #[test]
19416 fn trace_is_opt_in_and_parallels_the_output() {
19417 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19419 p.sampler_config.temperature = 0.0;
19420 p.sampler_config.repetition_penalty = 1.0;
19421 let r = p.generate("abcd", 10, None, None).unwrap();
19422 assert!(r.traces.is_empty(), "trace must be empty unless enabled");
19423
19424 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19426 p.sampler_config.temperature = 0.0;
19427 p.sampler_config.repetition_penalty = 1.0;
19428 p.set_trace(true);
19429 let r = p.generate("abcd", 10, None, None).unwrap();
19430 assert_eq!(r.traces.len(), r.token_ids.len(), "one trace row per token");
19431 for (i, tr) in r.traces.iter().enumerate() {
19432 assert_eq!(tr.t, i, "trace index is sequential");
19433 assert_eq!(tr.token_id, r.token_ids[i], "trace token_id matches output");
19434 assert_eq!(
19435 tr.confidence, r.token_confidence[i],
19436 "trace confidence matches the confidence channel"
19437 );
19438 assert!(tr.active_skill.is_none() && tr.recon.is_none() && !tr.switched);
19440 }
19441 }
19442
19443 #[test]
19444 fn explain_prefill_logits_match_greedy_first_token() {
19445 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
19449 p.sampler_config.temperature = 0.0;
19450 p.sampler_config.repetition_penalty = 1.0;
19451 let ids = p.tokenizer.encode("abcd");
19452 let logits = p.prefill_next_logits(&ids, None);
19453 let argmax = logits
19454 .iter()
19455 .enumerate()
19456 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
19457 .unwrap()
19458 .0 as u32;
19459 let r = p.generate("abcd", 1, None, None).unwrap();
19460 assert_eq!(
19461 argmax, r.token_ids[0],
19462 "explain preview must match greedy emit"
19463 );
19464 }
19465
19466 #[test]
19467 fn laguna_shared_expert_is_unconditionally_added() {
19468 let matrix = |values: Vec<f32>| QTensor::from_f32(values, 2, 2);
19469 let identity = || matrix(vec![1.0, 0.0, 0.0, 1.0]);
19470 let zero_dense = || DenseFfn {
19471 gate_proj: matrix(vec![0.0; 4]),
19472 up_proj: matrix(vec![0.0; 4]),
19473 down_proj: matrix(vec![0.0; 4]),
19474 act: Act::Silu,
19475 down_t: None,
19476 segs: Vec::new(),
19477 };
19478 let shared = DenseFfn {
19479 gate_proj: identity(),
19480 up_proj: identity(),
19481 down_proj: identity(),
19482 act: Act::Silu,
19483 down_t: None,
19484 segs: Vec::new(),
19485 };
19486 let x = [1.0, 2.0];
19487 let expected = dense_ffn(&shared, &x, None);
19488 let moe = MoeFfn {
19489 router: QTensor::from_f32(vec![0.0, 0.0], 1, 2),
19490 experts: vec![zero_dense()],
19491 top_k: 1,
19492 norm_topk_prob: true,
19493 router_sigmoid: true,
19494 expert_bias: None,
19495 routed_scaling: 1.0,
19496 route_tau: None,
19497 shared: Some((shared, None)),
19498 stats: std::cell::RefCell::new(Vec::new()),
19499 act_sq: std::cell::RefCell::new(Vec::new()),
19500 act_rows: std::cell::RefCell::new(Vec::new()),
19501 mask: None,
19502 per_expert_scale: None,
19503 router_input_norm: false,
19504 resonance: None,
19505 grown: Vec::new(),
19506 };
19507 let actual = moe_ffn_cpu(&moe, &x, &[0], &[0.0], 1.0, None);
19508 for (actual, expected) in actual.iter().zip(expected) {
19509 assert!((actual - expected).abs() < 1e-6);
19510 }
19511 }
19512
19513 fn mimo_test_pipeline() -> Pipeline {
19522 let (hs, inter, nh, hd, vd, vocab) = (16usize, 24usize, 4usize, 8usize, 4usize, 64usize);
19523 let kvh = [1usize, 2, 2, 1];
19524 let synth = |n: usize, salt: usize| -> Vec<f32> {
19525 (0..n)
19526 .map(|i| (((i * 31 + salt * 17 + 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
19527 .collect()
19528 };
19529 let qt = |rows: usize, cols: usize, salt: usize| {
19530 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19531 };
19532 let dense = |inter: usize, salt: usize| DenseFfn {
19533 gate_proj: qt(inter, hs, salt),
19534 up_proj: qt(inter, hs, salt + 1),
19535 down_proj: qt(hs, inter, salt + 2),
19536 act: Act::Silu,
19537 down_t: None,
19538 segs: Vec::new(),
19539 };
19540 let layers: Vec<LayerWeights> = (0..4)
19541 .map(|li| LayerWeights {
19542 input_norm: vec![1.0; hs],
19543 post_norm: vec![1.0; hs],
19544 attn_out_norm: None,
19545 ffn_out_norm: None,
19546 layer_scale: None,
19547 ffn: if li == 0 {
19548 FfnKind::Dense(dense(inter, 50))
19549 } else {
19550 FfnKind::Moe(MoeFfn {
19551 router: qt(4, hs, 60 + li),
19552 experts: (0..4).map(|e| dense(8, 70 + li * 10 + e * 3)).collect(),
19553 top_k: 2,
19554 norm_topk_prob: true,
19555 router_sigmoid: true,
19556 expert_bias: Some(vec![0.02, -0.03, 0.01, 0.0]),
19557 routed_scaling: 1.0,
19558 route_tau: None,
19559 shared: None,
19560 stats: std::cell::RefCell::new(Vec::new()),
19561 act_sq: std::cell::RefCell::new(Vec::new()),
19562 act_rows: std::cell::RefCell::new(Vec::new()),
19563 mask: None,
19564 per_expert_scale: None,
19565 router_input_norm: false,
19566 resonance: None,
19567 grown: Vec::new(),
19568 })
19569 },
19570 attn: AttnKind::Full {
19571 wq: qt(nh * hd, hs, li * 10 + 1),
19572 wk: qt(kvh[li] * hd, hs, li * 10 + 2),
19573 wv: qt(kvh[li] * vd, hs, li * 10 + 3),
19574 wo: qt(hs, nh * vd, li * 10 + 4),
19575 q_norm: None,
19576 k_norm: None,
19577 output_gate: false,
19578 softplus_gate: None,
19579 bias: None,
19580 },
19581 })
19582 .collect();
19583 let mut p = Pipeline::new(
19584 Tokenizer::byte_level(),
19585 PipelineWeights {
19586 embed_tokens: qt(vocab, hs, 100),
19587 layers,
19588 lm_head: qt(vocab, hs, 200),
19589 final_norm: vec![1.0; hs],
19590 },
19591 hs,
19592 inter,
19593 nh,
19594 1, hd,
19596 4,
19597 4,
19598 false,
19599 vocab,
19600 1e-6,
19601 1e7,
19602 NormStyle::Qwen,
19603 4096,
19604 SamplerConfig {
19605 seed: Some(7),
19606 ..Default::default()
19607 },
19608 );
19609 p.layer_dump = None;
19611 p.set_rotary(4, 1e7);
19612 p.sliding_layers = Some(vec![false, true, true, false]);
19613 p.swa = Some((3, usize::MAX));
19614 p.rotary_dim_local = Some(4);
19615 p.inv_freq_local = Some(std::sync::Arc::new(attention::rope_inv_freq(4, 1e4)));
19616 p.set_attn_geometry(Some(kvh.to_vec()), Some(vd)).unwrap();
19617 p.set_layer_sinks(1, vec![0.5, -1.0, 1.5, 0.0]).unwrap();
19618 p.set_layer_sinks(2, vec![-0.25, 2.0, 0.75, -1.5]).unwrap();
19619 p
19620 }
19621
19622 #[test]
19623 fn mimo_embedded_prompt_uses_rows_and_never_reuses_token_only_kv() {
19624 let mut p = mimo_test_pipeline();
19625 p.speculative = false;
19626 p.ignore_eos = true;
19627 p.sampler_config.temperature = 0.0;
19628 p.sampler_config.repetition_penalty = 1.0;
19629 let a = vec![3, 5, 7, 9, 11, 13];
19630 let b = vec![4, 8, 12, 16, 20, 24];
19631 let rows: Vec<_> = b.iter().flat_map(|&id| p.embed_id(id)).collect();
19632 let expected = p.generate_from_ids(&b, 8, None, None).unwrap().token_ids;
19633 let actual = p.generate_from_embeds(&a, &rows, 8, None, None).unwrap().token_ids;
19637 assert_eq!(actual, expected);
19638 assert!(p.kv_history.is_empty());
19639 let mut extended = a.clone();
19640 extended.push(17);
19641 let after_media = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
19642 p.reset_session();
19643 let fresh = p.generate_from_ids(&extended, 8, None, None).unwrap().token_ids;
19644 assert_eq!(after_media, fresh);
19645 assert!(p.generate_from_embeds(&a, &rows[..rows.len()-1], 1, None, None).is_err());
19646 p.reset_session();
19650 p.generate_from_ids(&a, 1, None, None).unwrap();
19651 let mut media_ids = p.kv_history.clone();
19652 assert!(!media_ids.is_empty());
19653 media_ids.extend_from_slice(&[19, 21, 23]);
19654 let source_ids: Vec<_> = (0..media_ids.len()).map(|i| b[i % b.len()]).collect();
19655 let source_rows: Vec<_> = source_ids.iter().flat_map(|&id| p.embed_id(id)).collect();
19656 let mut oracle = mimo_test_pipeline();
19657 oracle.speculative = false;
19658 oracle.ignore_eos = true;
19659 oracle.sampler_config.temperature = 0.0;
19660 oracle.sampler_config.repetition_penalty = 1.0;
19661 let expected = oracle.generate_from_ids(&source_ids, 8, None, None).unwrap().token_ids;
19662 assert_eq!(p.generate_from_embeds(&media_ids, &source_rows, 8, None, None).unwrap().token_ids, expected);
19663 assert!(p.kv_history.is_empty());
19664 let mut bad = rows;
19665 bad[0] = f32::NAN;
19666 assert!(p.generate_from_embeds(&a, &bad, 1, None, None).is_err());
19667 }
19668
19669 fn f32_bits(v: &[f32]) -> Vec<u32> {
19670 v.iter().map(|x| x.to_bits()).collect()
19671 }
19672
19673 #[test]
19680 fn mimo_shaped_decode_matches_prefill_batch_bitwise() {
19681 let mut p = mimo_test_pipeline();
19682 let kv: Vec<usize> = p.kv_cache.layers.iter().map(|l| l.num_kv_heads).collect();
19683 assert_eq!(kv, vec![1, 2, 2, 1]);
19684 assert!(p.kv_cache.layers[0].sinks.is_none() && p.kv_cache.layers[3].sinks.is_none());
19685 assert!(p.kv_cache.layers[1].sinks.is_some() && p.kv_cache.layers[2].sinks.is_some());
19686 let ids: Vec<u32> = (0..12u32).map(|i| (i * 7 + 3) % 64).collect();
19687 let hs = p.hidden_size;
19688 let mut decode = Vec::new();
19689 for (pos, &id) in ids.iter().enumerate() {
19690 let e = p.embed_single(id);
19691 let h = p.forward_layers(&e, pos, None);
19692 decode.push(p.logits_from_hidden(&h));
19693 }
19694 for l in &p.kv_cache.layers {
19695 assert_eq!(l.seq_len, 12);
19696 assert_eq!(l.head_values(0).len(), 12 * 8);
19698 }
19699 assert!(decode.iter().flatten().all(|v| v.is_finite()));
19700
19701 p.clear_sequence_state();
19702 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
19703 for pos in 0..ids.len() {
19704 let lg = p.logits_from_hidden(&hb[pos * hs..(pos + 1) * hs]);
19705 assert_eq!(
19706 f32_bits(&decode[pos]),
19707 f32_bits(&lg),
19708 "whole prompt, pos {pos}"
19709 );
19710 }
19711
19712 p.clear_sequence_state();
19713 let a = p.prefill_batch_span(PrefillIn::Ids(&ids[..5]), 0, None, 0, p.num_layers);
19714 let b = p.prefill_batch_span(PrefillIn::Ids(&ids[5..]), 5, None, 0, p.num_layers);
19715 for pos in 0..ids.len() {
19716 let row = if pos < 5 {
19717 &a[pos * hs..(pos + 1) * hs]
19718 } else {
19719 &b[(pos - 5) * hs..(pos - 4) * hs]
19720 };
19721 let lg = p.logits_from_hidden(row);
19722 assert_eq!(
19723 f32_bits(&decode[pos]),
19724 f32_bits(&lg),
19725 "two chunks, pos {pos}"
19726 );
19727 }
19728
19729 let last = |p: &mut Pipeline| {
19732 p.clear_sequence_state();
19733 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
19734 p.logits_from_hidden(&hb[11 * hs..12 * hs])
19735 };
19736 let base = last(&mut p);
19737 let mut no_sinks = mimo_test_pipeline();
19738 for l in &mut no_sinks.kv_cache.layers {
19739 l.sinks = None;
19740 }
19741 assert_ne!(
19742 f32_bits(&last(&mut no_sinks)),
19743 f32_bits(&base),
19744 "sinks are live"
19745 );
19746 let mut wide = mimo_test_pipeline();
19747 wide.swa = Some((64, usize::MAX));
19748 assert_ne!(
19749 f32_bits(&last(&mut wide)),
19750 f32_bits(&base),
19751 "window is live"
19752 );
19753
19754 p.clear_sequence_state();
19756 p.ignore_eos = true;
19757 let r = p.generate_from_ids(&ids, 4, None, None).unwrap();
19758 assert_eq!(r.token_ids.len(), 4);
19759 }
19760
19761 fn spark_test_pipeline(trim: Option<(usize, usize)>) -> Pipeline {
19764 let (hs, nh) = (16usize, 4usize);
19765 let mut p = create_test_pipeline(hs, 24, nh, 2, 8, 4, 64);
19766 p.layer_dump = None;
19767 p.swa = Some((6, 4));
19768 p.proj_gate_sigmoid = true;
19769 for (li, lw) in p.weights.layers.iter_mut().enumerate() {
19770 if let AttnKind::Full { softplus_gate, .. } = &mut lw.attn {
19771 let g: Vec<f32> = (0..nh * hs)
19772 .map(|i| (((i * 29 + li * 7) % 83) as f32 / 83.0 - 0.5) * 0.8)
19773 .collect();
19774 *softplus_gate = Some((QTensor::from_f32(g, nh, hs), true));
19775 }
19776 }
19777 p.swa_trim = trim;
19778 p
19779 }
19780
19781 #[test]
19787 fn swa_trim_pipeline_matches_untrimmed_bitwise() {
19788 let mut a = spark_test_pipeline(None);
19789 let mut b = spark_test_pipeline(Some((2, 4)));
19790 assert_eq!(
19791 (0..4).map(|li| b.layer_window(li)).collect::<Vec<_>>(),
19792 vec![Some(6), Some(6), Some(6), None]
19793 );
19794 let ids: Vec<u32> = (0..41u32).map(|i| (i * 11 + 5) % 64).collect();
19795 let mut pos = 0usize;
19796 for &n in [5usize, 13, 7, 16].iter().cycle() {
19797 if pos >= ids.len() {
19798 break;
19799 }
19800 let end = (pos + n).min(ids.len());
19801 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
19802 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids[pos..end]), pos, None, 0, 4);
19803 assert_eq!(f32_bits(&ha), f32_bits(&hb), "prefill chunk at {pos}");
19804 pos = end;
19805 }
19806 for _ in 0..23 {
19807 let e = a.embed_single(((pos * 7) % 64) as u32);
19808 let ha = a.forward_layers(&e, pos, None);
19809 let hb = b.forward_layers(&e, pos, None);
19810 assert_eq!(f32_bits(&ha), f32_bits(&hb), "decode at {pos}");
19811 for li in 0..3 {
19812 assert!(b.kv_cache.layers[li].seq_len <= 12, "layer {li} bounded");
19813 }
19814 pos += 1;
19815 }
19816 for round in 0..9 {
19817 let (e1, e2) = (a.embed_single(round * 3 + 1), a.embed_single(round * 5 + 2));
19818 let (a1, a2) = a.forward_pair(&e1, &e2, pos);
19819 let (b1, b2) = b.forward_pair(&e1, &e2, pos);
19820 assert_eq!(f32_bits(&a1), f32_bits(&b1), "pair lane 1 round {round}");
19821 assert_eq!(f32_bits(&a2), f32_bits(&b2), "pair lane 2 round {round}");
19822 if round % 2 == 1 {
19823 for p in [&mut a, &mut b] {
19825 for l in &mut p.kv_cache.layers {
19826 l.truncate_last(2);
19827 }
19828 }
19829 } else {
19830 pos += 2;
19831 }
19832 }
19833 let e = a.embed_single(9);
19834 let (ha, hb) = (
19835 a.forward_layers(&e, pos, None),
19836 b.forward_layers(&e, pos, None),
19837 );
19838 assert_eq!(f32_bits(&ha), f32_bits(&hb), "after the pairs");
19839 for li in 0..3 {
19840 let (la, lb) = (&a.kv_cache.layers[li], &b.kv_cache.layers[li]);
19841 assert!(lb.base() > 0, "layer {li} trimmed");
19842 assert_eq!(la.base(), 0);
19843 assert_eq!(lb.pos_len(), la.seq_len);
19844 assert_eq!(lb.head_keys(0), &la.head_keys(0)[lb.base() * 8..]);
19845 }
19846 let (ga, gb) = (&a.kv_cache.layers[3], &b.kv_cache.layers[3]);
19847 assert_eq!(
19848 (gb.base(), gb.seq_len),
19849 (0, ga.seq_len),
19850 "the global layer keeps all"
19851 );
19852 assert_eq!(b.kv_cache.seq_len(), a.kv_cache.seq_len());
19853 assert!(b.kv_cache.total_memory_bytes() < a.kv_cache.total_memory_bytes());
19854 }
19855
19856 #[test]
19861 fn swa_trim_wire_handoff_continues_bitwise() {
19862 let mut a = spark_test_pipeline(None);
19863 let mut b = spark_test_pipeline(Some((2, 4)));
19864 let ids: Vec<u32> = (0..29u32).map(|i| (i * 13 + 7) % 64).collect();
19865 let ha = a.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
19866 let hb = b.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, 4);
19867 assert_eq!(f32_bits(&ha), f32_bits(&hb));
19868 let mut pos = ids.len();
19869 for _ in 0..5 {
19870 let e = a.embed_single(((pos * 5) % 64) as u32);
19871 assert_eq!(
19872 f32_bits(&a.forward_layers(&e, pos, None)),
19873 f32_bits(&b.forward_layers(&e, pos, None))
19874 );
19875 pos += 1;
19876 }
19877 let mut c = spark_test_pipeline(Some((2, 4)));
19878 for li in 0..4 {
19879 let bytes = b.kv_cache.layers[li].export_wire(false).unwrap();
19880 c.kv_cache.layers[li].import_wire(&bytes).unwrap();
19881 }
19882 assert!(c.kv_cache.layers[0].base() > 0, "a tail travelled");
19883 assert_eq!(c.kv_cache.seq_len(), pos);
19884 for step in 0..17 {
19885 let e = a.embed_single(((pos * 3 + 1) % 64) as u32);
19886 let ha = a.forward_layers(&e, pos, None);
19887 let hc = c.forward_layers(&e, pos, None);
19888 assert_eq!(
19889 f32_bits(&ha),
19890 f32_bits(&hc),
19891 "after the hand-off, step {step}"
19892 );
19893 pos += 1;
19894 }
19895 assert!(c.kv_cache.layers[0].seq_len <= 12, "the receiver trims on");
19896 }
19897
19898 fn mimo_test_mtp(n: usize, gain: f32) -> mimo_mtp::MimoMtp {
19901 let (hs, inter, nh, hd, vd, nkv) = (16usize, 24usize, 4usize, 8usize, 4usize, 2usize);
19902 let synth = |len: usize, salt: usize| -> Vec<f32> {
19903 (0..len)
19904 .map(|i| (((i * 37 + salt * 13 + 3) % 89) as f32 / 89.0 - 0.5) * 0.6 * gain)
19905 .collect()
19906 };
19907 let qt = |rows: usize, cols: usize, salt: usize| {
19908 QTensor::from_f32(synth(rows * cols, salt), rows, cols)
19909 };
19910 let layers = (0..n)
19911 .map(|k| {
19912 let s = 500 + k * 40;
19913 let mut kv = crate::kv_cache::LayerKvCache::new(nkv, hd);
19914 kv.sinks = Some(vec![0.3, -0.7, 1.1, 0.0]);
19915 MtpModule {
19916 enorm: vec![1.0; hs],
19917 hnorm: vec![1.0; hs],
19918 eh_proj: qt(hs, 2 * hs, s),
19919 layer: LayerWeights {
19920 input_norm: vec![1.0; hs],
19921 post_norm: vec![1.0; hs],
19922 attn_out_norm: None,
19923 ffn_out_norm: None,
19924 layer_scale: None,
19925 attn: AttnKind::Full {
19926 wq: qt(nh * hd, hs, s + 1),
19927 wk: qt(nkv * hd, hs, s + 2),
19928 wv: qt(nkv * vd, hs, s + 3),
19929 wo: qt(hs, nh * vd, s + 4),
19930 q_norm: None,
19931 k_norm: None,
19932 output_gate: false,
19933 softplus_gate: None,
19934 bias: None,
19935 },
19936 ffn: FfnKind::Dense(DenseFfn {
19937 gate_proj: qt(inter, hs, s + 5),
19938 up_proj: qt(inter, hs, s + 6),
19939 down_proj: qt(hs, inter, s + 7),
19940 act: Act::Silu,
19941 down_t: None,
19942 segs: Vec::new(),
19943 }),
19944 },
19945 final_norm: vec![1.0; hs],
19946 kv,
19947 }
19948 })
19949 .collect();
19950 mimo_mtp::MimoMtp::from_layers(layers)
19951 }
19952
19953 fn mimo_greedy(p: &mut Pipeline, ids: &[u32], n: usize, spec: bool) -> GenerateResult {
19954 p.clear_sequence_state();
19955 p.speculative = spec;
19956 p.ignore_eos = true;
19957 p.sampler_config.temperature = 0.0;
19958 p.generate_from_ids(ids, n, None, None).unwrap()
19959 }
19960
19961 #[test]
19967 fn mimo_mtp_incremental_rounds_equal_the_teacher_forced_table() {
19968 for post in [false, true] {
19971 let mut p = mimo_test_pipeline();
19972 p.weights.final_norm = (0..p.hidden_size).map(|i| 0.5 + 0.1 * i as f32).collect();
19974 let mut st0 = mimo_test_mtp(3, 1.0);
19975 st0.post_norm_hidden = post;
19976 p.mimo_mtp = Some(st0);
19977 let ids: Vec<u32> = (0..14u32).map(|i| (i * 11 + 5) % 64).collect();
19978 let hs = p.hidden_size;
19979 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
19980 p.mimo_note_rows(&hb, 0);
19981 let mut st = p.mimo_mtp.take().unwrap();
19982 let k = 3;
19985 let mut inc = Vec::new();
19986 for t in 0..ids.len() - k - 1 {
19987 inc.push(p.mimo_mtp_draft(&mut st, t, &ids, k));
19988 }
19989 let s = ids.len();
19992 let mut reference = vec![vec![0u32; k]; s - k - 1];
19993 let mut fresh = mimo_test_mtp(3, 1.0);
19994 for (layer, m) in fresh.layers.iter_mut().enumerate() {
19995 let n = s - layer - 1;
19996 let mut cats = vec![0.0f32; n * 2 * hs];
19997 for j in 0..n {
19998 let e = p.embed_single(ids[j + layer + 1]);
19999 let raw = &hb[j * hs..(j + 1) * hs];
20000 let g = if post {
20001 inference::rms_norm(raw, &p.weights.final_norm, p.rms_eps, p.norm_style)
20002 } else {
20003 raw.to_vec()
20004 };
20005 let (ce, ch) = cats[j * 2 * hs..(j + 1) * 2 * hs].split_at_mut(hs);
20006 inference::rms_norm_into(&e, &m.enorm, p.rms_eps, p.norm_style, ce);
20007 inference::rms_norm_into(&g, &m.hnorm, p.rms_eps, p.norm_style, ch);
20008 }
20009 let mut x = vec![0.0f32; n * hs];
20010 m.eh_proj.matmat(&cats, n, &mut x, None);
20011 p.mimo_mtp_block(m, &mut x, n, 0);
20012 for (t, row) in reference.iter_mut().enumerate() {
20013 let y = inference::rms_norm(
20014 &x[t * hs..(t + 1) * hs],
20015 &m.final_norm,
20016 p.rms_eps,
20017 p.norm_style,
20018 );
20019 row[layer] = sampler::argmax(&p.lm_head_forward(&y));
20020 }
20021 }
20022 assert_eq!(inc, reference, "post_norm_hidden = {post}");
20023 let distinct: std::collections::HashSet<u32> =
20025 inc.iter().flatten().copied().collect();
20026 assert!(distinct.len() > 3, "{inc:?}");
20027 let last_t = ids.len() - k - 2;
20029 for m in &st.layers {
20030 assert_eq!(m.kv.seq_len, last_t + 1);
20031 }
20032 }
20033 }
20034
20035 #[test]
20042 fn mimo_speculative_greedy_equals_plain_greedy() {
20043 unsafe { std::env::set_var("CMF_GPU_WGPU_GRAPH", "0") };
20044 let ids: Vec<u32> = (0..9u32).map(|i| (i * 7 + 3) % 64).collect();
20045 let n = 24;
20046 let mut p = mimo_test_pipeline();
20047 let plain = mimo_greedy(&mut p, &ids, n, false);
20048 assert_eq!(plain.mtp_drafted, 0);
20049 assert_eq!(plain.token_ids.len(), n);
20050 let plain_kv = p.kv_cache.layers[0].seq_len;
20051
20052 p.mimo_mtp = Some(mimo_test_mtp(3, 1.0));
20054 let spec = mimo_greedy(&mut p, &ids, n, true);
20055 assert!(spec.mtp_drafted > 0, "the round must draft");
20056 assert_eq!(spec.token_ids, plain.token_ids);
20057 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20058
20059 let mut truth: Vec<u32> = ids.clone();
20062 truth.extend(&plain.token_ids);
20063 let mut noisy = truth.clone();
20064 for (i, t) in noisy.iter_mut().enumerate() {
20065 if i % 5 == 0 {
20066 *t = (*t + 1) % 64;
20067 }
20068 }
20069 let mut st = mimo_test_mtp(3, 1.0);
20070 st.draft_override = Some(noisy);
20071 p.mimo_mtp = Some(st);
20072 let spec = mimo_greedy(&mut p, &ids, n, true);
20073 assert_eq!(spec.token_ids, plain.token_ids);
20074 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20075 let stats = p.mimo_mtp.as_ref().unwrap().stats.clone();
20076 assert_eq!(stats.accepted as usize, spec.mtp_accepted);
20077 assert!(spec.mtp_accepted > 0 && spec.mtp_accepted < spec.mtp_drafted);
20078 assert!(
20079 stats.accept_hist.iter().filter(|&&c| c > 0).count() >= 3,
20080 "{:?}",
20081 stats.accept_hist
20082 );
20083 assert!(stats.tokens_per_round() > 1.5, "{}", stats.line());
20084
20085 let mut st = mimo_test_mtp(3, 1.0);
20088 st.draft_override = Some(truth);
20089 p.mimo_mtp = Some(st);
20090 let spec = mimo_greedy(&mut p, &ids, n, true);
20091 assert_eq!(spec.token_ids, plain.token_ids);
20092 assert_eq!(spec.mtp_accepted, spec.mtp_drafted);
20093 assert_eq!(p.kv_cache.layers[0].seq_len, plain_kv);
20094
20095 let off = mimo_greedy(&mut p, &ids, n, false);
20097 assert_eq!(off.token_ids, plain.token_ids);
20098 assert_eq!(off.mtp_drafted, 0);
20099 }
20100
20101 #[test]
20110 fn mimo_shaped_model_rides_the_wgpu_graph_geometry() {
20111 let p = mimo_test_pipeline();
20112 assert_eq!(
20113 p.graph_attn_decline_reason(),
20114 Some("per-layer KV head counts")
20115 );
20116 assert_eq!(p.wgpu_graph_attn_decline(), None);
20117 let g0 = p.graph_attn_geom(0).expect("full layer geometry");
20118 assert_eq!(
20119 (g0.nkv, g0.dv, g0.rd, g0.window, g0.sink.is_some()),
20120 (1, 4, 4, None, false)
20121 );
20122 assert_eq!(g0.invf, p.inv_freq.as_slice());
20123 let g1 = p.graph_attn_geom(1).expect("sliding layer geometry");
20124 assert_eq!((g1.nkv, g1.dv, g1.rd, g1.window), (2, 4, 4, Some(3)));
20125 assert_eq!(g1.sink, Some(&[0.5f32, -1.0, 1.5, 0.0][..]));
20126 assert_eq!(g1.invf, p.inv_freq_local.as_ref().unwrap().as_slice());
20127 assert_ne!(g0.invf, g1.invf, "two RoPE tables");
20128 let g3 = p.graph_attn_geom(3).expect("full layer geometry");
20129 assert_eq!((g3.nkv, g3.window, g3.sink.is_some()), (1, None, false));
20130
20131 let emb = p.embed_single(3);
20134 let mut lg = Vec::new();
20135 assert!(
20136 p.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, p.num_layers)
20137 .is_none()
20138 );
20139 let mut hid = emb.clone();
20140 assert_eq!(
20141 p.try_batch_graph_wgpu(&mut hid, &[0], 1, None),
20142 crate::gpu::BatchGraphOutcome::Declined
20143 );
20144 assert_eq!(hid, emb, "a declined batch graph leaves the rows untouched");
20145 assert!(p.try_multi_burst(3, 0, 4).is_none());
20146 assert!(
20147 p.graph_declines().is_empty(),
20148 "no attention decline logged: {:?}",
20149 p.graph_declines()
20150 );
20151 let plain = || create_test_pipeline(8, 16, 2, 1, 4, 2, 32);
20156 assert_eq!(plain().graph_attn_decline_reason(), None);
20157 assert_eq!(plain().wgpu_graph_attn_decline(), None);
20158 assert!(
20159 plain().graph_attn_geom(0).is_none(),
20160 "uniform models keep the historical arms"
20161 );
20162 let mut q = plain();
20163 q.set_layer_sinks(1, vec![0.25, -0.25]).unwrap();
20164 assert_eq!(
20165 q.graph_attn_decline_reason(),
20166 Some("learned attention sinks")
20167 );
20168 assert_eq!(
20169 q.graph_attn_geom(1).unwrap().sink,
20170 Some(&[0.25f32, -0.25][..])
20171 );
20172 let mut q = plain();
20173 q.set_attn_geometry(None, Some(2)).unwrap();
20174 assert_eq!(
20175 q.graph_attn_decline_reason(),
20176 Some("V heads narrower than Q/K heads")
20177 );
20178 assert_eq!(q.graph_attn_geom(0).unwrap().dv, 2);
20179 let mut q = plain();
20180 q.sliding_layers = Some(vec![true, false]);
20181 q.swa = Some((4, usize::MAX));
20182 assert_eq!(q.graph_attn_decline_reason(), Some("sliding-window layers"));
20183 assert_eq!(q.graph_attn_geom(0).unwrap().window, Some(4));
20184 assert_eq!(q.graph_attn_geom(1).unwrap().window, None);
20185
20186 let mut q = mimo_test_pipeline();
20189 q.rope_scale = 2.0;
20190 assert_eq!(
20191 q.wgpu_graph_attn_decline(),
20192 Some("scaled RoPE positions with per-layer geometry")
20193 );
20194 let emb = q.embed_single(3);
20195 assert!(
20196 q.try_token_graph_wgpu_steps(&emb, 0, &mut lg, 1, None, None, 0, q.num_layers)
20197 .is_none()
20198 );
20199 let _ = q.try_token_graph_wgpu_steps(&emb, 1, &mut lg, 1, None, None, 0, q.num_layers);
20200 let lines = q.graph_declines();
20201 assert_eq!(
20202 lines
20203 .iter()
20204 .filter(|l| l.starts_with("wgpu token graph") && l.contains("scaled RoPE"))
20205 .count(),
20206 1,
20207 "{lines:?}"
20208 );
20209 }
20210
20211 #[test]
20212 fn mimo_verify_rewind_preserves_lagging_host_caches() {
20213 let mut p = mimo_test_pipeline();
20214 for (li, layer) in p.kv_cache.layers.iter_mut().enumerate() {
20215 let row = vec![0.0; layer.num_kv_heads * layer.head_dim];
20216 for _ in 0..if li == 0 { 2 } else { 12 } {
20217 layer.append(&row, &row, &[]);
20218 }
20219 }
20220 p.mimo_verify_rewind(9).unwrap();
20221 assert_eq!(p.kv_cache.layers[0].seq_len, 2);
20222 for layer in &p.kv_cache.layers[1..] {
20223 assert_eq!(layer.seq_len, 9);
20224 }
20225 }
20226
20227 #[test]
20231 fn layer_dump_covers_every_position_and_layer_on_both_walks() {
20232 let dir = std::env::temp_dir().join(format!("cmf-layer-dump-{}", std::process::id()));
20233 let _ = std::fs::remove_dir_all(&dir);
20234 let mut p = mimo_test_pipeline();
20235 let hs = p.hidden_size;
20236 let ids = [5u32, 9, 11, 2, 40];
20237 p.layer_dump = Some(dir.join("decode"));
20238 for (pos, &id) in ids.iter().enumerate() {
20239 let e = p.embed_single(id);
20240 let _ = p.forward_layers(&e, pos, None);
20241 }
20242 p.clear_sequence_state();
20243 p.layer_dump = Some(dir.join("prefill"));
20244 let hb = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20245 for pos in 0..ids.len() {
20246 for li in 0..p.num_layers {
20247 let name = format!("p{pos:06}_l{li:02}.f32");
20248 let a = std::fs::read(dir.join("decode").join(&name)).unwrap();
20249 let b = std::fs::read(dir.join("prefill").join(&name)).unwrap();
20250 assert_eq!(a.len(), hs * 4, "{name}");
20251 assert_eq!(a, b, "{name}");
20252 }
20253 }
20254 let last = std::fs::read(dir.join("prefill").join("p000004_l03.f32")).unwrap();
20255 let vals: Vec<f32> = last
20256 .chunks(4)
20257 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
20258 .collect();
20259 assert_eq!(f32_bits(&vals), f32_bits(&hb[4 * hs..5 * hs]));
20260 let _ = std::fs::remove_dir_all(&dir);
20261 }
20262
20263 #[test]
20264 fn attn_geometry_and_sinks_are_validated() {
20265 let mut p = create_test_pipeline(8, 16, 4, 2, 4, 2, 32);
20266 assert!(
20267 p.set_attn_geometry(Some(vec![2]), None).is_err(),
20268 "one entry per layer"
20269 );
20270 assert!(
20271 p.set_attn_geometry(Some(vec![2, 3]), None).is_err(),
20272 "3 does not divide 4"
20273 );
20274 assert!(p.set_attn_geometry(Some(vec![2, 0]), None).is_err());
20275 assert!(p.set_attn_geometry(None, Some(0)).is_err());
20276 assert!(
20277 p.set_attn_geometry(None, Some(5)).is_err(),
20278 "V wider than the head"
20279 );
20280 p.set_attn_geometry(None, Some(4)).unwrap();
20281 assert_eq!(
20282 p.v_head_dim, None,
20283 "v_head_dim == head_dim is the uniform case"
20284 );
20285 p.set_layer_sinks(1, vec![0.1; 4]).unwrap();
20286 p.set_attn_geometry(Some(vec![1, 4]), None).unwrap();
20287 assert_eq!(p.kv_cache.layers[0].num_kv_heads, 1);
20288 assert_eq!(p.kv_cache.layers[1].num_kv_heads, 4);
20289 assert!(
20290 p.kv_cache.layers[1].sinks.is_some(),
20291 "a reshape keeps the layer's sinks"
20292 );
20293 assert_eq!(p.layer_geom(1).0, 4);
20294 assert!(
20295 p.set_layer_sinks(0, vec![0.0; 3]).is_err(),
20296 "one sink per Q head"
20297 );
20298 assert!(p.set_layer_sinks(7, vec![0.0; 4]).is_err());
20299 assert!(p.set_layer_sinks(0, vec![f32::NAN, 0.0, 0.0, 0.0]).is_err());
20300 }
20301
20302 #[test]
20305 fn o1_is_never_armed_on_sink_window_or_narrow_v_layers() {
20306 let cfg = || {
20307 Some(crate::nystrom::O1Cfg {
20308 layers: crate::nystrom::O1Layers::All,
20309 m: 4,
20310 w: 8,
20311 sink: 2,
20312 rect: crate::nystrom::O1Rect::Aggregate,
20313 })
20314 };
20315 let mut p = mimo_test_pipeline();
20316 p.set_o1(cfg());
20317 assert!(!p.o1_active(), "every MiMo-shaped layer is ineligible");
20318 let mut q = create_test_pipeline(8, 16, 2, 1, 4, 3, 64);
20319 q.set_layer_sinks(1, vec![0.0, 0.0]).unwrap();
20320 q.sliding_layers = Some(vec![false, false, true]);
20321 q.swa = Some((4, usize::MAX));
20322 q.set_o1(cfg());
20323 assert_eq!(q.o1_flags, vec![true, false, false]);
20324 }
20325
20326 #[test]
20327 fn o1_batch_transition_publishes_one_epoch_before_serial_handoff() {
20328 const B: usize = 19;
20329 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20330 p.set_o1(Some(crate::nystrom::O1Cfg {
20331 layers: crate::nystrom::O1Layers::All,
20332 m: 4,
20333 w: 8,
20334 sink: 2,
20335 rect: crate::nystrom::O1Rect::Aggregate,
20336 }));
20337 p.o1_begin_with_prefix(Some(B));
20338 let ids: Vec<u32> = (0..B as u32).collect();
20339 let _ = p.prefill_batch_span(PrefillIn::Ids(&ids), 0, None, 0, p.num_layers);
20340
20341 assert_eq!(p.o1_epoch, 1, "all layers publish one completed transition");
20342 assert!(p.kv_cache.layers.iter().all(|l| l.o1_sealed()));
20343 let next = p.embed_single(B as u32);
20344 let _ = p.forward_layers(&next, B, None);
20345 assert_eq!(p.o1_epoch, 1, "sealed handoff must not republish the epoch");
20346 }
20347
20348 #[test]
20349 fn o1_pair_transition_commits_scratch_before_epoch_publication() {
20350 const B: usize = 19;
20351 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 2, 260);
20352 let gdn_cfg = crate::linear_core::GdnCfg {
20356 num_v_heads: 2,
20357 num_k_heads: 1,
20358 key_head_dim: 2,
20359 value_head_dim: 4,
20360 conv_kernel: 3,
20361 hidden_size: 8,
20362 rms_eps: 1e-6,
20363 output_gate_sigmoid: false,
20364 };
20365 let synth = |n: usize, salt: usize| -> Vec<f32> {
20366 (0..n)
20367 .map(|i| (((i * 13 + salt * 7) % 97) as f32 / 97.0 - 0.5) * 0.4)
20368 .collect()
20369 };
20370 let qt = |rows: usize, cols: usize, salt: usize| {
20371 crate::qtensor::QTensor::from_f32(synth(rows * cols, salt), rows, cols)
20372 };
20373 let c_dim = gdn_cfg.conv_dim();
20374 let vd = gdn_cfg.num_v_heads * gdn_cfg.value_head_dim;
20375 p.weights.layers[0].attn = AttnKind::LinearGdn(crate::linear_core::GdnWeights {
20376 in_proj_qkv: qt(c_dim, 8, 1),
20377 in_proj_z: qt(vd, 8, 2),
20378 in_proj_a: qt(gdn_cfg.num_v_heads, 8, 3),
20379 in_proj_b: qt(gdn_cfg.num_v_heads, 8, 4),
20380 conv1d: synth(c_dim * gdn_cfg.conv_kernel, 5),
20381 a_log: vec![0.2, 0.5],
20382 dt_bias: synth(gdn_cfg.num_v_heads, 6),
20383 norm: vec![1.0; gdn_cfg.value_head_dim],
20384 out_proj: qt(8, vd, 7),
20385 });
20386 p.gdn_cfg = Some(gdn_cfg);
20387 p.set_o1(Some(crate::nystrom::O1Cfg {
20388 layers: crate::nystrom::O1Layers::All,
20389 m: 4,
20390 w: 8,
20391 sink: 2,
20392 rect: crate::nystrom::O1Rect::Aggregate,
20393 }));
20394 p.o1_begin_with_prefix(Some(B));
20395 for pos in 0..B - 2 {
20396 let emb = p.embed_single(pos as u32);
20397 let _ = p.forward_layers(&emb, pos, None);
20398 }
20399 let lane1_state = p.kv_cache.layers[0].linear_state.clone();
20400
20401 let e1 = p.embed_single((B - 2) as u32);
20402 let e2 = p.embed_single((B - 1) as u32);
20403 let _ = p.forward_pair(&e1, &e2, B - 2);
20404
20405 assert_eq!(p.o1_epoch, 1, "pair crossing B publishes one epoch");
20406 assert!(
20407 p.kv_cache
20408 .layers
20409 .iter()
20410 .enumerate()
20411 .all(|(li, l)| !p.o1_flags[li] || l.o1_sealed())
20412 );
20413 assert!(!p.kv_cache.layers[0].linear_state.is_empty());
20414 assert_ne!(
20415 p.kv_cache.layers[0].linear_state, lane1_state,
20416 "real pair must commit GDN lane 2 before returning"
20417 );
20418 assert!(p.kv_cache.layers[0].linear_scratch.is_empty());
20419 let next = p.embed_single(B as u32);
20420 let _ = p.forward_layers(&next, B, None);
20421 assert_eq!(p.o1_epoch, 1, "serial continuation must reuse the epoch");
20422 }
20423
20424 #[test]
20425 fn o1_error_observation_stays_terminal_until_reset() {
20426 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20427 p.set_o1(Some(crate::nystrom::O1Cfg {
20428 layers: crate::nystrom::O1Layers::All,
20429 m: 4,
20430 w: 8,
20431 sink: 2,
20432 rect: crate::nystrom::O1Rect::Aggregate,
20433 }));
20434 p.o1_begin();
20435 p.kv_cache.layers[0].o1_abort("synthetic transition failure".into());
20436
20437 assert!(p.o1_seal_checked().is_err());
20438 assert!(
20439 p.o1_seal_checked().is_err(),
20440 "retry must see the sticky error"
20441 );
20442 let k = vec![0.2f32; 4];
20443 let v = vec![0.3f32; 4];
20444 p.kv_cache.layers[0].append(&k, &v, &[]);
20445 assert_eq!(p.kv_cache.layers[0].seq_len, 0);
20446
20447 p.reset_session();
20448 p.o1_begin();
20449 p.kv_cache.layers[0].append(&k, &v, &[]);
20450 assert_eq!(p.kv_cache.layers[0].seq_len, 1);
20451 }
20452
20453 #[test]
20454 fn nll_graph_failure_is_terminal_and_request_is_reusable() {
20455 let ids = vec![1u32, 2, 3, 4, 5, 6];
20456 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20457 p.graph_logits = Some(vec![123.0]);
20458 p.graph_want_logits = true;
20459 p.graph_failed
20460 .store(true, std::sync::atomic::Ordering::Relaxed);
20461 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20462 let err = p.nll_ids_from(&ids, 0).expect_err("prior graph failure");
20463 assert!(err.contains("before NLL"));
20464 assert!(p.graph_logits.is_none());
20465 assert!(!p.graph_want_logits);
20466 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
20467 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
20468
20469 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20470 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
20471 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
20472 assert_eq!(actual.1, expected.1);
20473 assert!((actual.0 - expected.0).abs() < 1e-9);
20474 }
20475
20476 #[test]
20477 fn nll_forward_failure_discards_partial_score_and_clears_sidechannels() {
20478 let ids = vec![1u32, 2, 3, 4, 5, 6];
20479 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20480 p.nll_test_fail_at = Some(1);
20481 let err = p
20482 .nll_ids_from(&ids, 0)
20483 .expect_err("one-shot forward failure");
20484 assert!(err.contains("forward") || err.contains("score row"));
20485 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
20486 assert!(!p.graph_want_logits);
20487 assert!(p.graph_logits.is_none());
20488 assert!(p.kv_history.is_empty());
20489
20490 let mut fresh = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20491 let expected = fresh.nll_ids_from(&ids, 0).expect("fresh NLL");
20492 let actual = p.nll_ids_from(&ids, 0).expect("reused NLL");
20493 assert_eq!(actual.1, expected.1);
20494 assert!((actual.0 - expected.0).abs() < 1e-9);
20495 }
20496
20497 #[test]
20498 fn nll_serial_failure_before_first_row_is_reported() {
20499 let ids = vec![1u32, 2, 3, 4];
20500 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20501 p.nll_test_force_serial = true;
20502 p.nll_test_fail_at = Some(0);
20503 let err = p.nll_ids_from(&ids, 0).expect_err("serial forward failure");
20504 assert!(err.contains("serial forward"));
20505 assert!(p.kv_history.is_empty());
20506 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
20507 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
20508 }
20509
20510 #[test]
20511 fn ffn_probe_failure_discards_recorder_and_state() {
20512 let ids = vec![1u32, 2, 3, 4];
20513 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20514 p.nll_test_fail_at = Some(0);
20515 let err = p
20516 .probe_ffn_mass_batch(&ids)
20517 .expect_err("probe forward failure");
20518 assert!(err.contains("NLL"));
20519 assert!(FFN_PROBE.with(|probe| probe.borrow().is_none()));
20520 assert!(p.kv_history.is_empty());
20521 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
20522 }
20523
20524 #[test]
20525 fn nll_test_controls_are_pipeline_scoped() {
20526 let ids = vec![1u32, 2, 3, 4];
20527 let mut failing = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20528 let mut unaffected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20529 failing.nll_test_force_serial = true;
20530 failing.nll_test_fail_at = Some(0);
20531
20532 assert!(!failing.can_prefill_batched());
20533 assert!(unaffected.can_prefill_batched());
20534 let expected = unaffected
20535 .nll_ids_from(&ids, 0)
20536 .expect("unaffected pipeline remains usable");
20537 let err = failing
20538 .nll_ids_from(&ids, 0)
20539 .expect_err("failure injection belongs to failing pipeline");
20540 assert!(err.contains("serial forward"));
20541 assert!(failing.nll_test_fail_at.is_none());
20542 assert!(unaffected.can_prefill_batched());
20543 let actual = unaffected
20544 .nll_ids_from(&ids, 0)
20545 .expect("unaffected pipeline remains reusable");
20546 assert_eq!(actual.1, expected.1);
20547 assert!((actual.0 - expected.0).abs() < 1e-9);
20548 }
20549
20550 #[test]
20551 fn forward_ids_failure_channel_is_terminal_and_reusable() {
20552 let ids = vec![1u32, 2, 3, 4, 5, 6];
20553 let mut p = create_test_pipeline(8, 16, 2, 1, 4, 1, 64);
20554 p.graph_logits = Some(vec![123.0]);
20555 p.graph_want_logits = true;
20556 p.graph_failed
20557 .store(true, std::sync::atomic::Ordering::Relaxed);
20558 p.cancel.store(true, std::sync::atomic::Ordering::Relaxed);
20559
20560 let err = p
20561 .forward_ids(&ids, None)
20562 .expect_err("a failed forward must not become a valid head result");
20563 assert!(err.contains("forward_ids setup"));
20564 assert!(p.graph_logits.is_none());
20565 assert!(!p.graph_want_logits);
20566 assert!(!p.graph_failed.load(std::sync::atomic::Ordering::Relaxed));
20567 assert!(!p.cancel.load(std::sync::atomic::Ordering::Relaxed));
20568 assert_eq!(p.kv_cache.seq_len(), 0);
20569
20570 let expected = create_test_pipeline(8, 16, 2, 1, 4, 1, 64)
20571 .forward_ids(&ids, None)
20572 .expect("fresh forward_ids");
20573 let actual = p
20574 .forward_ids(&ids, None)
20575 .expect("pipeline remains reusable after a failed forward");
20576 assert_eq!(actual.len(), expected.len());
20577 assert!(
20578 actual
20579 .iter()
20580 .zip(expected)
20581 .all(|(a, b)| (a - b).abs() < 1e-9)
20582 );
20583 assert_eq!(p.kv_cache.seq_len(), ids.len());
20584 }
20585
20586 #[test]
20587 fn sigmoid_router_floor_is_explicit_per_architecture() {
20588 let zero = || DenseFfn {
20594 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20595 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20596 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20597 act: Act::Silu,
20598 down_t: None,
20599 segs: Vec::new(),
20600 };
20601 let m = MoeFfn {
20602 router: QTensor::from_f32(vec![0.0; 4], 2, 2),
20603 experts: vec![zero(), zero()],
20604 top_k: 1,
20605 norm_topk_prob: true,
20606 router_sigmoid: true,
20607 expert_bias: None,
20608 routed_scaling: 2.5,
20609 route_tau: None,
20610 shared: None,
20611 stats: std::cell::RefCell::new(Vec::new()),
20612 act_sq: std::cell::RefCell::new(Vec::new()),
20613 act_rows: std::cell::RefCell::new(Vec::new()),
20614 mask: None,
20615 per_expert_scale: None,
20616 router_input_norm: false,
20617 resonance: None,
20618 grown: Vec::new(),
20619 };
20620 let logits = [-20.0f32, -20.0];
20621 let (_, p, glm_wsum) = moe_route_with_eps(&logits, &m, None, 1e-20);
20622 let (_, _, generic_wsum) = moe_route(&logits, &m, None);
20623 let expected = (p[0] + 1e-20) / m.routed_scaling;
20624 assert!((glm_wsum - expected).abs() < 1e-15);
20625 assert!(generic_wsum > glm_wsum * 100.0);
20626 }
20627
20628 #[test]
20629 fn resonance_scores_match_formula_and_stable_tie() {
20630 let r = Resonance {
20631 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0],
20633 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0],
20634 k: 1,
20635 bias: vec![1.5, 0.5, 0.0],
20636 shell: Vec::new(),
20637 };
20638 let x = [1.0f32, 1.0];
20639 let mut got = vec![0.0; 3];
20640 r.scores(&x, &mut got);
20641 assert!((got[0] - 0.5).abs() < 1e-6);
20645 assert!((got[1] - 0.5).abs() < 1e-6);
20646 assert!(got[2].abs() < 1e-6);
20647 let best = got
20648 .iter()
20649 .enumerate()
20650 .max_by(|(ia, a), (ib, b)| a.partial_cmp(b).unwrap().then(ib.cmp(ia)))
20651 .map(|(i, _)| i);
20652 assert_eq!(best, Some(0));
20653 assert!(got.iter().all(|v| v.is_finite()));
20654 }
20655
20656 #[test]
20661 fn resonance_shell_masks_outside_keeps_inside_and_trunk_bits() {
20662 let plain = Resonance {
20668 mu: vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0],
20669 u: vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0],
20670 k: 1,
20671 bias: vec![1.5, 0.5, 0.0, 0.0],
20672 shell: Vec::new(),
20673 };
20674 let shelled = Resonance {
20675 mu: plain.mu.clone(),
20676 u: plain.u.clone(),
20677 k: 1,
20678 bias: plain.bias.clone(),
20679 shell: vec![f32::INFINITY, f32::INFINITY, 6.0, 0.25],
20680 };
20681 assert!(!plain.has_shell());
20682 assert!(shelled.has_shell());
20683 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
20684 let (mut a, mut b) = (vec![0.0; 4], vec![0.0; 4]);
20685 set_growth_shell(Some(true));
20686 assert!(growth_shell_enabled());
20687 let x = [1.0f32, 1.0];
20690 plain.scores(&x, &mut a);
20691 shelled.scores(&x, &mut b);
20692 assert_eq!(bits(&a), bits(&b), "inside every shell: unchanged");
20693 assert!(a[2] == 0.0 && a[3] == 0.0);
20694 let xo = [3.0f32, 0.0];
20697 plain.scores(&xo, &mut a);
20698 shelled.scores(&xo, &mut b);
20699 assert_eq!(a[2], -6.0);
20700 assert_eq!(a[3], -6.0);
20701 assert_eq!(bits(&a[..3]), bits(&b[..3]), "trunk rows + the boundary row");
20702 assert_eq!(b[3], f32::NEG_INFINITY, "outside the shell: −∞");
20703 assert_eq!(shelled.effective_shell(4), shelled.shell);
20704 set_growth_shell(Some(false));
20707 assert!(!growth_shell_enabled());
20708 assert_eq!(shelled.effective_shell(4), vec![f32::INFINITY; 4]);
20709 shelled.scores(&xo, &mut b);
20710 assert_eq!(bits(&a), bits(&b));
20711 set_growth_shell(None);
20712 let short = Resonance {
20715 shell: vec![f32::INFINITY, f32::INFINITY],
20716 ..shelled
20717 };
20718 set_growth_shell(Some(true));
20719 short.scores(&xo, &mut b);
20720 assert_eq!(bits(&a), bits(&b));
20721 set_growth_shell(None);
20722 }
20723
20724 #[test]
20728 fn moe_route_neg_inf_logits_pick_the_best_finite_expert_with_weight_one() {
20729 let zero = || DenseFfn {
20730 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20731 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20732 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20733 act: Act::Silu,
20734 down_t: None,
20735 segs: Vec::new(),
20736 };
20737 let moe = |sigmoid: bool| MoeFfn {
20738 router: QTensor::from_f32(vec![0.0; 8], 4, 2),
20739 experts: vec![zero(), zero(), zero(), zero()],
20740 top_k: 1,
20741 norm_topk_prob: true,
20742 router_sigmoid: sigmoid,
20743 expert_bias: None,
20744 routed_scaling: 1.0,
20745 route_tau: None,
20746 shared: None,
20747 stats: std::cell::RefCell::new(Vec::new()),
20748 act_sq: std::cell::RefCell::new(Vec::new()),
20749 act_rows: std::cell::RefCell::new(Vec::new()),
20750 mask: None,
20751 per_expert_scale: None,
20752 router_input_norm: false,
20753 resonance: None,
20754 grown: Vec::new(),
20755 };
20756 let logits = [-1.0f32, f32::NEG_INFINITY, -0.5, f32::NEG_INFINITY];
20757 for sigmoid in [false, true] {
20758 let m = moe(sigmoid);
20759 let (idx, p, wsum) = moe_route(&logits, &m, None);
20760 assert_eq!(idx, vec![2], "sigmoid {sigmoid}");
20761 assert_eq!(p[1], 0.0);
20762 assert_eq!(p[3], 0.0);
20763 assert!(p[2] > p[0] && p[0] > 0.0);
20764 assert!(p.iter().all(|v| v.is_finite()));
20765 let w = p[2] / wsum;
20766 if sigmoid {
20767 assert!((w - 1.0).abs() < 1e-5, "sigmoid: weight {w}");
20769 } else {
20770 assert_eq!(w, 1.0, "softmax: the renormalized top-1 weight is exactly 1");
20771 }
20772 let (idx, _, _) = moe_route(&logits, &m, Some(&[true, true, true, true]));
20775 assert_eq!(idx, vec![2]);
20776 }
20777 let m = moe(false);
20779 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY, -9.0], &m, None);
20780 assert_eq!(idx, vec![3]);
20781 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 4], &m, None);
20784 assert_eq!(idx, vec![0]);
20785 assert!(p.iter().all(|&v| v == 0.25));
20786 assert!(wsum.is_finite() && wsum > 0.0);
20787 }
20788
20789 #[test]
20794 fn resonance_route_picks_the_raw_argmax_not_the_softmax_tie() {
20795 let zero = || DenseFfn {
20796 gate_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20797 up_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20798 down_proj: QTensor::from_f32(vec![0.0; 4], 2, 2),
20799 act: Act::Silu,
20800 down_t: None,
20801 segs: Vec::new(),
20802 };
20803 let moe = |resonant: bool, norm_topk: bool| MoeFfn {
20804 router: QTensor::from_f32(vec![0.0; 6], 3, 2),
20805 experts: vec![zero(), zero(), zero()],
20806 top_k: 1,
20807 norm_topk_prob: norm_topk,
20808 router_sigmoid: false,
20809 expert_bias: None,
20810 routed_scaling: 1.0,
20811 route_tau: None,
20812 shared: None,
20813 stats: std::cell::RefCell::new(Vec::new()),
20814 act_sq: std::cell::RefCell::new(Vec::new()),
20815 act_rows: std::cell::RefCell::new(Vec::new()),
20816 mask: None,
20817 per_expert_scale: None,
20818 router_input_norm: false,
20819 resonance: resonant.then(|| Resonance {
20820 mu: vec![0.0; 6],
20821 u: Vec::new(),
20822 k: 0,
20823 bias: vec![0.0; 3],
20824 shell: Vec::new(),
20825 }),
20826 grown: Vec::new(),
20827 };
20828 let lo = -0.1f32;
20831 let hi = f32::from_bits(lo.to_bits() - 1);
20832 assert!(hi > lo && hi - lo < 2f32.powi(-25));
20833 assert_eq!((lo - hi).exp(), 1.0, "the tie this test is about");
20834 let (idx, _, _) = moe_route(&[lo, hi], &moe(false, true), None);
20836 assert_eq!(idx, vec![0], "softmax tie → lower index (the gated contract)");
20837 for norm in [true, false] {
20840 let m = moe(true, norm);
20841 let (idx, p, wsum) = moe_route(&[lo, hi, f32::NEG_INFINITY], &m, None);
20842 assert_eq!(idx, vec![1], "norm_topk {norm}");
20843 assert_eq!(p, vec![0.0, 1.0, 0.0]);
20844 assert_eq!(p[1] / wsum, 1.0);
20845 let (idx, _, _) = moe_route(&[hi, hi, lo], &m, None);
20848 assert_eq!(idx, vec![0]);
20849 let (idx, _, _) = moe_route(&[f32::NEG_INFINITY, lo, hi], &m, None);
20851 assert_eq!(idx, vec![2]);
20852 let (idx, p, wsum) = moe_route(&[lo, hi, hi], &m, Some(&[true, false, false]));
20853 assert_eq!(idx, vec![0]);
20854 assert_eq!(p[0] / wsum, 1.0);
20855 let (idx, p, wsum) = moe_route(&[f32::NEG_INFINITY; 3], &m, None);
20858 assert_eq!(idx, vec![0]);
20859 assert!(p.iter().all(|v| v.is_finite()) && wsum.is_finite() && wsum > 0.0);
20860 }
20861 }
20862}